More protobufjs migration
This commit is contained in:
parent
cf06e6638e
commit
ddbbe3a6b1
70 changed files with 3967 additions and 3369 deletions
|
@ -52,7 +52,6 @@ module.exports = grunt => {
|
|||
libtextsecuretest: {
|
||||
src: [
|
||||
'node_modules/jquery/dist/jquery.js',
|
||||
'components/mock-socket/dist/mock-socket.js',
|
||||
'node_modules/mocha/mocha.js',
|
||||
'node_modules/chai/chai.js',
|
||||
'libtextsecure/test/_test.js',
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mock-socket": "~0.3.2"
|
||||
},
|
||||
"preen": {
|
||||
"bytebuffer": [
|
||||
|
@ -20,9 +19,6 @@
|
|||
"long": [
|
||||
"dist/Long.js"
|
||||
],
|
||||
"mock-socket": [
|
||||
"dist/mock-socket.js"
|
||||
],
|
||||
"mp3lameencoder": [
|
||||
"lib/Mp3LameEncoder.js"
|
||||
],
|
||||
|
|
635
components/mock-socket/dist/mock-socket.js
vendored
635
components/mock-socket/dist/mock-socket.js
vendored
|
@ -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]);
|
|
@ -17,7 +17,7 @@ const GroupChange = require('../../ts/groupChange');
|
|||
const IndexedDB = require('./indexeddb');
|
||||
const Notifications = require('../../ts/notifications');
|
||||
const OS = require('../../ts/OS');
|
||||
const Stickers = require('./stickers');
|
||||
const Stickers = require('../../ts/types/Stickers');
|
||||
const Settings = require('./settings');
|
||||
const RemoteConfig = require('../../ts/RemoteConfig');
|
||||
const Util = require('../../ts/util');
|
||||
|
|
18
js/modules/stickers.d.ts
vendored
18
js/modules/stickers.d.ts
vendored
|
@ -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;
|
|
@ -20,8 +20,6 @@ module.exports = {
|
|||
dcodeIO: true,
|
||||
getString: true,
|
||||
hexToArrayBuffer: true,
|
||||
MockServer: true,
|
||||
MockSocket: true,
|
||||
PROTO_ROOT: true,
|
||||
stringToArrayBuffer: true,
|
||||
},
|
||||
|
|
|
@ -60,8 +60,6 @@ window.hexToArrayBuffer = str => {
|
|||
return ret;
|
||||
};
|
||||
|
||||
window.MockSocket.prototype.addEventListener = () => null;
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.events = {
|
||||
on() {},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -39,11 +39,9 @@
|
|||
|
||||
<script type="text/javascript" src="helpers_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="task_with_timeout_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>
|
||||
|
||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||
|
|
|
@ -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();
|
||||
// });
|
||||
// });
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
19
preload.js
19
preload.js
|
@ -509,25 +509,6 @@ try {
|
|||
// https://stackoverflow.com/a/23299989
|
||||
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.ReactDOM = require('react-dom');
|
||||
window.moment = require('moment');
|
||||
|
|
|
@ -15,8 +15,6 @@ module.exports = {
|
|||
dcodeIO: true,
|
||||
getString: true,
|
||||
hexToArrayBuffer: true,
|
||||
MockServer: true,
|
||||
MockSocket: true,
|
||||
PROTO_ROOT: true,
|
||||
stringToArrayBuffer: true,
|
||||
},
|
||||
|
|
|
@ -7,58 +7,58 @@ const { Stickers } = Signal;
|
|||
|
||||
describe('Stickers', () => {
|
||||
describe('getDataFromLink', () => {
|
||||
it('returns null for invalid URLs', () => {
|
||||
assert.isNull(Stickers.getDataFromLink('https://'));
|
||||
assert.isNull(Stickers.getDataFromLink('signal.art/addstickers/'));
|
||||
it('returns undefined for invalid URLs', () => {
|
||||
assert.isUndefined(Stickers.getDataFromLink('https://'));
|
||||
assert.isUndefined(Stickers.getDataFromLink('signal.art/addstickers/'));
|
||||
});
|
||||
|
||||
it("returns null for URLs that don't have a hash", () => {
|
||||
assert.isNull(
|
||||
it("returns undefined for URLs that don't have a hash", () => {
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink('https://signal.art/addstickers/')
|
||||
);
|
||||
assert.isNull(
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink('https://signal.art/addstickers/#')
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null when no key or pack ID is found', () => {
|
||||
assert.isNull(
|
||||
it('returns undefined when no key or pack ID is found', () => {
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a'
|
||||
)
|
||||
);
|
||||
assert.isNull(
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key='
|
||||
)
|
||||
);
|
||||
assert.isNull(
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
|
||||
)
|
||||
);
|
||||
assert.isNull(
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e&pack_id='
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null when the pack ID is invalid', () => {
|
||||
assert.isNull(
|
||||
it('returns undefined when the pack ID is invalid', () => {
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_id=garbage&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null if the ID or key are passed as arrays', () => {
|
||||
assert.isNull(
|
||||
it('returns undefined if the ID or key are passed as arrays', () => {
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_id[]=c8c83285b547872ac4c589d64a6edd6a&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
|
||||
)
|
||||
);
|
||||
assert.isNull(
|
||||
assert.isUndefined(
|
||||
Stickers.getDataFromLink(
|
||||
'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key[]=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e'
|
||||
)
|
||||
|
|
453
ts/background.ts
453
ts/background.ts
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
import { isNumber, noop } from 'lodash';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { render } from 'react-dom';
|
||||
import {
|
||||
|
@ -9,16 +9,23 @@ import {
|
|||
PlaintextContent,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { DataMessageClass, SyncMessageClass } from './textsecure.d';
|
||||
import { SessionResetsType } from './textsecure/Types.d';
|
||||
import { MessageAttributesType } from './model-types.d';
|
||||
import MessageReceiver from './textsecure/MessageReceiver';
|
||||
import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d';
|
||||
import {
|
||||
MessageAttributesType,
|
||||
ConversationAttributesType,
|
||||
} from './model-types.d';
|
||||
import * as Bytes from './Bytes';
|
||||
import { typedArrayToArrayBuffer } from './Crypto';
|
||||
import { WhatIsThis } from './window.d';
|
||||
import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings';
|
||||
import { SocketStatus } from './types/SocketStatus';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||
import { ChallengeHandler } from './challenge';
|
||||
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 { isNotNil } from './util/isNotNil';
|
||||
import { senderCertificateService } from './services/senderCertificate';
|
||||
|
@ -36,9 +43,30 @@ import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey'
|
|||
import { LatestQueue } from './util/LatestQueue';
|
||||
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||
import {
|
||||
DecryptionErrorType,
|
||||
RetryRequestType,
|
||||
} from './textsecure/MessageReceiver';
|
||||
TypingEvent,
|
||||
ErrorEvent,
|
||||
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 * as universalExpireTimer from './util/universalExpireTimer';
|
||||
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
||||
|
@ -59,6 +87,7 @@ import {
|
|||
SystemTraySetting,
|
||||
parseSystemTraySetting,
|
||||
} from './types/SystemTraySetting';
|
||||
import * as Stickers from './types/Stickers';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
@ -149,7 +178,7 @@ export async function startApp(): Promise<void> {
|
|||
name: 'Whisper.deliveryReceiptBatcher',
|
||||
wait: 500,
|
||||
maxSize: 500,
|
||||
processBatch: async (items: WhatIsThis) => {
|
||||
processBatch: async items => {
|
||||
const byConversationId = window._.groupBy(items, item =>
|
||||
window.ConversationController.ensureContactIds({
|
||||
e164: item.source,
|
||||
|
@ -320,7 +349,7 @@ export async function startApp(): Promise<void> {
|
|||
window.getAccountManager()!.refreshPreKeys();
|
||||
});
|
||||
|
||||
let messageReceiver: WhatIsThis;
|
||||
let messageReceiver: MessageReceiver | undefined;
|
||||
let preMessageReceiverStatus: SocketStatus | undefined;
|
||||
window.getSocketStatus = () => {
|
||||
if (messageReceiver) {
|
||||
|
@ -589,7 +618,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
if (messageReceiver) {
|
||||
messageReceiver.unregisterBatchers();
|
||||
messageReceiver = null;
|
||||
messageReceiver = undefined;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Kick off the download
|
||||
window.Signal.Stickers.downloadEphemeralPack(packId, key);
|
||||
Stickers.downloadEphemeralPack(packId, key);
|
||||
|
||||
const props = {
|
||||
packId,
|
||||
onClose: async () => {
|
||||
window.isShowingModal = false;
|
||||
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) => {
|
||||
window.Signal.Stickers.downloadStickerPack(packId, key, {
|
||||
Stickers.downloadStickerPack(packId, key, {
|
||||
finalStatus: 'installed',
|
||||
});
|
||||
},
|
||||
|
@ -894,7 +923,7 @@ export async function startApp(): Promise<void> {
|
|||
try {
|
||||
await Promise.all([
|
||||
window.ConversationController.load(),
|
||||
window.Signal.Stickers.load(),
|
||||
Stickers.load(),
|
||||
window.Signal.Emojis.load(),
|
||||
window.textsecure.storage.protocol.hydrateCaches(),
|
||||
]);
|
||||
|
@ -963,7 +992,7 @@ export async function startApp(): Promise<void> {
|
|||
},
|
||||
emojis: window.Signal.Emojis.getInitialState(),
|
||||
items: window.storage.getItemsState(),
|
||||
stickers: window.Signal.Stickers.getInitialState(),
|
||||
stickers: Stickers.getInitialState(),
|
||||
user: {
|
||||
attachmentsPath: window.baseAttachmentsPath,
|
||||
stickersPath: window.baseStickersPath,
|
||||
|
@ -1850,6 +1879,8 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
window.getSyncRequest = (timeoutMillis?: number) => {
|
||||
strictAssert(messageReceiver, 'MessageReceiver not initialized');
|
||||
|
||||
const syncRequest = new window.textsecure.SyncRequest(
|
||||
window.textsecure.messaging,
|
||||
messageReceiver,
|
||||
|
@ -1859,8 +1890,8 @@ export async function startApp(): Promise<void> {
|
|||
return syncRequest;
|
||||
};
|
||||
|
||||
let disconnectTimer: WhatIsThis | null = null;
|
||||
let reconnectTimer: WhatIsThis | null = null;
|
||||
let disconnectTimer: NodeJS.Timeout | undefined;
|
||||
let reconnectTimer: number | undefined;
|
||||
function onOffline() {
|
||||
window.log.info('offline');
|
||||
|
||||
|
@ -1886,12 +1917,12 @@ export async function startApp(): Promise<void> {
|
|||
if (disconnectTimer && isSocketOnline()) {
|
||||
window.log.warn('Already online. Had a blip in online/offline status.');
|
||||
clearTimeout(disconnectTimer);
|
||||
disconnectTimer = null;
|
||||
disconnectTimer = undefined;
|
||||
return;
|
||||
}
|
||||
if (disconnectTimer) {
|
||||
clearTimeout(disconnectTimer);
|
||||
disconnectTimer = null;
|
||||
disconnectTimer = undefined;
|
||||
}
|
||||
|
||||
connect();
|
||||
|
@ -1909,7 +1940,7 @@ export async function startApp(): Promise<void> {
|
|||
window.log.info('disconnect');
|
||||
|
||||
// Clear timer, since we're only called when the timer is expired
|
||||
disconnectTimer = null;
|
||||
disconnectTimer = undefined;
|
||||
|
||||
AttachmentDownloads.stop();
|
||||
if (messageReceiver) {
|
||||
|
@ -1934,7 +1965,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
reconnectTimer = undefined;
|
||||
}
|
||||
|
||||
// Bootstrap our online/offline detection, only the first time we connect
|
||||
|
@ -1964,7 +1995,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
if (messageReceiver) {
|
||||
messageReceiver.unregisterBatchers();
|
||||
messageReceiver = null;
|
||||
messageReceiver = undefined;
|
||||
}
|
||||
|
||||
const OLD_USERNAME = window.storage.get('number_id', '');
|
||||
|
@ -2043,8 +2074,11 @@ export async function startApp(): Promise<void> {
|
|||
preMessageReceiverStatus = undefined;
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function addQueuedEventListener(name: string, handler: WhatIsThis) {
|
||||
messageReceiver.addEventListener(name, (...args: Array<WhatIsThis>) =>
|
||||
function queuedEventListener<Args extends Array<unknown>>(
|
||||
handler: (...args: Args) => Promise<void> | void,
|
||||
track = true
|
||||
): (...args: Args) => void {
|
||||
return (...args: Args): void => {
|
||||
eventHandlerQueue.add(async () => {
|
||||
try {
|
||||
await handler(...args);
|
||||
|
@ -2052,40 +2086,97 @@ export async function startApp(): Promise<void> {
|
|||
// message/sent: Message.handleDataMessage has its own queue and will
|
||||
// trigger this event itself when complete.
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
addQueuedEventListener('message', onMessageReceived);
|
||||
addQueuedEventListener('delivery', onDeliveryReceipt);
|
||||
addQueuedEventListener('contact', onContactReceived);
|
||||
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
|
||||
messageReceiver.addEventListener(
|
||||
'message',
|
||||
queuedEventListener(onMessageReceived, false)
|
||||
);
|
||||
addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate);
|
||||
addQueuedEventListener('fetchLatest', onFetchLatestSync);
|
||||
addQueuedEventListener('keys', onKeysSync);
|
||||
messageReceiver.addEventListener(
|
||||
'delivery',
|
||||
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({
|
||||
getMessageReceiver: () => messageReceiver,
|
||||
|
@ -2093,7 +2184,7 @@ export async function startApp(): Promise<void> {
|
|||
});
|
||||
|
||||
if (connectCount === 1) {
|
||||
window.Signal.Stickers.downloadQueuedPacks();
|
||||
Stickers.downloadQueuedPacks();
|
||||
if (!newVersion) {
|
||||
runStorageService();
|
||||
}
|
||||
|
@ -2229,9 +2320,9 @@ export async function startApp(): Promise<void> {
|
|||
syncMessage: true,
|
||||
});
|
||||
|
||||
const installedStickerPacks = window.Signal.Stickers.getInstalledStickerPacks();
|
||||
const installedStickerPacks = Stickers.getInstalledStickerPacks();
|
||||
if (installedStickerPacks.length) {
|
||||
const operations = installedStickerPacks.map((pack: WhatIsThis) => ({
|
||||
const operations = installedStickerPacks.map(pack => ({
|
||||
packId: pack.id,
|
||||
packKey: pack.key,
|
||||
installed: true,
|
||||
|
@ -2313,18 +2404,22 @@ export async function startApp(): Promise<void> {
|
|||
window.log.info(
|
||||
'waitForEmptyEventQueue: Waiting for MessageReceiver empty event...'
|
||||
);
|
||||
let resolve: WhatIsThis;
|
||||
let reject: WhatIsThis;
|
||||
const promise = new Promise((innerResolve, innerReject) => {
|
||||
let resolve: undefined | (() => void);
|
||||
let reject: undefined | ((error: Error) => void);
|
||||
const promise = new Promise<void>((innerResolve, innerReject) => {
|
||||
resolve = innerResolve;
|
||||
reject = innerReject;
|
||||
});
|
||||
|
||||
const timeout = setTimeout(reject, FIVE_MINUTES);
|
||||
const timeout = reject && setTimeout(reject, FIVE_MINUTES);
|
||||
const onEmptyOnce = () => {
|
||||
messageReceiver.removeEventListener('empty', onEmptyOnce);
|
||||
if (messageReceiver) {
|
||||
messageReceiver.removeEventListener('empty', onEmptyOnce);
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
messageReceiver.addEventListener('empty', onEmptyOnce);
|
||||
|
||||
|
@ -2459,7 +2554,7 @@ export async function startApp(): Promise<void> {
|
|||
connect();
|
||||
}
|
||||
|
||||
function onConfiguration(ev: WhatIsThis) {
|
||||
function onConfiguration(ev: ConfigurationEvent) {
|
||||
ev.confirm();
|
||||
|
||||
const { configuration } = ev;
|
||||
|
@ -2470,7 +2565,7 @@ export async function startApp(): Promise<void> {
|
|||
linkPreviews,
|
||||
} = configuration;
|
||||
|
||||
window.storage.put('read-receipt-setting', readReceipts);
|
||||
window.storage.put('read-receipt-setting', Boolean(readReceipts));
|
||||
|
||||
if (
|
||||
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
|
||||
|
||||
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();
|
||||
|
||||
const packs = ev.stickerPacks || [];
|
||||
const packs = ev.stickerPacks;
|
||||
|
||||
packs.forEach((pack: WhatIsThis) => {
|
||||
packs.forEach(pack => {
|
||||
const { id, key, isInstall, isRemove } = pack || {};
|
||||
|
||||
if (!id || !key || (!isInstall && !isRemove)) {
|
||||
|
@ -2572,7 +2667,7 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const status = window.Signal.Stickers.getStickerPackStatus(id);
|
||||
const status = Stickers.getStickerPackStatus(id);
|
||||
|
||||
if (status === 'installed' && isRemove) {
|
||||
window.reduxActions.stickers.uninstallStickerPack(id, key, {
|
||||
|
@ -2584,7 +2679,7 @@ export async function startApp(): Promise<void> {
|
|||
fromSync: true,
|
||||
});
|
||||
} else {
|
||||
window.Signal.Stickers.downloadStickerPack(id, key, {
|
||||
Stickers.downloadStickerPack(id, key, {
|
||||
finalStatus: 'installed',
|
||||
fromSync: true,
|
||||
});
|
||||
|
@ -2598,7 +2693,7 @@ export async function startApp(): Promise<void> {
|
|||
await window.storage.put('synced_at', Date.now());
|
||||
}
|
||||
|
||||
async function onContactReceived(ev: WhatIsThis) {
|
||||
async function onContactReceived(ev: ContactEvent) {
|
||||
const details = ev.contactDetails;
|
||||
|
||||
if (
|
||||
|
@ -2610,20 +2705,20 @@ export async function startApp(): Promise<void> {
|
|||
// special case for syncing details about ourselves
|
||||
if (details.profileKey) {
|
||||
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,
|
||||
uuid: details.uuid,
|
||||
type: 'private',
|
||||
} as WhatIsThis);
|
||||
} as Partial<ConversationAttributesType>) as WhatIsThis);
|
||||
const validationError = c.validate();
|
||||
if (validationError) {
|
||||
window.log.error(
|
||||
'Invalid contact received:',
|
||||
Errors.toLogFormat(validationError as WhatIsThis)
|
||||
Errors.toLogFormat(validationError)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -2638,9 +2733,7 @@ export async function startApp(): Promise<void> {
|
|||
const conversation = window.ConversationController.get(detailsId)!;
|
||||
|
||||
if (details.profileKey) {
|
||||
const profileKey = window.Signal.Crypto.arrayBufferToBase64(
|
||||
details.profileKey
|
||||
);
|
||||
const profileKey = Bytes.toBase64(details.profileKey);
|
||||
conversation.setProfileKey(profileKey);
|
||||
}
|
||||
|
||||
|
@ -2698,14 +2791,18 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
if (details.verified) {
|
||||
const { verified } = details;
|
||||
const verifiedEvent = new Event('verified');
|
||||
verifiedEvent.verified = {
|
||||
state: verified.state,
|
||||
destination: verified.destination,
|
||||
destinationUuid: verified.destinationUuid,
|
||||
identityKey: verified.identityKey.toArrayBuffer(),
|
||||
};
|
||||
(verifiedEvent as WhatIsThis).viaContactSync = true;
|
||||
const verifiedEvent = new VerifiedEvent(
|
||||
{
|
||||
state: dropNull(verified.state),
|
||||
destination: dropNull(verified.destination),
|
||||
destinationUuid: dropNull(verified.destinationUuid),
|
||||
identityKey: verified.identityKey
|
||||
? typedArrayToArrayBuffer(verified.identityKey)
|
||||
: undefined,
|
||||
viaContactSync: true,
|
||||
},
|
||||
noop
|
||||
);
|
||||
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
|
||||
async function onGroupReceived(ev: WhatIsThis) {
|
||||
async function onGroupReceived(ev: GroupEvent) {
|
||||
const details = ev.groupDetails;
|
||||
const { id } = details;
|
||||
|
||||
const idBuffer = window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(id);
|
||||
const idBuffer = id;
|
||||
const idBytes = idBuffer.byteLength;
|
||||
if (idBytes !== 16) {
|
||||
window.log.error(
|
||||
|
@ -2740,7 +2837,7 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
id,
|
||||
Bytes.toBinary(id),
|
||||
'group'
|
||||
);
|
||||
if (isGroupV2(conversation.attributes)) {
|
||||
|
@ -2751,18 +2848,18 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const memberConversations = details.membersE164.map((e164: WhatIsThis) =>
|
||||
const memberConversations = details.membersE164.map(e164 =>
|
||||
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,
|
||||
members,
|
||||
type: 'group',
|
||||
inbox_position: details.inboxPosition,
|
||||
} as WhatIsThis;
|
||||
};
|
||||
|
||||
if (details.active) {
|
||||
updates.left = false;
|
||||
|
@ -2823,8 +2920,16 @@ export async function startApp(): Promise<void> {
|
|||
data,
|
||||
confirm,
|
||||
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);
|
||||
|
||||
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
|
||||
// inside a conversation-specific queue(). Any code here might run before an earlier
|
||||
// message is processed in handleDataMessage().
|
||||
function onMessageReceived(event: WhatIsThis) {
|
||||
function onMessageReceived(event: MessageEvent) {
|
||||
const { data, confirm } = event;
|
||||
|
||||
const messageDescriptor = getMessageDescriptor({
|
||||
|
@ -2903,10 +3008,13 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
if (data.message.reaction) {
|
||||
window.normalizeUuids(
|
||||
data.message.reaction,
|
||||
['targetAuthorUuid'],
|
||||
'background::onMessageReceived'
|
||||
strictAssert(
|
||||
data.message.reaction.targetAuthorUuid,
|
||||
'Reaction without targetAuthorUuid'
|
||||
);
|
||||
const targetAuthorUuid = normalizeUuid(
|
||||
data.message.reaction.targetAuthorUuid,
|
||||
'DataMessage.Reaction.targetAuthorUuid'
|
||||
);
|
||||
|
||||
const { reaction } = data.message;
|
||||
|
@ -2924,7 +3032,7 @@ export async function startApp(): Promise<void> {
|
|||
const reactionModel = Reactions.getSingleton().add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
targetAuthorUuid,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: Date.now(),
|
||||
fromId: window.ConversationController.ensureContactIds({
|
||||
|
@ -2965,7 +3073,7 @@ export async function startApp(): Promise<void> {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function onProfileKeyUpdate({ data, confirm }: WhatIsThis) {
|
||||
async function onProfileKeyUpdate({ data, confirm }: ProfileKeyUpdateEvent) {
|
||||
const conversationId = window.ConversationController.ensureContactIds({
|
||||
e164: data.source,
|
||||
uuid: data.sourceUuid,
|
||||
|
@ -3007,7 +3115,11 @@ export async function startApp(): Promise<void> {
|
|||
data,
|
||||
confirm,
|
||||
messageDescriptor,
|
||||
}: WhatIsThis) {
|
||||
}: {
|
||||
data: SentEventData;
|
||||
confirm: () => void;
|
||||
messageDescriptor: MessageDescriptor;
|
||||
}) {
|
||||
// First set profileSharing = true for the conversation we sent to
|
||||
const { id } = messageDescriptor;
|
||||
// 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();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
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
|
||||
await me.setProfileKey(profileKey);
|
||||
|
@ -3028,18 +3144,17 @@ export async function startApp(): Promise<void> {
|
|||
return confirm();
|
||||
}
|
||||
|
||||
function createSentMessage(data: WhatIsThis, descriptor: MessageDescriptor) {
|
||||
function createSentMessage(
|
||||
data: SentEventData,
|
||||
descriptor: MessageDescriptor
|
||||
) {
|
||||
const now = Date.now();
|
||||
const timestamp = data.timestamp || now;
|
||||
|
||||
const unidentifiedStatus: Array<SyncMessageClass.Sent.UnidentifiedDeliveryStatus> = Array.isArray(
|
||||
data.unidentifiedStatus
|
||||
)
|
||||
? data.unidentifiedStatus
|
||||
: [];
|
||||
|
||||
const { unidentifiedStatus = [] } = data;
|
||||
let sentTo: Array<string> = [];
|
||||
|
||||
let unidentifiedDeliveries: Array<string> = [];
|
||||
if (unidentifiedStatus.length) {
|
||||
sentTo = unidentifiedStatus
|
||||
.map(item => item.destinationUuid || item.destination)
|
||||
|
@ -3048,13 +3163,12 @@ export async function startApp(): Promise<void> {
|
|||
const unidentified = window._.filter(data.unidentifiedStatus, item =>
|
||||
Boolean(item.unidentified)
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
data.unidentifiedDeliveries = unidentified.map(
|
||||
item => item.destinationUuid || item.destination
|
||||
);
|
||||
unidentifiedDeliveries = unidentified
|
||||
.map(item => item.destinationUuid || item.destination)
|
||||
.filter(isNotNil);
|
||||
}
|
||||
|
||||
return new window.Whisper.Message({
|
||||
return new window.Whisper.Message(({
|
||||
source: window.textsecure.storage.user.getNumber(),
|
||||
sourceUuid: window.textsecure.storage.user.getUuid(),
|
||||
sourceDevice: data.device,
|
||||
|
@ -3067,12 +3181,12 @@ export async function startApp(): Promise<void> {
|
|||
timestamp,
|
||||
type: 'outgoing',
|
||||
sent: true,
|
||||
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
||||
unidentifiedDeliveries,
|
||||
expirationStartTimestamp: Math.min(
|
||||
data.expirationStartTimestamp || timestamp,
|
||||
now
|
||||
),
|
||||
} as WhatIsThis);
|
||||
} as Partial<MessageAttributesType>) as WhatIsThis);
|
||||
}
|
||||
|
||||
// Works with 'sent' and 'message' data sent from MessageReceiver, with a little massage
|
||||
|
@ -3084,11 +3198,11 @@ export async function startApp(): Promise<void> {
|
|||
destination,
|
||||
destinationUuid,
|
||||
}: {
|
||||
message: DataMessageClass;
|
||||
source: string;
|
||||
sourceUuid: string;
|
||||
destination: string;
|
||||
destinationUuid: string;
|
||||
message: ProcessedDataMessage;
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
destination?: string;
|
||||
destinationUuid?: string;
|
||||
}): MessageDescriptor => {
|
||||
if (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
|
||||
// inside a conversation-specific queue(). Any code here might run before an earlier
|
||||
// message is processed in handleDataMessage().
|
||||
function onSentMessage(event: WhatIsThis) {
|
||||
function onSentMessage(event: SentEvent) {
|
||||
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({
|
||||
...data,
|
||||
|
||||
// 'sent' event: the sender is always us!
|
||||
source: window.textsecure.storage.user.getNumber(),
|
||||
sourceUuid: window.textsecure.storage.user.getUuid(),
|
||||
source,
|
||||
sourceUuid,
|
||||
});
|
||||
|
||||
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
|
||||
|
@ -3210,10 +3329,13 @@ export async function startApp(): Promise<void> {
|
|||
const message = createSentMessage(data, messageDescriptor);
|
||||
|
||||
if (data.message.reaction) {
|
||||
window.normalizeUuids(
|
||||
data.message.reaction,
|
||||
['targetAuthorUuid'],
|
||||
'background::onSentMessage'
|
||||
strictAssert(
|
||||
data.message.reaction.targetAuthorUuid,
|
||||
'Reaction without targetAuthorUuid'
|
||||
);
|
||||
const targetAuthorUuid = normalizeUuid(
|
||||
data.message.reaction.targetAuthorUuid,
|
||||
'DataMessage.Reaction.targetAuthorUuid'
|
||||
);
|
||||
|
||||
const { reaction } = data.message;
|
||||
|
@ -3228,7 +3350,7 @@ export async function startApp(): Promise<void> {
|
|||
const reactionModel = Reactions.getSingleton().add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
targetAuthorUuid,
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: Date.now(),
|
||||
fromId: window.ConversationController.getOurConversationId(),
|
||||
|
@ -3246,7 +3368,7 @@ export async function startApp(): Promise<void> {
|
|||
window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
|
||||
const deleteModel = Deletes.getSingleton().add({
|
||||
targetSentTimestamp: del.targetSentTimestamp,
|
||||
serverTimestamp: del.serverTimestamp,
|
||||
serverTimestamp: data.serverTimestamp,
|
||||
fromId: window.ConversationController.getOurConversationId(),
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
|
@ -3274,14 +3396,14 @@ export async function startApp(): Promise<void> {
|
|||
};
|
||||
|
||||
function initIncomingMessage(
|
||||
data: WhatIsThis,
|
||||
data: MessageEventData,
|
||||
descriptor: MessageDescriptor
|
||||
) {
|
||||
assert(
|
||||
Boolean(data.receivedAtCounter),
|
||||
`Did not receive receivedAtCounter for message: ${data.timestamp}`
|
||||
);
|
||||
return new window.Whisper.Message({
|
||||
return new window.Whisper.Message(({
|
||||
source: data.source,
|
||||
sourceUuid: data.sourceUuid,
|
||||
sourceDevice: data.sourceDevice,
|
||||
|
@ -3293,13 +3415,14 @@ export async function startApp(): Promise<void> {
|
|||
conversationId: descriptor.id,
|
||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||
type: 'incoming',
|
||||
unread: 1,
|
||||
} as WhatIsThis);
|
||||
unread: true,
|
||||
timestamp: data.timestamp,
|
||||
} as Partial<MessageAttributesType>) as WhatIsThis);
|
||||
}
|
||||
|
||||
// Returns `false` if this message isn't a group call message.
|
||||
function handleGroupCallUpdateMessage(
|
||||
message: DataMessageClass,
|
||||
message: ProcessedDataMessage,
|
||||
messageDescriptor: MessageDescriptor
|
||||
): boolean {
|
||||
if (message.groupCallUpdate) {
|
||||
|
@ -3329,7 +3452,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
if (messageReceiver) {
|
||||
messageReceiver.unregisterBatchers();
|
||||
messageReceiver = null;
|
||||
messageReceiver = undefined;
|
||||
}
|
||||
|
||||
onEmpty();
|
||||
|
@ -3399,7 +3522,7 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
function onError(ev: WhatIsThis) {
|
||||
function onError(ev: ErrorEvent) {
|
||||
const { error } = ev;
|
||||
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');
|
||||
}
|
||||
|
||||
type RetryRequestEventType = Event & {
|
||||
retryRequest: RetryRequestType;
|
||||
};
|
||||
|
||||
function isInList(
|
||||
conversation: ConversationModel,
|
||||
list: Array<string | undefined | null> | undefined
|
||||
|
@ -3471,7 +3590,7 @@ export async function startApp(): Promise<void> {
|
|||
requesterUuid,
|
||||
requesterDevice,
|
||||
senderDevice,
|
||||
}: RetryRequestType): Promise<void> {
|
||||
}: RetryRequestEventData): Promise<void> {
|
||||
const ourDeviceId = parseIntOrThrow(
|
||||
window.textsecure.storage.user.getDeviceId(),
|
||||
'archiveSessionOnMatch/getDeviceId'
|
||||
|
@ -3486,7 +3605,7 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
async function sendDistributionMessageOrNullMessage(
|
||||
options: RetryRequestType
|
||||
options: RetryRequestEventData
|
||||
): Promise<void> {
|
||||
const { groupId, requesterUuid } = options;
|
||||
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 {
|
||||
requesterDevice,
|
||||
|
@ -3637,11 +3756,7 @@ export async function startApp(): Promise<void> {
|
|||
await targetMessage.resend(requesterUuid);
|
||||
}
|
||||
|
||||
type DecryptionErrorEventType = Event & {
|
||||
decryptionError: DecryptionErrorType;
|
||||
};
|
||||
|
||||
async function onDecryptionError(event: DecryptionErrorEventType) {
|
||||
async function onDecryptionError(event: DecryptionErrorEvent) {
|
||||
const { decryptionError } = event;
|
||||
const { senderUuid, senderDevice, timestamp } = decryptionError;
|
||||
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
|
||||
|
@ -3666,7 +3781,7 @@ export async function startApp(): Promise<void> {
|
|||
window.log.info(`onDecryptionError/${logId}: ...complete`);
|
||||
}
|
||||
|
||||
async function requestResend(decryptionError: DecryptionErrorType) {
|
||||
async function requestResend(decryptionError: DecryptionErrorEventData) {
|
||||
const {
|
||||
cipherTextBytes,
|
||||
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 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();
|
||||
|
||||
const { source, sourceUuid, timestamp } = ev;
|
||||
|
@ -3833,7 +3950,7 @@ export async function startApp(): Promise<void> {
|
|||
ViewSyncs.getSingleton().onSync(sync);
|
||||
}
|
||||
|
||||
async function onFetchLatestSync(ev: WhatIsThis) {
|
||||
async function onFetchLatestSync(ev: FetchLatestEvent) {
|
||||
ev.confirm();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
const {
|
||||
|
@ -3907,9 +4024,9 @@ export async function startApp(): Promise<void> {
|
|||
MessageRequests.getSingleton().onResponse(sync);
|
||||
}
|
||||
|
||||
function onReadReceipt(ev: WhatIsThis) {
|
||||
const readAt = ev.timestamp;
|
||||
function onReadReceipt(ev: ReadEvent) {
|
||||
const { envelopeTimestamp, timestamp, source, sourceUuid } = ev.read;
|
||||
const readAt = envelopeTimestamp;
|
||||
const reader = window.ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
|
@ -3941,9 +4058,9 @@ export async function startApp(): Promise<void> {
|
|||
ReadReceipts.getSingleton().onReceipt(receipt);
|
||||
}
|
||||
|
||||
function onReadSync(ev: WhatIsThis) {
|
||||
const readAt = ev.timestamp;
|
||||
function onReadSync(ev: ReadSyncEvent) {
|
||||
const { envelopeTimestamp, sender, senderUuid, timestamp } = ev.read;
|
||||
const readAt = envelopeTimestamp;
|
||||
const senderId = window.ConversationController.ensureContactIds({
|
||||
e164: sender,
|
||||
uuid: senderUuid,
|
||||
|
@ -3974,7 +4091,7 @@ export async function startApp(): Promise<void> {
|
|||
return ReadSyncs.getSingleton().onReceipt(receipt);
|
||||
}
|
||||
|
||||
async function onVerified(ev: WhatIsThis) {
|
||||
async function onVerified(ev: VerifiedEvent) {
|
||||
const e164 = ev.verified.destination;
|
||||
const uuid = ev.verified.destinationUuid;
|
||||
const key = ev.verified.identityKey;
|
||||
|
@ -3984,18 +4101,18 @@ export async function startApp(): Promise<void> {
|
|||
ev.confirm();
|
||||
}
|
||||
|
||||
const c = new window.Whisper.Conversation({
|
||||
const c = new window.Whisper.Conversation(({
|
||||
e164,
|
||||
uuid,
|
||||
type: 'private',
|
||||
} as WhatIsThis);
|
||||
} as Partial<ConversationAttributesType>) as WhatIsThis);
|
||||
const error = c.validate();
|
||||
if (error) {
|
||||
window.log.error(
|
||||
'Invalid verified sync received:',
|
||||
e164,
|
||||
uuid,
|
||||
Errors.toLogFormat(error as WhatIsThis)
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -4019,7 +4136,7 @@ export async function startApp(): Promise<void> {
|
|||
e164,
|
||||
uuid,
|
||||
state,
|
||||
ev.viaContactSync ? 'via contact sync' : ''
|
||||
ev.verified.viaContactSync ? 'via contact sync' : ''
|
||||
);
|
||||
|
||||
const verifiedId = window.ConversationController.ensureContactIds({
|
||||
|
@ -4031,7 +4148,7 @@ export async function startApp(): Promise<void> {
|
|||
const contact = window.ConversationController.get(verifiedId)!;
|
||||
const options = {
|
||||
viaSyncMessage: true,
|
||||
viaContactSync: ev.viaContactSync,
|
||||
viaContactSync: ev.verified.viaContactSync,
|
||||
key,
|
||||
};
|
||||
|
||||
|
@ -4044,7 +4161,7 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
function onDeliveryReceipt(ev: WhatIsThis) {
|
||||
function onDeliveryReceipt(ev: DeliveryEvent) {
|
||||
const { deliveryReceipt } = ev;
|
||||
const {
|
||||
envelopeTimestamp,
|
||||
|
|
|
@ -8,6 +8,7 @@ import { storiesOf } from '@storybook/react';
|
|||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { GroupV2ChangeType } from '../../groups';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
import { SmartContactRendererType } from '../../groupChange';
|
||||
import { GroupV2Change } from './GroupV2Change';
|
||||
|
||||
|
@ -20,25 +21,8 @@ const CONTACT_C = 'CONTACT_C';
|
|||
const ADMIN_A = 'ADMIN_A';
|
||||
const INVITEE_A = 'INVITEE_A';
|
||||
|
||||
class AccessControlEnum {
|
||||
static UNKNOWN = 0;
|
||||
|
||||
static ANY = 1;
|
||||
|
||||
static MEMBER = 2;
|
||||
|
||||
static ADMINISTRATOR = 3;
|
||||
|
||||
static UNSATISFIABLE = 4;
|
||||
}
|
||||
|
||||
class RoleEnum {
|
||||
static UNKNOWN = 0;
|
||||
|
||||
static ADMINISTRATOR = 1;
|
||||
|
||||
static DEFAULT = 2;
|
||||
}
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
const RoleEnum = Proto.Member.Role;
|
||||
|
||||
const renderContact: SmartContactRendererType = (conversationId: string) => (
|
||||
<React.Fragment key={conversationId}>
|
||||
|
@ -48,13 +32,11 @@ const renderContact: SmartContactRendererType = (conversationId: string) => (
|
|||
|
||||
const renderChange = (change: GroupV2ChangeType, groupName?: string) => (
|
||||
<GroupV2Change
|
||||
AccessControlEnum={AccessControlEnum}
|
||||
change={change}
|
||||
groupName={groupName}
|
||||
i18n={i18n}
|
||||
ourConversationId={OUR_ID}
|
||||
renderContact={renderContact}
|
||||
RoleEnum={RoleEnum}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -14,14 +14,10 @@ import { GroupV2ChangeType, GroupV2DescriptionChangeType } from '../../groups';
|
|||
import { renderChange, SmartContactRendererType } from '../../groupChange';
|
||||
import { Modal } from '../Modal';
|
||||
|
||||
import { AccessControlClass, MemberClass } from '../../textsecure.d';
|
||||
|
||||
export type PropsDataType = {
|
||||
groupName?: string;
|
||||
ourConversationId: string;
|
||||
change: GroupV2ChangeType;
|
||||
AccessControlEnum: typeof AccessControlClass.AccessRequired;
|
||||
RoleEnum: typeof MemberClass.Role;
|
||||
};
|
||||
|
||||
export type PropsHousekeepingType = {
|
||||
|
@ -40,15 +36,7 @@ function renderStringToIntl(
|
|||
}
|
||||
|
||||
export function GroupV2Change(props: PropsType): ReactElement {
|
||||
const {
|
||||
AccessControlEnum,
|
||||
change,
|
||||
groupName,
|
||||
i18n,
|
||||
ourConversationId,
|
||||
renderContact,
|
||||
RoleEnum,
|
||||
} = props;
|
||||
const { change, groupName, i18n, ourConversationId, renderContact } = props;
|
||||
|
||||
const [
|
||||
isGroupDescriptionDialogOpen,
|
||||
|
@ -64,12 +52,10 @@ export function GroupV2Change(props: PropsType): ReactElement {
|
|||
<div className="module-group-v2-change">
|
||||
<div className="module-group-v2-change--icon" />
|
||||
{renderChange(change, {
|
||||
AccessControlEnum,
|
||||
i18n,
|
||||
ourConversationId,
|
||||
renderContact,
|
||||
renderString: renderStringToIntl,
|
||||
RoleEnum,
|
||||
}).map((item: FullJSXType, index: number) => (
|
||||
// Difficult to find a unique key for this type
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
|
|
|
@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||
import enMessages from '../../../../_locales/en/messages.json';
|
||||
import { GroupLinkManagement, PropsType } from './GroupLinkManagement';
|
||||
import { SignalService as Proto } from '../../../protobuf';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
|
||||
|
@ -19,17 +20,7 @@ const story = storiesOf(
|
|||
module
|
||||
);
|
||||
|
||||
class AccessEnum {
|
||||
static ANY = 0;
|
||||
|
||||
static UNKNOWN = 1;
|
||||
|
||||
static MEMBER = 2;
|
||||
|
||||
static ADMINISTRATOR = 3;
|
||||
|
||||
static UNSATISFIABLE = 4;
|
||||
}
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
|
||||
function getConversation(
|
||||
groupLink?: string,
|
||||
|
@ -47,7 +38,7 @@ function getConversation(
|
|||
accessControlAddFromInviteLink:
|
||||
accessControlAddFromInviteLink !== undefined
|
||||
? accessControlAddFromInviteLink
|
||||
: AccessEnum.UNSATISFIABLE,
|
||||
: AccessControlEnum.UNSATISFIABLE,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -55,7 +46,6 @@ const createProps = (
|
|||
conversation?: ConversationType,
|
||||
isAdmin = false
|
||||
): PropsType => ({
|
||||
accessEnum: AccessEnum,
|
||||
changeHasGroupLink: action('changeHasGroupLink'),
|
||||
conversation: conversation || getConversation(),
|
||||
copyGroupLink: action('copyGroupLink'),
|
||||
|
@ -75,7 +65,7 @@ story.add('Off (Admin)', () => {
|
|||
|
||||
story.add('On (Admin)', () => {
|
||||
const props = createProps(
|
||||
getConversation('https://signal.group/1', AccessEnum.ANY),
|
||||
getConversation('https://signal.group/1', AccessControlEnum.ANY),
|
||||
true
|
||||
);
|
||||
|
||||
|
@ -84,7 +74,7 @@ story.add('On (Admin)', () => {
|
|||
|
||||
story.add('On (Admin + Admin Approval Needed)', () => {
|
||||
const props = createProps(
|
||||
getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR),
|
||||
getConversation('https://signal.group/1', AccessControlEnum.ADMINISTRATOR),
|
||||
true
|
||||
);
|
||||
|
||||
|
@ -93,7 +83,7 @@ story.add('On (Admin + Admin Approval Needed)', () => {
|
|||
|
||||
story.add('On (Non-admin)', () => {
|
||||
const props = createProps(
|
||||
getConversation('https://signal.group/1', AccessEnum.ANY)
|
||||
getConversation('https://signal.group/1', AccessControlEnum.ANY)
|
||||
);
|
||||
|
||||
return <GroupLinkManagement {...props} />;
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||
import { SignalService as Proto } from '../../../protobuf';
|
||||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { AccessControlClass } from '../../../textsecure.d';
|
||||
import { Select } from '../../Select';
|
||||
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
|
||||
export type PropsType = {
|
||||
accessEnum: typeof AccessControlClass.AccessRequired;
|
||||
changeHasGroupLink: (value: boolean) => void;
|
||||
conversation?: ConversationType;
|
||||
copyGroupLink: (groupLink: string) => void;
|
||||
|
@ -23,7 +24,6 @@ export type PropsType = {
|
|||
};
|
||||
|
||||
export const GroupLinkManagement: React.ComponentType<PropsType> = ({
|
||||
accessEnum,
|
||||
changeHasGroupLink,
|
||||
conversation,
|
||||
copyGroupLink,
|
||||
|
@ -43,11 +43,13 @@ export const GroupLinkManagement: React.ComponentType<PropsType> = ({
|
|||
};
|
||||
|
||||
const membersNeedAdminApproval =
|
||||
conversation.accessControlAddFromInviteLink === accessEnum.ADMINISTRATOR;
|
||||
conversation.accessControlAddFromInviteLink ===
|
||||
AccessControlEnum.ADMINISTRATOR;
|
||||
|
||||
const hasGroupLink =
|
||||
conversation.groupLink &&
|
||||
conversation.accessControlAddFromInviteLink !== accessEnum.UNSATISFIABLE;
|
||||
conversation.accessControlAddFromInviteLink !==
|
||||
AccessControlEnum.UNSATISFIABLE;
|
||||
const groupLinkInfo = hasGroupLink ? conversation.groupLink : '';
|
||||
|
||||
return (
|
||||
|
|
|
@ -29,20 +29,7 @@ const conversation: ConversationType = getDefaultConversation({
|
|||
sharedGroupNames: [],
|
||||
});
|
||||
|
||||
class AccessEnum {
|
||||
static ANY = 0;
|
||||
|
||||
static UNKNOWN = 1;
|
||||
|
||||
static MEMBER = 2;
|
||||
|
||||
static ADMINISTRATOR = 3;
|
||||
|
||||
static UNSATISFIABLE = 4;
|
||||
}
|
||||
|
||||
const createProps = (): PropsType => ({
|
||||
accessEnum: AccessEnum,
|
||||
conversation,
|
||||
i18n,
|
||||
setAccessControlAttributesSetting: action(
|
||||
|
|
|
@ -6,14 +6,12 @@ import React from 'react';
|
|||
import { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
import { getAccessControlOptions } from '../../../util/getAccessControlOptions';
|
||||
import { AccessControlClass } from '../../../textsecure.d';
|
||||
|
||||
import { PanelRow } from './PanelRow';
|
||||
import { PanelSection } from './PanelSection';
|
||||
import { Select } from '../../Select';
|
||||
|
||||
export type PropsType = {
|
||||
accessEnum: typeof AccessControlClass.AccessRequired;
|
||||
conversation?: ConversationType;
|
||||
i18n: LocalizerType;
|
||||
setAccessControlAttributesSetting: (value: number) => void;
|
||||
|
@ -21,7 +19,6 @@ export type PropsType = {
|
|||
};
|
||||
|
||||
export const GroupV2Permissions: React.ComponentType<PropsType> = ({
|
||||
accessEnum,
|
||||
conversation,
|
||||
i18n,
|
||||
setAccessControlAttributesSetting,
|
||||
|
@ -37,7 +34,7 @@ export const GroupV2Permissions: React.ComponentType<PropsType> = ({
|
|||
const updateAccessControlMembers = (value: string) => {
|
||||
setAccessControlMembersSetting(Number(value));
|
||||
};
|
||||
const accessControlOptions = getAccessControlOptions(accessEnum, i18n);
|
||||
const accessControlOptions = getAccessControlOptions(i18n);
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
|
|
|
@ -6,8 +6,8 @@ import { LocalizerType } from './types/Util';
|
|||
import { ReplacementValuesType } from './types/I18N';
|
||||
import { missingCaseError } from './util/missingCaseError';
|
||||
|
||||
import { AccessControlClass, MemberClass } from './textsecure.d';
|
||||
import { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups';
|
||||
import { SignalService as Proto } from './protobuf';
|
||||
|
||||
export type SmartContactRendererType = (conversationId: string) => FullJSXType;
|
||||
export type StringRendererType = (
|
||||
|
@ -17,15 +17,16 @@ export type StringRendererType = (
|
|||
) => FullJSXType;
|
||||
|
||||
export type RenderOptionsType = {
|
||||
AccessControlEnum: typeof AccessControlClass.AccessRequired;
|
||||
from?: string;
|
||||
i18n: LocalizerType;
|
||||
ourConversationId: string;
|
||||
renderContact: SmartContactRendererType;
|
||||
renderString: StringRendererType;
|
||||
RoleEnum: typeof MemberClass.Role;
|
||||
};
|
||||
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
const RoleEnum = Proto.Member.Role;
|
||||
|
||||
export function renderChange(
|
||||
change: GroupV2ChangeType,
|
||||
options: RenderOptionsType
|
||||
|
@ -45,13 +46,11 @@ export function renderChangeDetail(
|
|||
options: RenderOptionsType
|
||||
): FullJSXType {
|
||||
const {
|
||||
AccessControlEnum,
|
||||
from,
|
||||
i18n,
|
||||
ourConversationId,
|
||||
renderContact,
|
||||
renderString,
|
||||
RoleEnum,
|
||||
} = options;
|
||||
const fromYou = Boolean(from && from === ourConversationId);
|
||||
|
||||
|
|
|
@ -1370,6 +1370,13 @@ export function idForLogging(groupId: string | undefined): string {
|
|||
}
|
||||
|
||||
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 cached = groupFieldsCache.get(cacheKey);
|
||||
if (cached) {
|
||||
|
|
4
ts/model-types.d.ts
vendored
4
ts/model-types.d.ts
vendored
|
@ -58,7 +58,7 @@ export type QuotedMessageType = {
|
|||
author?: string;
|
||||
authorUuid?: string;
|
||||
bodyRanges?: BodyRangesType;
|
||||
id: string;
|
||||
id: number;
|
||||
referencedMessageNotFound: boolean;
|
||||
isViewOnce: boolean;
|
||||
text?: string;
|
||||
|
@ -190,6 +190,8 @@ export type MessageAttributesType = {
|
|||
// Backwards-compatibility with prerelease data schema
|
||||
invitedGV2Members?: Array<GroupV2PendingMemberType>;
|
||||
droppedGV2MemberIds?: Array<string>;
|
||||
|
||||
sendHQImages?: boolean;
|
||||
};
|
||||
|
||||
export type ConversationAttributesTypeType = 'private' | 'group';
|
||||
|
|
|
@ -14,7 +14,9 @@ import {
|
|||
VerificationOptions,
|
||||
WhatIsThis,
|
||||
} from '../model-types.d';
|
||||
import { AttachmentType } from '../types/Attachment';
|
||||
import { CallMode, CallHistoryDetailsType } from '../types/Calling';
|
||||
import * as Stickers from '../types/Stickers';
|
||||
import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
|
@ -3100,7 +3102,7 @@ export class ConversationModel extends window.Backbone
|
|||
? [{ contentType: 'image/jpeg', fileName: null }]
|
||||
: await this.getQuoteAttachment(attachments, preview, sticker),
|
||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||
id: String(quotedMessage.get('sent_at')),
|
||||
id: quotedMessage.get('sent_at'),
|
||||
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||
messageId: quotedMessage.get('id'),
|
||||
referencedMessageNotFound: false,
|
||||
|
@ -3109,8 +3111,8 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
async sendStickerMessage(packId: string, stickerId: number): Promise<void> {
|
||||
const packData = window.Signal.Stickers.getStickerPack(packId);
|
||||
const stickerData = window.Signal.Stickers.getSticker(packId, stickerId);
|
||||
const packData = Stickers.getStickerPack(packId);
|
||||
const stickerData = Stickers.getSticker(packId, stickerId);
|
||||
if (!stickerData || !packData) {
|
||||
window.log.warn(
|
||||
`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);
|
||||
}
|
||||
|
||||
|
@ -3451,10 +3453,10 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
sendMessage(
|
||||
body: string | null,
|
||||
attachments: Array<WhatIsThis>,
|
||||
quote: WhatIsThis,
|
||||
preview: WhatIsThis,
|
||||
body: string | undefined,
|
||||
attachments: Array<AttachmentType>,
|
||||
quote?: QuotedMessageType,
|
||||
preview?: WhatIsThis,
|
||||
sticker?: WhatIsThis,
|
||||
mentions?: BodyRangesType,
|
||||
{
|
||||
|
@ -3503,6 +3505,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
// Here we move attachments to disk
|
||||
const messageWithSchema = await upgradeMessageSchema({
|
||||
timestamp: now,
|
||||
type: 'outgoing',
|
||||
body,
|
||||
conversationId: this.id,
|
||||
|
@ -3575,7 +3578,7 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
const attachmentsWithData = await Promise.all(
|
||||
messageWithSchema.attachments.map(loadAttachmentData)
|
||||
messageWithSchema.attachments?.map(loadAttachmentData) ?? []
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -5086,7 +5089,7 @@ export class ConversationModel extends window.Backbone
|
|||
isTyping: boolean;
|
||||
senderId: string;
|
||||
fromMe: boolean;
|
||||
senderDevice: string;
|
||||
senderDevice: number;
|
||||
}): void {
|
||||
const { isTyping, senderId, fromMe, senderDevice } = options;
|
||||
|
||||
|
|
|
@ -12,9 +12,10 @@ import {
|
|||
QuotedMessageType,
|
||||
WhatIsThis,
|
||||
} from '../model-types.d';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { map, filter, find } from '../util/iterables';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { DataMessageClass } from '../textsecure.d';
|
||||
import { ConversationModel } from './conversations';
|
||||
import { MessageStatusType } from '../components/conversation/Message';
|
||||
import {
|
||||
|
@ -23,9 +24,17 @@ import {
|
|||
} from '../state/smart/MessageDetail';
|
||||
import { getCallingNotificationText } from '../util/callingNotification';
|
||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||
import { ProcessedDataMessage, ProcessedQuote } from '../textsecure/Types.d';
|
||||
import * as expirationTimer from '../util/expirationTimer';
|
||||
|
||||
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 { MIMEType, IMAGE_WEBP } from '../types/MIME';
|
||||
import { ourProfileKeyService } from '../services/ourProfileKey';
|
||||
|
@ -107,12 +116,6 @@ const {
|
|||
loadStickerData,
|
||||
upgradeMessageSchema,
|
||||
} = window.Signal.Migrations;
|
||||
const {
|
||||
copyStickerToAttachments,
|
||||
deletePackReference,
|
||||
savePackMetadata,
|
||||
getStickerPackStatus,
|
||||
} = window.Signal.Stickers;
|
||||
const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
|
||||
|
||||
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
|
||||
|
@ -124,7 +127,7 @@ const includesAny = <T>(haystack: Array<T>, ...needles: Array<T>) =>
|
|||
export function isQuoteAMatch(
|
||||
message: MessageModel | null | undefined,
|
||||
conversationId: string,
|
||||
quote: QuotedMessageType | DataMessageClass.Quote
|
||||
quote: QuotedMessageType
|
||||
): message is MessageModel {
|
||||
if (!message) {
|
||||
return false;
|
||||
|
@ -614,7 +617,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
const stickerData = this.get('sticker');
|
||||
if (stickerData) {
|
||||
const sticker = window.Signal.Stickers.getSticker(
|
||||
const sticker = Stickers.getSticker(
|
||||
stickerData.packId,
|
||||
stickerData.stickerId
|
||||
);
|
||||
|
@ -624,7 +627,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
return {
|
||||
text: window.i18n('message--getNotificationText--stickers'),
|
||||
emoji,
|
||||
emoji: dropNull(emoji),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1460,11 +1463,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
senderKeyInfo.distributionId
|
||||
);
|
||||
|
||||
contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap(
|
||||
window.Signal.Crypto.typedArrayToArrayBuffer(
|
||||
senderKeyDistributionMessage.serialize()
|
||||
)
|
||||
);
|
||||
contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2217,18 +2216,48 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async copyFromQuotedMessage(
|
||||
message: DataMessageClass,
|
||||
quote: ProcessedQuote | undefined,
|
||||
conversationId: string
|
||||
): Promise<DataMessageClass> {
|
||||
const { quote } = message;
|
||||
): Promise<QuotedMessageType | undefined> {
|
||||
if (!quote) {
|
||||
return message;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 matchingMessage = find(inMemoryMessages, item =>
|
||||
isQuoteAMatch(item, conversationId, quote)
|
||||
isQuoteAMatch(item, conversationId, result)
|
||||
);
|
||||
|
||||
let queryMessage: undefined | MessageModel;
|
||||
|
@ -2241,35 +2270,35 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
MessageCollection: window.Whisper.MessageCollection,
|
||||
});
|
||||
const found = collection.find(item =>
|
||||
isQuoteAMatch(item, conversationId, quote)
|
||||
isQuoteAMatch(item, conversationId, result)
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
quote.referencedMessageNotFound = true;
|
||||
return message;
|
||||
result.referencedMessageNotFound = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
queryMessage = window.MessageController.register(found.id, found);
|
||||
}
|
||||
|
||||
if (queryMessage) {
|
||||
await this.copyQuoteContentFromOriginal(queryMessage, quote);
|
||||
await this.copyQuoteContentFromOriginal(queryMessage, result);
|
||||
}
|
||||
|
||||
return message;
|
||||
return result;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async copyQuoteContentFromOriginal(
|
||||
originalMessage: MessageModel,
|
||||
quote: QuotedMessageType | DataMessageClass.Quote
|
||||
quote: QuotedMessageType
|
||||
): Promise<void> {
|
||||
const { attachments } = quote;
|
||||
const firstAttachment = attachments ? attachments[0] : undefined;
|
||||
|
||||
if (isTapToView(originalMessage.attributes)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
quote.text = null;
|
||||
quote.text = undefined;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
quote.attachments = [
|
||||
{
|
||||
|
@ -2362,7 +2391,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
handleDataMessage(
|
||||
initialMessage: DataMessageClass,
|
||||
initialMessage: ProcessedDataMessage,
|
||||
confirm: () => void,
|
||||
options: { data?: typeof window.WhatIsThis } = {}
|
||||
): WhatIsThis {
|
||||
|
@ -2631,16 +2660,19 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
});
|
||||
}
|
||||
|
||||
const withQuoteReference = await this.copyFromQuotedMessage(
|
||||
initialMessage,
|
||||
conversation.id
|
||||
);
|
||||
const withQuoteReference = {
|
||||
...initialMessage,
|
||||
quote: await this.copyFromQuotedMessage(
|
||||
initialMessage.quote,
|
||||
conversation.id
|
||||
),
|
||||
};
|
||||
const dataMessage = await upgradeMessageSchema(withQuoteReference);
|
||||
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
|
||||
const urls = LinkPreview.findLinks(dataMessage.body);
|
||||
const urls = LinkPreview.findLinks(dataMessage.body || '');
|
||||
const incomingPreview = dataMessage.preview || [];
|
||||
const preview = incomingPreview.filter(
|
||||
(item: typeof window.WhatIsThis) =>
|
||||
|
|
|
@ -22,7 +22,6 @@ import {
|
|||
GumVideoCapturer,
|
||||
HangupMessage,
|
||||
HangupType,
|
||||
OfferType,
|
||||
OpaqueMessage,
|
||||
PeekInfo,
|
||||
RingRTC,
|
||||
|
@ -38,7 +37,6 @@ import {
|
|||
GroupCallPeekInfoType,
|
||||
} from '../state/ducks/calling';
|
||||
import { getConversationCallMode } from '../state/ducks/conversations';
|
||||
import { EnvelopeClass } from '../textsecure.d';
|
||||
import {
|
||||
CallMode,
|
||||
AudioDevice,
|
||||
|
@ -57,12 +55,14 @@ import {
|
|||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import { assert } from '../util/assert';
|
||||
import { dropNull, shallowDropNull } from '../util/dropNull';
|
||||
import { getOwn } from '../util/getOwn';
|
||||
import {
|
||||
fetchMembershipProof,
|
||||
getMembershipList,
|
||||
wrapWithSyncMessageSend,
|
||||
} from '../groups';
|
||||
import { ProcessedEnvelope } from '../textsecure/Types.d';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp';
|
||||
import {
|
||||
|
@ -74,6 +74,9 @@ import { notify } from './notify';
|
|||
import { getSendOptions } from '../util/getSendOptions';
|
||||
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<
|
||||
HttpMethod,
|
||||
'GET' | 'PUT' | 'POST' | 'DELETE'
|
||||
|
@ -121,6 +124,135 @@ function translateSourceName(
|
|||
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 {
|
||||
readonly videoCapturer: GumVideoCapturer;
|
||||
|
||||
|
@ -1231,8 +1363,8 @@ export class CallingClass {
|
|||
}
|
||||
|
||||
async handleCallingMessage(
|
||||
envelope: EnvelopeClass,
|
||||
callingMessage: CallingMessage
|
||||
envelope: ProcessedEnvelope,
|
||||
callingMessage: Proto.ICallingMessage
|
||||
): Promise<void> {
|
||||
window.log.info('CallingClass.handleCallingMessage()');
|
||||
|
||||
|
@ -1298,9 +1430,10 @@ export class CallingClass {
|
|||
|
||||
await this.handleOutgoingSignaling(remoteUserId, message);
|
||||
|
||||
const ProtoOfferType = Proto.CallingMessage.Offer.Type;
|
||||
this.addCallHistoryForFailedIncomingCall(
|
||||
conversation,
|
||||
callingMessage.offer.type === OfferType.VideoCall,
|
||||
callingMessage.offer.type === ProtoOfferType.OFFER_VIDEO_CALL,
|
||||
envelope.timestamp
|
||||
);
|
||||
|
||||
|
@ -1321,7 +1454,7 @@ export class CallingClass {
|
|||
remoteDeviceId,
|
||||
this.localDeviceId,
|
||||
messageAgeSec,
|
||||
callingMessage,
|
||||
protoToCallingMessage(callingMessage),
|
||||
Buffer.from(senderIdentityKey),
|
||||
Buffer.from(receiverIdentityKey)
|
||||
);
|
||||
|
@ -1428,7 +1561,7 @@ export class CallingClass {
|
|||
try {
|
||||
await window.textsecure.messaging.sendCallingMessage(
|
||||
remoteUserId,
|
||||
message,
|
||||
callingMessageToProto(message),
|
||||
sendOptions
|
||||
);
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
waitThenRespondToGroupV2Migration,
|
||||
} from '../groups';
|
||||
import { assert } from '../util/assert';
|
||||
import { normalizeUuid } from '../util/normalizeUuid';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import {
|
||||
PhoneNumberSharingMode,
|
||||
|
@ -719,13 +720,18 @@ export async function mergeGroupV2Record(
|
|||
|
||||
export async function mergeContactRecord(
|
||||
storageID: string,
|
||||
contactRecord: ContactRecordClass
|
||||
originalContactRecord: ContactRecordClass
|
||||
): Promise<boolean> {
|
||||
window.normalizeUuids(
|
||||
contactRecord,
|
||||
['serviceUuid'],
|
||||
'storageService.mergeContactRecord'
|
||||
);
|
||||
const contactRecord = {
|
||||
...originalContactRecord,
|
||||
|
||||
serviceUuid: originalContactRecord.serviceUuid
|
||||
? normalizeUuid(
|
||||
originalContactRecord.serviceUuid,
|
||||
'ContactRecord.serviceUuid'
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const e164 = contactRecord.serviceE164 || undefined;
|
||||
const uuid = contactRecord.serviceUuid || undefined;
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
import type {
|
||||
ConversationAttributesType,
|
||||
ConversationModelCollectionType,
|
||||
MessageAttributesType,
|
||||
MessageModelCollectionType,
|
||||
} from '../model-types.d';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import { ConversationModel } from '../models/conversations';
|
||||
import { StoredJob } from '../jobs/types';
|
||||
import { ReactionType } from '../types/Reactions';
|
||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import type { StoredJob } from '../jobs/types';
|
||||
import type { ReactionType } from '../types/Reactions';
|
||||
import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
import { StorageAccessType } from '../types/Storage.d';
|
||||
import { AttachmentType } from '../types/Attachment';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
|
||||
export type AttachmentDownloadJobTypeType =
|
||||
| 'long-message'
|
||||
|
@ -111,41 +111,48 @@ export type SignedPreKeyType = {
|
|||
privateKey: ArrayBuffer;
|
||||
publicKey: ArrayBuffer;
|
||||
};
|
||||
export type StickerPackStatusType =
|
||||
| 'known'
|
||||
| 'ephemeral'
|
||||
| 'downloaded'
|
||||
| 'installed'
|
||||
| 'pending'
|
||||
| 'error';
|
||||
|
||||
export type StickerType = {
|
||||
export type StickerType = Readonly<{
|
||||
id: number;
|
||||
packId: string;
|
||||
|
||||
emoji: string | null;
|
||||
emoji?: string;
|
||||
isCoverOnly: boolean;
|
||||
lastUsed?: number;
|
||||
path: string;
|
||||
|
||||
width: 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;
|
||||
key: string;
|
||||
|
||||
attemptedStatus: 'downloaded' | 'installed' | 'ephemeral';
|
||||
attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
|
||||
author: string;
|
||||
coverStickerId: number;
|
||||
createdAt: number;
|
||||
downloadAttempts: number;
|
||||
installedAt: number | null;
|
||||
lastUsed: number;
|
||||
installedAt?: number;
|
||||
lastUsed?: number;
|
||||
status: StickerPackStatusType;
|
||||
stickerCount: number;
|
||||
stickers: ReadonlyArray<string>;
|
||||
stickers: Record<string, StickerType>;
|
||||
title: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type UnprocessedType = {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
|
|
|
@ -33,6 +33,7 @@ import { ReactionType } from '../types/Reactions';
|
|||
import { StoredJob } from '../jobs/types';
|
||||
import { assert } from '../util/assert';
|
||||
import { combineNames } from '../util/combineNames';
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { isNormalNumber } from '../util/isNormalNumber';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
|
@ -301,6 +302,7 @@ function rowToSticker(row: StickerRow): StickerType {
|
|||
return {
|
||||
...row,
|
||||
isCoverOnly: Boolean(row.isCoverOnly),
|
||||
emoji: dropNull(row.emoji),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4416,13 +4418,13 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
|||
)
|
||||
.all({ id });
|
||||
const payload = {
|
||||
attemptedStatus,
|
||||
attemptedStatus: attemptedStatus ?? null,
|
||||
author,
|
||||
coverStickerId,
|
||||
createdAt: createdAt || Date.now(),
|
||||
downloadAttempts: downloadAttempts || 1,
|
||||
id,
|
||||
installedAt,
|
||||
installedAt: installedAt ?? null,
|
||||
key,
|
||||
lastUsed: lastUsed || null,
|
||||
status,
|
||||
|
@ -4563,7 +4565,7 @@ async function createOrUpdateSticker(sticker: StickerType): Promise<void> {
|
|||
)
|
||||
`
|
||||
).run({
|
||||
emoji,
|
||||
emoji: emoji ?? null,
|
||||
height,
|
||||
id,
|
||||
isCoverOnly: isCoverOnly ? 1 : 0,
|
||||
|
|
|
@ -2,11 +2,17 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Dictionary, omit, reject } from 'lodash';
|
||||
import type {
|
||||
StickerPackStatusType,
|
||||
StickerType as StickerDBType,
|
||||
StickerPackType as StickerPackDBType,
|
||||
} from '../../sql/Interface';
|
||||
import dataInterface from '../../sql/Client';
|
||||
import {
|
||||
downloadStickerPack as externalDownloadStickerPack,
|
||||
maybeDeletePack,
|
||||
} from '../../../js/modules/stickers';
|
||||
RecentStickerType,
|
||||
} from '../../types/Stickers';
|
||||
import { sendStickerPackSync } from '../../shims/textsecure';
|
||||
import { trigger } from '../../shims/events';
|
||||
|
||||
|
@ -20,49 +26,6 @@ const {
|
|||
|
||||
// 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 = {
|
||||
readonly installedPack: string | null;
|
||||
readonly packs: Dictionary<StickerPackDBType>;
|
||||
|
@ -75,23 +38,23 @@ export type StickersStateType = {
|
|||
export type StickerType = {
|
||||
readonly id: number;
|
||||
readonly packId: string;
|
||||
readonly emoji: string | null;
|
||||
readonly emoji?: string;
|
||||
readonly url: string;
|
||||
};
|
||||
|
||||
export type StickerPackType = {
|
||||
readonly id: string;
|
||||
readonly key: string;
|
||||
readonly title: string;
|
||||
readonly author: string;
|
||||
readonly isBlessed: boolean;
|
||||
readonly cover?: StickerType;
|
||||
readonly lastUsed: number;
|
||||
readonly attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
|
||||
readonly status: StickerPackStatus;
|
||||
readonly stickers: Array<StickerType>;
|
||||
readonly stickerCount: number;
|
||||
};
|
||||
export type StickerPackType = Readonly<{
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
author: string;
|
||||
isBlessed: boolean;
|
||||
cover?: StickerType;
|
||||
lastUsed?: number;
|
||||
attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
|
||||
status: StickerPackStatusType;
|
||||
stickers: Array<StickerType>;
|
||||
stickerCount: number;
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
|
||||
|
@ -128,7 +91,7 @@ type UninstallStickerPackPayloadType = {
|
|||
packId: string;
|
||||
fromSync: boolean;
|
||||
status: 'downloaded';
|
||||
installedAt: null;
|
||||
installedAt?: undefined;
|
||||
recentStickers: Array<RecentStickerType>;
|
||||
};
|
||||
type UninstallStickerPackAction = {
|
||||
|
@ -306,7 +269,7 @@ async function doUninstallStickerPack(
|
|||
packId,
|
||||
fromSync,
|
||||
status,
|
||||
installedAt: null,
|
||||
installedAt: undefined,
|
||||
recentStickers: recentStickers.map(item => ({
|
||||
packId: item.packId,
|
||||
stickerId: item.id,
|
||||
|
|
|
@ -32,6 +32,7 @@ import { BodyRangesType } from '../../types/Util';
|
|||
import { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||
import { ConversationColors } from '../../types/Colors';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
import { AttachmentType, isVoiceMessage } from '../../types/Attachment';
|
||||
|
||||
import { CallingNotificationType } from '../../util/callingNotification';
|
||||
|
@ -430,8 +431,7 @@ function getPropsForUnsupportedMessage(
|
|||
ourNumber: string | undefined,
|
||||
ourUuid: string | undefined
|
||||
): PropsForUnsupportedMessage {
|
||||
const CURRENT_PROTOCOL_VERSION =
|
||||
window.textsecure.protobuf.DataMessage.ProtocolVersion.CURRENT;
|
||||
const CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
|
||||
|
||||
const requiredVersion = message.requiredProtocolVersion;
|
||||
const canProcessNow = Boolean(
|
||||
|
@ -463,9 +463,6 @@ function getPropsForGroupV2Change(
|
|||
conversationSelector: GetConversationByIdType,
|
||||
ourConversationId: string
|
||||
): GroupsV2Props {
|
||||
const AccessControlEnum =
|
||||
window.textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const RoleEnum = window.textsecure.protobuf.Member.Role;
|
||||
const change = message.groupV2Change;
|
||||
|
||||
if (!change) {
|
||||
|
@ -476,8 +473,6 @@ function getPropsForGroupV2Change(
|
|||
|
||||
return {
|
||||
groupName: conversation?.type === 'group' ? conversation?.name : undefined,
|
||||
AccessControlEnum,
|
||||
RoleEnum,
|
||||
ourConversationId,
|
||||
change,
|
||||
};
|
||||
|
@ -547,8 +542,7 @@ export function isMessageHistoryUnsynced(
|
|||
export function isExpirationTimerUpdate(
|
||||
message: Pick<MessageAttributesType, 'flags'>
|
||||
): boolean {
|
||||
const flag =
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
const flag = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return Boolean(message.flags && message.flags & flag);
|
||||
}
|
||||
|
@ -734,7 +728,7 @@ function getPropsForGroupNotification(
|
|||
export function isEndSession(
|
||||
message: Pick<MessageAttributesType, 'flags'>
|
||||
): boolean {
|
||||
const flag = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
const flag = Proto.DataMessage.Flags.END_SESSION;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return Boolean(message.flags && message.flags & flag);
|
||||
}
|
||||
|
|
|
@ -14,13 +14,15 @@ import {
|
|||
} from 'lodash';
|
||||
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 {
|
||||
RecentStickerType,
|
||||
StickerDBType,
|
||||
StickerPackDBType,
|
||||
StickerPackType,
|
||||
StickersStateType,
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
} from '../ducks/stickers';
|
||||
import { getStickersPath, getTempPath } from './user';
|
||||
|
@ -95,7 +97,7 @@ export const translatePackFromDB = (
|
|||
const filterAndTransformPacks = (
|
||||
packs: Dictionary<StickerPackDBType>,
|
||||
packFilter: (sticker: StickerPackDBType) => boolean,
|
||||
packSort: (sticker: StickerPackDBType) => number | null,
|
||||
packSort: (sticker: StickerPackDBType) => number | undefined,
|
||||
blessedPacks: Dictionary<boolean>,
|
||||
stickersPath: string,
|
||||
tempPath: string
|
||||
|
|
|
@ -10,10 +10,8 @@ import {
|
|||
} from '../../components/conversation/conversation-details/GroupLinkManagement';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { AccessControlClass } from '../../textsecure.d';
|
||||
|
||||
export type SmartGroupLinkManagementProps = {
|
||||
accessEnum: typeof AccessControlClass.AccessRequired;
|
||||
changeHasGroupLink: (value: boolean) => void;
|
||||
conversationId: string;
|
||||
copyGroupLink: (groupLink: string) => void;
|
||||
|
|
|
@ -10,10 +10,8 @@ import {
|
|||
} from '../../components/conversation/conversation-details/GroupV2Permissions';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { AccessControlClass } from '../../textsecure.d';
|
||||
|
||||
export type SmartGroupV2PermissionsProps = {
|
||||
accessEnum: typeof AccessControlClass.AccessRequired;
|
||||
conversationId: string;
|
||||
setAccessControlAttributesSetting: (value: number) => void;
|
||||
setAccessControlMembersSetting: (value: number) => void;
|
||||
|
|
126
ts/test-both/ContactsParser_test.ts
Normal file
126
ts/test-both/ContactsParser_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
333
ts/test-both/processDataMessage_test.ts
Normal file
333
ts/test-both/processDataMessage_test.ts
Normal 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);
|
||||
});
|
||||
});
|
37
ts/test-both/processSyncMessage_test.ts
Normal file
37
ts/test-both/processSyncMessage_test.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,7 +13,7 @@ describe('both/state/ducks/composer', () => {
|
|||
conversationId: '123',
|
||||
quote: {
|
||||
attachments: [],
|
||||
id: '456',
|
||||
id: 456,
|
||||
isViewOnce: false,
|
||||
messageId: '789',
|
||||
referencedMessageNotFound: false,
|
||||
|
@ -114,7 +114,7 @@ describe('both/state/ducks/composer', () => {
|
|||
const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE));
|
||||
|
||||
assert.equal(nextState.quotedMessage?.conversationId, '123');
|
||||
assert.equal(nextState.quotedMessage?.quote?.id, '456');
|
||||
assert.equal(nextState.quotedMessage?.quote?.id, 456);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
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', () => {
|
||||
it('swaps null with undefined', () => {
|
||||
|
@ -16,4 +21,42 @@ describe('dropNull', () => {
|
|||
it('non-null values undefined be', () => {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
76
ts/test-electron/MessageReceiver_test.ts
Normal file
76
ts/test-electron/MessageReceiver_test.ts
Normal 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();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@ import { assert } from 'chai';
|
|||
import * as sinon from 'sinon';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
|
||||
describe('Message', () => {
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
@ -384,8 +385,7 @@ describe('Message', () => {
|
|||
title: 'voice message',
|
||||
attachment: {
|
||||
contentType: 'audio/ogg',
|
||||
flags:
|
||||
window.textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
},
|
||||
expectedText: 'Voice Message',
|
||||
expectedEmoji: '🎤',
|
||||
|
|
212
ts/textsecure.d.ts
vendored
212
ts/textsecure.d.ts
vendored
|
@ -18,6 +18,7 @@ import { Storage } from './textsecure/Storage';
|
|||
import {
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
ProcessedAttachment,
|
||||
} from './textsecure/Types.d';
|
||||
|
||||
export type UnprocessedType = {
|
||||
|
@ -31,6 +32,7 @@ export type UnprocessedType = {
|
|||
source?: string;
|
||||
sourceDevice?: number;
|
||||
sourceUuid?: string;
|
||||
messageAgeSec?: number;
|
||||
version: number;
|
||||
};
|
||||
|
||||
|
@ -71,7 +73,6 @@ type DeviceNameProtobufTypes = {
|
|||
|
||||
type GroupsProtobufTypes = {
|
||||
AvatarUploadAttributes: typeof AvatarUploadAttributesClass;
|
||||
Member: typeof MemberClass;
|
||||
MemberPendingProfileKey: typeof MemberPendingProfileKeyClass;
|
||||
MemberPendingAdminApproval: typeof MemberPendingAdminApprovalClass;
|
||||
AccessControl: typeof AccessControlClass;
|
||||
|
@ -86,9 +87,6 @@ type GroupsProtobufTypes = {
|
|||
|
||||
type SignalServiceProtobufTypes = {
|
||||
AttachmentPointer: typeof AttachmentPointerClass;
|
||||
ContactDetails: typeof ContactDetailsClass;
|
||||
Content: typeof ContentClass;
|
||||
DataMessage: typeof DataMessageClass;
|
||||
Envelope: typeof EnvelopeClass;
|
||||
GroupContext: typeof GroupContextClass;
|
||||
GroupContextV2: typeof GroupContextV2Class;
|
||||
|
@ -159,39 +157,14 @@ export declare class AvatarUploadAttributesClass {
|
|||
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;
|
||||
|
||||
// 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 {
|
||||
static decode: (
|
||||
data: ArrayBuffer | ByteBufferClass,
|
||||
encoding?: string
|
||||
) => MemberPendingProfileKeyClass;
|
||||
|
||||
member?: MemberClass;
|
||||
addedByUserId?: ProtoBinaryType;
|
||||
timestamp?: ProtoBigNumberType;
|
||||
}
|
||||
|
@ -245,7 +218,6 @@ export declare class GroupClass {
|
|||
disappearingMessagesTimer?: ProtoBinaryType;
|
||||
accessControl?: AccessControlClass;
|
||||
version?: number;
|
||||
members?: Array<MemberClass>;
|
||||
membersPendingProfileKey?: Array<MemberPendingProfileKeyClass>;
|
||||
membersPendingAdminApproval?: Array<MemberPendingAdminApprovalClass>;
|
||||
inviteLinkPassword?: ProtoBinaryType;
|
||||
|
@ -299,7 +271,6 @@ export declare namespace GroupChangeClass {
|
|||
// Note: we need to use namespaces to express nested classes in Typescript
|
||||
export declare namespace GroupChangeClass.Actions {
|
||||
class AddMemberAction {
|
||||
added?: MemberClass;
|
||||
joinFromInviteLink?: boolean;
|
||||
}
|
||||
|
||||
|
@ -480,7 +451,7 @@ export declare class AttachmentPointerClass {
|
|||
GIF: number;
|
||||
};
|
||||
|
||||
cdnId?: ProtoBigNumberType;
|
||||
cdnId?: string;
|
||||
cdnKey?: string;
|
||||
contentType?: string;
|
||||
key?: ProtoBinaryType;
|
||||
|
@ -493,184 +464,16 @@ export declare class AttachmentPointerClass {
|
|||
height?: number;
|
||||
caption?: string;
|
||||
blurHash?: string;
|
||||
uploadTimestamp?: ProtoBigNumberType;
|
||||
cdnNumber?: number;
|
||||
}
|
||||
|
||||
export type DownloadAttachmentType = {
|
||||
export type DownloadAttachmentType = Omit<
|
||||
ProcessedAttachment,
|
||||
'digest' | 'key'
|
||||
> & {
|
||||
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 {
|
||||
static decode: (
|
||||
data: ArrayBuffer | ByteBufferClass,
|
||||
|
@ -1140,7 +943,6 @@ export declare namespace SyncMessageClass {
|
|||
destination?: string;
|
||||
destinationUuid?: string;
|
||||
timestamp?: ProtoBigNumberType;
|
||||
message?: DataMessageClass;
|
||||
expirationStartTimestamp?: ProtoBigNumberType;
|
||||
unidentifiedStatus?: Array<SyncMessageClass.Sent.UnidentifiedDeliveryStatus>;
|
||||
isRecipientUpdate?: boolean;
|
||||
|
|
|
@ -1,101 +1,160 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
import { AttachmentType } from './SendMessage';
|
||||
import { Reader } from 'protobufjs';
|
||||
|
||||
type ProtobufConstructorType = {
|
||||
decode: (data: ArrayBuffer) => ProtobufType;
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
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 = {
|
||||
avatar?: PackedAttachmentType;
|
||||
profileKey?: any;
|
||||
uuid?: string;
|
||||
members: Array<string>;
|
||||
export type MessageWithAvatar<Message extends OptionalAvatar> = Omit<
|
||||
Message,
|
||||
'avatar'
|
||||
> & {
|
||||
avatar?: (Avatar & { data: ArrayBuffer }) | null;
|
||||
};
|
||||
|
||||
export type PackedAttachmentType = AttachmentType & {
|
||||
length: number;
|
||||
};
|
||||
export type ModifiedGroupDetails = MessageWithAvatar<Proto.GroupDetails>;
|
||||
|
||||
export class ProtoParser {
|
||||
buffer: ByteBufferClass;
|
||||
export type ModifiedContactDetails = MessageWithAvatar<Proto.ContactDetails>;
|
||||
|
||||
protobuf: ProtobufConstructorType;
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
constructor(arrayBuffer: ArrayBuffer, protobuf: ProtobufConstructorType) {
|
||||
this.protobuf = protobuf;
|
||||
this.buffer = new window.dcodeIO.ByteBuffer();
|
||||
this.buffer.append(arrayBuffer);
|
||||
this.buffer.offset = 0;
|
||||
this.buffer.limit = arrayBuffer.byteLength;
|
||||
class ParserBase<
|
||||
Message extends OptionalAvatar,
|
||||
Decoder extends DecoderBase<Message>
|
||||
> {
|
||||
protected readonly reader: Reader;
|
||||
|
||||
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 {
|
||||
if (this.buffer.limit === this.buffer.offset) {
|
||||
return undefined; // eof
|
||||
}
|
||||
const len = this.buffer.readVarint32();
|
||||
const nextBuffer = this.buffer
|
||||
.slice(this.buffer.offset, this.buffer.offset + len)
|
||||
.toArrayBuffer();
|
||||
const proto = this.decoder.decodeDelimited(this.reader);
|
||||
|
||||
const proto = this.protobuf.decode(nextBuffer);
|
||||
this.buffer.skip(len);
|
||||
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (proto.profileKey) {
|
||||
proto.profileKey = proto.profileKey.toArrayBuffer();
|
||||
if (!proto.avatar) {
|
||||
return {
|
||||
...proto,
|
||||
avatar: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (proto.uuid) {
|
||||
window.normalizeUuids(
|
||||
proto,
|
||||
['uuid'],
|
||||
'ProtoParser::next (proto.uuid)'
|
||||
);
|
||||
}
|
||||
const attachmentLen = proto.avatar.length ?? 0;
|
||||
const avatarData = this.reader.buf.slice(
|
||||
this.reader.pos,
|
||||
this.reader.pos + attachmentLen
|
||||
);
|
||||
this.reader.skip(attachmentLen);
|
||||
|
||||
if (proto.members) {
|
||||
window.normalizeUuids(
|
||||
proto,
|
||||
proto.members.map((_member, i) => `members.${i}.uuid`),
|
||||
'ProtoParser::next (proto.members)'
|
||||
);
|
||||
}
|
||||
return {
|
||||
...proto,
|
||||
|
||||
return proto;
|
||||
avatar: {
|
||||
...proto.avatar,
|
||||
|
||||
data: typedArrayToArrayBuffer(avatarData),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'ProtoParser.next 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) {
|
||||
super(arrayBuffer, window.textsecure.protobuf.GroupDetails as any);
|
||||
super(arrayBuffer, Proto.ContactDetails);
|
||||
}
|
||||
}
|
||||
|
||||
export class ContactBuffer extends ProtoParser {
|
||||
constructor(arrayBuffer: ArrayBuffer) {
|
||||
super(arrayBuffer, window.textsecure.protobuf.ContactDetails as any);
|
||||
public next(): ModifiedContactDetails | undefined {
|
||||
const proto = this.decodeDelimited();
|
||||
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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ const Crypto = {
|
|||
async decryptAttachment(
|
||||
encryptedBin: ArrayBuffer,
|
||||
keys: ArrayBuffer,
|
||||
theirDigest: ArrayBuffer
|
||||
theirDigest?: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
if (keys.byteLength !== 64) {
|
||||
throw new Error('Got invalid length attachment keys');
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
|
||||
*/
|
||||
|
||||
export type EventHandler = (event: any) => unknown;
|
||||
|
||||
export default class EventTarget {
|
||||
listeners?: { [type: string]: Array<Function> };
|
||||
listeners?: { [type: string]: Array<EventHandler> };
|
||||
|
||||
dispatchEvent(ev: Event): Array<unknown> {
|
||||
if (!(ev instanceof Event)) {
|
||||
|
@ -36,7 +38,7 @@ export default class EventTarget {
|
|||
return results;
|
||||
}
|
||||
|
||||
addEventListener(eventName: string, callback: Function): void {
|
||||
addEventListener(eventName: string, callback: EventHandler): void {
|
||||
if (typeof eventName !== 'string') {
|
||||
throw new Error('First argument expects a string');
|
||||
}
|
||||
|
@ -54,7 +56,7 @@ export default class EventTarget {
|
|||
this.listeners[eventName] = listeners;
|
||||
}
|
||||
|
||||
removeEventListener(eventName: string, callback: Function): void {
|
||||
removeEventListener(eventName: string, callback: EventHandler): void {
|
||||
if (typeof eventName !== 'string') {
|
||||
throw new Error('First argument expects a string');
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,7 +23,6 @@ import {
|
|||
} from '@signalapp/signal-client';
|
||||
|
||||
import { WebAPIType } from './WebAPI';
|
||||
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
||||
import {
|
||||
CallbackResultType,
|
||||
SendMetadataType,
|
||||
|
@ -42,6 +41,7 @@ import { Sessions, IdentityKeys } from '../LibSignalStores';
|
|||
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
|
||||
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
||||
import { getKeysForIdentifier } from './getKeysForIdentifier';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
export const enum SenderCertificateMode {
|
||||
WithE164,
|
||||
|
@ -72,13 +72,13 @@ type OutgoingMessageOptionsType = SendOptionsType & {
|
|||
|
||||
function ciphertextMessageTypeToEnvelopeType(type: number) {
|
||||
if (type === CiphertextMessageType.PreKey) {
|
||||
return window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE;
|
||||
return Proto.Envelope.Type.PREKEY_BUNDLE;
|
||||
}
|
||||
if (type === CiphertextMessageType.Whisper) {
|
||||
return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT;
|
||||
return Proto.Envelope.Type.CIPHERTEXT;
|
||||
}
|
||||
if (type === CiphertextMessageType.Plaintext) {
|
||||
return window.textsecure.protobuf.Envelope.Type.PLAINTEXT_CONTENT;
|
||||
return Proto.Envelope.Type.PLAINTEXT_CONTENT;
|
||||
}
|
||||
throw new Error(
|
||||
`ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}`
|
||||
|
@ -96,11 +96,11 @@ function getPaddedMessageLength(messageLength: number): number {
|
|||
return messagePartCount * 160;
|
||||
}
|
||||
|
||||
export function padMessage(messageBuffer: ArrayBuffer): Uint8Array {
|
||||
export function padMessage(messageBuffer: Uint8Array): Uint8Array {
|
||||
const plaintext = new Uint8Array(
|
||||
getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
||||
);
|
||||
plaintext.set(new Uint8Array(messageBuffer));
|
||||
plaintext.set(messageBuffer);
|
||||
plaintext[messageBuffer.byteLength] = 0x80;
|
||||
|
||||
return plaintext;
|
||||
|
@ -113,7 +113,7 @@ export default class OutgoingMessage {
|
|||
|
||||
identifiers: Array<string>;
|
||||
|
||||
message: ContentClass | PlaintextContent;
|
||||
message: Proto.Content | PlaintextContent;
|
||||
|
||||
callback: (result: CallbackResultType) => void;
|
||||
|
||||
|
@ -141,14 +141,14 @@ export default class OutgoingMessage {
|
|||
server: WebAPIType,
|
||||
timestamp: number,
|
||||
identifiers: Array<string>,
|
||||
message: ContentClass | DataMessageClass | PlaintextContent,
|
||||
message: Proto.Content | Proto.DataMessage | PlaintextContent,
|
||||
contentHint: number,
|
||||
groupId: string | undefined,
|
||||
callback: (result: CallbackResultType) => void,
|
||||
options: OutgoingMessageOptionsType = {}
|
||||
) {
|
||||
if (message instanceof window.textsecure.protobuf.DataMessage) {
|
||||
const content = new window.textsecure.protobuf.Content();
|
||||
if (message instanceof Proto.DataMessage) {
|
||||
const content = new Proto.Content();
|
||||
content.dataMessage = message;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
this.message = content;
|
||||
|
@ -304,8 +304,8 @@ export default class OutgoingMessage {
|
|||
if (!this.plaintext) {
|
||||
const { message } = this;
|
||||
|
||||
if (message instanceof window.textsecure.protobuf.Content) {
|
||||
this.plaintext = padMessage(message.toArrayBuffer());
|
||||
if (message instanceof Proto.Content) {
|
||||
this.plaintext = padMessage(Proto.Content.encode(message).finish());
|
||||
} else {
|
||||
this.plaintext = message.serialize();
|
||||
}
|
||||
|
@ -324,7 +324,7 @@ export default class OutgoingMessage {
|
|||
}): Promise<CiphertextMessage> {
|
||||
const { message } = this;
|
||||
|
||||
if (message instanceof window.textsecure.protobuf.Content) {
|
||||
if (message instanceof Proto.Content) {
|
||||
return signalEncrypt(
|
||||
Buffer.from(this.getPlaintext()),
|
||||
protocolAddress,
|
||||
|
@ -421,8 +421,7 @@ export default class OutgoingMessage {
|
|||
);
|
||||
|
||||
return {
|
||||
type:
|
||||
window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
type: Proto.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
destinationDeviceId,
|
||||
destinationRegistrationId,
|
||||
content: buffer.toString('base64'),
|
||||
|
|
|
@ -14,7 +14,8 @@ import {
|
|||
} from '../Crypto';
|
||||
import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve';
|
||||
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
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
@ -35,7 +36,7 @@ class ProvisioningCipherInner {
|
|||
async decrypt(
|
||||
provisionEnvelope: Proto.ProvisionEnvelope
|
||||
): Promise<ProvisionDecryptResult> {
|
||||
assert(
|
||||
strictAssert(
|
||||
provisionEnvelope.publicKey && provisionEnvelope.body,
|
||||
'Missing required fields in ProvisionEnvelope'
|
||||
);
|
||||
|
@ -79,19 +80,17 @@ class ProvisioningCipherInner {
|
|||
new FIXMEU8(plaintext)
|
||||
);
|
||||
const privKey = provisionMessage.identityKeyPrivate;
|
||||
assert(privKey, 'Missing identityKeyPrivate in ProvisionMessage');
|
||||
strictAssert(privKey, 'Missing identityKeyPrivate in ProvisionMessage');
|
||||
|
||||
const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey));
|
||||
window.normalizeUuids(
|
||||
provisionMessage,
|
||||
['uuid'],
|
||||
'ProvisioningCipher.decrypt'
|
||||
);
|
||||
|
||||
const { uuid } = provisionMessage;
|
||||
strictAssert(uuid, 'Missing uuid in provisioning message');
|
||||
|
||||
const ret: ProvisionDecryptResult = {
|
||||
identityKeyPair: keyPair,
|
||||
number: provisionMessage.number,
|
||||
uuid: provisionMessage.uuid,
|
||||
uuid: normalizeUuid(uuid, 'ProvisionMessage.uuid'),
|
||||
provisioningCode: provisionMessage.provisioningCode,
|
||||
userAgent: provisionMessage.userAgent,
|
||||
readReceipts: provisionMessage.readReceipts,
|
||||
|
|
|
@ -30,22 +30,16 @@ import {
|
|||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage';
|
||||
import Crypto from './Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
concatenateBytes,
|
||||
getRandomBytes,
|
||||
getZeroes,
|
||||
hexToArrayBuffer,
|
||||
typedArrayToArrayBuffer,
|
||||
} from '../Crypto';
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
CallingMessageClass,
|
||||
ContentClass,
|
||||
DataMessageClass,
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
SyncMessageClass,
|
||||
} from '../textsecure.d';
|
||||
import { MessageError, SignedPreKeyRotationError } from './Errors';
|
||||
import { BodyRangesType } from '../types/Util';
|
||||
|
@ -56,18 +50,6 @@ import {
|
|||
import { concat } from '../util/iterables';
|
||||
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 = {
|
||||
[identifier: string]: {
|
||||
accessKey: string;
|
||||
|
@ -101,7 +83,7 @@ type PreviewType = {
|
|||
|
||||
type QuoteAttachmentType = {
|
||||
thumbnail?: AttachmentType;
|
||||
attachmentPointer?: AttachmentPointerClass;
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
};
|
||||
|
||||
export type GroupV2InfoType = {
|
||||
|
@ -130,7 +112,7 @@ export type AttachmentType = {
|
|||
height: number;
|
||||
caption: string;
|
||||
|
||||
attachmentPointer?: AttachmentPointerClass;
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
|
||||
blurHash?: string;
|
||||
};
|
||||
|
@ -174,6 +156,9 @@ export type GroupSendOptionsType = {
|
|||
groupCallUpdate?: GroupCallUpdateType;
|
||||
};
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
class Message {
|
||||
attachments: Array<any>;
|
||||
|
||||
|
@ -219,7 +204,7 @@ class Message {
|
|||
|
||||
dataMessage: any;
|
||||
|
||||
attachmentPointers?: Array<any>;
|
||||
attachmentPointers: Array<Proto.IAttachmentPointer> = [];
|
||||
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
|
||||
|
@ -301,17 +286,14 @@ class Message {
|
|||
}
|
||||
|
||||
isEndSession() {
|
||||
return (
|
||||
(this.flags || 0) &
|
||||
window.textsecure.protobuf.DataMessage.Flags.END_SESSION
|
||||
);
|
||||
return (this.flags || 0) & Proto.DataMessage.Flags.END_SESSION;
|
||||
}
|
||||
|
||||
toProto(): DataMessageClass {
|
||||
if (this.dataMessage instanceof window.textsecure.protobuf.DataMessage) {
|
||||
toProto(): Proto.DataMessage {
|
||||
if (this.dataMessage instanceof Proto.DataMessage) {
|
||||
return this.dataMessage;
|
||||
}
|
||||
const proto = new window.textsecure.protobuf.DataMessage();
|
||||
const proto = new Proto.DataMessage();
|
||||
|
||||
proto.timestamp = this.timestamp;
|
||||
proto.attachments = this.attachmentPointers;
|
||||
|
@ -330,19 +312,19 @@ class Message {
|
|||
proto.flags = this.flags;
|
||||
}
|
||||
if (this.groupV2) {
|
||||
proto.groupV2 = new window.textsecure.protobuf.GroupContextV2();
|
||||
proto.groupV2 = new Proto.GroupContextV2();
|
||||
proto.groupV2.masterKey = this.groupV2.masterKey;
|
||||
proto.groupV2.revision = this.groupV2.revision;
|
||||
proto.groupV2.groupChange = this.groupV2.groupChange || null;
|
||||
} else if (this.group) {
|
||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(this.group.id);
|
||||
proto.group = new Proto.GroupContext();
|
||||
proto.group.id = Bytes.fromString(this.group.id);
|
||||
proto.group.type = this.group.type;
|
||||
}
|
||||
if (this.sticker) {
|
||||
proto.sticker = new window.textsecure.protobuf.DataMessage.Sticker();
|
||||
proto.sticker.packId = hexToArrayBuffer(this.sticker.packId);
|
||||
proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey);
|
||||
proto.sticker = new Proto.DataMessage.Sticker();
|
||||
proto.sticker.packId = Bytes.fromHex(this.sticker.packId);
|
||||
proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey);
|
||||
proto.sticker.stickerId = this.sticker.stickerId;
|
||||
|
||||
if (this.sticker.attachmentPointer) {
|
||||
|
@ -350,7 +332,7 @@ class Message {
|
|||
}
|
||||
}
|
||||
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.remove = this.reaction.remove || false;
|
||||
proto.reaction.targetAuthorUuid = this.reaction.targetAuthorUuid || null;
|
||||
|
@ -359,7 +341,7 @@ class Message {
|
|||
|
||||
if (Array.isArray(this.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.url = preview.url;
|
||||
item.description = preview.description || null;
|
||||
|
@ -369,8 +351,8 @@ class Message {
|
|||
});
|
||||
}
|
||||
if (this.quote) {
|
||||
const { QuotedAttachment } = window.textsecure.protobuf.DataMessage.Quote;
|
||||
const { BodyRange, Quote } = window.textsecure.protobuf.DataMessage;
|
||||
const { QuotedAttachment } = Proto.DataMessage.Quote;
|
||||
const { BodyRange, Quote } = Proto.DataMessage;
|
||||
|
||||
proto.quote = new Quote();
|
||||
const { quote } = proto;
|
||||
|
@ -396,24 +378,26 @@ class Message {
|
|||
const bodyRange = new BodyRange();
|
||||
bodyRange.start = range.start;
|
||||
bodyRange.length = range.length;
|
||||
bodyRange.mentionUuid = range.mentionUuid;
|
||||
if (range.mentionUuid !== undefined) {
|
||||
bodyRange.mentionUuid = range.mentionUuid;
|
||||
}
|
||||
return bodyRange;
|
||||
});
|
||||
if (
|
||||
quote.bodyRanges.length &&
|
||||
(!proto.requiredProtocolVersion ||
|
||||
proto.requiredProtocolVersion <
|
||||
window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS)
|
||||
Proto.DataMessage.ProtocolVersion.MENTIONS)
|
||||
) {
|
||||
proto.requiredProtocolVersion =
|
||||
window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS;
|
||||
Proto.DataMessage.ProtocolVersion.MENTIONS;
|
||||
}
|
||||
}
|
||||
if (this.expireTimer) {
|
||||
proto.expireTimer = this.expireTimer;
|
||||
}
|
||||
if (this.profileKey) {
|
||||
proto.profileKey = this.profileKey;
|
||||
proto.profileKey = new FIXMEU8(this.profileKey);
|
||||
}
|
||||
if (this.deletedForEveryoneTimestamp) {
|
||||
proto.delete = {
|
||||
|
@ -422,7 +406,7 @@ class Message {
|
|||
}
|
||||
if (this.mentions) {
|
||||
proto.requiredProtocolVersion =
|
||||
window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS;
|
||||
Proto.DataMessage.ProtocolVersion.MENTIONS;
|
||||
proto.bodyRanges = this.mentions.map(
|
||||
({ start, length, mentionUuid }) => ({
|
||||
start,
|
||||
|
@ -433,7 +417,7 @@ class Message {
|
|||
}
|
||||
|
||||
if (this.groupCallUpdate) {
|
||||
const { GroupCallUpdate } = window.textsecure.protobuf.DataMessage;
|
||||
const { GroupCallUpdate } = Proto.DataMessage;
|
||||
|
||||
const groupCallUpdate = new GroupCallUpdate();
|
||||
groupCallUpdate.eraId = this.groupCallUpdate.eraId;
|
||||
|
@ -446,7 +430,9 @@ class Message {
|
|||
}
|
||||
|
||||
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
|
||||
const buffer = getRandomBytes(2);
|
||||
const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1;
|
||||
|
||||
// Generate a random padding buffer of the chosen size
|
||||
return getRandomBytes(paddingLength);
|
||||
return new FIXMEU8(getRandomBytes(paddingLength));
|
||||
}
|
||||
|
||||
getPaddedAttachment(data: ArrayBuffer): ArrayBuffer {
|
||||
|
@ -511,10 +497,11 @@ export default class MessageSender {
|
|||
|
||||
async makeAttachmentPointer(
|
||||
attachment: AttachmentType
|
||||
): Promise<AttachmentPointerClass | undefined> {
|
||||
if (typeof attachment !== 'object' || attachment == null) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
): Promise<Proto.IAttachmentPointer> {
|
||||
assert(
|
||||
typeof attachment === 'object' && attachment !== null,
|
||||
'Got null attachment in `makeAttachmentPointer`'
|
||||
);
|
||||
|
||||
const { data, size } = attachment;
|
||||
if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
|
||||
|
@ -535,12 +522,12 @@ export default class MessageSender {
|
|||
const result = await Crypto.encryptAttachment(padded, key, iv);
|
||||
const id = await this.server.putAttachment(result.ciphertext);
|
||||
|
||||
const proto = new window.textsecure.protobuf.AttachmentPointer();
|
||||
const proto = new Proto.AttachmentPointer();
|
||||
proto.cdnId = id;
|
||||
proto.contentType = attachment.contentType;
|
||||
proto.key = key;
|
||||
proto.key = new FIXMEU8(key);
|
||||
proto.size = attachment.size;
|
||||
proto.digest = result.digest;
|
||||
proto.digest = new FIXMEU8(result.digest);
|
||||
|
||||
if (attachment.fileName) {
|
||||
proto.fileName = attachment.fileName;
|
||||
|
@ -657,11 +644,11 @@ export default class MessageSender {
|
|||
return message.toArrayBuffer();
|
||||
}
|
||||
|
||||
async getContentMessage(options: MessageOptionsType): Promise<ContentClass> {
|
||||
async getContentMessage(options: MessageOptionsType): Promise<Proto.Content> {
|
||||
const message = await this.getHydratedMessage(options);
|
||||
const dataMessage = message.toProto();
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.dataMessage = dataMessage;
|
||||
|
||||
return contentMessage;
|
||||
|
@ -685,8 +672,8 @@ export default class MessageSender {
|
|||
groupMembers: Array<string>;
|
||||
isTyping: boolean;
|
||||
timestamp?: number;
|
||||
}): ContentClass {
|
||||
const ACTION_ENUM = window.textsecure.protobuf.TypingMessage.Action;
|
||||
}): Proto.Content {
|
||||
const ACTION_ENUM = Proto.TypingMessage.Action;
|
||||
const { recipientId, groupId, isTyping, timestamp } = options;
|
||||
|
||||
if (!recipientId && !groupId) {
|
||||
|
@ -698,12 +685,14 @@ export default class MessageSender {
|
|||
const finalTimestamp = timestamp || Date.now();
|
||||
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
|
||||
|
||||
const typingMessage = new window.textsecure.protobuf.TypingMessage();
|
||||
typingMessage.groupId = groupId || null;
|
||||
const typingMessage = new Proto.TypingMessage();
|
||||
if (groupId) {
|
||||
typingMessage.groupId = new FIXMEU8(groupId);
|
||||
}
|
||||
typingMessage.action = action;
|
||||
typingMessage.timestamp = finalTimestamp;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.typingMessage = typingMessage;
|
||||
|
||||
return contentMessage;
|
||||
|
@ -767,7 +756,7 @@ export default class MessageSender {
|
|||
group: groupV1
|
||||
? {
|
||||
id: groupV1.id,
|
||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
type: Proto.GroupContext.Type.DELIVER,
|
||||
}
|
||||
: undefined,
|
||||
mentions,
|
||||
|
@ -781,8 +770,8 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
createSyncMessage(): SyncMessageClass {
|
||||
const syncMessage = new window.textsecure.protobuf.SyncMessage();
|
||||
createSyncMessage(): Proto.SyncMessage {
|
||||
const syncMessage = new Proto.SyncMessage();
|
||||
|
||||
syncMessage.padding = this.getRandomPadding();
|
||||
|
||||
|
@ -843,7 +832,7 @@ export default class MessageSender {
|
|||
}: {
|
||||
timestamp: number;
|
||||
recipients: Array<string>;
|
||||
proto: ContentClass | DataMessageClass | PlaintextContent;
|
||||
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
|
||||
contentHint: number;
|
||||
groupId: string | undefined;
|
||||
callback: (result: CallbackResultType) => void;
|
||||
|
@ -885,7 +874,7 @@ export default class MessageSender {
|
|||
}: {
|
||||
timestamp: number;
|
||||
recipients: Array<string>;
|
||||
proto: ContentClass | DataMessageClass | PlaintextContent;
|
||||
proto: Proto.Content | Proto.DataMessage | PlaintextContent;
|
||||
contentHint: number;
|
||||
groupId: string | undefined;
|
||||
options?: SendOptionsType;
|
||||
|
@ -920,7 +909,7 @@ export default class MessageSender {
|
|||
options,
|
||||
}: {
|
||||
identifier: string | undefined;
|
||||
proto: DataMessageClass | ContentClass | PlaintextContent;
|
||||
proto: Proto.DataMessage | Proto.Content | PlaintextContent;
|
||||
timestamp: number;
|
||||
contentHint: number;
|
||||
options?: SendOptionsType;
|
||||
|
@ -1030,10 +1019,10 @@ export default class MessageSender {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const dataMessage = window.textsecure.protobuf.DataMessage.decode(
|
||||
encodedDataMessage
|
||||
const dataMessage = Proto.DataMessage.decode(
|
||||
new FIXMEU8(encodedDataMessage)
|
||||
);
|
||||
const sentMessage = new window.textsecure.protobuf.SyncMessage.Sent();
|
||||
const sentMessage = new Proto.SyncMessage.Sent();
|
||||
sentMessage.timestamp = timestamp;
|
||||
sentMessage.message = dataMessage;
|
||||
if (destination) {
|
||||
|
@ -1063,13 +1052,17 @@ export default class MessageSender {
|
|||
// number we sent to.
|
||||
if (sentTo && sentTo.length) {
|
||||
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);
|
||||
if (conv && conv.get('e164')) {
|
||||
status.destination = conv.get('e164');
|
||||
}
|
||||
if (conv && conv.get('uuid')) {
|
||||
status.destinationUuid = conv.get('uuid');
|
||||
if (conv) {
|
||||
const e164 = conv.get('e164');
|
||||
if (e164) {
|
||||
status.destination = e164;
|
||||
}
|
||||
const uuid = conv.get('uuid');
|
||||
if (uuid) {
|
||||
status.destinationUuid = uuid;
|
||||
}
|
||||
}
|
||||
status.unidentified = Boolean(unidentifiedLookup[identifier]);
|
||||
return status;
|
||||
|
@ -1078,12 +1071,10 @@ export default class MessageSender {
|
|||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.sent = sentMessage;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1101,17 +1092,14 @@ export default class MessageSender {
|
|||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1) {
|
||||
const request = new window.textsecure.protobuf.SyncMessage.Request();
|
||||
request.type =
|
||||
window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED;
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.BLOCKED;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1132,17 +1120,14 @@ export default class MessageSender {
|
|||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1) {
|
||||
const request = new window.textsecure.protobuf.SyncMessage.Request();
|
||||
request.type =
|
||||
window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION;
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1163,16 +1148,14 @@ export default class MessageSender {
|
|||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1) {
|
||||
const request = new window.textsecure.protobuf.SyncMessage.Request();
|
||||
request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS;
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.GROUPS;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1194,17 +1177,14 @@ export default class MessageSender {
|
|||
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice !== 1) {
|
||||
const request = new window.textsecure.protobuf.SyncMessage.Request();
|
||||
request.type =
|
||||
window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS;
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.CONTACTS;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1229,18 +1209,15 @@ export default class MessageSender {
|
|||
return;
|
||||
}
|
||||
|
||||
const fetchLatest = new window.textsecure.protobuf.SyncMessage.FetchLatest();
|
||||
fetchLatest.type =
|
||||
window.textsecure.protobuf.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
|
||||
const fetchLatest = new Proto.SyncMessage.FetchLatest();
|
||||
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.fetchLatest = fetchLatest;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1262,17 +1239,15 @@ export default class MessageSender {
|
|||
return;
|
||||
}
|
||||
|
||||
const request = new window.textsecure.protobuf.SyncMessage.Request();
|
||||
request.type = window.textsecure.protobuf.SyncMessage.Request.Type.KEYS;
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.KEYS;
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1295,25 +1270,19 @@ export default class MessageSender {
|
|||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice === 1) {
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.read = [];
|
||||
for (let i = 0; i < reads.length; i += 1) {
|
||||
const read = new window.textsecure.protobuf.SyncMessage.Read();
|
||||
read.timestamp = reads[i].timestamp;
|
||||
read.sender = reads[i].senderE164 || null;
|
||||
read.senderUuid = reads[i].senderUuid || null;
|
||||
const proto = new Proto.SyncMessage.Read(reads[i]);
|
||||
|
||||
syncMessage.read.push(read);
|
||||
syncMessage.read.push(proto);
|
||||
}
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1339,18 +1308,18 @@ export default class MessageSender {
|
|||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
|
||||
const viewOnceOpen = new window.textsecure.protobuf.SyncMessage.ViewOnceOpen();
|
||||
viewOnceOpen.sender = sender || null;
|
||||
viewOnceOpen.senderUuid = senderUuid || null;
|
||||
viewOnceOpen.timestamp = timestamp || null;
|
||||
const viewOnceOpen = new Proto.SyncMessage.ViewOnceOpen();
|
||||
if (sender !== undefined) {
|
||||
viewOnceOpen.sender = sender;
|
||||
}
|
||||
viewOnceOpen.senderUuid = senderUuid;
|
||||
viewOnceOpen.timestamp = timestamp;
|
||||
syncMessage.viewOnceOpen = viewOnceOpen;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1379,19 +1348,23 @@ export default class MessageSender {
|
|||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
|
||||
const response = new window.textsecure.protobuf.SyncMessage.MessageRequestResponse();
|
||||
response.threadE164 = responseArgs.threadE164 || null;
|
||||
response.threadUuid = responseArgs.threadUuid || null;
|
||||
response.groupId = responseArgs.groupId || null;
|
||||
const response = new Proto.SyncMessage.MessageRequestResponse();
|
||||
if (responseArgs.threadE164 !== undefined) {
|
||||
response.threadE164 = responseArgs.threadE164;
|
||||
}
|
||||
if (responseArgs.threadUuid !== undefined) {
|
||||
response.threadUuid = responseArgs.threadUuid;
|
||||
}
|
||||
if (responseArgs.groupId) {
|
||||
response.groupId = new FIXMEU8(responseArgs.groupId);
|
||||
}
|
||||
response.type = responseArgs.type;
|
||||
syncMessage.messageRequestResponse = response;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1417,15 +1390,14 @@ export default class MessageSender {
|
|||
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const ENUM =
|
||||
window.textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
|
||||
const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
|
||||
|
||||
const packOperations = operations.map(item => {
|
||||
const { packId, packKey, installed } = item;
|
||||
|
||||
const operation = new window.textsecure.protobuf.SyncMessage.StickerPackOperation();
|
||||
operation.packId = hexToArrayBuffer(packId);
|
||||
operation.packKey = base64ToArrayBuffer(packKey);
|
||||
const operation = new Proto.SyncMessage.StickerPackOperation();
|
||||
operation.packId = Bytes.fromHex(packId);
|
||||
operation.packKey = Bytes.fromBase64(packKey);
|
||||
operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE;
|
||||
|
||||
return operation;
|
||||
|
@ -1434,12 +1406,10 @@ export default class MessageSender {
|
|||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.stickerPackOperation = packOperations;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1476,7 +1446,7 @@ export default class MessageSender {
|
|||
);
|
||||
|
||||
return promise.then(async () => {
|
||||
const verified = new window.textsecure.protobuf.Verified();
|
||||
const verified = new Proto.Verified();
|
||||
verified.state = state;
|
||||
if (destinationE164) {
|
||||
verified.destination = destinationE164;
|
||||
|
@ -1484,18 +1454,16 @@ export default class MessageSender {
|
|||
if (destinationUuid) {
|
||||
verified.destinationUuid = destinationUuid;
|
||||
}
|
||||
verified.identityKey = identityKey;
|
||||
verified.identityKey = new FIXMEU8(identityKey);
|
||||
verified.nullMessage = padding;
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
syncMessage.verified = verified;
|
||||
|
||||
const secondMessage = new window.textsecure.protobuf.Content();
|
||||
const secondMessage = new Proto.Content();
|
||||
secondMessage.syncMessage = syncMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendIndividualProto({
|
||||
identifier: myUuid || myNumber,
|
||||
|
@ -1515,21 +1483,19 @@ export default class MessageSender {
|
|||
options: SendOptionsType,
|
||||
groupId?: string
|
||||
): Promise<CallbackResultType> {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendMessage({
|
||||
messageOptions: {
|
||||
recipients,
|
||||
timestamp: Date.now(),
|
||||
profileKey,
|
||||
flags: window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE,
|
||||
flags: Proto.DataMessage.Flags.PROFILE_KEY_UPDATE,
|
||||
...(groupId
|
||||
? {
|
||||
group: {
|
||||
id: groupId,
|
||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
type: Proto.GroupContext.Type.DELIVER,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
@ -1542,18 +1508,16 @@ export default class MessageSender {
|
|||
|
||||
async sendCallingMessage(
|
||||
recipientId: string,
|
||||
callingMessage: CallingMessageClass,
|
||||
callingMessage: Proto.ICallingMessage,
|
||||
options?: SendOptionsType
|
||||
): Promise<void> {
|
||||
const recipients = [recipientId];
|
||||
const finalTimestamp = Date.now();
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.callingMessage = callingMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await this.sendMessageProtoAndWait({
|
||||
timestamp: finalTimestamp,
|
||||
|
@ -1583,17 +1547,14 @@ export default class MessageSender {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const receiptMessage = new window.textsecure.protobuf.ReceiptMessage();
|
||||
receiptMessage.type =
|
||||
window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY;
|
||||
const receiptMessage = new Proto.ReceiptMessage();
|
||||
receiptMessage.type = Proto.ReceiptMessage.Type.DELIVERY;
|
||||
receiptMessage.timestamp = timestamps;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.receiptMessage = receiptMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: uuid || e164,
|
||||
|
@ -1615,16 +1576,14 @@ export default class MessageSender {
|
|||
timestamps: Array<number>;
|
||||
options?: SendOptionsType;
|
||||
}): Promise<CallbackResultType> {
|
||||
const receiptMessage = new window.textsecure.protobuf.ReceiptMessage();
|
||||
receiptMessage.type = window.textsecure.protobuf.ReceiptMessage.Type.READ;
|
||||
const receiptMessage = new Proto.ReceiptMessage();
|
||||
receiptMessage.type = Proto.ReceiptMessage.Type.READ;
|
||||
receiptMessage.timestamp = timestamps;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.receiptMessage = receiptMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendIndividualProto({
|
||||
identifier: senderUuid || senderE164,
|
||||
|
@ -1640,10 +1599,10 @@ export default class MessageSender {
|
|||
uuid,
|
||||
e164,
|
||||
padding,
|
||||
}: { uuid?: string; e164?: string; padding?: ArrayBuffer },
|
||||
}: { uuid?: string; e164?: string; padding?: Uint8Array },
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const nullMessage = new window.textsecure.protobuf.NullMessage();
|
||||
const nullMessage = new Proto.NullMessage();
|
||||
|
||||
const identifier = uuid || e164;
|
||||
if (!identifier) {
|
||||
|
@ -1652,12 +1611,10 @@ export default class MessageSender {
|
|||
|
||||
nullMessage.padding = padding || this.getRandomPadding();
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.nullMessage = nullMessage;
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
// We want the NullMessage to look like a normal outgoing message
|
||||
const timestamp = Date.now();
|
||||
|
@ -1679,9 +1636,9 @@ export default class MessageSender {
|
|||
CallbackResultType | void | Array<CallbackResultType | void | Array<void>>
|
||||
> {
|
||||
window.log.info('resetSession: start');
|
||||
const proto = new window.textsecure.protobuf.DataMessage();
|
||||
const proto = new Proto.DataMessage();
|
||||
proto.body = 'TERMINATE';
|
||||
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
proto.flags = Proto.DataMessage.Flags.END_SESSION;
|
||||
proto.timestamp = timestamp;
|
||||
|
||||
const identifier = uuid || e164;
|
||||
|
@ -1691,9 +1648,7 @@ export default class MessageSender {
|
|||
throw error;
|
||||
};
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const sendToContactPromise = window.textsecure.storage.protocol
|
||||
.archiveAllSessions(identifier)
|
||||
|
@ -1723,7 +1678,9 @@ export default class MessageSender {
|
|||
return sendToContactPromise;
|
||||
}
|
||||
|
||||
const buffer = proto.toArrayBuffer();
|
||||
const buffer = typedArrayToArrayBuffer(
|
||||
Proto.DataMessage.encode(proto).finish()
|
||||
);
|
||||
const sendSyncPromise = this.sendSyncMessage({
|
||||
encodedDataMessage: buffer,
|
||||
timestamp,
|
||||
|
@ -1745,9 +1702,7 @@ export default class MessageSender {
|
|||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendMessage({
|
||||
messageOptions: {
|
||||
|
@ -1755,8 +1710,7 @@ export default class MessageSender {
|
|||
timestamp,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
flags:
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
},
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
groupId: undefined,
|
||||
|
@ -1773,9 +1727,7 @@ export default class MessageSender {
|
|||
plaintext: PlaintextContent;
|
||||
uuid: string;
|
||||
}): Promise<CallbackResultType> {
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
return this.sendMessageProtoAndWait({
|
||||
timestamp: Date.now(),
|
||||
|
@ -1799,13 +1751,17 @@ export default class MessageSender {
|
|||
options,
|
||||
}: {
|
||||
recipients: Array<string>;
|
||||
proto: ContentClass;
|
||||
proto: Proto.Content;
|
||||
timestamp: number;
|
||||
contentHint: number;
|
||||
groupId: string | undefined;
|
||||
options?: SendOptionsType;
|
||||
}): 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 myUuid = window.textsecure.storage.user.getUuid();
|
||||
|
@ -1887,14 +1843,12 @@ export default class MessageSender {
|
|||
},
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
const contentMessage = new Proto.Content();
|
||||
|
||||
const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage(
|
||||
distributionId
|
||||
);
|
||||
contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap(
|
||||
typedArrayToArrayBuffer(senderKeyDistributionMessage.serialize())
|
||||
);
|
||||
contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize();
|
||||
|
||||
return this.sendGroupProto({
|
||||
recipients: identifiers,
|
||||
|
@ -1913,14 +1867,16 @@ export default class MessageSender {
|
|||
groupIdentifiers: Array<string>,
|
||||
options?: SendOptionsType
|
||||
): Promise<CallbackResultType> {
|
||||
const proto = new window.textsecure.protobuf.DataMessage();
|
||||
proto.group = new window.textsecure.protobuf.GroupContext();
|
||||
proto.group.id = stringToArrayBuffer(groupId);
|
||||
proto.group.type = window.textsecure.protobuf.GroupContext.Type.QUIT;
|
||||
const proto = new Proto.Content({
|
||||
dataMessage: {
|
||||
group: {
|
||||
id: Bytes.fromString(groupId),
|
||||
type: Proto.GroupContext.Type.QUIT,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
return this.sendGroupProto({
|
||||
recipients: groupIdentifiers,
|
||||
proto,
|
||||
|
@ -1949,11 +1905,10 @@ export default class MessageSender {
|
|||
timestamp,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
flags:
|
||||
window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
type: Proto.GroupContext.Type.DELIVER,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1967,9 +1922,7 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
return this.sendMessage({
|
||||
messageOptions,
|
||||
contentHint: ContentHint.DEFAULT,
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import EventTarget from './EventTarget';
|
||||
import EventTarget, { EventHandler } from './EventTarget';
|
||||
import MessageReceiver from './MessageReceiver';
|
||||
import { ContactSyncEvent, GroupSyncEvent } from './messageReceiverEvents';
|
||||
import MessageSender from './SendMessage';
|
||||
import { assert } from '../util/assert';
|
||||
|
||||
|
@ -20,9 +21,9 @@ class SyncRequestInner extends EventTarget {
|
|||
|
||||
timeout: any;
|
||||
|
||||
oncontact: Function;
|
||||
oncontact: (event: ContactSyncEvent) => void;
|
||||
|
||||
ongroup: Function;
|
||||
ongroup: (event: GroupSyncEvent) => void;
|
||||
|
||||
timeoutMillis: number;
|
||||
|
||||
|
@ -43,10 +44,10 @@ class SyncRequestInner extends EventTarget {
|
|||
}
|
||||
|
||||
this.oncontact = this.onContactSyncComplete.bind(this);
|
||||
receiver.addEventListener('contactsync', this.oncontact);
|
||||
receiver.addEventListener('contactSync', this.oncontact);
|
||||
|
||||
this.ongroup = this.onGroupSyncComplete.bind(this);
|
||||
receiver.addEventListener('groupsync', this.ongroup);
|
||||
receiver.addEventListener('groupSync', this.ongroup);
|
||||
|
||||
this.timeoutMillis = timeoutMillis || 60000;
|
||||
}
|
||||
|
@ -126,9 +127,15 @@ class SyncRequestInner extends EventTarget {
|
|||
export default class SyncRequest {
|
||||
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(
|
||||
sender: MessageSender,
|
||||
|
|
151
ts/textsecure/Types.d.ts
vendored
151
ts/textsecure/Types.d.ts
vendored
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { SignalService as Proto } from '../protobuf';
|
||||
|
||||
export {
|
||||
IdentityKeyType,
|
||||
PreKeyType,
|
||||
|
@ -56,3 +58,152 @@ export type OuterSignedPrekeyType = {
|
|||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -33,7 +33,7 @@ import { Long } from '../window.d';
|
|||
import { assert } from '../util/assert';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
import { toWebSafeBase64 } from '../util/webSafeBase64';
|
||||
import { isPackIdValid, redactPackId } from '../../js/modules/stickers';
|
||||
import { isPackIdValid, redactPackId } from '../types/Stickers';
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
|
@ -53,7 +53,6 @@ import { calculateAgreement, generateKeyPair } from '../Curve';
|
|||
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
|
||||
|
||||
import {
|
||||
AvatarUploadAttributesClass,
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
} from '../textsecure.d';
|
||||
|
@ -2161,7 +2160,7 @@ export function initialize({
|
|||
return Proto.GroupExternalCredential.decode(new FIXMEU8(response));
|
||||
}
|
||||
|
||||
function verifyAttributes(attributes: AvatarUploadAttributesClass) {
|
||||
function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) {
|
||||
const {
|
||||
key,
|
||||
credential,
|
||||
|
@ -2213,8 +2212,8 @@ export function initialize({
|
|||
responseType: 'arraybuffer',
|
||||
host: storageUrl,
|
||||
});
|
||||
const attributes = window.textsecure.protobuf.AvatarUploadAttributes.decode(
|
||||
response
|
||||
const attributes = Proto.AvatarUploadAttributes.decode(
|
||||
new FIXMEU8(response)
|
||||
);
|
||||
|
||||
const verified = verifyAttributes(attributes);
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
import { connection as WebSocket, IMessage } from 'websocket';
|
||||
|
||||
import EventTarget from './EventTarget';
|
||||
import EventTarget, { EventHandler } from './EventTarget';
|
||||
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { isOlderThan } from '../util/timestamp';
|
||||
|
@ -120,6 +120,12 @@ export type WebSocketResourceOptions = {
|
|||
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 {
|
||||
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(
|
||||
options: OutgoingWebSocketRequestOptions
|
||||
): OutgoingWebSocketRequest {
|
||||
|
@ -204,10 +219,7 @@ export default class WebSocketResource extends EventTarget {
|
|||
}
|
||||
|
||||
window.log.warn('Dispatching our own socket close event');
|
||||
const ev = new Event('close');
|
||||
ev.code = code;
|
||||
ev.reason = reason;
|
||||
this.dispatchEvent(ev);
|
||||
this.dispatchEvent(new CloseEvent(code, reason || 'normal'));
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
|
370
ts/textsecure/messageReceiverEvents.ts
Normal file
370
ts/textsecure/messageReceiverEvents.ts
Normal 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);
|
||||
}
|
||||
}
|
352
ts/textsecure/processDataMessage.ts
Normal file
352
ts/textsecure/processDataMessage.ts
Normal 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;
|
||||
}
|
60
ts/textsecure/processSyncMessage.ts
Normal file
60
ts/textsecure/processSyncMessage.ts
Normal 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),
|
||||
};
|
||||
}
|
|
@ -51,6 +51,9 @@ export type AttachmentType = {
|
|||
cdnNumber?: number;
|
||||
cdnId?: string;
|
||||
cdnKey?: string;
|
||||
|
||||
/** Legacy field. Used only for downloading old attachments */
|
||||
id?: number;
|
||||
};
|
||||
|
||||
type BaseAttachmentDraftType = {
|
||||
|
|
|
@ -1,17 +1,50 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
/* global
|
||||
textsecure,
|
||||
Signal,
|
||||
log,
|
||||
navigator,
|
||||
reduxStore,
|
||||
reduxActions,
|
||||
URLSearchParams
|
||||
*/
|
||||
import { isNumber, pick, reject, groupBy, values } from 'lodash';
|
||||
import pMap from 'p-map';
|
||||
import Queue from 'p-queue';
|
||||
|
||||
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': {
|
||||
key: 'Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI=',
|
||||
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');
|
||||
const pMap = require('p-map');
|
||||
const Queue = require('p-queue').default;
|
||||
|
||||
const { makeLookup } = require('../../ts/util/makeLookup');
|
||||
const { maybeParseUrl } = require('../../ts/util/url');
|
||||
const {
|
||||
base64ToArrayBuffer,
|
||||
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,
|
||||
author: '',
|
||||
coverStickerId: 0,
|
||||
createdAt: 0,
|
||||
downloadAttempts: 0,
|
||||
status: 'ephemeral',
|
||||
stickerCount: 0,
|
||||
stickers: {},
|
||||
title: '',
|
||||
};
|
||||
|
||||
let initialState = null;
|
||||
let packsToDownload = null;
|
||||
const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i;
|
||||
|
||||
let initialState: InitialState | undefined;
|
||||
let packsToDownload: DownloadMap | undefined;
|
||||
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([
|
||||
getPacksForRedux(),
|
||||
getRecentStickersForRedux(),
|
||||
]);
|
||||
|
||||
const blessedPacks: Record<string, boolean> = Object.create(null);
|
||||
for (const key of Object.keys(BLESSED_PACKS)) {
|
||||
blessedPacks[key] = true;
|
||||
}
|
||||
|
||||
initialState = {
|
||||
packs,
|
||||
recentStickers,
|
||||
blessedPacks: BLESSED_PACKS,
|
||||
blessedPacks,
|
||||
};
|
||||
|
||||
packsToDownload = capturePacksToDownload(packs);
|
||||
}
|
||||
|
||||
function getDataFromLink(link) {
|
||||
export function getDataFromLink(
|
||||
link: string
|
||||
): undefined | { id: string; key: string } {
|
||||
const url = maybeParseUrl(link);
|
||||
if (!url) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { hash } = url;
|
||||
if (!hash) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let params;
|
||||
try {
|
||||
params = new URLSearchParams(hash.slice(1));
|
||||
} catch (err) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const id = params.get('pack_id');
|
||||
if (!isPackIdValid(id)) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = params.get('pack_key');
|
||||
if (!key) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { id, key };
|
||||
}
|
||||
|
||||
function getInstalledStickerPacks() {
|
||||
const state = reduxStore.getState();
|
||||
export function getInstalledStickerPacks(): Array<StickerPackType> {
|
||||
const state = window.reduxStore.getState();
|
||||
const { stickers } = state;
|
||||
const { packs } = stickers;
|
||||
if (!packs) {
|
||||
|
@ -138,20 +148,24 @@ function getInstalledStickerPacks() {
|
|||
return items.filter(pack => pack.status === 'installed');
|
||||
}
|
||||
|
||||
function downloadQueuedPacks() {
|
||||
export function downloadQueuedPacks(): void {
|
||||
strictAssert(packsToDownload, 'Stickers not initialized');
|
||||
|
||||
const ids = Object.keys(packsToDownload);
|
||||
ids.forEach(id => {
|
||||
for (const id of ids) {
|
||||
const { key, status } = packsToDownload[id];
|
||||
|
||||
// The queuing is done inside this function, no need to await here
|
||||
downloadStickerPack(id, key, { finalStatus: status });
|
||||
});
|
||||
}
|
||||
|
||||
packsToDownload = {};
|
||||
}
|
||||
|
||||
function capturePacksToDownload(existingPackLookup) {
|
||||
const toDownload = Object.create(null);
|
||||
function capturePacksToDownload(
|
||||
existingPackLookup: Record<string, StickerPackType>
|
||||
): DownloadMap {
|
||||
const toDownload: DownloadMap = Object.create(null);
|
||||
|
||||
// First, ensure that blessed packs are in good shape
|
||||
const blessedIds = Object.keys(BLESSED_PACKS);
|
||||
|
@ -190,7 +204,7 @@ function capturePacksToDownload(existingPackLookup) {
|
|||
|
||||
if (doesPackNeedDownload(existing)) {
|
||||
const status =
|
||||
existing.attemptedStatus === 'installed' ? 'installed' : null;
|
||||
existing.attemptedStatus === 'installed' ? 'installed' : undefined;
|
||||
toDownload[id] = {
|
||||
id,
|
||||
key: existing.key,
|
||||
|
@ -202,7 +216,7 @@ function capturePacksToDownload(existingPackLookup) {
|
|||
return toDownload;
|
||||
}
|
||||
|
||||
function doesPackNeedDownload(pack) {
|
||||
function doesPackNeedDownload(pack?: StickerPackType): boolean {
|
||||
if (!pack) {
|
||||
return true;
|
||||
}
|
||||
|
@ -226,14 +240,14 @@ function doesPackNeedDownload(pack) {
|
|||
return true;
|
||||
}
|
||||
|
||||
async function getPacksForRedux() {
|
||||
async function getPacksForRedux(): Promise<Record<string, StickerPackType>> {
|
||||
const [packs, stickers] = await Promise.all([
|
||||
getAllStickerPacks(),
|
||||
getAllStickers(),
|
||||
Data.getAllStickerPacks(),
|
||||
Data.getAllStickers(),
|
||||
]);
|
||||
|
||||
const stickersByPack = groupBy(stickers, sticker => sticker.packId);
|
||||
const fullSet = packs.map(pack => ({
|
||||
const fullSet: Array<StickerPackType> = packs.map(pack => ({
|
||||
...pack,
|
||||
stickers: makeLookup(stickersByPack[pack.id] || [], 'id'),
|
||||
}));
|
||||
|
@ -241,40 +255,41 @@ async function getPacksForRedux() {
|
|||
return makeLookup(fullSet, 'id');
|
||||
}
|
||||
|
||||
async function getRecentStickersForRedux() {
|
||||
const recent = await getRecentStickers();
|
||||
async function getRecentStickersForRedux(): Promise<Array<RecentStickerType>> {
|
||||
const recent = await Data.getRecentStickers();
|
||||
return recent.map(sticker => ({
|
||||
packId: sticker.packId,
|
||||
stickerId: sticker.id,
|
||||
}));
|
||||
}
|
||||
|
||||
function getInitialState() {
|
||||
export function getInitialState(): InitialState {
|
||||
strictAssert(initialState !== undefined, 'Stickers not initialized');
|
||||
return initialState;
|
||||
}
|
||||
|
||||
function isPackIdValid(packId) {
|
||||
export function isPackIdValid(packId: unknown): packId is string {
|
||||
return typeof packId === 'string' && VALID_PACK_ID_REGEXP.test(packId);
|
||||
}
|
||||
|
||||
function redactPackId(packId) {
|
||||
export function redactPackId(packId: string): string {
|
||||
return `[REDACTED]${packId.slice(-3)}`;
|
||||
}
|
||||
|
||||
function getReduxStickerActions() {
|
||||
const actions = reduxActions;
|
||||
const actions = window.reduxActions;
|
||||
strictAssert(actions && actions.stickers, 'Redux not ready');
|
||||
|
||||
if (actions && actions.stickers) {
|
||||
return actions.stickers;
|
||||
}
|
||||
|
||||
return {};
|
||||
return actions.stickers;
|
||||
}
|
||||
|
||||
async function decryptSticker(packKey, ciphertext) {
|
||||
async function decryptSticker(
|
||||
packKey: string,
|
||||
ciphertext: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const binaryKey = base64ToArrayBuffer(packKey);
|
||||
const derivedKey = await deriveStickerPackKey(binaryKey);
|
||||
const plaintext = await textsecure.crypto.decryptAttachment(
|
||||
const plaintext = await window.textsecure.crypto.decryptAttachment(
|
||||
ciphertext,
|
||||
derivedKey
|
||||
);
|
||||
|
@ -282,26 +297,35 @@ async function decryptSticker(packKey, ciphertext) {
|
|||
return plaintext;
|
||||
}
|
||||
|
||||
async function downloadSticker(packId, packKey, proto, options) {
|
||||
const { ephemeral } = options || {};
|
||||
async function downloadSticker(
|
||||
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 sticker = ephemeral
|
||||
? await Signal.Migrations.processNewEphemeralSticker(plaintext, options)
|
||||
: await Signal.Migrations.processNewSticker(plaintext, options);
|
||||
? await window.Signal.Migrations.processNewEphemeralSticker(plaintext)
|
||||
: await window.Signal.Migrations.processNewSticker(plaintext);
|
||||
|
||||
return {
|
||||
...pick(proto, ['id', 'emoji']),
|
||||
id,
|
||||
emoji: dropNull(emoji),
|
||||
...sticker,
|
||||
packId,
|
||||
};
|
||||
}
|
||||
|
||||
async function savePackMetadata(packId, packKey, options = {}) {
|
||||
const { messageId } = options;
|
||||
|
||||
export async function savePackMetadata(
|
||||
packId: string,
|
||||
packKey: string,
|
||||
{ messageId }: { messageId?: string } = {}
|
||||
): Promise<void> {
|
||||
const existing = getStickerPack(packId);
|
||||
if (existing) {
|
||||
return;
|
||||
|
@ -309,20 +333,23 @@ async function savePackMetadata(packId, packKey, options = {}) {
|
|||
|
||||
const { stickerPackAdded } = getReduxStickerActions();
|
||||
const pack = {
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
|
||||
id: packId,
|
||||
key: packKey,
|
||||
status: 'known',
|
||||
status: 'known' as const,
|
||||
};
|
||||
stickerPackAdded(pack);
|
||||
|
||||
await createOrUpdateStickerPack(pack);
|
||||
await Data.createOrUpdateStickerPack(pack);
|
||||
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);
|
||||
strictAssert(existing, `No existing sticker pack with id: ${packId}`);
|
||||
if (
|
||||
existing.status !== 'ephemeral' &&
|
||||
!(existing.status === 'error' && existing.attemptedStatus === 'ephemeral')
|
||||
|
@ -335,16 +362,18 @@ async function removeEphemeralPack(packId) {
|
|||
|
||||
const stickers = values(existing.stickers);
|
||||
const paths = stickers.map(sticker => sticker.path);
|
||||
await pMap(paths, Signal.Migrations.deleteTempFile, {
|
||||
await pMap(paths, window.Signal.Migrations.deleteTempFile, {
|
||||
concurrency: 3,
|
||||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
|
||||
// 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 {
|
||||
stickerAdded,
|
||||
stickerPackAdded,
|
||||
|
@ -358,7 +387,7 @@ async function downloadEphemeralPack(packId, packKey) {
|
|||
existingPack.status === 'installed' ||
|
||||
existingPack.status === 'pending')
|
||||
) {
|
||||
log.warn(
|
||||
window.log.warn(
|
||||
`Ephemeral download for pack ${redactPackId(
|
||||
packId
|
||||
)} requested, we already know about it. Skipping.`
|
||||
|
@ -369,17 +398,19 @@ async function downloadEphemeralPack(packId, packKey) {
|
|||
try {
|
||||
// Synchronous placeholder to help with race conditions
|
||||
const placeholder = {
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
|
||||
id: packId,
|
||||
key: packKey,
|
||||
status: 'ephemeral',
|
||||
status: 'ephemeral' as const,
|
||||
};
|
||||
stickerPackAdded(placeholder);
|
||||
|
||||
const ciphertext = await textsecure.messaging.getStickerPackManifest(
|
||||
const ciphertext = await window.textsecure.messaging.getStickerPackManifest(
|
||||
packId
|
||||
);
|
||||
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 stickerCount = proto.stickers.length;
|
||||
|
||||
|
@ -402,16 +433,20 @@ async function downloadEphemeralPack(packId, packKey) {
|
|||
const coverIncludedInList = nonCoverStickers.length < stickerCount;
|
||||
|
||||
const pack = {
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
|
||||
id: packId,
|
||||
key: packKey,
|
||||
coverStickerId,
|
||||
stickerCount,
|
||||
status: 'ephemeral',
|
||||
status: 'ephemeral' as const,
|
||||
...pick(proto, ['title', 'author']),
|
||||
};
|
||||
stickerPackAdded(pack);
|
||||
|
||||
const downloadStickerJob = async stickerProto => {
|
||||
const downloadStickerJob = async (
|
||||
stickerProto: Proto.StickerPack.ISticker
|
||||
): Promise<void> => {
|
||||
const stickerInfo = await downloadSticker(packId, packKey, stickerProto, {
|
||||
ephemeral: true,
|
||||
});
|
||||
|
@ -438,7 +473,6 @@ async function downloadEphemeralPack(packId, packKey) {
|
|||
// Then the rest
|
||||
await pMap(nonCoverStickers, downloadStickerJob, {
|
||||
concurrency: 3,
|
||||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
} catch (error) {
|
||||
// Because the user could install this pack while we are still downloading this
|
||||
|
@ -451,20 +485,30 @@ async function downloadEphemeralPack(packId, packKey) {
|
|||
status: 'error',
|
||||
});
|
||||
}
|
||||
log.error(
|
||||
window.log.error(
|
||||
`Ephemeral download error for sticker pack ${redactPackId(packId)}:`,
|
||||
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
|
||||
return downloadQueue.add(async () => {
|
||||
try {
|
||||
await doDownloadStickerPack(packId, packKey, options);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
window.log.error(
|
||||
'doDownloadStickerPack threw an error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
@ -472,8 +516,15 @@ async function downloadStickerPack(packId, packKey, options = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
async function doDownloadStickerPack(packId, packKey, options = {}) {
|
||||
const { messageId, fromSync } = options;
|
||||
async function doDownloadStickerPack(
|
||||
packId: string,
|
||||
packKey: string,
|
||||
{
|
||||
finalStatus = 'downloaded',
|
||||
messageId,
|
||||
fromSync = false,
|
||||
}: DownloadStickerPackOptions
|
||||
): Promise<void> {
|
||||
const {
|
||||
stickerAdded,
|
||||
stickerPackAdded,
|
||||
|
@ -481,7 +532,6 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
installStickerPack,
|
||||
} = getReduxStickerActions();
|
||||
|
||||
const finalStatus = options.finalStatus || 'downloaded';
|
||||
if (finalStatus !== 'downloaded' && finalStatus !== 'installed') {
|
||||
throw new Error(
|
||||
`doDownloadStickerPack: invalid finalStatus of ${finalStatus} requested.`
|
||||
|
@ -490,7 +540,7 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
|
||||
const existing = getStickerPack(packId);
|
||||
if (!doesPackNeedDownload(existing)) {
|
||||
log.warn(
|
||||
window.log.warn(
|
||||
`Download for pack ${redactPackId(
|
||||
packId
|
||||
)} requested, but it does not need re-download. Skipping.`
|
||||
|
@ -503,14 +553,14 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
const downloadAttempts =
|
||||
(existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement;
|
||||
if (downloadAttempts > 3) {
|
||||
log.warn(
|
||||
window.log.warn(
|
||||
`Refusing to attempt another download for pack ${redactPackId(
|
||||
packId
|
||||
)}, attempt number ${downloadAttempts}`
|
||||
);
|
||||
|
||||
if (existing.status !== 'error') {
|
||||
await updateStickerPackStatus(packId, 'error');
|
||||
if (existing && existing.status !== 'error') {
|
||||
await Data.updateStickerPackStatus(packId, 'error');
|
||||
stickerPackUpdated(packId, {
|
||||
status: 'error',
|
||||
});
|
||||
|
@ -519,32 +569,34 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
return;
|
||||
}
|
||||
|
||||
let coverProto;
|
||||
let coverStickerId;
|
||||
let coverIncludedInList;
|
||||
let nonCoverStickers;
|
||||
let coverProto: Proto.StickerPack.ISticker | undefined;
|
||||
let coverStickerId: number | undefined;
|
||||
let coverIncludedInList = false;
|
||||
let nonCoverStickers: Array<Proto.StickerPack.ISticker> = [];
|
||||
|
||||
try {
|
||||
// Synchronous placeholder to help with race conditions
|
||||
const placeholder = {
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
|
||||
id: packId,
|
||||
key: packKey,
|
||||
attemptedStatus: finalStatus,
|
||||
downloadAttempts,
|
||||
status: 'pending',
|
||||
status: 'pending' as const,
|
||||
};
|
||||
stickerPackAdded(placeholder);
|
||||
|
||||
const ciphertext = await textsecure.messaging.getStickerPackManifest(
|
||||
const ciphertext = await window.textsecure.messaging.getStickerPackManifest(
|
||||
packId
|
||||
);
|
||||
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||
const proto = textsecure.protobuf.StickerPack.decode(plaintext);
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : null;
|
||||
const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext));
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined;
|
||||
const stickerCount = proto.stickers.length;
|
||||
|
||||
coverProto = proto.cover || firstStickerProto;
|
||||
coverStickerId = coverProto ? coverProto.id : null;
|
||||
coverStickerId = dropNull(coverProto ? coverProto.id : undefined);
|
||||
|
||||
if (!coverProto || !isNumber(coverStickerId)) {
|
||||
throw new Error(
|
||||
|
@ -568,7 +620,7 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
// - 'downloaded'
|
||||
// - 'error'
|
||||
// - 'installed'
|
||||
const pack = {
|
||||
const pack: StickerPackType = {
|
||||
id: packId,
|
||||
key: packKey,
|
||||
attemptedStatus: finalStatus,
|
||||
|
@ -576,28 +628,32 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
downloadAttempts,
|
||||
stickerCount,
|
||||
status: 'pending',
|
||||
createdAt: Date.now(),
|
||||
stickers: {},
|
||||
...pick(proto, ['title', 'author']),
|
||||
};
|
||||
await createOrUpdateStickerPack(pack);
|
||||
await Data.createOrUpdateStickerPack(pack);
|
||||
stickerPackAdded(pack);
|
||||
|
||||
if (messageId) {
|
||||
await addStickerPackReference(messageId, packId);
|
||||
await Data.addStickerPackReference(messageId, packId);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
window.log.error(
|
||||
`Error downloading manifest for sticker pack ${redactPackId(packId)}:`,
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
const pack = {
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
|
||||
id: packId,
|
||||
key: packKey,
|
||||
attemptedStatus: finalStatus,
|
||||
downloadAttempts,
|
||||
status: 'error',
|
||||
status: 'error' as const,
|
||||
};
|
||||
await createOrUpdateStickerPack(pack);
|
||||
await Data.createOrUpdateStickerPack(pack);
|
||||
stickerPackAdded(pack);
|
||||
|
||||
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
|
||||
// and we want to preserve more of the pack on an error.
|
||||
try {
|
||||
const downloadStickerJob = async stickerProto => {
|
||||
const downloadStickerJob = async (
|
||||
stickerProto: Proto.StickerPack.ISticker
|
||||
): Promise<void> => {
|
||||
const stickerInfo = await downloadSticker(packId, packKey, stickerProto);
|
||||
const sticker = {
|
||||
...stickerInfo,
|
||||
isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId,
|
||||
};
|
||||
await createOrUpdateSticker(sticker);
|
||||
await Data.createOrUpdateSticker(sticker);
|
||||
stickerAdded(sticker);
|
||||
};
|
||||
|
||||
|
@ -622,7 +680,6 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
// Then the rest
|
||||
await pMap(nonCoverStickers, downloadStickerJob, {
|
||||
concurrency: 3,
|
||||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
|
||||
// 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 });
|
||||
} else {
|
||||
// Mark the pack as complete
|
||||
await updateStickerPackStatus(packId, finalStatus);
|
||||
await Data.updateStickerPackStatus(packId, finalStatus);
|
||||
stickerPackUpdated(packId, {
|
||||
status: finalStatus,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
window.log.error(
|
||||
`Error downloading stickers for sticker pack ${redactPackId(packId)}:`,
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
const errorStatus = 'error';
|
||||
await updateStickerPackStatus(packId, errorStatus);
|
||||
await Data.updateStickerPackStatus(packId, errorStatus);
|
||||
if (stickerPackUpdated) {
|
||||
stickerPackUpdated(packId, {
|
||||
attemptedStatus: finalStatus,
|
||||
|
@ -658,45 +715,53 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
function getStickerPack(packId) {
|
||||
const state = reduxStore.getState();
|
||||
export function getStickerPack(packId: string): StickerPackType | undefined {
|
||||
const state = window.reduxStore.getState();
|
||||
const { stickers } = state;
|
||||
const { packs } = stickers;
|
||||
if (!packs) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return packs[packId];
|
||||
}
|
||||
|
||||
function getStickerPackStatus(packId) {
|
||||
export function getStickerPackStatus(
|
||||
packId: string
|
||||
): StickerPackStatusType | undefined {
|
||||
const pack = getStickerPack(packId);
|
||||
if (!pack) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return pack.status;
|
||||
}
|
||||
|
||||
function getSticker(packId, stickerId) {
|
||||
export function getSticker(
|
||||
packId: string,
|
||||
stickerId: number
|
||||
): StickerType | undefined {
|
||||
const pack = getStickerPack(packId);
|
||||
|
||||
if (!pack || !pack.stickers) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!sticker) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { path } = sticker;
|
||||
const absolutePath = Signal.Migrations.getAbsoluteStickerPath(path);
|
||||
const newPath = await Signal.Migrations.copyIntoAttachmentsDirectory(
|
||||
const absolutePath = window.Signal.Migrations.getAbsoluteStickerPath(path);
|
||||
const newPath = await window.Signal.Migrations.copyIntoAttachmentsDirectory(
|
||||
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
|
||||
// more references left. We'll delete a nonexistent reference, then check if there are
|
||||
// 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
|
||||
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
|
||||
// the last reference is deleted, we finally then remove the pack itself from database
|
||||
// and from disk.
|
||||
async function deletePackReference(messageId, packId) {
|
||||
export async function deletePackReference(
|
||||
messageId: string,
|
||||
packId: string
|
||||
): Promise<void> {
|
||||
const isBlessed = Boolean(BLESSED_PACKS[packId]);
|
||||
if (isBlessed) {
|
||||
return;
|
||||
|
@ -725,7 +793,7 @@ async function deletePackReference(messageId, packId) {
|
|||
|
||||
// This call uses locking to prevent race conditions with other reference removals,
|
||||
// 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 (!paths || !paths.length) {
|
||||
|
@ -735,14 +803,13 @@ async function deletePackReference(messageId, packId) {
|
|||
const { removeStickerPack } = getReduxStickerActions();
|
||||
removeStickerPack(packId);
|
||||
|
||||
await pMap(paths, Signal.Migrations.deleteSticker, {
|
||||
await pMap(paths, window.Signal.Migrations.deleteSticker, {
|
||||
concurrency: 3,
|
||||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
}
|
||||
|
||||
// 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]);
|
||||
if (isBlessed) {
|
||||
return;
|
||||
|
@ -750,13 +817,12 @@ async function deletePack(packId) {
|
|||
|
||||
// This call uses locking to prevent race conditions with other reference removals,
|
||||
// or an incoming message creating a new message->pack reference
|
||||
const paths = await deleteStickerPack(packId);
|
||||
const paths = await Data.deleteStickerPack(packId);
|
||||
|
||||
const { removeStickerPack } = getReduxStickerActions();
|
||||
removeStickerPack(packId);
|
||||
|
||||
await pMap(paths, Signal.Migrations.deleteSticker, {
|
||||
await pMap(paths, window.Signal.Migrations.deleteSticker, {
|
||||
concurrency: 3,
|
||||
timeout: 1000 * 60 * 2,
|
||||
});
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
export type BodyRangeType = {
|
||||
start: number;
|
||||
length: number;
|
||||
mentionUuid: string;
|
||||
replacementText: string;
|
||||
mentionUuid?: string;
|
||||
replacementText?: string;
|
||||
conversationID?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
DownloadAttachmentType,
|
||||
} from '../textsecure.d';
|
||||
import { DownloadAttachmentType } from '../textsecure.d';
|
||||
|
||||
type AttachmentData = AttachmentPointerClass & {
|
||||
id?: string;
|
||||
};
|
||||
import { AttachmentType } from '../types/Attachment';
|
||||
|
||||
export async function downloadAttachment(
|
||||
attachmentData: AttachmentData
|
||||
attachmentData: AttachmentType
|
||||
): Promise<DownloadAttachmentType | null> {
|
||||
let migratedAttachment: AttachmentType;
|
||||
|
||||
const { id: legacyId } = attachmentData;
|
||||
if (legacyId === undefined) {
|
||||
migratedAttachment = attachmentData;
|
||||
} else {
|
||||
migratedAttachment = {
|
||||
...attachmentData,
|
||||
cdnId: String(legacyId),
|
||||
};
|
||||
}
|
||||
|
||||
let downloaded;
|
||||
try {
|
||||
if (attachmentData.id) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
attachmentData.cdnId = attachmentData.id;
|
||||
}
|
||||
downloaded = await window.textsecure.messageReceiver.downloadAttachment(
|
||||
attachmentData
|
||||
migratedAttachment
|
||||
);
|
||||
} catch (error) {
|
||||
// Attachments on the server expire after 30 days, then start returning 404
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// 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>(
|
||||
value: NonNullable<T> | null | undefined
|
||||
|
@ -9,3 +14,25 @@ export function dropNull<T>(
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { AccessControlClass } from '../textsecure.d';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
const AccessControlEnum = Proto.AccessControl.AccessRequired;
|
||||
|
||||
type AccessControlOption = {
|
||||
text: string;
|
||||
|
@ -10,17 +12,16 @@ type AccessControlOption = {
|
|||
};
|
||||
|
||||
export function getAccessControlOptions(
|
||||
accessEnum: typeof AccessControlClass.AccessRequired,
|
||||
i18n: LocalizerType
|
||||
): Array<AccessControlOption> {
|
||||
return [
|
||||
{
|
||||
text: i18n('GroupV2--all-members'),
|
||||
value: accessEnum.MEMBER,
|
||||
value: AccessControlEnum.MEMBER,
|
||||
},
|
||||
{
|
||||
text: i18n('GroupV2--only-admins'),
|
||||
value: accessEnum.ADMINISTRATOR,
|
||||
value: AccessControlEnum.ADMINISTRATOR,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ConversationAttributesType } from '../model-types.d';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { isDirectConversation, isMe } from './whatTypeOfConversation';
|
||||
import { isInSystemContacts } from './isInSystemContacts';
|
||||
|
||||
|
@ -24,8 +25,7 @@ export function isConversationAccepted(
|
|||
return true;
|
||||
}
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
const { messageRequestResponseType } = conversationAttrs;
|
||||
if (messageRequestResponseType === messageRequestEnum.ACCEPT) {
|
||||
|
|
|
@ -198,13 +198,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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-$(",
|
||||
"path": "js/permissions_popup_start.js",
|
||||
|
@ -14141,6 +14134,20 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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",
|
||||
"path": "ts/util/hooks.js",
|
||||
|
@ -14157,4 +14164,4 @@
|
|||
"updated": "2021-03-18T21:41:28.361Z",
|
||||
"reasonDetail": "A generic hook. Typically not to be used with non-DOM values."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -57,6 +57,7 @@ const excludedFilesRegexps = [
|
|||
'^sticker-creator/dist/bundle.js',
|
||||
'^test/test.js',
|
||||
'^ts/test[^/]*/.+',
|
||||
'^ts/sql/mainWorker.bundle.js',
|
||||
|
||||
// Copied from dependency
|
||||
'^js/Mp3LameEncoder.min.js',
|
||||
|
|
|
@ -37,7 +37,7 @@ import {
|
|||
multiRecipient409ResponseSchema,
|
||||
multiRecipient410ResponseSchema,
|
||||
} from '../textsecure/WebAPI';
|
||||
import { ContentClass } from '../textsecure.d';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import { assert } from './assert';
|
||||
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.
|
||||
const MAX_RECURSION = 5;
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
// Public API:
|
||||
|
||||
export async function sendToGroup({
|
||||
|
@ -106,7 +109,7 @@ export async function sendContentMessageToGroup({
|
|||
timestamp,
|
||||
}: {
|
||||
contentHint: number;
|
||||
contentMessage: ContentClass;
|
||||
contentMessage: Proto.Content;
|
||||
conversation: ConversationModel;
|
||||
isPartialSend?: boolean;
|
||||
online?: boolean;
|
||||
|
@ -165,7 +168,7 @@ export async function sendContentMessageToGroup({
|
|||
|
||||
export async function sendToGroupViaSenderKey(options: {
|
||||
contentHint: number;
|
||||
contentMessage: ContentClass;
|
||||
contentMessage: Proto.Content;
|
||||
conversation: ConversationModel;
|
||||
isPartialSend?: boolean;
|
||||
online?: boolean;
|
||||
|
@ -185,9 +188,7 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
sendOptions,
|
||||
timestamp,
|
||||
} = options;
|
||||
const {
|
||||
ContentHint,
|
||||
} = window.textsecure.protobuf.UnidentifiedSenderMessage.Message;
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
const logId = conversation.idForLogging();
|
||||
window.log.info(
|
||||
|
@ -372,7 +373,9 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
contentHint,
|
||||
devices: devicesForSenderKey,
|
||||
distributionId,
|
||||
contentMessage: contentMessage.toArrayBuffer(),
|
||||
contentMessage: toArrayBuffer(
|
||||
Proto.Content.encode(contentMessage).finish()
|
||||
),
|
||||
groupId,
|
||||
});
|
||||
const accessKeys = getXorOfAccessKeys(devicesForSenderKey);
|
||||
|
@ -431,7 +434,11 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
const normalRecipients = getUuidsFromDevices(devicesForNormalSend);
|
||||
if (normalRecipients.length === 0) {
|
||||
return {
|
||||
dataMessage: contentMessage.dataMessage?.toArrayBuffer(),
|
||||
dataMessage: contentMessage.dataMessage
|
||||
? toArrayBuffer(
|
||||
Proto.DataMessage.encode(contentMessage.dataMessage).finish()
|
||||
)
|
||||
: undefined,
|
||||
successfulIdentifiers: senderKeyRecipients,
|
||||
unidentifiedDeliveries: senderKeyRecipients,
|
||||
};
|
||||
|
@ -449,7 +456,11 @@ export async function sendToGroupViaSenderKey(options: {
|
|||
});
|
||||
|
||||
return {
|
||||
dataMessage: contentMessage.dataMessage?.toArrayBuffer(),
|
||||
dataMessage: contentMessage.dataMessage
|
||||
? toArrayBuffer(
|
||||
Proto.DataMessage.encode(contentMessage.dataMessage).finish()
|
||||
)
|
||||
: undefined,
|
||||
errors: normalSendResult.errors,
|
||||
failoverIdentifiers: normalSendResult.failoverIdentifiers,
|
||||
successfulIdentifiers: [
|
||||
|
@ -669,7 +680,7 @@ async function encryptForSenderKey({
|
|||
);
|
||||
const ourAddress = getOurAddress();
|
||||
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(
|
||||
ourAddress,
|
||||
|
|
|
@ -9,7 +9,9 @@ import {
|
|||
InMemoryAttachmentDraftType,
|
||||
OnDiskAttachmentDraftType,
|
||||
} 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 {
|
||||
GroupV2PendingMemberType,
|
||||
|
@ -48,6 +50,7 @@ import {
|
|||
LinkPreviewWithDomain,
|
||||
} from '../types/LinkPreview';
|
||||
import * as LinkPreview from '../types/LinkPreview';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
type AttachmentOptions = {
|
||||
messageId: string;
|
||||
|
@ -616,8 +619,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
</div>
|
||||
`)[0];
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
const props = {
|
||||
id: model.id,
|
||||
|
@ -845,8 +847,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const { model }: { model: ConversationModel } = this;
|
||||
const { id } = model;
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
const contactSupport = () => {
|
||||
const baseUrl =
|
||||
|
@ -1564,8 +1565,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
|
||||
blockAndReportSpam(model: ConversationModel): Promise<void> {
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
return this.longRunningTaskWrapper({
|
||||
name: 'blockAndReportSpam',
|
||||
|
@ -2202,7 +2202,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
contentType: blob.type,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
flags: window.textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
};
|
||||
|
||||
// Note: The RecorderView removes itself on send
|
||||
|
@ -2443,12 +2443,12 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
: undefined;
|
||||
|
||||
conversation.sendMessage(
|
||||
null,
|
||||
undefined, // body
|
||||
[],
|
||||
null,
|
||||
undefined, // quote
|
||||
[],
|
||||
stickerNoPath,
|
||||
undefined,
|
||||
undefined, // BodyRanges
|
||||
{ ...sendMessageOptions, timestamp }
|
||||
);
|
||||
} else {
|
||||
|
@ -2469,11 +2469,11 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
);
|
||||
|
||||
conversation.sendMessage(
|
||||
messageBody || null,
|
||||
messageBody || undefined,
|
||||
attachmentsToSend,
|
||||
null, // quote
|
||||
undefined, // quote
|
||||
preview,
|
||||
null, // sticker
|
||||
undefined, // sticker
|
||||
undefined, // BodyRanges
|
||||
{ ...sendMessageOptions, timestamp }
|
||||
);
|
||||
|
@ -2953,14 +2953,14 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
|
||||
showStickerPackPreview(packId: string, packKey: string) {
|
||||
window.Signal.Stickers.downloadEphemeralPack(packId, packKey);
|
||||
Stickers.downloadEphemeralPack(packId, packKey);
|
||||
|
||||
const props = {
|
||||
packId,
|
||||
onClose: async () => {
|
||||
this.stickerPreviewModalView.remove();
|
||||
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(
|
||||
window.reduxStore,
|
||||
{
|
||||
accessEnum: window.textsecure.protobuf.AccessControl.AccessRequired,
|
||||
changeHasGroupLink: this.changeHasGroupLink.bind(this),
|
||||
conversationId: model.id,
|
||||
copyGroupLink: this.copyGroupLink.bind(this),
|
||||
|
@ -3224,7 +3223,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
JSX: window.Signal.State.Roots.createGroupV2Permissions(
|
||||
window.reduxStore,
|
||||
{
|
||||
accessEnum: window.textsecure.protobuf.AccessControl.AccessRequired,
|
||||
conversationId: model.id,
|
||||
setAccessControlAttributesSetting: this.setAccessControlAttributesSetting.bind(
|
||||
this
|
||||
|
@ -3282,8 +3280,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
showConversationDetails() {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
// these methods are used in more than one place and should probably be
|
||||
// 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(
|
||||
model.get('groupInviteLinkPassword') &&
|
||||
|
@ -4029,15 +4026,29 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
url: string,
|
||||
abortSignal: Readonly<AbortSignal>
|
||||
): Promise<null | LinkPreviewResult> {
|
||||
const isPackDownloaded = (pack: any) =>
|
||||
pack && (pack.status === 'downloaded' || pack.status === 'installed');
|
||||
const isPackValid = (pack: any) =>
|
||||
pack &&
|
||||
(pack.status === 'ephemeral' ||
|
||||
pack.status === 'downloaded' ||
|
||||
pack.status === 'installed');
|
||||
const isPackDownloaded = (
|
||||
pack?: StickerPackDBType
|
||||
): pack is StickerPackDBType => {
|
||||
if (!pack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
@ -4047,16 +4058,16 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const keyBytes = window.Signal.Crypto.bytesFromHexString(key);
|
||||
const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes);
|
||||
|
||||
const existing = window.Signal.Stickers.getStickerPack(id);
|
||||
const existing = Stickers.getStickerPack(id);
|
||||
if (!isPackDownloaded(existing)) {
|
||||
await window.Signal.Stickers.downloadEphemeralPack(id, keyBase64);
|
||||
await Stickers.downloadEphemeralPack(id, keyBase64);
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pack = window.Signal.Stickers.getStickerPack(id);
|
||||
const pack = Stickers.getStickerPack(id);
|
||||
|
||||
if (!isPackValid(pack)) {
|
||||
return null;
|
||||
|
@ -4083,7 +4094,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
...sticker,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
contentType: 'image/webp',
|
||||
contentType: IMAGE_WEBP,
|
||||
},
|
||||
description: null,
|
||||
date: null,
|
||||
|
@ -4096,7 +4107,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return null;
|
||||
} finally {
|
||||
if (id) {
|
||||
await window.Signal.Stickers.removeEphemeralPack(id);
|
||||
await Stickers.removeEphemeralPack(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
60
ts/window.d.ts
vendored
60
ts/window.d.ts
vendored
|
@ -235,7 +235,6 @@ declare global {
|
|||
};
|
||||
log: LoggerType;
|
||||
nodeSetImmediate: typeof setImmediate;
|
||||
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
|
||||
onFullScreenChange: (fullScreen: boolean) => void;
|
||||
platform: string;
|
||||
preloadedImages: Array<WhatIsThis>;
|
||||
|
@ -311,14 +310,31 @@ declare global {
|
|||
loadPreviewData: (preview: unknown) => WhatIsThis;
|
||||
loadStickerData: (sticker: unknown) => WhatIsThis;
|
||||
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;
|
||||
processNewAttachment: (
|
||||
attachment: DownloadAttachmentType
|
||||
) => Promise<AttachmentType>;
|
||||
|
||||
copyIntoTempDirectory: any;
|
||||
deleteDraftFile: any;
|
||||
deleteTempFile: any;
|
||||
deleteDraftFile: (path: string) => Promise<void>;
|
||||
deleteTempFile: (path: string) => Promise<void>;
|
||||
getAbsoluteDraftPath: any;
|
||||
getAbsoluteTempPath: any;
|
||||
openFileInFolder: any;
|
||||
|
@ -327,36 +343,6 @@ declare global {
|
|||
saveAttachmentToDisk: 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: {
|
||||
Attachment: {
|
||||
save: any;
|
||||
|
@ -548,6 +534,8 @@ declare global {
|
|||
// eslint-disable-next-line no-restricted-syntax
|
||||
interface Error {
|
||||
originalError?: Event;
|
||||
reason?: any;
|
||||
stackForLog?: string;
|
||||
}
|
||||
|
||||
// Uint8Array and ArrayBuffer are type-compatible in TypeScript's covariant
|
||||
|
@ -702,7 +690,11 @@ export type WhisperType = {
|
|||
TapToViewMessagesListener: WhatIsThis;
|
||||
|
||||
deliveryReceiptQueue: PQueue<WhatIsThis>;
|
||||
deliveryReceiptBatcher: BatcherType<WhatIsThis>;
|
||||
deliveryReceiptBatcher: BatcherType<{
|
||||
source?: string;
|
||||
sourceUuid?: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
RotateSignedPreKeyListener: WhatIsThis;
|
||||
|
||||
AlreadyGroupMemberToast: typeof window.Whisper.ToastView;
|
||||
|
|
Loading…
Reference in a new issue