More protobufjs migration

This commit is contained in:
Fedor Indutny 2021-07-09 12:36:10 -07:00 committed by GitHub
parent cf06e6638e
commit ddbbe3a6b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 3967 additions and 3369 deletions

View file

@ -52,7 +52,6 @@ module.exports = grunt => {
libtextsecuretest: { libtextsecuretest: {
src: [ src: [
'node_modules/jquery/dist/jquery.js', 'node_modules/jquery/dist/jquery.js',
'components/mock-socket/dist/mock-socket.js',
'node_modules/mocha/mocha.js', 'node_modules/mocha/mocha.js',
'node_modules/chai/chai.js', 'node_modules/chai/chai.js',
'libtextsecure/test/_test.js', 'libtextsecure/test/_test.js',

View file

@ -11,7 +11,6 @@
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git" "webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git"
}, },
"devDependencies": { "devDependencies": {
"mock-socket": "~0.3.2"
}, },
"preen": { "preen": {
"bytebuffer": [ "bytebuffer": [
@ -20,9 +19,6 @@
"long": [ "long": [
"dist/Long.js" "dist/Long.js"
], ],
"mock-socket": [
"dist/mock-socket.js"
],
"mp3lameencoder": [ "mp3lameencoder": [
"lib/Mp3LameEncoder.js" "lib/Mp3LameEncoder.js"
], ],

View file

@ -1,635 +0,0 @@
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// Starting point for browserify and throws important objects into the window object
var Service = require('./service');
var MockServer = require('./mock-server');
var MockSocket = require('./mock-socket');
var globalContext = require('./helpers/global-context');
globalContext.SocketService = Service;
globalContext.MockSocket = MockSocket;
globalContext.MockServer = MockServer;
},{"./helpers/global-context":3,"./mock-server":7,"./mock-socket":8,"./service":9}],2:[function(require,module,exports){
var globalContext = require('./global-context');
/*
* This delay allows the thread to finish assigning its on* methods
* before invoking the delay callback. This is purely a timing hack.
* http://geekabyte.blogspot.com/2014/01/javascript-effect-of-setting-settimeout.html
*
* @param {callback: function} the callback which will be invoked after the timeout
* @parma {context: object} the context in which to invoke the function
*/
function delay(callback, context) {
globalContext.setTimeout(function(context) {
callback.call(context);
}, 4, context);
}
module.exports = delay;
},{"./global-context":3}],3:[function(require,module,exports){
(function (global){
/*
* Determines the global context. This should be either window (in the)
* case where we are in a browser) or global (in the case where we are in
* node)
*/
var globalContext;
if(typeof window === 'undefined') {
globalContext = global;
}
else {
globalContext = window;
}
if (!globalContext) {
throw new Error('Unable to set the global context to either window or global.');
}
module.exports = globalContext;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],4:[function(require,module,exports){
/*
* This is a mock websocket event message that is passed into the onopen,
* opmessage, etc functions.
*
* @param {name: string} The name of the event
* @param {data: *} The data to send.
* @param {origin: string} The url of the place where the event is originating.
*/
function socketEventMessage(name, data, origin) {
var ports = null;
var source = null;
var bubbles = false;
var cancelable = false;
var lastEventId = '';
var targetPlacehold = null;
try {
var messageEvent = new MessageEvent(name);
messageEvent.initMessageEvent(name, bubbles, cancelable, data, origin, lastEventId);
Object.defineProperties(messageEvent, {
target: {
get: function() { return targetPlacehold; },
set: function(value) { targetPlacehold = value; }
},
srcElement: {
get: function() { return this.target; }
},
currentTarget: {
get: function() { return this.target; }
}
});
}
catch (e) {
// We are unable to create a MessageEvent Object. This should only be happening in PhantomJS.
var messageEvent = {
type : name,
bubbles : bubbles,
cancelable : cancelable,
data : data,
origin : origin,
lastEventId : lastEventId,
source : source,
ports : ports,
defaultPrevented : false,
returnValue : true,
clipboardData : undefined
};
Object.defineProperties(messageEvent, {
target: {
get: function() { return targetPlacehold; },
set: function(value) { targetPlacehold = value; }
},
srcElement: {
get: function() { return this.target; }
},
currentTarget: {
get: function() { return this.target; }
}
});
}
return messageEvent;
}
module.exports = socketEventMessage;
},{}],5:[function(require,module,exports){
/*
* The native websocket object will transform urls without a pathname to have just a /.
* As an example: ws://localhost:8080 would actually be ws://localhost:8080/ but ws://example.com/foo would not
* change. This function does this transformation to stay inline with the native websocket implementation.
*
* @param {url: string} The url to transform.
*/
function urlTransform(url) {
var urlPath = urlParse('path', url);
var urlQuery = urlParse('?', url);
urlQuery = (urlQuery) ? '?' + urlQuery : '';
if(urlPath === '') {
return url.split('?')[0] + '/' + urlQuery;
}
return url;
}
/*
* The following functions (isNumeric & urlParse) was taken from
* https://github.com/websanova/js-url/blob/764ed8d94012a79bfa91026f2a62fe3383a5a49e/url.js
* which is shared via the MIT license with minimal modifications.
*/
function isNumeric(arg) {
return !isNaN(parseFloat(arg)) && isFinite(arg);
}
function urlParse(arg, url) {
var _ls = url || window.location.toString();
if (!arg) { return _ls; }
else { arg = arg.toString(); }
if (_ls.substring(0,2) === '//') { _ls = 'http:' + _ls; }
else if (_ls.split('://').length === 1) { _ls = 'http://' + _ls; }
url = _ls.split('/');
var _l = {auth:''}, host = url[2].split('@');
if (host.length === 1) { host = host[0].split(':'); }
else { _l.auth = host[0]; host = host[1].split(':'); }
_l.protocol=url[0];
_l.hostname=host[0];
_l.port=(host[1] || ((_l.protocol.split(':')[0].toLowerCase() === 'https') ? '443' : '80'));
_l.pathname=( (url.length > 3 ? '/' : '') + url.slice(3, url.length).join('/').split('?')[0].split('#')[0]);
var _p = _l.pathname;
if (_p.charAt(_p.length-1) === '/') { _p=_p.substring(0, _p.length-1); }
var _h = _l.hostname, _hs = _h.split('.'), _ps = _p.split('/');
if (arg === 'hostname') { return _h; }
else if (arg === 'domain') {
if (/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(_h)) { return _h; }
return _hs.slice(-2).join('.');
}
//else if (arg === 'tld') { return _hs.slice(-1).join('.'); }
else if (arg === 'sub') { return _hs.slice(0, _hs.length - 2).join('.'); }
else if (arg === 'port') { return _l.port; }
else if (arg === 'protocol') { return _l.protocol.split(':')[0]; }
else if (arg === 'auth') { return _l.auth; }
else if (arg === 'user') { return _l.auth.split(':')[0]; }
else if (arg === 'pass') { return _l.auth.split(':')[1] || ''; }
else if (arg === 'path') { return _l.pathname; }
else if (arg.charAt(0) === '.') {
arg = arg.substring(1);
if(isNumeric(arg)) {arg = parseInt(arg, 10); return _hs[arg < 0 ? _hs.length + arg : arg-1] || ''; }
}
else if (isNumeric(arg)) { arg = parseInt(arg, 10); return _ps[arg < 0 ? _ps.length + arg : arg] || ''; }
else if (arg === 'file') { return _ps.slice(-1)[0]; }
else if (arg === 'filename') { return _ps.slice(-1)[0].split('.')[0]; }
else if (arg === 'fileext') { return _ps.slice(-1)[0].split('.')[1] || ''; }
else if (arg.charAt(0) === '?' || arg.charAt(0) === '#') {
var params = _ls, param = null;
if(arg.charAt(0) === '?') { params = (params.split('?')[1] || '').split('#')[0]; }
else if(arg.charAt(0) === '#') { params = (params.split('#')[1] || ''); }
if(!arg.charAt(1)) { return params; }
arg = arg.substring(1);
params = params.split('&');
for(var i=0,ii=params.length; i<ii; i++) {
param = params[i].split('=');
if(param[0] === arg) { return param[1] || ''; }
}
return null;
}
return '';
}
module.exports = urlTransform;
},{}],6:[function(require,module,exports){
/*
* This defines four methods: onopen, onmessage, onerror, and onclose. This is done this way instead of
* just placing the methods on the prototype because we need to capture the callback when it is defined like so:
*
* mockSocket.onopen = function() { // this is what we need to store };
*
* The only way is to capture the callback via the custom setter below and then place them into the correct
* namespace so they get invoked at the right time.
*
* @param {websocket: object} The websocket object which we want to define these properties onto
*/
function webSocketProperties(websocket) {
var eventMessageSource = function(callback) {
return function(event) {
event.target = websocket;
callback.apply(websocket, arguments);
}
};
Object.defineProperties(websocket, {
onopen: {
enumerable: true,
get: function() { return this._onopen; },
set: function(callback) {
this._onopen = eventMessageSource(callback);
this.service.setCallbackObserver('clientOnOpen', this._onopen, websocket);
}
},
onmessage: {
enumerable: true,
get: function() { return this._onmessage; },
set: function(callback) {
this._onmessage = eventMessageSource(callback);
this.service.setCallbackObserver('clientOnMessage', this._onmessage, websocket);
}
},
onclose: {
enumerable: true,
get: function() { return this._onclose; },
set: function(callback) {
this._onclose = eventMessageSource(callback);
this.service.setCallbackObserver('clientOnclose', this._onclose, websocket);
}
},
onerror: {
enumerable: true,
get: function() { return this._onerror; },
set: function(callback) {
this._onerror = eventMessageSource(callback);
this.service.setCallbackObserver('clientOnError', this._onerror, websocket);
}
}
});
};
module.exports = webSocketProperties;
},{}],7:[function(require,module,exports){
var Service = require('./service');
var delay = require('./helpers/delay');
var urlTransform = require('./helpers/url-transform');
var socketMessageEvent = require('./helpers/message-event');
var globalContext = require('./helpers/global-context');
function MockServer(url) {
var service = new Service();
this.url = urlTransform(url);
globalContext.MockSocket.services[this.url] = service;
this.service = service;
service.server = this;
}
MockServer.prototype = {
service: null,
/*
* This is the main function for the mock server to subscribe to the on events.
*
* ie: mockServer.on('connection', function() { console.log('a mock client connected'); });
*
* @param {type: string}: The event key to subscribe to. Valid keys are: connection, message, and close.
* @param {callback: function}: The callback which should be called when a certain event is fired.
*/
on: function(type, callback) {
var observerKey;
if(typeof callback !== 'function' || typeof type !== 'string') {
return false;
}
switch(type) {
case 'connection':
observerKey = 'clientHasJoined';
break;
case 'message':
observerKey = 'clientHasSentMessage';
break;
case 'close':
observerKey = 'clientHasLeft';
break;
}
// Make sure that the observerKey is valid before observing on it.
if(typeof observerKey === 'string') {
this.service.clearAll(observerKey);
this.service.setCallbackObserver(observerKey, callback, this);
}
},
/*
* This send function will notify all mock clients via their onmessage callbacks that the server
* has a message for them.
*
* @param {data: *}: Any javascript object which will be crafted into a MessageObject.
*/
send: function(data) {
delay(function() {
this.service.sendMessageToClients(socketMessageEvent('message', data, this.url));
}, this);
},
/*
* Notifies all mock clients that the server is closing and their onclose callbacks should fire.
*/
close: function() {
delay(function() {
this.service.closeConnectionFromServer(socketMessageEvent('close', null, this.url));
}, this);
}
};
module.exports = MockServer;
},{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./service":9}],8:[function(require,module,exports){
var delay = require('./helpers/delay');
var urlTransform = require('./helpers/url-transform');
var socketMessageEvent = require('./helpers/message-event');
var globalContext = require('./helpers/global-context');
var webSocketProperties = require('./helpers/websocket-properties');
function MockSocket(url) {
this.binaryType = 'blob';
this.url = urlTransform(url);
this.readyState = globalContext.MockSocket.CONNECTING;
this.service = globalContext.MockSocket.services[this.url];
webSocketProperties(this);
delay(function() {
// Let the service know that we are both ready to change our ready state and that
// this client is connecting to the mock server.
this.service.clientIsConnecting(this, this._updateReadyState);
}, this);
}
MockSocket.CONNECTING = 0;
MockSocket.OPEN = 1;
MockSocket.CLOSING = 2;
MockSocket.LOADING = 3;
MockSocket.CLOSED = 4;
MockSocket.services = {};
MockSocket.prototype = {
/*
* Holds the on*** callback functions. These are really just for the custom
* getters that are defined in the helpers/websocket-properties. Accessing these properties is not advised.
*/
_onopen : null,
_onmessage : null,
_onerror : null,
_onclose : null,
/*
* This holds reference to the service object. The service object is how we can
* communicate with the backend via the pub/sub model.
*
* The service has properties which we can use to observe or notifiy with.
* this.service.notify('foo') & this.service.observe('foo', callback, context)
*/
service: null,
/*
* This is a mock for the native send function found on the WebSocket object. It notifies the
* service that it has sent a message.
*
* @param {data: *}: Any javascript object which will be crafted into a MessageObject.
*/
send: function(data) {
delay(function() {
this.service.sendMessageToServer(socketMessageEvent('message', data, this.url));
}, this);
},
/*
* This is a mock for the native close function found on the WebSocket object. It notifies the
* service that it is closing the connection.
*/
close: function() {
delay(function() {
this.service.closeConnectionFromClient(socketMessageEvent('close', null, this.url), this);
}, this);
},
/*
* This is a private method that can be used to change the readyState. This is used
* like this: this.protocol.subject.observe('updateReadyState', this._updateReadyState, this);
* so that the service and the server can change the readyState simply be notifing a namespace.
*
* @param {newReadyState: number}: The new ready state. Must be 0-4
*/
_updateReadyState: function(newReadyState) {
if(newReadyState >= 0 && newReadyState <= 4) {
this.readyState = newReadyState;
}
}
};
module.exports = MockSocket;
},{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./helpers/websocket-properties":6}],9:[function(require,module,exports){
var socketMessageEvent = require('./helpers/message-event');
var globalContext = require('./helpers/global-context');
function SocketService() {
this.list = {};
}
SocketService.prototype = {
server: null,
/*
* This notifies the mock server that a client is connecting and also sets up
* the ready state observer.
*
* @param {client: object} the context of the client
* @param {readyStateFunction: function} the function that will be invoked on a ready state change
*/
clientIsConnecting: function(client, readyStateFunction) {
this.observe('updateReadyState', readyStateFunction, client);
// if the server has not been set then we notify the onclose method of this client
if(!this.server) {
this.notify(client, 'updateReadyState', globalContext.MockSocket.CLOSED);
this.notifyOnlyFor(client, 'clientOnError');
return false;
}
this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.OPEN);
this.notify('clientHasJoined', this.server);
this.notifyOnlyFor(client, 'clientOnOpen', socketMessageEvent('open', null, this.server.url));
},
/*
* Closes a connection from the server's perspective. This should
* close all clients.
*
* @param {messageEvent: object} the mock message event.
*/
closeConnectionFromServer: function(messageEvent) {
this.notify('updateReadyState', globalContext.MockSocket.CLOSING);
this.notify('clientOnclose', messageEvent);
this.notify('updateReadyState', globalContext.MockSocket.CLOSED);
this.notify('clientHasLeft');
},
/*
* Closes a connection from the clients perspective. This
* should only close the client who initiated the close and not
* all of the other clients.
*
* @param {messageEvent: object} the mock message event.
* @param {client: object} the context of the client
*/
closeConnectionFromClient: function(messageEvent, client) {
if(client.readyState === globalContext.MockSocket.OPEN) {
this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSING);
this.notifyOnlyFor(client, 'clientOnclose', messageEvent);
this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSED);
this.notify('clientHasLeft');
}
},
/*
* Notifies the mock server that a client has sent a message.
*
* @param {messageEvent: object} the mock message event.
*/
sendMessageToServer: function(messageEvent) {
this.notify('clientHasSentMessage', messageEvent.data, messageEvent);
},
/*
* Notifies all clients that the server has sent a message
*
* @param {messageEvent: object} the mock message event.
*/
sendMessageToClients: function(messageEvent) {
this.notify('clientOnMessage', messageEvent);
},
/*
* Setup the callback function observers for both the server and client.
*
* @param {observerKey: string} either: connection, message or close
* @param {callback: function} the callback to be invoked
* @param {server: object} the context of the server
*/
setCallbackObserver: function(observerKey, callback, server) {
this.observe(observerKey, callback, server);
},
/*
* Binds a callback to a namespace. If notify is called on a namespace all "observers" will be
* fired with the context that is passed in.
*
* @param {namespace: string}
* @param {callback: function}
* @param {context: object}
*/
observe: function(namespace, callback, context) {
// Make sure the arguments are of the correct type
if( typeof namespace !== 'string' || typeof callback !== 'function' || (context && typeof context !== 'object')) {
return false;
}
// If a namespace has not been created before then we need to "initialize" the namespace
if(!this.list[namespace]) {
this.list[namespace] = [];
}
this.list[namespace].push({callback: callback, context: context});
},
/*
* Remove all observers from a given namespace.
*
* @param {namespace: string} The namespace to clear.
*/
clearAll: function(namespace) {
if(!this.verifyNamespaceArg(namespace)) {
return false;
}
this.list[namespace] = [];
},
/*
* Notify all callbacks that have been bound to the given namespace.
*
* @param {namespace: string} The namespace to notify observers on.
* @param {namespace: url} The url to notify observers on.
*/
notify: function(namespace) {
// This strips the namespace from the list of args as we dont want to pass that into the callback.
var argumentsForCallback = Array.prototype.slice.call(arguments, 1);
if(!this.verifyNamespaceArg(namespace)) {
return false;
}
// Loop over all of the observers and fire the callback function with the context.
for(var i = 0, len = this.list[namespace].length; i < len; i++) {
this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback);
}
},
/*
* Notify only the callback of the given context and namespace.
*
* @param {context: object} the context to match against.
* @param {namespace: string} The namespace to notify observers on.
*/
notifyOnlyFor: function(context, namespace) {
// This strips the namespace from the list of args as we dont want to pass that into the callback.
var argumentsForCallback = Array.prototype.slice.call(arguments, 2);
if(!this.verifyNamespaceArg(namespace)) {
return false;
}
// Loop over all of the observers and fire the callback function with the context.
for(var i = 0, len = this.list[namespace].length; i < len; i++) {
if(this.list[namespace][i].context === context) {
this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback);
}
}
},
/*
* Verifies that the namespace is valid.
*
* @param {namespace: string} The namespace to verify.
*/
verifyNamespaceArg: function(namespace) {
if(typeof namespace !== 'string' || !this.list[namespace]) {
return false;
}
return true;
}
};
module.exports = SocketService;
},{"./helpers/global-context":3,"./helpers/message-event":4}]},{},[1]);

View file

@ -17,7 +17,7 @@ const GroupChange = require('../../ts/groupChange');
const IndexedDB = require('./indexeddb'); const IndexedDB = require('./indexeddb');
const Notifications = require('../../ts/notifications'); const Notifications = require('../../ts/notifications');
const OS = require('../../ts/OS'); const OS = require('../../ts/OS');
const Stickers = require('./stickers'); const Stickers = require('../../ts/types/Stickers');
const Settings = require('./settings'); const Settings = require('./settings');
const RemoteConfig = require('../../ts/RemoteConfig'); const RemoteConfig = require('../../ts/RemoteConfig');
const Util = require('../../ts/util'); const Util = require('../../ts/util');

View file

@ -1,18 +0,0 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function maybeDeletePack(packId: string): Promise<void>;
export function downloadStickerPack(
packId: string,
packKey: string,
options?: {
finalStatus?: 'installed' | 'downloaded';
messageId?: string;
fromSync?: boolean;
}
): Promise<void>;
export function isPackIdValid(packId: unknown): packId is string;
export function redactPackId(packId: string): string;

View file

@ -20,8 +20,6 @@ module.exports = {
dcodeIO: true, dcodeIO: true,
getString: true, getString: true,
hexToArrayBuffer: true, hexToArrayBuffer: true,
MockServer: true,
MockSocket: true,
PROTO_ROOT: true, PROTO_ROOT: true,
stringToArrayBuffer: true, stringToArrayBuffer: true,
}, },

View file

@ -60,8 +60,6 @@ window.hexToArrayBuffer = str => {
return ret; return ret;
}; };
window.MockSocket.prototype.addEventListener = () => null;
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
window.Whisper.events = { window.Whisper.events = {
on() {}, on() {},

View file

@ -1,118 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
describe('ContactBuffer', () => {
function getTestBuffer() {
const buffer = new dcodeIO.ByteBuffer();
const avatarBuffer = new dcodeIO.ByteBuffer();
const avatarLen = 255;
for (let i = 0; i < avatarLen; i += 1) {
avatarBuffer.writeUint8(i);
}
avatarBuffer.limit = avatarBuffer.offset;
avatarBuffer.offset = 0;
const contactInfo = new window.textsecure.protobuf.ContactDetails({
name: 'Zero Cool',
number: '+10000000000',
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
avatar: { contentType: 'image/jpeg', length: avatarLen },
});
const contactInfoBuffer = contactInfo.encode().toArrayBuffer();
for (let i = 0; i < 3; i += 1) {
buffer.writeVarint32(contactInfoBuffer.byteLength);
buffer.append(contactInfoBuffer);
buffer.append(avatarBuffer.clone());
}
buffer.limit = buffer.offset;
buffer.offset = 0;
return buffer.toArrayBuffer();
}
it('parses an array buffer of contacts', () => {
const arrayBuffer = getTestBuffer();
const contactBuffer = new window.textsecure.ContactBuffer(arrayBuffer);
let contact = contactBuffer.next();
let count = 0;
while (contact !== undefined) {
count += 1;
assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, '+10000000000');
assert.strictEqual(contact.uuid, '7198e1bd-1293-452a-a098-f982ff201902');
assert.strictEqual(contact.avatar.contentType, 'image/jpeg');
assert.strictEqual(contact.avatar.length, 255);
assert.strictEqual(contact.avatar.data.byteLength, 255);
const avatarBytes = new Uint8Array(contact.avatar.data);
for (let j = 0; j < 255; j += 1) {
assert.strictEqual(avatarBytes[j], j);
}
contact = contactBuffer.next();
}
assert.strictEqual(count, 3);
});
});
describe('GroupBuffer', () => {
function getTestBuffer() {
const buffer = new dcodeIO.ByteBuffer();
const avatarBuffer = new dcodeIO.ByteBuffer();
const avatarLen = 255;
for (let i = 0; i < avatarLen; i += 1) {
avatarBuffer.writeUint8(i);
}
avatarBuffer.limit = avatarBuffer.offset;
avatarBuffer.offset = 0;
const groupInfo = new window.textsecure.protobuf.GroupDetails({
id: window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 3, 3, 7])
),
name: 'Hackers',
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
avatar: { contentType: 'image/jpeg', length: avatarLen },
});
const groupInfoBuffer = groupInfo.encode().toArrayBuffer();
for (let i = 0; i < 3; i += 1) {
buffer.writeVarint32(groupInfoBuffer.byteLength);
buffer.append(groupInfoBuffer);
buffer.append(avatarBuffer.clone());
}
buffer.limit = buffer.offset;
buffer.offset = 0;
return buffer.toArrayBuffer();
}
it('parses an array buffer of groups', () => {
const arrayBuffer = getTestBuffer();
const groupBuffer = new window.textsecure.GroupBuffer(arrayBuffer);
let group = groupBuffer.next();
let count = 0;
while (group !== undefined) {
count += 1;
assert.strictEqual(group.name, 'Hackers');
assertEqualArrayBuffers(
group.id.toArrayBuffer(),
window.Signal.Crypto.typedArrayToArrayBuffer(
new Uint8Array([1, 3, 3, 7])
)
);
assert.sameMembers(group.membersE164, [
'cereal',
'burn',
'phreak',
'joey',
]);
assert.strictEqual(group.avatar.contentType, 'image/jpeg');
assert.strictEqual(group.avatar.length, 255);
assert.strictEqual(group.avatar.data.byteLength, 255);
const avatarBytes = new Uint8Array(group.avatar.data);
for (let j = 0; j < 255; j += 1) {
assert.strictEqual(avatarBytes[j], j);
}
group = groupBuffer.next();
}
assert.strictEqual(count, 3);
});
});

View file

@ -39,11 +39,9 @@
<script type="text/javascript" src="helpers_test.js"></script> <script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="crypto_test.js"></script> <script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="contacts_parser_test.js"></script>
<script type="text/javascript" src="generate_keys_test.js"></script> <script type="text/javascript" src="generate_keys_test.js"></script>
<script type="text/javascript" src="task_with_timeout_test.js"></script> <script type="text/javascript" src="task_with_timeout_test.js"></script>
<script type="text/javascript" src="account_manager_test.js"></script> <script type="text/javascript" src="account_manager_test.js"></script>
<script type="text/javascript" src="message_receiver_test.js"></script>
<script type="text/javascript" src="sendmessage_test.js"></script> <script type="text/javascript" src="sendmessage_test.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. --> <!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->

View file

@ -1,95 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global textsecure */
describe('MessageReceiver', () => {
const { WebSocket } = window;
const number = '+19999999999';
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
const deviceId = 1;
const signalingKey = window.Signal.Crypto.getRandomBytes(32 + 20);
before(() => {
localStorage.clear();
window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
textsecure.storage.put('password', 'password');
textsecure.storage.put('signaling_key', signalingKey);
});
after(() => {
localStorage.clear();
window.WebSocket = WebSocket;
});
describe('connecting', () => {
let attrs;
let websocketmessage;
before(() => {
attrs = {
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number,
sourceUuid: uuid,
sourceDevice: deviceId,
timestamp: Date.now(),
content: window.Signal.Crypto.getRandomBytes(200),
};
const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
websocketmessage = new textsecure.protobuf.WebSocketMessage({
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
request: { verb: 'PUT', path: '/api/v1/message', body },
});
});
it('generates decryption-error event when it cannot decrypt', done => {
const mockServer = new MockServer('ws://localhost:8081/');
mockServer.on('connection', server => {
setTimeout(() => {
server.send(new Blob([websocketmessage.toArrayBuffer()]));
}, 1);
});
const messageReceiver = new textsecure.MessageReceiver(
'oldUsername.2',
'username.2',
'password',
'signalingKey',
{
serverTrustRoot: 'AAAAAAAA',
}
);
messageReceiver.addEventListener('decrytion-error', done());
});
});
// For when we start testing individual MessageReceiver methods
// describe('methods', () => {
// let messageReceiver;
// let mockServer;
// beforeEach(() => {
// // Necessary to populate the server property inside of MockSocket. Without it, we
// // crash when doing any number of things to a MockSocket instance.
// mockServer = new MockServer('ws://localhost:8081');
// messageReceiver = new textsecure.MessageReceiver(
// 'oldUsername.3',
// 'username.3',
// 'password',
// 'signalingKey',
// {
// serverTrustRoot: 'AAAAAAAA',
// }
// );
// });
// afterEach(() => {
// mockServer.close();
// });
// });
});

View file

@ -1,40 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global textsecure */
describe('Protocol', () => {
describe('Unencrypted PushMessageProto "decrypt"', () => {
// exclusive
it('works', done => {
localStorage.clear();
const textMessage = new textsecure.protobuf.DataMessage();
textMessage.body = 'Hi Mom';
const serverMessage = {
type: 4, // unencrypted
source: '+19999999999',
timestamp: 42,
message: textMessage.encode(),
};
return textsecure.protocol_wrapper
.handleEncryptedMessage(
serverMessage.source,
serverMessage.source_device,
serverMessage.type,
serverMessage.message
)
.then(message => {
assert.equal(message.body, textMessage.body);
assert.equal(
message.attachments.length,
textMessage.attachments.length
);
assert.equal(textMessage.attachments.length, 0);
})
.then(done)
.catch(done);
});
});
});

View file

@ -1,69 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global TextSecureWebSocket */
describe('TextSecureWebSocket', () => {
const RealWebSocket = window.WebSocket;
before(() => {
window.WebSocket = MockSocket;
});
after(() => {
window.WebSocket = RealWebSocket;
});
it('connects and disconnects', done => {
const mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', server => {
socket.close();
server.close();
done();
});
const socket = new TextSecureWebSocket('ws://localhost:8080');
});
it('sends and receives', done => {
const mockServer = new MockServer('ws://localhost:8080');
mockServer.on('connection', server => {
server.on('message', () => {
server.send('ack');
server.close();
});
});
const socket = new TextSecureWebSocket('ws://localhost:8080');
socket.onmessage = response => {
assert.strictEqual(response.data, 'ack');
socket.close();
done();
};
socket.send('syn');
});
it('exposes the socket status', done => {
const mockServer = new MockServer('ws://localhost:8082');
mockServer.on('connection', server => {
assert.strictEqual(socket.getStatus(), WebSocket.OPEN);
server.close();
socket.close();
});
const socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = () => {
assert.strictEqual(socket.getStatus(), WebSocket.CLOSING);
done();
};
});
it('reconnects', function thisNeeded(done) {
this.timeout(60000);
const mockServer = new MockServer('ws://localhost:8082');
const socket = new TextSecureWebSocket('ws://localhost:8082');
socket.onclose = () => {
const secondServer = new MockServer('ws://localhost:8082');
secondServer.on('connection', server => {
socket.close();
server.close();
done();
});
};
mockServer.close();
});
});

View file

@ -509,25 +509,6 @@ try {
// https://stackoverflow.com/a/23299989 // https://stackoverflow.com/a/23299989
window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164); window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164);
window.normalizeUuids = (obj, paths, context) => {
if (!obj) {
return;
}
paths.forEach(path => {
const val = _.get(obj, path);
if (val) {
if (!val || !window.isValidGuid(val)) {
window.log.warn(
`Normalizing invalid uuid: ${val} at path ${path} in context "${context}"`
);
}
if (val && val.toLowerCase) {
_.set(obj, path, val.toLowerCase());
}
}
});
};
window.React = require('react'); window.React = require('react');
window.ReactDOM = require('react-dom'); window.ReactDOM = require('react-dom');
window.moment = require('moment'); window.moment = require('moment');

View file

@ -15,8 +15,6 @@ module.exports = {
dcodeIO: true, dcodeIO: true,
getString: true, getString: true,
hexToArrayBuffer: true, hexToArrayBuffer: true,
MockServer: true,
MockSocket: true,
PROTO_ROOT: true, PROTO_ROOT: true,
stringToArrayBuffer: true, stringToArrayBuffer: true,
}, },

View file

@ -7,58 +7,58 @@ const { Stickers } = Signal;
describe('Stickers', () => { describe('Stickers', () => {
describe('getDataFromLink', () => { describe('getDataFromLink', () => {
it('returns null for invalid URLs', () => { it('returns undefined for invalid URLs', () => {
assert.isNull(Stickers.getDataFromLink('https://')); assert.isUndefined(Stickers.getDataFromLink('https://'));
assert.isNull(Stickers.getDataFromLink('signal.art/addstickers/')); assert.isUndefined(Stickers.getDataFromLink('signal.art/addstickers/'));
}); });
it("returns null for URLs that don't have a hash", () => { it("returns undefined for URLs that don't have a hash", () => {
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink('https://signal.art/addstickers/') Stickers.getDataFromLink('https://signal.art/addstickers/')
); );
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink('https://signal.art/addstickers/#') Stickers.getDataFromLink('https://signal.art/addstickers/#')
); );
}); });
it('returns null when no key or pack ID is found', () => { it('returns undefined when no key or pack ID is found', () => {
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a' 'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a'
) )
); );
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key=' 'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key='
) )
); );
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' 'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
) )
); );
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e&pack_id=' 'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e&pack_id='
) )
); );
}); });
it('returns null when the pack ID is invalid', () => { it('returns undefined when the pack ID is invalid', () => {
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_id=garbage&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' 'https://signal.art/addstickers/#pack_id=garbage&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
) )
); );
}); });
it('returns null if the ID or key are passed as arrays', () => { it('returns undefined if the ID or key are passed as arrays', () => {
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_id[]=c8c83285b547872ac4c589d64a6edd6a&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' 'https://signal.art/addstickers/#pack_id[]=c8c83285b547872ac4c589d64a6edd6a&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
) )
); );
assert.isNull( assert.isUndefined(
Stickers.getDataFromLink( Stickers.getDataFromLink(
'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key[]=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' 'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key[]=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
) )

View file

@ -1,7 +1,7 @@
// Copyright 2020-2021 Signal Messenger, LLC // Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash'; import { isNumber, noop } from 'lodash';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { import {
@ -9,16 +9,23 @@ import {
PlaintextContent, PlaintextContent,
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { DataMessageClass, SyncMessageClass } from './textsecure.d'; import MessageReceiver from './textsecure/MessageReceiver';
import { SessionResetsType } from './textsecure/Types.d'; import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
import { MessageAttributesType } from './model-types.d'; import {
MessageAttributesType,
ConversationAttributesType,
} from './model-types.d';
import * as Bytes from './Bytes';
import { typedArrayToArrayBuffer } from './Crypto';
import { WhatIsThis } from './window.d'; import { WhatIsThis } from './window.d';
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
import { ChallengeHandler } from './challenge'; import { ChallengeHandler } from './challenge';
import { isWindowDragElement } from './util/isWindowDragElement'; import { isWindowDragElement } from './util/isWindowDragElement';
import { assert } from './util/assert'; import { assert, strictAssert } from './util/assert';
import { dropNull } from './util/dropNull';
import { normalizeUuid } from './util/normalizeUuid';
import { filter } from './util/iterables'; import { filter } from './util/iterables';
import { isNotNil } from './util/isNotNil'; import { isNotNil } from './util/isNotNil';
import { senderCertificateService } from './services/senderCertificate'; import { senderCertificateService } from './services/senderCertificate';
@ -36,9 +43,30 @@ import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey'
import { LatestQueue } from './util/LatestQueue'; import { LatestQueue } from './util/LatestQueue';
import { parseIntOrThrow } from './util/parseIntOrThrow'; import { parseIntOrThrow } from './util/parseIntOrThrow';
import { import {
DecryptionErrorType, TypingEvent,
RetryRequestType, ErrorEvent,
} from './textsecure/MessageReceiver'; DeliveryEvent,
DecryptionErrorEvent,
DecryptionErrorEventData,
SentEvent,
SentEventData,
ProfileKeyUpdateEvent,
MessageEvent,
MessageEventData,
RetryRequestEvent,
RetryRequestEventData,
ReadEvent,
ConfigurationEvent,
ViewSyncEvent,
MessageRequestResponseEvent,
FetchLatestEvent,
KeysEvent,
StickerPackEvent,
VerifiedEvent,
ReadSyncEvent,
ContactEvent,
GroupEvent,
} from './textsecure/messageReceiverEvents';
import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials';
import * as universalExpireTimer from './util/universalExpireTimer'; import * as universalExpireTimer from './util/universalExpireTimer';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
@ -59,6 +87,7 @@ import {
SystemTraySetting, SystemTraySetting,
parseSystemTraySetting, parseSystemTraySetting,
} from './types/SystemTraySetting'; } from './types/SystemTraySetting';
import * as Stickers from './types/Stickers';
import { SignalService as Proto } from './protobuf'; import { SignalService as Proto } from './protobuf';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -149,7 +178,7 @@ export async function startApp(): Promise<void> {
name: 'Whisper.deliveryReceiptBatcher', name: 'Whisper.deliveryReceiptBatcher',
wait: 500, wait: 500,
maxSize: 500, maxSize: 500,
processBatch: async (items: WhatIsThis) => { processBatch: async items => {
const byConversationId = window._.groupBy(items, item => const byConversationId = window._.groupBy(items, item =>
window.ConversationController.ensureContactIds({ window.ConversationController.ensureContactIds({
e164: item.source, e164: item.source,
@ -320,7 +349,7 @@ export async function startApp(): Promise<void> {
window.getAccountManager()!.refreshPreKeys(); window.getAccountManager()!.refreshPreKeys();
}); });
let messageReceiver: WhatIsThis; let messageReceiver: MessageReceiver | undefined;
let preMessageReceiverStatus: SocketStatus | undefined; let preMessageReceiverStatus: SocketStatus | undefined;
window.getSocketStatus = () => { window.getSocketStatus = () => {
if (messageReceiver) { if (messageReceiver) {
@ -589,7 +618,7 @@ export async function startApp(): Promise<void> {
if (messageReceiver) { if (messageReceiver) {
messageReceiver.unregisterBatchers(); messageReceiver.unregisterBatchers();
messageReceiver = null; messageReceiver = undefined;
} }
// A number of still-to-queue database queries might be waiting inside batchers. // A number of still-to-queue database queries might be waiting inside batchers.
@ -619,14 +648,14 @@ export async function startApp(): Promise<void> {
window.isShowingModal = true; window.isShowingModal = true;
// Kick off the download // Kick off the download
window.Signal.Stickers.downloadEphemeralPack(packId, key); Stickers.downloadEphemeralPack(packId, key);
const props = { const props = {
packId, packId,
onClose: async () => { onClose: async () => {
window.isShowingModal = false; window.isShowingModal = false;
stickerPreviewModalView.remove(); stickerPreviewModalView.remove();
await window.Signal.Stickers.removeEphemeralPack(packId); await Stickers.removeEphemeralPack(packId);
}, },
}; };
@ -703,7 +732,7 @@ export async function startApp(): Promise<void> {
}, },
installStickerPack: async (packId: string, key: string) => { installStickerPack: async (packId: string, key: string) => {
window.Signal.Stickers.downloadStickerPack(packId, key, { Stickers.downloadStickerPack(packId, key, {
finalStatus: 'installed', finalStatus: 'installed',
}); });
}, },
@ -894,7 +923,7 @@ export async function startApp(): Promise<void> {
try { try {
await Promise.all([ await Promise.all([
window.ConversationController.load(), window.ConversationController.load(),
window.Signal.Stickers.load(), Stickers.load(),
window.Signal.Emojis.load(), window.Signal.Emojis.load(),
window.textsecure.storage.protocol.hydrateCaches(), window.textsecure.storage.protocol.hydrateCaches(),
]); ]);
@ -963,7 +992,7 @@ export async function startApp(): Promise<void> {
}, },
emojis: window.Signal.Emojis.getInitialState(), emojis: window.Signal.Emojis.getInitialState(),
items: window.storage.getItemsState(), items: window.storage.getItemsState(),
stickers: window.Signal.Stickers.getInitialState(), stickers: Stickers.getInitialState(),
user: { user: {
attachmentsPath: window.baseAttachmentsPath, attachmentsPath: window.baseAttachmentsPath,
stickersPath: window.baseStickersPath, stickersPath: window.baseStickersPath,
@ -1850,6 +1879,8 @@ export async function startApp(): Promise<void> {
} }
window.getSyncRequest = (timeoutMillis?: number) => { window.getSyncRequest = (timeoutMillis?: number) => {
strictAssert(messageReceiver, 'MessageReceiver not initialized');
const syncRequest = new window.textsecure.SyncRequest( const syncRequest = new window.textsecure.SyncRequest(
window.textsecure.messaging, window.textsecure.messaging,
messageReceiver, messageReceiver,
@ -1859,8 +1890,8 @@ export async function startApp(): Promise<void> {
return syncRequest; return syncRequest;
}; };
let disconnectTimer: WhatIsThis | null = null; let disconnectTimer: NodeJS.Timeout | undefined;
let reconnectTimer: WhatIsThis | null = null; let reconnectTimer: number | undefined;
function onOffline() { function onOffline() {
window.log.info('offline'); window.log.info('offline');
@ -1886,12 +1917,12 @@ export async function startApp(): Promise<void> {
if (disconnectTimer && isSocketOnline()) { if (disconnectTimer && isSocketOnline()) {
window.log.warn('Already online. Had a blip in online/offline status.'); window.log.warn('Already online. Had a blip in online/offline status.');
clearTimeout(disconnectTimer); clearTimeout(disconnectTimer);
disconnectTimer = null; disconnectTimer = undefined;
return; return;
} }
if (disconnectTimer) { if (disconnectTimer) {
clearTimeout(disconnectTimer); clearTimeout(disconnectTimer);
disconnectTimer = null; disconnectTimer = undefined;
} }
connect(); connect();
@ -1909,7 +1940,7 @@ export async function startApp(): Promise<void> {
window.log.info('disconnect'); window.log.info('disconnect');
// Clear timer, since we're only called when the timer is expired // Clear timer, since we're only called when the timer is expired
disconnectTimer = null; disconnectTimer = undefined;
AttachmentDownloads.stop(); AttachmentDownloads.stop();
if (messageReceiver) { if (messageReceiver) {
@ -1934,7 +1965,7 @@ export async function startApp(): Promise<void> {
if (reconnectTimer) { if (reconnectTimer) {
clearTimeout(reconnectTimer); clearTimeout(reconnectTimer);
reconnectTimer = null; reconnectTimer = undefined;
} }
// Bootstrap our online/offline detection, only the first time we connect // Bootstrap our online/offline detection, only the first time we connect
@ -1964,7 +1995,7 @@ export async function startApp(): Promise<void> {
if (messageReceiver) { if (messageReceiver) {
messageReceiver.unregisterBatchers(); messageReceiver.unregisterBatchers();
messageReceiver = null; messageReceiver = undefined;
} }
const OLD_USERNAME = window.storage.get('number_id', ''); const OLD_USERNAME = window.storage.get('number_id', '');
@ -2043,8 +2074,11 @@ export async function startApp(): Promise<void> {
preMessageReceiverStatus = undefined; preMessageReceiverStatus = undefined;
// eslint-disable-next-line no-inner-declarations // eslint-disable-next-line no-inner-declarations
function addQueuedEventListener(name: string, handler: WhatIsThis) { function queuedEventListener<Args extends Array<unknown>>(
messageReceiver.addEventListener(name, (...args: Array<WhatIsThis>) => handler: (...args: Args) => Promise<void> | void,
track = true
): (...args: Args) => void {
return (...args: Args): void => {
eventHandlerQueue.add(async () => { eventHandlerQueue.add(async () => {
try { try {
await handler(...args); await handler(...args);
@ -2052,40 +2086,97 @@ export async function startApp(): Promise<void> {
// message/sent: Message.handleDataMessage has its own queue and will // message/sent: Message.handleDataMessage has its own queue and will
// trigger this event itself when complete. // trigger this event itself when complete.
// error: Error processing (below) also has its own queue and self-trigger. // error: Error processing (below) also has its own queue and self-trigger.
if (name !== 'message' && name !== 'sent' && name !== 'error') { if (track) {
window.Whisper.events.trigger('incrementProgress'); window.Whisper.events.trigger('incrementProgress');
} }
} }
}) });
); };
} }
addQueuedEventListener('message', onMessageReceived); messageReceiver.addEventListener(
addQueuedEventListener('delivery', onDeliveryReceipt); 'message',
addQueuedEventListener('contact', onContactReceived); queuedEventListener(onMessageReceived, false)
addQueuedEventListener('contactsync', onContactSyncComplete);
addQueuedEventListener('group', onGroupReceived);
addQueuedEventListener('groupsync', onGroupSyncComplete);
addQueuedEventListener('sent', onSentMessage);
addQueuedEventListener('readSync', onReadSync);
addQueuedEventListener('read', onReadReceipt);
addQueuedEventListener('verified', onVerified);
addQueuedEventListener('error', onError);
addQueuedEventListener('decryption-error', onDecryptionError);
addQueuedEventListener('retry-request', onRetryRequest);
addQueuedEventListener('empty', onEmpty);
addQueuedEventListener('reconnect', onReconnect);
addQueuedEventListener('configuration', onConfiguration);
addQueuedEventListener('typing', onTyping);
addQueuedEventListener('sticker-pack', onStickerPack);
addQueuedEventListener('viewSync', onViewSync);
addQueuedEventListener(
'messageRequestResponse',
onMessageRequestResponse
); );
addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate); messageReceiver.addEventListener(
addQueuedEventListener('fetchLatest', onFetchLatestSync); 'delivery',
addQueuedEventListener('keys', onKeysSync); queuedEventListener(onDeliveryReceipt)
);
messageReceiver.addEventListener(
'contact',
queuedEventListener(onContactReceived)
);
messageReceiver.addEventListener(
'contactSync',
queuedEventListener(onContactSyncComplete)
);
messageReceiver.addEventListener(
'group',
queuedEventListener(onGroupReceived)
);
messageReceiver.addEventListener(
'groupSync',
queuedEventListener(onGroupSyncComplete)
);
messageReceiver.addEventListener(
'sent',
queuedEventListener(onSentMessage, false)
);
messageReceiver.addEventListener(
'readSync',
queuedEventListener(onReadSync)
);
messageReceiver.addEventListener(
'read',
queuedEventListener(onReadReceipt)
);
messageReceiver.addEventListener(
'verified',
queuedEventListener(onVerified)
);
messageReceiver.addEventListener(
'error',
queuedEventListener(onError, false)
);
messageReceiver.addEventListener(
'decryption-error',
queuedEventListener(onDecryptionError)
);
messageReceiver.addEventListener(
'retry-request',
queuedEventListener(onRetryRequest)
);
messageReceiver.addEventListener('empty', queuedEventListener(onEmpty));
messageReceiver.addEventListener(
'reconnect',
queuedEventListener(onReconnect)
);
messageReceiver.addEventListener(
'configuration',
queuedEventListener(onConfiguration)
);
messageReceiver.addEventListener('typing', queuedEventListener(onTyping));
messageReceiver.addEventListener(
'sticker-pack',
queuedEventListener(onStickerPack)
);
messageReceiver.addEventListener(
'viewSync',
queuedEventListener(onViewSync)
);
messageReceiver.addEventListener(
'messageRequestResponse',
queuedEventListener(onMessageRequestResponse)
);
messageReceiver.addEventListener(
'profileKeyUpdate',
queuedEventListener(onProfileKeyUpdate)
);
messageReceiver.addEventListener(
'fetchLatest',
queuedEventListener(onFetchLatestSync)
);
messageReceiver.addEventListener('keys', queuedEventListener(onKeysSync));
AttachmentDownloads.start({ AttachmentDownloads.start({
getMessageReceiver: () => messageReceiver, getMessageReceiver: () => messageReceiver,
@ -2093,7 +2184,7 @@ export async function startApp(): Promise<void> {
}); });
if (connectCount === 1) { if (connectCount === 1) {
window.Signal.Stickers.downloadQueuedPacks(); Stickers.downloadQueuedPacks();
if (!newVersion) { if (!newVersion) {
runStorageService(); runStorageService();
} }
@ -2229,9 +2320,9 @@ export async function startApp(): Promise<void> {
syncMessage: true, syncMessage: true,
}); });
const installedStickerPacks = window.Signal.Stickers.getInstalledStickerPacks(); const installedStickerPacks = Stickers.getInstalledStickerPacks();
if (installedStickerPacks.length) { if (installedStickerPacks.length) {
const operations = installedStickerPacks.map((pack: WhatIsThis) => ({ const operations = installedStickerPacks.map(pack => ({
packId: pack.id, packId: pack.id,
packKey: pack.key, packKey: pack.key,
installed: true, installed: true,
@ -2313,18 +2404,22 @@ export async function startApp(): Promise<void> {
window.log.info( window.log.info(
'waitForEmptyEventQueue: Waiting for MessageReceiver empty event...' 'waitForEmptyEventQueue: Waiting for MessageReceiver empty event...'
); );
let resolve: WhatIsThis; let resolve: undefined | (() => void);
let reject: WhatIsThis; let reject: undefined | ((error: Error) => void);
const promise = new Promise((innerResolve, innerReject) => { const promise = new Promise<void>((innerResolve, innerReject) => {
resolve = innerResolve; resolve = innerResolve;
reject = innerReject; reject = innerReject;
}); });
const timeout = setTimeout(reject, FIVE_MINUTES); const timeout = reject && setTimeout(reject, FIVE_MINUTES);
const onEmptyOnce = () => { const onEmptyOnce = () => {
messageReceiver.removeEventListener('empty', onEmptyOnce); if (messageReceiver) {
messageReceiver.removeEventListener('empty', onEmptyOnce);
}
clearTimeout(timeout); clearTimeout(timeout);
resolve(); if (resolve) {
resolve();
}
}; };
messageReceiver.addEventListener('empty', onEmptyOnce); messageReceiver.addEventListener('empty', onEmptyOnce);
@ -2459,7 +2554,7 @@ export async function startApp(): Promise<void> {
connect(); connect();
} }
function onConfiguration(ev: WhatIsThis) { function onConfiguration(ev: ConfigurationEvent) {
ev.confirm(); ev.confirm();
const { configuration } = ev; const { configuration } = ev;
@ -2470,7 +2565,7 @@ export async function startApp(): Promise<void> {
linkPreviews, linkPreviews,
} = configuration; } = configuration;
window.storage.put('read-receipt-setting', readReceipts); window.storage.put('read-receipt-setting', Boolean(readReceipts));
if ( if (
unidentifiedDeliveryIndicators === true || unidentifiedDeliveryIndicators === true ||
@ -2491,7 +2586,7 @@ export async function startApp(): Promise<void> {
} }
} }
function onTyping(ev: WhatIsThis) { function onTyping(ev: TypingEvent) {
// Note: this type of message is automatically removed from cache in MessageReceiver // Note: this type of message is automatically removed from cache in MessageReceiver
const { typing, sender, senderUuid, senderDevice } = ev; const { typing, sender, senderUuid, senderDevice } = ev;
@ -2557,12 +2652,12 @@ export async function startApp(): Promise<void> {
}); });
} }
async function onStickerPack(ev: WhatIsThis) { async function onStickerPack(ev: StickerPackEvent) {
ev.confirm(); ev.confirm();
const packs = ev.stickerPacks || []; const packs = ev.stickerPacks;
packs.forEach((pack: WhatIsThis) => { packs.forEach(pack => {
const { id, key, isInstall, isRemove } = pack || {}; const { id, key, isInstall, isRemove } = pack || {};
if (!id || !key || (!isInstall && !isRemove)) { if (!id || !key || (!isInstall && !isRemove)) {
@ -2572,7 +2667,7 @@ export async function startApp(): Promise<void> {
return; return;
} }
const status = window.Signal.Stickers.getStickerPackStatus(id); const status = Stickers.getStickerPackStatus(id);
if (status === 'installed' && isRemove) { if (status === 'installed' && isRemove) {
window.reduxActions.stickers.uninstallStickerPack(id, key, { window.reduxActions.stickers.uninstallStickerPack(id, key, {
@ -2584,7 +2679,7 @@ export async function startApp(): Promise<void> {
fromSync: true, fromSync: true,
}); });
} else { } else {
window.Signal.Stickers.downloadStickerPack(id, key, { Stickers.downloadStickerPack(id, key, {
finalStatus: 'installed', finalStatus: 'installed',
fromSync: true, fromSync: true,
}); });
@ -2598,7 +2693,7 @@ export async function startApp(): Promise<void> {
await window.storage.put('synced_at', Date.now()); await window.storage.put('synced_at', Date.now());
} }
async function onContactReceived(ev: WhatIsThis) { async function onContactReceived(ev: ContactEvent) {
const details = ev.contactDetails; const details = ev.contactDetails;
if ( if (
@ -2610,20 +2705,20 @@ export async function startApp(): Promise<void> {
// special case for syncing details about ourselves // special case for syncing details about ourselves
if (details.profileKey) { if (details.profileKey) {
window.log.info('Got sync message with our own profile key'); window.log.info('Got sync message with our own profile key');
ourProfileKeyService.set(details.profileKey); ourProfileKeyService.set(typedArrayToArrayBuffer(details.profileKey));
} }
} }
const c = new window.Whisper.Conversation({ const c = new window.Whisper.Conversation(({
e164: details.number, e164: details.number,
uuid: details.uuid, uuid: details.uuid,
type: 'private', type: 'private',
} as WhatIsThis); } as Partial<ConversationAttributesType>) as WhatIsThis);
const validationError = c.validate(); const validationError = c.validate();
if (validationError) { if (validationError) {
window.log.error( window.log.error(
'Invalid contact received:', 'Invalid contact received:',
Errors.toLogFormat(validationError as WhatIsThis) Errors.toLogFormat(validationError)
); );
return; return;
} }
@ -2638,9 +2733,7 @@ export async function startApp(): Promise<void> {
const conversation = window.ConversationController.get(detailsId)!; const conversation = window.ConversationController.get(detailsId)!;
if (details.profileKey) { if (details.profileKey) {
const profileKey = window.Signal.Crypto.arrayBufferToBase64( const profileKey = Bytes.toBase64(details.profileKey);
details.profileKey
);
conversation.setProfileKey(profileKey); conversation.setProfileKey(profileKey);
} }
@ -2698,14 +2791,18 @@ export async function startApp(): Promise<void> {
if (details.verified) { if (details.verified) {
const { verified } = details; const { verified } = details;
const verifiedEvent = new Event('verified'); const verifiedEvent = new VerifiedEvent(
verifiedEvent.verified = { {
state: verified.state, state: dropNull(verified.state),
destination: verified.destination, destination: dropNull(verified.destination),
destinationUuid: verified.destinationUuid, destinationUuid: dropNull(verified.destinationUuid),
identityKey: verified.identityKey.toArrayBuffer(), identityKey: verified.identityKey
}; ? typedArrayToArrayBuffer(verified.identityKey)
(verifiedEvent as WhatIsThis).viaContactSync = true; : undefined,
viaContactSync: true,
},
noop
);
await onVerified(verifiedEvent); await onVerified(verifiedEvent);
} }
@ -2726,11 +2823,11 @@ export async function startApp(): Promise<void> {
} }
// Note: this handler is only for v1 groups received via 'group sync' messages // Note: this handler is only for v1 groups received via 'group sync' messages
async function onGroupReceived(ev: WhatIsThis) { async function onGroupReceived(ev: GroupEvent) {
const details = ev.groupDetails; const details = ev.groupDetails;
const { id } = details; const { id } = details;
const idBuffer = window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(id); const idBuffer = id;
const idBytes = idBuffer.byteLength; const idBytes = idBuffer.byteLength;
if (idBytes !== 16) { if (idBytes !== 16) {
window.log.error( window.log.error(
@ -2740,7 +2837,7 @@ export async function startApp(): Promise<void> {
} }
const conversation = await window.ConversationController.getOrCreateAndWait( const conversation = await window.ConversationController.getOrCreateAndWait(
id, Bytes.toBinary(id),
'group' 'group'
); );
if (isGroupV2(conversation.attributes)) { if (isGroupV2(conversation.attributes)) {
@ -2751,18 +2848,18 @@ export async function startApp(): Promise<void> {
return; return;
} }
const memberConversations = details.membersE164.map((e164: WhatIsThis) => const memberConversations = details.membersE164.map(e164 =>
window.ConversationController.getOrCreate(e164, 'private') window.ConversationController.getOrCreate(e164, 'private')
); );
const members = memberConversations.map((c: WhatIsThis) => c.get('id')); const members = memberConversations.map(c => c.get('id'));
const updates = { const updates: Partial<ConversationAttributesType> = {
name: details.name, name: details.name,
members, members,
type: 'group', type: 'group',
inbox_position: details.inboxPosition, inbox_position: details.inboxPosition,
} as WhatIsThis; };
if (details.active) { if (details.active) {
updates.left = false; updates.left = false;
@ -2823,8 +2920,16 @@ export async function startApp(): Promise<void> {
data, data,
confirm, confirm,
messageDescriptor, messageDescriptor,
}: WhatIsThis) { }: {
const profileKey = data.message.profileKey.toString('base64'); data: MessageEventData;
confirm: () => void;
messageDescriptor: MessageDescriptor;
}) {
const { profileKey } = data.message;
strictAssert(
profileKey !== undefined,
'handleMessageReceivedProfileUpdate: missing profileKey'
);
const sender = window.ConversationController.get(messageDescriptor.id); const sender = window.ConversationController.get(messageDescriptor.id);
if (sender) { if (sender) {
@ -2864,7 +2969,7 @@ export async function startApp(): Promise<void> {
// Note: We do very little in this function, since everything in handleDataMessage is // Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier // inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage(). // message is processed in handleDataMessage().
function onMessageReceived(event: WhatIsThis) { function onMessageReceived(event: MessageEvent) {
const { data, confirm } = event; const { data, confirm } = event;
const messageDescriptor = getMessageDescriptor({ const messageDescriptor = getMessageDescriptor({
@ -2903,10 +3008,13 @@ export async function startApp(): Promise<void> {
} }
if (data.message.reaction) { if (data.message.reaction) {
window.normalizeUuids( strictAssert(
data.message.reaction, data.message.reaction.targetAuthorUuid,
['targetAuthorUuid'], 'Reaction without targetAuthorUuid'
'background::onMessageReceived' );
const targetAuthorUuid = normalizeUuid(
data.message.reaction.targetAuthorUuid,
'DataMessage.Reaction.targetAuthorUuid'
); );
const { reaction } = data.message; const { reaction } = data.message;
@ -2924,7 +3032,7 @@ export async function startApp(): Promise<void> {
const reactionModel = Reactions.getSingleton().add({ const reactionModel = Reactions.getSingleton().add({
emoji: reaction.emoji, emoji: reaction.emoji,
remove: reaction.remove, remove: reaction.remove,
targetAuthorUuid: reaction.targetAuthorUuid, targetAuthorUuid,
targetTimestamp: reaction.targetTimestamp, targetTimestamp: reaction.targetTimestamp,
timestamp: Date.now(), timestamp: Date.now(),
fromId: window.ConversationController.ensureContactIds({ fromId: window.ConversationController.ensureContactIds({
@ -2965,7 +3073,7 @@ export async function startApp(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
async function onProfileKeyUpdate({ data, confirm }: WhatIsThis) { async function onProfileKeyUpdate({ data, confirm }: ProfileKeyUpdateEvent) {
const conversationId = window.ConversationController.ensureContactIds({ const conversationId = window.ConversationController.ensureContactIds({
e164: data.source, e164: data.source,
uuid: data.sourceUuid, uuid: data.sourceUuid,
@ -3007,7 +3115,11 @@ export async function startApp(): Promise<void> {
data, data,
confirm, confirm,
messageDescriptor, messageDescriptor,
}: WhatIsThis) { }: {
data: SentEventData;
confirm: () => void;
messageDescriptor: MessageDescriptor;
}) {
// First set profileSharing = true for the conversation we sent to // First set profileSharing = true for the conversation we sent to
const { id } = messageDescriptor; const { id } = messageDescriptor;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -3020,7 +3132,11 @@ export async function startApp(): Promise<void> {
const ourId = window.ConversationController.getOurConversationId(); const ourId = window.ConversationController.getOurConversationId();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const me = window.ConversationController.get(ourId)!; const me = window.ConversationController.get(ourId)!;
const profileKey = data.message.profileKey.toString('base64'); const { profileKey } = data.message;
strictAssert(
profileKey !== undefined,
'handleMessageSentProfileUpdate: missing profileKey'
);
// Will do the save for us if needed // Will do the save for us if needed
await me.setProfileKey(profileKey); await me.setProfileKey(profileKey);
@ -3028,18 +3144,17 @@ export async function startApp(): Promise<void> {
return confirm(); return confirm();
} }
function createSentMessage(data: WhatIsThis, descriptor: MessageDescriptor) { function createSentMessage(
data: SentEventData,
descriptor: MessageDescriptor
) {
const now = Date.now(); const now = Date.now();
const timestamp = data.timestamp || now; const timestamp = data.timestamp || now;
const unidentifiedStatus: Array<SyncMessageClass.Sent.UnidentifiedDeliveryStatus> = Array.isArray( const { unidentifiedStatus = [] } = data;
data.unidentifiedStatus
)
? data.unidentifiedStatus
: [];
let sentTo: Array<string> = []; let sentTo: Array<string> = [];
let unidentifiedDeliveries: Array<string> = [];
if (unidentifiedStatus.length) { if (unidentifiedStatus.length) {
sentTo = unidentifiedStatus sentTo = unidentifiedStatus
.map(item => item.destinationUuid || item.destination) .map(item => item.destinationUuid || item.destination)
@ -3048,13 +3163,12 @@ export async function startApp(): Promise<void> {
const unidentified = window._.filter(data.unidentifiedStatus, item => const unidentified = window._.filter(data.unidentifiedStatus, item =>
Boolean(item.unidentified) Boolean(item.unidentified)
); );
// eslint-disable-next-line no-param-reassign unidentifiedDeliveries = unidentified
data.unidentifiedDeliveries = unidentified.map( .map(item => item.destinationUuid || item.destination)
item => item.destinationUuid || item.destination .filter(isNotNil);
);
} }
return new window.Whisper.Message({ return new window.Whisper.Message(({
source: window.textsecure.storage.user.getNumber(), source: window.textsecure.storage.user.getNumber(),
sourceUuid: window.textsecure.storage.user.getUuid(), sourceUuid: window.textsecure.storage.user.getUuid(),
sourceDevice: data.device, sourceDevice: data.device,
@ -3067,12 +3181,12 @@ export async function startApp(): Promise<void> {
timestamp, timestamp,
type: 'outgoing', type: 'outgoing',
sent: true, sent: true,
unidentifiedDeliveries: data.unidentifiedDeliveries || [], unidentifiedDeliveries,
expirationStartTimestamp: Math.min( expirationStartTimestamp: Math.min(
data.expirationStartTimestamp || timestamp, data.expirationStartTimestamp || timestamp,
now now
), ),
} as WhatIsThis); } as Partial<MessageAttributesType>) as WhatIsThis);
} }
// Works with 'sent' and 'message' data sent from MessageReceiver, with a little massage // Works with 'sent' and 'message' data sent from MessageReceiver, with a little massage
@ -3084,11 +3198,11 @@ export async function startApp(): Promise<void> {
destination, destination,
destinationUuid, destinationUuid,
}: { }: {
message: DataMessageClass; message: ProcessedDataMessage;
source: string; source?: string;
sourceUuid: string; sourceUuid?: string;
destination: string; destination?: string;
destinationUuid: string; destinationUuid?: string;
}): MessageDescriptor => { }): MessageDescriptor => {
if (message.groupV2) { if (message.groupV2) {
const { id } = message.groupV2; const { id } = message.groupV2;
@ -3186,14 +3300,19 @@ export async function startApp(): Promise<void> {
// Note: We do very little in this function, since everything in handleDataMessage is // Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier // inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage(). // message is processed in handleDataMessage().
function onSentMessage(event: WhatIsThis) { function onSentMessage(event: SentEvent) {
const { data, confirm } = event; const { data, confirm } = event;
const source = window.textsecure.storage.user.getNumber();
const sourceUuid = window.textsecure.storage.user.getUuid();
strictAssert(source && sourceUuid, 'Missing user number and uuid');
const messageDescriptor = getMessageDescriptor({ const messageDescriptor = getMessageDescriptor({
...data, ...data,
// 'sent' event: the sender is always us! // 'sent' event: the sender is always us!
source: window.textsecure.storage.user.getNumber(), source,
sourceUuid: window.textsecure.storage.user.getUuid(), sourceUuid,
}); });
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags; const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
@ -3210,10 +3329,13 @@ export async function startApp(): Promise<void> {
const message = createSentMessage(data, messageDescriptor); const message = createSentMessage(data, messageDescriptor);
if (data.message.reaction) { if (data.message.reaction) {
window.normalizeUuids( strictAssert(
data.message.reaction, data.message.reaction.targetAuthorUuid,
['targetAuthorUuid'], 'Reaction without targetAuthorUuid'
'background::onSentMessage' );
const targetAuthorUuid = normalizeUuid(
data.message.reaction.targetAuthorUuid,
'DataMessage.Reaction.targetAuthorUuid'
); );
const { reaction } = data.message; const { reaction } = data.message;
@ -3228,7 +3350,7 @@ export async function startApp(): Promise<void> {
const reactionModel = Reactions.getSingleton().add({ const reactionModel = Reactions.getSingleton().add({
emoji: reaction.emoji, emoji: reaction.emoji,
remove: reaction.remove, remove: reaction.remove,
targetAuthorUuid: reaction.targetAuthorUuid, targetAuthorUuid,
targetTimestamp: reaction.targetTimestamp, targetTimestamp: reaction.targetTimestamp,
timestamp: Date.now(), timestamp: Date.now(),
fromId: window.ConversationController.getOurConversationId(), fromId: window.ConversationController.getOurConversationId(),
@ -3246,7 +3368,7 @@ export async function startApp(): Promise<void> {
window.log.info('Queuing sent DOE for', del.targetSentTimestamp); window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
const deleteModel = Deletes.getSingleton().add({ const deleteModel = Deletes.getSingleton().add({
targetSentTimestamp: del.targetSentTimestamp, targetSentTimestamp: del.targetSentTimestamp,
serverTimestamp: del.serverTimestamp, serverTimestamp: data.serverTimestamp,
fromId: window.ConversationController.getOurConversationId(), fromId: window.ConversationController.getOurConversationId(),
}); });
// Note: We do not wait for completion here // Note: We do not wait for completion here
@ -3274,14 +3396,14 @@ export async function startApp(): Promise<void> {
}; };
function initIncomingMessage( function initIncomingMessage(
data: WhatIsThis, data: MessageEventData,
descriptor: MessageDescriptor descriptor: MessageDescriptor
) { ) {
assert( assert(
Boolean(data.receivedAtCounter), Boolean(data.receivedAtCounter),
`Did not receive receivedAtCounter for message: ${data.timestamp}` `Did not receive receivedAtCounter for message: ${data.timestamp}`
); );
return new window.Whisper.Message({ return new window.Whisper.Message(({
source: data.source, source: data.source,
sourceUuid: data.sourceUuid, sourceUuid: data.sourceUuid,
sourceDevice: data.sourceDevice, sourceDevice: data.sourceDevice,
@ -3293,13 +3415,14 @@ export async function startApp(): Promise<void> {
conversationId: descriptor.id, conversationId: descriptor.id,
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
type: 'incoming', type: 'incoming',
unread: 1, unread: true,
} as WhatIsThis); timestamp: data.timestamp,
} as Partial<MessageAttributesType>) as WhatIsThis);
} }
// Returns `false` if this message isn't a group call message. // Returns `false` if this message isn't a group call message.
function handleGroupCallUpdateMessage( function handleGroupCallUpdateMessage(
message: DataMessageClass, message: ProcessedDataMessage,
messageDescriptor: MessageDescriptor messageDescriptor: MessageDescriptor
): boolean { ): boolean {
if (message.groupCallUpdate) { if (message.groupCallUpdate) {
@ -3329,7 +3452,7 @@ export async function startApp(): Promise<void> {
if (messageReceiver) { if (messageReceiver) {
messageReceiver.unregisterBatchers(); messageReceiver.unregisterBatchers();
messageReceiver = null; messageReceiver = undefined;
} }
onEmpty(); onEmpty();
@ -3399,7 +3522,7 @@ export async function startApp(): Promise<void> {
} }
} }
function onError(ev: WhatIsThis) { function onError(ev: ErrorEvent) {
const { error } = ev; const { error } = ev;
window.log.error('background onError:', Errors.toLogFormat(error)); window.log.error('background onError:', Errors.toLogFormat(error));
@ -3436,10 +3559,6 @@ export async function startApp(): Promise<void> {
window.log.warn('background onError: Doing nothing with incoming error'); window.log.warn('background onError: Doing nothing with incoming error');
} }
type RetryRequestEventType = Event & {
retryRequest: RetryRequestType;
};
function isInList( function isInList(
conversation: ConversationModel, conversation: ConversationModel,
list: Array<string | undefined | null> | undefined list: Array<string | undefined | null> | undefined
@ -3471,7 +3590,7 @@ export async function startApp(): Promise<void> {
requesterUuid, requesterUuid,
requesterDevice, requesterDevice,
senderDevice, senderDevice,
}: RetryRequestType): Promise<void> { }: RetryRequestEventData): Promise<void> {
const ourDeviceId = parseIntOrThrow( const ourDeviceId = parseIntOrThrow(
window.textsecure.storage.user.getDeviceId(), window.textsecure.storage.user.getDeviceId(),
'archiveSessionOnMatch/getDeviceId' 'archiveSessionOnMatch/getDeviceId'
@ -3486,7 +3605,7 @@ export async function startApp(): Promise<void> {
} }
async function sendDistributionMessageOrNullMessage( async function sendDistributionMessageOrNullMessage(
options: RetryRequestType options: RetryRequestEventData
): Promise<void> { ): Promise<void> {
const { groupId, requesterUuid } = options; const { groupId, requesterUuid } = options;
let sentDistributionMessage = false; let sentDistributionMessage = false;
@ -3558,7 +3677,7 @@ export async function startApp(): Promise<void> {
} }
} }
async function onRetryRequest(event: RetryRequestEventType) { async function onRetryRequest(event: RetryRequestEvent) {
const { retryRequest } = event; const { retryRequest } = event;
const { const {
requesterDevice, requesterDevice,
@ -3637,11 +3756,7 @@ export async function startApp(): Promise<void> {
await targetMessage.resend(requesterUuid); await targetMessage.resend(requesterUuid);
} }
type DecryptionErrorEventType = Event & { async function onDecryptionError(event: DecryptionErrorEvent) {
decryptionError: DecryptionErrorType;
};
async function onDecryptionError(event: DecryptionErrorEventType) {
const { decryptionError } = event; const { decryptionError } = event;
const { senderUuid, senderDevice, timestamp } = decryptionError; const { senderUuid, senderDevice, timestamp } = decryptionError;
const logId = `${senderUuid}.${senderDevice} ${timestamp}`; const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
@ -3666,7 +3781,7 @@ export async function startApp(): Promise<void> {
window.log.info(`onDecryptionError/${logId}: ...complete`); window.log.info(`onDecryptionError/${logId}: ...complete`);
} }
async function requestResend(decryptionError: DecryptionErrorType) { async function requestResend(decryptionError: DecryptionErrorEventData) {
const { const {
cipherTextBytes, cipherTextBytes,
cipherTextType, cipherTextType,
@ -3784,7 +3899,9 @@ export async function startApp(): Promise<void> {
}); });
} }
function startAutomaticSessionReset(decryptionError: DecryptionErrorType) { function startAutomaticSessionReset(
decryptionError: DecryptionErrorEventData
) {
const { senderUuid, senderDevice, timestamp } = decryptionError; const { senderUuid, senderDevice, timestamp } = decryptionError;
const logId = `${senderUuid}.${senderDevice} ${timestamp}`; const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
@ -3818,7 +3935,7 @@ export async function startApp(): Promise<void> {
}); });
} }
async function onViewSync(ev: WhatIsThis) { async function onViewSync(ev: ViewSyncEvent) {
ev.confirm(); ev.confirm();
const { source, sourceUuid, timestamp } = ev; const { source, sourceUuid, timestamp } = ev;
@ -3833,7 +3950,7 @@ export async function startApp(): Promise<void> {
ViewSyncs.getSingleton().onSync(sync); ViewSyncs.getSingleton().onSync(sync);
} }
async function onFetchLatestSync(ev: WhatIsThis) { async function onFetchLatestSync(ev: FetchLatestEvent) {
ev.confirm(); ev.confirm();
const { eventType } = ev; const { eventType } = ev;
@ -3856,7 +3973,7 @@ export async function startApp(): Promise<void> {
} }
} }
async function onKeysSync(ev: WhatIsThis) { async function onKeysSync(ev: KeysEvent) {
ev.confirm(); ev.confirm();
const { storageServiceKey } = ev; const { storageServiceKey } = ev;
@ -3877,7 +3994,7 @@ export async function startApp(): Promise<void> {
} }
} }
async function onMessageRequestResponse(ev: WhatIsThis) { async function onMessageRequestResponse(ev: MessageRequestResponseEvent) {
ev.confirm(); ev.confirm();
const { const {
@ -3907,9 +4024,9 @@ export async function startApp(): Promise<void> {
MessageRequests.getSingleton().onResponse(sync); MessageRequests.getSingleton().onResponse(sync);
} }
function onReadReceipt(ev: WhatIsThis) { function onReadReceipt(ev: ReadEvent) {
const readAt = ev.timestamp;
const { envelopeTimestamp, timestamp, source, sourceUuid } = ev.read; const { envelopeTimestamp, timestamp, source, sourceUuid } = ev.read;
const readAt = envelopeTimestamp;
const reader = window.ConversationController.ensureContactIds({ const reader = window.ConversationController.ensureContactIds({
e164: source, e164: source,
uuid: sourceUuid, uuid: sourceUuid,
@ -3941,9 +4058,9 @@ export async function startApp(): Promise<void> {
ReadReceipts.getSingleton().onReceipt(receipt); ReadReceipts.getSingleton().onReceipt(receipt);
} }
function onReadSync(ev: WhatIsThis) { function onReadSync(ev: ReadSyncEvent) {
const readAt = ev.timestamp;
const { envelopeTimestamp, sender, senderUuid, timestamp } = ev.read; const { envelopeTimestamp, sender, senderUuid, timestamp } = ev.read;
const readAt = envelopeTimestamp;
const senderId = window.ConversationController.ensureContactIds({ const senderId = window.ConversationController.ensureContactIds({
e164: sender, e164: sender,
uuid: senderUuid, uuid: senderUuid,
@ -3974,7 +4091,7 @@ export async function startApp(): Promise<void> {
return ReadSyncs.getSingleton().onReceipt(receipt); return ReadSyncs.getSingleton().onReceipt(receipt);
} }
async function onVerified(ev: WhatIsThis) { async function onVerified(ev: VerifiedEvent) {
const e164 = ev.verified.destination; const e164 = ev.verified.destination;
const uuid = ev.verified.destinationUuid; const uuid = ev.verified.destinationUuid;
const key = ev.verified.identityKey; const key = ev.verified.identityKey;
@ -3984,18 +4101,18 @@ export async function startApp(): Promise<void> {
ev.confirm(); ev.confirm();
} }
const c = new window.Whisper.Conversation({ const c = new window.Whisper.Conversation(({
e164, e164,
uuid, uuid,
type: 'private', type: 'private',
} as WhatIsThis); } as Partial<ConversationAttributesType>) as WhatIsThis);
const error = c.validate(); const error = c.validate();
if (error) { if (error) {
window.log.error( window.log.error(
'Invalid verified sync received:', 'Invalid verified sync received:',
e164, e164,
uuid, uuid,
Errors.toLogFormat(error as WhatIsThis) Errors.toLogFormat(error)
); );
return; return;
} }
@ -4019,7 +4136,7 @@ export async function startApp(): Promise<void> {
e164, e164,
uuid, uuid,
state, state,
ev.viaContactSync ? 'via contact sync' : '' ev.verified.viaContactSync ? 'via contact sync' : ''
); );
const verifiedId = window.ConversationController.ensureContactIds({ const verifiedId = window.ConversationController.ensureContactIds({
@ -4031,7 +4148,7 @@ export async function startApp(): Promise<void> {
const contact = window.ConversationController.get(verifiedId)!; const contact = window.ConversationController.get(verifiedId)!;
const options = { const options = {
viaSyncMessage: true, viaSyncMessage: true,
viaContactSync: ev.viaContactSync, viaContactSync: ev.verified.viaContactSync,
key, key,
}; };
@ -4044,7 +4161,7 @@ export async function startApp(): Promise<void> {
} }
} }
function onDeliveryReceipt(ev: WhatIsThis) { function onDeliveryReceipt(ev: DeliveryEvent) {
const { deliveryReceipt } = ev; const { deliveryReceipt } = ev;
const { const {
envelopeTimestamp, envelopeTimestamp,

View file

@ -8,6 +8,7 @@ import { storiesOf } from '@storybook/react';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { GroupV2ChangeType } from '../../groups'; import { GroupV2ChangeType } from '../../groups';
import { SignalService as Proto } from '../../protobuf';
import { SmartContactRendererType } from '../../groupChange'; import { SmartContactRendererType } from '../../groupChange';
import { GroupV2Change } from './GroupV2Change'; import { GroupV2Change } from './GroupV2Change';
@ -20,25 +21,8 @@ const CONTACT_C = 'CONTACT_C';
const ADMIN_A = 'ADMIN_A'; const ADMIN_A = 'ADMIN_A';
const INVITEE_A = 'INVITEE_A'; const INVITEE_A = 'INVITEE_A';
class AccessControlEnum { const AccessControlEnum = Proto.AccessControl.AccessRequired;
static UNKNOWN = 0; const RoleEnum = Proto.Member.Role;
static ANY = 1;
static MEMBER = 2;
static ADMINISTRATOR = 3;
static UNSATISFIABLE = 4;
}
class RoleEnum {
static UNKNOWN = 0;
static ADMINISTRATOR = 1;
static DEFAULT = 2;
}
const renderContact: SmartContactRendererType = (conversationId: string) => ( const renderContact: SmartContactRendererType = (conversationId: string) => (
<React.Fragment key={conversationId}> <React.Fragment key={conversationId}>
@ -48,13 +32,11 @@ const renderContact: SmartContactRendererType = (conversationId: string) => (
const renderChange = (change: GroupV2ChangeType, groupName?: string) => ( const renderChange = (change: GroupV2ChangeType, groupName?: string) => (
<GroupV2Change <GroupV2Change
AccessControlEnum={AccessControlEnum}
change={change} change={change}
groupName={groupName} groupName={groupName}
i18n={i18n} i18n={i18n}
ourConversationId={OUR_ID} ourConversationId={OUR_ID}
renderContact={renderContact} renderContact={renderContact}
RoleEnum={RoleEnum}
/> />
); );

View file

@ -14,14 +14,10 @@ import { GroupV2ChangeType, GroupV2DescriptionChangeType } from '../../groups';
import { renderChange, SmartContactRendererType } from '../../groupChange'; import { renderChange, SmartContactRendererType } from '../../groupChange';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { AccessControlClass, MemberClass } from '../../textsecure.d';
export type PropsDataType = { export type PropsDataType = {
groupName?: string; groupName?: string;
ourConversationId: string; ourConversationId: string;
change: GroupV2ChangeType; change: GroupV2ChangeType;
AccessControlEnum: typeof AccessControlClass.AccessRequired;
RoleEnum: typeof MemberClass.Role;
}; };
export type PropsHousekeepingType = { export type PropsHousekeepingType = {
@ -40,15 +36,7 @@ function renderStringToIntl(
} }
export function GroupV2Change(props: PropsType): ReactElement { export function GroupV2Change(props: PropsType): ReactElement {
const { const { change, groupName, i18n, ourConversationId, renderContact } = props;
AccessControlEnum,
change,
groupName,
i18n,
ourConversationId,
renderContact,
RoleEnum,
} = props;
const [ const [
isGroupDescriptionDialogOpen, isGroupDescriptionDialogOpen,
@ -64,12 +52,10 @@ export function GroupV2Change(props: PropsType): ReactElement {
<div className="module-group-v2-change"> <div className="module-group-v2-change">
<div className="module-group-v2-change--icon" /> <div className="module-group-v2-change--icon" />
{renderChange(change, { {renderChange(change, {
AccessControlEnum,
i18n, i18n,
ourConversationId, ourConversationId,
renderContact, renderContact,
renderString: renderStringToIntl, renderString: renderStringToIntl,
RoleEnum,
}).map((item: FullJSXType, index: number) => ( }).map((item: FullJSXType, index: number) => (
// Difficult to find a unique key for this type // Difficult to find a unique key for this type
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key

View file

@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions';
import { setup as setupI18n } from '../../../../js/modules/i18n'; import { setup as setupI18n } from '../../../../js/modules/i18n';
import enMessages from '../../../../_locales/en/messages.json'; import enMessages from '../../../../_locales/en/messages.json';
import { GroupLinkManagement, PropsType } from './GroupLinkManagement'; import { GroupLinkManagement, PropsType } from './GroupLinkManagement';
import { SignalService as Proto } from '../../../protobuf';
import { ConversationType } from '../../../state/ducks/conversations'; import { ConversationType } from '../../../state/ducks/conversations';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
@ -19,17 +20,7 @@ const story = storiesOf(
module module
); );
class AccessEnum { const AccessControlEnum = Proto.AccessControl.AccessRequired;
static ANY = 0;
static UNKNOWN = 1;
static MEMBER = 2;
static ADMINISTRATOR = 3;
static UNSATISFIABLE = 4;
}
function getConversation( function getConversation(
groupLink?: string, groupLink?: string,
@ -47,7 +38,7 @@ function getConversation(
accessControlAddFromInviteLink: accessControlAddFromInviteLink:
accessControlAddFromInviteLink !== undefined accessControlAddFromInviteLink !== undefined
? accessControlAddFromInviteLink ? accessControlAddFromInviteLink
: AccessEnum.UNSATISFIABLE, : AccessControlEnum.UNSATISFIABLE,
}); });
} }
@ -55,7 +46,6 @@ const createProps = (
conversation?: ConversationType, conversation?: ConversationType,
isAdmin = false isAdmin = false
): PropsType => ({ ): PropsType => ({
accessEnum: AccessEnum,
changeHasGroupLink: action('changeHasGroupLink'), changeHasGroupLink: action('changeHasGroupLink'),
conversation: conversation || getConversation(), conversation: conversation || getConversation(),
copyGroupLink: action('copyGroupLink'), copyGroupLink: action('copyGroupLink'),
@ -75,7 +65,7 @@ story.add('Off (Admin)', () => {
story.add('On (Admin)', () => { story.add('On (Admin)', () => {
const props = createProps( const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ANY), getConversation('https://signal.group/1', AccessControlEnum.ANY),
true true
); );
@ -84,7 +74,7 @@ story.add('On (Admin)', () => {
story.add('On (Admin + Admin Approval Needed)', () => { story.add('On (Admin + Admin Approval Needed)', () => {
const props = createProps( const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR), getConversation('https://signal.group/1', AccessControlEnum.ADMINISTRATOR),
true true
); );
@ -93,7 +83,7 @@ story.add('On (Admin + Admin Approval Needed)', () => {
story.add('On (Non-admin)', () => { story.add('On (Non-admin)', () => {
const props = createProps( const props = createProps(
getConversation('https://signal.group/1', AccessEnum.ANY) getConversation('https://signal.group/1', AccessControlEnum.ANY)
); );
return <GroupLinkManagement {...props} />; return <GroupLinkManagement {...props} />;

View file

@ -4,15 +4,16 @@
import React from 'react'; import React from 'react';
import { ConversationDetailsIcon } from './ConversationDetailsIcon'; import { ConversationDetailsIcon } from './ConversationDetailsIcon';
import { SignalService as Proto } from '../../../protobuf';
import { ConversationType } from '../../../state/ducks/conversations'; import { ConversationType } from '../../../state/ducks/conversations';
import { LocalizerType } from '../../../types/Util'; import { LocalizerType } from '../../../types/Util';
import { PanelRow } from './PanelRow'; import { PanelRow } from './PanelRow';
import { PanelSection } from './PanelSection'; import { PanelSection } from './PanelSection';
import { AccessControlClass } from '../../../textsecure.d';
import { Select } from '../../Select'; import { Select } from '../../Select';
const AccessControlEnum = Proto.AccessControl.AccessRequired;
export type PropsType = { export type PropsType = {
accessEnum: typeof AccessControlClass.AccessRequired;
changeHasGroupLink: (value: boolean) => void; changeHasGroupLink: (value: boolean) => void;
conversation?: ConversationType; conversation?: ConversationType;
copyGroupLink: (groupLink: string) => void; copyGroupLink: (groupLink: string) => void;
@ -23,7 +24,6 @@ export type PropsType = {
}; };
export const GroupLinkManagement: React.ComponentType<PropsType> = ({ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
accessEnum,
changeHasGroupLink, changeHasGroupLink,
conversation, conversation,
copyGroupLink, copyGroupLink,
@ -43,11 +43,13 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
}; };
const membersNeedAdminApproval = const membersNeedAdminApproval =
conversation.accessControlAddFromInviteLink === accessEnum.ADMINISTRATOR; conversation.accessControlAddFromInviteLink ===
AccessControlEnum.ADMINISTRATOR;
const hasGroupLink = const hasGroupLink =
conversation.groupLink && conversation.groupLink &&
conversation.accessControlAddFromInviteLink !== accessEnum.UNSATISFIABLE; conversation.accessControlAddFromInviteLink !==
AccessControlEnum.UNSATISFIABLE;
const groupLinkInfo = hasGroupLink ? conversation.groupLink : ''; const groupLinkInfo = hasGroupLink ? conversation.groupLink : '';
return ( return (

View file

@ -29,20 +29,7 @@ const conversation: ConversationType = getDefaultConversation({
sharedGroupNames: [], sharedGroupNames: [],
}); });
class AccessEnum {
static ANY = 0;
static UNKNOWN = 1;
static MEMBER = 2;
static ADMINISTRATOR = 3;
static UNSATISFIABLE = 4;
}
const createProps = (): PropsType => ({ const createProps = (): PropsType => ({
accessEnum: AccessEnum,
conversation, conversation,
i18n, i18n,
setAccessControlAttributesSetting: action( setAccessControlAttributesSetting: action(

View file

@ -6,14 +6,12 @@ import React from 'react';
import { ConversationType } from '../../../state/ducks/conversations'; import { ConversationType } from '../../../state/ducks/conversations';
import { LocalizerType } from '../../../types/Util'; import { LocalizerType } from '../../../types/Util';
import { getAccessControlOptions } from '../../../util/getAccessControlOptions'; import { getAccessControlOptions } from '../../../util/getAccessControlOptions';
import { AccessControlClass } from '../../../textsecure.d';
import { PanelRow } from './PanelRow'; import { PanelRow } from './PanelRow';
import { PanelSection } from './PanelSection'; import { PanelSection } from './PanelSection';
import { Select } from '../../Select'; import { Select } from '../../Select';
export type PropsType = { export type PropsType = {
accessEnum: typeof AccessControlClass.AccessRequired;
conversation?: ConversationType; conversation?: ConversationType;
i18n: LocalizerType; i18n: LocalizerType;
setAccessControlAttributesSetting: (value: number) => void; setAccessControlAttributesSetting: (value: number) => void;
@ -21,7 +19,6 @@ export type PropsType = {
}; };
export const GroupV2Permissions: React.ComponentType<PropsType> = ({ export const GroupV2Permissions: React.ComponentType<PropsType> = ({
accessEnum,
conversation, conversation,
i18n, i18n,
setAccessControlAttributesSetting, setAccessControlAttributesSetting,
@ -37,7 +34,7 @@ export const GroupV2Permissions: React.ComponentType<PropsType> = ({
const updateAccessControlMembers = (value: string) => { const updateAccessControlMembers = (value: string) => {
setAccessControlMembersSetting(Number(value)); setAccessControlMembersSetting(Number(value));
}; };
const accessControlOptions = getAccessControlOptions(accessEnum, i18n); const accessControlOptions = getAccessControlOptions(i18n);
return ( return (
<PanelSection> <PanelSection>

View file

@ -6,8 +6,8 @@ import { LocalizerType } from './types/Util';
import { ReplacementValuesType } from './types/I18N'; import { ReplacementValuesType } from './types/I18N';
import { missingCaseError } from './util/missingCaseError'; import { missingCaseError } from './util/missingCaseError';
import { AccessControlClass, MemberClass } from './textsecure.d';
import { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups'; import { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups';
import { SignalService as Proto } from './protobuf';
export type SmartContactRendererType = (conversationId: string) => FullJSXType; export type SmartContactRendererType = (conversationId: string) => FullJSXType;
export type StringRendererType = ( export type StringRendererType = (
@ -17,15 +17,16 @@ export type StringRendererType = (
) => FullJSXType; ) => FullJSXType;
export type RenderOptionsType = { export type RenderOptionsType = {
AccessControlEnum: typeof AccessControlClass.AccessRequired;
from?: string; from?: string;
i18n: LocalizerType; i18n: LocalizerType;
ourConversationId: string; ourConversationId: string;
renderContact: SmartContactRendererType; renderContact: SmartContactRendererType;
renderString: StringRendererType; renderString: StringRendererType;
RoleEnum: typeof MemberClass.Role;
}; };
const AccessControlEnum = Proto.AccessControl.AccessRequired;
const RoleEnum = Proto.Member.Role;
export function renderChange( export function renderChange(
change: GroupV2ChangeType, change: GroupV2ChangeType,
options: RenderOptionsType options: RenderOptionsType
@ -45,13 +46,11 @@ export function renderChangeDetail(
options: RenderOptionsType options: RenderOptionsType
): FullJSXType { ): FullJSXType {
const { const {
AccessControlEnum,
from, from,
i18n, i18n,
ourConversationId, ourConversationId,
renderContact, renderContact,
renderString, renderString,
RoleEnum,
} = options; } = options;
const fromYou = Boolean(from && from === ourConversationId); const fromYou = Boolean(from && from === ourConversationId);

View file

@ -1370,6 +1370,13 @@ export function idForLogging(groupId: string | undefined): string {
} }
export function deriveGroupFields(masterKey: Uint8Array): GroupFields { export function deriveGroupFields(masterKey: Uint8Array): GroupFields {
if (masterKey.length !== MASTER_KEY_LENGTH) {
throw new Error(
`deriveGroupFields: masterKey had length ${masterKey.length}, ` +
`expected ${MASTER_KEY_LENGTH}`
);
}
const cacheKey = Bytes.toBase64(masterKey); const cacheKey = Bytes.toBase64(masterKey);
const cached = groupFieldsCache.get(cacheKey); const cached = groupFieldsCache.get(cacheKey);
if (cached) { if (cached) {

4
ts/model-types.d.ts vendored
View file

@ -58,7 +58,7 @@ export type QuotedMessageType = {
author?: string; author?: string;
authorUuid?: string; authorUuid?: string;
bodyRanges?: BodyRangesType; bodyRanges?: BodyRangesType;
id: string; id: number;
referencedMessageNotFound: boolean; referencedMessageNotFound: boolean;
isViewOnce: boolean; isViewOnce: boolean;
text?: string; text?: string;
@ -190,6 +190,8 @@ export type MessageAttributesType = {
// Backwards-compatibility with prerelease data schema // Backwards-compatibility with prerelease data schema
invitedGV2Members?: Array<GroupV2PendingMemberType>; invitedGV2Members?: Array<GroupV2PendingMemberType>;
droppedGV2MemberIds?: Array<string>; droppedGV2MemberIds?: Array<string>;
sendHQImages?: boolean;
}; };
export type ConversationAttributesTypeType = 'private' | 'group'; export type ConversationAttributesTypeType = 'private' | 'group';

View file

@ -14,7 +14,9 @@ import {
VerificationOptions, VerificationOptions,
WhatIsThis, WhatIsThis,
} from '../model-types.d'; } from '../model-types.d';
import { AttachmentType } from '../types/Attachment';
import { CallMode, CallHistoryDetailsType } from '../types/Calling'; import { CallMode, CallHistoryDetailsType } from '../types/Calling';
import * as Stickers from '../types/Stickers';
import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage'; import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage';
import { ConversationType } from '../state/ducks/conversations'; import { ConversationType } from '../state/ducks/conversations';
import { import {
@ -3100,7 +3102,7 @@ export class ConversationModel extends window.Backbone
? [{ contentType: 'image/jpeg', fileName: null }] ? [{ contentType: 'image/jpeg', fileName: null }]
: await this.getQuoteAttachment(attachments, preview, sticker), : await this.getQuoteAttachment(attachments, preview, sticker),
bodyRanges: quotedMessage.get('bodyRanges'), bodyRanges: quotedMessage.get('bodyRanges'),
id: String(quotedMessage.get('sent_at')), id: quotedMessage.get('sent_at'),
isViewOnce: isTapToView(quotedMessage.attributes), isViewOnce: isTapToView(quotedMessage.attributes),
messageId: quotedMessage.get('id'), messageId: quotedMessage.get('id'),
referencedMessageNotFound: false, referencedMessageNotFound: false,
@ -3109,8 +3111,8 @@ export class ConversationModel extends window.Backbone
} }
async sendStickerMessage(packId: string, stickerId: number): Promise<void> { async sendStickerMessage(packId: string, stickerId: number): Promise<void> {
const packData = window.Signal.Stickers.getStickerPack(packId); const packData = Stickers.getStickerPack(packId);
const stickerData = window.Signal.Stickers.getSticker(packId, stickerId); const stickerData = Stickers.getSticker(packId, stickerId);
if (!stickerData || !packData) { if (!stickerData || !packData) {
window.log.warn( window.log.warn(
`Attempted to send nonexistent (${packId}, ${stickerId}) sticker!` `Attempted to send nonexistent (${packId}, ${stickerId}) sticker!`
@ -3152,7 +3154,7 @@ export class ConversationModel extends window.Backbone
}, },
}; };
this.sendMessage(null, [], null, [], sticker); this.sendMessage(undefined, [], undefined, [], sticker);
window.reduxActions.stickers.useSticker(packId, stickerId); window.reduxActions.stickers.useSticker(packId, stickerId);
} }
@ -3451,10 +3453,10 @@ export class ConversationModel extends window.Backbone
} }
sendMessage( sendMessage(
body: string | null, body: string | undefined,
attachments: Array<WhatIsThis>, attachments: Array<AttachmentType>,
quote: WhatIsThis, quote?: QuotedMessageType,
preview: WhatIsThis, preview?: WhatIsThis,
sticker?: WhatIsThis, sticker?: WhatIsThis,
mentions?: BodyRangesType, mentions?: BodyRangesType,
{ {
@ -3503,6 +3505,7 @@ export class ConversationModel extends window.Backbone
// Here we move attachments to disk // Here we move attachments to disk
const messageWithSchema = await upgradeMessageSchema({ const messageWithSchema = await upgradeMessageSchema({
timestamp: now,
type: 'outgoing', type: 'outgoing',
body, body,
conversationId: this.id, conversationId: this.id,
@ -3575,7 +3578,7 @@ export class ConversationModel extends window.Backbone
} }
const attachmentsWithData = await Promise.all( const attachmentsWithData = await Promise.all(
messageWithSchema.attachments.map(loadAttachmentData) messageWithSchema.attachments?.map(loadAttachmentData) ?? []
); );
const { const {
@ -5086,7 +5089,7 @@ export class ConversationModel extends window.Backbone
isTyping: boolean; isTyping: boolean;
senderId: string; senderId: string;
fromMe: boolean; fromMe: boolean;
senderDevice: string; senderDevice: number;
}): void { }): void {
const { isTyping, senderId, fromMe, senderDevice } = options; const { isTyping, senderId, fromMe, senderDevice } = options;

View file

@ -12,9 +12,10 @@ import {
QuotedMessageType, QuotedMessageType,
WhatIsThis, WhatIsThis,
} from '../model-types.d'; } from '../model-types.d';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { map, filter, find } from '../util/iterables'; import { map, filter, find } from '../util/iterables';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { DataMessageClass } from '../textsecure.d';
import { ConversationModel } from './conversations'; import { ConversationModel } from './conversations';
import { MessageStatusType } from '../components/conversation/Message'; import { MessageStatusType } from '../components/conversation/Message';
import { import {
@ -23,9 +24,17 @@ import {
} from '../state/smart/MessageDetail'; } from '../state/smart/MessageDetail';
import { getCallingNotificationText } from '../util/callingNotification'; import { getCallingNotificationText } from '../util/callingNotification';
import { CallbackResultType } from '../textsecure/SendMessage'; import { CallbackResultType } from '../textsecure/SendMessage';
import { ProcessedDataMessage, ProcessedQuote } from '../textsecure/Types.d';
import * as expirationTimer from '../util/expirationTimer'; import * as expirationTimer from '../util/expirationTimer';
import { ReactionType } from '../types/Reactions'; import { ReactionType } from '../types/Reactions';
import {
copyStickerToAttachments,
deletePackReference,
savePackMetadata,
getStickerPackStatus,
} from '../types/Stickers';
import * as Stickers from '../types/Stickers';
import { AttachmentType, isImage, isVideo } from '../types/Attachment'; import { AttachmentType, isImage, isVideo } from '../types/Attachment';
import { MIMEType, IMAGE_WEBP } from '../types/MIME'; import { MIMEType, IMAGE_WEBP } from '../types/MIME';
import { ourProfileKeyService } from '../services/ourProfileKey'; import { ourProfileKeyService } from '../services/ourProfileKey';
@ -107,12 +116,6 @@ const {
loadStickerData, loadStickerData,
upgradeMessageSchema, upgradeMessageSchema,
} = window.Signal.Migrations; } = window.Signal.Migrations;
const {
copyStickerToAttachments,
deletePackReference,
savePackMetadata,
getStickerPackStatus,
} = window.Signal.Stickers;
const { getTextWithMentions, GoogleChrome } = window.Signal.Util; const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data; const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
@ -124,7 +127,7 @@ const includesAny = <T>(haystack: Array<T>, ...needles: Array<T>) =>
export function isQuoteAMatch( export function isQuoteAMatch(
message: MessageModel | null | undefined, message: MessageModel | null | undefined,
conversationId: string, conversationId: string,
quote: QuotedMessageType | DataMessageClass.Quote quote: QuotedMessageType
): message is MessageModel { ): message is MessageModel {
if (!message) { if (!message) {
return false; return false;
@ -614,7 +617,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const stickerData = this.get('sticker'); const stickerData = this.get('sticker');
if (stickerData) { if (stickerData) {
const sticker = window.Signal.Stickers.getSticker( const sticker = Stickers.getSticker(
stickerData.packId, stickerData.packId,
stickerData.stickerId stickerData.stickerId
); );
@ -624,7 +627,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
return { return {
text: window.i18n('message--getNotificationText--stickers'), text: window.i18n('message--getNotificationText--stickers'),
emoji, emoji: dropNull(emoji),
}; };
} }
@ -1460,11 +1463,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
senderKeyInfo.distributionId senderKeyInfo.distributionId
); );
contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap( contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
window.Signal.Crypto.typedArrayToArrayBuffer(
senderKeyDistributionMessage.serialize()
)
);
} }
} }
@ -2217,18 +2216,48 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
async copyFromQuotedMessage( async copyFromQuotedMessage(
message: DataMessageClass, quote: ProcessedQuote | undefined,
conversationId: string conversationId: string
): Promise<DataMessageClass> { ): Promise<QuotedMessageType | undefined> {
const { quote } = message;
if (!quote) { if (!quote) {
return message; return undefined;
} }
const { id } = quote; const { id } = quote;
strictAssert(id, 'Quote must have an id');
const result: QuotedMessageType = {
...quote,
id,
attachments: quote.attachments.slice(),
bodyRanges: quote.bodyRanges.map(({ start, length, mentionUuid }) => {
strictAssert(
start !== undefined && start !== null,
'Received quote with a bodyRange.start == null'
);
strictAssert(
length !== undefined && length !== null,
'Received quote with a bodyRange.length == null'
);
return {
start,
length,
mentionUuid: dropNull(mentionUuid),
};
}),
// Just placeholder values for the fields
referencedMessageNotFound: false,
isViewOnce: false,
messageId: '',
};
const inMemoryMessages = window.MessageController.filterBySentAt(id); const inMemoryMessages = window.MessageController.filterBySentAt(id);
const matchingMessage = find(inMemoryMessages, item => const matchingMessage = find(inMemoryMessages, item =>
isQuoteAMatch(item, conversationId, quote) isQuoteAMatch(item, conversationId, result)
); );
let queryMessage: undefined | MessageModel; let queryMessage: undefined | MessageModel;
@ -2241,35 +2270,35 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
MessageCollection: window.Whisper.MessageCollection, MessageCollection: window.Whisper.MessageCollection,
}); });
const found = collection.find(item => const found = collection.find(item =>
isQuoteAMatch(item, conversationId, quote) isQuoteAMatch(item, conversationId, result)
); );
if (!found) { if (!found) {
quote.referencedMessageNotFound = true; result.referencedMessageNotFound = true;
return message; return result;
} }
queryMessage = window.MessageController.register(found.id, found); queryMessage = window.MessageController.register(found.id, found);
} }
if (queryMessage) { if (queryMessage) {
await this.copyQuoteContentFromOriginal(queryMessage, quote); await this.copyQuoteContentFromOriginal(queryMessage, result);
} }
return message; return result;
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
async copyQuoteContentFromOriginal( async copyQuoteContentFromOriginal(
originalMessage: MessageModel, originalMessage: MessageModel,
quote: QuotedMessageType | DataMessageClass.Quote quote: QuotedMessageType
): Promise<void> { ): Promise<void> {
const { attachments } = quote; const { attachments } = quote;
const firstAttachment = attachments ? attachments[0] : undefined; const firstAttachment = attachments ? attachments[0] : undefined;
if (isTapToView(originalMessage.attributes)) { if (isTapToView(originalMessage.attributes)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
quote.text = null; quote.text = undefined;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
quote.attachments = [ quote.attachments = [
{ {
@ -2362,7 +2391,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
handleDataMessage( handleDataMessage(
initialMessage: DataMessageClass, initialMessage: ProcessedDataMessage,
confirm: () => void, confirm: () => void,
options: { data?: typeof window.WhatIsThis } = {} options: { data?: typeof window.WhatIsThis } = {}
): WhatIsThis { ): WhatIsThis {
@ -2631,16 +2660,19 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}); });
} }
const withQuoteReference = await this.copyFromQuotedMessage( const withQuoteReference = {
initialMessage, ...initialMessage,
conversation.id quote: await this.copyFromQuotedMessage(
); initialMessage.quote,
conversation.id
),
};
const dataMessage = await upgradeMessageSchema(withQuoteReference); const dataMessage = await upgradeMessageSchema(withQuoteReference);
try { try {
const now = new Date().getTime(); const now = new Date().getTime();
const urls = LinkPreview.findLinks(dataMessage.body); const urls = LinkPreview.findLinks(dataMessage.body || '');
const incomingPreview = dataMessage.preview || []; const incomingPreview = dataMessage.preview || [];
const preview = incomingPreview.filter( const preview = incomingPreview.filter(
(item: typeof window.WhatIsThis) => (item: typeof window.WhatIsThis) =>

View file

@ -22,7 +22,6 @@ import {
GumVideoCapturer, GumVideoCapturer,
HangupMessage, HangupMessage,
HangupType, HangupType,
OfferType,
OpaqueMessage, OpaqueMessage,
PeekInfo, PeekInfo,
RingRTC, RingRTC,
@ -38,7 +37,6 @@ import {
GroupCallPeekInfoType, GroupCallPeekInfoType,
} from '../state/ducks/calling'; } from '../state/ducks/calling';
import { getConversationCallMode } from '../state/ducks/conversations'; import { getConversationCallMode } from '../state/ducks/conversations';
import { EnvelopeClass } from '../textsecure.d';
import { import {
CallMode, CallMode,
AudioDevice, AudioDevice,
@ -57,12 +55,14 @@ import {
typedArrayToArrayBuffer, typedArrayToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { dropNull, shallowDropNull } from '../util/dropNull';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import { import {
fetchMembershipProof, fetchMembershipProof,
getMembershipList, getMembershipList,
wrapWithSyncMessageSend, wrapWithSyncMessageSend,
} from '../groups'; } from '../groups';
import { ProcessedEnvelope } from '../textsecure/Types.d';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp'; import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp';
import { import {
@ -74,6 +74,9 @@ import { notify } from './notify';
import { getSendOptions } from '../util/getSendOptions'; import { getSendOptions } from '../util/getSendOptions';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map< const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map<
HttpMethod, HttpMethod,
'GET' | 'PUT' | 'POST' | 'DELETE' 'GET' | 'PUT' | 'POST' | 'DELETE'
@ -121,6 +124,135 @@ function translateSourceName(
return name; return name;
} }
function protoToCallingMessage({
offer,
answer,
iceCandidates,
legacyHangup,
busy,
hangup,
supportsMultiRing,
destinationDeviceId,
opaque,
}: Proto.ICallingMessage): CallingMessage {
return {
offer: offer
? {
...shallowDropNull(offer),
type: dropNull(offer.type) as number,
opaque: offer.opaque ? Buffer.from(offer.opaque) : undefined,
}
: undefined,
answer: answer
? {
...shallowDropNull(answer),
opaque: answer.opaque ? Buffer.from(answer.opaque) : undefined,
}
: undefined,
iceCandidates: iceCandidates
? iceCandidates.map(candidate => {
return {
...shallowDropNull(candidate),
opaque: candidate.opaque
? Buffer.from(candidate.opaque)
: undefined,
};
})
: undefined,
legacyHangup: legacyHangup
? {
...shallowDropNull(legacyHangup),
type: dropNull(legacyHangup.type) as number,
}
: undefined,
busy: shallowDropNull(busy),
hangup: hangup
? {
...shallowDropNull(hangup),
type: dropNull(hangup.type) as number,
}
: undefined,
supportsMultiRing: dropNull(supportsMultiRing),
destinationDeviceId: dropNull(destinationDeviceId),
opaque: opaque
? {
data: opaque.data ? Buffer.from(opaque.data) : undefined,
}
: undefined,
};
}
function bufferToProto(
value: Buffer | { toArrayBuffer(): ArrayBuffer } | undefined
): Uint8Array | undefined {
if (!value) {
return undefined;
}
if (value instanceof Uint8Array) {
return value;
}
return new FIXMEU8(value.toArrayBuffer());
}
function callingMessageToProto({
offer,
answer,
iceCandidates,
legacyHangup,
busy,
hangup,
supportsMultiRing,
destinationDeviceId,
opaque,
}: CallingMessage): Proto.ICallingMessage {
return {
offer: offer
? {
...offer,
type: offer.type as number,
opaque: bufferToProto(offer.opaque),
}
: undefined,
answer: answer
? {
...answer,
opaque: bufferToProto(answer.opaque),
}
: undefined,
iceCandidates: iceCandidates
? iceCandidates.map(candidate => {
return {
...candidate,
opaque: bufferToProto(candidate.opaque),
};
})
: undefined,
legacyHangup: legacyHangup
? {
...legacyHangup,
type: legacyHangup.type as number,
}
: undefined,
busy,
hangup: hangup
? {
...hangup,
type: hangup.type as number,
}
: undefined,
supportsMultiRing,
destinationDeviceId,
opaque: opaque
? {
...opaque,
data: bufferToProto(opaque.data),
}
: undefined,
};
}
export class CallingClass { export class CallingClass {
readonly videoCapturer: GumVideoCapturer; readonly videoCapturer: GumVideoCapturer;
@ -1231,8 +1363,8 @@ export class CallingClass {
} }
async handleCallingMessage( async handleCallingMessage(
envelope: EnvelopeClass, envelope: ProcessedEnvelope,
callingMessage: CallingMessage callingMessage: Proto.ICallingMessage
): Promise<void> { ): Promise<void> {
window.log.info('CallingClass.handleCallingMessage()'); window.log.info('CallingClass.handleCallingMessage()');
@ -1298,9 +1430,10 @@ export class CallingClass {
await this.handleOutgoingSignaling(remoteUserId, message); await this.handleOutgoingSignaling(remoteUserId, message);
const ProtoOfferType = Proto.CallingMessage.Offer.Type;
this.addCallHistoryForFailedIncomingCall( this.addCallHistoryForFailedIncomingCall(
conversation, conversation,
callingMessage.offer.type === OfferType.VideoCall, callingMessage.offer.type === ProtoOfferType.OFFER_VIDEO_CALL,
envelope.timestamp envelope.timestamp
); );
@ -1321,7 +1454,7 @@ export class CallingClass {
remoteDeviceId, remoteDeviceId,
this.localDeviceId, this.localDeviceId,
messageAgeSec, messageAgeSec,
callingMessage, protoToCallingMessage(callingMessage),
Buffer.from(senderIdentityKey), Buffer.from(senderIdentityKey),
Buffer.from(receiverIdentityKey) Buffer.from(receiverIdentityKey)
); );
@ -1428,7 +1561,7 @@ export class CallingClass {
try { try {
await window.textsecure.messaging.sendCallingMessage( await window.textsecure.messaging.sendCallingMessage(
remoteUserId, remoteUserId,
message, callingMessageToProto(message),
sendOptions sendOptions
); );

View file

@ -24,6 +24,7 @@ import {
waitThenRespondToGroupV2Migration, waitThenRespondToGroupV2Migration,
} from '../groups'; } from '../groups';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { normalizeUuid } from '../util/normalizeUuid';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { import {
PhoneNumberSharingMode, PhoneNumberSharingMode,
@ -719,13 +720,18 @@ export async function mergeGroupV2Record(
export async function mergeContactRecord( export async function mergeContactRecord(
storageID: string, storageID: string,
contactRecord: ContactRecordClass originalContactRecord: ContactRecordClass
): Promise<boolean> { ): Promise<boolean> {
window.normalizeUuids( const contactRecord = {
contactRecord, ...originalContactRecord,
['serviceUuid'],
'storageService.mergeContactRecord' serviceUuid: originalContactRecord.serviceUuid
); ? normalizeUuid(
originalContactRecord.serviceUuid,
'ContactRecord.serviceUuid'
)
: undefined,
};
const e164 = contactRecord.serviceE164 || undefined; const e164 = contactRecord.serviceE164 || undefined;
const uuid = contactRecord.serviceUuid || undefined; const uuid = contactRecord.serviceUuid || undefined;

View file

@ -4,19 +4,19 @@
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable camelcase */ /* eslint-disable camelcase */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { import type {
ConversationAttributesType, ConversationAttributesType,
ConversationModelCollectionType, ConversationModelCollectionType,
MessageAttributesType, MessageAttributesType,
MessageModelCollectionType, MessageModelCollectionType,
} from '../model-types.d'; } from '../model-types.d';
import { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import { StoredJob } from '../jobs/types'; import type { StoredJob } from '../jobs/types';
import { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import { ConversationColorType, CustomColorType } from '../types/Colors'; import type { ConversationColorType, CustomColorType } from '../types/Colors';
import { StorageAccessType } from '../types/Storage.d'; import { StorageAccessType } from '../types/Storage.d';
import { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
export type AttachmentDownloadJobTypeType = export type AttachmentDownloadJobTypeType =
| 'long-message' | 'long-message'
@ -111,41 +111,48 @@ export type SignedPreKeyType = {
privateKey: ArrayBuffer; privateKey: ArrayBuffer;
publicKey: ArrayBuffer; publicKey: ArrayBuffer;
}; };
export type StickerPackStatusType =
| 'known'
| 'ephemeral'
| 'downloaded'
| 'installed'
| 'pending'
| 'error';
export type StickerType = { export type StickerType = Readonly<{
id: number; id: number;
packId: string; packId: string;
emoji: string | null; emoji?: string;
isCoverOnly: boolean; isCoverOnly: boolean;
lastUsed?: number; lastUsed?: number;
path: string; path: string;
width: number; width: number;
height: number; height: number;
}; }>;
export type StickerPackType = {
export const StickerPackStatuses = [
'known',
'ephemeral',
'downloaded',
'installed',
'pending',
'error',
] as const;
export type StickerPackStatusType = typeof StickerPackStatuses[number];
export type StickerPackType = Readonly<{
id: string; id: string;
key: string; key: string;
attemptedStatus: 'downloaded' | 'installed' | 'ephemeral'; attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
author: string; author: string;
coverStickerId: number; coverStickerId: number;
createdAt: number; createdAt: number;
downloadAttempts: number; downloadAttempts: number;
installedAt: number | null; installedAt?: number;
lastUsed: number; lastUsed?: number;
status: StickerPackStatusType; status: StickerPackStatusType;
stickerCount: number; stickerCount: number;
stickers: ReadonlyArray<string>; stickers: Record<string, StickerType>;
title: string; title: string;
}; }>;
export type UnprocessedType = { export type UnprocessedType = {
id: string; id: string;
timestamp: number; timestamp: number;

View file

@ -33,6 +33,7 @@ import { ReactionType } from '../types/Reactions';
import { StoredJob } from '../jobs/types'; import { StoredJob } from '../jobs/types';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { combineNames } from '../util/combineNames'; import { combineNames } from '../util/combineNames';
import { dropNull } from '../util/dropNull';
import { isNormalNumber } from '../util/isNormalNumber'; import { isNormalNumber } from '../util/isNormalNumber';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { ConversationColorType, CustomColorType } from '../types/Colors'; import { ConversationColorType, CustomColorType } from '../types/Colors';
@ -301,6 +302,7 @@ function rowToSticker(row: StickerRow): StickerType {
return { return {
...row, ...row,
isCoverOnly: Boolean(row.isCoverOnly), isCoverOnly: Boolean(row.isCoverOnly),
emoji: dropNull(row.emoji),
}; };
} }
@ -4416,13 +4418,13 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
) )
.all({ id }); .all({ id });
const payload = { const payload = {
attemptedStatus, attemptedStatus: attemptedStatus ?? null,
author, author,
coverStickerId, coverStickerId,
createdAt: createdAt || Date.now(), createdAt: createdAt || Date.now(),
downloadAttempts: downloadAttempts || 1, downloadAttempts: downloadAttempts || 1,
id, id,
installedAt, installedAt: installedAt ?? null,
key, key,
lastUsed: lastUsed || null, lastUsed: lastUsed || null,
status, status,
@ -4563,7 +4565,7 @@ async function createOrUpdateSticker(sticker: StickerType): Promise<void> {
) )
` `
).run({ ).run({
emoji, emoji: emoji ?? null,
height, height,
id, id,
isCoverOnly: isCoverOnly ? 1 : 0, isCoverOnly: isCoverOnly ? 1 : 0,

View file

@ -2,11 +2,17 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { Dictionary, omit, reject } from 'lodash'; import { Dictionary, omit, reject } from 'lodash';
import type {
StickerPackStatusType,
StickerType as StickerDBType,
StickerPackType as StickerPackDBType,
} from '../../sql/Interface';
import dataInterface from '../../sql/Client'; import dataInterface from '../../sql/Client';
import { import {
downloadStickerPack as externalDownloadStickerPack, downloadStickerPack as externalDownloadStickerPack,
maybeDeletePack, maybeDeletePack,
} from '../../../js/modules/stickers'; RecentStickerType,
} from '../../types/Stickers';
import { sendStickerPackSync } from '../../shims/textsecure'; import { sendStickerPackSync } from '../../shims/textsecure';
import { trigger } from '../../shims/events'; import { trigger } from '../../shims/events';
@ -20,49 +26,6 @@ const {
// State // State
export type StickerDBType = {
readonly id: number;
readonly packId: string;
readonly emoji: string | null;
readonly isCoverOnly: boolean;
readonly lastUsed: number;
readonly path: string;
};
export const StickerPackStatuses = [
'known',
'ephemeral',
'downloaded',
'installed',
'pending',
'error',
] as const;
export type StickerPackStatus = typeof StickerPackStatuses[number];
export type StickerPackDBType = {
readonly id: string;
readonly key: string;
readonly attemptedStatus: 'downloaded' | 'installed' | 'ephemeral';
readonly author: string;
readonly coverStickerId: number;
readonly createdAt: number;
readonly downloadAttempts: number;
readonly installedAt: number | null;
readonly lastUsed: number;
readonly status: StickerPackStatus;
readonly stickerCount: number;
readonly stickers: Dictionary<StickerDBType>;
readonly title: string;
};
export type RecentStickerType = {
readonly stickerId: number;
readonly packId: string;
};
export type StickersStateType = { export type StickersStateType = {
readonly installedPack: string | null; readonly installedPack: string | null;
readonly packs: Dictionary<StickerPackDBType>; readonly packs: Dictionary<StickerPackDBType>;
@ -75,23 +38,23 @@ export type StickersStateType = {
export type StickerType = { export type StickerType = {
readonly id: number; readonly id: number;
readonly packId: string; readonly packId: string;
readonly emoji: string | null; readonly emoji?: string;
readonly url: string; readonly url: string;
}; };
export type StickerPackType = { export type StickerPackType = Readonly<{
readonly id: string; id: string;
readonly key: string; key: string;
readonly title: string; title: string;
readonly author: string; author: string;
readonly isBlessed: boolean; isBlessed: boolean;
readonly cover?: StickerType; cover?: StickerType;
readonly lastUsed: number; lastUsed?: number;
readonly attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral'; attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
readonly status: StickerPackStatus; status: StickerPackStatusType;
readonly stickers: Array<StickerType>; stickers: Array<StickerType>;
readonly stickerCount: number; stickerCount: number;
}; }>;
// Actions // Actions
@ -128,7 +91,7 @@ type UninstallStickerPackPayloadType = {
packId: string; packId: string;
fromSync: boolean; fromSync: boolean;
status: 'downloaded'; status: 'downloaded';
installedAt: null; installedAt?: undefined;
recentStickers: Array<RecentStickerType>; recentStickers: Array<RecentStickerType>;
}; };
type UninstallStickerPackAction = { type UninstallStickerPackAction = {
@ -306,7 +269,7 @@ async function doUninstallStickerPack(
packId, packId,
fromSync, fromSync,
status, status,
installedAt: null, installedAt: undefined,
recentStickers: recentStickers.map(item => ({ recentStickers: recentStickers.map(item => ({
packId: item.packId, packId: item.packId,
stickerId: item.id, stickerId: item.id,

View file

@ -32,6 +32,7 @@ import { BodyRangesType } from '../../types/Util';
import { LinkPreviewType } from '../../types/message/LinkPreviews'; import { LinkPreviewType } from '../../types/message/LinkPreviews';
import { ConversationColors } from '../../types/Colors'; import { ConversationColors } from '../../types/Colors';
import { CallMode } from '../../types/Calling'; import { CallMode } from '../../types/Calling';
import { SignalService as Proto } from '../../protobuf';
import { AttachmentType, isVoiceMessage } from '../../types/Attachment'; import { AttachmentType, isVoiceMessage } from '../../types/Attachment';
import { CallingNotificationType } from '../../util/callingNotification'; import { CallingNotificationType } from '../../util/callingNotification';
@ -430,8 +431,7 @@ function getPropsForUnsupportedMessage(
ourNumber: string | undefined, ourNumber: string | undefined,
ourUuid: string | undefined ourUuid: string | undefined
): PropsForUnsupportedMessage { ): PropsForUnsupportedMessage {
const CURRENT_PROTOCOL_VERSION = const CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
window.textsecure.protobuf.DataMessage.ProtocolVersion.CURRENT;
const requiredVersion = message.requiredProtocolVersion; const requiredVersion = message.requiredProtocolVersion;
const canProcessNow = Boolean( const canProcessNow = Boolean(
@ -463,9 +463,6 @@ function getPropsForGroupV2Change(
conversationSelector: GetConversationByIdType, conversationSelector: GetConversationByIdType,
ourConversationId: string ourConversationId: string
): GroupsV2Props { ): GroupsV2Props {
const AccessControlEnum =
window.textsecure.protobuf.AccessControl.AccessRequired;
const RoleEnum = window.textsecure.protobuf.Member.Role;
const change = message.groupV2Change; const change = message.groupV2Change;
if (!change) { if (!change) {
@ -476,8 +473,6 @@ function getPropsForGroupV2Change(
return { return {
groupName: conversation?.type === 'group' ? conversation?.name : undefined, groupName: conversation?.type === 'group' ? conversation?.name : undefined,
AccessControlEnum,
RoleEnum,
ourConversationId, ourConversationId,
change, change,
}; };
@ -547,8 +542,7 @@ export function isMessageHistoryUnsynced(
export function isExpirationTimerUpdate( export function isExpirationTimerUpdate(
message: Pick<MessageAttributesType, 'flags'> message: Pick<MessageAttributesType, 'flags'>
): boolean { ): boolean {
const flag = const flag = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return Boolean(message.flags && message.flags & flag); return Boolean(message.flags && message.flags & flag);
} }
@ -734,7 +728,7 @@ function getPropsForGroupNotification(
export function isEndSession( export function isEndSession(
message: Pick<MessageAttributesType, 'flags'> message: Pick<MessageAttributesType, 'flags'>
): boolean { ): boolean {
const flag = window.textsecure.protobuf.DataMessage.Flags.END_SESSION; const flag = Proto.DataMessage.Flags.END_SESSION;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return Boolean(message.flags && message.flags & flag); return Boolean(message.flags && message.flags & flag);
} }

View file

@ -14,13 +14,15 @@ import {
} from 'lodash'; } from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import type { RecentStickerType } from '../../types/Stickers';
import type {
StickerType as StickerDBType,
StickerPackType as StickerPackDBType,
} from '../../sql/Interface';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { import {
RecentStickerType,
StickerDBType,
StickerPackDBType,
StickerPackType,
StickersStateType, StickersStateType,
StickerPackType,
StickerType, StickerType,
} from '../ducks/stickers'; } from '../ducks/stickers';
import { getStickersPath, getTempPath } from './user'; import { getStickersPath, getTempPath } from './user';
@ -95,7 +97,7 @@ export const translatePackFromDB = (
const filterAndTransformPacks = ( const filterAndTransformPacks = (
packs: Dictionary<StickerPackDBType>, packs: Dictionary<StickerPackDBType>,
packFilter: (sticker: StickerPackDBType) => boolean, packFilter: (sticker: StickerPackDBType) => boolean,
packSort: (sticker: StickerPackDBType) => number | null, packSort: (sticker: StickerPackDBType) => number | undefined,
blessedPacks: Dictionary<boolean>, blessedPacks: Dictionary<boolean>,
stickersPath: string, stickersPath: string,
tempPath: string tempPath: string

View file

@ -10,10 +10,8 @@ import {
} from '../../components/conversation/conversation-details/GroupLinkManagement'; } from '../../components/conversation/conversation-details/GroupLinkManagement';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { AccessControlClass } from '../../textsecure.d';
export type SmartGroupLinkManagementProps = { export type SmartGroupLinkManagementProps = {
accessEnum: typeof AccessControlClass.AccessRequired;
changeHasGroupLink: (value: boolean) => void; changeHasGroupLink: (value: boolean) => void;
conversationId: string; conversationId: string;
copyGroupLink: (groupLink: string) => void; copyGroupLink: (groupLink: string) => void;

View file

@ -10,10 +10,8 @@ import {
} from '../../components/conversation/conversation-details/GroupV2Permissions'; } from '../../components/conversation/conversation-details/GroupV2Permissions';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { AccessControlClass } from '../../textsecure.d';
export type SmartGroupV2PermissionsProps = { export type SmartGroupV2PermissionsProps = {
accessEnum: typeof AccessControlClass.AccessRequired;
conversationId: string; conversationId: string;
setAccessControlAttributesSetting: (value: number) => void; setAccessControlAttributesSetting: (value: number) => void;
setAccessControlMembersSetting: (value: number) => void; setAccessControlMembersSetting: (value: number) => void;

View file

@ -0,0 +1,126 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { Writer } from 'protobufjs';
import * as Bytes from '../Bytes';
import { typedArrayToArrayBuffer } from '../Crypto';
import { SignalService as Proto } from '../protobuf';
import { ContactBuffer, GroupBuffer } from '../textsecure/ContactsParser';
describe('ContactsParser', () => {
function generateAvatar(): Uint8Array {
const result = new Uint8Array(255);
for (let i = 0; i < result.length; i += 1) {
result[i] = i;
}
return result;
}
describe('ContactBuffer', () => {
function getTestBuffer(): ArrayBuffer {
const avatarBuffer = generateAvatar();
const contactInfoBuffer = Proto.ContactDetails.encode({
name: 'Zero Cool',
number: '+10000000000',
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
avatar: { contentType: 'image/jpeg', length: avatarBuffer.length },
}).finish();
const writer = new Writer();
writer.bytes(contactInfoBuffer);
const prefixedContact = writer.finish();
const chunks: Array<Uint8Array> = [];
for (let i = 0; i < 3; i += 1) {
chunks.push(prefixedContact);
chunks.push(avatarBuffer);
}
return typedArrayToArrayBuffer(Bytes.concatenate(chunks));
}
it('parses an array buffer of contacts', () => {
const arrayBuffer = getTestBuffer();
const contactBuffer = new ContactBuffer(arrayBuffer);
let contact = contactBuffer.next();
let count = 0;
while (contact !== undefined) {
count += 1;
assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, '+10000000000');
assert.strictEqual(
contact.uuid,
'7198e1bd-1293-452a-a098-f982ff201902'
);
assert.strictEqual(contact.avatar?.contentType, 'image/jpeg');
assert.strictEqual(contact.avatar?.length, 255);
assert.strictEqual(contact.avatar?.data.byteLength, 255);
const avatarBytes = new Uint8Array(
contact.avatar?.data || new ArrayBuffer(0)
);
for (let j = 0; j < 255; j += 1) {
assert.strictEqual(avatarBytes[j], j);
}
contact = contactBuffer.next();
}
assert.strictEqual(count, 3);
});
});
describe('GroupBuffer', () => {
function getTestBuffer(): ArrayBuffer {
const avatarBuffer = generateAvatar();
const groupInfoBuffer = Proto.GroupDetails.encode({
id: new Uint8Array([1, 3, 3, 7]),
name: 'Hackers',
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
avatar: { contentType: 'image/jpeg', length: avatarBuffer.length },
}).finish();
const writer = new Writer();
writer.bytes(groupInfoBuffer);
const prefixedGroup = writer.finish();
const chunks: Array<Uint8Array> = [];
for (let i = 0; i < 3; i += 1) {
chunks.push(prefixedGroup);
chunks.push(avatarBuffer);
}
return typedArrayToArrayBuffer(Bytes.concatenate(chunks));
}
it('parses an array buffer of groups', () => {
const arrayBuffer = getTestBuffer();
const groupBuffer = new GroupBuffer(arrayBuffer);
let group = groupBuffer.next();
let count = 0;
while (group !== undefined) {
count += 1;
assert.strictEqual(group.name, 'Hackers');
assert.deepEqual(group.id, new Uint8Array([1, 3, 3, 7]));
assert.sameMembers(group.membersE164, [
'cereal',
'burn',
'phreak',
'joey',
]);
assert.strictEqual(group.avatar?.contentType, 'image/jpeg');
assert.strictEqual(group.avatar?.length, 255);
assert.strictEqual(group.avatar?.data.byteLength, 255);
const avatarBytes = new Uint8Array(
group.avatar?.data || new ArrayBuffer(0)
);
for (let j = 0; j < 255; j += 1) {
assert.strictEqual(avatarBytes[j], j);
}
group = groupBuffer.next();
}
assert.strictEqual(count, 3);
});
});
});

View file

@ -0,0 +1,333 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-restricted-syntax */
import { assert } from 'chai';
import {
processDataMessage,
ATTACHMENT_MAX,
} from '../textsecure/processDataMessage';
import { ProcessedAttachment } from '../textsecure/Types.d';
import { SignalService as Proto } from '../protobuf';
const FLAGS = Proto.DataMessage.Flags;
const TIMESTAMP = Date.now();
const UNPROCESSED_ATTACHMENT: Proto.IAttachmentPointer = {
cdnId: 123,
key: new Uint8Array([1, 2, 3]),
digest: new Uint8Array([4, 5, 6]),
};
const PROCESSED_ATTACHMENT: ProcessedAttachment = {
cdnId: '123',
key: 'AQID',
digest: 'BAUG',
};
const GROUP_ID = new Uint8Array([0x68, 0x65, 0x79]);
const DERIVED_GROUPV2_ID = '7qQUi8Wa6Jm3Rl+l63saATGeciEqokbHpP+lV3F5t9o=';
describe('processDataMessage', () => {
const check = (message: Proto.IDataMessage) =>
processDataMessage(
{
timestamp: TIMESTAMP,
...message,
},
TIMESTAMP
);
it('should process attachments', async () => {
const out = await check({
attachments: [UNPROCESSED_ATTACHMENT],
});
assert.deepStrictEqual(out.attachments, [PROCESSED_ATTACHMENT]);
});
it('should throw on too many attachments', async () => {
const attachments: Array<Proto.IAttachmentPointer> = [];
for (let i = 0; i < ATTACHMENT_MAX + 1; i += 1) {
attachments.push(UNPROCESSED_ATTACHMENT);
}
await assert.isRejected(
check({ attachments }),
`Too many attachments: ${ATTACHMENT_MAX + 1} included in one message` +
`, max is ${ATTACHMENT_MAX}`
);
});
it('should process group context UPDATE/QUIT message', async () => {
const { UPDATE, QUIT } = Proto.GroupContext.Type;
for (const type of [UPDATE, QUIT]) {
// eslint-disable-next-line no-await-in-loop
const out = await check({
body: 'should be deleted',
attachments: [UNPROCESSED_ATTACHMENT],
group: {
id: GROUP_ID,
name: 'Group',
avatar: UNPROCESSED_ATTACHMENT,
type,
membersE164: ['+1'],
},
});
assert.isUndefined(out.body);
assert.strictEqual(out.attachments.length, 0);
assert.deepStrictEqual(out.group, {
id: 'hey',
name: 'Group',
avatar: PROCESSED_ATTACHMENT,
type,
membersE164: ['+1'],
derivedGroupV2Id: DERIVED_GROUPV2_ID,
});
}
});
it('should process group context DELIVER message', async () => {
const out = await check({
body: 'should not be deleted',
attachments: [UNPROCESSED_ATTACHMENT],
group: {
id: GROUP_ID,
name: 'should be deleted',
membersE164: ['should be deleted'],
avatar: {},
type: Proto.GroupContext.Type.DELIVER,
},
});
assert.strictEqual(out.body, 'should not be deleted');
assert.strictEqual(out.attachments.length, 1);
assert.deepStrictEqual(out.group, {
id: 'hey',
type: Proto.GroupContext.Type.DELIVER,
membersE164: [],
derivedGroupV2Id: DERIVED_GROUPV2_ID,
avatar: undefined,
name: undefined,
});
});
it('should process groupv2 context', async () => {
const out = await check({
groupV2: {
masterKey: new Uint8Array(32),
revision: 1,
groupChange: new Uint8Array([4, 5, 6]),
},
});
assert.deepStrictEqual(out.groupV2, {
masterKey: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
revision: 1,
groupChange: 'BAUG',
id: 'd/rq8//fR4RzhvN3G9KcKlQoj7cguQFjTOqLV6JUSbo=',
secretParams:
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd/rq8//fR' +
'4RzhvN3G9KcKlQoj7cguQFjTOqLV6JUSbrURzeILsUmsymGJmHt3kpBJ2zosqp4ex' +
'sg+qwF1z6YdB/rxKnxKRLZZP/V0F7bERslYILy2lUh3Sh3iA98yO4CGfzjjFVo1SI' +
'7U8XApLeVNQHJo7nkflf/JyBrqPft5gEucbKW/h+S3OYjfQ5zl2Cpw3XrV7N6OKEu' +
'tLUWPHQuJx11A4xDPrmtAOnGy2NBxoOybDNlWipeNbn1WQJqOjMF7YA80oEm+5qnM' +
'kEYcFVqbYaSzPcMhg3mQ0SYfQpxYgSOJpwp9f/8EDnwJV4ISPBOo2CiaSqVfnd8Dw' +
'ZOc58gQA==',
publicParams:
'AHf66vP/30eEc4bzdxvSnCpUKI+3ILkBY0zqi1eiVEm6LnGylv4fk' +
'tzmI30Oc5dgqcN161ezejihLrS1Fjx0LieOJpwp9f/8EDnwJV4ISPBOo2CiaSqVfn' +
'd8DwZOc58gQA==',
});
});
it('should base64 profileKey', async () => {
const out = await check({
profileKey: new Uint8Array([42, 23, 55]),
});
assert.strictEqual(out.profileKey, 'Khc3');
});
it('should process quote', async () => {
const out = await check({
quote: {
id: 1,
authorUuid: 'author',
text: 'text',
attachments: [
{
contentType: 'image/jpeg',
fileName: 'image.jpg',
thumbnail: UNPROCESSED_ATTACHMENT,
},
],
},
});
assert.deepStrictEqual(out.quote, {
id: 1,
authorUuid: 'author',
text: 'text',
attachments: [
{
contentType: 'image/jpeg',
fileName: 'image.jpg',
thumbnail: PROCESSED_ATTACHMENT,
},
],
bodyRanges: [],
});
});
it('should process contact', async () => {
const out = await check({
contact: [
{
avatar: {
avatar: UNPROCESSED_ATTACHMENT,
},
},
{
avatar: {
avatar: UNPROCESSED_ATTACHMENT,
isProfile: true,
},
},
],
});
assert.deepStrictEqual(out.contact, [
{
avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: false },
},
{
avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: true },
},
]);
});
it('should process reaction', async () => {
assert.deepStrictEqual(
(
await check({
reaction: {
emoji: '😎',
targetTimestamp: TIMESTAMP,
},
})
).reaction,
{
emoji: '😎',
remove: false,
targetAuthorUuid: undefined,
targetTimestamp: TIMESTAMP,
}
);
assert.deepStrictEqual(
(
await check({
reaction: {
emoji: '😎',
remove: true,
targetTimestamp: TIMESTAMP,
},
})
).reaction,
{
emoji: '😎',
remove: true,
targetAuthorUuid: undefined,
targetTimestamp: TIMESTAMP,
}
);
});
it('should process preview', async () => {
const out = await check({
preview: [
{
date: TIMESTAMP,
image: UNPROCESSED_ATTACHMENT,
},
],
});
assert.deepStrictEqual(out.preview, [
{
date: TIMESTAMP,
description: undefined,
title: undefined,
url: undefined,
image: PROCESSED_ATTACHMENT,
},
]);
});
it('should process sticker', async () => {
const out = await check({
sticker: {
packId: new Uint8Array([1, 2, 3]),
packKey: new Uint8Array([4, 5, 6]),
stickerId: 1,
data: UNPROCESSED_ATTACHMENT,
},
});
assert.deepStrictEqual(out.sticker, {
packId: '010203',
packKey: 'BAUG',
stickerId: 1,
data: PROCESSED_ATTACHMENT,
});
});
it('should process FLAGS=END_SESSION', async () => {
const out = await check({
flags: FLAGS.END_SESSION,
body: 'should be deleted',
group: {
id: GROUP_ID,
type: Proto.GroupContext.Type.DELIVER,
},
attachments: [UNPROCESSED_ATTACHMENT],
});
assert.isUndefined(out.body);
assert.isUndefined(out.group);
assert.deepStrictEqual(out.attachments, []);
});
it('should process FLAGS=EXPIRATION_TIMER_UPDATE,PROFILE_KEY_UPDATE', async () => {
const values = [FLAGS.EXPIRATION_TIMER_UPDATE, FLAGS.PROFILE_KEY_UPDATE];
for (const flags of values) {
// eslint-disable-next-line no-await-in-loop
const out = await check({
flags,
body: 'should be deleted',
attachments: [UNPROCESSED_ATTACHMENT],
});
assert.isUndefined(out.body);
assert.deepStrictEqual(out.attachments, []);
}
});
it('processes trivial fields', async () => {
assert.strictEqual((await check({ flags: null })).flags, 0);
assert.strictEqual((await check({ flags: 1 })).flags, 1);
assert.strictEqual((await check({ expireTimer: null })).expireTimer, 0);
assert.strictEqual((await check({ expireTimer: 123 })).expireTimer, 123);
assert.isFalse((await check({ isViewOnce: null })).isViewOnce);
assert.isFalse((await check({ isViewOnce: false })).isViewOnce);
assert.isTrue((await check({ isViewOnce: true })).isViewOnce);
});
});

View file

@ -0,0 +1,37 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import getGuid from 'uuid/v4';
import { processSyncMessage } from '../textsecure/processSyncMessage';
describe('processSyncMessage', () => {
it('should normalize UUIDs in sent', () => {
const destinationUuid = getGuid();
const out = processSyncMessage({
sent: {
destinationUuid: destinationUuid.toUpperCase(),
unidentifiedStatus: [
{
destinationUuid: destinationUuid.toUpperCase(),
},
],
},
});
assert.deepStrictEqual(out, {
sent: {
destinationUuid,
unidentifiedStatus: [
{
destinationUuid,
},
],
},
});
});
});

View file

@ -13,7 +13,7 @@ describe('both/state/ducks/composer', () => {
conversationId: '123', conversationId: '123',
quote: { quote: {
attachments: [], attachments: [],
id: '456', id: 456,
isViewOnce: false, isViewOnce: false,
messageId: '789', messageId: '789',
referencedMessageNotFound: false, referencedMessageNotFound: false,
@ -114,7 +114,7 @@ describe('both/state/ducks/composer', () => {
const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE)); const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE));
assert.equal(nextState.quotedMessage?.conversationId, '123'); assert.equal(nextState.quotedMessage?.conversationId, '123');
assert.equal(nextState.quotedMessage?.quote?.id, '456'); assert.equal(nextState.quotedMessage?.quote?.id, 456);
}); });
}); });
}); });

View file

@ -2,7 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
import { dropNull } from '../../util/dropNull'; import { dropNull, shallowDropNull } from '../../util/dropNull';
type Test = {
a: number | null;
b: number | undefined;
};
describe('dropNull', () => { describe('dropNull', () => {
it('swaps null with undefined', () => { it('swaps null with undefined', () => {
@ -16,4 +21,42 @@ describe('dropNull', () => {
it('non-null values undefined be', () => { it('non-null values undefined be', () => {
assert.strictEqual(dropNull('test'), 'test'); assert.strictEqual(dropNull('test'), 'test');
}); });
describe('shallowDropNull', () => {
it('return undefined with given null', () => {
assert.strictEqual(shallowDropNull<Test>(null), undefined);
});
it('return undefined with given undefined', () => {
assert.strictEqual(shallowDropNull<Test>(undefined), undefined);
});
it('swaps null with undefined', () => {
const result:
| {
a: number | undefined;
b: number | undefined;
}
| undefined = shallowDropNull<Test>({
a: null,
b: 1,
});
assert.deepStrictEqual(result, { a: undefined, b: 1 });
});
it('leaves undefined be', () => {
const result:
| {
a: number | undefined;
b: number | undefined;
}
| undefined = shallowDropNull<Test>({
a: 1,
b: undefined,
});
assert.deepStrictEqual(result, { a: 1, b: undefined });
});
});
}); });

View file

@ -0,0 +1,76 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable
class-methods-use-this,
@typescript-eslint/no-empty-function
*/
import { assert } from 'chai';
import EventEmitter from 'events';
import { connection as WebSocket } from 'websocket';
import MessageReceiver from '../textsecure/MessageReceiver';
import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents';
import { SignalService as Proto } from '../protobuf';
import * as Crypto from '../Crypto';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
describe('MessageReceiver', () => {
class FakeSocket extends EventEmitter {
public sendBytes(_: Uint8Array) {}
public close() {}
}
const number = '+19999999999';
const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee';
const deviceId = 1;
const signalingKey = Crypto.getRandomBytes(32 + 20);
describe('connecting', () => {
it('generates decryption-error event when it cannot decrypt', done => {
const socket = new FakeSocket();
const messageReceiver = new MessageReceiver(
'oldUsername.2',
'username.2',
'password',
signalingKey,
{
serverTrustRoot: 'AAAAAAAA',
socket: socket as WebSocket,
}
);
const body = Proto.Envelope.encode({
type: Proto.Envelope.Type.CIPHERTEXT,
source: number,
sourceUuid: uuid,
sourceDevice: deviceId,
timestamp: Date.now(),
content: new FIXMEU8(Crypto.getRandomBytes(200)),
}).finish();
const message = Proto.WebSocketMessage.encode({
type: Proto.WebSocketMessage.Type.REQUEST,
request: { id: 1, verb: 'PUT', path: '/api/v1/message', body },
}).finish();
socket.emit('message', {
type: 'binary',
binaryData: message,
});
messageReceiver.addEventListener(
'decryption-error',
(error: DecryptionErrorEvent) => {
assert.strictEqual(error.decryptionError.senderUuid, uuid);
assert.strictEqual(error.decryptionError.senderDevice, deviceId);
done();
}
);
});
});
});

View file

@ -5,6 +5,7 @@ import { assert } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { SignalService as Proto } from '../../protobuf';
describe('Message', () => { describe('Message', () => {
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -384,8 +385,7 @@ describe('Message', () => {
title: 'voice message', title: 'voice message',
attachment: { attachment: {
contentType: 'audio/ogg', contentType: 'audio/ogg',
flags: flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
window.textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
}, },
expectedText: 'Voice Message', expectedText: 'Voice Message',
expectedEmoji: '🎤', expectedEmoji: '🎤',

212
ts/textsecure.d.ts vendored
View file

@ -18,6 +18,7 @@ import { Storage } from './textsecure/Storage';
import { import {
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
ProcessedAttachment,
} from './textsecure/Types.d'; } from './textsecure/Types.d';
export type UnprocessedType = { export type UnprocessedType = {
@ -31,6 +32,7 @@ export type UnprocessedType = {
source?: string; source?: string;
sourceDevice?: number; sourceDevice?: number;
sourceUuid?: string; sourceUuid?: string;
messageAgeSec?: number;
version: number; version: number;
}; };
@ -71,7 +73,6 @@ type DeviceNameProtobufTypes = {
type GroupsProtobufTypes = { type GroupsProtobufTypes = {
AvatarUploadAttributes: typeof AvatarUploadAttributesClass; AvatarUploadAttributes: typeof AvatarUploadAttributesClass;
Member: typeof MemberClass;
MemberPendingProfileKey: typeof MemberPendingProfileKeyClass; MemberPendingProfileKey: typeof MemberPendingProfileKeyClass;
MemberPendingAdminApproval: typeof MemberPendingAdminApprovalClass; MemberPendingAdminApproval: typeof MemberPendingAdminApprovalClass;
AccessControl: typeof AccessControlClass; AccessControl: typeof AccessControlClass;
@ -86,9 +87,6 @@ type GroupsProtobufTypes = {
type SignalServiceProtobufTypes = { type SignalServiceProtobufTypes = {
AttachmentPointer: typeof AttachmentPointerClass; AttachmentPointer: typeof AttachmentPointerClass;
ContactDetails: typeof ContactDetailsClass;
Content: typeof ContentClass;
DataMessage: typeof DataMessageClass;
Envelope: typeof EnvelopeClass; Envelope: typeof EnvelopeClass;
GroupContext: typeof GroupContextClass; GroupContext: typeof GroupContextClass;
GroupContextV2: typeof GroupContextV2Class; GroupContextV2: typeof GroupContextV2Class;
@ -159,39 +157,14 @@ export declare class AvatarUploadAttributesClass {
signature?: string; signature?: string;
} }
export declare class MemberClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => MemberClass;
userId?: ProtoBinaryType;
role?: MemberRoleEnum;
profileKey?: ProtoBinaryType;
presentation?: ProtoBinaryType;
joinedAtVersion?: number;
// Note: only role and presentation are required when creating a group
}
export type MemberRoleEnum = number; export type MemberRoleEnum = number;
// Note: we need to use namespaces to express nested classes in Typescript
export declare namespace MemberClass {
class Role {
static UNKNOWN: number;
static DEFAULT: number;
static ADMINISTRATOR: number;
}
}
export declare class MemberPendingProfileKeyClass { export declare class MemberPendingProfileKeyClass {
static decode: ( static decode: (
data: ArrayBuffer | ByteBufferClass, data: ArrayBuffer | ByteBufferClass,
encoding?: string encoding?: string
) => MemberPendingProfileKeyClass; ) => MemberPendingProfileKeyClass;
member?: MemberClass;
addedByUserId?: ProtoBinaryType; addedByUserId?: ProtoBinaryType;
timestamp?: ProtoBigNumberType; timestamp?: ProtoBigNumberType;
} }
@ -245,7 +218,6 @@ export declare class GroupClass {
disappearingMessagesTimer?: ProtoBinaryType; disappearingMessagesTimer?: ProtoBinaryType;
accessControl?: AccessControlClass; accessControl?: AccessControlClass;
version?: number; version?: number;
members?: Array<MemberClass>;
membersPendingProfileKey?: Array<MemberPendingProfileKeyClass>; membersPendingProfileKey?: Array<MemberPendingProfileKeyClass>;
membersPendingAdminApproval?: Array<MemberPendingAdminApprovalClass>; membersPendingAdminApproval?: Array<MemberPendingAdminApprovalClass>;
inviteLinkPassword?: ProtoBinaryType; inviteLinkPassword?: ProtoBinaryType;
@ -299,7 +271,6 @@ export declare namespace GroupChangeClass {
// Note: we need to use namespaces to express nested classes in Typescript // Note: we need to use namespaces to express nested classes in Typescript
export declare namespace GroupChangeClass.Actions { export declare namespace GroupChangeClass.Actions {
class AddMemberAction { class AddMemberAction {
added?: MemberClass;
joinFromInviteLink?: boolean; joinFromInviteLink?: boolean;
} }
@ -480,7 +451,7 @@ export declare class AttachmentPointerClass {
GIF: number; GIF: number;
}; };
cdnId?: ProtoBigNumberType; cdnId?: string;
cdnKey?: string; cdnKey?: string;
contentType?: string; contentType?: string;
key?: ProtoBinaryType; key?: ProtoBinaryType;
@ -493,184 +464,16 @@ export declare class AttachmentPointerClass {
height?: number; height?: number;
caption?: string; caption?: string;
blurHash?: string; blurHash?: string;
uploadTimestamp?: ProtoBigNumberType;
cdnNumber?: number; cdnNumber?: number;
} }
export type DownloadAttachmentType = { export type DownloadAttachmentType = Omit<
ProcessedAttachment,
'digest' | 'key'
> & {
data: ArrayBuffer; data: ArrayBuffer;
cdnId?: ProtoBigNumberType;
cdnKey?: string;
contentType?: string;
size?: number;
thumbnail?: ProtoBinaryType;
fileName?: string;
flags?: number;
width?: number;
height?: number;
caption?: string;
blurHash?: string;
uploadTimestamp?: ProtoBigNumberType;
cdnNumber?: number;
}; };
export declare class ContactDetailsClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => ContactDetailsClass;
number?: string;
uuid?: string;
name?: string;
avatar?: ContactDetailsClass.Avatar;
color?: string;
verified?: VerifiedClass;
profileKey?: ProtoBinaryType;
blocked?: boolean;
expireTimer?: number;
inboxPosition?: number;
}
// Note: we need to use namespaces to express nested classes in Typescript
export declare namespace ContactDetailsClass {
class Avatar {
contentType?: string;
length?: number;
}
}
export declare class ContentClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => ContentClass;
toArrayBuffer: () => ArrayBuffer;
dataMessage?: DataMessageClass;
syncMessage?: SyncMessageClass;
callingMessage?: CallingMessageClass;
nullMessage?: NullMessageClass;
receiptMessage?: ReceiptMessageClass;
typingMessage?: TypingMessageClass;
senderKeyDistributionMessage?: ByteBufferClass;
decryptionErrorMessage?: ByteBufferClass;
}
export declare class DataMessageClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => DataMessageClass;
toArrayBuffer(): ArrayBuffer;
body?: string | null;
attachments?: Array<AttachmentPointerClass>;
group?: GroupContextClass | null;
groupV2?: GroupContextV2Class | null;
flags?: number;
expireTimer?: number;
profileKey?: ProtoBinaryType;
timestamp?: ProtoBigNumberType;
quote?: DataMessageClass.Quote;
contact?: Array<DataMessageClass.Contact>;
preview?: Array<DataMessageClass.Preview>;
sticker?: DataMessageClass.Sticker;
requiredProtocolVersion?: number;
isViewOnce?: boolean;
reaction?: DataMessageClass.Reaction;
delete?: DataMessageClass.Delete;
bodyRanges?: Array<DataMessageClass.BodyRange>;
groupCallUpdate?: DataMessageClass.GroupCallUpdate;
}
// Note: we need to use namespaces to express nested classes in Typescript
export declare namespace DataMessageClass {
// Note: deep nesting
class Contact {
name: any;
number: any;
email: any;
address: any;
avatar: any;
organization?: string;
}
class Flags {
static END_SESSION: number;
static EXPIRATION_TIMER_UPDATE: number;
static PROFILE_KEY_UPDATE: number;
}
class Preview {
url?: string;
title?: string;
image?: AttachmentPointerClass;
description?: string;
date?: ProtoBigNumberType;
}
class ProtocolVersion {
static INITIAL: number;
static MESSAGE_TIMERS: number;
static VIEW_ONCE: number;
static VIEW_ONCE_VIDEO: number;
static REACTIONS: number;
static MENTIONS: number;
static CURRENT: number;
}
// Note: deep nesting
class Quote {
id?: ProtoBigNumberType | null;
authorUuid?: string | null;
text?: string | null;
attachments?: Array<DataMessageClass.Quote.QuotedAttachment>;
bodyRanges?: Array<DataMessageClass.BodyRange>;
// Added later during processing
referencedMessageNotFound?: boolean;
isViewOnce?: boolean;
}
class BodyRange {
start?: number;
length?: number;
mentionUuid?: string;
}
class Reaction {
emoji: string | null;
remove: boolean;
targetAuthorUuid: string | null;
targetTimestamp: ProtoBigNumberType | null;
}
class Delete {
targetSentTimestamp?: ProtoBigNumberType;
}
class Sticker {
packId?: ProtoBinaryType;
packKey?: ProtoBinaryType;
stickerId?: number;
data?: AttachmentPointerClass;
}
class GroupCallUpdate {
eraId?: string;
}
}
// Note: we need to use namespaces to express nested classes in Typescript
export declare namespace DataMessageClass.Quote {
class QuotedAttachment {
contentType?: string;
fileName?: string;
thumbnail?: AttachmentPointerClass;
}
}
declare class DeviceNameClass { declare class DeviceNameClass {
static decode: ( static decode: (
data: ArrayBuffer | ByteBufferClass, data: ArrayBuffer | ByteBufferClass,
@ -1140,7 +943,6 @@ export declare namespace SyncMessageClass {
destination?: string; destination?: string;
destinationUuid?: string; destinationUuid?: string;
timestamp?: ProtoBigNumberType; timestamp?: ProtoBigNumberType;
message?: DataMessageClass;
expirationStartTimestamp?: ProtoBigNumberType; expirationStartTimestamp?: ProtoBigNumberType;
unidentifiedStatus?: Array<SyncMessageClass.Sent.UnidentifiedDeliveryStatus>; unidentifiedStatus?: Array<SyncMessageClass.Sent.UnidentifiedDeliveryStatus>;
isRecipientUpdate?: boolean; isRecipientUpdate?: boolean;

View file

@ -1,101 +1,160 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { ByteBufferClass } from '../window.d'; import { Reader } from 'protobufjs';
import { AttachmentType } from './SendMessage';
type ProtobufConstructorType = { import { SignalService as Proto } from '../protobuf';
decode: (data: ArrayBuffer) => ProtobufType; import { normalizeUuid } from '../util/normalizeUuid';
import { typedArrayToArrayBuffer } from '../Crypto';
import Avatar = Proto.ContactDetails.IAvatar;
type OptionalAvatar = { avatar?: Avatar | null };
type DecoderBase<Message extends OptionalAvatar> = {
decodeDelimited(reader: Reader): Message | undefined;
}; };
type ProtobufType = { export type MessageWithAvatar<Message extends OptionalAvatar> = Omit<
avatar?: PackedAttachmentType; Message,
profileKey?: any; 'avatar'
uuid?: string; > & {
members: Array<string>; avatar?: (Avatar & { data: ArrayBuffer }) | null;
}; };
export type PackedAttachmentType = AttachmentType & { export type ModifiedGroupDetails = MessageWithAvatar<Proto.GroupDetails>;
length: number;
};
export class ProtoParser { export type ModifiedContactDetails = MessageWithAvatar<Proto.ContactDetails>;
buffer: ByteBufferClass;
protobuf: ProtobufConstructorType; // TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
constructor(arrayBuffer: ArrayBuffer, protobuf: ProtobufConstructorType) { class ParserBase<
this.protobuf = protobuf; Message extends OptionalAvatar,
this.buffer = new window.dcodeIO.ByteBuffer(); Decoder extends DecoderBase<Message>
this.buffer.append(arrayBuffer); > {
this.buffer.offset = 0; protected readonly reader: Reader;
this.buffer.limit = arrayBuffer.byteLength;
constructor(arrayBuffer: ArrayBuffer, private readonly decoder: Decoder) {
this.reader = new Reader(new FIXMEU8(arrayBuffer));
} }
next(): ProtobufType | undefined | null { protected decodeDelimited(): MessageWithAvatar<Message> | undefined {
if (this.reader.pos === this.reader.len) {
return undefined; // eof
}
try { try {
if (this.buffer.limit === this.buffer.offset) { const proto = this.decoder.decodeDelimited(this.reader);
return undefined; // eof
}
const len = this.buffer.readVarint32();
const nextBuffer = this.buffer
.slice(this.buffer.offset, this.buffer.offset + len)
.toArrayBuffer();
const proto = this.protobuf.decode(nextBuffer); if (!proto) {
this.buffer.skip(len); return undefined;
if (proto.avatar) {
const attachmentLen = proto.avatar.length;
proto.avatar.data = this.buffer
.slice(this.buffer.offset, this.buffer.offset + attachmentLen)
.toArrayBuffer();
this.buffer.skip(attachmentLen);
} }
if (proto.profileKey) { if (!proto.avatar) {
proto.profileKey = proto.profileKey.toArrayBuffer(); return {
...proto,
avatar: null,
};
} }
if (proto.uuid) { const attachmentLen = proto.avatar.length ?? 0;
window.normalizeUuids( const avatarData = this.reader.buf.slice(
proto, this.reader.pos,
['uuid'], this.reader.pos + attachmentLen
'ProtoParser::next (proto.uuid)' );
); this.reader.skip(attachmentLen);
}
if (proto.members) { return {
window.normalizeUuids( ...proto,
proto,
proto.members.map((_member, i) => `members.${i}.uuid`),
'ProtoParser::next (proto.members)'
);
}
return proto; avatar: {
...proto.avatar,
data: typedArrayToArrayBuffer(avatarData),
},
};
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'ProtoParser.next error:', 'ProtoParser.next error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
return undefined;
}
}
}
export class GroupBuffer extends ParserBase<
Proto.GroupDetails,
typeof Proto.GroupDetails
> {
constructor(arrayBuffer: ArrayBuffer) {
super(arrayBuffer, Proto.GroupDetails);
}
public next(): ModifiedGroupDetails | undefined {
const proto = this.decodeDelimited();
if (!proto) {
return undefined;
} }
return null; if (!proto.members) {
return proto;
}
return {
...proto,
members: proto.members.map((member, i) => {
if (!member.uuid) {
return member;
}
return {
...member,
uuid: normalizeUuid(member.uuid, `GroupBuffer.member[${i}].uuid`),
};
}),
};
} }
} }
export class GroupBuffer extends ProtoParser { export class ContactBuffer extends ParserBase<
Proto.ContactDetails,
typeof Proto.ContactDetails
> {
constructor(arrayBuffer: ArrayBuffer) { constructor(arrayBuffer: ArrayBuffer) {
super(arrayBuffer, window.textsecure.protobuf.GroupDetails as any); super(arrayBuffer, Proto.ContactDetails);
} }
}
export class ContactBuffer extends ProtoParser { public next(): ModifiedContactDetails | undefined {
constructor(arrayBuffer: ArrayBuffer) { const proto = this.decodeDelimited();
super(arrayBuffer, window.textsecure.protobuf.ContactDetails as any); if (!proto) {
return undefined;
}
if (!proto.uuid) {
return proto;
}
const { verified } = proto;
return {
...proto,
verified:
verified && verified.destinationUuid
? {
...verified,
destinationUuid: normalizeUuid(
verified.destinationUuid,
'ContactBuffer.verified.destinationUuid'
),
}
: verified,
uuid: normalizeUuid(proto.uuid, 'ContactBuffer.uuid'),
};
} }
} }

View file

@ -184,7 +184,7 @@ const Crypto = {
async decryptAttachment( async decryptAttachment(
encryptedBin: ArrayBuffer, encryptedBin: ArrayBuffer,
keys: ArrayBuffer, keys: ArrayBuffer,
theirDigest: ArrayBuffer theirDigest?: ArrayBuffer
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
if (keys.byteLength !== 64) { if (keys.byteLength !== 64) {
throw new Error('Got invalid length attachment keys'); throw new Error('Got invalid length attachment keys');

View file

@ -12,8 +12,10 @@
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
*/ */
export type EventHandler = (event: any) => unknown;
export default class EventTarget { export default class EventTarget {
listeners?: { [type: string]: Array<Function> }; listeners?: { [type: string]: Array<EventHandler> };
dispatchEvent(ev: Event): Array<unknown> { dispatchEvent(ev: Event): Array<unknown> {
if (!(ev instanceof Event)) { if (!(ev instanceof Event)) {
@ -36,7 +38,7 @@ export default class EventTarget {
return results; return results;
} }
addEventListener(eventName: string, callback: Function): void { addEventListener(eventName: string, callback: EventHandler): void {
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
throw new Error('First argument expects a string'); throw new Error('First argument expects a string');
} }
@ -54,7 +56,7 @@ export default class EventTarget {
this.listeners[eventName] = listeners; this.listeners[eventName] = listeners;
} }
removeEventListener(eventName: string, callback: Function): void { removeEventListener(eventName: string, callback: EventHandler): void {
if (typeof eventName !== 'string') { if (typeof eventName !== 'string') {
throw new Error('First argument expects a string'); throw new Error('First argument expects a string');
} }

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,6 @@ import {
} from '@signalapp/signal-client'; } from '@signalapp/signal-client';
import { WebAPIType } from './WebAPI'; import { WebAPIType } from './WebAPI';
import { ContentClass, DataMessageClass } from '../textsecure.d';
import { import {
CallbackResultType, CallbackResultType,
SendMetadataType, SendMetadataType,
@ -42,6 +41,7 @@ import { Sessions, IdentityKeys } from '../LibSignalStores';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier'; import { getKeysForIdentifier } from './getKeysForIdentifier';
import { SignalService as Proto } from '../protobuf';
export const enum SenderCertificateMode { export const enum SenderCertificateMode {
WithE164, WithE164,
@ -72,13 +72,13 @@ type OutgoingMessageOptionsType = SendOptionsType & {
function ciphertextMessageTypeToEnvelopeType(type: number) { function ciphertextMessageTypeToEnvelopeType(type: number) {
if (type === CiphertextMessageType.PreKey) { if (type === CiphertextMessageType.PreKey) {
return window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE; return Proto.Envelope.Type.PREKEY_BUNDLE;
} }
if (type === CiphertextMessageType.Whisper) { if (type === CiphertextMessageType.Whisper) {
return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT; return Proto.Envelope.Type.CIPHERTEXT;
} }
if (type === CiphertextMessageType.Plaintext) { if (type === CiphertextMessageType.Plaintext) {
return window.textsecure.protobuf.Envelope.Type.PLAINTEXT_CONTENT; return Proto.Envelope.Type.PLAINTEXT_CONTENT;
} }
throw new Error( throw new Error(
`ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}` `ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}`
@ -96,11 +96,11 @@ function getPaddedMessageLength(messageLength: number): number {
return messagePartCount * 160; return messagePartCount * 160;
} }
export function padMessage(messageBuffer: ArrayBuffer): Uint8Array { export function padMessage(messageBuffer: Uint8Array): Uint8Array {
const plaintext = new Uint8Array( const plaintext = new Uint8Array(
getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
); );
plaintext.set(new Uint8Array(messageBuffer)); plaintext.set(messageBuffer);
plaintext[messageBuffer.byteLength] = 0x80; plaintext[messageBuffer.byteLength] = 0x80;
return plaintext; return plaintext;
@ -113,7 +113,7 @@ export default class OutgoingMessage {
identifiers: Array<string>; identifiers: Array<string>;
message: ContentClass | PlaintextContent; message: Proto.Content | PlaintextContent;
callback: (result: CallbackResultType) => void; callback: (result: CallbackResultType) => void;
@ -141,14 +141,14 @@ export default class OutgoingMessage {
server: WebAPIType, server: WebAPIType,
timestamp: number, timestamp: number,
identifiers: Array<string>, identifiers: Array<string>,
message: ContentClass | DataMessageClass | PlaintextContent, message: Proto.Content | Proto.DataMessage | PlaintextContent,
contentHint: number, contentHint: number,
groupId: string | undefined, groupId: string | undefined,
callback: (result: CallbackResultType) => void, callback: (result: CallbackResultType) => void,
options: OutgoingMessageOptionsType = {} options: OutgoingMessageOptionsType = {}
) { ) {
if (message instanceof window.textsecure.protobuf.DataMessage) { if (message instanceof Proto.DataMessage) {
const content = new window.textsecure.protobuf.Content(); const content = new Proto.Content();
content.dataMessage = message; content.dataMessage = message;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
this.message = content; this.message = content;
@ -304,8 +304,8 @@ export default class OutgoingMessage {
if (!this.plaintext) { if (!this.plaintext) {
const { message } = this; const { message } = this;
if (message instanceof window.textsecure.protobuf.Content) { if (message instanceof Proto.Content) {
this.plaintext = padMessage(message.toArrayBuffer()); this.plaintext = padMessage(Proto.Content.encode(message).finish());
} else { } else {
this.plaintext = message.serialize(); this.plaintext = message.serialize();
} }
@ -324,7 +324,7 @@ export default class OutgoingMessage {
}): Promise<CiphertextMessage> { }): Promise<CiphertextMessage> {
const { message } = this; const { message } = this;
if (message instanceof window.textsecure.protobuf.Content) { if (message instanceof Proto.Content) {
return signalEncrypt( return signalEncrypt(
Buffer.from(this.getPlaintext()), Buffer.from(this.getPlaintext()),
protocolAddress, protocolAddress,
@ -421,8 +421,7 @@ export default class OutgoingMessage {
); );
return { return {
type: type: Proto.Envelope.Type.UNIDENTIFIED_SENDER,
window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
destinationDeviceId, destinationDeviceId,
destinationRegistrationId, destinationRegistrationId,
content: buffer.toString('base64'), content: buffer.toString('base64'),

View file

@ -14,7 +14,8 @@ import {
} from '../Crypto'; } from '../Crypto';
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve'; import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import { assert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { normalizeUuid } from '../util/normalizeUuid';
// TODO: remove once we move away from ArrayBuffers // TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array; const FIXMEU8 = Uint8Array;
@ -35,7 +36,7 @@ class ProvisioningCipherInner {
async decrypt( async decrypt(
provisionEnvelope: Proto.ProvisionEnvelope provisionEnvelope: Proto.ProvisionEnvelope
): Promise<ProvisionDecryptResult> { ): Promise<ProvisionDecryptResult> {
assert( strictAssert(
provisionEnvelope.publicKey && provisionEnvelope.body, provisionEnvelope.publicKey && provisionEnvelope.body,
'Missing required fields in ProvisionEnvelope' 'Missing required fields in ProvisionEnvelope'
); );
@ -79,19 +80,17 @@ class ProvisioningCipherInner {
new FIXMEU8(plaintext) new FIXMEU8(plaintext)
); );
const privKey = provisionMessage.identityKeyPrivate; const privKey = provisionMessage.identityKeyPrivate;
assert(privKey, 'Missing identityKeyPrivate in ProvisionMessage'); strictAssert(privKey, 'Missing identityKeyPrivate in ProvisionMessage');
const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey)); const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey));
window.normalizeUuids(
provisionMessage, const { uuid } = provisionMessage;
['uuid'], strictAssert(uuid, 'Missing uuid in provisioning message');
'ProvisioningCipher.decrypt'
);
const ret: ProvisionDecryptResult = { const ret: ProvisionDecryptResult = {
identityKeyPair: keyPair, identityKeyPair: keyPair,
number: provisionMessage.number, number: provisionMessage.number,
uuid: provisionMessage.uuid, uuid: normalizeUuid(uuid, 'ProvisionMessage.uuid'),
provisioningCode: provisionMessage.provisioningCode, provisioningCode: provisionMessage.provisioningCode,
userAgent: provisionMessage.userAgent, userAgent: provisionMessage.userAgent,
readReceipts: provisionMessage.readReceipts, readReceipts: provisionMessage.readReceipts,

View file

@ -30,22 +30,16 @@ import {
import createTaskWithTimeout from './TaskWithTimeout'; import createTaskWithTimeout from './TaskWithTimeout';
import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage'; import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage';
import Crypto from './Crypto'; import Crypto from './Crypto';
import * as Bytes from '../Bytes';
import { import {
base64ToArrayBuffer,
concatenateBytes, concatenateBytes,
getRandomBytes, getRandomBytes,
getZeroes, getZeroes,
hexToArrayBuffer,
typedArrayToArrayBuffer, typedArrayToArrayBuffer,
} from '../Crypto'; } from '../Crypto';
import { import {
AttachmentPointerClass,
CallingMessageClass,
ContentClass,
DataMessageClass,
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
SyncMessageClass,
} from '../textsecure.d'; } from '../textsecure.d';
import { MessageError, SignedPreKeyRotationError } from './Errors'; import { MessageError, SignedPreKeyRotationError } from './Errors';
import { BodyRangesType } from '../types/Util'; import { BodyRangesType } from '../types/Util';
@ -56,18 +50,6 @@ import {
import { concat } from '../util/iterables'; import { concat } from '../util/iterables';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
function stringToArrayBuffer(str: string): ArrayBuffer {
if (typeof str !== 'string') {
throw new Error('Passed non-string to stringToArrayBuffer');
}
const res = new ArrayBuffer(str.length);
const uint = new Uint8Array(res);
for (let i = 0; i < str.length; i += 1) {
uint[i] = str.charCodeAt(i);
}
return res;
}
export type SendMetadataType = { export type SendMetadataType = {
[identifier: string]: { [identifier: string]: {
accessKey: string; accessKey: string;
@ -101,7 +83,7 @@ type PreviewType = {
type QuoteAttachmentType = { type QuoteAttachmentType = {
thumbnail?: AttachmentType; thumbnail?: AttachmentType;
attachmentPointer?: AttachmentPointerClass; attachmentPointer?: Proto.IAttachmentPointer;
}; };
export type GroupV2InfoType = { export type GroupV2InfoType = {
@ -130,7 +112,7 @@ export type AttachmentType = {
height: number; height: number;
caption: string; caption: string;
attachmentPointer?: AttachmentPointerClass; attachmentPointer?: Proto.IAttachmentPointer;
blurHash?: string; blurHash?: string;
}; };
@ -174,6 +156,9 @@ export type GroupSendOptionsType = {
groupCallUpdate?: GroupCallUpdateType; groupCallUpdate?: GroupCallUpdateType;
}; };
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
class Message { class Message {
attachments: Array<any>; attachments: Array<any>;
@ -219,7 +204,7 @@ class Message {
dataMessage: any; dataMessage: any;
attachmentPointers?: Array<any>; attachmentPointers: Array<Proto.IAttachmentPointer> = [];
deletedForEveryoneTimestamp?: number; deletedForEveryoneTimestamp?: number;
@ -301,17 +286,14 @@ class Message {
} }
isEndSession() { isEndSession() {
return ( return (this.flags || 0) & Proto.DataMessage.Flags.END_SESSION;
(this.flags || 0) &
window.textsecure.protobuf.DataMessage.Flags.END_SESSION
);
} }
toProto(): DataMessageClass { toProto(): Proto.DataMessage {
if (this.dataMessage instanceof window.textsecure.protobuf.DataMessage) { if (this.dataMessage instanceof Proto.DataMessage) {
return this.dataMessage; return this.dataMessage;
} }
const proto = new window.textsecure.protobuf.DataMessage(); const proto = new Proto.DataMessage();
proto.timestamp = this.timestamp; proto.timestamp = this.timestamp;
proto.attachments = this.attachmentPointers; proto.attachments = this.attachmentPointers;
@ -330,19 +312,19 @@ class Message {
proto.flags = this.flags; proto.flags = this.flags;
} }
if (this.groupV2) { if (this.groupV2) {
proto.groupV2 = new window.textsecure.protobuf.GroupContextV2(); proto.groupV2 = new Proto.GroupContextV2();
proto.groupV2.masterKey = this.groupV2.masterKey; proto.groupV2.masterKey = this.groupV2.masterKey;
proto.groupV2.revision = this.groupV2.revision; proto.groupV2.revision = this.groupV2.revision;
proto.groupV2.groupChange = this.groupV2.groupChange || null; proto.groupV2.groupChange = this.groupV2.groupChange || null;
} else if (this.group) { } else if (this.group) {
proto.group = new window.textsecure.protobuf.GroupContext(); proto.group = new Proto.GroupContext();
proto.group.id = stringToArrayBuffer(this.group.id); proto.group.id = Bytes.fromString(this.group.id);
proto.group.type = this.group.type; proto.group.type = this.group.type;
} }
if (this.sticker) { if (this.sticker) {
proto.sticker = new window.textsecure.protobuf.DataMessage.Sticker(); proto.sticker = new Proto.DataMessage.Sticker();
proto.sticker.packId = hexToArrayBuffer(this.sticker.packId); proto.sticker.packId = Bytes.fromHex(this.sticker.packId);
proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey); proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey);
proto.sticker.stickerId = this.sticker.stickerId; proto.sticker.stickerId = this.sticker.stickerId;
if (this.sticker.attachmentPointer) { if (this.sticker.attachmentPointer) {
@ -350,7 +332,7 @@ class Message {
} }
} }
if (this.reaction) { if (this.reaction) {
proto.reaction = new window.textsecure.protobuf.DataMessage.Reaction(); proto.reaction = new Proto.DataMessage.Reaction();
proto.reaction.emoji = this.reaction.emoji || null; proto.reaction.emoji = this.reaction.emoji || null;
proto.reaction.remove = this.reaction.remove || false; proto.reaction.remove = this.reaction.remove || false;
proto.reaction.targetAuthorUuid = this.reaction.targetAuthorUuid || null; proto.reaction.targetAuthorUuid = this.reaction.targetAuthorUuid || null;
@ -359,7 +341,7 @@ class Message {
if (Array.isArray(this.preview)) { if (Array.isArray(this.preview)) {
proto.preview = this.preview.map(preview => { proto.preview = this.preview.map(preview => {
const item = new window.textsecure.protobuf.DataMessage.Preview(); const item = new Proto.DataMessage.Preview();
item.title = preview.title; item.title = preview.title;
item.url = preview.url; item.url = preview.url;
item.description = preview.description || null; item.description = preview.description || null;
@ -369,8 +351,8 @@ class Message {
}); });
} }
if (this.quote) { if (this.quote) {
const { QuotedAttachment } = window.textsecure.protobuf.DataMessage.Quote; const { QuotedAttachment } = Proto.DataMessage.Quote;
const { BodyRange, Quote } = window.textsecure.protobuf.DataMessage; const { BodyRange, Quote } = Proto.DataMessage;
proto.quote = new Quote(); proto.quote = new Quote();
const { quote } = proto; const { quote } = proto;
@ -396,24 +378,26 @@ class Message {
const bodyRange = new BodyRange(); const bodyRange = new BodyRange();
bodyRange.start = range.start; bodyRange.start = range.start;
bodyRange.length = range.length; bodyRange.length = range.length;
bodyRange.mentionUuid = range.mentionUuid; if (range.mentionUuid !== undefined) {
bodyRange.mentionUuid = range.mentionUuid;
}
return bodyRange; return bodyRange;
}); });
if ( if (
quote.bodyRanges.length && quote.bodyRanges.length &&
(!proto.requiredProtocolVersion || (!proto.requiredProtocolVersion ||
proto.requiredProtocolVersion < proto.requiredProtocolVersion <
window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS) Proto.DataMessage.ProtocolVersion.MENTIONS)
) { ) {
proto.requiredProtocolVersion = proto.requiredProtocolVersion =
window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS; Proto.DataMessage.ProtocolVersion.MENTIONS;
} }
} }
if (this.expireTimer) { if (this.expireTimer) {
proto.expireTimer = this.expireTimer; proto.expireTimer = this.expireTimer;
} }
if (this.profileKey) { if (this.profileKey) {
proto.profileKey = this.profileKey; proto.profileKey = new FIXMEU8(this.profileKey);
} }
if (this.deletedForEveryoneTimestamp) { if (this.deletedForEveryoneTimestamp) {
proto.delete = { proto.delete = {
@ -422,7 +406,7 @@ class Message {
} }
if (this.mentions) { if (this.mentions) {
proto.requiredProtocolVersion = proto.requiredProtocolVersion =
window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS; Proto.DataMessage.ProtocolVersion.MENTIONS;
proto.bodyRanges = this.mentions.map( proto.bodyRanges = this.mentions.map(
({ start, length, mentionUuid }) => ({ ({ start, length, mentionUuid }) => ({
start, start,
@ -433,7 +417,7 @@ class Message {
} }
if (this.groupCallUpdate) { if (this.groupCallUpdate) {
const { GroupCallUpdate } = window.textsecure.protobuf.DataMessage; const { GroupCallUpdate } = Proto.DataMessage;
const groupCallUpdate = new GroupCallUpdate(); const groupCallUpdate = new GroupCallUpdate();
groupCallUpdate.eraId = this.groupCallUpdate.eraId; groupCallUpdate.eraId = this.groupCallUpdate.eraId;
@ -446,7 +430,9 @@ class Message {
} }
toArrayBuffer() { toArrayBuffer() {
return this.toProto().toArrayBuffer(); return typedArrayToArrayBuffer(
Proto.DataMessage.encode(this.toProto()).finish()
);
} }
} }
@ -492,13 +478,13 @@ export default class MessageSender {
); );
} }
getRandomPadding(): ArrayBuffer { getRandomPadding(): Uint8Array {
// Generate a random int from 1 and 512 // Generate a random int from 1 and 512
const buffer = getRandomBytes(2); const buffer = getRandomBytes(2);
const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1; const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1;
// Generate a random padding buffer of the chosen size // Generate a random padding buffer of the chosen size
return getRandomBytes(paddingLength); return new FIXMEU8(getRandomBytes(paddingLength));
} }
getPaddedAttachment(data: ArrayBuffer): ArrayBuffer { getPaddedAttachment(data: ArrayBuffer): ArrayBuffer {
@ -511,10 +497,11 @@ export default class MessageSender {
async makeAttachmentPointer( async makeAttachmentPointer(
attachment: AttachmentType attachment: AttachmentType
): Promise<AttachmentPointerClass | undefined> { ): Promise<Proto.IAttachmentPointer> {
if (typeof attachment !== 'object' || attachment == null) { assert(
return Promise.resolve(undefined); typeof attachment === 'object' && attachment !== null,
} 'Got null attachment in `makeAttachmentPointer`'
);
const { data, size } = attachment; const { data, size } = attachment;
if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) { if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
@ -535,12 +522,12 @@ export default class MessageSender {
const result = await Crypto.encryptAttachment(padded, key, iv); const result = await Crypto.encryptAttachment(padded, key, iv);
const id = await this.server.putAttachment(result.ciphertext); const id = await this.server.putAttachment(result.ciphertext);
const proto = new window.textsecure.protobuf.AttachmentPointer(); const proto = new Proto.AttachmentPointer();
proto.cdnId = id; proto.cdnId = id;
proto.contentType = attachment.contentType; proto.contentType = attachment.contentType;
proto.key = key; proto.key = new FIXMEU8(key);
proto.size = attachment.size; proto.size = attachment.size;
proto.digest = result.digest; proto.digest = new FIXMEU8(result.digest);
if (attachment.fileName) { if (attachment.fileName) {
proto.fileName = attachment.fileName; proto.fileName = attachment.fileName;
@ -657,11 +644,11 @@ export default class MessageSender {
return message.toArrayBuffer(); return message.toArrayBuffer();
} }
async getContentMessage(options: MessageOptionsType): Promise<ContentClass> { async getContentMessage(options: MessageOptionsType): Promise<Proto.Content> {
const message = await this.getHydratedMessage(options); const message = await this.getHydratedMessage(options);
const dataMessage = message.toProto(); const dataMessage = message.toProto();
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.dataMessage = dataMessage; contentMessage.dataMessage = dataMessage;
return contentMessage; return contentMessage;
@ -685,8 +672,8 @@ export default class MessageSender {
groupMembers: Array<string>; groupMembers: Array<string>;
isTyping: boolean; isTyping: boolean;
timestamp?: number; timestamp?: number;
}): ContentClass { }): Proto.Content {
const ACTION_ENUM = window.textsecure.protobuf.TypingMessage.Action; const ACTION_ENUM = Proto.TypingMessage.Action;
const { recipientId, groupId, isTyping, timestamp } = options; const { recipientId, groupId, isTyping, timestamp } = options;
if (!recipientId && !groupId) { if (!recipientId && !groupId) {
@ -698,12 +685,14 @@ export default class MessageSender {
const finalTimestamp = timestamp || Date.now(); const finalTimestamp = timestamp || Date.now();
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED; const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
const typingMessage = new window.textsecure.protobuf.TypingMessage(); const typingMessage = new Proto.TypingMessage();
typingMessage.groupId = groupId || null; if (groupId) {
typingMessage.groupId = new FIXMEU8(groupId);
}
typingMessage.action = action; typingMessage.action = action;
typingMessage.timestamp = finalTimestamp; typingMessage.timestamp = finalTimestamp;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.typingMessage = typingMessage; contentMessage.typingMessage = typingMessage;
return contentMessage; return contentMessage;
@ -767,7 +756,7 @@ export default class MessageSender {
group: groupV1 group: groupV1
? { ? {
id: groupV1.id, id: groupV1.id,
type: window.textsecure.protobuf.GroupContext.Type.DELIVER, type: Proto.GroupContext.Type.DELIVER,
} }
: undefined, : undefined,
mentions, mentions,
@ -781,8 +770,8 @@ export default class MessageSender {
}; };
} }
createSyncMessage(): SyncMessageClass { createSyncMessage(): Proto.SyncMessage {
const syncMessage = new window.textsecure.protobuf.SyncMessage(); const syncMessage = new Proto.SyncMessage();
syncMessage.padding = this.getRandomPadding(); syncMessage.padding = this.getRandomPadding();
@ -843,7 +832,7 @@ export default class MessageSender {
}: { }: {
timestamp: number; timestamp: number;
recipients: Array<string>; recipients: Array<string>;
proto: ContentClass | DataMessageClass | PlaintextContent; proto: Proto.Content | Proto.DataMessage | PlaintextContent;
contentHint: number; contentHint: number;
groupId: string | undefined; groupId: string | undefined;
callback: (result: CallbackResultType) => void; callback: (result: CallbackResultType) => void;
@ -885,7 +874,7 @@ export default class MessageSender {
}: { }: {
timestamp: number; timestamp: number;
recipients: Array<string>; recipients: Array<string>;
proto: ContentClass | DataMessageClass | PlaintextContent; proto: Proto.Content | Proto.DataMessage | PlaintextContent;
contentHint: number; contentHint: number;
groupId: string | undefined; groupId: string | undefined;
options?: SendOptionsType; options?: SendOptionsType;
@ -920,7 +909,7 @@ export default class MessageSender {
options, options,
}: { }: {
identifier: string | undefined; identifier: string | undefined;
proto: DataMessageClass | ContentClass | PlaintextContent; proto: Proto.DataMessage | Proto.Content | PlaintextContent;
timestamp: number; timestamp: number;
contentHint: number; contentHint: number;
options?: SendOptionsType; options?: SendOptionsType;
@ -1030,10 +1019,10 @@ export default class MessageSender {
return Promise.resolve(); return Promise.resolve();
} }
const dataMessage = window.textsecure.protobuf.DataMessage.decode( const dataMessage = Proto.DataMessage.decode(
encodedDataMessage new FIXMEU8(encodedDataMessage)
); );
const sentMessage = new window.textsecure.protobuf.SyncMessage.Sent(); const sentMessage = new Proto.SyncMessage.Sent();
sentMessage.timestamp = timestamp; sentMessage.timestamp = timestamp;
sentMessage.message = dataMessage; sentMessage.message = dataMessage;
if (destination) { if (destination) {
@ -1063,13 +1052,17 @@ export default class MessageSender {
// number we sent to. // number we sent to.
if (sentTo && sentTo.length) { if (sentTo && sentTo.length) {
sentMessage.unidentifiedStatus = sentTo.map(identifier => { sentMessage.unidentifiedStatus = sentTo.map(identifier => {
const status = new window.textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus(); const status = new Proto.SyncMessage.Sent.UnidentifiedDeliveryStatus();
const conv = window.ConversationController.get(identifier); const conv = window.ConversationController.get(identifier);
if (conv && conv.get('e164')) { if (conv) {
status.destination = conv.get('e164'); const e164 = conv.get('e164');
} if (e164) {
if (conv && conv.get('uuid')) { status.destination = e164;
status.destinationUuid = conv.get('uuid'); }
const uuid = conv.get('uuid');
if (uuid) {
status.destinationUuid = uuid;
}
} }
status.unidentified = Boolean(unidentifiedLookup[identifier]); status.unidentified = Boolean(unidentifiedLookup[identifier]);
return status; return status;
@ -1078,12 +1071,10 @@ export default class MessageSender {
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.sent = sentMessage; syncMessage.sent = sentMessage;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1101,17 +1092,14 @@ export default class MessageSender {
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1) { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new Proto.SyncMessage.Request();
request.type = request.type = Proto.SyncMessage.Request.Type.BLOCKED;
window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.request = request; syncMessage.request = request;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1132,17 +1120,14 @@ export default class MessageSender {
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1) { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new Proto.SyncMessage.Request();
request.type = request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.request = request; syncMessage.request = request;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1163,16 +1148,14 @@ export default class MessageSender {
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1) { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new Proto.SyncMessage.Request();
request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS; request.type = Proto.SyncMessage.Request.Type.GROUPS;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.request = request; syncMessage.request = request;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1194,17 +1177,14 @@ export default class MessageSender {
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice !== 1) { if (myDevice !== 1) {
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new Proto.SyncMessage.Request();
request.type = request.type = Proto.SyncMessage.Request.Type.CONTACTS;
window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.request = request; syncMessage.request = request;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1229,18 +1209,15 @@ export default class MessageSender {
return; return;
} }
const fetchLatest = new window.textsecure.protobuf.SyncMessage.FetchLatest(); const fetchLatest = new Proto.SyncMessage.FetchLatest();
fetchLatest.type = fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
window.textsecure.protobuf.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.fetchLatest = fetchLatest; syncMessage.fetchLatest = fetchLatest;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
await this.sendIndividualProto({ await this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1262,17 +1239,15 @@ export default class MessageSender {
return; return;
} }
const request = new window.textsecure.protobuf.SyncMessage.Request(); const request = new Proto.SyncMessage.Request();
request.type = window.textsecure.protobuf.SyncMessage.Request.Type.KEYS; request.type = Proto.SyncMessage.Request.Type.KEYS;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.request = request; syncMessage.request = request;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
await this.sendIndividualProto({ await this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1295,25 +1270,19 @@ export default class MessageSender {
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const myDevice = window.textsecure.storage.user.getDeviceId(); const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1) { if (myDevice === 1) {
return Promise.resolve(); return;
} }
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.read = []; syncMessage.read = [];
for (let i = 0; i < reads.length; i += 1) { for (let i = 0; i < reads.length; i += 1) {
const read = new window.textsecure.protobuf.SyncMessage.Read(); const proto = new Proto.SyncMessage.Read(reads[i]);
read.timestamp = reads[i].timestamp;
read.sender = reads[i].senderE164 || null;
read.senderUuid = reads[i].senderUuid || null;
syncMessage.read.push(read); syncMessage.read.push(proto);
} }
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1339,18 +1308,18 @@ export default class MessageSender {
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
const viewOnceOpen = new window.textsecure.protobuf.SyncMessage.ViewOnceOpen(); const viewOnceOpen = new Proto.SyncMessage.ViewOnceOpen();
viewOnceOpen.sender = sender || null; if (sender !== undefined) {
viewOnceOpen.senderUuid = senderUuid || null; viewOnceOpen.sender = sender;
viewOnceOpen.timestamp = timestamp || null; }
viewOnceOpen.senderUuid = senderUuid;
viewOnceOpen.timestamp = timestamp;
syncMessage.viewOnceOpen = viewOnceOpen; syncMessage.viewOnceOpen = viewOnceOpen;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1379,19 +1348,23 @@ export default class MessageSender {
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
const response = new window.textsecure.protobuf.SyncMessage.MessageRequestResponse(); const response = new Proto.SyncMessage.MessageRequestResponse();
response.threadE164 = responseArgs.threadE164 || null; if (responseArgs.threadE164 !== undefined) {
response.threadUuid = responseArgs.threadUuid || null; response.threadE164 = responseArgs.threadE164;
response.groupId = responseArgs.groupId || null; }
if (responseArgs.threadUuid !== undefined) {
response.threadUuid = responseArgs.threadUuid;
}
if (responseArgs.groupId) {
response.groupId = new FIXMEU8(responseArgs.groupId);
}
response.type = responseArgs.type; response.type = responseArgs.type;
syncMessage.messageRequestResponse = response; syncMessage.messageRequestResponse = response;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1417,15 +1390,14 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber(); const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
const ENUM = const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
window.textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
const packOperations = operations.map(item => { const packOperations = operations.map(item => {
const { packId, packKey, installed } = item; const { packId, packKey, installed } = item;
const operation = new window.textsecure.protobuf.SyncMessage.StickerPackOperation(); const operation = new Proto.SyncMessage.StickerPackOperation();
operation.packId = hexToArrayBuffer(packId); operation.packId = Bytes.fromHex(packId);
operation.packKey = base64ToArrayBuffer(packKey); operation.packKey = Bytes.fromBase64(packKey);
operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE; operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE;
return operation; return operation;
@ -1434,12 +1406,10 @@ export default class MessageSender {
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.stickerPackOperation = packOperations; syncMessage.stickerPackOperation = packOperations;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1476,7 +1446,7 @@ export default class MessageSender {
); );
return promise.then(async () => { return promise.then(async () => {
const verified = new window.textsecure.protobuf.Verified(); const verified = new Proto.Verified();
verified.state = state; verified.state = state;
if (destinationE164) { if (destinationE164) {
verified.destination = destinationE164; verified.destination = destinationE164;
@ -1484,18 +1454,16 @@ export default class MessageSender {
if (destinationUuid) { if (destinationUuid) {
verified.destinationUuid = destinationUuid; verified.destinationUuid = destinationUuid;
} }
verified.identityKey = identityKey; verified.identityKey = new FIXMEU8(identityKey);
verified.nullMessage = padding; verified.nullMessage = padding;
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
syncMessage.verified = verified; syncMessage.verified = verified;
const secondMessage = new window.textsecure.protobuf.Content(); const secondMessage = new Proto.Content();
secondMessage.syncMessage = syncMessage; secondMessage.syncMessage = syncMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
await this.sendIndividualProto({ await this.sendIndividualProto({
identifier: myUuid || myNumber, identifier: myUuid || myNumber,
@ -1515,21 +1483,19 @@ export default class MessageSender {
options: SendOptionsType, options: SendOptionsType,
groupId?: string groupId?: string
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendMessage({ return this.sendMessage({
messageOptions: { messageOptions: {
recipients, recipients,
timestamp: Date.now(), timestamp: Date.now(),
profileKey, profileKey,
flags: window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE, flags: Proto.DataMessage.Flags.PROFILE_KEY_UPDATE,
...(groupId ...(groupId
? { ? {
group: { group: {
id: groupId, id: groupId,
type: window.textsecure.protobuf.GroupContext.Type.DELIVER, type: Proto.GroupContext.Type.DELIVER,
}, },
} }
: {}), : {}),
@ -1542,18 +1508,16 @@ export default class MessageSender {
async sendCallingMessage( async sendCallingMessage(
recipientId: string, recipientId: string,
callingMessage: CallingMessageClass, callingMessage: Proto.ICallingMessage,
options?: SendOptionsType options?: SendOptionsType
): Promise<void> { ): Promise<void> {
const recipients = [recipientId]; const recipients = [recipientId];
const finalTimestamp = Date.now(); const finalTimestamp = Date.now();
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.callingMessage = callingMessage; contentMessage.callingMessage = callingMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
await this.sendMessageProtoAndWait({ await this.sendMessageProtoAndWait({
timestamp: finalTimestamp, timestamp: finalTimestamp,
@ -1583,17 +1547,14 @@ export default class MessageSender {
return Promise.resolve(); return Promise.resolve();
} }
const receiptMessage = new window.textsecure.protobuf.ReceiptMessage(); const receiptMessage = new Proto.ReceiptMessage();
receiptMessage.type = receiptMessage.type = Proto.ReceiptMessage.Type.DELIVERY;
window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY;
receiptMessage.timestamp = timestamps; receiptMessage.timestamp = timestamps;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.receiptMessage = receiptMessage; contentMessage.receiptMessage = receiptMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: uuid || e164, identifier: uuid || e164,
@ -1615,16 +1576,14 @@ export default class MessageSender {
timestamps: Array<number>; timestamps: Array<number>;
options?: SendOptionsType; options?: SendOptionsType;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
const receiptMessage = new window.textsecure.protobuf.ReceiptMessage(); const receiptMessage = new Proto.ReceiptMessage();
receiptMessage.type = window.textsecure.protobuf.ReceiptMessage.Type.READ; receiptMessage.type = Proto.ReceiptMessage.Type.READ;
receiptMessage.timestamp = timestamps; receiptMessage.timestamp = timestamps;
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.receiptMessage = receiptMessage; contentMessage.receiptMessage = receiptMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({ return this.sendIndividualProto({
identifier: senderUuid || senderE164, identifier: senderUuid || senderE164,
@ -1640,10 +1599,10 @@ export default class MessageSender {
uuid, uuid,
e164, e164,
padding, padding,
}: { uuid?: string; e164?: string; padding?: ArrayBuffer }, }: { uuid?: string; e164?: string; padding?: Uint8Array },
options?: SendOptionsType options?: SendOptionsType
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const nullMessage = new window.textsecure.protobuf.NullMessage(); const nullMessage = new Proto.NullMessage();
const identifier = uuid || e164; const identifier = uuid || e164;
if (!identifier) { if (!identifier) {
@ -1652,12 +1611,10 @@ export default class MessageSender {
nullMessage.padding = padding || this.getRandomPadding(); nullMessage.padding = padding || this.getRandomPadding();
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
contentMessage.nullMessage = nullMessage; contentMessage.nullMessage = nullMessage;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
// We want the NullMessage to look like a normal outgoing message // We want the NullMessage to look like a normal outgoing message
const timestamp = Date.now(); const timestamp = Date.now();
@ -1679,9 +1636,9 @@ export default class MessageSender {
CallbackResultType | void | Array<CallbackResultType | void | Array<void>> CallbackResultType | void | Array<CallbackResultType | void | Array<void>>
> { > {
window.log.info('resetSession: start'); window.log.info('resetSession: start');
const proto = new window.textsecure.protobuf.DataMessage(); const proto = new Proto.DataMessage();
proto.body = 'TERMINATE'; proto.body = 'TERMINATE';
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION; proto.flags = Proto.DataMessage.Flags.END_SESSION;
proto.timestamp = timestamp; proto.timestamp = timestamp;
const identifier = uuid || e164; const identifier = uuid || e164;
@ -1691,9 +1648,7 @@ export default class MessageSender {
throw error; throw error;
}; };
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
const sendToContactPromise = window.textsecure.storage.protocol const sendToContactPromise = window.textsecure.storage.protocol
.archiveAllSessions(identifier) .archiveAllSessions(identifier)
@ -1723,7 +1678,9 @@ export default class MessageSender {
return sendToContactPromise; return sendToContactPromise;
} }
const buffer = proto.toArrayBuffer(); const buffer = typedArrayToArrayBuffer(
Proto.DataMessage.encode(proto).finish()
);
const sendSyncPromise = this.sendSyncMessage({ const sendSyncPromise = this.sendSyncMessage({
encodedDataMessage: buffer, encodedDataMessage: buffer,
timestamp, timestamp,
@ -1745,9 +1702,7 @@ export default class MessageSender {
profileKey?: ArrayBuffer, profileKey?: ArrayBuffer,
options?: SendOptionsType options?: SendOptionsType
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendMessage({ return this.sendMessage({
messageOptions: { messageOptions: {
@ -1755,8 +1710,7 @@ export default class MessageSender {
timestamp, timestamp,
expireTimer, expireTimer,
profileKey, profileKey,
flags: flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
}, },
contentHint: ContentHint.DEFAULT, contentHint: ContentHint.DEFAULT,
groupId: undefined, groupId: undefined,
@ -1773,9 +1727,7 @@ export default class MessageSender {
plaintext: PlaintextContent; plaintext: PlaintextContent;
uuid: string; uuid: string;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendMessageProtoAndWait({ return this.sendMessageProtoAndWait({
timestamp: Date.now(), timestamp: Date.now(),
@ -1799,13 +1751,17 @@ export default class MessageSender {
options, options,
}: { }: {
recipients: Array<string>; recipients: Array<string>;
proto: ContentClass; proto: Proto.Content;
timestamp: number; timestamp: number;
contentHint: number; contentHint: number;
groupId: string | undefined; groupId: string | undefined;
options?: SendOptionsType; options?: SendOptionsType;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
const dataMessage = proto.dataMessage?.toArrayBuffer(); const dataMessage = proto.dataMessage
? typedArrayToArrayBuffer(
Proto.DataMessage.encode(proto.dataMessage).finish()
)
: undefined;
const myE164 = window.textsecure.storage.user.getNumber(); const myE164 = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid(); const myUuid = window.textsecure.storage.user.getUuid();
@ -1887,14 +1843,12 @@ export default class MessageSender {
}, },
options?: SendOptionsType options?: SendOptionsType
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const contentMessage = new window.textsecure.protobuf.Content(); const contentMessage = new Proto.Content();
const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage( const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage(
distributionId distributionId
); );
contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap( contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
typedArrayToArrayBuffer(senderKeyDistributionMessage.serialize())
);
return this.sendGroupProto({ return this.sendGroupProto({
recipients: identifiers, recipients: identifiers,
@ -1913,14 +1867,16 @@ export default class MessageSender {
groupIdentifiers: Array<string>, groupIdentifiers: Array<string>,
options?: SendOptionsType options?: SendOptionsType
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const proto = new window.textsecure.protobuf.DataMessage(); const proto = new Proto.Content({
proto.group = new window.textsecure.protobuf.GroupContext(); dataMessage: {
proto.group.id = stringToArrayBuffer(groupId); group: {
proto.group.type = window.textsecure.protobuf.GroupContext.Type.QUIT; id: Bytes.fromString(groupId),
type: Proto.GroupContext.Type.QUIT,
},
},
});
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendGroupProto({ return this.sendGroupProto({
recipients: groupIdentifiers, recipients: groupIdentifiers,
proto, proto,
@ -1949,11 +1905,10 @@ export default class MessageSender {
timestamp, timestamp,
expireTimer, expireTimer,
profileKey, profileKey,
flags: flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
group: { group: {
id: groupId, id: groupId,
type: window.textsecure.protobuf.GroupContext.Type.DELIVER, type: Proto.GroupContext.Type.DELIVER,
}, },
}; };
@ -1967,9 +1922,7 @@ export default class MessageSender {
}); });
} }
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
return this.sendMessage({ return this.sendMessage({
messageOptions, messageOptions,
contentHint: ContentHint.DEFAULT, contentHint: ContentHint.DEFAULT,

View file

@ -6,8 +6,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import EventTarget from './EventTarget'; import EventTarget, { EventHandler } from './EventTarget';
import MessageReceiver from './MessageReceiver'; import MessageReceiver from './MessageReceiver';
import { ContactSyncEvent, GroupSyncEvent } from './messageReceiverEvents';
import MessageSender from './SendMessage'; import MessageSender from './SendMessage';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
@ -20,9 +21,9 @@ class SyncRequestInner extends EventTarget {
timeout: any; timeout: any;
oncontact: Function; oncontact: (event: ContactSyncEvent) => void;
ongroup: Function; ongroup: (event: GroupSyncEvent) => void;
timeoutMillis: number; timeoutMillis: number;
@ -43,10 +44,10 @@ class SyncRequestInner extends EventTarget {
} }
this.oncontact = this.onContactSyncComplete.bind(this); this.oncontact = this.onContactSyncComplete.bind(this);
receiver.addEventListener('contactsync', this.oncontact); receiver.addEventListener('contactSync', this.oncontact);
this.ongroup = this.onGroupSyncComplete.bind(this); this.ongroup = this.onGroupSyncComplete.bind(this);
receiver.addEventListener('groupsync', this.ongroup); receiver.addEventListener('groupSync', this.ongroup);
this.timeoutMillis = timeoutMillis || 60000; this.timeoutMillis = timeoutMillis || 60000;
} }
@ -126,9 +127,15 @@ class SyncRequestInner extends EventTarget {
export default class SyncRequest { export default class SyncRequest {
private inner: SyncRequestInner; private inner: SyncRequestInner;
addEventListener: (name: string, handler: Function) => void; addEventListener: (
name: 'success' | 'timeout',
handler: EventHandler
) => void;
removeEventListener: (name: string, handler: Function) => void; removeEventListener: (
name: 'success' | 'timeout',
handler: EventHandler
) => void;
constructor( constructor(
sender: MessageSender, sender: MessageSender,

View file

@ -1,6 +1,8 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { SignalService as Proto } from '../protobuf';
export { export {
IdentityKeyType, IdentityKeyType,
PreKeyType, PreKeyType,
@ -56,3 +58,152 @@ export type OuterSignedPrekeyType = {
}; };
export type SessionResetsType = Record<string, number>; export type SessionResetsType = Record<string, number>;
export type ProcessedEnvelope = Readonly<{
id: string;
receivedAtCounter: number;
receivedAtDate: number;
messageAgeSec: number;
// Mostly from Proto.Envelope except for null/undefined
type: Proto.Envelope.Type;
source?: string;
sourceUuid?: string;
sourceDevice?: number;
timestamp: number;
legacyMessage?: Uint8Array;
content?: Uint8Array;
serverGuid: string;
serverTimestamp: number;
}>;
export type ProcessedAttachment = {
cdnId?: string;
cdnKey?: string;
digest?: string;
contentType?: string;
key?: string;
size?: number;
fileName?: string;
flags?: number;
width?: number;
height?: number;
caption?: string;
blurHash?: string;
cdnNumber?: number;
};
export type ProcessedGroupContext = {
id: string;
type: Proto.GroupContext.Type;
name?: string;
membersE164: ReadonlyArray<string>;
avatar?: ProcessedAttachment;
// Computed fields
derivedGroupV2Id: string;
};
export type ProcessedGroupV2Context = {
masterKey: string;
revision?: number;
groupChange?: string;
// Computed fields
id: string;
secretParams: string;
publicParams: string;
};
export type ProcessedQuoteAttachment = {
contentType?: string;
fileName?: string;
thumbnail?: ProcessedAttachment;
};
export type ProcessedQuote = {
id?: number;
authorUuid?: string;
text?: string;
attachments: ReadonlyArray<ProcessedQuoteAttachment>;
bodyRanges: ReadonlyArray<Proto.DataMessage.IBodyRange>;
};
export type ProcessedAvatar = {
avatar?: ProcessedAttachment;
isProfile: boolean;
};
export type ProcessedContact = Omit<Proto.DataMessage.IContact, 'avatar'> & {
avatar?: ProcessedAvatar;
};
export type ProcessedPreview = {
url?: string;
title?: string;
image?: ProcessedAttachment;
description?: string;
date?: number;
};
export type ProcessedSticker = {
packId?: string;
packKey?: string;
stickerId?: number;
data?: ProcessedAttachment;
};
export type ProcessedReaction = {
emoji?: string;
remove: boolean;
targetAuthorUuid?: string;
targetTimestamp?: number;
};
export type ProcessedDelete = {
targetSentTimestamp?: number;
};
export type ProcessedBodyRange = Proto.DataMessage.IBodyRange;
export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate;
export type ProcessedDataMessage = {
body?: string;
attachments: ReadonlyArray<ProcessedAttachment>;
group?: ProcessedGroupContext;
groupV2?: ProcessedGroupV2Context;
flags: number;
expireTimer: number;
profileKey?: string;
timestamp: number;
quote?: ProcessedQuote;
contact?: ReadonlyArray<ProcessedContact>;
preview?: ReadonlyArray<ProcessedPreview>;
sticker?: ProcessedSticker;
requiredProtocolVersion?: number;
isViewOnce: boolean;
reaction?: ProcessedReaction;
delete?: ProcessedDelete;
bodyRanges?: ReadonlyArray<ProcessedBodyRange>;
groupCallUpdate?: ProcessedGroupCallUpdate;
};
export type ProcessedUnidentifiedDeliveryStatus = Omit<
Proto.SyncMessage.Sent.IUnidentifiedDeliveryStatus,
'destinationUuid'
> & {
destinationUuid?: string;
};
export type ProcessedSent = Omit<
Proto.SyncMessage.ISent,
'destinationId' | 'unidentifiedStatus'
> & {
destinationId?: string;
unidentifiedStatus?: Array<ProcessedUnidentifiedDeliveryStatus>;
};
export type ProcessedSyncMessage = Omit<Proto.ISyncMessage, 'sent'> & {
sent?: ProcessedSent;
};

View file

@ -33,7 +33,7 @@ import { Long } from '../window.d';
import { assert } from '../util/assert'; import { assert } from '../util/assert';
import { getUserAgent } from '../util/getUserAgent'; import { getUserAgent } from '../util/getUserAgent';
import { toWebSafeBase64 } from '../util/webSafeBase64'; import { toWebSafeBase64 } from '../util/webSafeBase64';
import { isPackIdValid, redactPackId } from '../../js/modules/stickers'; import { isPackIdValid, redactPackId } from '../types/Stickers';
import { import {
arrayBufferToBase64, arrayBufferToBase64,
base64ToArrayBuffer, base64ToArrayBuffer,
@ -53,7 +53,6 @@ import { calculateAgreement, generateKeyPair } from '../Curve';
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch'; import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
import { import {
AvatarUploadAttributesClass,
StorageServiceCallOptionsType, StorageServiceCallOptionsType,
StorageServiceCredentials, StorageServiceCredentials,
} from '../textsecure.d'; } from '../textsecure.d';
@ -2161,7 +2160,7 @@ export function initialize({
return Proto.GroupExternalCredential.decode(new FIXMEU8(response)); return Proto.GroupExternalCredential.decode(new FIXMEU8(response));
} }
function verifyAttributes(attributes: AvatarUploadAttributesClass) { function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) {
const { const {
key, key,
credential, credential,
@ -2213,8 +2212,8 @@ export function initialize({
responseType: 'arraybuffer', responseType: 'arraybuffer',
host: storageUrl, host: storageUrl,
}); });
const attributes = window.textsecure.protobuf.AvatarUploadAttributes.decode( const attributes = Proto.AvatarUploadAttributes.decode(
response new FIXMEU8(response)
); );
const verified = verifyAttributes(attributes); const verified = verifyAttributes(attributes);

View file

@ -26,7 +26,7 @@
import { connection as WebSocket, IMessage } from 'websocket'; import { connection as WebSocket, IMessage } from 'websocket';
import EventTarget from './EventTarget'; import EventTarget, { EventHandler } from './EventTarget';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
import { isOlderThan } from '../util/timestamp'; import { isOlderThan } from '../util/timestamp';
@ -120,6 +120,12 @@ export type WebSocketResourceOptions = {
keepalive?: KeepAliveOptionsType | true; keepalive?: KeepAliveOptionsType | true;
}; };
export class CloseEvent extends Event {
constructor(public readonly code: number, public readonly reason: string) {
super('close');
}
}
export default class WebSocketResource extends EventTarget { export default class WebSocketResource extends EventTarget {
private outgoingId = 1; private outgoingId = 1;
@ -159,6 +165,15 @@ export default class WebSocketResource extends EventTarget {
}); });
} }
public addEventListener(
name: 'close',
handler: (ev: CloseEvent) => void
): void;
public addEventListener(name: string, handler: EventHandler): void {
return super.addEventListener(name, handler);
}
public sendRequest( public sendRequest(
options: OutgoingWebSocketRequestOptions options: OutgoingWebSocketRequestOptions
): OutgoingWebSocketRequest { ): OutgoingWebSocketRequest {
@ -204,10 +219,7 @@ export default class WebSocketResource extends EventTarget {
} }
window.log.warn('Dispatching our own socket close event'); window.log.warn('Dispatching our own socket close event');
const ev = new Event('close'); this.dispatchEvent(new CloseEvent(code, reason || 'normal'));
ev.code = code;
ev.reason = reason;
this.dispatchEvent(ev);
}, 5000); }, 5000);
} }

View file

@ -0,0 +1,370 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { SignalService as Proto } from '../protobuf';
import { ProcessedDataMessage, ProcessedSent } from './Types.d';
import type {
ModifiedContactDetails,
ModifiedGroupDetails,
} from './ContactsParser';
export class ReconnectEvent extends Event {
constructor() {
super('reconnect');
}
}
export class EmptyEvent extends Event {
constructor() {
super('empty');
}
}
export class ProgressEvent extends Event {
public readonly count: number;
constructor({ count }: { count: number }) {
super('progress');
this.count = count;
}
}
export type TypingEventData = Readonly<{
typingMessage: Proto.ITypingMessage;
timestamp: number;
started: boolean;
stopped: boolean;
groupId?: string;
groupV2Id?: string;
}>;
export type TypingEventConfig = {
sender?: string;
senderUuid?: string;
senderDevice: number;
typing: TypingEventData;
};
export class TypingEvent extends Event {
public readonly sender?: string;
public readonly senderUuid?: string;
public readonly senderDevice: number;
public readonly typing: TypingEventData;
constructor({ sender, senderUuid, senderDevice, typing }: TypingEventConfig) {
super('typing');
this.sender = sender;
this.senderUuid = senderUuid;
this.senderDevice = senderDevice;
this.typing = typing;
}
}
export class ErrorEvent extends Event {
constructor(public readonly error: Error) {
super('error');
}
}
export type DecryptionErrorEventData = Readonly<{
cipherTextBytes?: ArrayBuffer;
cipherTextType?: number;
contentHint?: number;
groupId?: string;
receivedAtCounter: number;
receivedAtDate: number;
senderDevice: number;
senderUuid: string;
timestamp: number;
}>;
export class DecryptionErrorEvent extends Event {
constructor(public readonly decryptionError: DecryptionErrorEventData) {
super('decryption-error');
}
}
export type RetryRequestEventData = Readonly<{
groupId?: string;
requesterUuid: string;
requesterDevice: number;
senderDevice: number;
sentAt: number;
}>;
export class RetryRequestEvent extends Event {
constructor(public readonly retryRequest: RetryRequestEventData) {
super('retry-request');
}
}
export class ContactEvent extends Event {
constructor(public readonly contactDetails: ModifiedContactDetails) {
super('contact');
}
}
export class ContactSyncEvent extends Event {
constructor() {
super('contactSync');
}
}
export class GroupEvent extends Event {
constructor(public readonly groupDetails: ModifiedGroupDetails) {
super('group');
}
}
export class GroupSyncEvent extends Event {
constructor() {
super('groupSync');
}
}
//
// Confirmable events below
//
export type ConfirmCallback = () => void;
export class ConfirmableEvent extends Event {
constructor(type: string, public readonly confirm: ConfirmCallback) {
super(type);
}
}
export type DeliveryEventData = Readonly<{
timestamp: number;
envelopeTimestamp?: number;
source?: string;
sourceUuid?: string;
sourceDevice?: number;
}>;
export class DeliveryEvent extends ConfirmableEvent {
constructor(
public readonly deliveryReceipt: DeliveryEventData,
confirm: ConfirmCallback
) {
super('delivery', confirm);
}
}
export type SentEventData = Readonly<{
destination?: string;
destinationUuid?: string;
timestamp?: number;
serverTimestamp?: number;
device?: number;
unidentifiedStatus: ProcessedSent['unidentifiedStatus'];
message: ProcessedDataMessage;
isRecipientUpdate: boolean;
receivedAtCounter: number;
receivedAtDate: number;
expirationStartTimestamp?: number;
}>;
export class SentEvent extends ConfirmableEvent {
constructor(public readonly data: SentEventData, confirm: ConfirmCallback) {
super('sent', confirm);
}
}
export type ProfileKeyUpdateData = Readonly<{
source?: string;
sourceUuid?: string;
profileKey: string;
}>;
export class ProfileKeyUpdateEvent extends ConfirmableEvent {
constructor(
public readonly data: ProfileKeyUpdateData,
confirm: ConfirmCallback
) {
super('profileKeyUpdate', confirm);
}
}
export type MessageEventData = Readonly<{
source?: string;
sourceUuid?: string;
sourceDevice?: number;
timestamp: number;
serverGuid?: string;
serverTimestamp?: number;
unidentifiedDeliveryReceived: boolean;
message: ProcessedDataMessage;
receivedAtCounter: number;
receivedAtDate: number;
}>;
export class MessageEvent extends ConfirmableEvent {
constructor(
public readonly data: MessageEventData,
confirm: ConfirmCallback
) {
super('message', confirm);
}
}
export type ReadEventData = Readonly<{
timestamp: number;
envelopeTimestamp: number;
source?: string;
sourceUuid?: string;
}>;
export class ReadEvent extends ConfirmableEvent {
constructor(public readonly read: ReadEventData, confirm: ConfirmCallback) {
super('read', confirm);
}
}
export class ConfigurationEvent extends ConfirmableEvent {
constructor(
public readonly configuration: Proto.SyncMessage.IConfiguration,
confirm: ConfirmCallback
) {
super('configuration', confirm);
}
}
export type ViewSyncOptions = {
source?: string;
sourceUuid?: string;
timestamp?: number;
};
export class ViewSyncEvent extends ConfirmableEvent {
public readonly source?: string;
public readonly sourceUuid?: string;
public readonly timestamp?: number;
constructor(
{ source, sourceUuid, timestamp }: ViewSyncOptions,
confirm: ConfirmCallback
) {
super('viewSync', confirm);
this.source = source;
this.sourceUuid = sourceUuid;
this.timestamp = timestamp;
}
}
export type MessageRequestResponseOptions = {
threadE164?: string;
threadUuid?: string;
messageRequestResponseType: Proto.SyncMessage.IMessageRequestResponse['type'];
groupId?: string;
groupV2Id?: string;
};
export class MessageRequestResponseEvent extends ConfirmableEvent {
public readonly threadE164?: string;
public readonly threadUuid?: string;
public readonly messageRequestResponseType?: MessageRequestResponseOptions['messageRequestResponseType'];
public readonly groupId?: string;
public readonly groupV2Id?: string;
constructor(
{
threadE164,
threadUuid,
messageRequestResponseType,
groupId,
groupV2Id,
}: MessageRequestResponseOptions,
confirm: ConfirmCallback
) {
super('messageRequestResponse', confirm);
this.threadE164 = threadE164;
this.threadUuid = threadUuid;
this.messageRequestResponseType = messageRequestResponseType;
this.groupId = groupId;
this.groupV2Id = groupV2Id;
}
}
export class FetchLatestEvent extends ConfirmableEvent {
constructor(
public readonly eventType: Proto.SyncMessage.IFetchLatest['type'],
confirm: ConfirmCallback
) {
super('fetchLatest', confirm);
}
}
export class KeysEvent extends ConfirmableEvent {
constructor(
public readonly storageServiceKey: ArrayBuffer,
confirm: ConfirmCallback
) {
super('keys', confirm);
}
}
export type StickerPackEventData = Readonly<{
id?: string;
key?: string;
isInstall: boolean;
isRemove: boolean;
}>;
export class StickerPackEvent extends ConfirmableEvent {
constructor(
public readonly stickerPacks: ReadonlyArray<StickerPackEventData>,
confirm: ConfirmCallback
) {
super('sticker-pack', confirm);
}
}
export type VerifiedEventData = Readonly<{
state: Proto.IVerified['state'];
destination?: string;
destinationUuid?: string;
identityKey?: ArrayBuffer;
// Used in `ts/background.ts`
viaContactSync?: boolean;
}>;
export class VerifiedEvent extends ConfirmableEvent {
constructor(
public readonly verified: VerifiedEventData,
confirm: ConfirmCallback
) {
super('verified', confirm);
}
}
export type ReadSyncEventData = Readonly<{
timestamp?: number;
envelopeTimestamp: number;
sender?: string;
senderUuid?: string;
}>;
export class ReadSyncEvent extends ConfirmableEvent {
constructor(
public readonly read: ReadSyncEventData,
confirm: ConfirmCallback
) {
super('readSync', confirm);
}
}

View file

@ -0,0 +1,352 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Long from 'long';
import { assert, strictAssert } from '../util/assert';
import { dropNull, shallowDropNull } from '../util/dropNull';
import { normalizeNumber } from '../util/normalizeNumber';
import { SignalService as Proto } from '../protobuf';
import { deriveGroupFields } from '../groups';
import * as Bytes from '../Bytes';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
import {
ProcessedAttachment,
ProcessedDataMessage,
ProcessedGroupContext,
ProcessedGroupV2Context,
ProcessedQuote,
ProcessedContact,
ProcessedPreview,
ProcessedSticker,
ProcessedReaction,
ProcessedDelete,
} from './Types.d';
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
const FLAGS = Proto.DataMessage.Flags;
export const ATTACHMENT_MAX = 32;
export function processAttachment(
attachment: Proto.IAttachmentPointer
): ProcessedAttachment;
export function processAttachment(
attachment?: Proto.IAttachmentPointer | null
): ProcessedAttachment | undefined;
export function processAttachment(
attachment?: Proto.IAttachmentPointer | null
): ProcessedAttachment | undefined {
if (!attachment) {
return undefined;
}
return {
...shallowDropNull(attachment),
cdnId: attachment.cdnId ? attachment.cdnId.toString() : undefined,
key: attachment.key ? Bytes.toBase64(attachment.key) : undefined,
digest: attachment.digest ? Bytes.toBase64(attachment.digest) : undefined,
};
}
async function processGroupContext(
group?: Proto.IGroupContext | null
): Promise<ProcessedGroupContext | undefined> {
if (!group) {
return undefined;
}
strictAssert(group.id, 'group context without id');
strictAssert(
group.type !== undefined && group.type !== null,
'group context without type'
);
const masterKey = await deriveMasterKeyFromGroupV1(
typedArrayToArrayBuffer(group.id)
);
const data = deriveGroupFields(new FIXMEU8(masterKey));
const derivedGroupV2Id = Bytes.toBase64(data.id);
const result: ProcessedGroupContext = {
id: Bytes.toBinary(group.id),
type: group.type,
name: dropNull(group.name),
membersE164: group.membersE164 ?? [],
avatar: processAttachment(group.avatar),
derivedGroupV2Id,
};
if (result.type === Proto.GroupContext.Type.DELIVER) {
result.name = undefined;
result.membersE164 = [];
result.avatar = undefined;
}
return result;
}
export function processGroupV2Context(
groupV2?: Proto.IGroupContextV2 | null
): ProcessedGroupV2Context | undefined {
if (!groupV2) {
return undefined;
}
strictAssert(groupV2.masterKey, 'groupV2 context without masterKey');
const data = deriveGroupFields(groupV2.masterKey);
return {
masterKey: Bytes.toBase64(groupV2.masterKey),
revision: dropNull(groupV2.revision),
groupChange: groupV2.groupChange
? Bytes.toBase64(groupV2.groupChange)
: undefined,
id: Bytes.toBase64(data.id),
secretParams: Bytes.toBase64(data.secretParams),
publicParams: Bytes.toBase64(data.publicParams),
};
}
export function processQuote(
quote?: Proto.DataMessage.IQuote | null
): ProcessedQuote | undefined {
if (!quote) {
return undefined;
}
return {
id: normalizeNumber(dropNull(quote.id)),
authorUuid: dropNull(quote.authorUuid),
text: dropNull(quote.text),
attachments: (quote.attachments ?? []).map(attachment => {
return {
contentType: dropNull(attachment.contentType),
fileName: dropNull(attachment.fileName),
thumbnail: processAttachment(attachment.thumbnail),
};
}),
bodyRanges: quote.bodyRanges ?? [],
};
}
export function processContact(
contact?: ReadonlyArray<Proto.DataMessage.IContact> | null
): ReadonlyArray<ProcessedContact> | undefined {
if (!contact) {
return undefined;
}
return contact.map(item => {
return {
...item,
avatar: item.avatar
? {
avatar: processAttachment(item.avatar.avatar),
isProfile: Boolean(item.avatar.isProfile),
}
: undefined,
};
});
}
function isLinkPreviewDateValid(value: unknown): value is number {
return (
typeof value === 'number' &&
!Number.isNaN(value) &&
Number.isFinite(value) &&
value > 0
);
}
function cleanLinkPreviewDate(
value?: Long | number | null
): number | undefined {
const result = normalizeNumber(value ?? undefined);
return isLinkPreviewDateValid(result) ? result : undefined;
}
export function processPreview(
preview?: ReadonlyArray<Proto.DataMessage.IPreview> | null
): ReadonlyArray<ProcessedPreview> | undefined {
if (!preview) {
return undefined;
}
return preview.map(item => {
return {
url: dropNull(item.url),
title: dropNull(item.title),
image: item.image ? processAttachment(item.image) : undefined,
description: dropNull(item.description),
date: cleanLinkPreviewDate(item.date),
};
});
}
export function processSticker(
sticker?: Proto.DataMessage.ISticker | null
): ProcessedSticker | undefined {
if (!sticker) {
return undefined;
}
return {
packId: sticker.packId ? Bytes.toHex(sticker.packId) : undefined,
packKey: sticker.packKey ? Bytes.toBase64(sticker.packKey) : undefined,
stickerId: normalizeNumber(dropNull(sticker.stickerId)),
data: processAttachment(sticker.data),
};
}
export function processReaction(
reaction?: Proto.DataMessage.IReaction | null
): ProcessedReaction | undefined {
if (!reaction) {
return undefined;
}
return {
emoji: dropNull(reaction.emoji),
remove: Boolean(reaction.remove),
targetAuthorUuid: dropNull(reaction.targetAuthorUuid),
targetTimestamp: normalizeNumber(dropNull(reaction.targetTimestamp)),
};
}
export function processDelete(
del?: Proto.DataMessage.IDelete | null
): ProcessedDelete | undefined {
if (!del) {
return undefined;
}
return {
targetSentTimestamp: normalizeNumber(dropNull(del.targetSentTimestamp)),
};
}
export async function processDataMessage(
message: Proto.IDataMessage,
envelopeTimestamp: number
): Promise<ProcessedDataMessage> {
/* eslint-disable no-bitwise */
// Now that its decrypted, validate the message and clean it up for consumer
// processing
// Note that messages may (generally) only perform one action and we ignore remaining
// fields after the first action.
if (!message.timestamp) {
throw new Error('Missing timestamp on dataMessage');
}
const timestamp = normalizeNumber(message.timestamp);
if (envelopeTimestamp !== timestamp) {
throw new Error(
`Timestamp ${timestamp} in DataMessage did not ` +
`match envelope timestamp ${envelopeTimestamp}`
);
}
const result: ProcessedDataMessage = {
body: dropNull(message.body),
attachments: (
message.attachments ?? []
).map((attachment: Proto.IAttachmentPointer) =>
processAttachment(attachment)
),
group: await processGroupContext(message.group),
groupV2: processGroupV2Context(message.groupV2),
flags: message.flags ?? 0,
expireTimer: message.expireTimer ?? 0,
profileKey: message.profileKey
? Bytes.toBase64(message.profileKey)
: undefined,
timestamp,
quote: processQuote(message.quote),
contact: processContact(message.contact),
preview: processPreview(message.preview),
sticker: processSticker(message.sticker),
requiredProtocolVersion: normalizeNumber(
dropNull(message.requiredProtocolVersion)
),
isViewOnce: Boolean(message.isViewOnce),
reaction: processReaction(message.reaction),
delete: processDelete(message.delete),
bodyRanges: message.bodyRanges ?? [],
groupCallUpdate: dropNull(message.groupCallUpdate),
};
const isEndSession = Boolean(result.flags & FLAGS.END_SESSION);
const isExpirationTimerUpdate = Boolean(
result.flags & FLAGS.EXPIRATION_TIMER_UPDATE
);
const isProfileKeyUpdate = Boolean(result.flags & FLAGS.PROFILE_KEY_UPDATE);
// The following assertion codifies an assumption: 0 or 1 flags are set, but never
// more. This assumption is fine as of this writing, but may not always be.
const flagCount = [
isEndSession,
isExpirationTimerUpdate,
isProfileKeyUpdate,
].filter(Boolean).length;
assert(
flagCount <= 1,
`Expected exactly <=1 flags to be set, but got ${flagCount}`
);
if (isEndSession) {
result.body = undefined;
result.attachments = [];
result.group = undefined;
return result;
}
if (isExpirationTimerUpdate) {
result.body = undefined;
result.attachments = [];
} else if (isProfileKeyUpdate) {
result.body = undefined;
result.attachments = [];
} else if (result.flags !== 0) {
throw new Error(`Unknown flags in message: ${result.flags}`);
}
if (result.group) {
switch (result.group.type) {
case Proto.GroupContext.Type.UPDATE:
result.body = undefined;
result.attachments = [];
break;
case Proto.GroupContext.Type.QUIT:
result.body = undefined;
result.attachments = [];
break;
case Proto.GroupContext.Type.DELIVER:
// Cleaned up in `processGroupContext`
break;
default: {
const err = new Error(
`Unknown group message type: ${result.group.type}`
);
err.warn = true;
throw err;
}
}
}
const attachmentCount = result.attachments.length;
if (attachmentCount > ATTACHMENT_MAX) {
throw new Error(
`Too many attachments: ${attachmentCount} included in one message, ` +
`max is ${ATTACHMENT_MAX}`
);
}
return result;
}

View file

@ -0,0 +1,60 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { SignalService as Proto } from '../protobuf';
import { normalizeUuid } from '../util/normalizeUuid';
import {
ProcessedUnidentifiedDeliveryStatus,
ProcessedSent,
ProcessedSyncMessage,
} from './Types.d';
import UnidentifiedDeliveryStatus = Proto.SyncMessage.Sent.IUnidentifiedDeliveryStatus;
function processUnidentifiedDeliveryStatus(
status: UnidentifiedDeliveryStatus
): ProcessedUnidentifiedDeliveryStatus {
const { destinationUuid } = status;
return {
...status,
destinationUuid: destinationUuid
? normalizeUuid(
destinationUuid,
'syncMessage.sent.unidentifiedStatus.destinationUuid'
)
: undefined,
};
}
function processSent(
sent?: Proto.SyncMessage.ISent | null
): ProcessedSent | undefined {
if (!sent) {
return undefined;
}
const { destinationUuid, unidentifiedStatus } = sent;
return {
...sent,
destinationUuid: destinationUuid
? normalizeUuid(destinationUuid, 'syncMessage.sent.destinationUuid')
: undefined,
unidentifiedStatus: unidentifiedStatus
? unidentifiedStatus.map(processUnidentifiedDeliveryStatus)
: undefined,
};
}
export function processSyncMessage(
syncMessage: Proto.ISyncMessage
): ProcessedSyncMessage {
return {
...syncMessage,
sent: processSent(syncMessage.sent),
};
}

View file

@ -51,6 +51,9 @@ export type AttachmentType = {
cdnNumber?: number; cdnNumber?: number;
cdnId?: string; cdnId?: string;
cdnKey?: string; cdnKey?: string;
/** Legacy field. Used only for downloading old attachments */
id?: number;
}; };
type BaseAttachmentDraftType = { type BaseAttachmentDraftType = {

View file

@ -1,17 +1,50 @@
// Copyright 2019-2021 Signal Messenger, LLC // Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-restricted-syntax */
/* global import { isNumber, pick, reject, groupBy, values } from 'lodash';
textsecure, import pMap from 'p-map';
Signal, import Queue from 'p-queue';
log,
navigator,
reduxStore,
reduxActions,
URLSearchParams
*/
const BLESSED_PACKS = { import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { makeLookup } from '../util/makeLookup';
import { maybeParseUrl } from '../util/url';
import { base64ToArrayBuffer, deriveStickerPackKey } from '../Crypto';
import type {
StickerType,
StickerPackType,
StickerPackStatusType,
} from '../sql/Interface';
import Data from '../sql/Client';
import { SignalService as Proto } from '../protobuf';
export type RecentStickerType = Readonly<{
stickerId: number;
packId: string;
}>;
export type BlessedType = Pick<StickerPackType, 'key' | 'status'>;
export type InitialState = {
packs: Record<string, StickerPackType>;
recentStickers: Array<RecentStickerType>;
blessedPacks: Record<string, boolean>;
};
export type DownloadMap = Record<
string,
{
id: string;
key: string;
status?: StickerPackStatusType;
}
>;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
export const BLESSED_PACKS: Record<string, BlessedType> = {
'9acc9e8aba563d26a4994e69263e3b25': { '9acc9e8aba563d26a4994e69263e3b25': {
key: 'Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI=', key: 'Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI=',
status: 'downloaded', status: 'downloaded',
@ -30,104 +63,81 @@ const BLESSED_PACKS = {
}, },
}; };
const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i; const STICKER_PACK_DEFAULTS: StickerPackType = {
id: '',
key: '',
const { isNumber, pick, reject, groupBy, values } = require('lodash'); author: '',
const pMap = require('p-map'); coverStickerId: 0,
const Queue = require('p-queue').default; createdAt: 0,
downloadAttempts: 0,
const { makeLookup } = require('../../ts/util/makeLookup'); status: 'ephemeral',
const { maybeParseUrl } = require('../../ts/util/url'); stickerCount: 0,
const { stickers: {},
base64ToArrayBuffer, title: '',
deriveStickerPackKey,
} = require('../../ts/Crypto');
const {
addStickerPackReference,
createOrUpdateSticker,
createOrUpdateStickerPack,
deleteStickerPack,
deleteStickerPackReference,
getAllStickerPacks,
getAllStickers,
getRecentStickers,
updateStickerPackStatus,
} = require('../../ts/sql/Client').default;
module.exports = {
BLESSED_PACKS,
copyStickerToAttachments,
deletePack,
deletePackReference,
downloadStickerPack,
downloadEphemeralPack,
getDataFromLink,
getInitialState,
getInstalledStickerPacks,
getSticker,
getStickerPack,
getStickerPackStatus,
load,
maybeDeletePack,
downloadQueuedPacks,
isPackIdValid,
redactPackId,
removeEphemeralPack,
savePackMetadata,
}; };
let initialState = null; const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i;
let packsToDownload = null;
let initialState: InitialState | undefined;
let packsToDownload: DownloadMap | undefined;
const downloadQueue = new Queue({ concurrency: 1, timeout: 1000 * 60 * 2 }); const downloadQueue = new Queue({ concurrency: 1, timeout: 1000 * 60 * 2 });
async function load() { export async function load(): Promise<void> {
const [packs, recentStickers] = await Promise.all([ const [packs, recentStickers] = await Promise.all([
getPacksForRedux(), getPacksForRedux(),
getRecentStickersForRedux(), getRecentStickersForRedux(),
]); ]);
const blessedPacks: Record<string, boolean> = Object.create(null);
for (const key of Object.keys(BLESSED_PACKS)) {
blessedPacks[key] = true;
}
initialState = { initialState = {
packs, packs,
recentStickers, recentStickers,
blessedPacks: BLESSED_PACKS, blessedPacks,
}; };
packsToDownload = capturePacksToDownload(packs); packsToDownload = capturePacksToDownload(packs);
} }
function getDataFromLink(link) { export function getDataFromLink(
link: string
): undefined | { id: string; key: string } {
const url = maybeParseUrl(link); const url = maybeParseUrl(link);
if (!url) { if (!url) {
return null; return undefined;
} }
const { hash } = url; const { hash } = url;
if (!hash) { if (!hash) {
return null; return undefined;
} }
let params; let params;
try { try {
params = new URLSearchParams(hash.slice(1)); params = new URLSearchParams(hash.slice(1));
} catch (err) { } catch (err) {
return null; return undefined;
} }
const id = params.get('pack_id'); const id = params.get('pack_id');
if (!isPackIdValid(id)) { if (!isPackIdValid(id)) {
return null; return undefined;
} }
const key = params.get('pack_key'); const key = params.get('pack_key');
if (!key) { if (!key) {
return null; return undefined;
} }
return { id, key }; return { id, key };
} }
function getInstalledStickerPacks() { export function getInstalledStickerPacks(): Array<StickerPackType> {
const state = reduxStore.getState(); const state = window.reduxStore.getState();
const { stickers } = state; const { stickers } = state;
const { packs } = stickers; const { packs } = stickers;
if (!packs) { if (!packs) {
@ -138,20 +148,24 @@ function getInstalledStickerPacks() {
return items.filter(pack => pack.status === 'installed'); return items.filter(pack => pack.status === 'installed');
} }
function downloadQueuedPacks() { export function downloadQueuedPacks(): void {
strictAssert(packsToDownload, 'Stickers not initialized');
const ids = Object.keys(packsToDownload); const ids = Object.keys(packsToDownload);
ids.forEach(id => { for (const id of ids) {
const { key, status } = packsToDownload[id]; const { key, status } = packsToDownload[id];
// The queuing is done inside this function, no need to await here // The queuing is done inside this function, no need to await here
downloadStickerPack(id, key, { finalStatus: status }); downloadStickerPack(id, key, { finalStatus: status });
}); }
packsToDownload = {}; packsToDownload = {};
} }
function capturePacksToDownload(existingPackLookup) { function capturePacksToDownload(
const toDownload = Object.create(null); existingPackLookup: Record<string, StickerPackType>
): DownloadMap {
const toDownload: DownloadMap = Object.create(null);
// First, ensure that blessed packs are in good shape // First, ensure that blessed packs are in good shape
const blessedIds = Object.keys(BLESSED_PACKS); const blessedIds = Object.keys(BLESSED_PACKS);
@ -190,7 +204,7 @@ function capturePacksToDownload(existingPackLookup) {
if (doesPackNeedDownload(existing)) { if (doesPackNeedDownload(existing)) {
const status = const status =
existing.attemptedStatus === 'installed' ? 'installed' : null; existing.attemptedStatus === 'installed' ? 'installed' : undefined;
toDownload[id] = { toDownload[id] = {
id, id,
key: existing.key, key: existing.key,
@ -202,7 +216,7 @@ function capturePacksToDownload(existingPackLookup) {
return toDownload; return toDownload;
} }
function doesPackNeedDownload(pack) { function doesPackNeedDownload(pack?: StickerPackType): boolean {
if (!pack) { if (!pack) {
return true; return true;
} }
@ -226,14 +240,14 @@ function doesPackNeedDownload(pack) {
return true; return true;
} }
async function getPacksForRedux() { async function getPacksForRedux(): Promise<Record<string, StickerPackType>> {
const [packs, stickers] = await Promise.all([ const [packs, stickers] = await Promise.all([
getAllStickerPacks(), Data.getAllStickerPacks(),
getAllStickers(), Data.getAllStickers(),
]); ]);
const stickersByPack = groupBy(stickers, sticker => sticker.packId); const stickersByPack = groupBy(stickers, sticker => sticker.packId);
const fullSet = packs.map(pack => ({ const fullSet: Array<StickerPackType> = packs.map(pack => ({
...pack, ...pack,
stickers: makeLookup(stickersByPack[pack.id] || [], 'id'), stickers: makeLookup(stickersByPack[pack.id] || [], 'id'),
})); }));
@ -241,40 +255,41 @@ async function getPacksForRedux() {
return makeLookup(fullSet, 'id'); return makeLookup(fullSet, 'id');
} }
async function getRecentStickersForRedux() { async function getRecentStickersForRedux(): Promise<Array<RecentStickerType>> {
const recent = await getRecentStickers(); const recent = await Data.getRecentStickers();
return recent.map(sticker => ({ return recent.map(sticker => ({
packId: sticker.packId, packId: sticker.packId,
stickerId: sticker.id, stickerId: sticker.id,
})); }));
} }
function getInitialState() { export function getInitialState(): InitialState {
strictAssert(initialState !== undefined, 'Stickers not initialized');
return initialState; return initialState;
} }
function isPackIdValid(packId) { export function isPackIdValid(packId: unknown): packId is string {
return typeof packId === 'string' && VALID_PACK_ID_REGEXP.test(packId); return typeof packId === 'string' && VALID_PACK_ID_REGEXP.test(packId);
} }
function redactPackId(packId) { export function redactPackId(packId: string): string {
return `[REDACTED]${packId.slice(-3)}`; return `[REDACTED]${packId.slice(-3)}`;
} }
function getReduxStickerActions() { function getReduxStickerActions() {
const actions = reduxActions; const actions = window.reduxActions;
strictAssert(actions && actions.stickers, 'Redux not ready');
if (actions && actions.stickers) { return actions.stickers;
return actions.stickers;
}
return {};
} }
async function decryptSticker(packKey, ciphertext) { async function decryptSticker(
packKey: string,
ciphertext: ArrayBuffer
): Promise<ArrayBuffer> {
const binaryKey = base64ToArrayBuffer(packKey); const binaryKey = base64ToArrayBuffer(packKey);
const derivedKey = await deriveStickerPackKey(binaryKey); const derivedKey = await deriveStickerPackKey(binaryKey);
const plaintext = await textsecure.crypto.decryptAttachment( const plaintext = await window.textsecure.crypto.decryptAttachment(
ciphertext, ciphertext,
derivedKey derivedKey
); );
@ -282,26 +297,35 @@ async function decryptSticker(packKey, ciphertext) {
return plaintext; return plaintext;
} }
async function downloadSticker(packId, packKey, proto, options) { async function downloadSticker(
const { ephemeral } = options || {}; packId: string,
packKey: string,
proto: Proto.StickerPack.ISticker,
{ ephemeral }: { ephemeral?: boolean } = {}
): Promise<Omit<StickerType, 'isCoverOnly'>> {
const { id, emoji } = proto;
strictAssert(id !== undefined && id !== null, "Sticker id can't be null");
const ciphertext = await textsecure.messaging.getSticker(packId, proto.id); const ciphertext = await window.textsecure.messaging.getSticker(packId, id);
const plaintext = await decryptSticker(packKey, ciphertext); const plaintext = await decryptSticker(packKey, ciphertext);
const sticker = ephemeral const sticker = ephemeral
? await Signal.Migrations.processNewEphemeralSticker(plaintext, options) ? await window.Signal.Migrations.processNewEphemeralSticker(plaintext)
: await Signal.Migrations.processNewSticker(plaintext, options); : await window.Signal.Migrations.processNewSticker(plaintext);
return { return {
...pick(proto, ['id', 'emoji']), id,
emoji: dropNull(emoji),
...sticker, ...sticker,
packId, packId,
}; };
} }
async function savePackMetadata(packId, packKey, options = {}) { export async function savePackMetadata(
const { messageId } = options; packId: string,
packKey: string,
{ messageId }: { messageId?: string } = {}
): Promise<void> {
const existing = getStickerPack(packId); const existing = getStickerPack(packId);
if (existing) { if (existing) {
return; return;
@ -309,20 +333,23 @@ async function savePackMetadata(packId, packKey, options = {}) {
const { stickerPackAdded } = getReduxStickerActions(); const { stickerPackAdded } = getReduxStickerActions();
const pack = { const pack = {
...STICKER_PACK_DEFAULTS,
id: packId, id: packId,
key: packKey, key: packKey,
status: 'known', status: 'known' as const,
}; };
stickerPackAdded(pack); stickerPackAdded(pack);
await createOrUpdateStickerPack(pack); await Data.createOrUpdateStickerPack(pack);
if (messageId) { if (messageId) {
await addStickerPackReference(messageId, packId); await Data.addStickerPackReference(messageId, packId);
} }
} }
async function removeEphemeralPack(packId) { export async function removeEphemeralPack(packId: string): Promise<void> {
const existing = getStickerPack(packId); const existing = getStickerPack(packId);
strictAssert(existing, `No existing sticker pack with id: ${packId}`);
if ( if (
existing.status !== 'ephemeral' && existing.status !== 'ephemeral' &&
!(existing.status === 'error' && existing.attemptedStatus === 'ephemeral') !(existing.status === 'error' && existing.attemptedStatus === 'ephemeral')
@ -335,16 +362,18 @@ async function removeEphemeralPack(packId) {
const stickers = values(existing.stickers); const stickers = values(existing.stickers);
const paths = stickers.map(sticker => sticker.path); const paths = stickers.map(sticker => sticker.path);
await pMap(paths, Signal.Migrations.deleteTempFile, { await pMap(paths, window.Signal.Migrations.deleteTempFile, {
concurrency: 3, concurrency: 3,
timeout: 1000 * 60 * 2,
}); });
// Remove it from database in case it made it there // Remove it from database in case it made it there
await deleteStickerPack(packId); await Data.deleteStickerPack(packId);
} }
async function downloadEphemeralPack(packId, packKey) { export async function downloadEphemeralPack(
packId: string,
packKey: string
): Promise<void> {
const { const {
stickerAdded, stickerAdded,
stickerPackAdded, stickerPackAdded,
@ -358,7 +387,7 @@ async function downloadEphemeralPack(packId, packKey) {
existingPack.status === 'installed' || existingPack.status === 'installed' ||
existingPack.status === 'pending') existingPack.status === 'pending')
) { ) {
log.warn( window.log.warn(
`Ephemeral download for pack ${redactPackId( `Ephemeral download for pack ${redactPackId(
packId packId
)} requested, we already know about it. Skipping.` )} requested, we already know about it. Skipping.`
@ -369,17 +398,19 @@ async function downloadEphemeralPack(packId, packKey) {
try { try {
// Synchronous placeholder to help with race conditions // Synchronous placeholder to help with race conditions
const placeholder = { const placeholder = {
...STICKER_PACK_DEFAULTS,
id: packId, id: packId,
key: packKey, key: packKey,
status: 'ephemeral', status: 'ephemeral' as const,
}; };
stickerPackAdded(placeholder); stickerPackAdded(placeholder);
const ciphertext = await textsecure.messaging.getStickerPackManifest( const ciphertext = await window.textsecure.messaging.getStickerPackManifest(
packId packId
); );
const plaintext = await decryptSticker(packKey, ciphertext); const plaintext = await decryptSticker(packKey, ciphertext);
const proto = textsecure.protobuf.StickerPack.decode(plaintext); const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext));
const firstStickerProto = proto.stickers ? proto.stickers[0] : null; const firstStickerProto = proto.stickers ? proto.stickers[0] : null;
const stickerCount = proto.stickers.length; const stickerCount = proto.stickers.length;
@ -402,16 +433,20 @@ async function downloadEphemeralPack(packId, packKey) {
const coverIncludedInList = nonCoverStickers.length < stickerCount; const coverIncludedInList = nonCoverStickers.length < stickerCount;
const pack = { const pack = {
...STICKER_PACK_DEFAULTS,
id: packId, id: packId,
key: packKey, key: packKey,
coverStickerId, coverStickerId,
stickerCount, stickerCount,
status: 'ephemeral', status: 'ephemeral' as const,
...pick(proto, ['title', 'author']), ...pick(proto, ['title', 'author']),
}; };
stickerPackAdded(pack); stickerPackAdded(pack);
const downloadStickerJob = async stickerProto => { const downloadStickerJob = async (
stickerProto: Proto.StickerPack.ISticker
): Promise<void> => {
const stickerInfo = await downloadSticker(packId, packKey, stickerProto, { const stickerInfo = await downloadSticker(packId, packKey, stickerProto, {
ephemeral: true, ephemeral: true,
}); });
@ -438,7 +473,6 @@ async function downloadEphemeralPack(packId, packKey) {
// Then the rest // Then the rest
await pMap(nonCoverStickers, downloadStickerJob, { await pMap(nonCoverStickers, downloadStickerJob, {
concurrency: 3, concurrency: 3,
timeout: 1000 * 60 * 2,
}); });
} catch (error) { } catch (error) {
// Because the user could install this pack while we are still downloading this // Because the user could install this pack while we are still downloading this
@ -451,20 +485,30 @@ async function downloadEphemeralPack(packId, packKey) {
status: 'error', status: 'error',
}); });
} }
log.error( window.log.error(
`Ephemeral download error for sticker pack ${redactPackId(packId)}:`, `Ephemeral download error for sticker pack ${redactPackId(packId)}:`,
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
} }
async function downloadStickerPack(packId, packKey, options = {}) { export type DownloadStickerPackOptions = Readonly<{
messageId?: string;
fromSync?: boolean;
finalStatus?: StickerPackStatusType;
}>;
export async function downloadStickerPack(
packId: string,
packKey: string,
options: DownloadStickerPackOptions = {}
): Promise<void> {
// This will ensure that only one download process is in progress at any given time // This will ensure that only one download process is in progress at any given time
return downloadQueue.add(async () => { return downloadQueue.add(async () => {
try { try {
await doDownloadStickerPack(packId, packKey, options); await doDownloadStickerPack(packId, packKey, options);
} catch (error) { } catch (error) {
log.error( window.log.error(
'doDownloadStickerPack threw an error:', 'doDownloadStickerPack threw an error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
@ -472,8 +516,15 @@ async function downloadStickerPack(packId, packKey, options = {}) {
}); });
} }
async function doDownloadStickerPack(packId, packKey, options = {}) { async function doDownloadStickerPack(
const { messageId, fromSync } = options; packId: string,
packKey: string,
{
finalStatus = 'downloaded',
messageId,
fromSync = false,
}: DownloadStickerPackOptions
): Promise<void> {
const { const {
stickerAdded, stickerAdded,
stickerPackAdded, stickerPackAdded,
@ -481,7 +532,6 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
installStickerPack, installStickerPack,
} = getReduxStickerActions(); } = getReduxStickerActions();
const finalStatus = options.finalStatus || 'downloaded';
if (finalStatus !== 'downloaded' && finalStatus !== 'installed') { if (finalStatus !== 'downloaded' && finalStatus !== 'installed') {
throw new Error( throw new Error(
`doDownloadStickerPack: invalid finalStatus of ${finalStatus} requested.` `doDownloadStickerPack: invalid finalStatus of ${finalStatus} requested.`
@ -490,7 +540,7 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
const existing = getStickerPack(packId); const existing = getStickerPack(packId);
if (!doesPackNeedDownload(existing)) { if (!doesPackNeedDownload(existing)) {
log.warn( window.log.warn(
`Download for pack ${redactPackId( `Download for pack ${redactPackId(
packId packId
)} requested, but it does not need re-download. Skipping.` )} requested, but it does not need re-download. Skipping.`
@ -503,14 +553,14 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
const downloadAttempts = const downloadAttempts =
(existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement; (existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement;
if (downloadAttempts > 3) { if (downloadAttempts > 3) {
log.warn( window.log.warn(
`Refusing to attempt another download for pack ${redactPackId( `Refusing to attempt another download for pack ${redactPackId(
packId packId
)}, attempt number ${downloadAttempts}` )}, attempt number ${downloadAttempts}`
); );
if (existing.status !== 'error') { if (existing && existing.status !== 'error') {
await updateStickerPackStatus(packId, 'error'); await Data.updateStickerPackStatus(packId, 'error');
stickerPackUpdated(packId, { stickerPackUpdated(packId, {
status: 'error', status: 'error',
}); });
@ -519,32 +569,34 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
return; return;
} }
let coverProto; let coverProto: Proto.StickerPack.ISticker | undefined;
let coverStickerId; let coverStickerId: number | undefined;
let coverIncludedInList; let coverIncludedInList = false;
let nonCoverStickers; let nonCoverStickers: Array<Proto.StickerPack.ISticker> = [];
try { try {
// Synchronous placeholder to help with race conditions // Synchronous placeholder to help with race conditions
const placeholder = { const placeholder = {
...STICKER_PACK_DEFAULTS,
id: packId, id: packId,
key: packKey, key: packKey,
attemptedStatus: finalStatus, attemptedStatus: finalStatus,
downloadAttempts, downloadAttempts,
status: 'pending', status: 'pending' as const,
}; };
stickerPackAdded(placeholder); stickerPackAdded(placeholder);
const ciphertext = await textsecure.messaging.getStickerPackManifest( const ciphertext = await window.textsecure.messaging.getStickerPackManifest(
packId packId
); );
const plaintext = await decryptSticker(packKey, ciphertext); const plaintext = await decryptSticker(packKey, ciphertext);
const proto = textsecure.protobuf.StickerPack.decode(plaintext); const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext));
const firstStickerProto = proto.stickers ? proto.stickers[0] : null; const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined;
const stickerCount = proto.stickers.length; const stickerCount = proto.stickers.length;
coverProto = proto.cover || firstStickerProto; coverProto = proto.cover || firstStickerProto;
coverStickerId = coverProto ? coverProto.id : null; coverStickerId = dropNull(coverProto ? coverProto.id : undefined);
if (!coverProto || !isNumber(coverStickerId)) { if (!coverProto || !isNumber(coverStickerId)) {
throw new Error( throw new Error(
@ -568,7 +620,7 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
// - 'downloaded' // - 'downloaded'
// - 'error' // - 'error'
// - 'installed' // - 'installed'
const pack = { const pack: StickerPackType = {
id: packId, id: packId,
key: packKey, key: packKey,
attemptedStatus: finalStatus, attemptedStatus: finalStatus,
@ -576,28 +628,32 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
downloadAttempts, downloadAttempts,
stickerCount, stickerCount,
status: 'pending', status: 'pending',
createdAt: Date.now(),
stickers: {},
...pick(proto, ['title', 'author']), ...pick(proto, ['title', 'author']),
}; };
await createOrUpdateStickerPack(pack); await Data.createOrUpdateStickerPack(pack);
stickerPackAdded(pack); stickerPackAdded(pack);
if (messageId) { if (messageId) {
await addStickerPackReference(messageId, packId); await Data.addStickerPackReference(messageId, packId);
} }
} catch (error) { } catch (error) {
log.error( window.log.error(
`Error downloading manifest for sticker pack ${redactPackId(packId)}:`, `Error downloading manifest for sticker pack ${redactPackId(packId)}:`,
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
const pack = { const pack = {
...STICKER_PACK_DEFAULTS,
id: packId, id: packId,
key: packKey, key: packKey,
attemptedStatus: finalStatus, attemptedStatus: finalStatus,
downloadAttempts, downloadAttempts,
status: 'error', status: 'error' as const,
}; };
await createOrUpdateStickerPack(pack); await Data.createOrUpdateStickerPack(pack);
stickerPackAdded(pack); stickerPackAdded(pack);
return; return;
@ -606,13 +662,15 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
// We have a separate try/catch here because we're starting to download stickers here // We have a separate try/catch here because we're starting to download stickers here
// and we want to preserve more of the pack on an error. // and we want to preserve more of the pack on an error.
try { try {
const downloadStickerJob = async stickerProto => { const downloadStickerJob = async (
stickerProto: Proto.StickerPack.ISticker
): Promise<void> => {
const stickerInfo = await downloadSticker(packId, packKey, stickerProto); const stickerInfo = await downloadSticker(packId, packKey, stickerProto);
const sticker = { const sticker = {
...stickerInfo, ...stickerInfo,
isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId, isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId,
}; };
await createOrUpdateSticker(sticker); await Data.createOrUpdateSticker(sticker);
stickerAdded(sticker); stickerAdded(sticker);
}; };
@ -622,7 +680,6 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
// Then the rest // Then the rest
await pMap(nonCoverStickers, downloadStickerJob, { await pMap(nonCoverStickers, downloadStickerJob, {
concurrency: 3, concurrency: 3,
timeout: 1000 * 60 * 2,
}); });
// Allow for the user marking this pack as installed in the middle of our download; // Allow for the user marking this pack as installed in the middle of our download;
@ -636,19 +693,19 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
await installStickerPack(packId, packKey, { fromSync }); await installStickerPack(packId, packKey, { fromSync });
} else { } else {
// Mark the pack as complete // Mark the pack as complete
await updateStickerPackStatus(packId, finalStatus); await Data.updateStickerPackStatus(packId, finalStatus);
stickerPackUpdated(packId, { stickerPackUpdated(packId, {
status: finalStatus, status: finalStatus,
}); });
} }
} catch (error) { } catch (error) {
log.error( window.log.error(
`Error downloading stickers for sticker pack ${redactPackId(packId)}:`, `Error downloading stickers for sticker pack ${redactPackId(packId)}:`,
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
const errorStatus = 'error'; const errorStatus = 'error';
await updateStickerPackStatus(packId, errorStatus); await Data.updateStickerPackStatus(packId, errorStatus);
if (stickerPackUpdated) { if (stickerPackUpdated) {
stickerPackUpdated(packId, { stickerPackUpdated(packId, {
attemptedStatus: finalStatus, attemptedStatus: finalStatus,
@ -658,45 +715,53 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
} }
} }
function getStickerPack(packId) { export function getStickerPack(packId: string): StickerPackType | undefined {
const state = reduxStore.getState(); const state = window.reduxStore.getState();
const { stickers } = state; const { stickers } = state;
const { packs } = stickers; const { packs } = stickers;
if (!packs) { if (!packs) {
return null; return undefined;
} }
return packs[packId]; return packs[packId];
} }
function getStickerPackStatus(packId) { export function getStickerPackStatus(
packId: string
): StickerPackStatusType | undefined {
const pack = getStickerPack(packId); const pack = getStickerPack(packId);
if (!pack) { if (!pack) {
return null; return undefined;
} }
return pack.status; return pack.status;
} }
function getSticker(packId, stickerId) { export function getSticker(
packId: string,
stickerId: number
): StickerType | undefined {
const pack = getStickerPack(packId); const pack = getStickerPack(packId);
if (!pack || !pack.stickers) { if (!pack || !pack.stickers) {
return null; return undefined;
} }
return pack.stickers[stickerId]; return pack.stickers[stickerId];
} }
async function copyStickerToAttachments(packId, stickerId) { export async function copyStickerToAttachments(
packId: string,
stickerId: number
): Promise<StickerType | undefined> {
const sticker = getSticker(packId, stickerId); const sticker = getSticker(packId, stickerId);
if (!sticker) { if (!sticker) {
return null; return undefined;
} }
const { path } = sticker; const { path } = sticker;
const absolutePath = Signal.Migrations.getAbsoluteStickerPath(path); const absolutePath = window.Signal.Migrations.getAbsoluteStickerPath(path);
const newPath = await Signal.Migrations.copyIntoAttachmentsDirectory( const newPath = await window.Signal.Migrations.copyIntoAttachmentsDirectory(
absolutePath absolutePath
); );
@ -709,7 +774,7 @@ async function copyStickerToAttachments(packId, stickerId) {
// In the case where a sticker pack is uninstalled, we want to delete it if there are no // In the case where a sticker pack is uninstalled, we want to delete it if there are no
// more references left. We'll delete a nonexistent reference, then check if there are // more references left. We'll delete a nonexistent reference, then check if there are
// any references left, just like usual. // any references left, just like usual.
async function maybeDeletePack(packId) { export async function maybeDeletePack(packId: string): Promise<void> {
// This hardcoded string is fine because message ids are GUIDs // This hardcoded string is fine because message ids are GUIDs
await deletePackReference('NOT-USED', packId); await deletePackReference('NOT-USED', packId);
} }
@ -717,7 +782,10 @@ async function maybeDeletePack(packId) {
// We don't generally delete packs outright; we just remove references to them, and if // We don't generally delete packs outright; we just remove references to them, and if
// the last reference is deleted, we finally then remove the pack itself from database // the last reference is deleted, we finally then remove the pack itself from database
// and from disk. // and from disk.
async function deletePackReference(messageId, packId) { export async function deletePackReference(
messageId: string,
packId: string
): Promise<void> {
const isBlessed = Boolean(BLESSED_PACKS[packId]); const isBlessed = Boolean(BLESSED_PACKS[packId]);
if (isBlessed) { if (isBlessed) {
return; return;
@ -725,7 +793,7 @@ async function deletePackReference(messageId, packId) {
// This call uses locking to prevent race conditions with other reference removals, // This call uses locking to prevent race conditions with other reference removals,
// or an incoming message creating a new message->pack reference // or an incoming message creating a new message->pack reference
const paths = await deleteStickerPackReference(messageId, packId); const paths = await Data.deleteStickerPackReference(messageId, packId);
// If we don't get a list of paths back, then the sticker pack was not deleted // If we don't get a list of paths back, then the sticker pack was not deleted
if (!paths || !paths.length) { if (!paths || !paths.length) {
@ -735,14 +803,13 @@ async function deletePackReference(messageId, packId) {
const { removeStickerPack } = getReduxStickerActions(); const { removeStickerPack } = getReduxStickerActions();
removeStickerPack(packId); removeStickerPack(packId);
await pMap(paths, Signal.Migrations.deleteSticker, { await pMap(paths, window.Signal.Migrations.deleteSticker, {
concurrency: 3, concurrency: 3,
timeout: 1000 * 60 * 2,
}); });
} }
// The override; doesn't honor our ref-counting scheme - just deletes it all. // The override; doesn't honor our ref-counting scheme - just deletes it all.
async function deletePack(packId) { export async function deletePack(packId: string): Promise<void> {
const isBlessed = Boolean(BLESSED_PACKS[packId]); const isBlessed = Boolean(BLESSED_PACKS[packId]);
if (isBlessed) { if (isBlessed) {
return; return;
@ -750,13 +817,12 @@ async function deletePack(packId) {
// This call uses locking to prevent race conditions with other reference removals, // This call uses locking to prevent race conditions with other reference removals,
// or an incoming message creating a new message->pack reference // or an incoming message creating a new message->pack reference
const paths = await deleteStickerPack(packId); const paths = await Data.deleteStickerPack(packId);
const { removeStickerPack } = getReduxStickerActions(); const { removeStickerPack } = getReduxStickerActions();
removeStickerPack(packId); removeStickerPack(packId);
await pMap(paths, Signal.Migrations.deleteSticker, { await pMap(paths, window.Signal.Migrations.deleteSticker, {
concurrency: 3, concurrency: 3,
timeout: 1000 * 60 * 2,
}); });
} }

View file

@ -4,8 +4,8 @@
export type BodyRangeType = { export type BodyRangeType = {
start: number; start: number;
length: number; length: number;
mentionUuid: string; mentionUuid?: string;
replacementText: string; replacementText?: string;
conversationID?: string; conversationID?: string;
}; };

View file

@ -1,26 +1,29 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { import { DownloadAttachmentType } from '../textsecure.d';
AttachmentPointerClass,
DownloadAttachmentType,
} from '../textsecure.d';
type AttachmentData = AttachmentPointerClass & { import { AttachmentType } from '../types/Attachment';
id?: string;
};
export async function downloadAttachment( export async function downloadAttachment(
attachmentData: AttachmentData attachmentData: AttachmentType
): Promise<DownloadAttachmentType | null> { ): Promise<DownloadAttachmentType | null> {
let migratedAttachment: AttachmentType;
const { id: legacyId } = attachmentData;
if (legacyId === undefined) {
migratedAttachment = attachmentData;
} else {
migratedAttachment = {
...attachmentData,
cdnId: String(legacyId),
};
}
let downloaded; let downloaded;
try { try {
if (attachmentData.id) {
// eslint-disable-next-line no-param-reassign
attachmentData.cdnId = attachmentData.id;
}
downloaded = await window.textsecure.messageReceiver.downloadAttachment( downloaded = await window.textsecure.messageReceiver.downloadAttachment(
attachmentData migratedAttachment
); );
} catch (error) { } catch (error) {
// Attachments on the server expire after 30 days, then start returning 404 // Attachments on the server expire after 30 days, then start returning 404

View file

@ -1,5 +1,10 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-restricted-syntax */
export type NullToUndefined<T> = Extract<T, null> extends never
? T
: Exclude<T, null> | undefined;
export function dropNull<T>( export function dropNull<T>(
value: NonNullable<T> | null | undefined value: NonNullable<T> | null | undefined
@ -9,3 +14,25 @@ export function dropNull<T>(
} }
return value; return value;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function shallowDropNull<O extends { [key: string]: any }>(
value: O | null | undefined
):
| {
[Property in keyof O]: NullToUndefined<O[Property]>;
}
| undefined {
if (value === null || value === undefined) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = {};
for (const [key, propertyValue] of Object.entries(value)) {
result[key] = dropNull(propertyValue);
}
return result;
}

View file

@ -2,7 +2,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { AccessControlClass } from '../textsecure.d'; import { SignalService as Proto } from '../protobuf';
const AccessControlEnum = Proto.AccessControl.AccessRequired;
type AccessControlOption = { type AccessControlOption = {
text: string; text: string;
@ -10,17 +12,16 @@ type AccessControlOption = {
}; };
export function getAccessControlOptions( export function getAccessControlOptions(
accessEnum: typeof AccessControlClass.AccessRequired,
i18n: LocalizerType i18n: LocalizerType
): Array<AccessControlOption> { ): Array<AccessControlOption> {
return [ return [
{ {
text: i18n('GroupV2--all-members'), text: i18n('GroupV2--all-members'),
value: accessEnum.MEMBER, value: AccessControlEnum.MEMBER,
}, },
{ {
text: i18n('GroupV2--only-admins'), text: i18n('GroupV2--only-admins'),
value: accessEnum.ADMINISTRATOR, value: AccessControlEnum.ADMINISTRATOR,
}, },
]; ];
} }

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ConversationAttributesType } from '../model-types.d'; import { ConversationAttributesType } from '../model-types.d';
import { SignalService as Proto } from '../protobuf';
import { isDirectConversation, isMe } from './whatTypeOfConversation'; import { isDirectConversation, isMe } from './whatTypeOfConversation';
import { isInSystemContacts } from './isInSystemContacts'; import { isInSystemContacts } from './isInSystemContacts';
@ -24,8 +25,7 @@ export function isConversationAccepted(
return true; return true;
} }
const messageRequestEnum = const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
const { messageRequestResponseType } = conversationAttrs; const { messageRequestResponseType } = conversationAttrs;
if (messageRequestResponseType === messageRequestEnum.ACCEPT) { if (messageRequestResponseType === messageRequestEnum.ACCEPT) {

View file

@ -198,13 +198,6 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-07-21T18:34:59.251Z" "updated": "2020-07-21T18:34:59.251Z"
}, },
{
"rule": "jQuery-load(",
"path": "js/modules/stickers.js",
"line": "async function load() {",
"reasonCategory": "falseMatch",
"updated": "2019-04-26T17:48:30.675Z"
},
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/permissions_popup_start.js", "path": "js/permissions_popup_start.js",
@ -14141,6 +14134,20 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
}, },
{
"rule": "jQuery-load(",
"path": "ts/types/Stickers.js",
"line": "async function load() {",
"reasonCategory": "falseMatch",
"updated": "2021-07-02T02:57:58.052Z"
},
{
"rule": "jQuery-load(",
"path": "ts/types/Stickers.ts",
"line": "export async function load(): Promise<void> {",
"reasonCategory": "falseMatch",
"updated": "2019-04-26T17:48:30.675Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/util/hooks.js", "path": "ts/util/hooks.js",
@ -14157,4 +14164,4 @@
"updated": "2021-03-18T21:41:28.361Z", "updated": "2021-03-18T21:41:28.361Z",
"reasonDetail": "A generic hook. Typically not to be used with non-DOM values." "reasonDetail": "A generic hook. Typically not to be used with non-DOM values."
} }
] ]

View file

@ -57,6 +57,7 @@ const excludedFilesRegexps = [
'^sticker-creator/dist/bundle.js', '^sticker-creator/dist/bundle.js',
'^test/test.js', '^test/test.js',
'^ts/test[^/]*/.+', '^ts/test[^/]*/.+',
'^ts/sql/mainWorker.bundle.js',
// Copied from dependency // Copied from dependency
'^js/Mp3LameEncoder.min.js', '^js/Mp3LameEncoder.min.js',

View file

@ -37,7 +37,7 @@ import {
multiRecipient409ResponseSchema, multiRecipient409ResponseSchema,
multiRecipient410ResponseSchema, multiRecipient410ResponseSchema,
} from '../textsecure/WebAPI'; } from '../textsecure/WebAPI';
import { ContentClass } from '../textsecure.d'; import { SignalService as Proto } from '../protobuf';
import { assert } from './assert'; import { assert } from './assert';
import { isGroupV2 } from './whatTypeOfConversation'; import { isGroupV2 } from './whatTypeOfConversation';
@ -53,6 +53,9 @@ const MAX_CONCURRENCY = 5;
// sendWithSenderKey is recursive, but we don't want to loop back too many times. // sendWithSenderKey is recursive, but we don't want to loop back too many times.
const MAX_RECURSION = 5; const MAX_RECURSION = 5;
// TODO: remove once we move away from ArrayBuffers
const FIXMEU8 = Uint8Array;
// Public API: // Public API:
export async function sendToGroup({ export async function sendToGroup({
@ -106,7 +109,7 @@ export async function sendContentMessageToGroup({
timestamp, timestamp,
}: { }: {
contentHint: number; contentHint: number;
contentMessage: ContentClass; contentMessage: Proto.Content;
conversation: ConversationModel; conversation: ConversationModel;
isPartialSend?: boolean; isPartialSend?: boolean;
online?: boolean; online?: boolean;
@ -165,7 +168,7 @@ export async function sendContentMessageToGroup({
export async function sendToGroupViaSenderKey(options: { export async function sendToGroupViaSenderKey(options: {
contentHint: number; contentHint: number;
contentMessage: ContentClass; contentMessage: Proto.Content;
conversation: ConversationModel; conversation: ConversationModel;
isPartialSend?: boolean; isPartialSend?: boolean;
online?: boolean; online?: boolean;
@ -185,9 +188,7 @@ export async function sendToGroupViaSenderKey(options: {
sendOptions, sendOptions,
timestamp, timestamp,
} = options; } = options;
const { const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
ContentHint,
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
const logId = conversation.idForLogging(); const logId = conversation.idForLogging();
window.log.info( window.log.info(
@ -372,7 +373,9 @@ export async function sendToGroupViaSenderKey(options: {
contentHint, contentHint,
devices: devicesForSenderKey, devices: devicesForSenderKey,
distributionId, distributionId,
contentMessage: contentMessage.toArrayBuffer(), contentMessage: toArrayBuffer(
Proto.Content.encode(contentMessage).finish()
),
groupId, groupId,
}); });
const accessKeys = getXorOfAccessKeys(devicesForSenderKey); const accessKeys = getXorOfAccessKeys(devicesForSenderKey);
@ -431,7 +434,11 @@ export async function sendToGroupViaSenderKey(options: {
const normalRecipients = getUuidsFromDevices(devicesForNormalSend); const normalRecipients = getUuidsFromDevices(devicesForNormalSend);
if (normalRecipients.length === 0) { if (normalRecipients.length === 0) {
return { return {
dataMessage: contentMessage.dataMessage?.toArrayBuffer(), dataMessage: contentMessage.dataMessage
? toArrayBuffer(
Proto.DataMessage.encode(contentMessage.dataMessage).finish()
)
: undefined,
successfulIdentifiers: senderKeyRecipients, successfulIdentifiers: senderKeyRecipients,
unidentifiedDeliveries: senderKeyRecipients, unidentifiedDeliveries: senderKeyRecipients,
}; };
@ -449,7 +456,11 @@ export async function sendToGroupViaSenderKey(options: {
}); });
return { return {
dataMessage: contentMessage.dataMessage?.toArrayBuffer(), dataMessage: contentMessage.dataMessage
? toArrayBuffer(
Proto.DataMessage.encode(contentMessage.dataMessage).finish()
)
: undefined,
errors: normalSendResult.errors, errors: normalSendResult.errors,
failoverIdentifiers: normalSendResult.failoverIdentifiers, failoverIdentifiers: normalSendResult.failoverIdentifiers,
successfulIdentifiers: [ successfulIdentifiers: [
@ -669,7 +680,7 @@ async function encryptForSenderKey({
); );
const ourAddress = getOurAddress(); const ourAddress = getOurAddress();
const senderKeyStore = new SenderKeys(); const senderKeyStore = new SenderKeys();
const message = Buffer.from(padMessage(contentMessage)); const message = Buffer.from(padMessage(new FIXMEU8(contentMessage)));
const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob( const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob(
ourAddress, ourAddress,

View file

@ -9,7 +9,9 @@ import {
InMemoryAttachmentDraftType, InMemoryAttachmentDraftType,
OnDiskAttachmentDraftType, OnDiskAttachmentDraftType,
} from '../types/Attachment'; } from '../types/Attachment';
import { IMAGE_JPEG } from '../types/MIME'; import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
import * as Stickers from '../types/Stickers';
import { IMAGE_JPEG, IMAGE_WEBP } from '../types/MIME';
import { ConversationModel } from '../models/conversations'; import { ConversationModel } from '../models/conversations';
import { import {
GroupV2PendingMemberType, GroupV2PendingMemberType,
@ -48,6 +50,7 @@ import {
LinkPreviewWithDomain, LinkPreviewWithDomain,
} from '../types/LinkPreview'; } from '../types/LinkPreview';
import * as LinkPreview from '../types/LinkPreview'; import * as LinkPreview from '../types/LinkPreview';
import { SignalService as Proto } from '../protobuf';
type AttachmentOptions = { type AttachmentOptions = {
messageId: string; messageId: string;
@ -616,8 +619,7 @@ Whisper.ConversationView = Whisper.View.extend({
</div> </div>
`)[0]; `)[0];
const messageRequestEnum = const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
const props = { const props = {
id: model.id, id: model.id,
@ -845,8 +847,7 @@ Whisper.ConversationView = Whisper.View.extend({
const { model }: { model: ConversationModel } = this; const { model }: { model: ConversationModel } = this;
const { id } = model; const { id } = model;
const messageRequestEnum = const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
const contactSupport = () => { const contactSupport = () => {
const baseUrl = const baseUrl =
@ -1564,8 +1565,7 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
blockAndReportSpam(model: ConversationModel): Promise<void> { blockAndReportSpam(model: ConversationModel): Promise<void> {
const messageRequestEnum = const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
return this.longRunningTaskWrapper({ return this.longRunningTaskWrapper({
name: 'blockAndReportSpam', name: 'blockAndReportSpam',
@ -2202,7 +2202,7 @@ Whisper.ConversationView = Whisper.View.extend({
contentType: blob.type, contentType: blob.type,
data, data,
size: data.byteLength, size: data.byteLength,
flags: window.textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE, flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
}; };
// Note: The RecorderView removes itself on send // Note: The RecorderView removes itself on send
@ -2443,12 +2443,12 @@ Whisper.ConversationView = Whisper.View.extend({
: undefined; : undefined;
conversation.sendMessage( conversation.sendMessage(
null, undefined, // body
[], [],
null, undefined, // quote
[], [],
stickerNoPath, stickerNoPath,
undefined, undefined, // BodyRanges
{ ...sendMessageOptions, timestamp } { ...sendMessageOptions, timestamp }
); );
} else { } else {
@ -2469,11 +2469,11 @@ Whisper.ConversationView = Whisper.View.extend({
); );
conversation.sendMessage( conversation.sendMessage(
messageBody || null, messageBody || undefined,
attachmentsToSend, attachmentsToSend,
null, // quote undefined, // quote
preview, preview,
null, // sticker undefined, // sticker
undefined, // BodyRanges undefined, // BodyRanges
{ ...sendMessageOptions, timestamp } { ...sendMessageOptions, timestamp }
); );
@ -2953,14 +2953,14 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
showStickerPackPreview(packId: string, packKey: string) { showStickerPackPreview(packId: string, packKey: string) {
window.Signal.Stickers.downloadEphemeralPack(packId, packKey); Stickers.downloadEphemeralPack(packId, packKey);
const props = { const props = {
packId, packId,
onClose: async () => { onClose: async () => {
this.stickerPreviewModalView.remove(); this.stickerPreviewModalView.remove();
this.stickerPreviewModalView = null; this.stickerPreviewModalView = null;
await window.Signal.Stickers.removeEphemeralPack(packId); await Stickers.removeEphemeralPack(packId);
}, },
}; };
@ -3199,7 +3199,6 @@ Whisper.ConversationView = Whisper.View.extend({
JSX: window.Signal.State.Roots.createGroupLinkManagement( JSX: window.Signal.State.Roots.createGroupLinkManagement(
window.reduxStore, window.reduxStore,
{ {
accessEnum: window.textsecure.protobuf.AccessControl.AccessRequired,
changeHasGroupLink: this.changeHasGroupLink.bind(this), changeHasGroupLink: this.changeHasGroupLink.bind(this),
conversationId: model.id, conversationId: model.id,
copyGroupLink: this.copyGroupLink.bind(this), copyGroupLink: this.copyGroupLink.bind(this),
@ -3224,7 +3223,6 @@ Whisper.ConversationView = Whisper.View.extend({
JSX: window.Signal.State.Roots.createGroupV2Permissions( JSX: window.Signal.State.Roots.createGroupV2Permissions(
window.reduxStore, window.reduxStore,
{ {
accessEnum: window.textsecure.protobuf.AccessControl.AccessRequired,
conversationId: model.id, conversationId: model.id,
setAccessControlAttributesSetting: this.setAccessControlAttributesSetting.bind( setAccessControlAttributesSetting: this.setAccessControlAttributesSetting.bind(
this this
@ -3282,8 +3280,7 @@ Whisper.ConversationView = Whisper.View.extend({
showConversationDetails() { showConversationDetails() {
const { model }: { model: ConversationModel } = this; const { model }: { model: ConversationModel } = this;
const messageRequestEnum = const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
// these methods are used in more than one place and should probably be // these methods are used in more than one place and should probably be
// dried up and hoisted to methods on ConversationView // dried up and hoisted to methods on ConversationView
@ -3303,7 +3300,7 @@ Whisper.ConversationView = Whisper.View.extend({
); );
}; };
const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
const hasGroupLink = Boolean( const hasGroupLink = Boolean(
model.get('groupInviteLinkPassword') && model.get('groupInviteLinkPassword') &&
@ -4029,15 +4026,29 @@ Whisper.ConversationView = Whisper.View.extend({
url: string, url: string,
abortSignal: Readonly<AbortSignal> abortSignal: Readonly<AbortSignal>
): Promise<null | LinkPreviewResult> { ): Promise<null | LinkPreviewResult> {
const isPackDownloaded = (pack: any) => const isPackDownloaded = (
pack && (pack.status === 'downloaded' || pack.status === 'installed'); pack?: StickerPackDBType
const isPackValid = (pack: any) => ): pack is StickerPackDBType => {
pack && if (!pack) {
(pack.status === 'ephemeral' || return false;
pack.status === 'downloaded' || }
pack.status === 'installed');
const dataFromLink = window.Signal.Stickers.getDataFromLink(url); return pack.status === 'downloaded' || pack.status === 'installed';
};
const isPackValid = (
pack?: StickerPackDBType
): pack is StickerPackDBType => {
if (!pack) {
return false;
}
return (
pack.status === 'ephemeral' ||
pack.status === 'downloaded' ||
pack.status === 'installed'
);
};
const dataFromLink = Stickers.getDataFromLink(url);
if (!dataFromLink) { if (!dataFromLink) {
return null; return null;
} }
@ -4047,16 +4058,16 @@ Whisper.ConversationView = Whisper.View.extend({
const keyBytes = window.Signal.Crypto.bytesFromHexString(key); const keyBytes = window.Signal.Crypto.bytesFromHexString(key);
const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes); const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes);
const existing = window.Signal.Stickers.getStickerPack(id); const existing = Stickers.getStickerPack(id);
if (!isPackDownloaded(existing)) { if (!isPackDownloaded(existing)) {
await window.Signal.Stickers.downloadEphemeralPack(id, keyBase64); await Stickers.downloadEphemeralPack(id, keyBase64);
} }
if (abortSignal.aborted) { if (abortSignal.aborted) {
return null; return null;
} }
const pack = window.Signal.Stickers.getStickerPack(id); const pack = Stickers.getStickerPack(id);
if (!isPackValid(pack)) { if (!isPackValid(pack)) {
return null; return null;
@ -4083,7 +4094,7 @@ Whisper.ConversationView = Whisper.View.extend({
...sticker, ...sticker,
data, data,
size: data.byteLength, size: data.byteLength,
contentType: 'image/webp', contentType: IMAGE_WEBP,
}, },
description: null, description: null,
date: null, date: null,
@ -4096,7 +4107,7 @@ Whisper.ConversationView = Whisper.View.extend({
return null; return null;
} finally { } finally {
if (id) { if (id) {
await window.Signal.Stickers.removeEphemeralPack(id); await Stickers.removeEphemeralPack(id);
} }
} }
}, },

60
ts/window.d.ts vendored
View file

@ -235,7 +235,6 @@ declare global {
}; };
log: LoggerType; log: LoggerType;
nodeSetImmediate: typeof setImmediate; nodeSetImmediate: typeof setImmediate;
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
onFullScreenChange: (fullScreen: boolean) => void; onFullScreenChange: (fullScreen: boolean) => void;
platform: string; platform: string;
preloadedImages: Array<WhatIsThis>; preloadedImages: Array<WhatIsThis>;
@ -311,14 +310,31 @@ declare global {
loadPreviewData: (preview: unknown) => WhatIsThis; loadPreviewData: (preview: unknown) => WhatIsThis;
loadStickerData: (sticker: unknown) => WhatIsThis; loadStickerData: (sticker: unknown) => WhatIsThis;
readStickerData: (path: string) => Promise<ArrayBuffer>; readStickerData: (path: string) => Promise<ArrayBuffer>;
deleteSticker: (path: string) => Promise<void>;
getAbsoluteStickerPath: (path: string) => string;
processNewEphemeralSticker: (
stickerData: ArrayBuffer
) => {
path: string;
width: number;
height: number;
};
processNewSticker: (
stickerData: ArrayBuffer
) => {
path: string;
width: number;
height: number;
};
copyIntoAttachmentsDirectory: (path: string) => Promise<string>;
upgradeMessageSchema: (attributes: unknown) => WhatIsThis; upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
processNewAttachment: ( processNewAttachment: (
attachment: DownloadAttachmentType attachment: DownloadAttachmentType
) => Promise<AttachmentType>; ) => Promise<AttachmentType>;
copyIntoTempDirectory: any; copyIntoTempDirectory: any;
deleteDraftFile: any; deleteDraftFile: (path: string) => Promise<void>;
deleteTempFile: any; deleteTempFile: (path: string) => Promise<void>;
getAbsoluteDraftPath: any; getAbsoluteDraftPath: any;
getAbsoluteTempPath: any; getAbsoluteTempPath: any;
openFileInFolder: any; openFileInFolder: any;
@ -327,36 +343,6 @@ declare global {
saveAttachmentToDisk: any; saveAttachmentToDisk: any;
writeNewDraftData: any; writeNewDraftData: any;
}; };
Stickers: {
getDataFromLink: any;
copyStickerToAttachments: (
packId: string,
stickerId: number
) => Promise<typeof window.Signal.Types.Sticker>;
deletePackReference: (id: string, packId: string) => Promise<void>;
downloadEphemeralPack: (packId: string, key: string) => Promise<void>;
downloadQueuedPacks: () => void;
downloadStickerPack: (
id: string,
key: string,
options: WhatIsThis
) => void;
getInitialState: () => WhatIsThis;
load: () => void;
removeEphemeralPack: (packId: string) => Promise<void>;
savePackMetadata: (
packId: string,
packKey: string,
metadata: unknown
) => void;
getStickerPackStatus: (packId: string) => 'downloaded' | 'installed';
getSticker: (
packId: string,
stickerId: number
) => typeof window.Signal.Types.Sticker;
getStickerPack: (packId: string) => WhatIsThis;
getInstalledStickerPacks: () => WhatIsThis;
};
Types: { Types: {
Attachment: { Attachment: {
save: any; save: any;
@ -548,6 +534,8 @@ declare global {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
interface Error { interface Error {
originalError?: Event; originalError?: Event;
reason?: any;
stackForLog?: string;
} }
// Uint8Array and ArrayBuffer are type-compatible in TypeScript's covariant // Uint8Array and ArrayBuffer are type-compatible in TypeScript's covariant
@ -702,7 +690,11 @@ export type WhisperType = {
TapToViewMessagesListener: WhatIsThis; TapToViewMessagesListener: WhatIsThis;
deliveryReceiptQueue: PQueue<WhatIsThis>; deliveryReceiptQueue: PQueue<WhatIsThis>;
deliveryReceiptBatcher: BatcherType<WhatIsThis>; deliveryReceiptBatcher: BatcherType<{
source?: string;
sourceUuid?: string;
timestamp: number;
}>;
RotateSignedPreKeyListener: WhatIsThis; RotateSignedPreKeyListener: WhatIsThis;
AlreadyGroupMemberToast: typeof window.Whisper.ToastView; AlreadyGroupMemberToast: typeof window.Whisper.ToastView;