diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 6f62b5e2ed..9bf97b0939 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -13,6 +13,10 @@ import { SendOptionsType, CallbackResultType } from './textsecure/SendMessage'; import { ConversationModel } from './models/conversations'; import { maybeDeriveGroupV2Id } from './groups'; import { assert } from './util/assert'; +import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation'; +import { deprecated } from './util/deprecated'; +import { getSendOptions } from './util/getSendOptions'; +import { handleMessageSend } from './util/handleMessageSend'; const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; @@ -237,7 +241,7 @@ export class ConversationController { } try { - if (conversation.isGroupV1()) { + if (isGroupV1(conversation.attributes)) { await maybeDeriveGroupV2Id(conversation); } await saveConversation(conversation.attributes); @@ -556,7 +560,7 @@ export class ConversationController { } let groupV2Id: undefined | string; - if (conversation.isGroupV1()) { + if (isGroupV1(conversation.attributes)) { // eslint-disable-next-line no-await-in-loop await maybeDeriveGroupV2Id(conversation); groupV2Id = conversation.get('derivedGroupV2Id'); @@ -564,7 +568,7 @@ export class ConversationController { groupV2Id, 'checkForConflicts: expected the group V2 ID to have been derived, but it was falsy' ); - } else if (conversation.isGroupV2()) { + } else if (isGroupV2(conversation.attributes)) { groupV2Id = conversation.get('groupId'); } @@ -573,7 +577,7 @@ export class ConversationController { if (!existing) { byGroupV2Id[groupV2Id] = conversation; } else { - const logParenthetical = conversation.isGroupV1() + const logParenthetical = isGroupV1(conversation.attributes) ? ' (derived from a GV1 group ID)' : ''; window.log.warn( @@ -581,7 +585,10 @@ export class ConversationController { ); // Prefer the GV2 group. - if (conversation.isGroupV2() && !existing.isGroupV2()) { + if ( + isGroupV2(conversation.attributes) && + !isGroupV2(existing.attributes) + ) { // eslint-disable-next-line no-await-in-loop await this.combineConversations(conversation, existing); byGroupV2Id[groupV2Id] = conversation; @@ -730,16 +737,14 @@ export class ConversationController { ) => Promise; sendOptions: SendOptionsType | undefined; }> { + deprecated('prepareForSend'); // id is any valid conversation identifier const conversation = this.get(id); const sendOptions = conversation - ? await conversation.getSendOptions(options) + ? await getSendOptions(conversation.attributes, options) : undefined; - const wrap = conversation - ? conversation.wrapSend.bind(conversation) - : async (promise: Promise) => promise; - return { wrap, sendOptions }; + return { wrap: handleMessageSend, sendOptions }; } async getAllGroupsInvolvingId( diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index 6ffbbe8c50..dd89aca075 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -43,6 +43,7 @@ import { UnprocessedType, UnprocessedUpdateType, } from './textsecure/Types.d'; +import { getSendOptions } from './util/getSendOptions'; const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds @@ -1230,7 +1231,7 @@ export class SignalProtocolStore extends EventsMixin { await this.archiveSession(id); // Send a null message with newly-created session - const sendOptions = await conversation.getSendOptions(); + const sendOptions = await getSendOptions(conversation.attributes); await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions); } catch (error) { // If we failed to do the session reset, then we'll allow another attempt sooner diff --git a/ts/background.ts b/ts/background.ts index c2a8242fd5..82bb743de5 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -35,6 +35,8 @@ import { } from './textsecure/MessageReceiver'; import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; import * as universalExpireTimer from './util/universalExpireTimer'; +import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; +import { getSendOptions } from './util/getSendOptions'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -1955,7 +1957,7 @@ export async function startApp(): Promise { .getConversations() .filter(c => Boolean( - c.isPrivate() && + isDirectConversation(c.attributes) && c.get('e164') && !c.get('uuid') && !c.isEverUnregistered() @@ -2502,7 +2504,10 @@ export async function startApp(): Promise { } // We drop typing notifications in groups we're not a part of - if (!conversation.isPrivate() && !conversation.hasMember(ourId)) { + if ( + !isDirectConversation(conversation.attributes) && + !conversation.hasMember(ourId) + ) { window.log.warn( `Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.` ); @@ -2703,7 +2708,7 @@ export async function startApp(): Promise { id, 'group' ); - if (conversation.isGroupV2()) { + if (isGroupV2(conversation.attributes)) { window.log.warn( 'Got group sync for v2 group: ', conversation.idForLogging() @@ -3578,7 +3583,7 @@ export async function startApp(): Promise { ); const plaintext = PlaintextContent.from(message); - const options = await conversation.getSendOptions(); + const options = await getSendOptions(conversation.attributes); const result = await window.textsecure.messaging.sendRetryRequest({ plaintext, options, diff --git a/ts/groups.ts b/ts/groups.ts index 8684750fd5..609a6dee8c 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -79,6 +79,13 @@ import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from '../js/modules/type import { ConversationModel } from './models/conversations'; import { getGroupSizeHardLimit } from './groups/limits'; import { ourProfileKeyService } from './services/ourProfileKey'; +import { + isGroupV1 as getIsGroupV1, + isGroupV2 as getIsGroupV2, + isMe, +} from './util/whatTypeOfConversation'; +import { handleMessageSend } from './util/handleMessageSend'; +import { getSendOptions } from './util/getSendOptions'; export { joinViaLink } from './groups/joinViaLink'; @@ -1231,7 +1238,7 @@ export async function modifyGroupV2({ }): Promise { const idLog = `${name}/${conversation.idForLogging()}`; - if (!conversation.isGroupV2()) { + if (!getIsGroupV2(conversation.attributes)) { throw new Error( `modifyGroupV2/${idLog}: Called for non-GroupV2 conversation` ); @@ -1297,13 +1304,13 @@ export async function modifyGroupV2({ ? await ourProfileKeyService.get() : undefined; - const sendOptions = await conversation.getSendOptions(); + const sendOptions = await getSendOptions(conversation.attributes); const timestamp = Date.now(); const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; - const promise = conversation.wrapSend( + const promise = handleMessageSend( window.Signal.Util.sendToGroup( { groupV2: conversation.getGroupV2Info({ @@ -1676,7 +1683,7 @@ export async function createGroupV2({ const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; - const sendOptions = await conversation.getSendOptions(); + const sendOptions = await getSendOptions(conversation.attributes); await wrapWithSyncMessageSend({ conversation, @@ -1729,7 +1736,7 @@ export async function hasV1GroupBeenMigrated( conversation: ConversationModel ): Promise { const logId = conversation.idForLogging(); - const isGroupV1 = conversation.isGroupV1(); + const isGroupV1 = getIsGroupV1(conversation.attributes); if (!isGroupV1) { window.log.warn( `checkForGV2Existence/${logId}: Called for non-GroupV1 conversation!` @@ -1766,7 +1773,7 @@ export async function hasV1GroupBeenMigrated( export async function maybeDeriveGroupV2Id( conversation: ConversationModel ): Promise { - const isGroupV1 = conversation.isGroupV1(); + const isGroupV1 = getIsGroupV1(conversation.attributes); const groupV1Id = conversation.get('groupId'); const derived = conversation.get('derivedGroupV2Id'); @@ -1797,7 +1804,7 @@ type MigratePropsType = { export async function isGroupEligibleToMigrate( conversation: ConversationModel ): Promise { - if (!conversation.isGroupV1()) { + if (!getIsGroupV1(conversation.attributes)) { return false; } @@ -1860,7 +1867,7 @@ export async function getGroupMigrationMembers( `getGroupMigrationMembers/${logId}: membersV2 - missing local contact for ${e164}, skipping.` ); } - if (!contact.isMe() && window.GV2_MIGRATION_DISABLE_ADD) { + if (!isMe(contact.attributes) && window.GV2_MIGRATION_DISABLE_ADD) { window.log.warn( `getGroupMigrationMembers/${logId}: membersV2 - skipping ${e164} due to GV2_MIGRATION_DISABLE_ADD flag` ); @@ -1947,7 +1954,7 @@ export async function getGroupMigrationMembers( return null; } - if (!contact.isMe() && window.GV2_MIGRATION_DISABLE_INVITE) { + if (!isMe(contact.attributes) && window.GV2_MIGRATION_DISABLE_INVITE) { window.log.warn( `getGroupMigrationMembers/${logId}: pendingMembersV2 - skipping ${e164} due to GV2_MIGRATION_DISABLE_INVITE flag` ); @@ -2173,7 +2180,7 @@ export async function initiateMigrationToGroupV2( }); } catch (error) { const logId = conversation.idForLogging(); - if (!conversation.isGroupV1()) { + if (!getIsGroupV1(conversation.attributes)) { throw error; } @@ -2203,7 +2210,7 @@ export async function initiateMigrationToGroupV2( const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; - const sendOptions = await conversation.getSendOptions(); + const sendOptions = await getSendOptions(conversation.attributes); await wrapWithSyncMessageSend({ conversation, @@ -2361,7 +2368,7 @@ export async function joinGroupV2ViaLinkAndMigrate({ inviteLinkPassword: string; revision: number; }): Promise { - const isGroupV1 = conversation.isGroupV1(); + const isGroupV1 = getIsGroupV1(conversation.attributes); const previousGroupV1Id = conversation.get('groupId'); if (!isGroupV1 || !previousGroupV1Id) { @@ -2451,7 +2458,7 @@ export async function respondToGroupV2Migration({ // Ensure we have the credentials we need before attempting GroupsV2 operations await maybeFetchNewCredentials(); - const isGroupV1 = conversation.isGroupV1(); + const isGroupV1 = getIsGroupV1(conversation.attributes); const previousGroupV1Id = conversation.get('groupId'); if (!isGroupV1 || !previousGroupV1Id) { diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index d0eafd08b9..eecf3edd4b 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -13,6 +13,7 @@ import { } from '../groups'; import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; +import { isGroupV1 } from '../util/whatTypeOfConversation'; import type { GroupJoinInfoClass } from '../textsecure.d'; import type { ConversationAttributesType } from '../model-types.d'; @@ -285,7 +286,7 @@ export async function joinViaLink(hash: string): Promise { ); } - if (targetConversation.isGroupV1()) { + if (isGroupV1(targetConversation.attributes)) { await targetConversation.joinGroupV2ViaLinkAndMigrate({ approvalRequired, inviteLinkPassword, diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index d65a41cdd9..a29fc3b7ff 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -14,11 +14,7 @@ import { VerificationOptions, } from '../model-types.d'; import { CallMode, CallHistoryDetailsType } from '../types/Calling'; -import { - CallbackResultType, - GroupV2InfoType, - SendOptionsType, -} from '../textsecure/SendMessage'; +import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage'; import { ConversationType } from '../state/ducks/conversations'; import { AvatarColorType, @@ -58,6 +54,13 @@ import { updateConversationsWithUuidLookup } from '../updateConversationsWithUui import { filter, map, take } from '../util/iterables'; import * as universalExpireTimer from '../util/universalExpireTimer'; import { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions'; +import { + isDirectConversation, + isGroupV1, + isGroupV2, + isMe, +} from '../util/whatTypeOfConversation'; +import { deprecated } from '../util/deprecated'; /* eslint-disable more/no-then */ window.Whisper = window.Whisper || {}; @@ -173,12 +176,12 @@ export class ConversationModel extends window.Backbone } idForLogging(): string { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { const uuid = this.get('uuid'); const e164 = this.get('e164'); return `${uuid || e164} (${this.id})`; } - if (this.isGroupV2()) { + if (isGroupV2(this.attributes)) { return `groupv2(${this.get('groupId')})`; } @@ -239,7 +242,7 @@ export class ConversationModel extends window.Backbone this.debouncedUpdateLastMessage, this ); - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { this.contactCollection.on( 'change:verified', this.onMemberVerifiedChange.bind(this) @@ -324,46 +327,13 @@ export class ConversationModel extends window.Backbone } } - isMe(): boolean { - const e164 = this.get('e164'); - const uuid = this.get('uuid'); - return Boolean( - (e164 && e164 === this.ourNumber) || (uuid && uuid === this.ourUuid) - ); - } - - isGroupV1(): boolean { - const groupId = this.get('groupId'); - if (!groupId) { - return false; - } - - const buffer = fromEncodedBinaryToArrayBuffer(groupId); - return buffer.byteLength === window.Signal.Groups.ID_V1_LENGTH; - } - - isGroupV2(): boolean { - const groupId = this.get('groupId'); - if (!groupId) { - return false; - } - - const groupVersion = this.get('groupVersion') || 0; - - try { - return ( - groupVersion === 2 && - base64ToArrayBuffer(groupId).byteLength === - window.Signal.Groups.ID_LENGTH - ); - } catch (error) { - window.log.error('isGroupV2: Failed to process groupId in base64!'); - return false; - } + isPrivate(): boolean { + deprecated('isPrivate()'); + return isDirectConversation(this.attributes); } isMemberRequestingToJoin(conversationId: string): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return false; } const pendingAdminApprovalV2 = this.get('pendingAdminApprovalV2'); @@ -378,7 +348,7 @@ export class ConversationModel extends window.Backbone } isMemberPending(conversationId: string): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return false; } const pendingMembersV2 = this.get('pendingMembersV2'); @@ -394,7 +364,7 @@ export class ConversationModel extends window.Backbone } isMemberAwaitingApproval(conversationId: string): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return false; } const pendingAdminApprovalV2 = this.get('pendingAdminApprovalV2'); @@ -410,7 +380,7 @@ export class ConversationModel extends window.Backbone } isMember(conversationId: string): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { throw new Error( `isMember: Called for non-GroupV2 conversation ${this.idForLogging()}` ); @@ -736,7 +706,7 @@ export class ConversationModel extends window.Backbone async toggleAdminChange( conversationId: string ): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return undefined; } @@ -807,7 +777,7 @@ export class ConversationModel extends window.Backbone isSMSOnly(): boolean { return isConversationSMSOnly({ ...this.attributes, - type: this.isPrivate() ? 'direct' : 'unknown', + type: isDirectConversation(this.attributes) ? 'direct' : 'unknown', }); } @@ -835,7 +805,7 @@ export class ConversationModel extends window.Backbone isGroupV1AndDisabled(): boolean { return ( - this.isGroupV1() && + isGroupV1(this.attributes) && window.Signal.RemoteConfig.isEnabled('desktop.disableGV1') ); } @@ -1030,7 +1000,7 @@ export class ConversationModel extends window.Backbone } async fetchLatestGroupV2Data(): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -1084,11 +1054,15 @@ export class ConversationModel extends window.Backbone } isValid(): boolean { - return this.isPrivate() || this.isGroupV1() || this.isGroupV2(); + return ( + isDirectConversation(this.attributes) || + isGroupV1(this.attributes) || + isGroupV2(this.attributes) + ); } async maybeMigrateV1Group(): Promise { - if (!this.isGroupV1()) { + if (!isGroupV1(this.attributes)) { return; } @@ -1133,7 +1107,7 @@ export class ConversationModel extends window.Backbone includePendingMembers?: boolean; extraConversationsForSend?: Array; } = {}): GroupV2InfoType | undefined { - if (this.isPrivate() || !this.isGroupV2()) { + if (isDirectConversation(this.attributes) || !isGroupV2(this.attributes)) { return undefined; } return { @@ -1152,8 +1126,11 @@ export class ConversationModel extends window.Backbone } getGroupV1Info(): WhatIsThis { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (this.isPrivate() || this.get('groupVersion')! > 0) { + if ( + isDirectConversation(this.attributes) || + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.get('groupVersion')! > 0 + ) { return undefined; } @@ -1170,10 +1147,10 @@ export class ConversationModel extends window.Backbone return undefined; } - if (this.isGroupV1()) { + if (isGroupV1(this.attributes)) { return fromEncodedBinaryToArrayBuffer(groupIdString); } - if (this.isGroupV2()) { + if (isGroupV2(this.attributes)) { return base64ToArrayBuffer(groupIdString); } @@ -1186,17 +1163,19 @@ export class ConversationModel extends window.Backbone } // We don't send typing messages to our other devices - if (this.isMe()) { + if (isMe(this.attributes)) { return; } await this.queueJob(async () => { - const recipientId = this.isPrivate() ? this.getSendTarget() : undefined; + const recipientId = isDirectConversation(this.attributes) + ? this.getSendTarget() + : undefined; const groupId = this.getGroupIdBuffer(); const groupMembers = this.getRecipients(); // We don't send typing messages if our recipients list is empty - if (!this.isPrivate() && !groupMembers.length) { + if (!isDirectConversation(this.attributes) && !groupMembers.length) { return; } @@ -1214,9 +1193,9 @@ export class ConversationModel extends window.Backbone const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; - const sendOptions = await this.getSendOptions(); - if (this.isPrivate()) { - this.wrapSend( + const sendOptions = await getSendOptions(this.attributes); + if (isDirectConversation(this.attributes)) { + handleMessageSend( window.textsecure.messaging.sendMessageProtoAndWait( timestamp, groupMembers, @@ -1230,7 +1209,7 @@ export class ConversationModel extends window.Backbone ) ); } else { - this.wrapSend( + handleMessageSend( window.Signal.Util.sendContentMessageToGroup({ contentHint: ContentHint.SUPPLEMENTARY, contentMessage, @@ -1432,13 +1411,13 @@ export class ConversationModel extends window.Backbone const ourConversationId = window.ConversationController.getOurConversationId(); let groupVersion: undefined | 1 | 2; - if (this.isGroupV1()) { + if (isGroupV1(this.attributes)) { groupVersion = 1; - } else if (this.isGroupV2()) { + } else if (isGroupV2(this.attributes)) { groupVersion = 2; } - const sortedGroupMembers = this.isGroupV2() + const sortedGroupMembers = isGroupV2(this.attributes) ? this.getMembers() .sort((left, right) => sortConversationTitles(left, right, this.intlCollator) @@ -1486,9 +1465,9 @@ export class ConversationModel extends window.Backbone inboxPosition, isArchived: this.get('isArchived')!, isBlocked: this.isBlocked(), - isMe: this.isMe(), + isMe: isMe(this.attributes), isGroupV1AndDisabled: this.isGroupV1AndDisabled(), - isGroupV2Capable: this.isPrivate() + isGroupV2Capable: isDirectConversation(this.attributes) ? Boolean(this.get('capabilities')?.gv2) : undefined, isPinned: this.get('isPinned'), @@ -1525,11 +1504,11 @@ export class ConversationModel extends window.Backbone sortedGroupMembers, timestamp, title: this.getTitle()!, - searchableTitle: this.isMe() + searchableTitle: isMe(this.attributes) ? window.i18n('noteToSelf') : this.getTitle(), unreadCount: this.get('unreadCount')! || 0, - ...(this.isPrivate() + ...(isDirectConversation(this.attributes) ? { type: 'direct' as const, sharedGroupNames: this.get('sharedGroupNames') || [], @@ -1590,7 +1569,7 @@ export class ConversationModel extends window.Backbone } getMembersCount(): number | undefined { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return undefined; } @@ -1725,11 +1704,14 @@ export class ConversationModel extends window.Backbone } if (isLocalAction) { - if (this.isGroupV1() || this.isPrivate()) { + if ( + isGroupV1(this.attributes) || + isDirectConversation(this.attributes) + ) { this.sendProfileKeyUpdate(); } else if ( ourConversationId && - this.isGroupV2() && + isGroupV2(this.attributes) && this.isMemberPending(ourConversationId) ) { await this.modifyGroupV2({ @@ -1739,7 +1721,7 @@ export class ConversationModel extends window.Backbone }); } else if ( ourConversationId && - this.isGroupV2() && + isGroupV2(this.attributes) && this.isMember(ourConversationId) ) { window.log.info( @@ -1757,9 +1739,12 @@ export class ConversationModel extends window.Backbone this.disableProfileSharing({ viaStorageServiceSync }); if (isLocalAction) { - if (this.isGroupV1() || this.isPrivate()) { + if ( + isGroupV1(this.attributes) || + isDirectConversation(this.attributes) + ) { await this.leaveGroup(); - } else if (this.isGroupV2()) { + } else if (isGroupV2(this.attributes)) { await this.leaveGroupV2(); } } @@ -1774,9 +1759,12 @@ export class ConversationModel extends window.Backbone if (isLocalAction) { this.trigger('unload', 'deleted from message request'); - if (this.isGroupV1() || this.isPrivate()) { + if ( + isGroupV1(this.attributes) || + isDirectConversation(this.attributes) + ) { await this.leaveGroup(); - } else if (this.isGroupV2()) { + } else if (isGroupV2(this.attributes)) { await this.leaveGroupV2(); } } @@ -1793,9 +1781,12 @@ export class ConversationModel extends window.Backbone if (isLocalAction) { this.trigger('unload', 'blocked and deleted from message request'); - if (this.isGroupV1() || this.isPrivate()) { + if ( + isGroupV1(this.attributes) || + isDirectConversation(this.attributes) + ) { await this.leaveGroup(); - } else if (this.isGroupV2()) { + } else if (isGroupV2(this.attributes)) { await this.leaveGroupV2(); } } @@ -1940,7 +1931,7 @@ export class ConversationModel extends window.Backbone if ( ourConversationId && - this.isGroupV2() && + isGroupV2(this.attributes) && this.isMemberPending(ourConversationId) ) { await this.modifyGroupV2({ @@ -1949,7 +1940,7 @@ export class ConversationModel extends window.Backbone }); } else if ( ourConversationId && - this.isGroupV2() && + isGroupV2(this.attributes) && this.isMember(ourConversationId) ) { await this.modifyGroupV2({ @@ -1964,7 +1955,7 @@ export class ConversationModel extends window.Backbone } async toggleAdmin(conversationId: string): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -1984,7 +1975,10 @@ export class ConversationModel extends window.Backbone async approvePendingMembershipFromGroupV2( conversationId: string ): Promise { - if (this.isGroupV2() && this.isMemberRequestingToJoin(conversationId)) { + if ( + isGroupV2(this.attributes) && + this.isMemberRequestingToJoin(conversationId) + ) { await this.modifyGroupV2({ name: 'approvePendingApprovalRequest', createGroupChange: () => @@ -1996,7 +1990,7 @@ export class ConversationModel extends window.Backbone async revokePendingMembershipsFromGroupV2( conversationIds: Array ): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -2026,20 +2020,26 @@ export class ConversationModel extends window.Backbone } async removeFromGroupV2(conversationId: string): Promise { - if (this.isGroupV2() && this.isMemberRequestingToJoin(conversationId)) { + if ( + isGroupV2(this.attributes) && + this.isMemberRequestingToJoin(conversationId) + ) { await this.modifyGroupV2({ name: 'denyPendingApprovalRequest', createGroupChange: () => this.denyPendingApprovalRequest(conversationId), extraConversationsForSend: [conversationId], }); - } else if (this.isGroupV2() && this.isMemberPending(conversationId)) { + } else if ( + isGroupV2(this.attributes) && + this.isMemberPending(conversationId) + ) { await this.modifyGroupV2({ name: 'removePendingMember', createGroupChange: () => this.removePendingMember([conversationId]), extraConversationsForSend: [conversationId], }); - } else if (this.isGroupV2() && this.isMember(conversationId)) { + } else if (isGroupV2(this.attributes) && this.isMember(conversationId)) { await this.modifyGroupV2({ name: 'removeFromGroup', createGroupChange: () => this.removeMember(conversationId), @@ -2123,7 +2123,7 @@ export class ConversationModel extends window.Backbone } async updateVerified(): Promise { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { await this.initialPromise; const verified = await this.safeGetVerified(); @@ -2140,7 +2140,7 @@ export class ConversationModel extends window.Backbone await Promise.all( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.contactCollection!.map(async contact => { - if (!contact.isMe()) { + if (!isMe(contact.attributes)) { await contact.updateVerified(); } }) @@ -2180,7 +2180,7 @@ export class ConversationModel extends window.Backbone // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { VERIFIED, UNVERIFIED } = this.verifiedEnum!; - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { throw new Error( 'You cannot verify a group conversation. ' + 'You must verify individual contacts.' @@ -2258,12 +2258,12 @@ export class ConversationModel extends window.Backbone this.ourNumber || this.ourUuid!, { syncMessage: true } ); - const contactSendOptions = await this.getSendOptions(); + const contactSendOptions = await getSendOptions(this.attributes); const options = { ...sendOptions, ...contactSendOptions }; const promise = window.textsecure.storage.protocol.loadIdentityKey(e164); return promise.then(key => - this.wrapSend( + handleMessageSend( window.textsecure.messaging.syncVerification( e164, uuid, @@ -2277,7 +2277,7 @@ export class ConversationModel extends window.Backbone } isVerified(): boolean { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.get('verified') === this.verifiedEnum!.VERIFIED; } @@ -2288,7 +2288,7 @@ export class ConversationModel extends window.Backbone // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.contactCollection!.every(contact => { - if (contact.isMe()) { + if (isMe(contact.attributes)) { return true; } return contact.isVerified(); @@ -2296,7 +2296,7 @@ export class ConversationModel extends window.Backbone } isUnverified(): boolean { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { const verified = this.get('verified'); return ( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2315,7 +2315,7 @@ export class ConversationModel extends window.Backbone // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.contactCollection!.any(contact => { - if (contact.isMe()) { + if (isMe(contact.attributes)) { return false; } return contact.isUnverified(); @@ -2323,7 +2323,7 @@ export class ConversationModel extends window.Backbone } getUnverified(): Backbone.Collection { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return this.isUnverified() ? new window.Backbone.Collection([this]) : new window.Backbone.Collection(); @@ -2331,7 +2331,7 @@ export class ConversationModel extends window.Backbone return new window.Backbone.Collection( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.contactCollection!.filter(contact => { - if (contact.isMe()) { + if (isMe(contact.attributes)) { return false; } return contact.isUnverified(); @@ -2340,7 +2340,7 @@ export class ConversationModel extends window.Backbone } async setApproved(): Promise { - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { throw new Error( 'You cannot set a group conversation as trusted. ' + 'You must set individual contacts as trusted.' @@ -2359,7 +2359,7 @@ export class ConversationModel extends window.Backbone } isUntrusted(): boolean { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return this.safeIsUntrusted(); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2369,7 +2369,7 @@ export class ConversationModel extends window.Backbone // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.contactCollection!.any(contact => { - if (contact.isMe()) { + if (isMe(contact.attributes)) { return false; } return contact.safeIsUntrusted(); @@ -2377,7 +2377,7 @@ export class ConversationModel extends window.Backbone } getUntrusted(): Backbone.Collection { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { if (this.isUntrusted()) { return new window.Backbone.Collection([this]); } @@ -2387,7 +2387,7 @@ export class ConversationModel extends window.Backbone return new window.Backbone.Collection( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.contactCollection!.filter(contact => { - if (contact.isMe()) { + if (isMe(contact.attributes)) { return false; } return contact.isUntrusted(); @@ -2417,7 +2417,7 @@ export class ConversationModel extends window.Backbone return false; } - if (!this.isGroupV1() && !this.isPrivate()) { + if (!isGroupV1(this.attributes) && !isDirectConversation(this.attributes)) { return false; } @@ -2595,7 +2595,7 @@ export class ConversationModel extends window.Backbone const options = providedOptions || {}; window._.defaults(options, { local: true }); - if (this.isMe()) { + if (isMe(this.attributes)) { window.log.info( 'refusing to add verified change advisory for our own number' ); @@ -2638,7 +2638,7 @@ export class ConversationModel extends window.Backbone this.trigger('newmessage', model); - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { window.ConversationController.getAllGroupsInvolvingId(this.id).then( groups => { window._.forEach(groups, group => { @@ -2752,7 +2752,7 @@ export class ConversationModel extends window.Backbone this.trigger('newmessage', model); - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { window.ConversationController.getAllGroupsInvolvingId(this.id).then( groups => { window._.forEach(groups, group => { @@ -2792,7 +2792,7 @@ export class ConversationModel extends window.Backbone } async maybeSetPendingUniversalTimer(): Promise { - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { return; } @@ -2897,7 +2897,7 @@ export class ConversationModel extends window.Backbone } validateNumber(): string | null { - if (this.isPrivate() && this.get('e164')) { + if (isDirectConversation(this.attributes) && this.get('e164')) { const regionCode = window.storage.get('regionCode'); const number = window.libphonenumber.util.parseNumber( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -2920,7 +2920,7 @@ export class ConversationModel extends window.Backbone } validateUuid(): string | null { - if (this.isPrivate() && this.get('uuid')) { + if (isDirectConversation(this.attributes) && this.get('uuid')) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (window.isValidGuid(this.get('uuid')!)) { return null; @@ -2944,7 +2944,7 @@ export class ConversationModel extends window.Backbone } isAdmin(conversationId: string): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return false; } @@ -2963,7 +2963,7 @@ export class ConversationModel extends window.Backbone conversationId: string; isAdmin: boolean; }> { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return []; } @@ -2976,7 +2976,7 @@ export class ConversationModel extends window.Backbone } getGroupLink(): string | undefined { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return undefined; } @@ -2991,7 +2991,7 @@ export class ConversationModel extends window.Backbone addedByUserId?: string; conversationId: string; }> { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return []; } @@ -3003,7 +3003,7 @@ export class ConversationModel extends window.Backbone } private getPendingApprovalMemberships(): Array<{ conversationId: string }> { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return []; } @@ -3052,7 +3052,9 @@ export class ConversationModel extends window.Backbone // Eliminate ourself return window._.compact( - unique.map(member => (member.isMe() ? null : member.getSendTarget())) + unique.map(member => + isMe(member.attributes) ? null : member.getSendTarget() + ) ); } @@ -3246,7 +3248,7 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown) as typeof window.Whisper.MessageAttributesType; - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { attributes.destination = destination; } @@ -3259,7 +3261,7 @@ export class ConversationModel extends window.Backbone throw new Error('Cannot send DOE while offline!'); } - const options = await this.getSendOptions(); + const options = await getSendOptions(this.attributes); const promise = (async () => { let profileKey: ArrayBuffer | undefined; @@ -3271,7 +3273,7 @@ export class ConversationModel extends window.Backbone ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return window.textsecure.messaging.sendMessageToIdentifier( destination, undefined, // body @@ -3308,7 +3310,7 @@ export class ConversationModel extends window.Backbone // anything to the database. message.doNotSave = true; - const result = await message.send(this.wrapSend(promise)); + const result = await message.send(handleMessageSend(promise)); if (!message.hasSuccessfulDelivery()) { // This is handled by `conversation_view` which displays a toast on @@ -3380,7 +3382,7 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown) as typeof window.Whisper.MessageAttributesType; - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { attributes.destination = destination; } @@ -3403,7 +3405,7 @@ export class ConversationModel extends window.Backbone } // Special-case the self-send case - we send only a sync message - if (this.isMe()) { + if (isMe(this.attributes)) { const dataMessage = await window.textsecure.messaging.getDataMessage({ attachments: [], // body @@ -3422,13 +3424,13 @@ export class ConversationModel extends window.Backbone return result; } - const options = await this.getSendOptions(); + const options = await getSendOptions(this.attributes); const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; const promise = (() => { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return window.textsecure.messaging.sendMessageToIdentifier( destination, undefined, // body @@ -3464,7 +3466,7 @@ export class ConversationModel extends window.Backbone ); })(); - const result = await message.send(this.wrapSend(promise)); + const result = await message.send(handleMessageSend(promise)); if (!message.hasSuccessfulDelivery()) { // This is handled by `conversation_view` which displays a toast on @@ -3514,7 +3516,7 @@ export class ConversationModel extends window.Backbone await window.textsecure.messaging.sendProfileKeyUpdate( profileKey, recipients, - await this.getSendOptions(), + await getSendOptions(this.attributes), this.get('groupId') ); } @@ -3584,7 +3586,7 @@ export class ConversationModel extends window.Backbone bodyRanges: mentions, }); - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { messageWithSchema.destination = destination; } const attributes: MessageModel = { @@ -3656,7 +3658,7 @@ export class ConversationModel extends window.Backbone } // Special-case the self-send case - we send only a sync message - if (this.isMe()) { + if (isMe(this.attributes)) { const dataMessage = await window.textsecure.messaging.getDataMessage({ attachments: finalAttachments, body: messageBody, @@ -3674,7 +3676,7 @@ export class ConversationModel extends window.Backbone } const conversationType = this.get('type'); - const options = await this.getSendOptions(); + const options = await getSendOptions(this.attributes); const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; @@ -3718,27 +3720,15 @@ export class ConversationModel extends window.Backbone ); } - return message.send(this.wrapSend(promise)); + return message.send(handleMessageSend(promise)); }); } - async wrapSend( - promise: Promise - ): Promise { - return handleMessageSend(promise); - } - - async getSendOptions( - options: { syncMessage?: boolean } = {} - ): Promise { - return getSendOptions(this.attributes, options); - } - // Is this someone who is a contact, or are we sharing our profile with them? // Or is the person who added us to this group a contact or are we sharing profile // with them? isFromOrAddedByTrustedContact(): boolean { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return Boolean(this.get('name')) || this.get('profileSharing'); } @@ -3753,7 +3743,7 @@ export class ConversationModel extends window.Backbone } return Boolean( - conv.isMe() || conv.get('name') || conv.get('profileSharing') + isMe(conv.attributes) || conv.get('name') || conv.get('profileSharing') ); } @@ -3863,7 +3853,7 @@ export class ConversationModel extends window.Backbone } async refreshGroupLink(): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -3886,7 +3876,7 @@ export class ConversationModel extends window.Backbone } async toggleGroupLink(value: boolean): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -3944,7 +3934,7 @@ export class ConversationModel extends window.Backbone } async updateAccessControlAddFromInviteLink(value: boolean): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -3973,7 +3963,7 @@ export class ConversationModel extends window.Backbone } async updateAccessControlAttributes(value: number): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -3998,7 +3988,7 @@ export class ConversationModel extends window.Backbone } async updateAccessControlMembers(value: number): Promise { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return; } @@ -4028,7 +4018,7 @@ export class ConversationModel extends window.Backbone receivedAt?: number, options: { fromSync?: unknown; fromGroupUpdate?: unknown } = {} ): Promise { - if (this.isGroupV2()) { + if (isGroupV2(this.attributes)) { if (providedSource || receivedAt) { throw new Error( 'updateExpirationTimer: GroupV2 timers are not updated this way' @@ -4100,7 +4090,7 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown) as MessageAttributesType); - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { model.set({ destination: this.getSendTarget() }); } const id = await window.Signal.Data.saveMessage(model.attributes, { @@ -4117,7 +4107,7 @@ export class ConversationModel extends window.Backbone return message; } - const sendOptions = await this.getSendOptions(); + const sendOptions = await getSendOptions(this.attributes); let profileKey; if (this.get('profileSharing')) { @@ -4126,7 +4116,7 @@ export class ConversationModel extends window.Backbone let promise; - if (this.isMe()) { + if (isMe(this.attributes)) { const flags = window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; const dataMessage = await window.textsecure.messaging.getDataMessage({ @@ -4147,7 +4137,7 @@ export class ConversationModel extends window.Backbone return message.sendSyncMessageOnly(dataMessage); } - if (this.get('type') === 'private') { + if (isDirectConversation(this.attributes)) { promise = window.textsecure.messaging.sendExpirationTimerUpdateToIdentifier( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.getSendTarget()!, @@ -4168,7 +4158,7 @@ export class ConversationModel extends window.Backbone ); } - await message.send(this.wrapSend(promise)); + await message.send(handleMessageSend(promise)); return message; } @@ -4195,7 +4185,7 @@ export class ConversationModel extends window.Backbone // TODO: DESKTOP-722 } as unknown) as MessageAttributesType); - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { model.set({ destination: this.id }); } const id = await window.Signal.Data.saveMessage(model.attributes, { @@ -4215,7 +4205,7 @@ export class ConversationModel extends window.Backbone } async endSession(): Promise { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { const now = Date.now(); const model = new window.Whisper.Message(({ conversationId: this.id, @@ -4283,9 +4273,9 @@ export class ConversationModel extends window.Backbone const message = window.MessageController.register(model.id, model); this.addSingleMessage(message); - const options = await this.getSendOptions(); + const options = await getSendOptions(this.attributes); message.send( - this.wrapSend( + handleMessageSend( window.textsecure.messaging.leaveGroup( groupId, groupIdentifiers, @@ -4318,10 +4308,10 @@ export class ConversationModel extends window.Backbone // This is an expensive operation we use to populate the message request hero row. It // shows groups the current user has in common with this potential new contact. async updateSharedGroups(): Promise { - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { return; } - if (this.isMe()) { + if (isMe(this.attributes)) { return; } @@ -4342,7 +4332,7 @@ export class ConversationModel extends window.Backbone } onChangeProfileKey(): void { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { this.getProfiles(); } } @@ -4428,7 +4418,7 @@ export class ConversationModel extends window.Backbone )); } - const { sendMetadata = {} } = await c.getSendOptions(); + const { sendMetadata = {} } = await getSendOptions(c.attributes); const getInfo = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sendMetadata[c.get('uuid')!] || sendMetadata[c.get('e164')!] || {}; @@ -4650,7 +4640,7 @@ export class ConversationModel extends window.Backbone // first/last name in their profile data. const nameChanged = oldName !== newName; - if (!this.isMe() && hadPreviousName && nameChanged) { + if (!isMe(this.attributes) && hadPreviousName && nameChanged) { const change = { type: 'name', oldName, @@ -4666,7 +4656,7 @@ export class ConversationModel extends window.Backbone return; } - if (this.isMe()) { + if (isMe(this.attributes)) { window.storage.put('avatarUrl', avatarPath); } @@ -4802,7 +4792,7 @@ export class ConversationModel extends window.Backbone } getTitle(): string { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return ( this.get('name') || this.getProfileName() || @@ -4814,7 +4804,7 @@ export class ConversationModel extends window.Backbone } getProfileName(): string | undefined { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return Util.combineNames( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.get('profileName')!, @@ -4826,7 +4816,7 @@ export class ConversationModel extends window.Backbone } getNumber(): string { - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { return ''; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -4866,12 +4856,8 @@ export class ConversationModel extends window.Backbone return initials.slice(0, 2).join(''); } - isPrivate(): boolean { - return this.get('type') === 'private'; - } - getColor(): AvatarColorType { - if (!this.isPrivate()) { + if (!isDirectConversation(this.attributes)) { return 'ultramarine'; } @@ -4912,7 +4898,7 @@ export class ConversationModel extends window.Backbone } private getAvatarPath(): undefined | string { - const avatar = this.isMe() + const avatar = isMe(this.attributes) ? this.get('profileAvatar') || this.get('avatar') : this.get('avatar') || this.get('profileAvatar'); return avatar?.path || undefined; @@ -4940,7 +4926,7 @@ export class ConversationModel extends window.Backbone } private canChangeTimer(): boolean { - if (this.isPrivate()) { + if (isDirectConversation(this.attributes)) { return true; } @@ -4948,7 +4934,7 @@ export class ConversationModel extends window.Backbone return false; } - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return true; } @@ -4967,7 +4953,7 @@ export class ConversationModel extends window.Backbone } canEditGroupInfo(): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return false; } @@ -4983,7 +4969,7 @@ export class ConversationModel extends window.Backbone } areWeAdmin(): boolean { - if (!this.isGroupV2()) { + if (!isGroupV2(this.attributes)) { return false; } @@ -5101,7 +5087,7 @@ export class ConversationModel extends window.Backbone const senderName = sender ? sender.getTitle() : window.i18n('unknownContact'); - const senderTitle = this.isPrivate() + const senderTitle = isDirectConversation(this.attributes) ? senderName : window.i18n('notificationSenderInGroup', { sender: senderName, @@ -5112,7 +5098,7 @@ export class ConversationModel extends window.Backbone const avatar = this.get('avatar') || this.get('profileAvatar'); if (avatar && avatar.path) { notificationIconUrl = getAbsoluteAttachmentPath(avatar.path); - } else if (this.isPrivate()) { + } else if (isDirectConversation(this.attributes)) { notificationIconUrl = await this.getIdenticon(); } else { // Not technically needed, but helps us be explicit: we don't show an icon for a @@ -5463,7 +5449,7 @@ window.Whisper.GroupMemberConversation = window.Backbone.Model.extend({ }, isMe() { - return this.conversation.isMe(); + return isMe(this.conversation.attributes); }, }); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 91e8d7b507..bfbaf616a1 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -57,6 +57,14 @@ import { MIMEType } from '../types/MIME'; import { LinkPreviewType } from '../types/message/LinkPreviews'; import { ourProfileKeyService } from '../services/ourProfileKey'; import { markRead, setToExpire } from '../services/MessageUpdater'; +import { + isDirectConversation, + isGroupV1, + isGroupV2, + isMe, +} from '../util/whatTypeOfConversation'; +import { handleMessageSend } from '../util/handleMessageSend'; +import { getSendOptions } from '../util/getSendOptions'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -744,7 +752,9 @@ export class MessageModel extends window.Backbone.Model { getPropsForSafetyNumberNotification(): SafetyNumberNotificationProps { const conversation = this.getConversation(); - const isGroup = Boolean(conversation && !conversation.isPrivate()); + const isGroup = Boolean( + conversation && !isDirectConversation(conversation.attributes) + ); const identifier = this.get('key_changed'); const contact = this.findAndFormatContact(identifier); @@ -981,7 +991,8 @@ export class MessageModel extends window.Backbone.Model { : undefined; const conversation = this.getConversation(); - const isGroup = conversation && !conversation.isPrivate(); + const isGroup = + conversation && !isDirectConversation(conversation.attributes); const sticker = this.get('sticker'); const isTapToView = this.isTapToView(); @@ -1316,7 +1327,7 @@ export class MessageModel extends window.Backbone.Model { let authorTitle: string; let isFromMe: boolean; - if (contact && contact.isPrivate()) { + if (contact && isDirectConversation(contact.attributes)) { const contactPhoneNumber = contact.get('e164'); authorId = contact.id; @@ -1328,7 +1339,7 @@ export class MessageModel extends window.Backbone.Model { : undefined; authorProfileName = contact.getProfileName(); authorTitle = contact.getTitle(); - isFromMe = contact.isMe(); + isFromMe = isMe(contact.attributes); } else { window.log.warn( 'getPropsForQuote: contact was missing. This may indicate a bookkeeping error or bad data from another client. Returning a placeholder contact.' @@ -1529,7 +1540,7 @@ export class MessageModel extends window.Backbone.Model { return { text: '' }; } - if (fromContact.isMe()) { + if (isMe(fromContact.attributes)) { messages.push(window.i18n('youUpdatedTheGroup')); } else { messages.push(window.i18n('updatedTheGroup', [fromContact.getTitle()])); @@ -1540,7 +1551,7 @@ export class MessageModel extends window.Backbone.Model { window.ConversationController.getOrCreate(item, 'private') ); const joinedWithoutMe = joinedContacts.filter( - contact => !contact.isMe() + contact => !isMe(contact.attributes) ); if (joinedContacts.length > 1) { @@ -1558,7 +1569,7 @@ export class MessageModel extends window.Backbone.Model { groupUpdate.joined[0], 'private' ); - if (joinedContact.isMe()) { + if (isMe(joinedContact.attributes)) { messages.push(window.i18n('youJoinedTheGroup')); } else { messages.push( @@ -2282,13 +2293,13 @@ export class MessageModel extends window.Backbone.Model { } let promise; - const options = await conversation.getSendOptions(); + const options = await getSendOptions(conversation.attributes); const { ContentHint, } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; - if (conversation.isPrivate()) { + if (isDirectConversation(conversation.attributes)) { const [identifier] = recipients; promise = window.textsecure.messaging.sendMessageToIdentifier( identifier, @@ -2351,7 +2362,7 @@ export class MessageModel extends window.Backbone.Model { ); } - return this.send(conversation.wrapSend(promise)); + return this.send(handleMessageSend(promise)); } // eslint-disable-next-line class-methods-use-this @@ -2534,7 +2545,7 @@ export class MessageModel extends window.Backbone.Model { sendOptions, } = await window.ConversationController.prepareForSend(identifier); const group = - groupId && parentConversation?.isGroupV1() + groupId && isGroupV1(parentConversation?.attributes) ? { id: groupId, type: window.textsecure.protobuf.GroupContext.Type.DELIVER, @@ -2576,7 +2587,9 @@ export class MessageModel extends window.Backbone.Model { [identifier], contentMessage, ContentHint.RESENDABLE, - groupId && parentConversation?.isGroupV2() ? groupId : undefined, + groupId && isGroupV2(parentConversation?.attributes) + ? groupId + : undefined, sendOptions ); @@ -2767,7 +2780,7 @@ export class MessageModel extends window.Backbone.Model { retryOptions: options, }); - const sendOptions = await conv.getSendOptions(); + const sendOptions = await getSendOptions(conv.attributes); // We don't have to check `sent_to` here, because: // @@ -2776,11 +2789,11 @@ export class MessageModel extends window.Backbone.Model { // in a single request to the server. So partial success is not // possible. await this.send( - conv.wrapSend( + handleMessageSend( // TODO: DESKTOP-724 // resetSession returns `Array` which is incompatible with the - // expected promise return values. `[]` is truthy and wrapSend assumes - // it's a valid callback result type + // expected promise return values. `[]` is truthy and handleMessageSend + // assumes it's a valid callback result type // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore window.textsecure.messaging.resetSession( @@ -3601,7 +3614,7 @@ export class MessageModel extends window.Backbone.Model { // GroupV2 if (initialMessage.groupV2) { - if (conversation.isGroupV1()) { + if (isGroupV1(conversation.attributes)) { // If we received a GroupV2 message in a GroupV1 group, we migrate! const { revision, groupChange } = initialMessage.groupV2; @@ -3660,7 +3673,7 @@ export class MessageModel extends window.Backbone.Model { e164: source, uuid: sourceUuid, })!; - const isGroupV2 = Boolean(initialMessage.groupV2); + const hasGroupV2Prop = Boolean(initialMessage.groupV2); const isV1GroupUpdate = initialMessage.group && initialMessage.group.type !== @@ -3670,8 +3683,8 @@ export class MessageModel extends window.Backbone.Model { // after applying the message's associated group changes. if ( type === 'incoming' && - !conversation.isPrivate() && - isGroupV2 && + !isDirectConversation(conversation.attributes) && + hasGroupV2Prop && (conversation.get('left') || !conversation.hasMember(ourConversationId) || !conversation.hasMember(senderId)) @@ -3690,8 +3703,8 @@ export class MessageModel extends window.Backbone.Model { // messages. We detect that via a missing 'members' field. if ( type === 'incoming' && - !conversation.isPrivate() && - !isGroupV2 && + !isDirectConversation(conversation.attributes) && + !hasGroupV2Prop && !isV1GroupUpdate && conversation.get('members') && (conversation.get('left') || !conversation.hasMember(ourConversationId)) @@ -3705,7 +3718,7 @@ export class MessageModel extends window.Backbone.Model { // Because GroupV1 messages can now be multiplexed into GroupV2 conversations, we // drop GroupV1 updates in GroupV2 groups. - if (isV1GroupUpdate && conversation.isGroupV2()) { + if (isV1GroupUpdate && isGroupV2(conversation.attributes)) { window.log.warn( `Received GroupV1 update in GroupV2 conversation ${conversation.idForLogging()}. Dropping.` ); @@ -3793,7 +3806,7 @@ export class MessageModel extends window.Backbone.Model { }; // GroupV1 - if (!isGroupV2 && dataMessage.group) { + if (!hasGroupV2Prop && dataMessage.group) { const pendingGroupUpdate = []; const memberConversations: Array = await Promise.all( dataMessage.group.membersE164.map((e164: string) => @@ -3920,7 +3933,7 @@ export class MessageModel extends window.Backbone.Model { return; } - if (sender.isMe()) { + if (isMe(sender.attributes)) { attributes.left = true; pendingGroupUpdate.push(['left', 'You']); } else { @@ -3962,7 +3975,7 @@ export class MessageModel extends window.Backbone.Model { message.set({ expireTimer: dataMessage.expireTimer }); } - if (!isGroupV2) { + if (!hasGroupV2Prop) { if (message.isExpirationTimerUpdate()) { message.set({ expirationTimerUpdate: { @@ -4023,7 +4036,7 @@ export class MessageModel extends window.Backbone.Model { sourceUuid === window.textsecure.storage.user.getUuid() ) { conversation.set({ profileSharing: true }); - } else if (conversation.isPrivate()) { + } else if (isDirectConversation(conversation.attributes)) { conversation.setProfileKey(profileKey); } else { const localId = window.ConversationController.ensureContactIds({ @@ -4214,7 +4227,7 @@ export class MessageModel extends window.Backbone.Model { } // A sync'd message to ourself is automatically considered read/delivered - if (isFirstRun && conversation.isMe()) { + if (isFirstRun && isMe(conversation.attributes)) { message.set({ read_by: conversation.getRecipients(), delivered_to: conversation.getRecipients(), diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 0168f912cc..bb43a708a2 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -69,6 +69,7 @@ import { REQUESTED_VIDEO_FRAMERATE, } from '../calling/constants'; import { notify } from './notify'; +import { getSendOptions } from '../util/getSendOptions'; const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map< HttpMethod, @@ -783,7 +784,7 @@ export class CallingClass { } const groupV2 = conversation.getGroupV2Info(); - const sendOptions = await conversation.getSendOptions(); + const sendOptions = await getSendOptions(conversation.attributes); if (!groupV2) { window.log.error( 'Unable to send group call update message for conversation that lacks groupV2 info' @@ -1408,7 +1409,7 @@ export class CallingClass { ): Promise { const conversation = window.ConversationController.get(remoteUserId); const sendOptions = conversation - ? await conversation.getSendOptions() + ? await getSendOptions(conversation.attributes) : undefined; if (!window.textsecure.messaging) { diff --git a/ts/services/storage.ts b/ts/services/storage.ts index 646eec9352..4726350046 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -35,6 +35,10 @@ import { sleep } from '../util/sleep'; import { isMoreRecentThan } from '../util/timestamp'; import { isStorageWriteFeatureEnabled } from '../storage/isFeatureEnabled'; import { ourProfileKeyService } from './ourProfileKey'; +import { + ConversationTypes, + typeofConversation, +} from '../util/whatTypeOfConversation'; const { eraseStorageServiceStateFromConversations, @@ -152,22 +156,24 @@ async function generateManifest( const identifier = new window.textsecure.protobuf.ManifestRecord.Identifier(); let storageRecord; - if (conversation.isMe()) { + + const conversationType = typeofConversation(conversation.attributes); + if (conversationType === ConversationTypes.Me) { storageRecord = new window.textsecure.protobuf.StorageRecord(); // eslint-disable-next-line no-await-in-loop storageRecord.account = await toAccountRecord(conversation); identifier.type = ITEM_TYPE.ACCOUNT; - } else if (conversation.isPrivate()) { + } else if (conversationType === ConversationTypes.Direct) { storageRecord = new window.textsecure.protobuf.StorageRecord(); // eslint-disable-next-line no-await-in-loop storageRecord.contact = await toContactRecord(conversation); identifier.type = ITEM_TYPE.CONTACT; - } else if (conversation.isGroupV2()) { + } else if (conversationType === ConversationTypes.GroupV2) { storageRecord = new window.textsecure.protobuf.StorageRecord(); // eslint-disable-next-line no-await-in-loop storageRecord.groupV2 = await toGroupV2Record(conversation); identifier.type = ITEM_TYPE.GROUPV2; - } else if (conversation.isGroupV1()) { + } else if (conversationType === ConversationTypes.GroupV1) { storageRecord = new window.textsecure.protobuf.StorageRecord(); // eslint-disable-next-line no-await-in-loop storageRecord.groupV1 = await toGroupV1Record(conversation); diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 49cb63bf13..901881f9bf 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -43,6 +43,7 @@ import { set as setUniversalExpireTimer, } from '../util/universalExpireTimer'; import { ourProfileKeyService } from './ourProfileKey'; +import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation'; const { updateConversation } = dataInterface; @@ -241,7 +242,7 @@ export async function toAccountRecord( uuid: pinnedConversation.get('uuid'), e164: pinnedConversation.get('e164'), }; - } else if (pinnedConversation.isGroupV1()) { + } else if (isGroupV1(pinnedConversation.attributes)) { pinnedConversationRecord.identifier = 'legacyGroupId'; const groupId = pinnedConversation.get('groupId'); if (!groupId) { @@ -252,7 +253,7 @@ export async function toAccountRecord( pinnedConversationRecord.legacyGroupId = fromEncodedBinaryToArrayBuffer( groupId ); - } else if (pinnedConversation.isGroupV2()) { + } else if (isGroupV2(pinnedConversation.attributes)) { pinnedConversationRecord.identifier = 'groupMasterKey'; const masterKey = pinnedConversation.get('masterKey'); if (!masterKey) { @@ -508,7 +509,7 @@ export async function mergeGroupV1Record( // where the binary representation of its ID matches a v2 record in memory. // Here we ensure that the record we're about to process is GV1 otherwise // we drop the update. - if (conversation && !conversation.isGroupV1()) { + if (conversation && !isGroupV1(conversation.attributes)) { throw new Error( `Record has group type mismatch ${conversation.idForLogging()}` ); @@ -565,7 +566,7 @@ export async function mergeGroupV1Record( let hasPendingChanges: boolean; - if (conversation.isGroupV1()) { + if (isGroupV1(conversation.attributes)) { addUnknownFields(groupV1Record, conversation); hasPendingChanges = doesRecordHavePendingChanges( @@ -684,7 +685,7 @@ export async function mergeGroupV2Record( const isFirstSync = !window.storage.get('storageFetchComplete'); const dropInitialJoinMessage = isFirstSync; - if (conversation.isGroupV1()) { + if (isGroupV1(conversation.attributes)) { // If we found a GroupV1 conversation from this incoming GroupV2 record, we need to // migrate it! diff --git a/ts/util/deprecated.ts b/ts/util/deprecated.ts new file mode 100644 index 0000000000..e24f263780 --- /dev/null +++ b/ts/util/deprecated.ts @@ -0,0 +1,11 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { getEnvironment, Environment } from '../environment'; +import * as log from '../logging/log'; + +export function deprecated(message?: string): void { + if (getEnvironment() === Environment.Development) { + log.error(new Error(`This method is deprecated: ${message}`)); + } +} diff --git a/ts/util/getSendOptions.ts b/ts/util/getSendOptions.ts index f6a4dc565e..19e556042d 100644 --- a/ts/util/getSendOptions.ts +++ b/ts/util/getSendOptions.ts @@ -40,9 +40,10 @@ export async function getSendOptions( if (!conversation) { return; } - const { - sendMetadata: conversationSendMetadata, - } = await conversation.getSendOptions(options); + const { sendMetadata: conversationSendMetadata } = await getSendOptions( + conversation.attributes, + options + ); Object.assign(sendMetadata, conversationSendMetadata || {}); }) ); diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index d960fc7442..40bf22b052 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -38,6 +38,7 @@ import { import { ContentClass } from '../textsecure.d'; import { assert } from './assert'; +import { isGroupV2 } from './whatTypeOfConversation'; const ERROR_EXPIRED_OR_MISSING_DEVICES = 409; const ERROR_STALE_DEVICES = 410; @@ -116,7 +117,7 @@ export async function sendContentMessageToGroup({ if ( ourConversation?.get('capabilities')?.senderKey && - conversation.isGroupV2() + isGroupV2(conversation.attributes) ) { try { return await sendToGroupViaSenderKey({ @@ -138,7 +139,7 @@ export async function sendContentMessageToGroup({ } } - const groupId = conversation.isGroupV2() + const groupId = isGroupV2(conversation.attributes) ? conversation.get('groupId') : undefined; return window.textsecure.messaging.sendGroupProto( @@ -191,7 +192,7 @@ export async function sendToGroupViaSenderKey(options: { } const groupId = conversation.get('groupId'); - if (!groupId || !conversation.isGroupV2()) { + if (!groupId || !isGroupV2(conversation.attributes)) { throw new Error( `sendToGroupViaSenderKey/${logId}: Missing groupId or group is not GV2` ); diff --git a/ts/util/shouldRespondWithProfileKey.ts b/ts/util/shouldRespondWithProfileKey.ts index 136f7c2362..7f8f0029db 100644 --- a/ts/util/shouldRespondWithProfileKey.ts +++ b/ts/util/shouldRespondWithProfileKey.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: AGPL-3.0-only import { ConversationModel } from '../models/conversations'; +import { isMe } from './whatTypeOfConversation'; export async function shouldRespondWithProfileKey( sender: ConversationModel ): Promise { - if (sender.isMe() || !sender.getAccepted() || sender.isBlocked()) { + if (isMe(sender.attributes) || !sender.getAccepted() || sender.isBlocked()) { return false; } diff --git a/ts/util/whatTypeOfConversation.ts b/ts/util/whatTypeOfConversation.ts index bcd724971d..ac03274b44 100644 --- a/ts/util/whatTypeOfConversation.ts +++ b/ts/util/whatTypeOfConversation.ts @@ -2,6 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-only import { ConversationAttributesType } from '../model-types.d'; +import { base64ToArrayBuffer, fromEncodedBinaryToArrayBuffer } from '../Crypto'; + +export enum ConversationTypes { + Me = 'Me', + Direct = 'Direct', + GroupV1 = 'GroupV1', + GroupV2 = 'GroupV2', +} export function isDirectConversation( conversationAttrs: ConversationAttributesType @@ -15,3 +23,56 @@ export function isMe(conversationAttrs: ConversationAttributesType): boolean { const ourUuid = window.textsecure.storage.user.getUuid(); return Boolean((e164 && e164 === ourNumber) || (uuid && uuid === ourUuid)); } + +export function isGroupV1( + conversationAttrs: ConversationAttributesType +): boolean { + const { groupId } = conversationAttrs; + if (!groupId) { + return false; + } + + const buffer = fromEncodedBinaryToArrayBuffer(groupId); + return buffer.byteLength === window.Signal.Groups.ID_V1_LENGTH; +} + +export function isGroupV2( + conversationAttrs: ConversationAttributesType +): boolean { + const { groupId, groupVersion = 0 } = conversationAttrs; + if (!groupId) { + return false; + } + + try { + return ( + groupVersion === 2 && + base64ToArrayBuffer(groupId).byteLength === window.Signal.Groups.ID_LENGTH + ); + } catch (error) { + window.log.error('isGroupV2: Failed to process groupId in base64!'); + return false; + } +} + +export function typeofConversation( + conversationAttrs: ConversationAttributesType +): ConversationTypes | undefined { + if (isMe(conversationAttrs)) { + return ConversationTypes.Me; + } + + if (isDirectConversation(conversationAttrs)) { + return ConversationTypes.Direct; + } + + if (isGroupV2(conversationAttrs)) { + return ConversationTypes.GroupV2; + } + + if (isGroupV1(conversationAttrs)) { + return ConversationTypes.GroupV1; + } + + return undefined; +} diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index f756aa080d..e8c561f82b 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -15,6 +15,12 @@ import { maybeParseUrl } from '../util/url'; import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob'; import { reportSpamJobQueue } from '../jobs/reportSpamJobQueue'; import { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions'; +import { + isDirectConversation, + isGroupV1, + isGroupV2, + isMe, +} from '../util/whatTypeOfConversation'; type GetLinkPreviewImageResult = { data: ArrayBuffer; @@ -486,7 +492,7 @@ Whisper.ConversationView = Whisper.View.extend({ onResetSession: () => this.endSession(), onSearchInConversation: () => { const { searchInConversation } = window.reduxActions.search; - const name = this.model.isMe() + const name = isMe(this.model.attributes) ? window.i18n('noteToSelf') : this.model.getTitle(); searchInConversation(this.model.id, name); @@ -1281,7 +1287,7 @@ Whisper.ConversationView = Whisper.View.extend({ async startMigrationToGV2(): Promise { const logId = this.model.idForLogging(); - if (!this.model.isGroupV1()) { + if (!isGroupV1(this.model.attributes)) { throw new Error( `startMigrationToGV2/${logId}: Cannot start, not a GroupV1 group` ); @@ -2682,7 +2688,7 @@ Whisper.ConversationView = Whisper.View.extend({ let model = providedMembers || this.model.contactCollection; - if (!providedMembers && this.model.isGroupV2()) { + if (!providedMembers && isGroupV2(this.model.attributes)) { model = new Whisper.GroupConversationCollection( this.model.get('membersV2').map(({ conversationId, role }: any) => ({ conversation: window.ConversationController.get(conversationId), @@ -2738,7 +2744,7 @@ Whisper.ConversationView = Whisper.View.extend({ showSafetyNumber(id: any) { let conversation; - if (!id && this.model.isPrivate()) { + if (!id && isDirectConversation(this.model.attributes)) { // eslint-disable-next-line prefer-destructuring conversation = this.model; } else { @@ -3786,19 +3792,22 @@ Whisper.ConversationView = Whisper.View.extend({ ToastView = Whisper.InvalidConversationToast; } if ( - this.model.isPrivate() && + isDirectConversation(this.model.attributes) && (window.storage.isBlocked(this.model.get('e164')) || window.storage.isUuidBlocked(this.model.get('uuid'))) ) { ToastView = Whisper.BlockedToast; } if ( - !this.model.isPrivate() && + !isDirectConversation(this.model.attributes) && window.storage.isGroupBlocked(this.model.get('groupId')) ) { ToastView = Whisper.BlockedGroupToast; } - if (!this.model.isPrivate() && this.model.get('left')) { + if ( + !isDirectConversation(this.model.attributes) && + this.model.get('left') + ) { ToastView = Whisper.LeftGroupToast; } if (messageText && messageText.length > MAX_MESSAGE_BODY_LENGTH) {