Merge contacts when we discover split or duplicated contacts

This commit is contained in:
Scott Nonnenberg 2020-07-10 11:28:49 -07:00
parent 68e432188b
commit 901179440f
32 changed files with 1199 additions and 824 deletions

View file

@ -0,0 +1,675 @@
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
import { debounce, reduce, uniq, without } from 'lodash';
import dataInterface from './sql/Client';
import {
ConversationModelCollectionType,
ConversationModelType,
ConversationTypeType,
} from './model-types.d';
import { SendOptionsType } from './textsecure/SendMessage';
const {
getAllConversations,
getAllGroupsInvolvingId,
getMessagesBySentAt,
migrateConversationMessages,
removeConversation,
saveConversation,
updateConversation,
} = dataInterface;
// 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
// require statements for referencing these things, giving us more flexibility here.
export function start() {
const conversations = new window.Whisper.ConversationCollection();
// This class is entirely designed to keep the app title, badge and tray icon updated.
// In the future it could listen to redux changes and do its updates there.
const inboxCollection = new (window.Backbone.Collection.extend({
initialize() {
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', () => this.reset([]));
this.on(
'add remove change:unreadCount',
debounce(this.updateUnreadCount.bind(this), 1000)
);
},
addActive(model: ConversationModelType) {
if (model.get('active_at')) {
this.add(model);
} else {
this.remove(model);
}
},
updateUnreadCount() {
const newUnreadCount = reduce(
this.map((m: ConversationModelType) => m.get('unreadCount')),
(item: number, memo: number) => (item || 0) + memo,
0
);
window.storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
} else {
window.setBadgeCount(0);
window.document.title = window.getTitle();
}
window.updateTrayIcon(newUnreadCount);
},
}))();
window.getInboxCollection = () => inboxCollection;
window.getConversations = () => conversations;
window.ConversationController = new ConversationController(conversations);
}
export class ConversationController {
_initialFetchComplete: boolean | undefined;
_initialPromise: Promise<void> = Promise.resolve();
_conversations: ConversationModelCollectionType;
constructor(conversations?: ConversationModelCollectionType) {
if (!conversations) {
throw new Error('ConversationController: need conversation collection!');
}
this._conversations = conversations;
}
get(id?: string | null): ConversationModelType | undefined {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
// This function takes null just fine. Backbone typings are too restrictive.
return this._conversations.get(id as string);
}
// Needed for some model setup which happens during the initial fetch() call below
getUnsafe(id: string) {
return this._conversations.get(id);
}
dangerouslyCreateAndAdd(attributes: Partial<ConversationModelType>) {
return this._conversations.add(attributes);
}
getOrCreate(
identifier: string,
type: ConversationTypeType,
additionalInitialProps = {}
) {
if (typeof identifier !== 'string') {
throw new TypeError("'id' must be a string");
}
if (type !== 'private' && type !== 'group') {
throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
let conversation = this._conversations.get(identifier);
if (conversation) {
return conversation;
}
const id = window.getGuid();
if (type === 'group') {
conversation = this._conversations.add({
id,
uuid: null,
e164: null,
groupId: identifier,
type,
version: 2,
...additionalInitialProps,
});
} else if (window.isValidGuid(identifier)) {
conversation = this._conversations.add({
id,
uuid: identifier,
e164: null,
groupId: null,
type,
version: 2,
...additionalInitialProps,
});
} else {
conversation = this._conversations.add({
id,
uuid: null,
e164: identifier,
groupId: null,
type,
version: 2,
...additionalInitialProps,
});
}
const create = async () => {
if (!conversation.isValid()) {
const validationError = conversation.validationError || {};
window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return conversation;
}
try {
await saveConversation(conversation.attributes);
} catch (error) {
window.log.error(
'Conversation save failed! ',
identifier,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
return conversation;
}
async getOrCreateAndWait(
id: string,
type: ConversationTypeType,
additionalInitialProps = {}
) {
return this._initialPromise.then(async () => {
const conversation = this.getOrCreate(id, type, additionalInitialProps);
if (conversation) {
return conversation.initialPromise.then(() => conversation);
}
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
});
}
getConversationId(address: string) {
if (!address) {
return null;
}
const [id] = window.textsecure.utils.unencodeNumber(address);
const conv = this.get(id);
if (conv) {
return conv.get('id');
}
return null;
}
getOurConversationId() {
const e164 = window.textsecure.storage.user.getNumber();
const uuid = window.textsecure.storage.user.getUuid();
return this.ensureContactIds({ e164, uuid, highTrust: true });
}
/**
* Given a UUID and/or an E164, resolves to a string representing the local
* database id of the given contact. It may create new contacts, and it may merge
* contacts.
*
* lowTrust = uuid/e164 pairing came from source like GroupV1 member list
* highTrust = uuid/e164 pairing came from source like CDS
*/
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
ensureContactIds({
e164,
uuid,
highTrust,
}: {
e164?: string;
uuid?: string;
highTrust?: boolean;
}) {
// Check for at least one parameter being provided. This is necessary
// because this path can be called on startup to resolve our own ID before
// our phone number or UUID are known. The existing behavior in these
// cases can handle a returned `undefined` id, so we do that.
const normalizedUuid = uuid ? uuid.toLowerCase() : undefined;
const identifier = normalizedUuid || e164;
if ((!e164 && !uuid) || !identifier) {
return undefined;
}
const convoE164 = this.get(e164);
const convoUuid = this.get(normalizedUuid);
// 1. Handle no match at all
if (!convoE164 && !convoUuid) {
window.log.info(
'ensureContactIds: Creating new contact, no matches found'
);
const newConvo = this.getOrCreate(identifier, 'private');
if (highTrust && e164) {
newConvo.updateE164(e164);
}
if (normalizedUuid) {
newConvo.updateUuid(normalizedUuid);
}
if (highTrust && e164 && normalizedUuid) {
updateConversation(newConvo.attributes);
}
return newConvo.get('id');
// 2. Handle match on only E164
} else if (convoE164 && !convoUuid) {
const haveUuid = Boolean(normalizedUuid);
window.log.info(
`ensureContactIds: e164-only match found (have UUID: ${haveUuid})`
);
// If we are only searching based on e164 anyway, then return the first result
if (!normalizedUuid) {
return convoE164.get('id');
}
// Fill in the UUID for an e164-only contact
if (normalizedUuid && !convoE164.get('uuid')) {
if (highTrust) {
window.log.info('ensureContactIds: Adding UUID to e164-only match');
convoE164.updateUuid(normalizedUuid);
updateConversation(convoE164.attributes);
}
return convoE164.get('id');
}
window.log.info(
'ensureContactIds: e164 already had UUID, creating a new contact'
);
// If existing e164 match already has UUID, create a new contact...
const newConvo = this.getOrCreate(normalizedUuid, 'private');
if (highTrust) {
window.log.info(
'ensureContactIds: Moving e164 from old contact to new'
);
// Remove the e164 from the old contact...
convoE164.set({ e164: undefined });
updateConversation(convoE164.attributes);
// ...and add it to the new one.
newConvo.updateE164(e164);
updateConversation(newConvo.attributes);
}
return newConvo.get('id');
// 3. Handle match on only UUID
} else if (!convoE164 && convoUuid) {
window.log.info(
`ensureContactIds: UUID-only match found (have e164: ${Boolean(e164)})`
);
if (e164 && highTrust) {
convoUuid.updateE164(e164);
updateConversation(convoUuid.attributes);
}
return convoUuid.get('id');
}
// For some reason, TypeScript doesn't believe that we can trust that these two values
// are truthy by this point. So we'll throw if we get there.
if (!convoE164 || !convoUuid) {
throw new Error('ensureContactIds: convoE164 or convoUuid are falsey!');
}
// Now, we know that we have a match for both e164 and uuid checks
if (convoE164 === convoUuid) {
return convoUuid.get('id');
}
if (highTrust) {
// Conflict: If e164 match already has a UUID, we remove its e164.
if (convoE164.get('uuid') && convoE164.get('uuid') !== normalizedUuid) {
window.log.info(
'ensureContactIds: e164 match had different UUID than incoming pair, removing its e164.'
);
// Remove the e164 from the old contact...
convoE164.set({ e164: undefined });
updateConversation(convoE164.attributes);
// ...and add it to the new one.
convoUuid.updateE164(e164);
updateConversation(convoUuid.attributes);
return convoUuid.get('id');
}
window.log.warn(
`ensureContactIds: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Merging.`
);
// 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
this.combineContacts(convoUuid, convoE164)
.then(() => {
// If the old conversation was currently displayed, we load the new one
window.Whisper.events.trigger('refreshConversation', {
newId: convoUuid.get('id'),
oldId: convoE164.get('id'),
});
})
.catch(error => {
const errorText = error && error.stack ? error.stack : error;
window.log.warn(
`ensureContactIds error combining contacts: ${errorText}`
);
});
}
return convoUuid.get('id');
}
async checkForConflicts() {
window.log.info('checkForConflicts: starting...');
const byUuid = Object.create(null);
const byE164 = Object.create(null);
// We iterate from the oldest conversations to the newest. This allows us, in a
// conflict case, to keep the one with activity the most recently.
const models = [...this._conversations.models.reverse()];
const max = models.length;
for (let i = 0; i < max; i += 1) {
const conversation = models[i];
const uuid = conversation.get('uuid');
const e164 = conversation.get('e164');
if (uuid) {
const existing = byUuid[uuid];
if (!existing) {
byUuid[uuid] = conversation;
} else {
window.log.warn(
`checkForConflicts: Found conflict with uuid ${uuid}`
);
// Keep the newer one if it has an e164, otherwise keep existing
if (conversation.get('e164')) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(conversation, existing);
byUuid[uuid] = conversation;
} else {
// Keep existing - note that this applies if neither had an e164
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(existing, conversation);
}
}
}
if (e164) {
const existing = byE164[e164];
if (!existing) {
byE164[e164] = conversation;
} else {
// If we have two contacts with the same e164 but different truthy UUIDs, then
// we'll delete the e164 on the older one
if (
conversation.get('uuid') &&
existing.get('uuid') &&
conversation.get('uuid') !== existing.get('uuid')
) {
window.log.warn(
`checkForConflicts: Found two matches on e164 ${e164} with different truthy UUIDs. Dropping e164 on older.`
);
existing.set({ e164: undefined });
updateConversation(existing.attributes);
byE164[e164] = conversation;
// eslint-disable-next-line no-continue
continue;
}
window.log.warn(
`checkForConflicts: Found conflict with e164 ${e164}`
);
// Keep the newer one if it has a UUID, otherwise keep existing
if (conversation.get('uuid')) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(conversation, existing);
byE164[e164] = conversation;
} else {
// Keep existing - note that this applies if neither had a UUID
// eslint-disable-next-line no-await-in-loop
await this.combineContacts(existing, conversation);
}
}
}
}
window.log.info('checkForConflicts: complete!');
}
async combineContacts(
current: ConversationModelType,
obsolete: ConversationModelType
) {
const obsoleteId = obsolete.get('id');
const currentId = current.get('id');
window.log.warn('combineContacts: Combining two conversations', {
obsolete: obsoleteId,
current: currentId,
});
if (!current.get('profileKey') && obsolete.get('profileKey')) {
window.log.warn(
'combineContacts: Copying profile key from old to new contact'
);
await current.setProfileKey(obsolete.get('profileKey'));
}
window.log.warn(
'combineContacts: Delete all sessions tied to old conversationId'
);
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
obsoleteId
);
await Promise.all(
deviceIds.map(async deviceId => {
await window.textsecure.storage.protocol.removeSession(
`${obsoleteId}.${deviceId}`
);
})
);
window.log.warn(
'combineContacts: Delete all identity information tied to old conversationId'
);
await window.textsecure.storage.protocol.removeIdentityKey(obsoleteId);
window.log.warn(
'combineContacts: Ensure that all V1 groups have new conversationId instead of old'
);
const groups = await this.getAllGroupsInvolvingId(obsoleteId);
groups.forEach(group => {
const members = group.get('members');
const withoutObsolete = without(members, obsoleteId);
const currentAdded = uniq([...withoutObsolete, currentId]);
group.set({
members: currentAdded,
});
updateConversation(group.attributes);
});
// Note: we explicitly don't want to update V2 groups
window.log.warn(
'combineContacts: Delete the obsolete conversation from the database'
);
await removeConversation(obsoleteId, {
Conversation: window.Whisper.Conversation,
});
window.log.warn('combineContacts: Update messages table');
await migrateConversationMessages(obsoleteId, currentId);
window.log.warn(
'combineContacts: Eliminate old conversation from ConversationController lookups'
);
this._conversations.remove(obsolete);
this.regenerateLookups();
window.log.warn('combineContacts: Complete!', {
obsolete: obsoleteId,
current: currentId,
});
}
regenerateLookups() {
const models = [...this._conversations.models];
this.reset();
this._conversations.add(models);
// We force the initial fetch to be true
this._initialFetchComplete = true;
}
/**
* Given a groupId and optional additional initialization properties,
* ensures the existence of a group conversation and returns a string
* representing the local database ID of the group conversation.
*/
ensureGroup(groupId: string, additionalInitProps = {}) {
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
}
/**
* Given certain metadata about a message (an identifier of who wrote the
* message and the sent_at timestamp of the message) returns the
* conversation the message belongs to OR null if a conversation isn't
* found.
*/
async getConversationForTargetMessage(
targetFromId: string,
targetTimestamp: number
) {
const messages = await getMessagesBySentAt(targetTimestamp, {
MessageCollection: window.Whisper.MessageCollection,
});
const targetMessage = messages.find(m => {
const contact = m.getContact();
if (!contact) {
return false;
}
const mcid = contact.get('id');
return mcid === targetFromId;
});
if (targetMessage) {
return targetMessage.getConversation();
}
return null;
}
prepareForSend(
id: string,
options?: any
): {
wrap: (promise: Promise<any>) => Promise<void>;
sendOptions: SendOptionsType | undefined;
} {
// id is any valid conversation identifier
const conversation = this.get(id);
const sendOptions = conversation
? conversation.getSendOptions(options)
: undefined;
const wrap = conversation
? conversation.wrapSend.bind(conversation)
: async (promise: Promise<any>) => promise;
return { wrap, sendOptions };
}
async getAllGroupsInvolvingId(
conversationId: string
): Promise<Array<ConversationModelType>> {
const groups = await getAllGroupsInvolvingId(conversationId, {
ConversationCollection: window.Whisper.ConversationCollection,
});
return groups.map(group => this._conversations.add(group));
}
async loadPromise() {
return this._initialPromise;
}
reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
this._conversations.reset([]);
}
async load() {
window.log.info('ConversationController: starting initial fetch');
if (this._conversations.length) {
throw new Error('ConversationController: Already loaded!');
}
const load = async () => {
try {
const collection = await getAllConversations({
ConversationCollection: window.Whisper.ConversationCollection,
});
this._conversations.add(collection.models);
this._initialFetchComplete = true;
await Promise.all(
this._conversations.map(async conversation => {
if (!conversation.get('lastMessage')) {
await conversation.updateLastMessage();
}
// In case a too-large draft was saved to the database
const draft = conversation.get('draft');
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
conversation.set({
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
});
updateConversation(conversation.attributes);
}
})
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
this._initialPromise = load();
return this._initialPromise;
}
}

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

@ -1,7 +1,9 @@
import * as Backbone from 'backbone';
import { ColorType, LocalizerType } from './types/Util';
import { SendOptionsType } from './textsecure/SendMessage';
import { ConversationType } from './state/ducks/conversations';
import { CallingClass, CallHistoryDetailsType } from './services/calling';
import { SendOptionsType } from './textsecure/SendMessage';
import { SyncMessageClass } from './textsecure.d';
interface ModelAttributesInterface {
@ -74,6 +76,7 @@ declare class ConversationModelType extends Backbone.Model<
cachedProps: ConversationType;
initialPromise: Promise<any>;
addCallHistory(details: CallHistoryDetailsType): void;
applyMessageRequestResponse(
response: number,
options?: { fromSync: boolean }

View file

@ -16,7 +16,7 @@ import {
CallDetailsType,
} from '../state/ducks/calling';
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
import { ConversationType } from '../window.d';
import { ConversationModelType } from '../model-types.d';
import is from '@sindresorhus/is';
export {
@ -72,7 +72,7 @@ export class CallingClass {
}
async startOutgoingCall(
conversation: ConversationType,
conversation: ConversationModelType,
isVideoCall: boolean
) {
if (!this.uxActions) {
@ -218,7 +218,14 @@ export class CallingClass {
message: CallingMessageClass
): Promise<boolean> {
const conversation = window.ConversationController.get(remoteUserId);
const sendOptions = conversation ? conversation.getSendOptions() : {};
const sendOptions = conversation
? conversation.getSendOptions()
: undefined;
if (!window.textsecure.messaging) {
window.log.warn('handleOutgoingSignaling() returning false; offline');
return false;
}
try {
await window.textsecure.messaging.sendCallingMessage(
@ -292,7 +299,7 @@ export class CallingClass {
this.addCallHistoryForAutoEndedIncomingCall(conversation, reason);
}
private attachToCall(conversation: ConversationType, call: Call): void {
private attachToCall(conversation: ConversationModelType, call: Call): void {
const { uxActions } = this;
if (!uxActions) {
return;
@ -340,7 +347,7 @@ export class CallingClass {
}
private getRemoteUserIdFromConversation(
conversation: ConversationType
conversation: ConversationModelType
): UserId | undefined {
const recipients = conversation.getRecipients();
if (recipients.length !== 1) {
@ -366,8 +373,12 @@ export class CallingClass {
}
private async getCallSettings(
conversation: ConversationType
conversation: ConversationModelType
): Promise<CallSettings> {
if (!window.textsecure.messaging) {
throw new Error('getCallSettings: offline!');
}
const iceServerJson = await window.textsecure.messaging.server.getIceServers();
const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls());
@ -382,7 +393,7 @@ export class CallingClass {
}
private getUxCallDetails(
conversation: ConversationType,
conversation: ConversationModelType,
call: Call
): CallDetailsType {
return {
@ -398,7 +409,7 @@ export class CallingClass {
}
private addCallHistoryForEndedCall(
conversation: ConversationType,
conversation: ConversationModelType,
call: Call,
acceptedTime: number | undefined
) {
@ -429,7 +440,7 @@ export class CallingClass {
}
private addCallHistoryForFailedIncomingCall(
conversation: ConversationType,
conversation: ConversationModelType,
call: Call
) {
const callHistoryDetails: CallHistoryDetailsType = {
@ -444,7 +455,7 @@ export class CallingClass {
}
private addCallHistoryForAutoEndedIncomingCall(
conversation: ConversationType,
conversation: ConversationModelType,
_reason: CallEndedReason
) {
const callHistoryDetails: CallHistoryDetailsType = {

View file

@ -19,12 +19,15 @@ import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
import { createBatcher } from '../util/batcher';
import {
ConversationModelCollectionType,
ConversationModelType,
MessageModelCollectionType,
MessageModelType,
} from '../model-types.d';
import {
AttachmentDownloadJobType,
BackboneConversationCollectionType,
BackboneConversationModelType,
BackboneMessageCollectionType,
BackboneMessageModelType,
ClientInterface,
ClientJobType,
ConversationType,
@ -153,6 +156,7 @@ const dataInterface: ClientInterface = {
getOlderMessagesByConversation,
getNewerMessagesByConversation,
getMessageMetricsForConversation,
migrateConversationMessages,
getUnprocessedCount,
getAllUnprocessed,
@ -719,7 +723,7 @@ async function saveConversations(array: Array<ConversationType>) {
async function getConversationById(
id: string,
{ Conversation }: { Conversation: BackboneConversationModelType }
{ Conversation }: { Conversation: typeof ConversationModelType }
) {
const data = await channels.getConversationById(id);
@ -749,7 +753,7 @@ async function updateConversations(array: Array<ConversationType>) {
async function removeConversation(
id: string,
{ Conversation }: { Conversation: BackboneConversationModelType }
{ Conversation }: { Conversation: typeof ConversationModelType }
) {
const existing = await getConversationById(id, { Conversation });
@ -769,8 +773,8 @@ async function _removeConversations(ids: Array<string>) {
async function getAllConversations({
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
}) {
ConversationCollection: typeof ConversationModelCollectionType;
}): Promise<ConversationModelCollectionType> {
const conversations = await channels.getAllConversations();
const collection = new ConversationCollection();
@ -788,7 +792,7 @@ async function getAllConversationIds() {
async function getAllPrivateConversations({
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
ConversationCollection: typeof ConversationModelCollectionType;
}) {
const conversations = await channels.getAllPrivateConversations();
@ -803,7 +807,7 @@ async function getAllGroupsInvolvingId(
{
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
ConversationCollection: typeof ConversationModelCollectionType;
}
) {
const conversations = await channels.getAllGroupsInvolvingId(id);
@ -861,7 +865,7 @@ async function saveMessage(
{
forceSave,
Message,
}: { forceSave?: boolean; Message: BackboneMessageModelType }
}: { forceSave?: boolean; Message: typeof MessageModelType }
) {
const id = await channels.saveMessage(_cleanData(data), { forceSave });
Message.updateTimers();
@ -878,7 +882,7 @@ async function saveMessages(
async function removeMessage(
id: string,
{ Message }: { Message: BackboneMessageModelType }
{ Message }: { Message: typeof MessageModelType }
) {
const message = await getMessageById(id, { Message });
@ -897,7 +901,7 @@ async function _removeMessages(ids: Array<string>) {
async function getMessageById(
id: string,
{ Message }: { Message: BackboneMessageModelType }
{ Message }: { Message: typeof MessageModelType }
) {
const message = await channels.getMessageById(id);
if (!message) {
@ -911,7 +915,7 @@ async function getMessageById(
async function _getAllMessages({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels._getAllMessages();
@ -936,7 +940,7 @@ async function getMessageBySender(
sourceDevice: string;
sent_at: number;
},
{ Message }: { Message: BackboneMessageModelType }
{ Message }: { Message: typeof MessageModelType }
) {
const messages = await channels.getMessageBySender({
source,
@ -953,7 +957,9 @@ async function getMessageBySender(
async function getUnreadByConversation(
conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) {
const messages = await channels.getUnreadByConversation(conversationId);
@ -975,7 +981,7 @@ async function getOlderMessagesByConversation(
limit?: number;
receivedAt?: number;
messageId?: string;
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}
) {
const messages = await channels.getOlderMessagesByConversation(
@ -998,7 +1004,7 @@ async function getNewerMessagesByConversation(
}: {
limit?: number;
receivedAt?: number;
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}
) {
const messages = await channels.getNewerMessagesByConversation(
@ -1018,10 +1024,18 @@ async function getMessageMetricsForConversation(conversationId: string) {
return result;
}
async function migrateConversationMessages(
obsoleteId: string,
currentId: string
) {
await channels.migrateConversationMessages(obsoleteId, currentId);
}
async function removeAllMessagesInConversation(
conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) {
let messages;
do {
@ -1036,12 +1050,12 @@ async function removeAllMessagesInConversation(
return;
}
const ids = messages.map((message: BackboneMessageModelType) => message.id);
const ids = messages.map((message: MessageModelType) => message.id);
// Note: It's very important that these models are fully hydrated because
// we need to delete all associated on-disk files along with the database delete.
await Promise.all(
messages.map((message: BackboneMessageModelType) => message.cleanup())
messages.map(async (message: MessageModelType) => message.cleanup())
);
await channels.removeMessage(ids);
@ -1050,7 +1064,9 @@ async function removeAllMessagesInConversation(
async function getMessagesBySentAt(
sentAt: number,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) {
const messages = await channels.getMessagesBySentAt(sentAt);
@ -1060,7 +1076,7 @@ async function getMessagesBySentAt(
async function getExpiredMessages({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels.getExpiredMessages();
@ -1070,7 +1086,7 @@ async function getExpiredMessages({
async function getOutgoingWithoutExpiresAt({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels.getOutgoingWithoutExpiresAt();
@ -1080,7 +1096,7 @@ async function getOutgoingWithoutExpiresAt({
async function getNextExpiringMessage({
Message,
}: {
Message: BackboneMessageModelType;
Message: typeof MessageModelType;
}) {
const message = await channels.getNextExpiringMessage();
@ -1094,7 +1110,7 @@ async function getNextExpiringMessage({
async function getNextTapToViewMessageToAgeOut({
Message,
}: {
Message: BackboneMessageModelType;
Message: typeof MessageModelType;
}) {
const message = await channels.getNextTapToViewMessageToAgeOut();
if (!message) {
@ -1106,7 +1122,7 @@ async function getNextTapToViewMessageToAgeOut({
async function getTapToViewMessagesNeedingErase({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels.getTapToViewMessagesNeedingErase();

View file

@ -17,10 +17,12 @@ export type StickerPackType = any;
export type StickerType = any;
export type UnprocessedType = any;
export type BackboneConversationModelType = any;
export type BackboneConversationCollectionType = any;
export type BackboneMessageModelType = any;
export type BackboneMessageCollectionType = any;
import {
ConversationModelCollectionType,
ConversationModelType,
MessageModelCollectionType,
MessageModelType,
} from '../model-types.d';
export interface DataInterface {
close: () => Promise<void>;
@ -94,6 +96,10 @@ export interface DataInterface {
getMessageMetricsForConversation: (
conversationId: string
) => Promise<ConverationMetricsType>;
migrateConversationMessages: (
obsoleteId: string,
currentId: string
) => Promise<void>;
getUnprocessedCount: () => Promise<number>;
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
@ -242,33 +248,33 @@ export type ClientInterface = DataInterface & {
getAllConversations: ({
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
}) => Promise<Array<ConversationType>>;
ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>;
getAllGroupsInvolvingId: (
id: string,
{
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
ConversationCollection: typeof ConversationModelCollectionType;
}
) => Promise<Array<ConversationType>>;
) => Promise<ConversationModelCollectionType>;
getAllPrivateConversations: ({
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
}) => Promise<Array<ConversationType>>;
ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>;
getConversationById: (
id: string,
{ Conversation }: { Conversation: BackboneConversationModelType }
) => Promise<ConversationType>;
{ Conversation }: { Conversation: typeof ConversationModelType }
) => Promise<ConversationModelType>;
getExpiredMessages: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>;
getMessageById: (
id: string,
{ Message }: { Message: BackboneMessageModelType }
{ Message }: { Message: typeof MessageModelType }
) => Promise<MessageType | undefined>;
getMessageBySender: (
options: {
@ -277,63 +283,67 @@ export type ClientInterface = DataInterface & {
sourceDevice: string;
sent_at: number;
},
{ Message }: { Message: BackboneMessageModelType }
) => Promise<Array<MessageType>>;
{ Message }: { Message: typeof MessageModelType }
) => Promise<MessageModelType | null>;
getMessagesBySentAt: (
sentAt: number,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
) => Promise<Array<MessageType>>;
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>;
getOlderMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
receivedAt?: number;
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}
) => Promise<Array<MessageTypeUnhydrated>>;
) => Promise<MessageModelCollectionType>;
getNewerMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
receivedAt?: number;
MessageCollection: BackboneMessageCollectionType;
MessageCollection: typeof MessageModelCollectionType;
}
) => Promise<Array<MessageTypeUnhydrated>>;
) => Promise<MessageModelCollectionType>;
getNextExpiringMessage: ({
Message,
}: {
Message: BackboneMessageModelType;
}) => Promise<MessageType>;
Message: typeof MessageModelType;
}) => Promise<MessageModelType | null>;
getNextTapToViewMessageToAgeOut: ({
Message,
}: {
Message: BackboneMessageModelType;
}) => Promise<MessageType>;
Message: typeof MessageModelType;
}) => Promise<MessageModelType | null>;
getOutgoingWithoutExpiresAt: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>;
getTapToViewMessagesNeedingErase: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>;
getUnreadByConversation: (
conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
) => Promise<Array<MessageType>>;
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>;
removeConversation: (
id: string,
{ Conversation }: { Conversation: BackboneConversationModelType }
{ Conversation }: { Conversation: typeof ConversationModelType }
) => Promise<void>;
removeMessage: (
id: string,
{ Message }: { Message: BackboneMessageModelType }
{ Message }: { Message: typeof MessageModelType }
) => Promise<void>;
saveMessage: (
data: MessageType,
options: { forceSave?: boolean; Message: BackboneMessageModelType }
options: { forceSave?: boolean; Message: typeof MessageModelType }
) => Promise<number>;
updateConversation: (data: ConversationType) => void;
@ -342,15 +352,17 @@ export type ClientInterface = DataInterface & {
_getAllMessages: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>;
// Client-side only
shutdown: () => Promise<void>;
removeAllMessagesInConversation: (
conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<void>;
removeOtherData: () => Promise<void>;
cleanupOrphanedAttachments: () => Promise<void>;

View file

@ -131,6 +131,7 @@ const dataInterface: ServerInterface = {
getOlderMessagesByConversation,
getNewerMessagesByConversation,
getMessageMetricsForConversation,
migrateConversationMessages,
getUnprocessedCount,
getAllUnprocessed,
@ -2797,6 +2798,25 @@ async function getMessageMetricsForConversation(conversationId: string) {
}
getMessageMetricsForConversation.needsSerial = true;
async function migrateConversationMessages(
obsoleteId: string,
currentId: string
) {
const db = getInstance();
await db.run(
`UPDATE messages SET
conversationId = $currentId,
json = json_set(json, '$.conversationId', $currentId)
WHERE conversationId = $obsoleteId;`,
{
$obsoleteId: obsoleteId,
$currentId: currentId,
}
);
}
migrateConversationMessages.needsSerial = true;
async function getMessagesBySentAt(sentAt: number) {
const db = getInstance();
const rows = await db.all(

29
ts/textsecure.d.ts vendored
View file

@ -10,6 +10,8 @@ import EventTarget from './textsecure/EventTarget';
import { ByteBufferClass } from './window.d';
import { SendOptionsType } from './textsecure/SendMessage';
import { WebAPIType } from './textsecure/WebAPI';
import utils from './textsecure/Helpers';
import SendMessage from './textsecure/SendMessage';
type AttachmentType = any;
@ -79,31 +81,9 @@ export type TextSecureType = {
attachment: AttachmentPointerClass
) => Promise<DownloadAttachmentType>;
};
messaging: {
getStorageCredentials: () => Promise<StorageServiceCredentials>;
getStorageManifest: (
options: StorageServiceCallOptionsType
) => Promise<ArrayBuffer>;
getStorageRecords: (
data: ArrayBuffer,
options: StorageServiceCallOptionsType
) => Promise<ArrayBuffer>;
sendStickerPackSync: (
operations: Array<{
packId: string;
packKey: string;
installed: boolean;
}>,
options: Object
) => Promise<void>;
sendCallingMessage: (
recipientId: string,
callingMessage: CallingMessageClass,
sendOptions: SendOptionsType
) => Promise<void>;
server: WebAPIType;
};
messaging?: SendMessage;
protobuf: ProtobufCollectionType;
utils: typeof utils;
EventTarget: typeof EventTarget;
MessageReceiver: typeof MessageReceiver;
@ -150,6 +130,7 @@ export type StorageProtocolType = StorageType & {
verifiedStatus: number,
publicKey: ArrayBuffer
) => Promise<boolean>;
removeIdentityKey: (identifier: string) => Promise<void>;
saveIdentityWithAttributes: (
number: string,
options: IdentityKeyRecord

View file

@ -696,6 +696,7 @@ export default class AccountManager extends EventTarget {
const conversationId = window.ConversationController.ensureContactIds({
e164: number,
uuid,
highTrust: true,
});
if (!conversationId) {
throw new Error('registrationDone: no conversationId!');

View file

@ -1058,6 +1058,7 @@ class MessageReceiverInner extends EventTarget {
) {
const {
destination,
destinationUuid,
timestamp,
message: msg,
expirationStartTimestamp,
@ -1083,12 +1084,13 @@ class MessageReceiverInner extends EventTarget {
msg.flags &&
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
) {
if (!destination) {
const identifier = destination || destinationUuid;
if (!identifier) {
throw new Error(
'MessageReceiver.handleSentMessage: Cannot end session with falsey destination'
);
}
p = this.handleEndSession(destination);
p = this.handleEndSession(identifier);
}
return p.then(async () =>
this.processDecrypted(envelope, msg).then(message => {
@ -1120,6 +1122,7 @@ class MessageReceiverInner extends EventTarget {
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
destination,
destinationUuid,
timestamp: timestamp.toNumber(),
serverTimestamp: envelope.serverTimestamp,
device: envelope.sourceDevice,
@ -1303,7 +1306,8 @@ class MessageReceiverInner extends EventTarget {
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
timestamp: receiptMessage.timestamp[i].toNumber(),
reader: envelope.source || envelope.sourceUuid,
source: envelope.source,
sourceUuid: envelope.sourceUuid,
};
results.push(this.dispatchAndWait(ev));
}

View file

@ -925,7 +925,7 @@ export default class MessageSender {
async sendCallingMessage(
recipientId: string,
callingMessage: CallingMessageClass,
sendOptions: SendOptionsType
sendOptions?: SendOptionsType
) {
const recipients = [recipientId];
const finalTimestamp = Date.now();
@ -1001,7 +1001,11 @@ export default class MessageSender {
);
}
async syncReadMessages(
reads: Array<{ sender: string; timestamp: number }>,
reads: Array<{
senderUuid?: string;
senderE164?: string;
timestamp: number;
}>,
options?: SendOptionsType
) {
const myNumber = window.textsecure.storage.user.getNumber();
@ -1013,7 +1017,8 @@ export default class MessageSender {
for (let i = 0; i < reads.length; i += 1) {
const read = new window.textsecure.protobuf.SyncMessage.Read();
read.timestamp = reads[i].timestamp;
read.sender = reads[i].sender;
read.sender = reads[i].senderE164;
read.senderUuid = reads[i].senderUuid;
syncMessage.read.push(read);
}
@ -1352,20 +1357,20 @@ export default class MessageSender {
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
proto.timestamp = timestamp;
const identifier = e164 || uuid;
const identifier = uuid || e164;
const logError = (prefix: string) => (error: Error) => {
window.log.error(prefix, error && error.stack ? error.stack : error);
throw error;
};
const deleteAllSessions = async (targetNumber: string) =>
const deleteAllSessions = async (targetIdentifier: string) =>
window.textsecure.storage.protocol
.getDeviceIds(targetNumber)
.getDeviceIds(targetIdentifier)
.then(async deviceIds =>
Promise.all(
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
targetNumber,
targetIdentifier,
deviceId
);
window.log.info('deleting sessions for', address.toString());
@ -1401,7 +1406,7 @@ export default class MessageSender {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
// We already sent the reset session to our other devices in the code above!
if (e164 === myNumber || uuid === myUuid) {
if ((e164 && e164 === myNumber) || (uuid && uuid === myUuid)) {
return sendToContactPromise;
}

View file

@ -6,6 +6,7 @@ import { Agent } from 'https';
import is from '@sindresorhus/is';
import { redactPackId } from '../../js/modules/stickers';
import { getRandomValue } from '../Crypto';
import MessageSender from './SendMessage';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
@ -13,7 +14,6 @@ import { v4 as getGuid } from 'uuid';
import {
StorageServiceCallOptionsType,
StorageServiceCredentials,
TextSecureType,
} from '../textsecure.d';
// tslint:disable no-bitwise
@ -589,9 +589,9 @@ export type WebAPIType = {
getSenderCertificate: (withUuid?: boolean) => Promise<any>;
getSticker: (packId: string, stickerId: string) => Promise<any>;
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
getStorageCredentials: TextSecureType['messaging']['getStorageCredentials'];
getStorageManifest: TextSecureType['messaging']['getStorageManifest'];
getStorageRecords: TextSecureType['messaging']['getStorageRecords'];
getStorageCredentials: MessageSender['getStorageCredentials'];
getStorageManifest: MessageSender['getStorageManifest'];
getStorageRecords: MessageSender['getStorageRecords'];
makeProxiedRequest: (
targetUrl: string,
options?: ProxiedRequestOptionsType

View file

@ -160,22 +160,6 @@
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " async load() {",
"lineNumber": 306,
"reasonCategory": "falseMatch",
"updated": "2020-06-19T18:29:40.067Z"
},
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " this._initialPromise = load();",
"lineNumber": 348,
"reasonCategory": "falseMatch",
"updated": "2020-06-19T18:29:40.067Z"
},
{
"rule": "jQuery-$(",
"path": "js/debug_log_start.js",
@ -223,7 +207,7 @@
"rule": "jQuery-wrap(",
"path": "js/models/conversations.js",
"line": " await wrap(",
"lineNumber": 652,
"lineNumber": 663,
"reasonCategory": "falseMatch",
"updated": "2020-06-09T20:26:46.515Z"
},
@ -504,7 +488,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
"lineNumber": 90,
"lineNumber": 97,
"reasonCategory": "usageTrusted",
"updated": "2019-10-21T22:30:15.622Z",
"reasonDetail": "Known DOM elements"
@ -513,7 +497,7 @@
"rule": "jQuery-appendTo(",
"path": "js/views/inbox_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 99,
"lineNumber": 106,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements"
@ -522,7 +506,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
"lineNumber": 121,
"lineNumber": 128,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "<optional>"
@ -531,7 +515,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
"lineNumber": 121,
"lineNumber": 128,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "<optional>"
@ -540,7 +524,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 132,
"lineNumber": 139,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements"
@ -549,7 +533,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 132,
"lineNumber": 139,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements"
@ -558,7 +542,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
"lineNumber": 183,
"lineNumber": 190,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Known DOM elements"
@ -567,7 +551,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('#header, .gutter').addClass('inactive');",
"lineNumber": 187,
"lineNumber": 194,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Hardcoded selector"
@ -576,7 +560,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation-stack').addClass('inactive');",
"lineNumber": 191,
"lineNumber": 198,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Hardcoded selector"
@ -585,7 +569,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .menu').trigger('close');",
"lineNumber": 193,
"lineNumber": 200,
"reasonCategory": "usageTrusted",
"updated": "2020-05-28T17:42:35.329Z",
"reasonDetail": "Hardcoded selector"
@ -594,7 +578,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"lineNumber": 213,
"lineNumber": 220,
"reasonCategory": "usageTrusted",
"updated": "2020-05-29T18:29:18.234Z",
"reasonDetail": "Known DOM elements"
@ -603,7 +587,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .recorder').trigger('close');",
"lineNumber": 216,
"lineNumber": 223,
"reasonCategory": "usageTrusted",
"updated": "2020-05-29T18:29:18.234Z",
"reasonDetail": "Hardcoded selector"
@ -11875,4 +11859,4 @@
"reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z"
}
]
]

View file

@ -60,6 +60,8 @@ const excludedFiles = [
'^ts/Crypto.ts',
'^ts/textsecure/MessageReceiver.js',
'^ts/textsecure/MessageReceiver.ts',
'^ts/ConversationController.js',
'^ts/ConversationController.ts',
// Generated files
'^js/components.js',

View file

@ -17,7 +17,7 @@ import {
ManifestRecordClass,
StorageItemClass,
} from '../textsecure.d';
import { ConversationType } from '../window.d';
import { ConversationModelType } from '../model-types.d';
function fromRecordVerified(verified: number): number {
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
@ -35,6 +35,11 @@ function fromRecordVerified(verified: number): number {
async function fetchManifest(manifestVersion: string) {
window.log.info('storageService.fetchManifest');
if (!window.textsecure.messaging) {
throw new Error('fetchManifest: We are offline!');
}
try {
const credentials = await window.textsecure.messaging.getStorageCredentials();
window.storage.put('storageCredentials', credentials);
@ -286,6 +291,10 @@ async function processManifest(
const storageKeyBase64 = window.storage.get('storageKey');
const storageKey = base64ToArrayBuffer(storageKeyBase64);
if (!window.textsecure.messaging) {
throw new Error('processManifest: We are offline!');
}
const remoteKeysTypeMap = new Map();
manifest.keys.forEach(key => {
remoteKeysTypeMap.set(
@ -296,7 +305,7 @@ async function processManifest(
const localKeys = window
.getConversations()
.map((conversation: ConversationType) => conversation.get('storageID'))
.map((conversation: ConversationModelType) => conversation.get('storageID'))
.filter(Boolean);
window.log.info(
`storageService.processManifest localKeys.length ${localKeys.length}`

98
ts/window.d.ts vendored
View file

@ -1,6 +1,14 @@
// Captures the globals put in place by preload.js, background.js and others
import * as Backbone from 'backbone';
import * as Underscore from 'underscore';
import { Ref } from 'react';
import {
ConversationModelCollectionType,
ConversationModelType,
MessageModelCollectionType,
MessageModelType,
} from './model-types.d';
import {
LibSignalType,
SignalProtocolAddressClass,
@ -11,6 +19,7 @@ import { WebAPIConnectType } from './textsecure/WebAPI';
import { CallingClass, CallHistoryDetailsType } from './services/calling';
import * as Crypto from './Crypto';
import { ColorType, LocalizerType } from './types/Util';
import { ConversationController } from './ConversationController';
import { SendOptionsType } from './textsecure/SendMessage';
import Data from './sql/Client';
@ -19,18 +28,22 @@ type TaskResultType = any;
declare global {
interface Window {
dcodeIO: DCodeIOType;
getConversations: () => ConversationControllerType;
getExpiration: () => string;
getEnvironment: () => string;
getSocketStatus: () => number;
getAlwaysRelayCalls: () => Promise<boolean>;
getIncomingCallNotification: () => Promise<boolean>;
getCallRingtoneNotification: () => Promise<boolean>;
getCallSystemNotification: () => Promise<boolean>;
getMediaPermissions: () => Promise<boolean>;
getConversations: () => ConversationModelCollectionType;
getEnvironment: () => string;
getExpiration: () => string;
getGuid: () => string;
getInboxCollection: () => ConversationModelCollectionType;
getIncomingCallNotification: () => Promise<boolean>;
getMediaCameraPermissions: () => Promise<boolean>;
getMediaPermissions: () => Promise<boolean>;
getSocketStatus: () => number;
getTitle: () => string;
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
i18n: LocalizerType;
isValidGuid: (maybeGuid: string) => boolean;
libphonenumber: {
util: {
getRegionCodeForNumber: (number: string) => string;
@ -46,6 +59,7 @@ declare global {
platform: string;
restart: () => void;
showWindow: () => void;
setBadgeCount: (count: number) => void;
storage: {
put: (key: string, value: any) => void;
remove: (key: string) => void;
@ -55,7 +69,9 @@ declare global {
removeBlockedNumber: (number: string) => void;
};
textsecure: TextSecureType;
updateTrayIcon: (count: number) => void;
Backbone: typeof Backbone;
Signal: {
Crypto: typeof Crypto;
Data: typeof Data;
@ -69,7 +85,7 @@ declare global {
calling: CallingClass;
};
};
ConversationController: ConversationControllerType;
ConversationController: ConversationController;
WebAPI: WebAPIConnectType;
Whisper: WhisperType;
@ -82,68 +98,6 @@ declare global {
}
}
export type ConversationAttributes = {
e164?: string | null;
isArchived?: boolean;
profileFamilyName?: string | null;
profileKey?: string | null;
profileName?: string | null;
profileSharing?: boolean;
name?: string;
storageID?: string;
uuid?: string | null;
verified?: number;
};
export type ConversationType = {
attributes: ConversationAttributes;
fromRecordVerified: (
verified: ContactRecordIdentityState
) => ContactRecordIdentityState;
set: (props: Partial<ConversationAttributes>) => void;
updateE164: (e164?: string) => void;
updateUuid: (uuid?: string) => void;
id: string;
get: (key: string) => any;
getAvatarPath(): string | undefined;
getColor(): ColorType | undefined;
getName(): string | undefined;
getNumber(): string;
getProfiles(): Promise<Array<Promise<void>>>;
getProfileName(): string | undefined;
getRecipients: () => Array<string>;
getSendOptions(): SendOptionsType;
getTitle(): string;
isVerified(): boolean;
safeGetVerified(): Promise<number>;
getIsAddedByContact(): boolean;
addCallHistory(details: CallHistoryDetailsType): void;
toggleVerified(): Promise<TaskResultType>;
};
export type ConversationControllerType = {
getOrCreateAndWait: (
identifier: string,
type: 'private' | 'group'
) => Promise<ConversationType>;
getOrCreate: (
identifier: string,
type: 'private' | 'group'
) => ConversationType;
getConversationId: (identifier: string) => string | null;
ensureContactIds: (o: { e164?: string; uuid?: string }) => string;
getOurConversationId: () => string | null;
prepareForSend: (
id: string,
options: Object
) => {
wrap: (promise: Promise<any>) => Promise<void>;
sendOptions: Object;
};
get: (identifier: string) => null | ConversationType;
map: (mapFn: (conversation: ConversationType) => any) => any;
};
export type DCodeIOType = {
ByteBuffer: typeof ByteBufferClass;
Long: {
@ -212,7 +166,7 @@ export type LoggerType = (...args: Array<any>) => void;
export type WhisperType = {
events: {
trigger: (name: string, param1: any, param2: any) => void;
trigger: (name: string, param1: any, param2?: any) => void;
};
Database: {
open: () => Promise<IDBDatabase>;
@ -222,4 +176,8 @@ export type WhisperType = {
reject: Function
) => void;
};
ConversationCollection: typeof ConversationModelCollectionType;
Conversation: typeof ConversationModelType;
MessageCollection: typeof MessageModelCollectionType;
Message: typeof MessageModelType;
};