Migrate base ts to eslint
This commit is contained in:
parent
ad555ec8a9
commit
50378ed9bc
10 changed files with 475 additions and 409 deletions
|
@ -30,7 +30,6 @@ webpack.config.ts
|
||||||
|
|
||||||
# Temporarily ignored during TSLint transition
|
# Temporarily ignored during TSLint transition
|
||||||
# JIRA: DESKTOP-304
|
# JIRA: DESKTOP-304
|
||||||
ts/*.ts
|
|
||||||
ts/components/*.ts
|
ts/components/*.ts
|
||||||
ts/components/*.tsx
|
ts/components/*.tsx
|
||||||
ts/components/conversation/**
|
ts/components/conversation/**
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
|
||||||
|
|
||||||
import { debounce, reduce, uniq, without } from 'lodash';
|
import { debounce, reduce, uniq, without } from 'lodash';
|
||||||
import dataInterface from './sql/Client';
|
import dataInterface from './sql/Client';
|
||||||
import {
|
import {
|
||||||
|
@ -9,6 +7,8 @@ import {
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
|
|
||||||
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getAllConversations,
|
getAllConversations,
|
||||||
getAllGroupsInvolvingId,
|
getAllGroupsInvolvingId,
|
||||||
|
@ -22,7 +22,7 @@ const {
|
||||||
// We have to run this in background.js, after all backbone models and collections on
|
// We have to run this in background.js, after all backbone models and collections on
|
||||||
// Whisper.* have been created. Once those are in typescript we can use more reasonable
|
// Whisper.* have been created. Once those are in typescript we can use more reasonable
|
||||||
// require statements for referencing these things, giving us more flexibility here.
|
// require statements for referencing these things, giving us more flexibility here.
|
||||||
export function start() {
|
export function start(): void {
|
||||||
const conversations = new window.Whisper.ConversationCollection();
|
const conversations = new window.Whisper.ConversationCollection();
|
||||||
|
|
||||||
// This class is entirely designed to keep the app title, badge and tray icon updated.
|
// This class is entirely designed to keep the app title, badge and tray icon updated.
|
||||||
|
@ -70,7 +70,9 @@ export function start() {
|
||||||
|
|
||||||
export class ConversationController {
|
export class ConversationController {
|
||||||
_initialFetchComplete: boolean | undefined;
|
_initialFetchComplete: boolean | undefined;
|
||||||
|
|
||||||
_initialPromise: Promise<void> = Promise.resolve();
|
_initialPromise: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
_conversations: ConversationModelCollectionType;
|
_conversations: ConversationModelCollectionType;
|
||||||
|
|
||||||
constructor(conversations?: ConversationModelCollectionType) {
|
constructor(conversations?: ConversationModelCollectionType) {
|
||||||
|
@ -91,14 +93,18 @@ export class ConversationController {
|
||||||
// This function takes null just fine. Backbone typings are too restrictive.
|
// This function takes null just fine. Backbone typings are too restrictive.
|
||||||
return this._conversations.get(id as string);
|
return this._conversations.get(id as string);
|
||||||
}
|
}
|
||||||
dangerouslyCreateAndAdd(attributes: Partial<ConversationModelType>) {
|
|
||||||
|
dangerouslyCreateAndAdd(
|
||||||
|
attributes: Partial<ConversationModelType>
|
||||||
|
): ConversationModelType {
|
||||||
return this._conversations.add(attributes);
|
return this._conversations.add(attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrCreate(
|
getOrCreate(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
type: ConversationTypeType,
|
type: ConversationTypeType,
|
||||||
additionalInitialProps = {}
|
additionalInitialProps = {}
|
||||||
) {
|
): ConversationModelType {
|
||||||
if (typeof identifier !== 'string') {
|
if (typeof identifier !== 'string') {
|
||||||
throw new TypeError("'id' must be a string");
|
throw new TypeError("'id' must be a string");
|
||||||
}
|
}
|
||||||
|
@ -186,24 +192,24 @@ export class ConversationController {
|
||||||
|
|
||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateAndWait(
|
async getOrCreateAndWait(
|
||||||
id: string,
|
id: string,
|
||||||
type: ConversationTypeType,
|
type: ConversationTypeType,
|
||||||
additionalInitialProps = {}
|
additionalInitialProps = {}
|
||||||
) {
|
): Promise<ConversationModelType> {
|
||||||
return this._initialPromise.then(async () => {
|
await this._initialPromise;
|
||||||
const conversation = this.getOrCreate(id, type, additionalInitialProps);
|
const conversation = this.getOrCreate(id, type, additionalInitialProps);
|
||||||
|
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
return conversation.initialPromise.then(() => conversation);
|
await conversation.initialPromise;
|
||||||
}
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(
|
throw new Error('getOrCreateAndWait: did not get conversation');
|
||||||
new Error('getOrCreateAndWait: did not get conversation')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
getConversationId(address: string) {
|
|
||||||
|
getConversationId(address: string): string | null {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -217,11 +223,13 @@ export class ConversationController {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOurConversationId(): string | undefined {
|
getOurConversationId(): string | undefined {
|
||||||
const e164 = window.textsecure.storage.user.getNumber();
|
const e164 = window.textsecure.storage.user.getNumber();
|
||||||
const uuid = window.textsecure.storage.user.getUuid();
|
const uuid = window.textsecure.storage.user.getUuid();
|
||||||
return this.ensureContactIds({ e164, uuid, highTrust: true });
|
return this.ensureContactIds({ e164, uuid, highTrust: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a UUID and/or an E164, resolves to a string representing the local
|
* Given a UUID and/or an E164, resolves to a string representing the local
|
||||||
* database id of the given contact. In high trust mode, it may create new contacts,
|
* database id of the given contact. In high trust mode, it may create new contacts,
|
||||||
|
@ -272,7 +280,8 @@ export class ConversationController {
|
||||||
return newConvo.get('id');
|
return newConvo.get('id');
|
||||||
|
|
||||||
// 2. Handle match on only E164
|
// 2. Handle match on only E164
|
||||||
} else if (convoE164 && !convoUuid) {
|
}
|
||||||
|
if (convoE164 && !convoUuid) {
|
||||||
const haveUuid = Boolean(normalizedUuid);
|
const haveUuid = Boolean(normalizedUuid);
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`ensureContactIds: e164-only match found (have UUID: ${haveUuid})`
|
`ensureContactIds: e164-only match found (have UUID: ${haveUuid})`
|
||||||
|
@ -315,7 +324,8 @@ export class ConversationController {
|
||||||
return newConvo.get('id');
|
return newConvo.get('id');
|
||||||
|
|
||||||
// 3. Handle match on only UUID
|
// 3. Handle match on only UUID
|
||||||
} else if (!convoE164 && convoUuid) {
|
}
|
||||||
|
if (!convoE164 && convoUuid) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`ensureContactIds: UUID-only match found (have e164: ${Boolean(e164)})`
|
`ensureContactIds: UUID-only match found (have e164: ${Boolean(e164)})`
|
||||||
);
|
);
|
||||||
|
@ -363,6 +373,8 @@ export class ConversationController {
|
||||||
// Conflict: If e164 match has no UUID, we merge. We prefer the UUID match.
|
// Conflict: If e164 match has no UUID, we merge. We prefer the UUID match.
|
||||||
// Note: no await here, we want to keep this function synchronous
|
// Note: no await here, we want to keep this function synchronous
|
||||||
convoUuid.updateE164(e164);
|
convoUuid.updateE164(e164);
|
||||||
|
// `then` is used to trigger async updates, not affecting return value
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
this.combineContacts(convoUuid, convoE164)
|
this.combineContacts(convoUuid, convoE164)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// If the old conversation was currently displayed, we load the new one
|
// If the old conversation was currently displayed, we load the new one
|
||||||
|
@ -381,7 +393,8 @@ export class ConversationController {
|
||||||
|
|
||||||
return convoUuid.get('id');
|
return convoUuid.get('id');
|
||||||
}
|
}
|
||||||
async checkForConflicts() {
|
|
||||||
|
async checkForConflicts(): Promise<void> {
|
||||||
window.log.info('checkForConflicts: starting...');
|
window.log.info('checkForConflicts: starting...');
|
||||||
const byUuid = Object.create(null);
|
const byUuid = Object.create(null);
|
||||||
const byE164 = Object.create(null);
|
const byE164 = Object.create(null);
|
||||||
|
@ -465,10 +478,11 @@ export class ConversationController {
|
||||||
|
|
||||||
window.log.info('checkForConflicts: complete!');
|
window.log.info('checkForConflicts: complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
async combineContacts(
|
async combineContacts(
|
||||||
current: ConversationModelType,
|
current: ConversationModelType,
|
||||||
obsolete: ConversationModelType
|
obsolete: ConversationModelType
|
||||||
) {
|
): Promise<void> {
|
||||||
const obsoleteId = obsolete.get('id');
|
const obsoleteId = obsolete.get('id');
|
||||||
const currentId = current.get('id');
|
const currentId = current.get('id');
|
||||||
window.log.warn('combineContacts: Combining two conversations', {
|
window.log.warn('combineContacts: Combining two conversations', {
|
||||||
|
@ -541,6 +555,7 @@ export class ConversationController {
|
||||||
current: currentId,
|
current: currentId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a groupId and optional additional initialization properties,
|
* Given a groupId and optional additional initialization properties,
|
||||||
* ensures the existence of a group conversation and returns a string
|
* ensures the existence of a group conversation and returns a string
|
||||||
|
@ -549,16 +564,17 @@ export class ConversationController {
|
||||||
ensureGroup(groupId: string, additionalInitProps = {}): string {
|
ensureGroup(groupId: string, additionalInitProps = {}): string {
|
||||||
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
|
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given certain metadata about a message (an identifier of who wrote the
|
* Given certain metadata about a message (an identifier of who wrote the
|
||||||
* message and the sent_at timestamp of the message) returns the
|
* message and the sent_at timestamp of the message) returns the
|
||||||
* conversation the message belongs to OR null if a conversation isn't
|
* conversation the message belongs to OR null if a conversation isn't
|
||||||
* found.
|
* found.
|
||||||
*/
|
*/
|
||||||
async getConversationForTargetMessage(
|
static async getConversationForTargetMessage(
|
||||||
targetFromId: string,
|
targetFromId: string,
|
||||||
targetTimestamp: number
|
targetTimestamp: number
|
||||||
) {
|
): Promise<boolean | ConversationModelType | null | undefined> {
|
||||||
const messages = await getMessagesBySentAt(targetTimestamp, {
|
const messages = await getMessagesBySentAt(targetTimestamp, {
|
||||||
MessageCollection: window.Whisper.MessageCollection,
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
});
|
});
|
||||||
|
@ -579,11 +595,12 @@ export class ConversationController {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
prepareForSend(
|
|
||||||
|
prepareForSend<T>(
|
||||||
id: string,
|
id: string,
|
||||||
options?: any
|
options?: unknown
|
||||||
): {
|
): {
|
||||||
wrap: (promise: Promise<any>) => Promise<void>;
|
wrap: (promise: Promise<T>) => Promise<T>;
|
||||||
sendOptions: SendOptionsType | undefined;
|
sendOptions: SendOptionsType | undefined;
|
||||||
} {
|
} {
|
||||||
// id is any valid conversation identifier
|
// id is any valid conversation identifier
|
||||||
|
@ -593,10 +610,11 @@ export class ConversationController {
|
||||||
: undefined;
|
: undefined;
|
||||||
const wrap = conversation
|
const wrap = conversation
|
||||||
? conversation.wrapSend.bind(conversation)
|
? conversation.wrapSend.bind(conversation)
|
||||||
: async (promise: Promise<any>) => promise;
|
: async (promise: Promise<T>) => promise;
|
||||||
|
|
||||||
return { wrap, sendOptions };
|
return { wrap, sendOptions };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllGroupsInvolvingId(
|
async getAllGroupsInvolvingId(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): Promise<Array<ConversationModelType>> {
|
): Promise<Array<ConversationModelType>> {
|
||||||
|
@ -605,18 +623,22 @@ export class ConversationController {
|
||||||
});
|
});
|
||||||
return groups.map(group => this._conversations.add(group));
|
return groups.map(group => this._conversations.add(group));
|
||||||
}
|
}
|
||||||
async loadPromise() {
|
|
||||||
|
async loadPromise(): Promise<void> {
|
||||||
return this._initialPromise;
|
return this._initialPromise;
|
||||||
}
|
}
|
||||||
reset() {
|
|
||||||
|
reset(): void {
|
||||||
this._initialPromise = Promise.resolve();
|
this._initialPromise = Promise.resolve();
|
||||||
this._initialFetchComplete = false;
|
this._initialFetchComplete = false;
|
||||||
this._conversations.reset([]);
|
this._conversations.reset([]);
|
||||||
}
|
}
|
||||||
isFetchComplete() {
|
|
||||||
|
isFetchComplete(): boolean | undefined {
|
||||||
return this._initialFetchComplete;
|
return this._initialFetchComplete;
|
||||||
}
|
}
|
||||||
async load() {
|
|
||||||
|
async load(): Promise<void> {
|
||||||
window.log.info('ConversationController: starting initial fetch');
|
window.log.info('ConversationController: starting initial fetch');
|
||||||
|
|
||||||
if (this._conversations.length) {
|
if (this._conversations.length) {
|
||||||
|
|
149
ts/Crypto.ts
149
ts/Crypto.ts
|
@ -1,55 +1,47 @@
|
||||||
import pProps from 'p-props';
|
import pProps from 'p-props';
|
||||||
|
|
||||||
// Yep, we're doing some bitwise stuff in an encryption-related file
|
|
||||||
// tslint:disable no-bitwise
|
|
||||||
|
|
||||||
// We want some extra variables to make the decrption algorithm easier to understand
|
|
||||||
// tslint:disable no-unnecessary-local-variable
|
|
||||||
|
|
||||||
// Seems that tslint doesn't understand that crypto.subtle.importKey does return a Promise
|
|
||||||
// tslint:disable await-promise
|
|
||||||
|
|
||||||
export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
|
export function typedArrayToArrayBuffer(typedArray: Uint8Array): ArrayBuffer {
|
||||||
const { buffer, byteOffset, byteLength } = typedArray;
|
const { buffer, byteOffset, byteLength } = typedArray;
|
||||||
|
|
||||||
// tslint:disable-next-line no-unnecessary-type-assertion
|
|
||||||
return buffer.slice(byteOffset, byteLength + byteOffset) as typeof typedArray;
|
return buffer.slice(byteOffset, byteLength + byteOffset) as typeof typedArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) {
|
export function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayBufferToHex(arrayBuffer: ArrayBuffer) {
|
export function arrayBufferToHex(arrayBuffer: ArrayBuffer): string {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('hex');
|
return window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64ToArrayBuffer(base64string: string) {
|
export function base64ToArrayBuffer(base64string: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
return window.dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hexToArrayBuffer(hexString: string) {
|
export function hexToArrayBuffer(hexString: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(hexString, 'hex').toArrayBuffer();
|
return window.dcodeIO.ByteBuffer.wrap(hexString, 'hex').toArrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromEncodedBinaryToArrayBuffer(key: string) {
|
export function fromEncodedBinaryToArrayBuffer(key: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
return window.dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bytesFromString(string: string) {
|
export function bytesFromString(string: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
return window.dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
||||||
}
|
}
|
||||||
export function stringFromBytes(buffer: ArrayBuffer) {
|
export function stringFromBytes(buffer: ArrayBuffer): string {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||||
}
|
}
|
||||||
export function hexFromBytes(buffer: ArrayBuffer) {
|
export function hexFromBytes(buffer: ArrayBuffer): string {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
|
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
|
||||||
}
|
}
|
||||||
export function bytesFromHexString(string: string) {
|
export function bytesFromHexString(string: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deriveStickerPackKey(packKey: ArrayBuffer) {
|
export async function deriveStickerPackKey(
|
||||||
|
packKey: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
const salt = getZeroes(32);
|
const salt = getZeroes(32);
|
||||||
const info = bytesFromString('Sticker Pack');
|
const info = bytesFromString('Sticker Pack');
|
||||||
|
|
||||||
|
@ -72,7 +64,7 @@ export async function computeHash(data: ArrayBuffer): Promise<string> {
|
||||||
export async function encryptDeviceName(
|
export async function encryptDeviceName(
|
||||||
deviceName: string,
|
deviceName: string,
|
||||||
identityPublic: ArrayBuffer
|
identityPublic: ArrayBuffer
|
||||||
) {
|
): Promise<Record<string, ArrayBuffer>> {
|
||||||
const plaintext = bytesFromString(deviceName);
|
const plaintext = bytesFromString(deviceName);
|
||||||
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
|
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
|
||||||
const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
|
const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
|
||||||
|
@ -107,7 +99,7 @@ export async function decryptDeviceName(
|
||||||
ciphertext: ArrayBuffer;
|
ciphertext: ArrayBuffer;
|
||||||
},
|
},
|
||||||
identityPrivate: ArrayBuffer
|
identityPrivate: ArrayBuffer
|
||||||
) {
|
): Promise<string> {
|
||||||
const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
|
const masterSecret = await window.libsignal.Curve.async.calculateAgreement(
|
||||||
ephemeralPublic,
|
ephemeralPublic,
|
||||||
identityPrivate
|
identityPrivate
|
||||||
|
@ -130,7 +122,7 @@ export async function decryptDeviceName(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'
|
// Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'
|
||||||
export function getAttachmentLabel(path: string) {
|
export function getAttachmentLabel(path: string): ArrayBuffer {
|
||||||
const filename = path.slice(3);
|
const filename = path.slice(3);
|
||||||
|
|
||||||
return base64ToArrayBuffer(filename);
|
return base64ToArrayBuffer(filename);
|
||||||
|
@ -141,7 +133,7 @@ export async function encryptAttachment(
|
||||||
staticPublicKey: ArrayBuffer,
|
staticPublicKey: ArrayBuffer,
|
||||||
path: string,
|
path: string,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const uniqueId = getAttachmentLabel(path);
|
const uniqueId = getAttachmentLabel(path);
|
||||||
|
|
||||||
return encryptFile(staticPublicKey, uniqueId, plaintext);
|
return encryptFile(staticPublicKey, uniqueId, plaintext);
|
||||||
|
@ -151,7 +143,7 @@ export async function decryptAttachment(
|
||||||
staticPrivateKey: ArrayBuffer,
|
staticPrivateKey: ArrayBuffer,
|
||||||
path: string,
|
path: string,
|
||||||
data: ArrayBuffer
|
data: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const uniqueId = getAttachmentLabel(path);
|
const uniqueId = getAttachmentLabel(path);
|
||||||
|
|
||||||
return decryptFile(staticPrivateKey, uniqueId, data);
|
return decryptFile(staticPrivateKey, uniqueId, data);
|
||||||
|
@ -161,7 +153,7 @@ export async function encryptFile(
|
||||||
staticPublicKey: ArrayBuffer,
|
staticPublicKey: ArrayBuffer,
|
||||||
uniqueId: ArrayBuffer,
|
uniqueId: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
|
const ephemeralKeyPair = await window.libsignal.KeyHelper.generateIdentityKeyPair();
|
||||||
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
||||||
staticPublicKey,
|
staticPublicKey,
|
||||||
|
@ -178,7 +170,7 @@ export async function decryptFile(
|
||||||
staticPrivateKey: ArrayBuffer,
|
staticPrivateKey: ArrayBuffer,
|
||||||
uniqueId: ArrayBuffer,
|
uniqueId: ArrayBuffer,
|
||||||
data: ArrayBuffer
|
data: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH);
|
const ephemeralPublicKey = getFirstBytes(data, PUB_KEY_LENGTH);
|
||||||
const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength);
|
const ciphertext = getBytes(data, PUB_KEY_LENGTH, data.byteLength);
|
||||||
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
const agreement = await window.libsignal.Curve.async.calculateAgreement(
|
||||||
|
@ -194,18 +186,20 @@ export async function decryptFile(
|
||||||
export async function deriveStorageManifestKey(
|
export async function deriveStorageManifestKey(
|
||||||
storageServiceKey: ArrayBuffer,
|
storageServiceKey: ArrayBuffer,
|
||||||
version: number
|
version: number
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
return hmacSha256(storageServiceKey, bytesFromString(`Manifest_${version}`));
|
return hmacSha256(storageServiceKey, bytesFromString(`Manifest_${version}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deriveStorageItemKey(
|
export async function deriveStorageItemKey(
|
||||||
storageServiceKey: ArrayBuffer,
|
storageServiceKey: ArrayBuffer,
|
||||||
itemID: string
|
itemID: string
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
return hmacSha256(storageServiceKey, bytesFromString(`Item_${itemID}`));
|
return hmacSha256(storageServiceKey, bytesFromString(`Item_${itemID}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deriveAccessKey(profileKey: ArrayBuffer) {
|
export async function deriveAccessKey(
|
||||||
|
profileKey: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
const iv = getZeroes(12);
|
const iv = getZeroes(12);
|
||||||
const plaintext = getZeroes(16);
|
const plaintext = getZeroes(16);
|
||||||
const accessKey = await encryptAesGcm(profileKey, iv, plaintext);
|
const accessKey = await encryptAesGcm(profileKey, iv, plaintext);
|
||||||
|
@ -213,7 +207,9 @@ export async function deriveAccessKey(profileKey: ArrayBuffer) {
|
||||||
return getFirstBytes(accessKey, 16);
|
return getFirstBytes(accessKey, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAccessKeyVerifier(accessKey: ArrayBuffer) {
|
export async function getAccessKeyVerifier(
|
||||||
|
accessKey: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
const plaintext = getZeroes(32);
|
const plaintext = getZeroes(32);
|
||||||
|
|
||||||
return hmacSha256(accessKey, plaintext);
|
return hmacSha256(accessKey, plaintext);
|
||||||
|
@ -222,7 +218,7 @@ export async function getAccessKeyVerifier(accessKey: ArrayBuffer) {
|
||||||
export async function verifyAccessKey(
|
export async function verifyAccessKey(
|
||||||
accessKey: ArrayBuffer,
|
accessKey: ArrayBuffer,
|
||||||
theirVerifier: ArrayBuffer
|
theirVerifier: ArrayBuffer
|
||||||
) {
|
): Promise<boolean> {
|
||||||
const ourVerifier = await getAccessKeyVerifier(accessKey);
|
const ourVerifier = await getAccessKeyVerifier(accessKey);
|
||||||
|
|
||||||
if (constantTimeEqual(ourVerifier, theirVerifier)) {
|
if (constantTimeEqual(ourVerifier, theirVerifier)) {
|
||||||
|
@ -239,14 +235,14 @@ const NONCE_LENGTH = 16;
|
||||||
export async function encryptSymmetric(
|
export async function encryptSymmetric(
|
||||||
key: ArrayBuffer,
|
key: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const iv = getZeroes(IV_LENGTH);
|
const iv = getZeroes(IV_LENGTH);
|
||||||
const nonce = getRandomBytes(NONCE_LENGTH);
|
const nonce = getRandomBytes(NONCE_LENGTH);
|
||||||
|
|
||||||
const cipherKey = await hmacSha256(key, nonce);
|
const cipherKey = await hmacSha256(key, nonce);
|
||||||
const macKey = await hmacSha256(key, cipherKey);
|
const macKey = await hmacSha256(key, cipherKey);
|
||||||
|
|
||||||
const cipherText = await _encrypt_aes256_CBC_PKCSPadding(
|
const cipherText = await _encryptAes256CbcPkcsPadding(
|
||||||
cipherKey,
|
cipherKey,
|
||||||
iv,
|
iv,
|
||||||
plaintext
|
plaintext
|
||||||
|
@ -256,7 +252,10 @@ export async function encryptSymmetric(
|
||||||
return concatenateBytes(nonce, cipherText, mac);
|
return concatenateBytes(nonce, cipherText, mac);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) {
|
export async function decryptSymmetric(
|
||||||
|
key: ArrayBuffer,
|
||||||
|
data: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
const iv = getZeroes(IV_LENGTH);
|
const iv = getZeroes(IV_LENGTH);
|
||||||
|
|
||||||
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
const nonce = getFirstBytes(data, NONCE_LENGTH);
|
||||||
|
@ -280,10 +279,13 @@ export async function decryptSymmetric(key: ArrayBuffer, data: ArrayBuffer) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText);
|
return _decryptAes256CbcPkcsPadding(cipherKey, iv, cipherText);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function constantTimeEqual(left: ArrayBuffer, right: ArrayBuffer) {
|
export function constantTimeEqual(
|
||||||
|
left: ArrayBuffer,
|
||||||
|
right: ArrayBuffer
|
||||||
|
): boolean {
|
||||||
if (left.byteLength !== right.byteLength) {
|
if (left.byteLength !== right.byteLength) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -301,8 +303,11 @@ export function constantTimeEqual(left: ArrayBuffer, right: ArrayBuffer) {
|
||||||
|
|
||||||
// Encryption
|
// Encryption
|
||||||
|
|
||||||
export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) {
|
export async function hmacSha256(
|
||||||
const algorithm = {
|
key: ArrayBuffer,
|
||||||
|
plaintext: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const algorithm: HmacImportParams = {
|
||||||
name: 'HMAC',
|
name: 'HMAC',
|
||||||
hash: 'SHA-256',
|
hash: 'SHA-256',
|
||||||
};
|
};
|
||||||
|
@ -311,7 +316,7 @@ export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) {
|
||||||
const cryptoKey = await window.crypto.subtle.importKey(
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
algorithm as any,
|
algorithm,
|
||||||
extractable,
|
extractable,
|
||||||
['sign']
|
['sign']
|
||||||
);
|
);
|
||||||
|
@ -319,11 +324,11 @@ export async function hmacSha256(key: ArrayBuffer, plaintext: ArrayBuffer) {
|
||||||
return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext);
|
return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _encrypt_aes256_CBC_PKCSPadding(
|
export async function _encryptAes256CbcPkcsPadding(
|
||||||
key: ArrayBuffer,
|
key: ArrayBuffer,
|
||||||
iv: ArrayBuffer,
|
iv: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-CBC',
|
name: 'AES-CBC',
|
||||||
iv,
|
iv,
|
||||||
|
@ -333,6 +338,10 @@ export async function _encrypt_aes256_CBC_PKCSPadding(
|
||||||
const cryptoKey = await window.crypto.subtle.importKey(
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
|
// `algorithm` appears to be an instance of AesCbcParams,
|
||||||
|
// which is not in the param's types, so we need to pass as `any`.
|
||||||
|
// TODO: just pass the string "AES-CBC", per the docs?
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
algorithm as any,
|
algorithm as any,
|
||||||
extractable,
|
extractable,
|
||||||
['encrypt']
|
['encrypt']
|
||||||
|
@ -341,11 +350,11 @@ export async function _encrypt_aes256_CBC_PKCSPadding(
|
||||||
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function _decrypt_aes256_CBC_PKCSPadding(
|
export async function _decryptAes256CbcPkcsPadding(
|
||||||
key: ArrayBuffer,
|
key: ArrayBuffer,
|
||||||
iv: ArrayBuffer,
|
iv: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer
|
plaintext: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-CBC',
|
name: 'AES-CBC',
|
||||||
iv,
|
iv,
|
||||||
|
@ -355,6 +364,10 @@ export async function _decrypt_aes256_CBC_PKCSPadding(
|
||||||
const cryptoKey = await window.crypto.subtle.importKey(
|
const cryptoKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
|
// `algorithm` appears to be an instance of AesCbcParams,
|
||||||
|
// which is not in the param's types, so we need to pass as `any`.
|
||||||
|
// TODO: just pass the string "AES-CBC", per the docs?
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
algorithm as any,
|
algorithm as any,
|
||||||
extractable,
|
extractable,
|
||||||
['decrypt']
|
['decrypt']
|
||||||
|
@ -367,7 +380,7 @@ export async function encryptAesCtr(
|
||||||
key: ArrayBuffer,
|
key: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer,
|
plaintext: ArrayBuffer,
|
||||||
counter: ArrayBuffer
|
counter: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const extractable = false;
|
const extractable = false;
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-CTR',
|
name: 'AES-CTR',
|
||||||
|
@ -378,6 +391,10 @@ export async function encryptAesCtr(
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
|
// `algorithm` appears to be an instance of AesCtrParams,
|
||||||
|
// which is not in the param's types, so we need to pass as `any`.
|
||||||
|
// TODO: just pass the string "AES-CTR", per the docs?
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
algorithm as any,
|
algorithm as any,
|
||||||
extractable,
|
extractable,
|
||||||
['encrypt']
|
['encrypt']
|
||||||
|
@ -396,7 +413,7 @@ export async function decryptAesCtr(
|
||||||
key: ArrayBuffer,
|
key: ArrayBuffer,
|
||||||
ciphertext: ArrayBuffer,
|
ciphertext: ArrayBuffer,
|
||||||
counter: ArrayBuffer
|
counter: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const extractable = false;
|
const extractable = false;
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-CTR',
|
name: 'AES-CTR',
|
||||||
|
@ -407,6 +424,10 @@ export async function decryptAesCtr(
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
|
// `algorithm` appears to be an instance of AesCtrParams,
|
||||||
|
// which is not in the param's types, so we need to pass as `any`.
|
||||||
|
// TODO: just pass the string "AES-CTR", per the docs?
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
algorithm as any,
|
algorithm as any,
|
||||||
extractable,
|
extractable,
|
||||||
['decrypt']
|
['decrypt']
|
||||||
|
@ -425,7 +446,7 @@ export async function encryptAesGcm(
|
||||||
iv: ArrayBuffer,
|
iv: ArrayBuffer,
|
||||||
plaintext: ArrayBuffer,
|
plaintext: ArrayBuffer,
|
||||||
additionalData?: ArrayBuffer
|
additionalData?: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv,
|
iv,
|
||||||
|
@ -437,6 +458,10 @@ export async function encryptAesGcm(
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
|
// `algorithm` appears to be an instance of AesGcmParams,
|
||||||
|
// which is not in the param's types, so we need to pass as `any`.
|
||||||
|
// TODO: just pass the string "AES-GCM", per the docs?
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
algorithm as any,
|
algorithm as any,
|
||||||
extractable,
|
extractable,
|
||||||
['encrypt']
|
['encrypt']
|
||||||
|
@ -450,7 +475,7 @@ export async function decryptAesGcm(
|
||||||
iv: ArrayBuffer,
|
iv: ArrayBuffer,
|
||||||
ciphertext: ArrayBuffer,
|
ciphertext: ArrayBuffer,
|
||||||
additionalData?: ArrayBuffer
|
additionalData?: ArrayBuffer
|
||||||
) {
|
): Promise<ArrayBuffer> {
|
||||||
const algorithm = {
|
const algorithm = {
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv,
|
iv,
|
||||||
|
@ -462,6 +487,10 @@ export async function decryptAesGcm(
|
||||||
const cryptoKey = await crypto.subtle.importKey(
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
key,
|
key,
|
||||||
|
// `algorithm` appears to be an instance of AesGcmParams,
|
||||||
|
// which is not in the param's types, so we need to pass as `any`.
|
||||||
|
// TODO: just pass the string "AES-GCM", per the docs?
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
algorithm as any,
|
algorithm as any,
|
||||||
extractable,
|
extractable,
|
||||||
['decrypt']
|
['decrypt']
|
||||||
|
@ -472,13 +501,13 @@ export async function decryptAesGcm(
|
||||||
|
|
||||||
// Hashing
|
// Hashing
|
||||||
|
|
||||||
export async function sha256(data: ArrayBuffer) {
|
export async function sha256(data: ArrayBuffer): Promise<ArrayBuffer> {
|
||||||
return crypto.subtle.digest('SHA-256', data);
|
return crypto.subtle.digest('SHA-256', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
|
|
||||||
export function getRandomBytes(n: number) {
|
export function getRandomBytes(n: number): ArrayBuffer {
|
||||||
const bytes = new Uint8Array(n);
|
const bytes = new Uint8Array(n);
|
||||||
window.crypto.getRandomValues(bytes);
|
window.crypto.getRandomValues(bytes);
|
||||||
|
|
||||||
|
@ -496,7 +525,7 @@ export function getRandomValue(low: number, high: number): number {
|
||||||
return (bytes[0] % mod) + low;
|
return (bytes[0] % mod) + low;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getZeroes(n: number) {
|
export function getZeroes(n: number): ArrayBuffer {
|
||||||
const result = new Uint8Array(n);
|
const result = new Uint8Array(n);
|
||||||
|
|
||||||
const value = 0;
|
const value = 0;
|
||||||
|
@ -508,6 +537,7 @@ export function getZeroes(n: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highBitsToInt(byte: number): number {
|
export function highBitsToInt(byte: number): number {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
return (byte & 0xff) >> 4;
|
return (byte & 0xff) >> 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -515,10 +545,11 @@ export function intsToByteHighAndLow(
|
||||||
highValue: number,
|
highValue: number,
|
||||||
lowValue: number
|
lowValue: number
|
||||||
): number {
|
): number {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
return ((highValue << 4) | lowValue) & 0xff;
|
return ((highValue << 4) | lowValue) & 0xff;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trimBytes(buffer: ArrayBuffer, length: number) {
|
export function trimBytes(buffer: ArrayBuffer, length: number): ArrayBuffer {
|
||||||
return getFirstBytes(buffer, length);
|
return getFirstBytes(buffer, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,14 +557,16 @@ export function getViewOfArrayBuffer(
|
||||||
buffer: ArrayBuffer,
|
buffer: ArrayBuffer,
|
||||||
start: number,
|
start: number,
|
||||||
finish: number
|
finish: number
|
||||||
) {
|
): ArrayBuffer | SharedArrayBuffer {
|
||||||
const source = new Uint8Array(buffer);
|
const source = new Uint8Array(buffer);
|
||||||
const result = source.slice(start, finish);
|
const result = source.slice(start, finish);
|
||||||
|
|
||||||
return result.buffer;
|
return result.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function concatenateBytes(...elements: Array<ArrayBuffer | Uint8Array>) {
|
export function concatenateBytes(
|
||||||
|
...elements: Array<ArrayBuffer | Uint8Array>
|
||||||
|
): ArrayBuffer {
|
||||||
const length = elements.reduce(
|
const length = elements.reduce(
|
||||||
(total, element) => total + element.byteLength,
|
(total, element) => total + element.byteLength,
|
||||||
0
|
0
|
||||||
|
@ -585,7 +618,7 @@ export function splitBytes(
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFirstBytes(data: ArrayBuffer, n: number) {
|
export function getFirstBytes(data: ArrayBuffer, n: number): ArrayBuffer {
|
||||||
const source = new Uint8Array(data);
|
const source = new Uint8Array(data);
|
||||||
|
|
||||||
return typedArrayToArrayBuffer(source.subarray(0, n));
|
return typedArrayToArrayBuffer(source.subarray(0, n));
|
||||||
|
@ -595,7 +628,7 @@ export function getBytes(
|
||||||
data: ArrayBuffer | Uint8Array,
|
data: ArrayBuffer | Uint8Array,
|
||||||
start: number,
|
start: number,
|
||||||
n: number
|
n: number
|
||||||
) {
|
): ArrayBuffer {
|
||||||
const source = new Uint8Array(data);
|
const source = new Uint8Array(data);
|
||||||
|
|
||||||
return typedArrayToArrayBuffer(source.subarray(start, start + n));
|
return typedArrayToArrayBuffer(source.subarray(start, start + n));
|
||||||
|
@ -614,7 +647,7 @@ export async function encryptCdsDiscoveryRequest(
|
||||||
[key: string]: { clientKey: ArrayBuffer; requestId: ArrayBuffer };
|
[key: string]: { clientKey: ArrayBuffer; requestId: ArrayBuffer };
|
||||||
},
|
},
|
||||||
phoneNumbers: ReadonlyArray<string>
|
phoneNumbers: ReadonlyArray<string>
|
||||||
) {
|
): Promise<Record<string, unknown>> {
|
||||||
const nonce = getRandomBytes(32);
|
const nonce = getRandomBytes(32);
|
||||||
const numbersArray = new window.dcodeIO.ByteBuffer(
|
const numbersArray = new window.dcodeIO.ByteBuffer(
|
||||||
phoneNumbers.length * 8,
|
phoneNumbers.length * 8,
|
||||||
|
@ -669,7 +702,7 @@ export async function encryptCdsDiscoveryRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitUuids(arrayBuffer: ArrayBuffer) {
|
export function splitUuids(arrayBuffer: ArrayBuffer): Array<string | null> {
|
||||||
const uuids = [];
|
const uuids = [];
|
||||||
for (let i = 0; i < arrayBuffer.byteLength; i += 16) {
|
for (let i = 0; i < arrayBuffer.byteLength; i += 16) {
|
||||||
const bytes = getBytes(arrayBuffer, i, 16);
|
const bytes = getBytes(arrayBuffer, i, 16);
|
||||||
|
|
6
ts/OS.ts
6
ts/OS.ts
|
@ -2,9 +2,9 @@ import is from '@sindresorhus/is';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
export const isMacOS = () => process.platform === 'darwin';
|
export const isMacOS = (): boolean => process.platform === 'darwin';
|
||||||
export const isLinux = () => process.platform === 'linux';
|
export const isLinux = (): boolean => process.platform === 'linux';
|
||||||
export const isWindows = (minVersion?: string) => {
|
export const isWindows = (minVersion?: string): boolean => {
|
||||||
const osRelease = os.release();
|
const osRelease = os.release();
|
||||||
|
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
|
|
|
@ -34,12 +34,15 @@ function getServer(): WebAPIType {
|
||||||
let config: ConfigMapType = {};
|
let config: ConfigMapType = {};
|
||||||
const listeners: ConfigListenersMapType = {};
|
const listeners: ConfigListenersMapType = {};
|
||||||
|
|
||||||
export async function initRemoteConfig() {
|
export async function initRemoteConfig(): Promise<void> {
|
||||||
config = window.storage.get('remoteConfig') || {};
|
config = window.storage.get('remoteConfig') || {};
|
||||||
await maybeRefreshRemoteConfig();
|
await maybeRefreshRemoteConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onChange(key: ConfigKeyType, fn: ConfigListenerType) {
|
export function onChange(
|
||||||
|
key: ConfigKeyType,
|
||||||
|
fn: ConfigListenerType
|
||||||
|
): () => void {
|
||||||
const keyListeners: Array<ConfigListenerType> = get(listeners, key, []);
|
const keyListeners: Array<ConfigListenerType> = get(listeners, key, []);
|
||||||
keyListeners.push(fn);
|
keyListeners.push(fn);
|
||||||
listeners[key] = keyListeners;
|
listeners[key] = keyListeners;
|
||||||
|
@ -49,7 +52,7 @@ export function onChange(key: ConfigKeyType, fn: ConfigListenerType) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const refreshRemoteConfig = async () => {
|
export const refreshRemoteConfig = async (): Promise<void> => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const server = getServer();
|
const server = getServer();
|
||||||
const newConfig = await server.getConfig();
|
const newConfig = await server.getConfig();
|
||||||
|
@ -61,7 +64,8 @@ export const refreshRemoteConfig = async () => {
|
||||||
config = newConfig.reduce((acc, { name, enabled, value }) => {
|
config = newConfig.reduce((acc, { name, enabled, value }) => {
|
||||||
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
||||||
const previousValue: unknown = get(oldConfig, [name, 'value'], undefined);
|
const previousValue: unknown = get(oldConfig, [name, 'value'], undefined);
|
||||||
// If a flag was previously not enabled and is now enabled, record the time it was enabled
|
// If a flag was previously not enabled and is now enabled,
|
||||||
|
// record the time it was enabled
|
||||||
const enabledAt: number | undefined =
|
const enabledAt: number | undefined =
|
||||||
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export type RenderOptionsType = {
|
||||||
export function renderChange(
|
export function renderChange(
|
||||||
change: GroupV2ChangeType,
|
change: GroupV2ChangeType,
|
||||||
options: RenderOptionsType
|
options: RenderOptionsType
|
||||||
) {
|
): Array<FullJSXType> {
|
||||||
const { details, from } = change;
|
const { details, from } = change;
|
||||||
|
|
||||||
return details.map((detail: GroupV2ChangeDetailType) =>
|
return details.map((detail: GroupV2ChangeDetailType) =>
|
||||||
|
@ -37,7 +37,6 @@ export function renderChange(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
|
||||||
export function renderChangeDetail(
|
export function renderChangeDetail(
|
||||||
detail: GroupV2ChangeDetailType,
|
detail: GroupV2ChangeDetailType,
|
||||||
options: RenderOptionsType
|
options: RenderOptionsType
|
||||||
|
@ -59,108 +58,103 @@ export function renderChangeDetail(
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--title--change--you', i18n, [newTitle]);
|
return renderString('GroupV2--title--change--you', i18n, [newTitle]);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--title--change--other', i18n, {
|
return renderString('GroupV2--title--change--other', i18n, {
|
||||||
memberName: renderContact(from),
|
memberName: renderContact(from),
|
||||||
newTitle,
|
newTitle,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--title--change--unknown', i18n, [
|
|
||||||
newTitle,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString('GroupV2--title--remove--you', i18n);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString('GroupV2--title--remove--other', i18n, [
|
|
||||||
renderContact(from),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--title--remove--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--title--change--unknown', i18n, [newTitle]);
|
||||||
}
|
}
|
||||||
} else if (detail.type === 'avatar') {
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--title--remove--you', i18n);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--title--remove--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return renderString('GroupV2--title--remove--unknown', i18n);
|
||||||
|
}
|
||||||
|
if (detail.type === 'avatar') {
|
||||||
if (detail.removed) {
|
if (detail.removed) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--avatar--remove--you', i18n);
|
return renderString('GroupV2--avatar--remove--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--avatar--remove--other', i18n, [
|
return renderString('GroupV2--avatar--remove--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--avatar--remove--unknown', i18n);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString('GroupV2--avatar--change--you', i18n);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString('GroupV2--avatar--change--other', i18n, [
|
|
||||||
renderContact(from),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--avatar--change--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--avatar--remove--unknown', i18n);
|
||||||
}
|
}
|
||||||
} else if (detail.type === 'access-attributes') {
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--avatar--change--you', i18n);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--avatar--change--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return renderString('GroupV2--avatar--change--unknown', i18n);
|
||||||
|
}
|
||||||
|
if (detail.type === 'access-attributes') {
|
||||||
const { newPrivilege } = detail;
|
const { newPrivilege } = detail;
|
||||||
|
|
||||||
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--access-attributes--admins--you', i18n);
|
return renderString('GroupV2--access-attributes--admins--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--access-attributes--admins--other', i18n, [
|
return renderString('GroupV2--access-attributes--admins--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--access-attributes--admins--unknown',
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (newPrivilege === AccessControlEnum.MEMBER) {
|
return renderString('GroupV2--access-attributes--admins--unknown', i18n);
|
||||||
|
}
|
||||||
|
if (newPrivilege === AccessControlEnum.MEMBER) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--access-attributes--all--you', i18n);
|
return renderString('GroupV2--access-attributes--all--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--access-attributes--all--other', i18n, [
|
return renderString('GroupV2--access-attributes--all--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--access-attributes--all--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
} else {
|
return renderString('GroupV2--access-attributes--all--unknown', i18n);
|
||||||
throw new Error(
|
|
||||||
`access-attributes change type, privilege ${newPrivilege} is unknown`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
|
`access-attributes change type, privilege ${newPrivilege} is unknown`
|
||||||
|
);
|
||||||
} else if (detail.type === 'access-members') {
|
} else if (detail.type === 'access-members') {
|
||||||
const { newPrivilege } = detail;
|
const { newPrivilege } = detail;
|
||||||
|
|
||||||
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--access-members--admins--you', i18n);
|
return renderString('GroupV2--access-members--admins--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--access-members--admins--other', i18n, [
|
return renderString('GroupV2--access-members--admins--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--access-members--admins--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
} else if (newPrivilege === AccessControlEnum.MEMBER) {
|
return renderString('GroupV2--access-members--admins--unknown', i18n);
|
||||||
|
}
|
||||||
|
if (newPrivilege === AccessControlEnum.MEMBER) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--access-members--all--you', i18n);
|
return renderString('GroupV2--access-members--all--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--access-members--all--other', i18n, [
|
return renderString('GroupV2--access-members--all--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--access-members--all--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
} else {
|
return renderString('GroupV2--access-members--all--unknown', i18n);
|
||||||
throw new Error(
|
|
||||||
`access-members change type, privilege ${newPrivilege} is unknown`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
|
`access-members change type, privilege ${newPrivilege} is unknown`
|
||||||
|
);
|
||||||
} else if (detail.type === 'member-add') {
|
} else if (detail.type === 'member-add') {
|
||||||
const { conversationId } = detail;
|
const { conversationId } = detail;
|
||||||
const weAreJoiner = conversationId === ourConversationId;
|
const weAreJoiner = conversationId === ourConversationId;
|
||||||
|
@ -168,29 +162,28 @@ export function renderChangeDetail(
|
||||||
if (weAreJoiner) {
|
if (weAreJoiner) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--member-add--you--you', i18n);
|
return renderString('GroupV2--member-add--you--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--member-add--you--other', i18n, [
|
return renderString('GroupV2--member-add--you--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--member-add--you--unknown', i18n);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString('GroupV2--member-add--other--you', i18n, [
|
|
||||||
renderContact(conversationId),
|
|
||||||
]);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString('GroupV2--member-add--other--other', i18n, {
|
|
||||||
adderName: renderContact(from),
|
|
||||||
addeeName: renderContact(conversationId),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--member-add--other--unknown', i18n, [
|
|
||||||
renderContact(conversationId),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--member-add--you--unknown', i18n);
|
||||||
}
|
}
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--member-add--other--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--member-add--other--other', i18n, {
|
||||||
|
adderName: renderContact(from),
|
||||||
|
addeeName: renderContact(conversationId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return renderString('GroupV2--member-add--other--unknown', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
} else if (detail.type === 'member-add-from-invite') {
|
} else if (detail.type === 'member-add-from-invite') {
|
||||||
const { conversationId, inviter } = detail;
|
const { conversationId, inviter } = detail;
|
||||||
const weAreJoiner = conversationId === ourConversationId;
|
const weAreJoiner = conversationId === ourConversationId;
|
||||||
|
@ -200,16 +193,16 @@ export function renderChangeDetail(
|
||||||
return renderString('GroupV2--member-add--from-invite--you', i18n, [
|
return renderString('GroupV2--member-add--from-invite--you', i18n, [
|
||||||
renderContact(inviter),
|
renderContact(inviter),
|
||||||
]);
|
]);
|
||||||
} else if (weAreInviter) {
|
}
|
||||||
|
if (weAreInviter) {
|
||||||
return renderString('GroupV2--member-add--from-invite--from-you', i18n, [
|
return renderString('GroupV2--member-add--from-invite--from-you', i18n, [
|
||||||
renderContact(conversationId),
|
renderContact(conversationId),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--member-add--from-invite--other', i18n, {
|
|
||||||
inviteeName: renderContact(conversationId),
|
|
||||||
inviterName: renderContact(inviter),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--member-add--from-invite--other', i18n, {
|
||||||
|
inviteeName: renderContact(conversationId),
|
||||||
|
inviterName: renderContact(inviter),
|
||||||
|
});
|
||||||
} else if (detail.type === 'member-remove') {
|
} else if (detail.type === 'member-remove') {
|
||||||
const { conversationId } = detail;
|
const { conversationId } = detail;
|
||||||
const weAreLeaver = conversationId === ourConversationId;
|
const weAreLeaver = conversationId === ourConversationId;
|
||||||
|
@ -217,33 +210,34 @@ export function renderChangeDetail(
|
||||||
if (weAreLeaver) {
|
if (weAreLeaver) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString('GroupV2--member-remove--you--you', i18n);
|
return renderString('GroupV2--member-remove--you--you', i18n);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--member-remove--you--other', i18n, [
|
return renderString('GroupV2--member-remove--you--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--member-remove--you--unknown', i18n);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString('GroupV2--member-remove--other--you', i18n, [
|
|
||||||
renderContact(conversationId),
|
|
||||||
]);
|
|
||||||
} else if (from && from === conversationId) {
|
|
||||||
return renderString('GroupV2--member-remove--other--self', i18n, [
|
|
||||||
renderContact(from),
|
|
||||||
]);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString('GroupV2--member-remove--other--other', i18n, {
|
|
||||||
adminName: renderContact(from),
|
|
||||||
memberName: renderContact(conversationId),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--member-remove--other--unknown', i18n, [
|
|
||||||
renderContact(conversationId),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--member-remove--you--unknown', i18n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--member-remove--other--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (from && from === conversationId) {
|
||||||
|
return renderString('GroupV2--member-remove--other--self', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--member-remove--other--other', i18n, {
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(conversationId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return renderString('GroupV2--member-remove--other--unknown', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
} else if (detail.type === 'member-privilege') {
|
} else if (detail.type === 'member-privilege') {
|
||||||
const { conversationId, newPrivilege } = detail;
|
const { conversationId, newPrivilege } = detail;
|
||||||
const weAreMember = conversationId === ourConversationId;
|
const weAreMember = conversationId === ourConversationId;
|
||||||
|
@ -256,37 +250,38 @@ export function renderChangeDetail(
|
||||||
i18n,
|
i18n,
|
||||||
[renderContact(from)]
|
[renderContact(from)]
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--promote--you--unknown',
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--promote--other--you',
|
|
||||||
i18n,
|
|
||||||
[renderContact(conversationId)]
|
|
||||||
);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--promote--other--other',
|
|
||||||
i18n,
|
|
||||||
{
|
|
||||||
adminName: renderContact(from),
|
|
||||||
memberName: renderContact(conversationId),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--promote--other--unknown',
|
|
||||||
i18n,
|
|
||||||
[renderContact(conversationId)]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--you--unknown',
|
||||||
|
i18n
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (newPrivilege === RoleEnum.DEFAULT) {
|
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--other--you',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--other--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(conversationId),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--promote--other--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (newPrivilege === RoleEnum.DEFAULT) {
|
||||||
if (weAreMember) {
|
if (weAreMember) {
|
||||||
if (from) {
|
if (from) {
|
||||||
return renderString(
|
return renderString(
|
||||||
|
@ -294,41 +289,39 @@ export function renderChangeDetail(
|
||||||
i18n,
|
i18n,
|
||||||
[renderContact(from)]
|
[renderContact(from)]
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--demote--you--unknown',
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--demote--other--you',
|
|
||||||
i18n,
|
|
||||||
[renderContact(conversationId)]
|
|
||||||
);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--demote--other--other',
|
|
||||||
i18n,
|
|
||||||
{
|
|
||||||
adminName: renderContact(from),
|
|
||||||
memberName: renderContact(conversationId),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--member-privilege--demote--other--unknown',
|
|
||||||
i18n,
|
|
||||||
[renderContact(conversationId)]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--you--unknown',
|
||||||
|
i18n
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error(
|
if (fromYou) {
|
||||||
`member-privilege change type, privilege ${newPrivilege} is unknown`
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--other--you',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--other--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
adminName: renderContact(from),
|
||||||
|
memberName: renderContact(conversationId),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--member-privilege--demote--other--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
|
`member-privilege change type, privilege ${newPrivilege} is unknown`
|
||||||
|
);
|
||||||
} else if (detail.type === 'pending-add-one') {
|
} else if (detail.type === 'pending-add-one') {
|
||||||
const { conversationId } = detail;
|
const { conversationId } = detail;
|
||||||
const weAreInvited = conversationId === ourConversationId;
|
const weAreInvited = conversationId === ourConversationId;
|
||||||
|
@ -337,22 +330,20 @@ export function renderChangeDetail(
|
||||||
return renderString('GroupV2--pending-add--one--you--other', i18n, [
|
return renderString('GroupV2--pending-add--one--you--other', i18n, [
|
||||||
renderContact(from),
|
renderContact(from),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--pending-add--one--you--unknown', i18n);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString('GroupV2--pending-add--one--other--you', i18n, [
|
|
||||||
renderContact(conversationId),
|
|
||||||
]);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString('GroupV2--pending-add--one--other--other', i18n, [
|
|
||||||
renderContact(from),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--pending-add--one--other--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--pending-add--one--you--unknown', i18n);
|
||||||
}
|
}
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--pending-add--one--other--you', i18n, [
|
||||||
|
renderContact(conversationId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--pending-add--one--other--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return renderString('GroupV2--pending-add--one--other--unknown', i18n);
|
||||||
} else if (detail.type === 'pending-add-many') {
|
} else if (detail.type === 'pending-add-many') {
|
||||||
const { count } = detail;
|
const { count } = detail;
|
||||||
|
|
||||||
|
@ -360,16 +351,16 @@ export function renderChangeDetail(
|
||||||
return renderString('GroupV2--pending-add--many--you', i18n, [
|
return renderString('GroupV2--pending-add--many--you', i18n, [
|
||||||
count.toString(),
|
count.toString(),
|
||||||
]);
|
]);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString('GroupV2--pending-add--many--other', i18n, {
|
return renderString('GroupV2--pending-add--many--other', i18n, {
|
||||||
memberName: renderContact(from),
|
memberName: renderContact(from),
|
||||||
count: count.toString(),
|
count: count.toString(),
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--pending-add--many--unknown', i18n, [
|
|
||||||
count.toString(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
return renderString('GroupV2--pending-add--many--unknown', i18n, [
|
||||||
|
count.toString(),
|
||||||
|
]);
|
||||||
} else if (detail.type === 'pending-remove-one') {
|
} else if (detail.type === 'pending-remove-one') {
|
||||||
const { inviter, conversationId } = detail;
|
const { inviter, conversationId } = detail;
|
||||||
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
||||||
|
@ -380,13 +371,15 @@ export function renderChangeDetail(
|
||||||
return renderString('GroupV2--pending-remove--decline--you', i18n, [
|
return renderString('GroupV2--pending-remove--decline--you', i18n, [
|
||||||
renderContact(conversationId),
|
renderContact(conversationId),
|
||||||
]);
|
]);
|
||||||
} else if (fromYou) {
|
}
|
||||||
|
if (fromYou) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from-you--one--you',
|
'GroupV2--pending-remove--revoke-invite-from-you--one--you',
|
||||||
i18n,
|
i18n,
|
||||||
[renderContact(conversationId)]
|
[renderContact(conversationId)]
|
||||||
);
|
);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from-you--one--other',
|
'GroupV2--pending-remove--revoke-invite-from-you--one--other',
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -395,29 +388,30 @@ export function renderChangeDetail(
|
||||||
inviteeName: renderContact(conversationId),
|
inviteeName: renderContact(conversationId),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
|
|
||||||
i18n,
|
|
||||||
[renderContact(conversationId)]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (sentByInvited) {
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(conversationId)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sentByInvited) {
|
||||||
if (inviter) {
|
if (inviter) {
|
||||||
return renderString('GroupV2--pending-remove--decline--other', i18n, [
|
return renderString('GroupV2--pending-remove--decline--other', i18n, [
|
||||||
renderContact(inviter),
|
renderContact(inviter),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
return renderString('GroupV2--pending-remove--decline--unknown', i18n);
|
|
||||||
}
|
}
|
||||||
} else if (inviter) {
|
return renderString('GroupV2--pending-remove--decline--unknown', i18n);
|
||||||
|
}
|
||||||
|
if (inviter) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from--one--you',
|
'GroupV2--pending-remove--revoke-invite-from--one--you',
|
||||||
i18n,
|
i18n,
|
||||||
[renderContact(inviter)]
|
[renderContact(inviter)]
|
||||||
);
|
);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from--one--other',
|
'GroupV2--pending-remove--revoke-invite-from--one--other',
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -426,29 +420,22 @@ export function renderChangeDetail(
|
||||||
memberName: renderContact(inviter),
|
memberName: renderContact(inviter),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke-invite-from--one--unknown',
|
|
||||||
i18n,
|
|
||||||
[renderContact(inviter)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString('GroupV2--pending-remove--revoke--one--you', i18n);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke--one--other',
|
|
||||||
i18n,
|
|
||||||
[renderContact(from)]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke--one--unknown',
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--one--unknown',
|
||||||
|
i18n,
|
||||||
|
[renderContact(inviter)]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--pending-remove--revoke--one--you', i18n);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString('GroupV2--pending-remove--revoke--one--other', i18n, [
|
||||||
|
renderContact(from),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return renderString('GroupV2--pending-remove--revoke--one--unknown', i18n);
|
||||||
} else if (detail.type === 'pending-remove-many') {
|
} else if (detail.type === 'pending-remove-many') {
|
||||||
const { count, inviter } = detail;
|
const { count, inviter } = detail;
|
||||||
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
|
||||||
|
@ -460,7 +447,8 @@ export function renderChangeDetail(
|
||||||
i18n,
|
i18n,
|
||||||
[count.toString()]
|
[count.toString()]
|
||||||
);
|
);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from-you--many--other',
|
'GroupV2--pending-remove--revoke-invite-from-you--many--other',
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -469,14 +457,14 @@ export function renderChangeDetail(
|
||||||
count: count.toString(),
|
count: count.toString(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
|
|
||||||
i18n,
|
|
||||||
[count.toString()]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (inviter) {
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
|
||||||
|
i18n,
|
||||||
|
[count.toString()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (inviter) {
|
||||||
if (fromYou) {
|
if (fromYou) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from--many--you',
|
'GroupV2--pending-remove--revoke-invite-from--many--you',
|
||||||
|
@ -486,7 +474,8 @@ export function renderChangeDetail(
|
||||||
memberName: renderContact(inviter),
|
memberName: renderContact(inviter),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else if (from) {
|
}
|
||||||
|
if (from) {
|
||||||
return renderString(
|
return renderString(
|
||||||
'GroupV2--pending-remove--revoke-invite-from--many--other',
|
'GroupV2--pending-remove--revoke-invite-from--many--other',
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -496,40 +485,36 @@ export function renderChangeDetail(
|
||||||
memberName: renderContact(inviter),
|
memberName: renderContact(inviter),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke-invite-from--many--unknown',
|
|
||||||
i18n,
|
|
||||||
{
|
|
||||||
count: count.toString(),
|
|
||||||
memberName: renderContact(inviter),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (fromYou) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke--many--you',
|
|
||||||
i18n,
|
|
||||||
[count.toString()]
|
|
||||||
);
|
|
||||||
} else if (from) {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke--many--other',
|
|
||||||
i18n,
|
|
||||||
{
|
|
||||||
memberName: renderContact(from),
|
|
||||||
count: count.toString(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return renderString(
|
|
||||||
'GroupV2--pending-remove--revoke--many--unknown',
|
|
||||||
i18n,
|
|
||||||
[count.toString()]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke-invite-from--many--unknown',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
count: count.toString(),
|
||||||
|
memberName: renderContact(inviter),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if (fromYou) {
|
||||||
|
return renderString('GroupV2--pending-remove--revoke--many--you', i18n, [
|
||||||
|
count.toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (from) {
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--many--other',
|
||||||
|
i18n,
|
||||||
|
{
|
||||||
|
memberName: renderContact(from),
|
||||||
|
count: count.toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return renderString(
|
||||||
|
'GroupV2--pending-remove--revoke--many--unknown',
|
||||||
|
i18n,
|
||||||
|
[count.toString()]
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(detail);
|
throw missingCaseError(detail);
|
||||||
}
|
}
|
||||||
|
|
124
ts/groups.ts
124
ts/groups.ts
|
@ -1,5 +1,3 @@
|
||||||
/* tslint:disable no-dynamic-delete no-unnecessary-local-variable */
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
compact,
|
compact,
|
||||||
Dictionary,
|
Dictionary,
|
||||||
|
@ -8,6 +6,8 @@ import {
|
||||||
isNumber,
|
isNumber,
|
||||||
values,
|
values,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
import { ClientZkGroupCipher } from 'zkgroup';
|
||||||
|
import { v4 as getGuid } from 'uuid';
|
||||||
import {
|
import {
|
||||||
getCredentialsForToday,
|
getCredentialsForToday,
|
||||||
GROUP_CREDENTIALS_KEY,
|
GROUP_CREDENTIALS_KEY,
|
||||||
|
@ -33,7 +33,6 @@ import {
|
||||||
getClientZkAuthOperations,
|
getClientZkAuthOperations,
|
||||||
getClientZkGroupCipher,
|
getClientZkGroupCipher,
|
||||||
} from './util/zkgroup';
|
} from './util/zkgroup';
|
||||||
import { ClientZkGroupCipher } from 'zkgroup';
|
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
arrayBufferToHex,
|
arrayBufferToHex,
|
||||||
|
@ -50,7 +49,6 @@ import {
|
||||||
ProtoBinaryType,
|
ProtoBinaryType,
|
||||||
} from './textsecure.d';
|
} from './textsecure.d';
|
||||||
import { GroupCredentialsType } from './textsecure/WebAPI';
|
import { GroupCredentialsType } from './textsecure/WebAPI';
|
||||||
import { v4 as getGuid } from 'uuid';
|
|
||||||
import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from '../js/modules/types/message';
|
import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from '../js/modules/types/message';
|
||||||
|
|
||||||
export type GroupV2AccessAttributesChangeType = {
|
export type GroupV2AccessAttributesChangeType = {
|
||||||
|
@ -250,7 +248,9 @@ export async function uploadGroupChange({
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
|
|
||||||
export function deriveGroupFields(masterKey: ArrayBuffer) {
|
export function deriveGroupFields(
|
||||||
|
masterKey: ArrayBuffer
|
||||||
|
): Record<string, ArrayBuffer> {
|
||||||
const secretParams = deriveGroupSecretParams(masterKey);
|
const secretParams = deriveGroupSecretParams(masterKey);
|
||||||
const publicParams = deriveGroupPublicParams(secretParams);
|
const publicParams = deriveGroupPublicParams(secretParams);
|
||||||
const id = deriveGroupID(secretParams);
|
const id = deriveGroupID(secretParams);
|
||||||
|
@ -273,7 +273,9 @@ type MaybeUpdatePropsType = {
|
||||||
dropInitialJoinMessage?: boolean;
|
dropInitialJoinMessage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function waitThenMaybeUpdateGroup(options: MaybeUpdatePropsType) {
|
export async function waitThenMaybeUpdateGroup(
|
||||||
|
options: MaybeUpdatePropsType
|
||||||
|
): Promise<void> {
|
||||||
// First wait to process all incoming messages on the websocket
|
// First wait to process all incoming messages on the websocket
|
||||||
await window.waitForEmptyEventQueue();
|
await window.waitForEmptyEventQueue();
|
||||||
|
|
||||||
|
@ -321,8 +323,9 @@ export async function maybeUpdateGroup({
|
||||||
|
|
||||||
conversation.set(newAttributes);
|
conversation.set(newAttributes);
|
||||||
|
|
||||||
// Ensure that all generated message are ordered properly. Before the provided timestamp
|
// Ensure that all generated messages are ordered properly.
|
||||||
// so update messages appear before the initiating message, or after now().
|
// Before the provided timestamp so update messages appear before the
|
||||||
|
// initiating message, or after now().
|
||||||
let syntheticTimestamp = receivedAt
|
let syntheticTimestamp = receivedAt
|
||||||
? receivedAt - (groupChangeMessages.length + 1)
|
? receivedAt - (groupChangeMessages.length + 1)
|
||||||
: Date.now();
|
: Date.now();
|
||||||
|
@ -355,7 +358,6 @@ export async function maybeUpdateGroup({
|
||||||
const contact = window.ConversationController.get(member.uuid);
|
const contact = window.ConversationController.get(member.uuid);
|
||||||
|
|
||||||
if (member.profileKey && contact && !contact.get('profileKey')) {
|
if (member.profileKey && contact && !contact.get('profileKey')) {
|
||||||
// tslint:disable-next-line no-floating-promises
|
|
||||||
contact.setProfileKey(member.profileKey);
|
contact.setProfileKey(member.profileKey);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -487,8 +489,8 @@ async function updateGroupViaState({
|
||||||
authCredentialBase64: groupCredentials.tomorrow.credential,
|
authCredentialBase64: groupCredentials.tomorrow.credential,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (subError) {
|
||||||
if (error.code === GROUP_ACCESS_DENIED_CODE) {
|
if (subError.code === GROUP_ACCESS_DENIED_CODE) {
|
||||||
return generateLeftGroupChanges(group);
|
return generateLeftGroupChanges(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -537,9 +539,8 @@ async function updateGroupViaLogs({
|
||||||
...deltaOptions,
|
...deltaOptions,
|
||||||
authCredentialBase64: groupCredentials.tomorrow.credential,
|
authCredentialBase64: groupCredentials.tomorrow.credential,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,6 +655,7 @@ async function getGroupDelta({
|
||||||
let response;
|
let response;
|
||||||
const changes: Array<GroupChangesClass> = [];
|
const changes: Array<GroupChangesClass> = [];
|
||||||
do {
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
response = await sender.getGroupLog(revisionToFetch, options);
|
response = await sender.getGroupLog(revisionToFetch, options);
|
||||||
changes.push(response.changes);
|
changes.push(response.changes);
|
||||||
if (response.end) {
|
if (response.end) {
|
||||||
|
@ -689,6 +691,7 @@ async function integrateGroupChanges({
|
||||||
const { groupChanges } = changes[i];
|
const { groupChanges } = changes[i];
|
||||||
|
|
||||||
if (!groupChanges) {
|
if (!groupChanges) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -699,6 +702,7 @@ async function integrateGroupChanges({
|
||||||
const { groupChange } = changeState;
|
const { groupChange } = changeState;
|
||||||
|
|
||||||
if (!groupChange) {
|
if (!groupChange) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -707,6 +711,7 @@ async function integrateGroupChanges({
|
||||||
newAttributes,
|
newAttributes,
|
||||||
groupChangeMessages,
|
groupChangeMessages,
|
||||||
members,
|
members,
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
} = await integrateGroupChange({
|
} = await integrateGroupChange({
|
||||||
group: attributes,
|
group: attributes,
|
||||||
newRevision,
|
newRevision,
|
||||||
|
@ -868,7 +873,6 @@ export async function getCurrentGroupState({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
|
||||||
function extractDiffs({
|
function extractDiffs({
|
||||||
current,
|
current,
|
||||||
dropInitialJoinMessage,
|
dropInitialJoinMessage,
|
||||||
|
@ -1126,7 +1130,6 @@ type GroupChangeResultType = {
|
||||||
newProfileKeys: Array<GroupChangeMemberType>;
|
newProfileKeys: Array<GroupChangeMemberType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
|
||||||
async function applyGroupChange({
|
async function applyGroupChange({
|
||||||
group,
|
group,
|
||||||
actions,
|
actions,
|
||||||
|
@ -1158,7 +1161,7 @@ async function applyGroupChange({
|
||||||
result.revision = version;
|
result.revision = version;
|
||||||
|
|
||||||
// addMembers?: Array<GroupChangeClass.Actions.AddMemberAction>;
|
// addMembers?: Array<GroupChangeClass.Actions.AddMemberAction>;
|
||||||
(actions.addMembers || []).map(addMember => {
|
(actions.addMembers || []).forEach(addMember => {
|
||||||
const { added } = addMember;
|
const { added } = addMember;
|
||||||
if (!added) {
|
if (!added) {
|
||||||
throw new Error('applyGroupChange: addMember.added is missing');
|
throw new Error('applyGroupChange: addMember.added is missing');
|
||||||
|
@ -1249,7 +1252,8 @@ async function applyGroupChange({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// modifyMemberProfileKeys?: Array<GroupChangeClass.Actions.ModifyMemberProfileKeyAction>;
|
// modifyMemberProfileKeys?:
|
||||||
|
// Array<GroupChangeClass.Actions.ModifyMemberProfileKeyAction>;
|
||||||
(actions.modifyMemberProfileKeys || []).forEach(modifyMemberProfileKey => {
|
(actions.modifyMemberProfileKeys || []).forEach(modifyMemberProfileKey => {
|
||||||
const { profileKey, uuid } = modifyMemberProfileKey;
|
const { profileKey, uuid } = modifyMemberProfileKey;
|
||||||
if (!profileKey || !uuid) {
|
if (!profileKey || !uuid) {
|
||||||
|
@ -1374,8 +1378,7 @@ async function applyGroupChange({
|
||||||
|
|
||||||
// modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction;
|
// modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction;
|
||||||
if (actions.modifyTitle) {
|
if (actions.modifyTitle) {
|
||||||
const title: GroupAttributeBlobClass | undefined =
|
const { title } = actions.modifyTitle;
|
||||||
actions.modifyTitle.title;
|
|
||||||
if (title && title.content === 'title') {
|
if (title && title.content === 'title') {
|
||||||
result.name = title.title;
|
result.name = title.title;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1388,11 +1391,12 @@ async function applyGroupChange({
|
||||||
|
|
||||||
// modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction;
|
// modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction;
|
||||||
if (actions.modifyAvatar) {
|
if (actions.modifyAvatar) {
|
||||||
const avatar = actions.modifyAvatar.avatar;
|
const { avatar } = actions.modifyAvatar;
|
||||||
await applyNewAvatar(avatar, result, logId);
|
await applyNewAvatar(avatar, result, logId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// modifyDisappearingMessagesTimer?: GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction;
|
// modifyDisappearingMessagesTimer?:
|
||||||
|
// GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction;
|
||||||
if (actions.modifyDisappearingMessagesTimer) {
|
if (actions.modifyDisappearingMessagesTimer) {
|
||||||
const disappearingMessagesTimer: GroupAttributeBlobClass | undefined =
|
const disappearingMessagesTimer: GroupAttributeBlobClass | undefined =
|
||||||
actions.modifyDisappearingMessagesTimer.timer;
|
actions.modifyDisappearingMessagesTimer.timer;
|
||||||
|
@ -1415,7 +1419,8 @@ async function applyGroupChange({
|
||||||
attributes: ACCESS_ENUM.MEMBER,
|
attributes: ACCESS_ENUM.MEMBER,
|
||||||
};
|
};
|
||||||
|
|
||||||
// modifyAttributesAccess?: GroupChangeClass.Actions.ModifyAttributesAccessControlAction;
|
// modifyAttributesAccess?:
|
||||||
|
// GroupChangeClass.Actions.ModifyAttributesAccessControlAction;
|
||||||
if (actions.modifyAttributesAccess) {
|
if (actions.modifyAttributesAccess) {
|
||||||
result.accessControl = {
|
result.accessControl = {
|
||||||
...result.accessControl,
|
...result.accessControl,
|
||||||
|
@ -1447,6 +1452,8 @@ async function applyGroupChange({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ovewriting result.avatar as part of functionality
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
async function applyNewAvatar(
|
async function applyNewAvatar(
|
||||||
newAvatar: string | undefined,
|
newAvatar: string | undefined,
|
||||||
result: ConversationAttributesType,
|
result: ConversationAttributesType,
|
||||||
|
@ -1514,8 +1521,8 @@ async function applyNewAvatar(
|
||||||
result.avatar = undefined;
|
result.avatar = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-param-reassign */
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
|
||||||
async function applyGroupState(
|
async function applyGroupState(
|
||||||
group: ConversationAttributesType,
|
group: ConversationAttributesType,
|
||||||
groupState: GroupClass
|
groupState: GroupClass
|
||||||
|
@ -1532,7 +1539,7 @@ async function applyGroupState(
|
||||||
|
|
||||||
// title
|
// title
|
||||||
// Note: During decryption, title becomes a GroupAttributeBlob
|
// Note: During decryption, title becomes a GroupAttributeBlob
|
||||||
const title: GroupAttributeBlobClass | undefined = groupState.title;
|
const { title } = groupState;
|
||||||
if (title && title.content === 'title') {
|
if (title && title.content === 'title') {
|
||||||
result.name = title.title;
|
result.name = title.title;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1544,8 +1551,7 @@ async function applyGroupState(
|
||||||
|
|
||||||
// disappearingMessagesTimer
|
// disappearingMessagesTimer
|
||||||
// Note: during decryption, disappearingMessageTimer becomes a GroupAttributeBlob
|
// Note: during decryption, disappearingMessageTimer becomes a GroupAttributeBlob
|
||||||
const disappearingMessagesTimer: GroupAttributeBlobClass | undefined =
|
const { disappearingMessagesTimer } = groupState;
|
||||||
groupState.disappearingMessagesTimer;
|
|
||||||
if (
|
if (
|
||||||
disappearingMessagesTimer &&
|
disappearingMessagesTimer &&
|
||||||
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
|
disappearingMessagesTimer.content === 'disappearingMessagesDuration'
|
||||||
|
@ -1665,13 +1671,13 @@ function hasData(data: ProtoBinaryType): boolean {
|
||||||
return data && data.limit > 0;
|
return data && data.limit > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
|
||||||
function decryptGroupChange(
|
function decryptGroupChange(
|
||||||
actions: GroupChangeClass.Actions,
|
_actions: GroupChangeClass.Actions,
|
||||||
groupSecretParams: string,
|
groupSecretParams: string,
|
||||||
logId: string
|
logId: string
|
||||||
): GroupChangeClass.Actions {
|
): GroupChangeClass.Actions {
|
||||||
const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams);
|
const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams);
|
||||||
|
const actions = _actions;
|
||||||
|
|
||||||
if (hasData(actions.sourceUuid)) {
|
if (hasData(actions.sourceUuid)) {
|
||||||
try {
|
try {
|
||||||
|
@ -1701,7 +1707,9 @@ function decryptGroupChange(
|
||||||
|
|
||||||
// addMembers?: Array<GroupChangeClass.Actions.AddMemberAction>;
|
// addMembers?: Array<GroupChangeClass.Actions.AddMemberAction>;
|
||||||
actions.addMembers = compact(
|
actions.addMembers = compact(
|
||||||
(actions.addMembers || []).map(addMember => {
|
(actions.addMembers || []).map(_addMember => {
|
||||||
|
const addMember = _addMember;
|
||||||
|
|
||||||
if (addMember.added) {
|
if (addMember.added) {
|
||||||
const decrypted = decryptMember(
|
const decrypted = decryptMember(
|
||||||
clientZkGroupCipher,
|
clientZkGroupCipher,
|
||||||
|
@ -1714,17 +1722,16 @@ function decryptGroupChange(
|
||||||
|
|
||||||
addMember.added = decrypted;
|
addMember.added = decrypted;
|
||||||
return addMember;
|
return addMember;
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'decryptGroupChange: AddMember was missing added field!'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
throw new Error('decryptGroupChange: AddMember was missing added field!');
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// deleteMembers?: Array<GroupChangeClass.Actions.DeleteMemberAction>;
|
// deleteMembers?: Array<GroupChangeClass.Actions.DeleteMemberAction>;
|
||||||
actions.deleteMembers = compact(
|
actions.deleteMembers = compact(
|
||||||
(actions.deleteMembers || []).map(deleteMember => {
|
(actions.deleteMembers || []).map(_deleteMember => {
|
||||||
|
const deleteMember = _deleteMember;
|
||||||
|
|
||||||
if (hasData(deleteMember.deletedUserId)) {
|
if (hasData(deleteMember.deletedUserId)) {
|
||||||
try {
|
try {
|
||||||
deleteMember.deletedUserId = decryptUuid(
|
deleteMember.deletedUserId = decryptUuid(
|
||||||
|
@ -1764,7 +1771,9 @@ function decryptGroupChange(
|
||||||
|
|
||||||
// modifyMemberRoles?: Array<GroupChangeClass.Actions.ModifyMemberRoleAction>;
|
// modifyMemberRoles?: Array<GroupChangeClass.Actions.ModifyMemberRoleAction>;
|
||||||
actions.modifyMemberRoles = compact(
|
actions.modifyMemberRoles = compact(
|
||||||
(actions.modifyMemberRoles || []).map(modifyMember => {
|
(actions.modifyMemberRoles || []).map(_modifyMember => {
|
||||||
|
const modifyMember = _modifyMember;
|
||||||
|
|
||||||
if (hasData(modifyMember.userId)) {
|
if (hasData(modifyMember.userId)) {
|
||||||
try {
|
try {
|
||||||
modifyMember.userId = decryptUuid(
|
modifyMember.userId = decryptUuid(
|
||||||
|
@ -1808,9 +1817,12 @@ function decryptGroupChange(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// modifyMemberProfileKeys?: Array<GroupChangeClass.Actions.ModifyMemberProfileKeyAction>;
|
// modifyMemberProfileKeys?:
|
||||||
|
// Array<GroupChangeClass.Actions.ModifyMemberProfileKeyAction>;
|
||||||
actions.modifyMemberProfileKeys = compact(
|
actions.modifyMemberProfileKeys = compact(
|
||||||
(actions.modifyMemberProfileKeys || []).map(modifyMemberProfileKey => {
|
(actions.modifyMemberProfileKeys || []).map(_modifyMemberProfileKey => {
|
||||||
|
const modifyMemberProfileKey = _modifyMemberProfileKey;
|
||||||
|
|
||||||
if (hasData(modifyMemberProfileKey.presentation)) {
|
if (hasData(modifyMemberProfileKey.presentation)) {
|
||||||
const { profileKey, uuid } = decryptProfileKeyCredentialPresentation(
|
const { profileKey, uuid } = decryptProfileKeyCredentialPresentation(
|
||||||
clientZkGroupCipher,
|
clientZkGroupCipher,
|
||||||
|
@ -1854,7 +1866,9 @@ function decryptGroupChange(
|
||||||
|
|
||||||
// addPendingMembers?: Array<GroupChangeClass.Actions.AddPendingMemberAction>;
|
// addPendingMembers?: Array<GroupChangeClass.Actions.AddPendingMemberAction>;
|
||||||
actions.addPendingMembers = compact(
|
actions.addPendingMembers = compact(
|
||||||
(actions.addPendingMembers || []).map(addPendingMember => {
|
(actions.addPendingMembers || []).map(_addPendingMember => {
|
||||||
|
const addPendingMember = _addPendingMember;
|
||||||
|
|
||||||
if (addPendingMember.added) {
|
if (addPendingMember.added) {
|
||||||
const decrypted = decryptPendingMember(
|
const decrypted = decryptPendingMember(
|
||||||
clientZkGroupCipher,
|
clientZkGroupCipher,
|
||||||
|
@ -1867,17 +1881,18 @@ function decryptGroupChange(
|
||||||
|
|
||||||
addPendingMember.added = decrypted;
|
addPendingMember.added = decrypted;
|
||||||
return addPendingMember;
|
return addPendingMember;
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'decryptGroupChange: addPendingMember was missing added field!'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
throw new Error(
|
||||||
|
'decryptGroupChange: addPendingMember was missing added field!'
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// deletePendingMembers?: Array<GroupChangeClass.Actions.DeletePendingMemberAction>;
|
// deletePendingMembers?: Array<GroupChangeClass.Actions.DeletePendingMemberAction>;
|
||||||
actions.deletePendingMembers = compact(
|
actions.deletePendingMembers = compact(
|
||||||
(actions.deletePendingMembers || []).map(deletePendingMember => {
|
(actions.deletePendingMembers || []).map(_deletePendingMember => {
|
||||||
|
const deletePendingMember = _deletePendingMember;
|
||||||
|
|
||||||
if (hasData(deletePendingMember.deletedUserId)) {
|
if (hasData(deletePendingMember.deletedUserId)) {
|
||||||
try {
|
try {
|
||||||
deletePendingMember.deletedUserId = decryptUuid(
|
deletePendingMember.deletedUserId = decryptUuid(
|
||||||
|
@ -1917,7 +1932,9 @@ function decryptGroupChange(
|
||||||
|
|
||||||
// promotePendingMembers?: Array<GroupChangeClass.Actions.PromotePendingMemberAction>;
|
// promotePendingMembers?: Array<GroupChangeClass.Actions.PromotePendingMemberAction>;
|
||||||
actions.promotePendingMembers = compact(
|
actions.promotePendingMembers = compact(
|
||||||
(actions.promotePendingMembers || []).map(promotePendingMember => {
|
(actions.promotePendingMembers || []).map(_promotePendingMember => {
|
||||||
|
const promotePendingMember = _promotePendingMember;
|
||||||
|
|
||||||
if (hasData(promotePendingMember.presentation)) {
|
if (hasData(promotePendingMember.presentation)) {
|
||||||
const { profileKey, uuid } = decryptProfileKeyCredentialPresentation(
|
const { profileKey, uuid } = decryptProfileKeyCredentialPresentation(
|
||||||
clientZkGroupCipher,
|
clientZkGroupCipher,
|
||||||
|
@ -1979,7 +1996,8 @@ function decryptGroupChange(
|
||||||
// modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction;
|
// modifyAvatar?: GroupChangeClass.Actions.ModifyAvatarAction;
|
||||||
// Note: decryption happens during application of the change, on download of the avatar
|
// Note: decryption happens during application of the change, on download of the avatar
|
||||||
|
|
||||||
// modifyDisappearingMessagesTimer?: GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction;
|
// modifyDisappearingMessagesTimer?:
|
||||||
|
// GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction;
|
||||||
if (
|
if (
|
||||||
actions.modifyDisappearingMessagesTimer &&
|
actions.modifyDisappearingMessagesTimer &&
|
||||||
hasData(actions.modifyDisappearingMessagesTimer.timer)
|
hasData(actions.modifyDisappearingMessagesTimer.timer)
|
||||||
|
@ -2002,7 +2020,8 @@ function decryptGroupChange(
|
||||||
actions.modifyDisappearingMessagesTimer.timer = undefined;
|
actions.modifyDisappearingMessagesTimer.timer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// modifyAttributesAccess?: GroupChangeClass.Actions.ModifyAttributesAccessControlAction;
|
// modifyAttributesAccess?:
|
||||||
|
// GroupChangeClass.Actions.ModifyAttributesAccessControlAction;
|
||||||
if (
|
if (
|
||||||
actions.modifyAttributesAccess &&
|
actions.modifyAttributesAccess &&
|
||||||
!isValidAccess(actions.modifyAttributesAccess.attributesAccess)
|
!isValidAccess(actions.modifyAttributesAccess.attributesAccess)
|
||||||
|
@ -2025,13 +2044,13 @@ function decryptGroupChange(
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
|
||||||
function decryptGroupState(
|
function decryptGroupState(
|
||||||
groupState: GroupClass,
|
_groupState: GroupClass,
|
||||||
groupSecretParams: string,
|
groupSecretParams: string,
|
||||||
logId: string
|
logId: string
|
||||||
): GroupClass {
|
): GroupClass {
|
||||||
const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams);
|
const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams);
|
||||||
|
const groupState = _groupState;
|
||||||
|
|
||||||
// title
|
// title
|
||||||
if (hasData(groupState.title)) {
|
if (hasData(groupState.title)) {
|
||||||
|
@ -2121,9 +2140,11 @@ function decryptGroupState(
|
||||||
|
|
||||||
function decryptMember(
|
function decryptMember(
|
||||||
clientZkGroupCipher: ClientZkGroupCipher,
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
member: MemberClass,
|
_member: MemberClass,
|
||||||
logId: string
|
logId: string
|
||||||
) {
|
) {
|
||||||
|
const member = _member;
|
||||||
|
|
||||||
// userId
|
// userId
|
||||||
if (hasData(member.userId)) {
|
if (hasData(member.userId)) {
|
||||||
try {
|
try {
|
||||||
|
@ -2175,12 +2196,13 @@ function decryptMember(
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
|
||||||
function decryptPendingMember(
|
function decryptPendingMember(
|
||||||
clientZkGroupCipher: ClientZkGroupCipher,
|
clientZkGroupCipher: ClientZkGroupCipher,
|
||||||
member: PendingMemberClass,
|
_member: PendingMemberClass,
|
||||||
logId: string
|
logId: string
|
||||||
) {
|
) {
|
||||||
|
const member = _member;
|
||||||
|
|
||||||
// addedByUserId
|
// addedByUserId
|
||||||
if (hasData(member.addedByUserId)) {
|
if (hasData(member.addedByUserId)) {
|
||||||
try {
|
try {
|
||||||
|
|
8
ts/model-types.d.ts
vendored
8
ts/model-types.d.ts
vendored
|
@ -25,7 +25,7 @@ declare class DeletesModelType extends Backbone.Model<DeletesAttributesType> {
|
||||||
|
|
||||||
type TaskResultType = any;
|
type TaskResultType = any;
|
||||||
|
|
||||||
type MessageAttributesType = {
|
export type MessageAttributesType = {
|
||||||
id: string;
|
id: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
|
@ -63,9 +63,9 @@ declare class MessageModelType extends Backbone.Model<MessageAttributesType> {
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConversationTypeType = 'private' | 'group';
|
export type ConversationTypeType = 'private' | 'group';
|
||||||
|
|
||||||
type ConversationAttributesType = {
|
export type ConversationAttributesType = {
|
||||||
id: string;
|
id: string;
|
||||||
type: ConversationTypeType;
|
type: ConversationTypeType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
@ -201,7 +201,7 @@ export declare class ConversationModelType extends Backbone.Model<
|
||||||
wrapSend: (sendPromise: Promise<any>) => Promise<any>;
|
wrapSend: (sendPromise: Promise<any>) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class ConversationModelCollectionType extends Backbone.Collection<
|
export declare class ConversationModelCollectionType extends Backbone.Collection<
|
||||||
ConversationModelType
|
ConversationModelType
|
||||||
> {
|
> {
|
||||||
resetLookups(): void;
|
resetLookups(): void;
|
||||||
|
|
2
ts/textsecure.d.ts
vendored
2
ts/textsecure.d.ts
vendored
|
@ -209,7 +209,7 @@ type ProtobufCollectionType = DeviceMessagesProtobufTypes &
|
||||||
// Note: there are a lot of places in the code that overwrite a field like this
|
// Note: there are a lot of places in the code that overwrite a field like this
|
||||||
// with a type that the app can use. Being more rigorous with these
|
// with a type that the app can use. Being more rigorous with these
|
||||||
// types would require code changes, out of scope for now.
|
// types would require code changes, out of scope for now.
|
||||||
type ProtoBinaryType = any;
|
export type ProtoBinaryType = any;
|
||||||
type ProtoBigNumberType = any;
|
type ProtoBigNumberType = any;
|
||||||
|
|
||||||
// Groups.proto
|
// Groups.proto
|
||||||
|
|
|
@ -177,6 +177,7 @@
|
||||||
"rulesDirectory": ["node_modules/tslint-microsoft-contrib"],
|
"rulesDirectory": ["node_modules/tslint-microsoft-contrib"],
|
||||||
"linterOptions": {
|
"linterOptions": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
"ts/*.ts",
|
||||||
"ts/components/emoji/**",
|
"ts/components/emoji/**",
|
||||||
"ts/backbone/**",
|
"ts/backbone/**",
|
||||||
"ts/build/**",
|
"ts/build/**",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue