// // Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation import SignalServiceKit import SignalMessaging #if DEBUG class DebugUIGroupsV2: DebugUIPage { // MARK: Overrides override func name() -> String { return "Groups v2" } override func section(thread: TSThread?) -> OWSTableSection? { var sectionItems = [OWSTableItem]() if let groupThread = thread as? TSGroupThread { sectionItems.append(OWSTableItem(title: "Make group update info messages.") { [weak self] in self?.insertGroupUpdateInfoMessages(groupThread: groupThread) }) sectionItems.append(OWSTableItem(title: "Send group update.") { [weak self] in self?.sendGroupUpdate(groupThread: groupThread) }) if groupThread.isGroupV2Thread { // v2 Group sectionItems.append(OWSTableItem(title: "Kick other group members.") { [weak self] in self?.kickOtherGroupMembers(groupThread: groupThread) }) } else { // v1 Group sectionItems.append(OWSTableItem(title: "Send empty v1 group update.") { [weak self] in self?.sendEmptyV1GroupUpdate(groupThread: groupThread) }) } } if let contactThread = thread as? TSContactThread { sectionItems.append(OWSTableItem(title: "Send invalid group messages.") { [weak self] in self?.sendInvalidGroupMessages(contactThread: contactThread) }) } if let groupThread = thread as? TSGroupThread, groupThread.isGroupV2Thread { sectionItems.append(OWSTableItem(title: "Send partially-invalid group messages.") { [weak self] in self?.sendPartiallyInvalidGroupMessages(groupThread: groupThread) }) sectionItems.append(OWSTableItem(title: "Update v2 group immediately.") { [weak self] in self?.updateV2GroupImmediately(groupThread: groupThread) }) } if let groupThread = thread as? TSGroupThread { sectionItems.append(OWSTableItem(title: "Try to migrate group (is already migrated on service).") { Self.migrate(groupThread: groupThread, migrationMode: .isAlreadyMigratedOnService) }) sectionItems.append(OWSTableItem(title: "Try to migrate group (only if already migrated on service).") { Self.migrate(groupThread: groupThread, migrationMode: .possiblyAlreadyMigratedOnService) }) sectionItems.append(OWSTableItem(title: "Try to migrate group (polite manual migration).") { Self.migrate(groupThread: groupThread, migrationMode: .manualMigrationPolite) }) sectionItems.append(OWSTableItem(title: "Try to migrate group (aggressive manual migration).") { Self.migrate(groupThread: groupThread, migrationMode: .manualMigrationAggressive) }) sectionItems.append(OWSTableItem(title: "Try to migrate group (polite auto migration).") { Self.migrate(groupThread: groupThread, migrationMode: .autoMigrationPolite) }) sectionItems.append(OWSTableItem(title: "Try to migrate group (aggressive auto migration).") { Self.migrate(groupThread: groupThread, migrationMode: .autoMigrationAggressive) }) } return OWSTableSection(title: "Groups v2", items: sectionItems) } private static func migrate(groupThread: TSGroupThread, migrationMode: GroupsV2MigrationMode) { _ = firstly { () -> Promise in GroupsV2Migration.tryToMigrate(groupThread: groupThread, migrationMode: migrationMode) }.done { _ in Logger.verbose("Done.") }.catch { error in Logger.verbose("Error: \(error).") } } private func insertGroupUpdateInfoMessages(groupThread: TSGroupThread) { databaseStorage.asyncWrite { transaction in do { try self.insertGroupUpdateInfoMessages(groupThread: groupThread, groupsVersion: .V1, isLocalUpdate: true, prefix: "V1 Group, Local Updater:", transaction: transaction) try self.insertGroupUpdateInfoMessages(groupThread: groupThread, groupsVersion: .V1, isLocalUpdate: false, prefix: "V1 Group, Other Updater:", transaction: transaction) try self.insertGroupUpdateInfoMessages(groupThread: groupThread, groupsVersion: .V1, isAnonymousUpdate: true, prefix: "V1 Group, Anon Updater:", transaction: transaction) try self.insertGroupUpdateInfoMessages(groupThread: groupThread, groupsVersion: .V2, isLocalUpdate: true, prefix: "V2 Group, Local Updater:", transaction: transaction) try self.insertGroupUpdateInfoMessages(groupThread: groupThread, groupsVersion: .V2, isLocalUpdate: false, prefix: "V2 Group, Other Updater:", transaction: transaction) try self.insertGroupUpdateInfoMessages(groupThread: groupThread, groupsVersion: .V2, isAnonymousUpdate: true, prefix: "V2 Group, Anon Updater:", transaction: transaction) } catch { owsFailDebug("Error: \(error)") } } } private func insertGroupUpdateInfoMessages(groupThread: TSGroupThread, groupsVersion: GroupsVersion, isLocalUpdate: Bool = false, isAnonymousUpdate: Bool = false, prefix: String, transaction: SDSAnyWriteTransaction) throws { let prefix = prefix + " " let signalAccounts = self.contactsManagerImpl.unsortedSignalAccounts(transaction: transaction) // These will fail if you aren't registered or // don't have some Signal users in your contacts. let localAddress = tsAccountManager.localAddress! var allAddresses = signalAccounts.map { $0.recipientAddress } if groupsVersion == .V2 { // V2 group members must have a uuid. allAddresses = allAddresses.filter { $0.uuid != nil } } guard allAddresses.count >= 3 else { return owsFailDebug("Not enough Signal users in your contacts.") } let updaterAddress: SignalServiceAddress? if isAnonymousUpdate { updaterAddress = nil } else if isLocalUpdate { updaterAddress = localAddress } else { updaterAddress = allAddresses[0] } let otherAddresses = allAddresses.filter { $0 != localAddress && $0 != updaterAddress }.shuffled() let otherAddress0: SignalServiceAddress = otherAddresses[0] let otherAddress1: SignalServiceAddress = otherAddresses[1] let randoAddress = SignalServiceAddress(uuid: UUID()) let insertOutgoingMessage = { body in TSOutgoingMessageBuilder(thread: groupThread, messageBody: body).build().anyInsert(transaction: transaction) } var defaultModelBuilder = TSGroupModelBuilder() defaultModelBuilder.groupsVersion = groupsVersion var defaultMembershipBuilder = GroupMembership.Builder() defaultMembershipBuilder.addFullMember(localAddress, role: .administrator) defaultModelBuilder.groupMembership = defaultMembershipBuilder.build() let defaultModel = try defaultModelBuilder.build(transaction: transaction) let defaultDMToken = DisappearingMessageToken.disabledToken do { insertOutgoingMessage(prefix + "created, empty.") let model1 = defaultModel let dmToken1 = defaultDMToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: nil, newGroupModel: model1, oldDisappearingMessageToken: nil, newDisappearingMessageToken: dmToken1, groupUpdateSourceAddress: updaterAddress, transaction: transaction) guard groupsVersion == .V2 else { // We add a "revision = 1" variant for v2 only. return } var modelBuilder2 = model1.asBuilder modelBuilder2.groupV2Revision = 1 let model2 = try modelBuilder2.build(transaction: transaction) let dmToken2 = defaultDMToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: nil, newGroupModel: model2, oldDisappearingMessageToken: nil, newDisappearingMessageToken: dmToken2, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { guard groupsVersion == .V1 else { // Inserting info messages for no-op updates will // trip an assert in v2 groups. return } insertOutgoingMessage(prefix + "modified, empty.") let model1 = defaultModel let dmToken1 = defaultDMToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model1, newGroupModel: model1, oldDisappearingMessageToken: dmToken1, newDisappearingMessageToken: dmToken1, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { insertOutgoingMessage(prefix + "modified, empty -> complicated -> empty.") let model1 = defaultModel let dmToken1 = defaultDMToken var modelBuilder2 = model1.asBuilder modelBuilder2.name = "name 2" modelBuilder2.avatarData = "avatar 2".data(using: .utf8) var groupMembershipBuilder1 = model1.groupMembership.asBuilder groupMembershipBuilder1.addFullMember(otherAddress0, role: .normal) if groupsVersion == .V2, let updaterUuid = updaterAddress?.uuid { groupMembershipBuilder1.addInvitedMember(otherAddress1, role: .normal, addedByUuid: updaterUuid) } modelBuilder2.groupMembership = groupMembershipBuilder1.build() modelBuilder2.groupAccess = .adminOnly let model2 = try modelBuilder2.build(transaction: transaction) let dmToken2 = DisappearingMessageToken(isEnabled: true, durationSeconds: 30) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model1, newGroupModel: model2, oldDisappearingMessageToken: dmToken1, newDisappearingMessageToken: dmToken2, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model2, newGroupModel: model1, oldDisappearingMessageToken: dmToken2, newDisappearingMessageToken: dmToken1, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { insertOutgoingMessage(prefix + "DMs off -> on -> changed -> off.") let model = defaultModel let dmToken0 = DisappearingMessageToken.disabledToken let dmToken1 = DisappearingMessageToken(isEnabled: true, durationSeconds: 30) let dmToken2 = DisappearingMessageToken(isEnabled: true, durationSeconds: 60) let dmToken3 = DisappearingMessageToken.disabledToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model, newGroupModel: model, oldDisappearingMessageToken: dmToken0, newDisappearingMessageToken: dmToken1, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model, newGroupModel: model, oldDisappearingMessageToken: dmToken1, newDisappearingMessageToken: dmToken2, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model, newGroupModel: model, oldDisappearingMessageToken: dmToken2, newDisappearingMessageToken: dmToken3, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { guard groupsVersion == .V2 else { return } insertOutgoingMessage(prefix + "access changes.") var modelBuilder1 = TSGroupModelBuilder() modelBuilder1.groupsVersion = groupsVersion modelBuilder1.groupAccess = .defaultForV2 modelBuilder1.groupMembership = defaultMembershipBuilder.build() let model1 = try modelBuilder1.build(transaction: transaction) var modelBuilder2 = model1.asBuilder modelBuilder2.groupAccess = .adminOnly let model2 = try modelBuilder2.build(transaction: transaction) var modelBuilder3 = model1.asBuilder modelBuilder3.groupAccess = .allAccess let model3 = try modelBuilder3.build(transaction: transaction) let dmToken = defaultDMToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model1, newGroupModel: model2, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model2, newGroupModel: model3, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model3, newGroupModel: model1, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { guard groupsVersion == .V2 else { return } insertOutgoingMessage(prefix + "role changes.") var members = [localAddress, otherAddress0, randoAddress ] if let updaterAddress = updaterAddress { members.append(updaterAddress) } members = Array(Set(members)).shuffled() var modelBuilder1 = defaultModel.asBuilder var groupMembershipBuilder1 = defaultModel.groupMembership.asBuilder for member in members { groupMembershipBuilder1.remove(member) groupMembershipBuilder1.addFullMember(member, role: .normal) } modelBuilder1.groupMembership = groupMembershipBuilder1.build() let model1 = try modelBuilder1.build(transaction: transaction) var modelBuilder2 = defaultModel.asBuilder var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder for member in members { groupMembershipBuilder2.remove(member) groupMembershipBuilder2.addFullMember(member, role: .administrator) } modelBuilder2.groupMembership = groupMembershipBuilder2.build() let model2 = try modelBuilder2.build(transaction: transaction) let dmToken = defaultDMToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model1, newGroupModel: model2, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model2, newGroupModel: model1, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { guard groupsVersion == .V2 else { return } insertOutgoingMessage(prefix + "basic membership changes.") var members = [localAddress, otherAddress0 ] if let updaterAddress = updaterAddress { members.append(updaterAddress) } members = Array(Set(members)).shuffled() var modelBuilder1 = defaultModel.asBuilder modelBuilder1.groupMembership = GroupMembership() let model1 = try modelBuilder1.build(transaction: transaction) var modelBuilder2 = defaultModel.asBuilder var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder for member in members { groupMembershipBuilder2.remove(member) groupMembershipBuilder2.addFullMember(member, role: .administrator) } modelBuilder2.groupMembership = groupMembershipBuilder2.build() let model2 = try modelBuilder2.build(transaction: transaction) let dmToken = defaultDMToken GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model1, newGroupModel: model2, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model2, newGroupModel: model1, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } do { guard groupsVersion == .V2 else { return } guard !isAnonymousUpdate else { return } insertOutgoingMessage(prefix + "invite variations.") var members = [localAddress, otherAddress0, otherAddress1 ] var inviters = [localAddress, otherAddress0 ] if let updaterAddress = updaterAddress { members.append(updaterAddress) inviters.append(updaterAddress) } members = Array(Set(members)).shuffled() inviters = Array(Set(inviters)).shuffled() for inviter in inviters { guard let inviterUuid = inviter.uuid else { continue } // Model 1: Empty. var modelBuilder1 = defaultModel.asBuilder modelBuilder1.groupMembership = GroupMembership() let model1 = try modelBuilder1.build(transaction: transaction) // Model 2: Invited. var modelBuilder2 = defaultModel.asBuilder var groupMembershipBuilder2 = defaultModel.groupMembership.asBuilder for member in members { groupMembershipBuilder2.remove(member) groupMembershipBuilder2.addInvitedMember(member, role: .normal, addedByUuid: inviterUuid) } modelBuilder2.groupMembership = groupMembershipBuilder2.build() let model2 = try modelBuilder2.build(transaction: transaction) // Model 3: Active members. var modelBuilder3 = defaultModel.asBuilder var groupMembershipBuilder3 = defaultModel.groupMembership.asBuilder for member in members { groupMembershipBuilder3.remove(member) groupMembershipBuilder3.addFullMember(member, role: .administrator) } modelBuilder3.groupMembership = groupMembershipBuilder3.build() let model3 = try modelBuilder3.build(transaction: transaction) let dmToken = defaultDMToken // Invite: 1 -> 2 GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model1, newGroupModel: model2, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) // Invite Accepted: 2 -> 3 GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model2, newGroupModel: model3, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) // Invite Declined or Revoked: 2 -> 1 GroupManager.insertGroupUpdateInfoMessage(groupThread: groupThread, oldGroupModel: model2, newGroupModel: model1, oldDisappearingMessageToken: dmToken, newDisappearingMessageToken: dmToken, groupUpdateSourceAddress: updaterAddress, transaction: transaction) } } } private func kickOtherGroupMembers(groupThread: TSGroupThread) { guard let localAddress = tsAccountManager.localAddress else { return owsFailDebug("Missing localAddress.") } let oldGroupModel = groupThread.groupModel let oldGroupMembership = oldGroupModel.groupMembership var groupMembershipBuilder = oldGroupMembership.asBuilder for address in oldGroupMembership.allMembersOfAnyKind { if address != localAddress { groupMembershipBuilder.remove(address) } } let newGroupMembership = groupMembershipBuilder.build() firstly { () -> Promise in var groupModelBuilder = oldGroupModel.asBuilder groupModelBuilder.groupMembership = newGroupMembership let newGroupModel = try databaseStorage.read { transaction in try groupModelBuilder.buildAsV2(transaction: transaction) } return GroupManager.localUpdateExistingGroup(oldGroupModel: oldGroupModel, newGroupModel: newGroupModel, dmConfiguration: nil, groupUpdateSourceAddress: localAddress) }.done { (_) -> Void in Logger.info("Success.") }.catch { error in owsFailDebug("Error: \(error)") } } private func sendInvalidGroupMessages(contactThread: TSContactThread) { let otherUserAddress = contactThread.contactAddress guard let otherUserUuid = otherUserAddress.uuid else { owsFailDebug("Recipient is missing UUID.") return } firstly { () -> Promise in // Make a real v2 group on the service. // Local user and "other user" are members. return firstly { GroupManager.localCreateNewGroup(members: [otherUserAddress], name: "Real group, both users are in the group", disappearingMessageToken: .disabledToken, shouldSendMessage: false) }.map(on: .global()) { (groupThread: TSGroupThread) in guard let validGroupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { throw OWSAssertionError("Invalid groupModel.") } return validGroupModelV2 } }.then(on: .global()) { (validGroupModelV2: TSGroupModelV2) -> Promise<(TSGroupModelV2, TSGroupModelV2)> in // Make a real v2 group on the service. // Local user is a member but "other user" is not. return firstly { GroupManager.localCreateNewGroup(members: [], name: "Real group, recipient is not in the group", disappearingMessageToken: .disabledToken, shouldSendMessage: false) }.map(on: .global()) { (groupThread: TSGroupThread) in guard let missingOtherUserGroupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { throw OWSAssertionError("Invalid groupModel.") } return (validGroupModelV2, missingOtherUserGroupModelV2) } }.then(on: .global()) { (validGroupModelV2: TSGroupModelV2, missingOtherUserGroupModelV2: TSGroupModelV2) -> Promise<(TSGroupModelV2, TSGroupModelV2, TSGroupModelV2)> in // Make a real v2 group on the service. // "Other user" is a member but local user is not. return firstly { () -> Promise in GroupManager.localCreateNewGroup(members: [otherUserAddress], name: "Real group, sender is not in the group", disappearingMessageToken: .disabledToken, shouldSendMessage: false) }.then(on: .global()) { (groupThread: TSGroupThread) -> Promise in guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { throw OWSAssertionError("Invalid groupModel.") } guard groupModel.groupMembership.isFullMember(otherUserAddress) else { throw OWSAssertionError("Other user is not a full member.") } // Last admin (local user) can't leave group, so first // make the "other user" an admin. return GroupManager.changeMemberRoleV2(groupModel: groupModel, uuid: otherUserUuid, role: .administrator) }.then(on: .global()) { (groupThread: TSGroupThread) -> Promise in GroupManager.localLeaveGroupOrDeclineInvite(groupThread: groupThread) }.map(on: .global()) { (groupThread: TSGroupThread) in guard let missingLocalUserGroupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { throw OWSAssertionError("Invalid groupModel.") } return (validGroupModelV2, missingOtherUserGroupModelV2, missingLocalUserGroupModelV2) } }.map(on: .global()) { (validGroupModelV2: TSGroupModelV2, missingOtherUserGroupModelV2: TSGroupModelV2, missingLocalUserGroupModelV2: TSGroupModelV2) in self.sendInvalidGroupMessages(contactThread: contactThread, validGroupModelV2: validGroupModelV2, missingOtherUserGroupModelV2: missingOtherUserGroupModelV2, missingLocalUserGroupModelV2: missingLocalUserGroupModelV2) }.done(on: .global()) { _ in Logger.info("Complete.") }.catch(on: .global()) { error in owsFailDebug("Error: \(error)") } } private func sendInvalidGroupMessages(contactThread: TSContactThread, validGroupModelV2: TSGroupModelV2, missingOtherUserGroupModelV2: TSGroupModelV2, missingLocalUserGroupModelV2: TSGroupModelV2) { var messages = [TSOutgoingMessage]() let groupContextInfoForGroupModel = { (groupModelV2: TSGroupModelV2) -> GroupV2ContextInfo in let masterKey = try! GroupsV2Protos.masterKeyData(forGroupModel: groupModelV2) return try! self.groupsV2.groupV2ContextInfo(forMasterKeyData: masterKey) } let validGroupContextInfo = groupContextInfoForGroupModel(validGroupModelV2) let missingOtherUserGroupContextInfo = groupContextInfoForGroupModel(missingOtherUserGroupModelV2) let missingLocalUserGroupContextInfo = groupContextInfoForGroupModel(missingLocalUserGroupModelV2) let buildValidGroupContextInfo = { () -> GroupV2ContextInfo in let groupsV2 = self.groupsV2 let groupSecretParamsData = try! groupsV2.generateGroupSecretParamsData() let masterKeyData = try! GroupsV2Protos.masterKeyData(forGroupSecretParamsData: groupSecretParamsData) return try! groupsV2.groupV2ContextInfo(forMasterKeyData: masterKeyData) } messages.append(OWSDynamicOutgoingMessage(thread: contactThread) { // Real and valid group id/master key/secret params. // Other user is not in the group. let masterKeyData = missingOtherUserGroupContextInfo.masterKeyData // Real revision. let revision: UInt32 = 0 let builder = SSKProtoGroupContextV2.builder() builder.setMasterKey(masterKeyData) builder.setRevision(revision) let dataBuilder = SSKProtoDataMessage.builder() dataBuilder.setGroupV2(try! builder.build()) dataBuilder.setRequiredProtocolVersion(0) dataBuilder.setBody("\(messages.count)") return Self.contentProtoData(forDataBuilder: dataBuilder) }) messages.append(OWSDynamicOutgoingMessage(thread: contactThread) { // Real and valid group id/master key/secret params. // Local user is not in the group. let masterKeyData = missingLocalUserGroupContextInfo.masterKeyData // Real revision. let revision: UInt32 = 0 let builder = SSKProtoGroupContextV2.builder() builder.setMasterKey(masterKeyData) builder.setRevision(revision) let dataBuilder = SSKProtoDataMessage.builder() dataBuilder.setGroupV2(try! builder.build()) dataBuilder.setRequiredProtocolVersion(0) dataBuilder.setBody("\(messages.count)") return Self.contentProtoData(forDataBuilder: dataBuilder) }) messages.append(OWSDynamicOutgoingMessage(thread: contactThread) { // Real and valid group id/master key/secret params. let masterKeyData = validGroupContextInfo.masterKeyData // Non-existent revision. let revision: UInt32 = 99 let builder = SSKProtoGroupContextV2.builder() builder.setMasterKey(masterKeyData) builder.setRevision(revision) let dataBuilder = SSKProtoDataMessage.builder() dataBuilder.setGroupV2(try! builder.build()) dataBuilder.setRequiredProtocolVersion(0) dataBuilder.setBody("\(messages.count)") return Self.contentProtoData(forDataBuilder: dataBuilder) }) messages.append(OWSDynamicOutgoingMessage(thread: contactThread) { // Real and valid group id/master key/secret params. var masterKeyData = validGroupContextInfo.masterKeyData // Truncate the master key. masterKeyData = masterKeyData.subdata(in: Int(0).. Data { let dataProto = try! dataBuilder.build() let contentBuilder = SSKProtoContent.builder() contentBuilder.setDataMessage(dataProto) let plaintextData = try! contentBuilder.buildSerializedData() return plaintextData } private func sendEmptyV1GroupUpdate(groupThread: TSGroupThread) { guard let localAddress = tsAccountManager.localAddress else { return owsFailDebug("Missing localAddress.") } let groupModel = groupThread.groupModel let timestamp = NSDate.ows_millisecondTimeStamp() let message = OWSDynamicOutgoingMessage(thread: groupThread) { let groupContextBuilder = SSKProtoGroupContext.builder(id: groupModel.groupId) groupContextBuilder.setType(.update) groupContextBuilder.addMembersE164(localAddress.phoneNumber!) let memberBuilder = SSKProtoGroupContextMember.builder() memberBuilder.setE164(localAddress.phoneNumber!) groupContextBuilder.addMembers(try! memberBuilder.build()) let dataBuilder = SSKProtoDataMessage.builder() dataBuilder.setTimestamp(timestamp) dataBuilder.setGroup(try! groupContextBuilder.build()) dataBuilder.setRequiredProtocolVersion(0) return Self.contentProtoData(forDataBuilder: dataBuilder) } firstly { () -> Promise in messageSender.sendMessage(.promise, message.asPreparer) }.done { (_) -> Void in Logger.info("Success.") }.catch { error in owsFailDebug("Error: \(error)") } } private func sendGroupUpdate(groupThread: TSGroupThread) { firstly { GroupManager.sendGroupUpdateMessage(thread: groupThread) }.done { _ in Logger.info("Success.") }.catch { error in owsFailDebug("Error: \(error)") } } private func updateV2GroupImmediately(groupThread: TSGroupThread) { guard let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { owsFailDebug("Invalid groupModel.") return } firstly { self.groupV2Updates.tryToRefreshV2GroupUpToCurrentRevisionImmediately(groupId: groupModelV2.groupId, groupSecretParamsData: groupModelV2.secretParamsData) }.done { _ in Logger.info("Success.") }.catch { error in owsFailDebug("Error: \(error)") } } } #endif