More protobufjs migration

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

View File

@ -52,7 +52,6 @@ module.exports = grunt => {
libtextsecuretest: {
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',

View File

@ -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"
],

View File

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

View File

@ -17,7 +17,7 @@ const GroupChange = require('../../ts/groupChange');
const IndexedDB = require('./indexeddb');
const 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');

View File

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

View File

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

View File

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

View File

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

View File

@ -39,11 +39,9 @@
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="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. -->

View File

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

View File

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

View File

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

View File

@ -509,25 +509,6 @@ try {
// https://stackoverflow.com/a/23299989
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');

View File

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

View File

@ -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'
)

View File

@ -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,

View File

@ -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}
/>
);

View File

@ -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

View File

@ -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} />;

View File

@ -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 (

View File

@ -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(

View File

@ -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>

View File

@ -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);

View File

@ -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
View File

@ -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';

View File

@ -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;

View File

@ -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) =>

View File

@ -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
);

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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);
}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ describe('both/state/ducks/composer', () => {
conversationId: '123',
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);
});
});
});

View File

@ -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 });
});
});
});

View File

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

View File

@ -5,6 +5,7 @@ import { assert } from 'chai';
import * as sinon from 'sinon';
import { 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
View File

@ -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;

View File

@ -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'),
};
}
}

View File

@ -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');

View File

@ -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

View File

@ -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'),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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;
};

View File

@ -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);

View File

@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
});
}

View File

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

View File

@ -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

View File

@ -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;
}

View File

@ -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,
},
];
}

View File

@ -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) {

View File

@ -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."
}
]
]

View File

@ -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',

View File

@ -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,

View File

@ -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
View File

@ -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;