Remove GroupContext proto

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Josh Perez 2023-04-14 20:52:50 -04:00 committed by GitHub
parent 9bfbee464b
commit 68ae25f5cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 74 additions and 713 deletions

View file

@ -354,7 +354,7 @@ message DataMessage {
optional string body = 1;
repeated AttachmentPointer attachments = 2;
optional GroupContext group = 3;
reserved /*groupV1*/ 3;
optional GroupContextV2 groupV2 = 15;
optional uint32 flags = 4;
optional uint32 expireTimer = 5;
@ -664,23 +664,6 @@ message AttachmentPointer {
// Next ID: 16
}
message GroupContext {
enum Type {
UNKNOWN = 0;
UPDATE = 1;
DELIVER = 2;
QUIT = 3;
REQUEST_INFO = 4;
}
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string membersE164 = 4;
// field 6 was removed; do not use
optional AttachmentPointer avatar = 5;
}
message GroupContextV2 {
optional bytes masterKey = 1;
optional uint32 revision = 2;

View file

@ -2702,7 +2702,7 @@ export async function startApp(): Promise<void> {
// Note: this type of message is automatically removed from cache in MessageReceiver
const { typing, sender, senderUuid, senderDevice } = ev;
const { groupId, groupV2Id, started } = typing || {};
const { groupV2Id, started } = typing || {};
// We don't do anything with incoming typing messages if the setting is disabled
if (!window.storage.get('typingIndicators')) {
@ -2721,11 +2721,7 @@ export async function startApp(): Promise<void> {
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
if (groupV2Id) {
conversation = window.ConversationController.get(groupV2Id);
}
if (!conversation && groupId) {
conversation = window.ConversationController.get(groupId);
}
if (!groupV2Id && !groupId) {
} else {
conversation = senderConversation;
}
@ -2737,7 +2733,7 @@ export async function startApp(): Promise<void> {
}
if (!conversation) {
log.warn(
`onTyping: Did not find conversation for typing indicator (groupv2(${groupV2Id}), group(${groupId}), ${sender}, ${senderUuid})`
`onTyping: Did not find conversation for typing indicator (groupv2(${groupV2Id}), ${sender}, ${senderUuid})`
);
return;
}
@ -2988,8 +2984,6 @@ export async function startApp(): Promise<void> {
const messageDescriptor = getMessageDescriptor({
message: data.message,
source: data.source,
sourceUuid: data.sourceUuid,
// 'message' event: for 1:1 converations, the conversation is same as sender
destination: data.source,
destinationUuid: data.sourceUuid,
@ -3290,18 +3284,13 @@ export async function startApp(): Promise<void> {
return new window.Whisper.Message(partialMessage);
}
// Works with 'sent' and 'message' data sent from MessageReceiver, with a little massage
// at callsites to make sure both source and destination are populated.
// Works with 'sent' and 'message' data sent from MessageReceiver
const getMessageDescriptor = ({
message,
source,
sourceUuid,
destination,
destinationUuid,
}: {
message: ProcessedDataMessage;
source?: string;
sourceUuid?: string;
destination?: string;
destinationUuid?: string;
}): MessageDescriptor => {
@ -3342,44 +3331,6 @@ export async function startApp(): Promise<void> {
id: conversationId,
};
}
if (message.group) {
const { id, derivedGroupV2Id } = message.group;
if (!id) {
throw new Error('getMessageDescriptor: GroupV1 data was missing id');
}
if (!derivedGroupV2Id) {
log.warn(
'getMessageDescriptor: GroupV1 data was missing derivedGroupV2Id'
);
} else {
// First we check for an already-migrated GroupV2 group
const migratedGroup =
window.ConversationController.get(derivedGroupV2Id);
if (migratedGroup) {
return {
type: Message.GROUP,
id: migratedGroup.id,
};
}
}
// If we can't find one, we treat this as a normal GroupV1 group
const { conversation: fromContact } =
window.ConversationController.maybeMergeContacts({
aci: sourceUuid,
e164: source,
reason: `getMessageDescriptor(${message.timestamp}): group v1`,
});
const conversationId = window.ConversationController.ensureGroup(id, {
addedBy: fromContact.id,
});
return {
type: Message.GROUP,
id: conversationId,
};
}
const conversation = window.ConversationController.lookupOrCreate({
uuid: destinationUuid,
@ -3406,10 +3357,6 @@ export async function startApp(): Promise<void> {
const messageDescriptor = getMessageDescriptor({
...data,
// 'sent' event: the sender is always us!
source,
sourceUuid,
});
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
@ -3765,18 +3712,12 @@ export async function startApp(): Promise<void> {
function onMessageRequestResponse(ev: MessageRequestResponseEvent): void {
ev.confirm();
const {
threadE164,
threadUuid,
groupId,
groupV2Id,
messageRequestResponseType,
} = ev;
const { threadE164, threadUuid, groupV2Id, messageRequestResponseType } =
ev;
log.info('onMessageRequestResponse', {
threadE164,
threadUuid,
groupId: `group(${groupId})`,
groupV2Id: `groupv2(${groupV2Id})`,
messageRequestResponseType,
});
@ -3789,7 +3730,6 @@ export async function startApp(): Promise<void> {
const attributes: MessageRequestAttributesType = {
threadE164,
threadUuid,
groupId,
groupV2Id,
type: messageRequestResponseType,
};

View file

@ -404,6 +404,44 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
const hasGV2AdminEnabled = isGroup && groupVersion === 2;
if (isGroup && groupVersion !== 2) {
return (
<ContextMenu id={triggerId}>
<MenuItem
onClick={() =>
pushPanelForConversation({ type: PanelType.GroupV1Members })
}
>
{i18n('icu:showMembers')}
</MenuItem>
<MenuItem
onClick={() =>
pushPanelForConversation({ type: PanelType.AllMedia })
}
>
{i18n('icu:viewRecentMedia')}
</MenuItem>
<MenuItem divider />
{isArchived ? (
<MenuItem onClick={() => onMoveToInbox(id)}>
{i18n('icu:moveConversationToInbox')}
</MenuItem>
) : (
<MenuItem onClick={() => onArchive(id)}>
{i18n('icu:archiveConversation')}
</MenuItem>
)}
<MenuItem
onClick={() =>
this.setState({ hasDeleteMessagesConfirmation: true })
}
>
{i18n('icu:deleteMessages')}
</MenuItem>
</ContextMenu>
);
}
const isActiveExpireTimer = (value: number): boolean => {
if (!expireTimer) {
return value === 0;
@ -487,15 +525,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
: i18n('icu:showConversationDetails--direct')}
</MenuItem>
) : null}
{isGroup && !hasGV2AdminEnabled ? (
<MenuItem
onClick={() =>
pushPanelForConversation({ type: PanelType.GroupV1Members })
}
>
{i18n('icu:showMembers')}
</MenuItem>
) : null}
<MenuItem
onClick={() => pushPanelForConversation({ type: PanelType.AllMedia })}
>

View file

@ -230,7 +230,6 @@ export async function sendDeleteForEveryone(
abortSignal,
contentHint,
groupSendOptions: {
groupV1: conversation.getGroupV1Info(recipients),
groupV2: groupV2Info,
deletedForEveryoneTimestamp: targetTimestamp,
timestamp,

View file

@ -257,9 +257,6 @@ export async function sendNormalMessage(
contact,
deletedForEveryoneTimestamp,
expireTimer,
groupV1: conversation.getGroupV1Info(
recipientIdentifiersWithoutMe
),
groupV2: groupV2Info,
messageText: body,
preview,

View file

@ -152,7 +152,6 @@ export async function sendProfileKey(
contentHint,
groupSendOptions: {
flags: Proto.DataMessage.Flags.PROFILE_KEY_UPDATE,
groupV1: conversation.getGroupV1Info(),
groupV2: groupV2Info,
profileKey,
timestamp,

View file

@ -255,9 +255,6 @@ export async function sendReaction(
abortSignal,
contentHint: ContentHint.RESENDABLE,
groupSendOptions: {
groupV1: conversation.getGroupV1Info(
recipientIdentifiersWithoutMe
),
groupV2: groupV2Info,
reaction: reactionForSend,
timestamp: pendingReaction.timestamp,

View file

@ -11,7 +11,6 @@ import * as Errors from '../types/errors';
export type MessageRequestAttributesType = {
threadE164?: string;
threadUuid?: string;
groupId?: string;
groupV2Id?: string;
type: number;
};
@ -56,20 +55,6 @@ export class MessageRequests extends Collection<MessageRequestModel> {
}
}
// V1 Group
if (conversation.get('groupId')) {
const syncByGroupId = this.findWhere({
groupId: conversation.get('groupId'),
});
if (syncByGroupId) {
log.info(
`Found early message request response for group v1 ID ${conversation.idForLogging()}`
);
this.remove(syncByGroupId);
return syncByGroupId;
}
}
// V2 group
if (conversation.get('groupId')) {
const syncByGroupId = this.findWhere({
@ -91,7 +76,6 @@ export class MessageRequests extends Collection<MessageRequestModel> {
try {
const threadE164 = sync.get('threadE164');
const threadUuid = sync.get('threadUuid');
const groupId = sync.get('groupId');
const groupV2Id = sync.get('groupV2Id');
let conversation;
@ -100,9 +84,6 @@ export class MessageRequests extends Collection<MessageRequestModel> {
if (groupV2Id) {
conversation = window.ConversationController.get(groupV2Id);
}
if (!conversation && groupId) {
conversation = window.ConversationController.get(groupId);
}
if (!conversation && (threadE164 || threadUuid)) {
conversation = window.ConversationController.lookupOrCreate({
e164: threadE164,
@ -113,7 +94,7 @@ export class MessageRequests extends Collection<MessageRequestModel> {
if (!conversation) {
log.warn(
`Received message request response for unknown conversation: groupv2(${groupV2Id}) group(${groupId}) ${threadUuid} ${threadE164}`
`Received message request response for unknown conversation: groupv2(${groupV2Id}) ${threadUuid} ${threadE164}`
);
return;
}

View file

@ -40,7 +40,6 @@ import * as Stickers from '../types/Stickers';
import { StorySendMode } from '../types/Stories';
import type {
ContactWithHydratedAvatar,
GroupV1InfoType,
GroupV2InfoType,
} from '../textsecure/SendMessage';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
@ -1307,24 +1306,6 @@ export class ConversationModel extends window.Backbone
};
}
getGroupV1Info(members?: Array<string>): GroupV1InfoType | undefined {
const groupId = this.get('groupId');
const groupVersion = this.get('groupVersion');
if (
isDirectConversation(this.attributes) ||
!groupId ||
(groupVersion && groupVersion > 0)
) {
return undefined;
}
return {
id: groupId,
members: members || this.getRecipients(),
};
}
getGroupIdBuffer(): Uint8Array | undefined {
const groupIdString = this.get('groupId');
@ -2457,9 +2438,7 @@ export class ConversationModel extends window.Backbone
this.disableProfileSharing({ viaStorageServiceSync });
if (isLocalAction) {
if (isGroupV1(this.attributes)) {
await this.leaveGroup();
} else if (isGroupV2(this.attributes)) {
if (isGroupV2(this.attributes)) {
await this.leaveGroupV2();
}
}
@ -2477,9 +2456,7 @@ export class ConversationModel extends window.Backbone
'deleted from message request'
);
if (isGroupV1(this.attributes)) {
await this.leaveGroup();
} else if (isGroupV2(this.attributes)) {
if (isGroupV2(this.attributes)) {
await this.leaveGroupV2();
}
}
@ -2499,9 +2476,7 @@ export class ConversationModel extends window.Backbone
'blocked and deleted from message request'
);
if (isGroupV1(this.attributes)) {
await this.leaveGroup();
} else if (isGroupV2(this.attributes)) {
if (isGroupV2(this.attributes)) {
await this.leaveGroupV2();
}
}
@ -4875,59 +4850,6 @@ export class ConversationModel extends window.Backbone
return !this.get('left');
}
// Deprecated: only applies to GroupV1
async leaveGroup(): Promise<void> {
const { messaging } = window.textsecure;
if (!messaging) {
throw new Error('leaveGroup: Cannot leave v1 group when offline!');
}
if (!isGroupV1(this.attributes)) {
throw new Error(
`leaveGroup: Group ${this.idForLogging()} is not GroupV1!`
);
}
const now = Date.now();
const groupId = this.get('groupId');
if (!groupId) {
throw new Error(`leaveGroup/${this.idForLogging()}: No groupId!`);
}
const groupIdentifiers = this.getRecipients();
this.set({ left: true });
window.Signal.Data.updateConversation(this.attributes);
const model = new window.Whisper.Message({
conversationId: this.id,
group_update: { left: 'You' },
readStatus: ReadStatus.Read,
received_at_ms: now,
received_at: incrementMessageCounter(),
seenStatus: SeenStatus.NotApplicable,
sent_at: now,
type: 'group',
// TODO: DESKTOP-722
} as unknown as MessageAttributesType);
const id = await window.Signal.Data.saveMessage(model.attributes, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
model.set({ id });
const message = window.MessageController.register(model.id, model);
void this.addSingleMessage(message);
const options = await getSendOptions(this.attributes);
void message.send(
handleMessageSend(
messaging.leaveGroup(groupId, groupIdentifiers, options),
{ messageIds: [], sendType: 'legacyGroupChange' }
)
);
}
async markRead(
newestUnreadAt: number,
options: {

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import {
difference,
isEmpty,
isEqual,
isNumber,
@ -14,11 +13,9 @@ import {
partition,
pick,
union,
without,
} from 'lodash';
import type {
CustomError,
GroupV1Update,
MessageAttributesType,
MessageReactionType,
QuotedMessageType,
@ -85,7 +82,6 @@ import {
isDirectConversation,
isGroup,
isGroupV1,
isGroupV2,
isMe,
} from '../util/whatTypeOfConversation';
import { handleMessageSend } from '../util/handleMessageSend';
@ -145,11 +141,9 @@ import { notificationService } from '../services/notifications';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as log from '../logging/log';
import * as Bytes from '../Bytes';
import { computeHash } from '../Crypto';
import { cleanupMessage, deleteMessageData } from '../util/cleanup';
import {
getContact,
getContactId,
getSource,
getSourceUuid,
isCustomError,
@ -173,7 +167,6 @@ import { SeenStatus } from '../MessageSeenStatus';
import { isNewReactionReplacingPrevious } from '../reactions/util';
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
import { GiftBadgeStates } from '../components/conversation/Message';
import { downloadAttachment } from '../util/downloadAttachment';
import type { StickerWithHydratedData } from '../types/Stickers';
import { getStringForConversationMerge } from '../util/getStringForConversationMerge';
import {
@ -2139,7 +2132,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const sourceUuid = message.get('sourceUuid');
const type = message.get('type');
const conversationId = message.get('conversationId');
const GROUP_TYPES = Proto.GroupContext.Type;
const fromContact = getContact(this.attributes);
if (fromContact) {
@ -2352,9 +2344,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
reason: 'handleDataMessage',
})!;
const hasGroupV2Prop = Boolean(initialMessage.groupV2);
const isV1GroupUpdate =
initialMessage.group &&
initialMessage.group.type !== Proto.GroupContext.Type.DELIVER;
// Drop if from blocked user. Only GroupV2 messages should need to be dropped here.
const isBlocked =
@ -2397,7 +2386,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
type === 'incoming' &&
!isDirectConversation(conversation.attributes) &&
!hasGroupV2Prop &&
!isV1GroupUpdate &&
conversation.get('members') &&
!areWeMember
) {
@ -2408,16 +2396,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return;
}
// Because GroupV1 messages can now be multiplexed into GroupV2 conversations, we
// drop GroupV1 updates in GroupV2 groups.
if (isV1GroupUpdate && isGroupV2(conversation.attributes)) {
log.warn(
`Received GroupV1 update in GroupV2 conversation ${conversation.idForLogging()}. Dropping.`
);
confirm();
return;
}
// Drop incoming messages to announcement only groups where sender is not admin
if (
conversation.get('announcementsOnly') &&
@ -2615,160 +2593,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
if (isSupported) {
let attributes = {
const attributes = {
...conversation.attributes,
};
// GroupV1
if (!hasGroupV2Prop && initialMessage.group) {
const pendingGroupUpdate: GroupV1Update = {};
const memberConversations: Array<ConversationModel> =
await Promise.all(
initialMessage.group.membersE164.map((e164: string) =>
window.ConversationController.getOrCreateAndWait(
e164,
'private'
)
)
);
const members = memberConversations.map(c => c.get('id'));
attributes = {
...attributes,
type: 'group',
groupId: initialMessage.group.id,
};
if (initialMessage.group.type === GROUP_TYPES.UPDATE) {
attributes = {
...attributes,
name: initialMessage.group.name,
members: union(members, conversation.get('members')),
};
if (initialMessage.group.name !== conversation.get('name')) {
pendingGroupUpdate.name = initialMessage.group.name;
}
const avatarAttachment = initialMessage.group.avatar;
let downloadedAvatar;
let hash;
if (avatarAttachment) {
try {
downloadedAvatar = await downloadAttachment(avatarAttachment);
if (downloadedAvatar) {
const loadedAttachment =
await window.Signal.Migrations.loadAttachmentData(
downloadedAvatar
);
hash = computeHash(loadedAttachment.data);
}
} catch (err) {
log.info(`${idLog}: group avatar download failed`);
}
}
const existingAvatar = conversation.get('avatar');
if (
// Avatar added
(!existingAvatar && avatarAttachment) ||
// Avatar changed
(existingAvatar && existingAvatar.hash !== hash) ||
// Avatar removed
(existingAvatar && !avatarAttachment)
) {
// Removes existing avatar from disk
if (existingAvatar && existingAvatar.path) {
await window.Signal.Migrations.deleteAttachmentData(
existingAvatar.path
);
}
let avatar = null;
if (downloadedAvatar && avatarAttachment != null) {
const onDiskAttachment =
await Attachment.migrateDataToFileSystem(downloadedAvatar, {
writeNewAttachmentData:
window.Signal.Migrations.writeNewAttachmentData,
logger: log,
});
avatar = {
...onDiskAttachment,
hash,
};
}
if (!avatar) {
attributes.avatar = avatar;
} else {
const { url, path } = avatar;
strictAssert(url, 'Avatar needs url');
strictAssert(path, 'Avatar needs path');
attributes.avatar = {
url,
path,
...avatar,
};
}
pendingGroupUpdate.avatarUpdated = true;
} else {
log.info(
`${idLog}: Group avatar hash matched; not replacing group avatar`
);
}
const differentMembers = difference(
members,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
conversation.get('members')!
);
if (differentMembers.length > 0) {
// Because GroupV1 groups are based on e164 only
const maybeE164s = map(differentMembers, id =>
window.ConversationController.get(id)?.get('e164')
);
const e164s = filter(maybeE164s, isNotNil);
pendingGroupUpdate.joined = [...e164s];
}
if (conversation.get('left')) {
log.warn('re-added to a left group');
attributes.left = false;
conversation.set({ addedBy: getContactId(message.attributes) });
}
} else if (initialMessage.group.type === GROUP_TYPES.QUIT) {
const inGroup = Boolean(
sender &&
(conversation.get('members') || []).includes(sender.id)
);
if (!inGroup) {
const senderString = sender ? sender.idForLogging() : null;
log.info(
`${idLog}: Got 'left' message from someone not in group: ${senderString}. Dropping.`
);
return;
}
if (isMe(sender.attributes)) {
attributes.left = true;
pendingGroupUpdate.left = 'You';
} else {
pendingGroupUpdate.left = sender.get('id');
}
attributes.members = without(
conversation.get('members'),
sender.get('id')
);
}
if (!isEmpty(pendingGroupUpdate)) {
message.set('group_update', pendingGroupUpdate);
}
}
// Drop empty messages after. This needs to happen after the initial
// message.set call and after GroupV1 processing to make sure all possible
// properties are set before we determine that a message is empty.

View file

@ -32,10 +32,6 @@ const PROCESSED_ATTACHMENT: ProcessedAttachment = {
size: 34,
};
const GROUP_ID = new Uint8Array([0x68, 0x65, 0x79]);
const DERIVED_GROUPV2_ID = '7qQUi8Wa6Jm3Rl+l63saATGeciEqokbHpP+lV3F5t9o=';
describe('processDataMessage', () => {
const check = (message: Proto.IDataMessage) =>
processDataMessage(
@ -85,59 +81,6 @@ describe('processDataMessage', () => {
);
});
it('should process group context UPDATE/QUIT message', () => {
const { UPDATE, QUIT } = Proto.GroupContext.Type;
for (const type of [UPDATE, QUIT]) {
const out = check({
body: 'should be deleted',
attachments: [UNPROCESSED_ATTACHMENT],
group: {
id: GROUP_ID,
name: 'Group',
avatar: UNPROCESSED_ATTACHMENT,
type,
membersE164: ['+1'],
},
});
assert.isUndefined(out.body);
assert.strictEqual(out.attachments.length, 0);
assert.deepStrictEqual(out.group, {
id: 'hey',
name: 'Group',
avatar: PROCESSED_ATTACHMENT,
type,
membersE164: ['+1'],
derivedGroupV2Id: DERIVED_GROUPV2_ID,
});
}
});
it('should process group context DELIVER message', () => {
const out = check({
body: 'should not be deleted',
attachments: [UNPROCESSED_ATTACHMENT],
group: {
id: GROUP_ID,
name: 'should be deleted',
membersE164: ['should be deleted'],
type: Proto.GroupContext.Type.DELIVER,
},
});
assert.strictEqual(out.body, 'should not be deleted');
assert.strictEqual(out.attachments.length, 1);
assert.deepStrictEqual(out.group, {
id: 'hey',
type: Proto.GroupContext.Type.DELIVER,
membersE164: [],
derivedGroupV2Id: DERIVED_GROUPV2_ID,
avatar: undefined,
name: undefined,
});
});
it('should process groupv2 context', () => {
const out = check({
groupV2: {
@ -312,15 +255,10 @@ describe('processDataMessage', () => {
const out = check({
flags: FLAGS.END_SESSION,
body: 'should be deleted',
group: {
id: GROUP_ID,
type: Proto.GroupContext.Type.DELIVER,
},
attachments: [UNPROCESSED_ATTACHMENT],
});
assert.isUndefined(out.body);
assert.isUndefined(out.group);
assert.deepStrictEqual(out.attachments, []);
});

View file

@ -47,7 +47,7 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { Zone } from '../util/Zone';
import { DurationInSeconds } from '../util/durations';
import { deriveMasterKeyFromGroupV1, bytesToUuid } from '../Crypto';
import { bytesToUuid } from '../Crypto';
import type { DownloadedAttachmentType } from '../types/Attachment';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
@ -127,7 +127,6 @@ import { inspectUnknownFieldTags } from '../util/inspectProtobufs';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { filterAndClean } from '../types/BodyRange';
const GROUPV1_ID_LENGTH = 16;
const GROUPV2_ID_LENGTH = 32;
const RETRY_TIMEOUT = 2 * 60 * 1000;
@ -2007,19 +2006,8 @@ export default class MessageReceiver
const message = this.processDecrypted(envelope, msg);
const groupId = this.getProcessedGroupId(message);
const isBlocked = groupId ? this.isGroupBlocked(groupId) : false;
const { source, sourceUuid } = envelope;
const ourE164 = this.storage.user.getNumber();
const ourUuid = this.storage.user.getCheckedUuid().toString();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
!message.groupV2 &&
message.group &&
message.group.type === Proto.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
if (groupId && isBlocked) {
log.warn(
`Message ${getEnvelopeId(envelope)} ignored; destined for blocked group`
);
@ -2282,19 +2270,8 @@ export default class MessageReceiver
const message = this.processDecrypted(envelope, msg.dataMessage);
const groupId = this.getProcessedGroupId(message);
const isBlocked = groupId ? this.isGroupBlocked(groupId) : false;
const { source, sourceUuid } = envelope;
const ourE164 = this.storage.user.getNumber();
const ourUuid = this.storage.user.getCheckedUuid().toString();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
!message.groupV2 &&
message.group &&
message.group.type === Proto.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
if (groupId && isBlocked) {
log.warn(
`Message ${getEnvelopeId(envelope)} ignored; destined for blocked group`
);
@ -2354,8 +2331,6 @@ export default class MessageReceiver
return undefined;
}
this.checkGroupV1Data(msg);
if (msg.flags && msg.flags & Proto.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(envelope, new UUID(destination));
}
@ -2393,8 +2368,6 @@ export default class MessageReceiver
msg.flags & Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
) {
type = 'expirationTimerUpdate';
} else if (msg.group) {
type = 'legacyGroupChange';
}
// Note: other data messages without any of these attributes will fall into the
// 'message' bucket - like stickers, gift badges, etc.
@ -2404,19 +2377,8 @@ export default class MessageReceiver
const message = this.processDecrypted(envelope, msg);
const groupId = this.getProcessedGroupId(message);
const isBlocked = groupId ? this.isGroupBlocked(groupId) : false;
const { source, sourceUuid } = envelope;
const ourE164 = this.storage.user.getNumber();
const ourUuid = this.storage.user.getCheckedUuid().toString();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
!message.groupV2 &&
message.group &&
message.group.type === Proto.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
if (groupId && isBlocked) {
log.warn(
`Message ${getEnvelopeId(envelope)} ignored; destined for blocked group`
);
@ -2780,17 +2742,11 @@ export default class MessageReceiver
const { groupId, timestamp, action } = typingMessage;
let groupIdString: string | undefined;
let groupV2IdString: string | undefined;
if (groupId && groupId.byteLength > 0) {
if (groupId.byteLength === GROUPV1_ID_LENGTH) {
groupIdString = Bytes.toBinary(groupId);
groupV2IdString = this.deriveGroupV2FromV1(groupId);
} else if (groupId.byteLength === GROUPV2_ID_LENGTH) {
groupV2IdString = Bytes.toBase64(groupId);
} else {
log.error('handleTypingMessage: Received invalid groupId value');
}
if (groupId && groupId.byteLength === GROUPV2_ID_LENGTH) {
groupV2IdString = Bytes.toBase64(groupId);
} else {
log.error('handleTypingMessage: Received invalid groupId value');
}
this.dispatchEvent(
@ -2799,13 +2755,11 @@ export default class MessageReceiver
senderUuid: envelope.sourceUuid,
senderDevice: envelope.sourceDevice,
typing: {
groupV2Id: groupV2IdString,
typingMessage,
timestamp: timestamp?.toNumber() ?? Date.now(),
started: action === Proto.TypingMessage.Action.STARTED,
stopped: action === Proto.TypingMessage.Action.STOPPED,
groupId: groupIdString,
groupV2Id: groupV2IdString,
},
})
);
@ -2823,22 +2777,7 @@ export default class MessageReceiver
message: Proto.IDataMessage,
envelope: ProcessedEnvelope
): boolean {
const { group, groupV2 } = message;
if (group) {
const { id } = group;
strictAssert(id, 'Group data has no id');
const isInvalid = id.byteLength !== GROUPV1_ID_LENGTH;
if (isInvalid) {
log.info(
'isInvalidGroupData: invalid GroupV1 message from',
getEnvelopeId(envelope)
);
}
return isInvalid;
}
const { groupV2 } = message;
if (groupV2) {
const { masterKey } = groupV2;
@ -2857,46 +2796,12 @@ export default class MessageReceiver
return false;
}
private deriveGroupV2FromV1(groupId: Uint8Array): string {
if (groupId.byteLength !== GROUPV1_ID_LENGTH) {
throw new Error(
`deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}`
);
}
const masterKey = deriveMasterKeyFromGroupV1(groupId);
const data = deriveGroupFields(masterKey);
return Bytes.toBase64(data.id);
}
private checkGroupV1Data(message: Readonly<Proto.IDataMessage>): void {
const { group } = message;
if (!group) {
return;
}
if (!group.id) {
throw new Error('deriveGroupV1Data: had falsey id');
}
const { id } = group;
if (id.byteLength !== GROUPV1_ID_LENGTH) {
throw new Error(
`deriveGroupV1Data: had id with wrong byteLength: ${id.byteLength}`
);
}
}
private getProcessedGroupId(
message: ProcessedDataMessage
): string | undefined {
if (message.groupV2) {
return message.groupV2.id;
}
if (message.group && message.group.id) {
return message.group.id;
}
return undefined;
}
@ -2906,9 +2811,6 @@ export default class MessageReceiver
const { id } = deriveGroupFields(message.groupV2.masterKey);
return Bytes.toBase64(id);
}
if (message.group && message.group.id) {
return Bytes.toBinary(message.group.id);
}
return undefined;
}
@ -2917,10 +2819,6 @@ export default class MessageReceiver
if (sentMessage.message && sentMessage.message.groupV2) {
return `groupv2(${this.getGroupId(sentMessage.message)})`;
}
if (sentMessage.message && sentMessage.message.group) {
strictAssert(sentMessage.message.group.id, 'group without id');
return `group(${this.getGroupId(sentMessage.message)})`;
}
return sentMessage.destination || sentMessage.destinationUuid;
}
@ -2998,8 +2896,6 @@ export default class MessageReceiver
return;
}
this.checkGroupV1Data(sentMessage.message);
strictAssert(sentMessage.timestamp, 'sent message without timestamp');
log.info(
@ -3192,19 +3088,13 @@ export default class MessageReceiver
const { groupId } = sync;
let groupIdString: string | undefined;
let groupV2IdString: string | undefined;
if (groupId && groupId.byteLength > 0) {
if (groupId.byteLength === GROUPV1_ID_LENGTH) {
groupIdString = Bytes.toBinary(groupId);
groupV2IdString = this.deriveGroupV2FromV1(groupId);
} else if (groupId.byteLength === GROUPV2_ID_LENGTH) {
groupV2IdString = Bytes.toBase64(groupId);
} else {
this.removeFromCache(envelope);
log.error('Received message request with invalid groupId');
return undefined;
}
if (groupId && groupId.byteLength === GROUPV2_ID_LENGTH) {
groupV2IdString = Bytes.toBase64(groupId);
} else {
this.removeFromCache(envelope);
log.error('Received message request with invalid groupId');
return undefined;
}
const ev = new MessageRequestResponseEvent(
@ -3217,7 +3107,6 @@ export default class MessageReceiver
)
: undefined,
messageRequestResponseType: sync.type,
groupId: groupIdString,
groupV2Id: groupV2IdString,
},
this.removeFromCache.bind(this, envelope)
@ -3583,14 +3472,10 @@ export default class MessageReceiver
if (blocked.groupIds) {
const previous = this.storage.get('blocked-groups', []);
const groupV1Ids: Array<string> = [];
const groupIds: Array<string> = [];
blocked.groupIds.forEach(groupId => {
if (groupId.byteLength === GROUPV1_ID_LENGTH) {
groupV1Ids.push(Bytes.toBinary(groupId));
groupIds.push(this.deriveGroupV2FromV1(groupId));
} else if (groupId.byteLength === GROUPV2_ID_LENGTH) {
if (groupId.byteLength === GROUPV2_ID_LENGTH) {
groupIds.push(Bytes.toBase64(groupId));
} else {
log.error('handleBlocked: Received invalid groupId value');
@ -3598,18 +3483,15 @@ export default class MessageReceiver
});
log.info(
'handleBlocked: Blocking these groups - v2:',
groupIds.map(groupId => `groupv2(${groupId})`),
'v1:',
groupV1Ids.map(groupId => `group(${groupId})`)
groupIds.map(groupId => `groupv2(${groupId})`)
);
const ids = [...groupIds, ...groupV1Ids];
await this.storage.put('blocked-groups', ids);
await this.storage.put('blocked-groups', groupIds);
if (!areArraysMatchingSets(previous, ids)) {
if (!areArraysMatchingSets(previous, groupIds)) {
changed = true;
allIdentifiers.push(...previous);
allIdentifiers.push(...ids);
allIdentifiers.push(...groupIds);
}
}

View file

@ -103,10 +103,6 @@ export type GroupV2InfoType = {
revision: number;
members: ReadonlyArray<string>;
};
export type GroupV1InfoType = {
id: string;
members: ReadonlyArray<string>;
};
type GroupCallUpdateType = {
eraId: string;
@ -207,7 +203,6 @@ export type GroupSendOptionsType = {
expireTimer?: DurationInSeconds;
flags?: number;
groupCallUpdate?: GroupCallUpdateType;
groupV1?: GroupV1InfoType;
groupV2?: GroupV2InfoType;
messageText?: string;
preview?: ReadonlyArray<LinkPreviewType>;
@ -381,10 +376,6 @@ class Message {
proto.groupV2.masterKey = this.groupV2.masterKey;
proto.groupV2.revision = this.groupV2.revision;
proto.groupV2.groupChange = this.groupV2.groupChange || null;
} else if (this.group) {
proto.group = new Proto.GroupContext();
proto.group.id = Bytes.fromString(this.group.id);
proto.group.type = this.group.type;
}
if (this.sticker) {
proto.sticker = new Proto.DataMessage.Sticker();
@ -1106,7 +1097,6 @@ export default class MessageSender {
expireTimer,
flags,
groupCallUpdate,
groupV1,
groupV2,
messageText,
preview,
@ -1118,16 +1108,16 @@ export default class MessageSender {
timestamp,
} = options;
if (!groupV1 && !groupV2) {
if (!groupV2) {
throw new Error(
'getAttrsFromGroupOptions: Neither group1 nor groupv2 information provided!'
'getAttrsFromGroupOptions: No groupv2 information provided!'
);
}
const myE164 = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
const groupMembers = groupV2?.members || groupV1?.members || [];
const groupMembers = groupV2?.members || [];
// We should always have a UUID but have this check just in case we don't.
let isNotMe: (recipient: string) => boolean;
@ -1158,12 +1148,6 @@ export default class MessageSender {
flags,
groupCallUpdate,
groupV2,
group: groupV1
? {
id: groupV1.id,
type: Proto.GroupContext.Type.DELIVER,
}
: undefined,
preview,
profileKey,
quote,
@ -2424,50 +2408,6 @@ export default class MessageSender {
});
}
// GroupV1-only functions; not to be used in the future
async leaveGroup(
groupId: string,
groupIdentifiers: Array<string>,
options?: SendOptionsType
): Promise<CallbackResultType> {
const timestamp = Date.now();
const proto = new Proto.Content({
dataMessage: {
group: {
id: Bytes.fromString(groupId),
type: Proto.GroupContext.Type.QUIT,
},
},
});
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const contentHint = ContentHint.RESENDABLE;
const sendLogCallback =
groupIdentifiers.length > 1
? this.makeSendLogCallback({
contentHint,
proto: Buffer.from(Proto.Content.encode(proto).finish()),
sendType: 'legacyGroupChange',
timestamp,
urgent: false,
hasPniSignatureMessage: false,
})
: undefined;
return this.sendGroupProto({
contentHint,
groupId: undefined, // only for GV2 ids
options,
proto,
recipients: groupIdentifiers,
sendLogCallback,
timestamp,
urgent: false,
});
}
// Simple pass-throughs
// Note: instead of updating these functions, or adding new ones, remove these and go

View file

@ -118,17 +118,6 @@ export type ProcessedAttachment = {
textAttachment?: Omit<TextAttachmentType, 'preview'>;
};
export type ProcessedGroupContext = {
id: string;
type: Proto.GroupContext.Type;
name?: string;
membersE164: ReadonlyArray<string>;
avatar?: ProcessedAttachment;
// Computed fields
derivedGroupV2Id: string;
};
export type ProcessedGroupV2Context = {
masterKey: string;
revision?: number;
@ -208,7 +197,6 @@ export type ProcessedGiftBadge = {
export type ProcessedDataMessage = {
body?: string;
attachments: ReadonlyArray<ProcessedAttachment>;
group?: ProcessedGroupContext;
groupV2?: ProcessedGroupV2Context;
flags: number;
expireTimer: DurationInSeconds;

View file

@ -10,12 +10,10 @@ import { dropNull, shallowDropNull } from '../util/dropNull';
import { SignalService as Proto } from '../protobuf';
import { deriveGroupFields } from '../groups';
import * as Bytes from '../Bytes';
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
import type {
ProcessedAttachment,
ProcessedDataMessage,
ProcessedGroupContext,
ProcessedGroupV2Context,
ProcessedQuote,
ProcessedContact,
@ -25,7 +23,6 @@ import type {
ProcessedDelete,
ProcessedGiftBadge,
} from './Types.d';
import { WarnOnlyError } from './Errors';
import { GiftBadgeStates } from '../components/conversation/Message';
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
import { SECOND, DurationInSeconds } from '../util/durations';
@ -71,39 +68,6 @@ export function processAttachment(
};
}
function processGroupContext(
group?: Proto.IGroupContext | null
): ProcessedGroupContext | undefined {
if (!group) {
return undefined;
}
strictAssert(group.id, 'group context without id');
strictAssert(group.type != null, 'group context without type');
const masterKey = deriveMasterKeyFromGroupV1(group.id);
const data = deriveGroupFields(masterKey);
const derivedGroupV2Id = Bytes.toBase64(data.id);
const result: ProcessedGroupContext = {
id: Bytes.toBinary(group.id),
type: group.type,
name: dropNull(group.name),
membersE164: group.membersE164 ?? [],
avatar: processAttachment(group.avatar),
derivedGroupV2Id,
};
if (result.type === Proto.GroupContext.Type.DELIVER) {
result.name = undefined;
result.membersE164 = [];
result.avatar = undefined;
}
return result;
}
export function processGroupV2Context(
groupV2?: Proto.IGroupContextV2 | null
): ProcessedGroupV2Context | undefined {
@ -331,7 +295,6 @@ export function processDataMessage(
attachments: (message.attachments ?? []).map(
(attachment: Proto.IAttachmentPointer) => processAttachment(attachment)
),
group: processGroupContext(message.group),
groupV2: processGroupV2Context(message.groupV2),
flags: message.flags ?? 0,
expireTimer: DurationInSeconds.fromSeconds(message.expireTimer ?? 0),
@ -375,7 +338,6 @@ export function processDataMessage(
if (isEndSession) {
result.body = undefined;
result.attachments = [];
result.group = undefined;
return result;
}
@ -389,27 +351,6 @@ export function processDataMessage(
throw new Error(`Unknown flags in message: ${result.flags}`);
}
if (result.group) {
switch (result.group.type) {
case Proto.GroupContext.Type.UPDATE:
result.body = undefined;
result.attachments = [];
break;
case Proto.GroupContext.Type.QUIT:
result.body = undefined;
result.attachments = [];
break;
case Proto.GroupContext.Type.DELIVER:
// Cleaned up in `processGroupContext`
break;
default: {
throw new WarnOnlyError(
`Unknown group message type: ${result.group.type}`
);
}
}
}
const attachmentCount = result.attachments.length;
if (attachmentCount > ATTACHMENT_MAX) {
throw new Error(

View file

@ -859,9 +859,6 @@ function getRecipients(options: GroupSendOptionsType): ReadonlyArray<string> {
if (options.groupV2) {
return options.groupV2.members;
}
if (options.groupV1) {
return options.groupV1.members;
}
throw new Error('getRecipients: Unable to extract recipients!');
}