Further work on SessionMessagingKit migrations

Added migrations for contacts and started working through thread migration (have contact and closed group threads migrating)
Deprecated usage of ECKeyPair in the migrations (want to be able to remove Curve25519Kit in the future)
This commit is contained in:
Morgan Pretty 2022-04-06 15:43:26 +10:00
parent 0f4df804ed
commit cf66edb723
138 changed files with 4255 additions and 2397 deletions

View File

@ -230,7 +230,6 @@
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; };
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32032258B235D0020074B /* Storage+Contacts.swift */; };
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; };
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
@ -739,6 +738,21 @@
FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; };
FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; };
FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; };
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797127FAA2F500936362 /* Optional+Utilities.swift */; };
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797327FAB3E200936362 /* ProfileManager.swift */; };
FD09797727FAB7A600936362 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797627FAB7A600936362 /* Data+Image.swift */; };
FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797827FAB7E800936362 /* ImageFormat.swift */; };
FD09797B27FBB25900936362 /* Updatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797A27FBB25900936362 /* Updatable.swift */; };
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797C27FBDB2000936362 /* Notification+Utilities.swift */; };
FD09797F27FCFBFF00936362 /* OWSAES256Key+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797E27FCFBFF00936362 /* OWSAES256Key+Utilities.swift */; };
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798027FCFEE800936362 /* SessionThread.swift */; };
FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798227FD1A1500936362 /* ClosedGroup.swift */; };
FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798427FD1A6500936362 /* ClosedGroupKeyPair.swift */; };
FD09798727FD1B7800936362 /* GroupMember.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798627FD1B7800936362 /* GroupMember.swift */; };
FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798827FD1C5A00936362 /* OpenGroup.swift */; };
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798A27FD1CFE00936362 /* Capability.swift */; };
FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */; };
FD09799127FD499200936362 /* BoxKeyPair+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */; };
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */; };
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */; };
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
@ -1227,7 +1241,6 @@
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = "<group>"; };
B8B32032258B235D0020074B /* Storage+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Contacts.swift"; sourceTree = "<group>"; };
B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = "<group>"; };
B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; };
B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
@ -1772,6 +1785,21 @@
FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
FD09796D27FA6D0000936362 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
FD09796F27FA6FF300936362 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
FD09797127FAA2F500936362 /* Optional+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Utilities.swift"; sourceTree = "<group>"; };
FD09797327FAB3E200936362 /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
FD09797627FAB7A600936362 /* Data+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Image.swift"; sourceTree = "<group>"; };
FD09797827FAB7E800936362 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = "<group>"; };
FD09797A27FBB25900936362 /* Updatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updatable.swift; sourceTree = "<group>"; };
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Utilities.swift"; sourceTree = "<group>"; };
FD09797E27FCFBFF00936362 /* OWSAES256Key+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSAES256Key+Utilities.swift"; sourceTree = "<group>"; };
FD09798027FCFEE800936362 /* SessionThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThread.swift; sourceTree = "<group>"; };
FD09798227FD1A1500936362 /* ClosedGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroup.swift; sourceTree = "<group>"; };
FD09798427FD1A6500936362 /* ClosedGroupKeyPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroupKeyPair.swift; sourceTree = "<group>"; };
FD09798627FD1B7800936362 /* GroupMember.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMember.swift; sourceTree = "<group>"; };
FD09798827FD1C5A00936362 /* OpenGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = "<group>"; };
FD09798A27FD1CFE00936362 /* Capability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capability.swift; sourceTree = "<group>"; };
FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessageConfiguration.swift; sourceTree = "<group>"; };
FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoxKeyPair+Utilities.swift"; sourceTree = "<group>"; };
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacyModels.swift; sourceTree = "<group>"; };
@ -2314,13 +2342,16 @@
children = (
C33FDB54255A580D00E217F9 /* DataSource.h */,
C33FDBB6255A581600E217F9 /* DataSource.m */,
FD09797827FAB7E800936362 /* ImageFormat.swift */,
C33FDAFC255A580600E217F9 /* MIMETypeUtil.h */,
C33FDB41255A580C00E217F9 /* MIMETypeUtil.m */,
C33FDB29255A580A00E217F9 /* NSData+Image.h */,
C33FDAEF255A580500E217F9 /* NSData+Image.m */,
FD09797627FAB7A600936362 /* Data+Image.swift */,
C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */,
C33FDB1C255A580900E217F9 /* UIImage+OWS.h */,
C33FDB81255A581100E217F9 /* UIImage+OWS.m */,
FD09797A27FBB25900936362 /* Updatable.swift */,
);
path = Media;
sourceTree = "<group>";
@ -2666,7 +2697,6 @@
C33FDAB1255A580000E217F9 /* OWSStorage.m */,
C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */,
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */,
B8B32032258B235D0020074B /* Storage+Contacts.swift */,
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */,
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */,
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */,
@ -3148,6 +3178,7 @@
children = (
C33FDB01255A580700E217F9 /* AppReadiness.h */,
C33FDB75255A581000E217F9 /* AppReadiness.m */,
FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
C37F53E8255BA9BB002AEA92 /* Environment.h */,
C37F5402255BA9ED002AEA92 /* Environment.m */,
@ -3157,6 +3188,7 @@
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */,
C3A71D4825589FF20043A11F /* NSData+messagePadding.m */,
FD09797E27FCFBFF00936362 /* OWSAES256Key+Utilities.swift */,
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */,
C38EF281255B6D84007E1867 /* OWSAudioSession.swift */,
@ -3175,6 +3207,7 @@
7B1581E1271E743B00848B49 /* OWSSounds.swift */,
C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */,
C38EF306255B6DBE007E1867 /* OWSWindowManager.m */,
FD09797327FAB3E200936362 /* ProfileManager.swift */,
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */,
C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */,
C33FDB91255A581200E217F9 /* ProtoUtils.h */,
@ -3558,6 +3591,8 @@
isa = PBXGroup;
children = (
FD09796A27F6C67500936362 /* Failable.swift */,
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
);
path = Utilities;
@ -3568,6 +3603,13 @@
children = (
FD09796D27FA6D0000936362 /* Contact.swift */,
FD09796F27FA6FF300936362 /* Profile.swift */,
FD09798027FCFEE800936362 /* SessionThread.swift */,
FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */,
FD09798227FD1A1500936362 /* ClosedGroup.swift */,
FD09798427FD1A6500936362 /* ClosedGroupKeyPair.swift */,
FD09798827FD1C5A00936362 /* OpenGroup.swift */,
FD09798627FD1B7800936362 /* GroupMember.swift */,
FD09798A27FD1CFE00936362 /* Capability.swift */,
);
path = Models;
sourceTree = "<group>";
@ -4720,11 +4762,13 @@
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */,
FD17D7EA27F6A1C600122BE0 /* SUKLegacyModels.swift in Sources */,
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
FD09797B27FBB25900936362 /* Updatable.swift in Sources */,
C32C5A2D256DB849003C73A2 /* LKGroupUtilities.m in Sources */,
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */,
B8856DEF256F161F001CE70E /* NSString+SSK.m in Sources */,
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */,
FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */,
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */,
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */,
C3D9E43125676D3D0040E4F3 /* Configuration.swift in Sources */,
@ -4740,6 +4784,7 @@
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */,
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */,
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
@ -4777,10 +4822,12 @@
FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */,
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */,
B87EF18126377A1D00124B3C /* Features.swift in Sources */,
FD09797727FAB7A600936362 /* Data+Image.swift in Sources */,
C300A60D2554B31900555489 /* Logging.swift in Sources */,
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */,
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */,
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
FD17D7E727F6A16700122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
@ -4816,6 +4863,7 @@
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */,
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */,
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */,
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
@ -4824,14 +4872,19 @@
B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */,
C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */,
C32C5E75256DE020003C73A2 /* YapDatabaseTransaction+OWS.m in Sources */,
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */,
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
C3DB66AC260ACA42001EFC55 /* OpenGroupManagerV2.swift in Sources */,
B8F5F61B25EDE4BF003BF8D4 /* DataExtractionNotificationInfoMessage.swift in Sources */,
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
FD09797F27FCFBFF00936362 /* OWSAES256Key+Utilities.swift in Sources */,
FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */,
B8856D34256F1192001CE70E /* Environment.m in Sources */,
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */,
C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */,
C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */,
FD09798727FD1B7800936362 /* GroupMember.swift in Sources */,
FD09799127FD499200936362 /* BoxKeyPair+Utilities.swift in Sources */,
C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,
B8856ECE256F1E58001CE70E /* OWSPreferences.m in Sources */,
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
@ -4861,10 +4914,10 @@
C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */,
B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */,
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */,
FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */,
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */,
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
@ -4908,6 +4961,7 @@
C3A3A18A256E2092004D228D /* SignalRecipient.m in Sources */,
C3C2A74425539EB700C340D1 /* Message.swift in Sources */,
C32C5F11256DF79A003C73A2 /* SSKIncrementingIdFinder.swift in Sources */,
FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */,
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */,
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
@ -4916,11 +4970,13 @@
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */,
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */,
C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */,
C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */,
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */,
C32C5AF8256DC051003C73A2 /* OWSDisappearingMessagesConfiguration.m in Sources */,
C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */,

View File

@ -1,4 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import PromiseKit
import SessionMessagingKit
@objc(SNEditClosedGroupVC)
final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
@ -73,7 +77,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
backButton.tintColor = Colors.text
navigationItem.backBarButtonItem = backButton
func getDisplayName(for publicKey: String) -> String {
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
return Profile.displayName(for: publicKey)
}
setUpViewHierarchy()
// Always show zombies at the bottom
@ -107,7 +111,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
membersLabel.font = .systemFont(ofSize: Values.mediumFontSize)
membersLabel.text = "Members"
// Add members button
let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.membersAndZombies).isEmpty
let hasContactsToAdd = !Set(Contact.fetchAllIds()).subtracting(self.membersAndZombies).isEmpty
if (!hasContactsToAdd) {
addMembersButton.isUserInteractionEnabled = false
let disabledColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
@ -246,10 +250,10 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
var members = self.membersAndZombies
members.append(contentsOf: selectedUsers)
func getDisplayName(for publicKey: String) -> String {
return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
return Profile.displayName(for: publicKey)
}
self.membersAndZombies = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.membersAndZombies).isEmpty
let hasContactsToAdd = !Set(Contact.fetchAllIds()).subtracting(self.membersAndZombies).isEmpty
self.addMembersButton.isUserInteractionEnabled = hasContactsToAdd
let color = hasContactsToAdd ? Colors.accent : Colors.text.withAlphaComponent(Values.mediumOpacity)
self.addMembersButton.layer.borderColor = color.cgColor

View File

@ -1,4 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import PromiseKit
import SessionMessagingKit
private protocol TableViewTouchDelegate {
@ -15,7 +19,7 @@ private final class TableView : UITableView {
}
final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate, TableViewTouchDelegate, UITextFieldDelegate, UIScrollViewDelegate {
private let contacts = ContactUtilities.getAllContacts()
private let contacts = Contact.fetchAllIds()
private var selectedContacts: Set<String> = []
// MARK: Components
@ -174,7 +178,10 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
promise = MessageSender.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
}
let _ = promise.done(on: DispatchQueue.main) { thread in
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
self?.presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}

View File

@ -1,8 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import CoreServices
import Photos
import PhotosUI
import PromiseKit
import GRDB
import SessionUtilitiesKit
import SignalUtilitiesKit
@ -14,7 +17,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
// Don't take the user to settings for message requests
guard
let contactThread: TSContactThread = thread as? TSContactThread,
let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()),
let contact: Contact = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }),
contact.isApproved,
contact.didApproveMe
else {
@ -44,23 +47,31 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
@objc func unblock() {
guard let thread = thread as? TSContactThread else { return }
let publicKey = thread.contactSessionID()
UIView.animate(withDuration: 0.25, animations: {
self.blockedBanner.alpha = 0
}, completion: { _ in
if let contact: Contact = Storage.shared.getContact(with: publicKey) {
Storage.shared.write(
with: { transaction in
guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return }
contact.isBlocked = false
Storage.shared.setContact(contact, using: transaction)
UIView.animate(
withDuration: 0.25,
animations: {
self.blockedBanner.alpha = 0
},
completion: { _ in
GRDBStorage.shared.writeAsync(
updates: { db in
try Contact
.fetchOne(db, id: publicKey)?
.with(isBlocked: false)
.update(db)
},
completion: {
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
completion: { db, result in
switch result {
case .success:
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
default: break
}
}
)
}
})
)
}
func showBlockedModalIfNeeded() -> Bool {
@ -374,7 +385,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
// Update the input state if this is a contact thread
if let contactThread: TSContactThread = thread as? TSContactThread {
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
let contact: Contact? = GRDBStorage.shared.read { db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }
// If the contact doesn't exist yet then it's a message request without the first message sent
// so only allow text-based messages
@ -525,9 +536,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
} else {
switch viewItem.messageCellType {
case .audio:
if viewItem.interaction is TSIncomingMessage,
if
viewItem.interaction is TSIncomingMessage,
let thread = self.thread as? TSContactThread,
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
contact?.isTrusted != true {
confirmDownload()
} else {
playOrPauseAudio(for: viewItem)
@ -535,9 +548,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
case .mediaMessage:
guard let index = viewItems.firstIndex(where: { $0 === viewItem }),
let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell else { return }
if viewItem.interaction is TSIncomingMessage,
if
viewItem.interaction is TSIncomingMessage,
let thread = self.thread as? TSContactThread,
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
contact?.isTrusted != true {
confirmDownload()
} else {
guard let albumView = cell.albumView else { return }
@ -559,9 +574,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
gallery.presentDetailView(fromViewController: self, mediaAttachment: stream)
}
case .genericAttachment:
if viewItem.interaction is TSIncomingMessage,
if
viewItem.interaction is TSIncomingMessage,
let thread = self.thread as? TSContactThread,
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
contact?.isTrusted != true {
confirmDownload()
}
else if (
@ -1108,9 +1125,13 @@ extension ConversationVC {
// (it'll be updated with correct profile info if they accept the message request so this
// shouldn't cause weird behaviours)
let sessionId: String = contactThread.contactSessionID()
let contact: Contact = (Storage.shared.getContact(with: sessionId) ?? Contact(sessionID: sessionId))
guard !contact.isApproved else { return Promise.value(()) }
guard
let contact: Contact = GRDBStorage.shared.read({ db in Contact.fetchOrCreate(db, id: sessionId) }),
!contact.isApproved
else {
return Promise.value(())
}
return Promise.value(())
.then { [weak self] _ -> Promise<Void> in
@ -1151,49 +1172,58 @@ extension ConversationVC {
}
.map { _ in
// Default 'didApproveMe' to true for the person approving the message request
Storage.write { transaction in
contact.isApproved = true
contact.didApproveMe = (contact.didApproveMe || !isNewThread)
Storage.shared.setContact(contact, using: transaction)
}
// Send a sync message with the details of the contact
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
// Hide the 'messageRequestView' since the request has been approved and force a config
// sync to propagate the contact approval state (both must run on the main thread)
DispatchQueue.main.async { [weak self] in
let messageRequestViewWasVisible: Bool = (self?.messageRequestView.isHidden == false)
UIView.animate(withDuration: 0.3) {
self?.messageRequestView.isHidden = true
self?.scrollButtonMessageRequestsBottomConstraint?.isActive = false
self?.scrollButtonBottomConstraint?.isActive = true
// Update the table content inset and offset to account for the dissapearance of
// the messageRequestsView
if messageRequestViewWasVisible {
let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16)
let oldContentInset: UIEdgeInsets = (self?.messagesTableView.contentInset ?? UIEdgeInsets.zero)
self?.messagesTableView.contentInset = UIEdgeInsets(
top: 0,
leading: 0,
bottom: max(oldContentInset.bottom - messageRequestsOffset, 0),
trailing: 0
GRDBStorage.shared.writeAsync(
updates: { db in
try contact
.with(
isApproved: true,
didApproveMe: .update(contact.didApproveMe || !isNewThread)
)
.save(db)
},
completion: { db, _ in
// Send a sync message with the details of the contact
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
// Hide the 'messageRequestView' since the request has been approved
DispatchQueue.main.async { [weak self] in
let messageRequestViewWasVisible: Bool = (self?.messageRequestView.isHidden == false)
UIView.animate(withDuration: 0.3) {
self?.messageRequestView.isHidden = true
self?.scrollButtonMessageRequestsBottomConstraint?.isActive = false
self?.scrollButtonBottomConstraint?.isActive = true
// Update the table content inset and offset to account for
// the dissapearance of the messageRequestsView
if messageRequestViewWasVisible {
let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16)
let oldContentInset: UIEdgeInsets = (self?.messagesTableView.contentInset ?? UIEdgeInsets.zero)
self?.messagesTableView.contentInset = UIEdgeInsets(
top: 0,
leading: 0,
bottom: max(oldContentInset.bottom - messageRequestsOffset, 0),
trailing: 0
)
}
}
// Update UI
self?.updateNavBarButtons()
// Remove the 'MessageRequestsViewController' from the nav hierarchy if present
if
let viewControllers: [UIViewController] = self?.navigationController?.viewControllers,
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
messageRequestsIndex > 0
{
var newViewControllers = viewControllers
newViewControllers.remove(at: messageRequestsIndex)
self?.navigationController?.setViewControllers(newViewControllers, animated: false)
}
}
}
// Update UI
self?.updateNavBarButtons()
if let viewControllers: [UIViewController] = self?.navigationController?.viewControllers,
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
messageRequestsIndex > 0 {
var newViewControllers = viewControllers
newViewControllers.remove(at: messageRequestsIndex)
self?.navigationController?.setViewControllers(newViewControllers, animated: false)
}
}
)
}
}
@ -1220,44 +1250,52 @@ extension ConversationVC {
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
// Delete the request
Storage.write(
with: { [weak self] transaction in
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
GRDBStorage.shared.writeAsync(
updates: { [weak self] db in
// Update the contact
if let contactThread: TSContactThread = self?.thread as? TSContactThread {
let sessionId: String = contactThread.contactSessionID()
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
// Stop observing the `BlockListDidChange` notification (we are about to pop the screen
// so showing the banner just looks buggy)
if let strongSelf = self {
NotificationCenter.default.removeObserver(strongSelf, name: .contactBlockedStateChanged, object: nil)
}
contact.isApproved = false
contact.isBlocked = true
// Note: We set this to true so the current user will be able to send a
// message to the person who originally sent them the message request in
// the future if they unblock them
contact.didApproveMe = true
Storage.shared.setContact(contact, using: transaction)
// Stop observing the `BlockListDidChange` notification (we are about to pop the screen
// so showing the banner just looks buggy)
if let strongSelf = self {
NotificationCenter.default.removeObserver(strongSelf, name: .contactBlockedStateChanged, object: nil)
}
try? Contact
.fetchOne(db, id: sessionId)?
.with(
isApproved: false,
isBlocked: true,
// Note: We set this to true so the current user will be able to send a
// message to the person who originally sent them the message request in
// the future if they unblock them
didApproveMe: true
)
.update(db)
}
// Delete all thread content
self?.thread.removeAllThreadInteractions(with: transaction)
self?.thread.remove(with: transaction)
},
completion: { [weak self] in
// Force a config sync and pop to the previous screen
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
DispatchQueue.main.async {
self?.navigationController?.popViewController(animated: true)
}
completion: { db, _ in
Storage.write(
with: { [weak self] transaction in
// TODO: This should be above the contact updating
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
// Delete all thread content
self?.thread.removeAllThreadInteractions(with: transaction)
self?.thread.remove(with: transaction)
},
completion: { [weak self] in
// Force a config sync and pop to the previous screen
// TODO: This might cause an "incorrect thread" crash
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
DispatchQueue.main.async {
self?.navigationController?.popViewController(animated: true)
}
}
)
}
)
})

View File

@ -77,7 +77,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
}()
lazy var viewModel = ConversationViewModel(thread: thread, focusMessageIdOnOpen: nil, delegate: self)
@ -150,10 +150,9 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
lazy var blockedBanner: InfoBanner = {
let name: String
if let thread = thread as? TSContactThread {
let publicKey = thread.contactSessionID()
let context = Contact.context(for: thread)
name = Storage.shared.getContact(with: publicKey)?.displayName(for: context) ?? publicKey
} else {
name = Profile.displayName(for: thread.contactSessionID(), thread: thread)
}
else {
name = "Thread"
}
let message = "\(name) is blocked. Unblock them?"
@ -378,7 +377,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// Update the input state if this is a contact thread
if let contactThread: TSContactThread = thread as? TSContactThread {
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
let contact: Contact? = GRDBStorage.shared.read { db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }
// If the contact doesn't exist yet then it's a message request without the first message sent
// so only allow text-based messages
@ -473,7 +472,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
else {
if let contactThread: TSContactThread = thread as? TSContactThread {
// Don't show the settings button for message requests
if let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()), contact.isApproved, contact.didApproveMe {
if
let contact: Contact = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }),
contact.isApproved,
contact.didApproveMe
{
let size = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView()
profilePictureView.size = size
@ -657,7 +660,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
// Update the input state if this is a contact thread
if let contactThread: TSContactThread = thread as? TSContactThread {
let contact: Contact? = Storage.shared.getContact(with: contactThread.contactSessionID())
let contact: Contact? = GRDBStorage.shared.read { db in try Contact.fetchOne(db, id: contactThread.contactSessionID()) }
// If the contact doesn't exist yet then it's a message request without the first message sent
// so only allow text-based messages
@ -718,18 +721,17 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
}
// MARK: General
// MARK: - General
@objc func addOrRemoveBlockedBanner() {
func detach() {
blockedBanner.removeFromSuperview()
}
guard let thread = thread as? TSContactThread else { return detach() }
if thread.isBlocked() {
view.addSubview(blockedBanner)
blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
}
else {
detach()
DispatchQueue.main.async {
guard let thread = self.thread as? TSContactThread, thread.isBlocked() else {
self.blockedBanner.removeFromSuperview()
return
}
self.view.addSubview(self.blockedBanner)
self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view)
}
}

View File

@ -18,6 +18,7 @@
#import <SessionMessagingKit/TSThread.h>
#import <SessionMessagingKit/TSGroupThread.h>
#import <SessionMessagingKit/TSGroupModel.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseViewChange.h>
@ -258,11 +259,6 @@ NS_ASSUME_NONNULL_BEGIN
return SSKEnvironment.shared.tsAccountManager;
}
- (OWSProfileManager *)profileManager
{
return [OWSProfileManager sharedManager];
}
#pragma mark -
- (void)addNotificationListeners
@ -281,7 +277,7 @@ NS_ASSUME_NONNULL_BEGIN
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(localProfileDidChange:)
name:kNSNotificationName_LocalProfileDidChange
name:NSNotification.localProfileDidChange
object:nil];
}
@ -1271,8 +1267,7 @@ NS_ASSUME_NONNULL_BEGIN
}
if (shouldShowSenderName) {
SNContactContext context = [SNContact contextForThread:self.thread];
senderName = [[NSAttributedString alloc] initWithString:[[LKStorage.shared getContactWithSessionID:incomingSenderId] displayNameFor:context] ?: incomingSenderId];
senderName = [[NSAttributedString alloc] initWithString:[SMKProfile displayNameWithId:incomingSenderId thread:self.thread]];
}
// Show the sender profile picture for incoming group messages unless the

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
final class QuoteView : UIView {
private let mode: Mode
@ -180,8 +185,7 @@ final class QuoteView : UIView {
if let groupThread = thread as? TSGroupThread {
let authorLabel = UILabel()
authorLabel.lineBreakMode = .byTruncatingTail
let context: Contact.Context = groupThread.isOpenGroup ? .openGroup : .regular
authorLabel.text = Storage.shared.getContact(with: authorID)?.displayName(for: context) ?? authorID
authorLabel.text = Profile.displayName(for: authorID, thread: groupThread)
authorLabel.textColor = textColor
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
let authorLabelSize = authorLabel.systemLayoutSizeFitting(availableSpace)

View File

@ -1,5 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class InfoMessageCell : MessageCell {
import UIKit
import SessionUIKit
import SessionMessagingKit
final class InfoMessageCell: MessageCell {
private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: InfoMessageCell.iconSize)
private lazy var iconImageViewHeightConstraint = iconImageView.set(.height, to: InfoMessageCell.iconSize)
@ -48,7 +53,7 @@ final class InfoMessageCell : MessageCell {
let icon: UIImage?
switch message.messageType {
case .disappearingMessagesUpdate:
var configuration: OWSDisappearingMessagesConfiguration?
var configuration: SessionMessagingKit.Legacy.DisappearingMessagesConfiguration?
Storage.read { transaction in
configuration = message.thread(with: transaction).disappearingMessagesConfiguration(with: transaction)
}

View File

@ -1,3 +1,4 @@
import SessionUtilitiesKit
final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
private var unloadContent: (() -> Void)?
@ -351,11 +352,14 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
stackView.pin(to: snContentView, withInset: inset)
}
case .mediaMessage:
if viewItem.interaction is TSIncomingMessage,
if
viewItem.interaction is TSIncomingMessage,
let thread = thread as? TSContactThread,
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
contact?.isTrusted != true {
showMediaPlaceholder()
} else {
}
else {
guard let cache = delegate?.getMediaCache() else { preconditionFailure() }
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
@ -385,11 +389,14 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
stackView.pin(to: snContentView)
}
case .audio:
if viewItem.interaction is TSIncomingMessage,
if
viewItem.interaction is TSIncomingMessage,
let thread = thread as? TSContactThread,
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
contact?.isTrusted != true {
showMediaPlaceholder()
} else {
}
else {
let voiceMessageView = VoiceMessageView(viewItem: viewItem)
snContentView.addSubview(voiceMessageView)
voiceMessageView.pin(to: snContentView)
@ -397,11 +404,14 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
viewItem.lastAudioMessageView = voiceMessageView
}
case .genericAttachment:
if viewItem.interaction is TSIncomingMessage,
if
viewItem.interaction is TSIncomingMessage,
let thread = thread as? TSContactThread,
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
let contact: Contact? = GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: thread.contactSessionID()) }),
contact?.isTrusted != true {
showMediaPlaceholder()
} else {
}
else {
let inset: CGFloat = 12
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
// Stack view

View File

@ -105,18 +105,13 @@ CGFloat kIconViewLength = 24;
return SSKEnvironment.shared.tsAccountManager;
}
- (OWSProfileManager *)profileManager
{
return [OWSProfileManager sharedManager];
}
#pragma mark
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:kNSNotificationName_OtherUsersProfileDidChange
name:NSNotification.otherUsersProfileDidChange
object:nil];
}
@ -130,7 +125,7 @@ CGFloat kIconViewLength = 24;
NSString *threadName = self.thread.name;
if ([self.thread isKindOfClass:TSContactThread.class]) {
TSContactThread *thread = (TSContactThread *)self.thread;
return [[LKStorage.shared getContactWithSessionID:thread.contactSessionID] displayNameFor:SNContactContextRegular] ?: @"Anonymous";
return [SMKProfile displayNameWithId:thread.contactSessionID customFallback: @"Anonymous"];
} else if (threadName.length == 0 && [self isGroupThread]) {
threadName = [MessageStrings newGroupDefaultTitle];
}
@ -235,13 +230,12 @@ CGFloat kIconViewLength = 24;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _disappearingMessagesDurationLabel);
self.disappearingMessagesDurations = [OWSDisappearingMessagesConfiguration validDurationsSeconds];
self.disappearingMessagesConfiguration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueId:self.thread.uniqueId];
if (!self.disappearingMessagesConfiguration) {
self.disappearingMessagesConfiguration =
[[OWSDisappearingMessagesConfiguration alloc] initDefaultWithThreadId:self.thread.uniqueId];
self.disappearingMessagesConfiguration = [OWSDisappearingMessagesConfiguration defaultWith: self.thread.uniqueId];
}
[self updateTableContents];
@ -361,7 +355,7 @@ CGFloat kIconViewLength = 24;
displayName = @"the group";
} else {
TSContactThread *thread = (TSContactThread *)self.thread;
displayName = [[LKStorage.shared getContactWithSessionID:thread.contactSessionID] displayNameFor:SNContactContextRegular] ?: @"anonymous";
displayName = [SMKProfile displayNameWithId:thread.contactSessionID customFallback:@"anonymous"];
}
subtitleLabel.text = [NSString stringWithFormat:NSLocalizedString(@"When enabled, messages between you and %@ will disappear after they have been seen.", ""), displayName];
subtitleLabel.textColor = LKColors.text;
@ -762,7 +756,7 @@ CGFloat kIconViewLength = 24;
[infoMessage saveWithTransaction:transaction];
SNExpirationTimerUpdate *expirationTimerUpdate = [SNExpirationTimerUpdate new];
BOOL isEnabled = self.disappearingMessagesConfiguration.enabled;
BOOL isEnabled = self.disappearingMessagesConfiguration.isEnabled;
expirationTimerUpdate.duration = isEnabled ? self.disappearingMessagesConfiguration.durationSeconds : 0;
[SNMessageSender send:expirationTimerUpdate inThread:self.thread usingTransaction:transaction];
}];
@ -908,7 +902,7 @@ CGFloat kIconViewLength = 24;
- (void)toggleDisappearingMessages:(BOOL)flag
{
self.disappearingMessagesConfiguration.enabled = flag;
self.disappearingMessagesConfiguration.isEnabled = flag;
[self updateTableContents];
}
@ -1027,16 +1021,11 @@ CGFloat kIconViewLength = 24;
{
if (![self.thread isKindOfClass:TSContactThread.class]) { return; }
NSString *sessionID = ((TSContactThread *)self.thread).contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
if (contact == nil) {
contact = [[SNContact alloc] initWithSessionID:sessionID];
}
SMKProfile *profile = [SMKProfile fetchOrCreateWithId:sessionID];
NSString *text = [self.displayNameTextField.text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
contact.nickname = text.length > 0 ? text : nil;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
}];
self.displayNameLabel.text = text.length > 0 ? text : contact.name;
profile.nickname = text.length > 0 ? text : nil;
[SMKProfile saveProfile: profile];
self.displayNameLabel.text = text.length > 0 ? text : profile.name;
[self hideEditNameUI];
}
@ -1069,7 +1058,7 @@ CGFloat kIconViewLength = 24;
{
OWSAssertIsOnMainThread();
NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
NSString *recipientId = notification.userInfo[NSNotification.profileRecipientIdKey];
OWSAssertDebug(recipientId.length > 0);
if (recipientId.length > 0 && [self.thread isKindOfClass:[TSContactThread class]] &&

View File

@ -1,3 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
final class BlockedModal: Modal {
@ -19,7 +25,7 @@ final class BlockedModal: Modal {
override func populateContentView() {
// Name
let name = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
let name = Profile.displayName(for: publicKey)
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
@ -66,17 +72,15 @@ final class BlockedModal: Modal {
@objc private func unblock() {
let publicKey: String = self.publicKey
Storage.shared.write(
with: { transaction in
guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else {
return
}
contact.isBlocked = false
Storage.shared.setContact(contact, using: transaction as Any)
GRDBStorage.shared.writeAsync(
updates: { db in
try? Contact
.fetchOne(db, id: publicKey)?
.with(isBlocked: true)
.update(db)
},
completion: {
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
completion: { db, _ in
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
)

View File

@ -52,7 +52,7 @@ final class ConversationTitleView : UIView {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.groupThreadUpdated, object: nil)
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.muteSettingUpdated, object: nil)
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.contactUpdated, object: nil)
notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.profileUpdated, object: nil)
update()
}
@ -62,11 +62,18 @@ final class ConversationTitleView : UIView {
// MARK: Updating
@objc private func update() {
titleLabel.text = getTitle()
let subtitle = getSubtitle()
subtitleLabel.attributedText = subtitle
let titleFontSize = (subtitle != nil) ? Values.mediumFontSize : Values.veryLargeFontSize
titleLabel.font = .boldSystemFont(ofSize: titleFontSize)
DispatchQueue.main.async {
self.titleLabel.text = self.getTitle()
let subtitle: NSAttributedString? = self.getSubtitle()
self.subtitleLabel.attributedText = subtitle
self.titleLabel.font = .boldSystemFont(
ofSize: (subtitle != nil ?
Values.mediumFontSize :
Values.veryLargeFontSize
)
)
}
}
// MARK: General
@ -79,13 +86,9 @@ final class ConversationTitleView : UIView {
}
else {
let sessionID = (thread as! TSContactThread).contactSessionID()
var result = sessionID
Storage.read { transaction in
let displayName: String = ((Storage.shared.getContact(with: sessionID)?.displayName(for: .regular)) ?? sessionID)
let middleTruncatedHexKey: String = "\(sessionID.prefix(4))...\(sessionID.suffix(4))"
result = (displayName == sessionID ? middleTruncatedHexKey : displayName)
}
return result
let middleTruncatedHexKey: String = "\(sessionID.prefix(4))...\(sessionID.suffix(4))"
return Profile.displayName(for: sessionID, customFallback: middleTruncatedHexKey)
}
}

View File

@ -1,3 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
final class DownloadAttachmentModal : Modal {
private let viewItem: ConversationViewItem
@ -19,7 +26,7 @@ final class DownloadAttachmentModal : Modal {
override func populateContentView() {
guard let publicKey = (viewItem.interaction as? TSIncomingMessage)?.authorId else { return }
// Name
let name = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
let name = Profile.displayName(for: publicKey)
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
@ -65,15 +72,23 @@ final class DownloadAttachmentModal : Modal {
// MARK: Interaction
@objc private func trust() {
guard let message = viewItem.interaction as? TSIncomingMessage else { return }
let publicKey = message.authorId
let contact = Storage.shared.getContact(with: publicKey) ?? Contact(sessionID: publicKey)
contact.isTrusted = true
Storage.write(with: { transaction in
Storage.shared.setContact(contact, using: transaction)
MessageInvalidator.invalidate(message, with: transaction)
}, completion: {
Storage.shared.resumeAttachmentDownloadJobsIfNeeded(for: message.uniqueThreadId)
})
GRDBStorage.shared.writeAsync(
updates: { db in
try? Contact
.fetchOrCreate(db, id: message.authorId)
.with(isTrusted: true)
.save(db)
},
completion: { _, _ in
Storage.write(with: { transaction in
MessageInvalidator.invalidate(message, with: transaction)
}, completion: {
Storage.shared.resumeAttachmentDownloadJobsIfNeeded(for: message.uniqueThreadId)
})
}
)
presentingViewController?.dismiss(animated: true, completion: nil)
}
}

View File

@ -72,7 +72,9 @@ final class JoinOpenGroupModal : Modal {
Storage.shared.write { [presentingViewController = self.presentingViewController!] transaction in
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction)
.done(on: DispatchQueue.main) { _ in
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
}
}
.catch(on: DispatchQueue.main) { error in
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)

View File

@ -1,5 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class UserDetailsSheet : Sheet {
import UIKit
import SessionMessagingKit
final class UserDetailsSheet: Sheet {
private let sessionID: String
init(for sessionID: String) {
@ -26,7 +30,7 @@ final class UserDetailsSheet : Sheet {
profilePictureView.update()
// Display name label
let displayNameLabel = UILabel()
let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID
let displayName = Profile.displayName(for: sessionID)
displayNameLabel.text = displayName
displayNameLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
displayNameLabel.textColor = Colors.text

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Curve25519Kit
import SessionUtilitiesKit
final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionMessagingKit
import SessionUtilitiesKit
@ -155,8 +156,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
// Notifications
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject)
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: Notification.Name.otherUsersProfileDidChange, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name.localProfileDidChange, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: .OWSApplicationDidBecomeActive, object: nil)
@ -167,7 +168,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
self.threads.update(with: transaction) // Perform the initial update
}
// Start polling if needed (i.e. if the user just created or restored their Session ID)
if Identity.fetchUserKeyPair() != nil {
if Identity.userExists() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPoller()
@ -399,7 +400,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {
updateNavBarButtons()
DispatchQueue.main.async {
self.updateNavBarButtons()
}
}
@objc private func handleSeedViewedNotification(_ notification: Notification) {
@ -531,40 +534,48 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
let publicKey = thread.contactSessionID()
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
Storage.shared.write(
with: { transaction in
guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else {
return
}
contact.isBlocked = true
Storage.shared.setContact(contact, using: transaction as Any)
GRDBStorage.shared.writeAsync(
updates: { db in
try Contact
.fetchOrCreate(db, id: publicKey)
.with(isBlocked: true)
.save(db)
},
completion: {
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
DispatchQueue.main.async {
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
completion: { db, result in
switch result {
case .success:
MessageSender.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
DispatchQueue.main.async {
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
}
default: break
}
}
)
}
block.backgroundColor = Colors.unimportant
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
Storage.shared.write(
with: { transaction in
guard let transaction = transaction as? YapDatabaseReadWriteTransaction, let contact: Contact = Storage.shared.getContact(with: publicKey, using: transaction) else {
return
}
contact.isBlocked = false
Storage.shared.setContact(contact, using: transaction as Any)
GRDBStorage.shared.writeAsync(
updates: { db in
try Contact
.fetchOrCreate(db, id: publicKey)
.with(isBlocked: false)
.save(db)
},
completion: {
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
DispatchQueue.main.async {
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
completion: { db, result in
switch result {
case .success:
MessageSender.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
DispatchQueue.main.async {
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
}
default: break
}
}
)

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionMessagingKit
@ -107,7 +108,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
NotificationCenter.default.addObserver(
self,
selector: #selector(handleProfileDidChangeNotification(_:)),
name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange),
name: Notification.Name.otherUsersProfileDidChange,
object: nil
)
NotificationCenter.default.addObserver(
@ -302,32 +303,6 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
// MARK: - Interaction
private func updateContactAndThread(thread: TSThread, with transaction: YapDatabaseReadWriteTransaction, onComplete: ((Bool) -> ())? = nil) {
guard let contactThread: TSContactThread = thread as? TSContactThread else {
onComplete?(false)
return
}
var needsSync: Bool = false
// Update the contact
let sessionId: String = contactThread.contactSessionID()
if let contact: Contact = Storage.shared.getContact(with: sessionId), (contact.isApproved || !contact.isBlocked) {
contact.isApproved = false
contact.isBlocked = true
Storage.shared.setContact(contact, using: transaction)
needsSync = true
}
// Delete all thread content
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
onComplete?(needsSync)
}
@objc private func clearAllTapped() {
let threadCount: Int = Int(messageRequestCount)
let threads: [TSThread] = (0..<threadCount).compactMap { self.thread(at: $0) }
@ -336,36 +311,47 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet)
alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in
// Clear the requests
Storage.write(
with: { [weak self] transaction in
GRDBStorage.shared.writeAsync(
updates: { db in
threads.forEach { thread in
if let uniqueId: String = thread.uniqueId {
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
}
self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in
if threadNeedsSync {
needsSync = true
}
}
// Block the contact
if
guard
let sessionId: String = (thread as? TSContactThread)?.contactSessionID(),
!thread.isBlocked(),
let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction)
{
contact.isBlocked = true
Storage.shared.setContact(contact, using: transaction)
needsSync = true
}
let contact: Contact = try? Contact.fetchOrCreate(db, id: sessionId),
!contact.isBlocked
else { return }
try? contact
.with(
isApproved: false,
isBlocked: true
)
.save(db)
needsSync = true
}
},
completion: {
// Force a config sync
if needsSync {
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
}
completion: { db, _ in
Storage.write(
with: { transaction in
threads.forEach { thread in
if let uniqueId: String = thread.uniqueId {
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
}
// Delete all thread content
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
}
},
completion: {
// Force a config sync
if needsSync {
// TODO: This might crash due to a "wrong thread" error
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
}
)
}
)
})
@ -378,24 +364,36 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
Storage.write(
with: { [weak self] transaction in
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
self?.updateContactAndThread(thread: thread, with: transaction)
// Block the contact
if
GRDBStorage.shared.writeAsync(
updates: { db in
guard
let sessionId: String = (thread as? TSContactThread)?.contactSessionID(),
!thread.isBlocked(),
let contact: Contact = Storage.shared.getContact(with: sessionId, using: transaction)
{
contact.isBlocked = true
Storage.shared.setContact(contact, using: transaction)
}
let contact: Contact = try? Contact.fetchOrCreate(db, id: sessionId),
!contact.isBlocked
else { return }
try? contact
.with(
isApproved: false,
isBlocked: true
)
.save(db)
},
completion: {
// Force a config sync
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
completion: { db, _ in
Storage.write(
with: { transaction in
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
// Delete all thread content
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
},
completion: {
// Force a config sync
// TODO: This might crash due to a "wrong thread" error
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
)
}
)
})

View File

@ -680,9 +680,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
private func senderName(message: TSMessage) -> String {
switch message {
case let incomingMessage as TSIncomingMessage:
let publicKey = incomingMessage.authorId
let context = Contact.context(for: incomingMessage.thread)
return Storage.shared.getContact(with: publicKey)?.displayName(for: context) ?? publicKey
return Profile.displayName(for: incomingMessage.authorId, thread: incomingMessage.thread)
case is TSOutgoingMessage:
return NSLocalizedString("MEDIA_GALLERY_SENDER_NAME_YOU", comment: "Short sender label for media sent by you")
default:

View File

@ -56,11 +56,6 @@ static NSTimeInterval launchStartedAt;
#pragma mark - Dependencies
- (OWSProfileManager *)profileManager
{
return [OWSProfileManager sharedManager];
}
- (OWSReadReceiptManager *)readReceiptManager
{
return [OWSReadReceiptManager sharedManager];
@ -365,14 +360,10 @@ static NSTimeInterval launchStartedAt;
NSDate *now = [NSDate new];
NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"];
if (lastProfilePictureUpload != nil && [now timeIntervalSinceDate:lastProfilePictureUpload] > 14 * 24 * 60 * 60) {
OWSProfileManager *profileManager = OWSProfileManager.sharedManager;
NSString *name = [[LKStorage.shared getUser] name];
UIImage *profilePicture = [profileManager profileAvatarForRecipientId:userPublicKey];
[profileManager updateLocalProfileName:name avatarImage:profilePicture success:^{
// Do nothing; the user defaults flag is updated in LokiFileServerAPI
} failure:^(NSError *error) {
// Do nothing
} requiresSync:YES];
// The user defaults flag is updated in ProfileManager
NSString *name = [SMKProfile fetchCurrentUserName];
UIImage *profilePicture = [SMKProfileManager profileAvatarWithRecipientId:userPublicKey];
[SMKProfileManager updateLocalWithProfileName:name avatarImage:profilePicture requiresSync:YES];
}
if (CurrentAppContext().isMainApp) {

View File

@ -9,24 +9,26 @@ extension AppDelegate {
@objc(syncConfigurationIfNeeded)
func syncConfigurationIfNeeded() {
guard Storage.shared.getUser()?.name != nil else { return }
let userDefaults = UserDefaults.standard
let lastSync = userDefaults[.lastConfigurationSync] ?? .distantPast
guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60 else { return } // Sync every 2 days
let lastSync: Date = (UserDefaults.standard[.lastConfigurationSync] ?? .distantPast)
MessageSender.syncConfiguration(forceSyncNow: false)
.done {
// Only update the 'lastConfigurationSync' timestamp if we have done the first sync (Don't want
// a new device config sync to override config syncs from other devices)
if userDefaults[.hasSyncedInitialConfiguration] {
userDefaults[.lastConfigurationSync] = Date()
guard Date().timeIntervalSince(lastSync) > (7 * 24 * 60 * 60) else { return } // Sync every 2 days
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: false)
.done {
// Only update the 'lastConfigurationSync' timestamp if we have done the
// first sync (Don't want a new device config sync to override config
// syncs from other devices)
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
UserDefaults.standard[.lastConfigurationSync] = Date()
}
}
}
.retainUntilComplete()
.retainUntilComplete()
}
}
@objc func startClosedGroupPoller() {
guard Identity.fetchUserKeyPair() != nil else { return }
guard Identity.userExists() else { return }
ClosedGroupPoller.shared.start()
}

View File

@ -197,8 +197,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
return
}
let context = Contact.context(for: thread)
let senderName = Storage.shared.getContact(with: incomingMessage.authorId, using: transaction)?.displayName(for: context) ?? incomingMessage.authorId
let senderName = Profile.displayName(for: incomingMessage.authorId, thread: incomingMessage.thread)
let notificationTitle: String?
var notificationBody: String?

View File

@ -131,10 +131,12 @@ final class DisplayNameVC : BaseVC {
guard !displayName.isEmpty else {
return showError(title: NSLocalizedString("vc_display_name_display_name_missing_error", comment: ""))
}
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
guard !ProfileManager.isToLong(profileName: displayName) else {
return showError(title: NSLocalizedString("vc_display_name_display_name_too_long_error", comment: ""))
}
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { _ in }, requiresSync: false) // Try to save the user name but ignore the result
// Try to save the user name but ignore the result
ProfileManager.updateLocal(profileName: displayName, avatarImage: nil, requiredSync: false)
let pnModeVC = PNModeVC()
navigationController!.pushViewController(pnModeVC, animated: true)
}

View File

@ -2,7 +2,10 @@
import Foundation
import Sodium
import GRDB
import Curve25519Kit
import SessionUtilitiesKit
import SessionMessagingKit
enum Onboarding {
@ -14,11 +17,13 @@ enum Onboarding {
Identity.store(seed: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519PublicKey
Storage.writeSync { transaction in
let user = Contact(sessionID: x25519PublicKey)
user.isApproved = true
user.didApproveMe = true
Storage.shared.setContact(user, using: transaction)
GRDBStorage.shared.write { db in
try Contact(id: x25519PublicKey)
.with(
isApproved: true,
didApproveMe: true
)
.save(db)
}
switch self {

View File

@ -1,4 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Sodium
import Curve25519Kit
final class RegisterVC : BaseVC {
private var seed: Data! { didSet { updateKeyPair() } }

View File

@ -10,7 +10,7 @@ final class SeedVC: BaseVC {
}
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
}()
private lazy var redactedMnemonic: String = {

View File

@ -1,3 +1,4 @@
import SessionUtilitiesKit
final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
@ -143,8 +144,11 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
Storage.shared.write { transaction in
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction)
.done(on: DispatchQueue.main) { [weak self] _ in
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
}
self?.presentingViewController?.dismiss(animated: true, completion: nil)
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
}
.catch(on: DispatchQueue.main) { [weak self] error in
self?.dismiss(animated: true, completion: nil) // Dismiss the loader

View File

@ -124,12 +124,16 @@ final class NukeDataModal : Modal {
@objc private func clearDeviceOnly() {
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] _ in
MessageSender.syncConfiguration(forceSyncNow: true).ensure(on: DispatchQueue.main) {
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
}.retainUntilComplete()
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: true)
.ensure(on: DispatchQueue.main) {
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later
General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
}
.retainUntilComplete()
}
}
}

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Curve25519Kit
import SessionUtilitiesKit
final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)

View File

@ -12,7 +12,7 @@ final class SeedModal: Modal {
}
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
}()
// MARK: Lifecycle

View File

@ -147,7 +147,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
profilePictureView.publicKey = getUserHexEncodedPublicKey()
profilePictureView.update()
// Display name label
displayNameLabel.text = Storage.shared.getUser()?.name
displayNameLabel.text = Profile.fetchOrCreateCurrentUser().name
// Display name container
let displayNameContainer = UIView()
displayNameContainer.accessibilityLabel = "Edit display name text field"
@ -346,7 +346,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
}
func avatarDidChange(_ image: UIImage) {
let maxSize = Int(kOWSProfileManager_MaxAvatarDiameter)
let maxSize = Int(ProfileManager.maxAvatarDiameter)
profilePictureToBeUploaded = image.resizedImage(toFillPixelSize: CGSize(width: maxSize, height: maxSize))
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
}
@ -358,41 +358,47 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
private func updateProfile(isUpdatingDisplayName: Bool, isUpdatingProfilePicture: Bool) {
let userDefaults = UserDefaults.standard
let name = displayNameToBeUploaded ?? Storage.shared.getUser()?.name
let profilePicture = profilePictureToBeUploaded ?? OWSProfileManager.shared().profileAvatar(forRecipientId: getUserHexEncodedPublicKey())
let name: String? = (displayNameToBeUploaded ?? Profile.fetchOrCreateCurrentUser().name)
let profilePicture: UIImage? = (profilePictureToBeUploaded ?? ProfileManager.profileAvatar(for: getUserHexEncodedPublicKey()))
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self, displayNameToBeUploaded, profilePictureToBeUploaded] modalActivityIndicator in
OWSProfileManager.shared().updateLocalProfileName(name, avatarImage: profilePicture, success: {
if displayNameToBeUploaded != nil {
userDefaults[.lastDisplayNameUpdate] = Date()
}
if profilePictureToBeUploaded != nil {
userDefaults[.lastProfilePictureUpdate] = Date()
}
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
guard let self = self else { return }
self.profilePictureView.update()
self.displayNameLabel.text = name
self.profilePictureToBeUploaded = nil
self.displayNameToBeUploaded = nil
ProfileManager.updateLocal(
profileName: (name ?? ""),
avatarImage: profilePicture,
requiredSync: true,
success: {
if displayNameToBeUploaded != nil {
userDefaults[.lastDisplayNameUpdate] = Date()
}
}
}, failure: { error in
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
var isMaxFileSizeExceeded = false
if let error = error as? FileServerAPIV2.Error {
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
if profilePictureToBeUploaded != nil {
userDefaults[.lastProfilePictureUpdate] = Date()
}
GRDBStorage.shared.write { db in
MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
guard let self = self else { return }
self.profilePictureView.update()
self.displayNameLabel.text = name
self.profilePictureToBeUploaded = nil
self.displayNameToBeUploaded = nil
}
}
},
failure: { error in
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
let isMaxFileSizeExceeded = (error == .avatarUploadMaxFileSizeExceeded)
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
}
}, requiresSync: true)
)
}
}
@ -461,7 +467,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
guard !displayName.isEmpty else {
return showError(title: NSLocalizedString("vc_settings_display_name_missing_error", comment: ""))
}
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
guard !ProfileManager.isToLong(profileName: displayName) else {
return showError(title: NSLocalizedString("vc_settings_display_name_too_long_error", comment: ""))
}
isEditingDisplayName = false

View File

@ -239,10 +239,9 @@ final class ConversationCell : UITableViewCell {
// Contact
if threadViewModel.isGroupThread, let thread = threadViewModel.threadRecord as? TSGroupThread {
displayNameLabel.attributedText = getHighlightedSnippet(snippet: getDisplayName(), searchText: normalizedSearchText, fontSize: Values.mediumFontSize)
let context: Contact.Context = thread.isOpenGroup ? .openGroup : .regular
var rawSnippet: String = ""
thread.groupModel.groupMemberIds.forEach{ id in
if let displayName = Storage.shared.getContact(with: id)?.displayName(for: context) {
thread.groupModel.groupMemberIds.forEach { id in
if let displayName = Profile.displayNameNoFallback(for: id, thread: thread) {
if !rawSnippet.isEmpty {
rawSnippet += ", \(displayName)"
}
@ -348,7 +347,7 @@ final class ConversationCell : UITableViewCell {
private func getMessageAuthorName(message: TSMessage) -> String? {
guard threadViewModel.isGroupThread else { return nil }
if let incomingMessage = message as? TSIncomingMessage {
return Storage.shared.getContact(with: incomingMessage.authorId)?.displayName(for: .regular) ?? "Anonymous"
return Profile.displayName(for: incomingMessage.authorId, customFallback: "Anonymous")
}
return nil
}
@ -356,14 +355,14 @@ final class ConversationCell : UITableViewCell {
private func getDisplayNameForSearch(_ sessionID: String) -> String {
if threadViewModel.threadRecord.isNoteToSelf() {
return NSLocalizedString("NOTE_TO_SELF", comment: "")
} else {
var result = sessionID
if let contact = Storage.shared.getContact(with: sessionID), let name = contact.name {
result = name
if let nickname = contact.nickname { result += "(\(nickname))"}
}
return result
}
return [
Profile.displayName(for: sessionID),
Profile.fetchOrCreate(id: sessionID).nickname.map { "(\($0)" }
]
.compactMap { $0 }
.joined(separator: " ")
}
private func getDisplayName() -> String {
@ -381,9 +380,9 @@ final class ConversationCell : UITableViewCell {
}
else {
let hexEncodedPublicKey: String = threadViewModel.contactSessionID!
let displayName: String = (Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey)
let middleTruncatedHexKey: String = "\(hexEncodedPublicKey.prefix(4))...\(hexEncodedPublicKey.suffix(4))"
return (displayName == hexEncodedPublicKey ? middleTruncatedHexKey : displayName)
return Profile.displayName(for: hexEncodedPublicKey, customFallback: middleTruncatedHexKey)
}
}
}

View File

@ -82,7 +82,7 @@ final class UserCell : UITableViewCell {
func update() {
profilePictureView.publicKey = publicKey
profilePictureView.update()
displayNameLabel.text = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
displayNameLabel.text = Profile.displayName(for: publicKey)
switch accessory {
case .none: accessoryImageView.isHidden = true

View File

@ -1,3 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionMessagingKit
@objc(SNUserSelectionVC)
final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
@ -7,7 +11,7 @@ final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate
private var selectedUsers: Set<String> = []
private lazy var users: [String] = {
var result = ContactUtilities.getAllContacts()
var result = Contact.fetchAllIds()
result.removeAll { usersToExclude.contains($0) }
return result
}()

View File

@ -15,10 +15,6 @@ public class AccountManager: NSObject {
// MARK: - Dependencies
var profileManager: OWSProfileManager {
return OWSProfileManager.shared()
}
private var preferences: OWSPreferences {
return Environment.shared.preferences
}

View File

@ -17,9 +17,7 @@ public final class MentionUtilities : NSObject {
while let match = outerMatch {
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
let matchEnd: Int
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
if let displayName = displayName {
if let displayName = Profile.displayNameNoFallback(for: publicKey, context: (openGroupV2 != nil ? .openGroup : .regular)) {
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
mentions.append((range: NSRange(location: match.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
matchEnd = match.range.location + displayName.utf16.count

View File

@ -2,6 +2,7 @@
import Foundation
import Sodium
import Curve25519Kit
import SessionMessagingKit
enum MockDataGenerator {
@ -146,7 +147,7 @@ enum MockDataGenerator {
!isMessageRequest &&
(((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user
)
Storage.shared.setContact(contact, using: transaction)
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
logProgress("DM Thread \(threadIndex)", "Generate \(numMessages) Messages")
@ -209,7 +210,7 @@ enum MockDataGenerator {
contact.name = (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
.joined()
Storage.shared.setContact(contact, using: transaction)
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
members.append(randomSessionId)
}
@ -229,8 +230,12 @@ enum MockDataGenerator {
// Add the group to the user's set of public keys to poll for and store the key pair
let encryptionKeyPair = Curve25519.generateKeyPair()
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: randomGroupPublicKey, using: transaction)
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
@ -291,7 +296,7 @@ enum MockDataGenerator {
contact.name = (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
Storage.shared.setContact(contact, using: transaction)
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
members.append(randomSessionId)
}

View File

@ -1,18 +1,30 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Mantle
import YapDatabase
import SignalCoreKit
public enum Legacy {
// MARK: - Collections and Keys
internal static let contactThreadPrefix = "c"
internal static let groupThreadPrefix = "g"
internal static let closedGroupIdPrefix = "__textsecure_group__!"
internal static let closedGroupKeyPairPrefix = "SNClosedGroupEncryptionKeyPairCollection-"
public static let contactCollection = "LokiContactCollection"
internal static let threadCollection = "TSThread"
internal static let contactCollection = "LokiContactCollection"
internal static let disappearingMessagesCollection = "OWSDisappearingMessagesConfiguration"
internal static let closedGroupPublicKeyCollection = "SNClosedGroupPublicKeyCollection"
internal static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
internal static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
// MARK: - Types
public typealias Contact = _LegacyContact
public typealias DisappearingMessagesConfiguration = _LegacyDisappearingMessagesConfiguration
@objc(SNProfile)
public class Profile: NSObject, NSCoding {
public var displayName: String?

View File

@ -39,5 +39,56 @@ enum _001_InitialSetupMigration: Migration {
t.column(.profilePictureFileName, .text)
t.column(.profileEncryptionKey, .blob)
}
try db.create(table: SessionThread.self) { t in
t.column(.id, .text)
.notNull()
.primaryKey()
t.column(.variant, .integer).notNull()
t.column(.creationDateTimestamp, .double).notNull()
t.column(.shouldBeVisible, .boolean).notNull()
t.column(.isPinned, .boolean).notNull()
t.column(.messageDraft, .text)
t.column(.notificationMode, .integer).notNull()
t.column(.mutedUntilTimestamp, .double)
}
try db.create(table: DisappearingMessagesConfiguration.self) { t in
t.column(.id, .text)
.notNull()
.primaryKey()
.references(SessionThread.self)
t.column(.isEnabled, .boolean)
.defaults(to: false)
.notNull()
t.column(.durationSeconds, .double)
.defaults(to: 0)
.notNull()
}
try db.create(table: ClosedGroup.self) { t in
t.column(.publicKey, .text)
.notNull()
.primaryKey()
t.column(.name, .text).notNull()
t.column(.formationTimestamp, .double).notNull()
}
try db.create(table: ClosedGroupKeyPair.self) { t in
t.column(.publicKey, .text)
.notNull()
.indexed()
.references(ClosedGroup.self)
t.column(.secretKey, .blob).notNull()
t.column(.receivedTimestamp, .double).notNull()
}
try db.create(table: GroupMember.self) { t in
t.column(.groupId, .text)
.notNull()
.indexed()
t.column(.profileId, .text).notNull()
t.column(.role, .integer).notNull()
}
}
}

View File

@ -2,6 +2,7 @@
import Foundation
import GRDB
import Curve25519Kit
import SessionUtilitiesKit
enum _002_YDBToGRDBMigration: Migration {
@ -9,10 +10,19 @@ enum _002_YDBToGRDBMigration: Migration {
// TODO: Autorelease pool???.
static func migrate(_ db: Database) throws {
// MARK: - Contacts
// MARK: - Contacts & Threads
var shouldFailMigration: Bool = false
var contacts: Set<Legacy.Contact> = []
var contactThreadIds: Set<String> = []
var threads: Set<TSThread> = []
var disappearingMessagesConfiguration: [String: Legacy.DisappearingMessagesConfiguration] = [:]
var closedGroupKeys: [String: (timestamp: TimeInterval, keys: SessionUtilitiesKit.Legacy.KeyPair)] = [:]
var closedGroupName: [String: String] = [:]
var closedGroupFormation: [String: UInt64] = [:]
var closedGroupModel: [String: TSGroupModel] = [:]
var closedGroupZombieMemberIds: [String: Set<String>] = [:]
var openGroupInfo: [String: OpenGroupV2] = [:]
Storage.read { transaction in
// Process the Contacts
@ -21,24 +31,105 @@ enum _002_YDBToGRDBMigration: Migration {
contacts.insert(contact)
}
// Process the contact threads (only want to create "real" contacts in the new structure)
transaction.enumerateKeys(inCollection: Legacy.threadCollection) { key, _ in
guard key.starts(with: Legacy.contactThreadPrefix) else { return }
contactThreadIds.insert(key)
let userClosedGroupPublicKeys: [String] = transaction.allKeys(inCollection: Legacy.closedGroupPublicKeyCollection)
// Process the threads
transaction.enumerateKeysAndObjects(inCollection: Legacy.threadCollection) { key, object, _ in
guard let thread: TSThread = object as? TSThread else { return }
guard let threadId: String = thread.uniqueId else { return }
threads.insert(thread)
// Want to exclude threads which aren't visible (ie. threads which we started
// but the user never ended up sending a message)
if key.starts(with: Legacy.contactThreadPrefix) && thread.shouldBeVisible {
contactThreadIds.insert(key)
}
// Get the disappearing messages config
disappearingMessagesConfiguration[threadId] = transaction
.object(forKey: threadId, inCollection: Legacy.disappearingMessagesCollection)
.asType(Legacy.DisappearingMessagesConfiguration.self)
.defaulting(to: Legacy.DisappearingMessagesConfiguration.defaultWith(threadId))
// Process group-specific info
guard let groupThread: TSGroupThread = thread as? TSGroupThread else { return }
if groupThread.isClosedGroup {
// The old threadId for closed groups was in the below format, we don't
// really need the unnecessary complexity so process the key and extract
// the publicKey from it
// `g{base64String(Data(__textsecure_group__!{publicKey}))}
let base64GroupId: String = String(threadId.suffix(from: threadId.index(after: threadId.startIndex)))
guard
let groupIdData: Data = Data(base64Encoded: base64GroupId),
let groupId: String = String(data: groupIdData, encoding: .utf8),
let publicKey: String = groupId.split(separator: "!").last.map({ String($0) }),
let formationTimestamp: UInt64 = transaction.object(forKey: publicKey, inCollection: Legacy.closedGroupFormationTimestampCollection) as? UInt64
else {
SNLog("Unable to decode Closed Group during migration")
shouldFailMigration = true
return
}
guard userClosedGroupPublicKeys.contains(publicKey) else {
SNLog("Found unexpected invalid closed group public key during migration")
shouldFailMigration = true
return
}
let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
closedGroupName[threadId] = groupThread.name(with: transaction)
closedGroupModel[threadId] = groupThread.groupModel
closedGroupFormation[threadId] = formationTimestamp
closedGroupZombieMemberIds[threadId] = transaction.object(
forKey: publicKey,
inCollection: Legacy.closedGroupZombieMembersCollection
) as? Set<String>
transaction.enumerateKeysAndObjects(inCollection: keyCollection) { key, object, _ in
guard let timestamp: TimeInterval = TimeInterval(key), let keyPair: SessionUtilitiesKit.Legacy.KeyPair = object as? SessionUtilitiesKit.Legacy.KeyPair else {
return
}
closedGroupKeys[threadId] = (timestamp, keyPair)
}
}
else if groupThread.isOpenGroup {
}
}
}
// We can't properly throw within the 'enumerateKeysAndObjects' block so have to throw here
guard !shouldFailMigration else { throw GRDBStorageError.migrationFailed }
// Insert the data into GRDB
// MARK: - Insert Contacts
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
try contacts.forEach { contact in
let isCurrentUser: Bool = (contact.sessionID == currentUserPublicKey)
let contactThreadId: String = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
// Determine if this contact is a "real" contact
// Create the "Profile" for the legacy contact
try Profile(
id: contact.sessionID,
name: (contact.name ?? contact.sessionID),
nickname: contact.nickname,
profilePictureUrl: contact.profilePictureURL,
profilePictureFileName: contact.profilePictureFileName,
profileEncryptionKey: contact.profileEncryptionKey
).insert(db)
// Determine if this contact is a "real" contact (don't want to create contacts for
// every user in the new structure but still want profiles for every user)
if
// TODO: Thread.shouldBeVisible???
isCurrentUser ||
contactThreadIds.contains(contactThreadId) ||
contact.isApproved ||
@ -56,16 +147,110 @@ enum _002_YDBToGRDBMigration: Migration {
hasBeenBlocked: (!isCurrentUser && (contact.hasBeenBlocked || contact.isBlocked))
).insert(db)
}
}
// MARK: - Insert Threads
try threads.forEach { thread in
guard let legacyThreadId: String = thread.uniqueId else { return }
// Create the "Profile" for the legacy contact
try Profile(
id: contact.sessionID,
name: (contact.name ?? contact.sessionID),
nickname: contact.nickname,
profilePictureUrl: contact.profilePictureURL,
profilePictureFileName: contact.profilePictureFileName,
profileEncryptionKey: contact.profileEncryptionKey
let id: String
let variant: SessionThread.Variant
let notificationMode: SessionThread.NotificationMode
switch thread {
case let groupThread as TSGroupThread:
if groupThread.isOpenGroup {
id = legacyThreadId//openGroup.id
variant = .openGroup
}
else {
guard let publicKey: Data = closedGroupKeys[legacyThreadId]?.keys.publicKey else {
throw GRDBStorageError.migrationFailed
}
id = publicKey.toHexString()
variant = .closedGroup
}
notificationMode = (thread.isMuted ? .none :
(groupThread.isOnlyNotifyingForMentions ?
.mentionsOnly :
.all
)
)
default:
id = legacyThreadId.substring(from: Legacy.contactThreadPrefix.count)
variant = .contact
notificationMode = (thread.isMuted ? .none : .all)
}
try SessionThread(
id: id,
variant: variant,
creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
shouldBeVisible: thread.shouldBeVisible,
isPinned: thread.isPinned,
messageDraft: thread.messageDraft,
notificationMode: notificationMode,
mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
).insert(db)
// Disappearing Messages Configuration
if let config: Legacy.DisappearingMessagesConfiguration = disappearingMessagesConfiguration[id] {
try DisappearingMessagesConfiguration(
id: id,
isEnabled: config.isEnabled,
durationSeconds: TimeInterval(config.durationSeconds)
).insert(db)
}
// Closed Groups
if (thread as? TSGroupThread)?.isClosedGroup == true {
guard
let keyInfo = closedGroupKeys[legacyThreadId],
let name: String = closedGroupName[legacyThreadId],
let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
else { throw GRDBStorageError.migrationFailed }
try ClosedGroup(
publicKey: keyInfo.keys.publicKey.toHexString(),
name: name,
formationTimestamp: TimeInterval(formationTimestamp)
).insert(db)
try ClosedGroupKeyPair(
publicKey: keyInfo.keys.publicKey.toHexString(),
secretKey: keyInfo.keys.privateKey,
receivedTimestamp: keyInfo.timestamp
).insert(db)
try groupModel.groupMemberIds.forEach { memberId in
try GroupMember(
groupId: id,
profileId: memberId,
role: .standard
).insert(db)
}
try groupModel.groupAdminIds.forEach { adminId in
try GroupMember(
groupId: id,
profileId: adminId,
role: .admin
).insert(db)
}
try (closedGroupZombieMemberIds[legacyThreadId] ?? []).forEach { zombieId in
try GroupMember(
groupId: id,
profileId: zombieId,
role: .zombie
).insert(db)
}
}
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct Capability: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "capability" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case server
case room
case capability
case isMissing
}
public let server: String
public let room: String
public let capability: String
public let isMissing: Bool
}

View File

@ -0,0 +1,48 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "closedGroup" }
static let keyPairs = hasMany(ClosedGroupKeyPair.self)
static let members = hasMany(GroupMember.self)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case publicKey
case name
case formationTimestamp
}
public var id: String { publicKey }
public let publicKey: String
public let name: String
public let formationTimestamp: TimeInterval
public var keyPairs: QueryInterfaceRequest<ClosedGroupKeyPair> {
request(for: ClosedGroup.keyPairs)
}
public var memberIds: QueryInterfaceRequest<GroupMember> {
request(for: ClosedGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
}
public var zombieIds: QueryInterfaceRequest<GroupMember> {
request(for: ClosedGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.zombie)
}
public var moderatorIds: QueryInterfaceRequest<GroupMember> {
request(for: ClosedGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.moderator)
}
public var adminIds: QueryInterfaceRequest<GroupMember> {
request(for: ClosedGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
}
}

View File

@ -0,0 +1,22 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct ClosedGroupKeyPair: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "closedGroupKeyPair" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case publicKey
case secretKey
case receivedTimestamp
}
public var id: String { publicKey }
public let publicKey: String
public let secretKey: Data
public let receivedTimestamp: TimeInterval
}

View File

@ -4,7 +4,7 @@ import Foundation
import GRDB
import SessionUtilitiesKit
public struct Contact: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public struct Contact: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "contact" }
public typealias Columns = CodingKeys
@ -22,23 +22,129 @@ public struct Contact: Codable, FetchableRecord, PersistableRecord, TableRecord,
public let id: String
/// This flag is used to determine whether we should auto-download files sent by this contact.
public var isTrusted = false
public let isTrusted: Bool
/// This flag is used to determine whether message requests from this contact are approved
public var isApproved = false
public let isApproved: Bool
/// This flag is used to determine whether message requests from this contact are blocked
public var isBlocked = false {
didSet {
if isBlocked {
hasBeenBlocked = true
public let isBlocked: Bool
/// This flag is used to determine whether this contact has approved the current users message request
public let didApproveMe: Bool
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
public let hasBeenBlocked: Bool
// MARK: - Initialization
public init(
id: String,
isTrusted: Bool = false,
isApproved: Bool = false,
isBlocked: Bool = false,
didApproveMe: Bool = false,
hasBeenBlocked: Bool = false
) {
self.id = id
self.isTrusted = (
isTrusted ||
id == getUserHexEncodedPublicKey() // Always trust ourselves
)
self.isApproved = isApproved
self.isBlocked = isBlocked
self.didApproveMe = didApproveMe
self.hasBeenBlocked = (isBlocked || hasBeenBlocked)
}
// MARK: - PersistableRecord
public func save(_ db: Database) throws {
let oldContact: Contact? = try? Contact.fetchOne(db, id: id)
try performSave(db)
db.afterNextTransactionCommit { db in
if isBlocked != oldContact?.isBlocked {
NotificationCenter.default.post(name: .contactBlockedStateChanged, object: id)
}
}
}
/// This flag is used to determine whether this contact has approved the current users message request
public var didApproveMe = false
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
public var hasBeenBlocked = false
}
// MARK: - Convenience
public extension Contact {
func with(
isTrusted: Updatable<Bool> = .existing,
isApproved: Updatable<Bool> = .existing,
isBlocked: Updatable<Bool> = .existing,
didApproveMe: Updatable<Bool> = .existing
) -> Contact {
return Contact(
id: id,
isTrusted: (
(isTrusted ?? self.isTrusted) ||
self.id == getUserHexEncodedPublicKey() // Always trust ourselves
),
isApproved: (isApproved ?? self.isApproved),
isBlocked: (isBlocked ?? self.isBlocked),
didApproveMe: (didApproveMe ?? self.didApproveMe),
hasBeenBlocked: ((isBlocked ?? self.isBlocked) || self.hasBeenBlocked)
)
}
}
// MARK: - GRDB Interactions
public extension Contact {
static func fetchOrCreate(_ db: Database, id: ID) -> Contact {
return ((try? fetchOne(db, id: id)) ?? Contact(id: id))
}
static func fetchAllIds() -> [String] {
return GRDBStorage.shared
.read { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let contacts: [Contact] = try Contact
.filter(Contact.Columns.id != userPublicKey)
.filter(Contact.Columns.didApproveMe == true)
.fetchAll(db)
let profiles: [Profile] = try Profile
.fetchAll(db, ids: contacts.map { $0.id })
// Sort the contacts by their displayName value
return profiles
.sorted(by: { lhs, rhs -> Bool in lhs.displayName() < rhs.displayName() })
.map { $0.id }
}
.defaulting(to: [])
}
}
// MARK: - Objective-C Support
@objc(SMKContact)
public class SMKContact: NSObject {
@objc let isApproved: Bool
@objc let isBlocked: Bool
@objc let didApproveMe: Bool
init(isApproved: Bool, isBlocked: Bool, didApproveMe: Bool) {
self.isApproved = isApproved
self.isBlocked = isBlocked
self.didApproveMe = didApproveMe
}
@objc public static func fetchOrCreate(id: String) -> SMKContact {
let existingContact: Contact? = GRDBStorage.shared.read { db in
try Contact.fetchOne(db, id: id)
}
return SMKContact(
isApproved: existingContact?.isApproved ?? false,
isBlocked: existingContact?.isBlocked ?? false,
didApproveMe: existingContact?.didApproveMe ?? false
)
}
}

View File

@ -0,0 +1,64 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct DisappearingMessagesConfiguration: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "disappearingMessagesConfiguration" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case id
case isEnabled
case durationSeconds
}
public let id: String
public let isEnabled: Bool
public let durationSeconds: TimeInterval
}
// MARK: - Convenience
extension DisappearingMessagesConfiguration {
public var durationIndex: Int {
return DisappearingMessagesConfiguration.validDurationsSeconds
.firstIndex(of: durationSeconds)
.defaulting(to: 0)
}
public var durationString: String {
NSString.formatDurationSeconds(UInt32(durationSeconds), useShortFormat: false)
}
}
// MARK: - UI Constraints
extension DisappearingMessagesConfiguration {
public static var validDurationsSeconds: [TimeInterval] {
return [
5,
10,
30,
(1 * 60),
(5 * 60),
(30 * 60),
(1 * 60 * 60),
(6 * 60 * 60),
(12 * 60 * 60),
(24 * 60 * 60),
(7 * 24 * 60 * 60)
]
}
public static var maxDurationSeconds: TimeInterval = {
return (validDurationsSeconds.max() ?? 0)
}()
}
// MARK: - Objective-C Support
@objc(SMKDisappearingMessagesConfiguration)
public class SMKDisappearingMessagesConfiguration: NSObject {
@objc public static var maxDurationSeconds: UInt = UInt(DisappearingMessagesConfiguration.maxDurationSeconds)
}

View File

@ -0,0 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct GroupMember: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "groupMember" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case groupId
case profileId
case role
}
public enum Role: Int, Codable, DatabaseValueConvertible {
case standard
case zombie
case moderator
case admin
}
public let groupId: String
public let profileId: String
public let role: Role
}

View File

@ -0,0 +1,50 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "openGroup" }
static let capabilities = hasMany(Capability.self)
static let members = hasMany(GroupMember.self)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case server
case room
case publicKey
case name
case groupDescription = "description"
case imageId
case imageData
case userCount
case infoUpdates
}
public var id: String { "\(server).\(room)" }
public let server: String
public let room: String
public let publicKey: String
public let name: String
public let groupDescription: String?
public let imageId: Int?
public let imageData: Data?
public let userCount: Int
public let infoUpdates: Int
public var capabilities: QueryInterfaceRequest<Capability> {
request(for: OpenGroup.capabilities)
}
public var moderatorIds: QueryInterfaceRequest<GroupMember> {
request(for: OpenGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.moderator)
}
public var adminIds: QueryInterfaceRequest<GroupMember> {
request(for: OpenGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
}
}

View File

@ -2,9 +2,10 @@
import Foundation
import GRDB
import SignalCoreKit
import SessionUtilitiesKit
public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
public static var databaseTableName: String { "profile" }
public typealias Columns = CodingKeys
@ -23,19 +24,19 @@ public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord,
public let id: String
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
public var name: String
public let name: String
/// A custom name for the profile set by the current user
public var nickname: String?
public let nickname: String?
/// The URL from which to fetch the contact's profile picture.
public var profilePictureUrl: String?
public let profilePictureUrl: String?
/// The file name of the contact's profile picture on local storage.
public var profilePictureFileName: String?
public let profilePictureFileName: String?
/// The key with which the profile is encrypted.
public var profileEncryptionKey: OWSAES256Key?
public let profileEncryptionKey: OWSAES256Key?
// MARK: - Description
@ -48,6 +49,33 @@ public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord,
)
"""
}
// MARK: - PersistableRecord
public func save(_ db: Database) throws {
let oldProfile: Profile? = try? Profile.fetchOne(db, id: id)
try performSave(db)
db.afterNextTransactionCommit { db in
// Delete old profile picture if needed
if let oldProfilePictureFileName: String = oldProfile?.profilePictureFileName, oldProfilePictureFileName != profilePictureFileName {
let path: String = OWSUserProfile.profileAvatarFilepath(withFilename: oldProfilePictureFileName)
DispatchQueue.global(qos: .default).async {
OWSFileSystem.deleteFileIfExists(path)
}
}
NotificationCenter.default.post(name: .profileUpdated, object: id)
if id == getUserHexEncodedPublicKey(db) {
NotificationCenter.default.post(name: .localProfileDidChange, object: nil)
}
else {
let userInfo = [ Notification.Key.profileRecipientId.rawValue: id ]
NotificationCenter.default.post(name: .otherUsersProfileDidChange, object: nil, userInfo: userInfo)
}
}
}
}
// MARK: - Codable
@ -149,15 +177,32 @@ public extension Profile {
// MARK: - Convenience
public extension Profile {
func with(
name: String? = nil,
nickname: Updatable<String> = .existing,
profilePictureUrl: Updatable<String> = .existing,
profilePictureFileName: Updatable<String> = .existing,
profileEncryptionKey: Updatable<OWSAES256Key> = .existing
) -> Profile {
return Profile(
id: id,
name: (name ?? self.name),
nickname: (nickname ?? self.nickname),
profilePictureUrl: (profilePictureUrl ?? self.profilePictureUrl),
profilePictureFileName: (profilePictureFileName ?? self.profilePictureFileName),
profileEncryptionKey: (profileEncryptionKey ?? self.profileEncryptionKey)
)
}
// MARK: - Context
enum Context: Int {
@objc enum Context: Int {
case regular
case openGroup
}
/// The name to display in the UI. For local use only.
func displayName(for context: Context) -> String? {
func displayName(for context: Context = .regular) -> String {
if let nickname: String = nickname { return nickname }
switch context {
@ -172,3 +217,150 @@ public extension Profile {
}
}
}
// MARK: - GRDB Interactions
public extension Profile {
static func displayName(for id: ID, thread: TSThread, customFallback: String? = nil) -> String {
return displayName(
for: id,
context: ((thread as? TSGroupThread)?.isOpenGroup == true ? .openGroup : .regular),
customFallback: customFallback
)
}
static func displayName(for id: ID, context: Context = .regular, customFallback: String? = nil) -> String {
let existingDisplayName: String? = GRDBStorage.shared
.read { db in try Profile.fetchOne(db, id: id) }?
.displayName(for: context)
return (existingDisplayName ?? (customFallback ?? id))
}
static func displayNameNoFallback(for id: ID, thread: TSThread) -> String? {
return displayName(
for: id,
context: ((thread as? TSGroupThread)?.isOpenGroup == true ? .openGroup : .regular)
)
}
static func displayNameNoFallback(for id: ID, context: Context = .regular) -> String? {
return GRDBStorage.shared
.read { db in try Profile.fetchOne(db, id: id) }?
.displayName(for: context)
}
// MARK: - Fetch or Create
private static func defaultFor(_ id: String) -> Profile {
return Profile(
id: id,
name: id,
nickname: nil,
profilePictureUrl: nil,
profilePictureFileName: nil,
profileEncryptionKey: nil
)
}
static func fetchOrCreateCurrentUser() -> Profile {
var userPublicKey: String = ""
let exisingProfile: Profile? = GRDBStorage.shared.read { db in
userPublicKey = getUserHexEncodedPublicKey(db)
return try Profile.fetchOne(db, id: userPublicKey)
}
return (exisingProfile ?? defaultFor(userPublicKey))
}
static func fetchOrCreateCurrentUser(_ db: Database) -> Profile {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
return (
(try? Profile.fetchOne(db, id: userPublicKey)) ??
defaultFor(userPublicKey)
)
}
static func fetchOrCreate(id: String) -> Profile {
let exisingProfile: Profile? = GRDBStorage.shared.read { db in
try Profile.fetchOne(db, id: id)
}
return (exisingProfile ?? defaultFor(id))
}
static func fetchOrCreate(_ db: Database, id: String) -> Profile {
return (
(try? Profile.fetchOne(db, id: id)) ??
defaultFor(id)
)
}
}
// MARK: - Objective-C Support
@objc(SMKProfile)
public class SMKProfile: NSObject {
var id: String
@objc var name: String
@objc var nickname: String?
init(id: String, name: String, nickname: String?) {
self.id = id
self.name = name
self.nickname = nickname
}
@objc public static func fetchCurrentUserName() -> String {
let existingProfile: Profile? = GRDBStorage.shared.read { db in
Profile.fetchOrCreateCurrentUser(db)
}
return (existingProfile?.name ?? "")
}
@objc public static func fetchOrCreate(id: String) -> SMKProfile {
let profile: Profile = Profile.fetchOrCreate(id: id)
return SMKProfile(
id: id,
name: profile.name,
nickname: profile.nickname
)
}
@objc public static func saveProfile(_ profile: SMKProfile) {
GRDBStorage.shared.write { db in
try? Profile
.fetchOrCreate(db, id: profile.id)
.with(nickname: .updateTo(profile.nickname))
.save(db)
}
}
@objc public static func displayName(id: String) -> String {
return Profile.displayName(for: id)
}
@objc public static func displayName(id: String, customFallback: String) -> String {
return Profile.displayName(for: id, customFallback: customFallback)
}
@objc public static func displayName(id: String, context: Profile.Context = .regular) -> String {
let existingProfile: Profile? = GRDBStorage.shared.read { db in
Profile.fetchOrCreateCurrentUser(db)
}
return (existingProfile?.name ?? id)
}
@objc public static func displayName(id: String, thread: TSThread) -> String {
return Profile.displayName(for: id, thread: thread)
}
@objc public static var localProfileKey: OWSAES256Key? {
Profile.fetchOrCreateCurrentUser().profileEncryptionKey
}
}

View File

@ -0,0 +1,59 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct SessionThread: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "thread" }
static let disappearingMessagesConfiguration = hasOne(DisappearingMessagesConfiguration.self)
static let closedGroup = hasOne(ClosedGroup.self)
static let openGroup = hasOne(OpenGroup.self)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case id
case variant
case creationDateTimestamp
case shouldBeVisible
case isPinned
case messageDraft
case notificationMode
case mutedUntilTimestamp
}
public enum Variant: Int, Codable, DatabaseValueConvertible {
case contact
case closedGroup
case openGroup
}
public enum NotificationMode: Int, Codable, DatabaseValueConvertible {
case all
case none
case mentionsOnly // Only applicable to group threads
}
public let id: String
public let variant: Variant
public let creationDateTimestamp: TimeInterval
public let shouldBeVisible: Bool
public let isPinned: Bool
public let messageDraft: String?
public let notificationMode: NotificationMode
public let mutedUntilTimestamp: TimeInterval?
public var disappearingMessagesConfiguration: QueryInterfaceRequest<DisappearingMessagesConfiguration> {
request(for: SessionThread.disappearingMessagesConfiguration)
}
// public var lastInteraction
public var closedGroup: QueryInterfaceRequest<ClosedGroup> {
request(for: SessionThread.closedGroup)
}
public var openGroup: QueryInterfaceRequest<OpenGroup> {
request(for: SessionThread.openGroup)
}
}

View File

@ -1,12 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import SessionUtilitiesKit
public extension Notification.Name {
static let contactUpdated = Notification.Name("contactUpdated")
static let profileUpdated = Notification.Name("profileUpdated")
static let localProfileDidChange = Notification.Name("localProfileDidChange")
static let otherUsersProfileDidChange = Notification.Name("otherUsersProfileDidChange")
static let contactBlockedStateChanged = Notification.Name("contactBlockedStateChanged")
}
@objc public extension NSNotification {
@objc static let contactUpdated = Notification.Name.contactUpdated.rawValue as NSString
@objc static let profileUpdated = Notification.Name.profileUpdated.rawValue as NSString
@objc static let localProfileDidChange = Notification.Name.localProfileDidChange.rawValue as NSString
@objc static let otherUsersProfileDidChange = Notification.Name.otherUsersProfileDidChange.rawValue as NSString
@objc static let contactBlockedStateChanged = Notification.Name.contactBlockedStateChanged.rawValue as NSString
}
extension Notification.Key {
static let profileRecipientId = Notification.Key("profileRecipientId")
}
@objc public extension NSNotification {
static let profileRecipientIdKey = Notification.Key.profileRecipientId.rawValue as NSString
}

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Curve25519Kit
extension Storage {
@ -13,12 +14,18 @@ extension Storage {
private static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
private static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [ECKeyPair] {
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [Box.KeyPair] {
var result: [ECKeyPair] = []
Storage.read { transaction in
result = self.getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction)
}
return result
.map { keyPair -> Box.KeyPair in
Box.KeyPair(
publicKey: keyPair.publicKey.bytes,
secretKey: keyPair.privateKey.bytes
)
}
}
public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> [ECKeyPair] {
@ -31,7 +38,7 @@ extension Storage {
return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair }
}
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> ECKeyPair? {
public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> Box.KeyPair? {
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey).last
}
@ -39,10 +46,14 @@ extension Storage {
return getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction).last
}
public func addClosedGroupEncryptionKeyPair(_ keyPair: ECKeyPair, for groupPublicKey: String, using transaction: Any) {
public func addClosedGroupEncryptionKeyPair(_ keyPair: Box.KeyPair, for groupPublicKey: String, using transaction: Any) {
let ecKeyPair: ECKeyPair = try! ECKeyPair(
publicKeyData: Data(keyPair.publicKey),
privateKeyData: Data(keyPair.secretKey)
)
let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey)
let timestamp = String(Date().timeIntervalSince1970)
(transaction as! YapDatabaseReadWriteTransaction).setObject(keyPair, forKey: timestamp, inCollection: collection)
(transaction as! YapDatabaseReadWriteTransaction).setObject(ecKeyPair, forKey: timestamp, inCollection: collection)
}
public func removeAllClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: Any) {

View File

@ -1,77 +0,0 @@
extension Storage {
private static let contactCollection = "LokiContactCollection"
@objc(getContactWithSessionID:)
public func getContact(with sessionID: String) -> Contact? {
var result: Contact?
Storage.read { transaction in
result = self.getContact(with: sessionID, using: transaction)
}
return result
}
@objc(getContactWithSessionID:using:)
public func getContact(with sessionID: String, using transaction: Any) -> Contact? {
var result: Contact?
let transaction = transaction as! YapDatabaseReadTransaction
result = transaction.object(forKey: sessionID, inCollection: Storage.contactCollection) as? Contact
if let result = result, result.sessionID == getUserHexEncodedPublicKey() {
result.isTrusted = true // Always trust ourselves
}
return result
}
@objc(setContact:usingTransaction:)
public func setContact(_ contact: Contact, using transaction: Any) {
let transaction = transaction as! YapDatabaseReadWriteTransaction
let oldContact = getContact(with: contact.sessionID, using: transaction)
if contact.sessionID == getUserHexEncodedPublicKey() {
contact.isTrusted = true // Always trust ourselves
}
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Storage.contactCollection)
transaction.addCompletionQueue(DispatchQueue.main) {
// Delete old profile picture if needed
if let oldProfilePictureFileName = oldContact?.profilePictureFileName,
oldProfilePictureFileName != contact.profilePictureFileName {
let path = OWSUserProfile.profileAvatarFilepath(withFilename: oldProfilePictureFileName)
DispatchQueue.global(qos: .default).async {
OWSFileSystem.deleteFileIfExists(path)
}
}
// Post notification
let notificationCenter = NotificationCenter.default
notificationCenter.post(name: .contactUpdated, object: contact.sessionID)
if contact.sessionID == getUserHexEncodedPublicKey() {
notificationCenter.post(name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
}
else {
let userInfo = [ kNSNotificationKey_ProfileRecipientId : contact.sessionID ]
notificationCenter.post(name: Notification.Name(kNSNotificationName_OtherUsersProfileDidChange), object: nil, userInfo: userInfo)
}
if contact.isBlocked != oldContact?.isBlocked {
notificationCenter.post(name: .contactBlockedStateChanged, object: contact.sessionID)
}
}
}
@objc public func getAllContacts() -> Set<Contact> {
var result: Set<Contact> = []
Storage.read { transaction in
result = self.getAllContacts(with: transaction)
}
return result
}
@objc public func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact> {
var result: Set<Contact> = []
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in
guard let contact = object as? Contact else { return }
result.insert(contact)
}
return result
}
}

View File

@ -1,3 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Sodium
@ -16,23 +19,22 @@ extension Storage {
public func writeSync(with block: @escaping (Any) -> Void) {
Storage.writeSync { block($0) }
}
@objc public func getUser() -> Contact? {
return getUser(using: nil)
}
public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? {
let userPublicKey = getUserHexEncodedPublicKey()
var result: Contact?
if let transaction = transaction {
result = Storage.shared.getContact(with: userPublicKey, using: transaction)
}
else {
Storage.read { transaction in
result = Storage.shared.getContact(with: userPublicKey, using: transaction)
}
}
return result
}
// @objc public func getUser() -> Legacy.Contact? {
// return getUser(using: nil)
// }
//
// public func getUser(using transaction: YapDatabaseReadTransaction?) -> Legacy.Contact? {
// let userPublicKey = getUserHexEncodedPublicKey()
// var result: Legacy.Contact?
//
// if let transaction = transaction {
// result = Storage.shared.getContact(with: userPublicKey, using: transaction)
// }
// else {
// Storage.read { transaction in
// result = Storage.shared.getContact(with: userPublicKey, using: transaction)
// }
// }
// return result
// }
}

View File

@ -318,7 +318,7 @@ NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"
if (!thread.isGroupThread) {
TSContactThread *contactThead = (TSContactThread *)thread;
SNContact *contact = [LKStorage.shared getContactWithSessionID:[contactThead contactSessionID]];
SMKContact *contact = [SMKContact fetchOrCreateWithId:[contactThead contactSessionID]];
if (contact == nil || !contact.didApproveMe) {
return nil;

View File

@ -1,4 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility

View File

@ -62,25 +62,37 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC
JobQueue.currentlyExecutingJobs.insert(id)
}
let (promise, seal) = Promise<Void>.pending()
SNMessagingKitConfiguration.shared.storage.write(with: { transaction in // Intentionally capture self
do {
let isRetry = (self.failureCount != 0)
let (message, proto) = try MessageReceiver.parse(self.data, openGroupMessageServerID: self.openGroupMessageServerID, isRetry: isRetry, using: transaction)
message.serverHash = self.serverHash
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: self.openGroupID, isBackgroundPoll: self.isBackgroundPoll, using: transaction)
self.handleSuccess()
seal.fulfill(())
} catch {
if let error = error as? MessageReceiver.Error, !error.isRetryable {
SNLog("Message receive job permanently failed due to error: \(error).")
self.handlePermanentFailure(error: error)
} else {
SNLog("Couldn't receive message due to error: \(error).")
self.handleFailure(error: error)
GRDBStorage.shared.writeAsync(
updates: { db in
SNMessagingKitConfiguration.shared.storage.write(with: { transaction in // Intentionally capture self
do {
let isRetry = (self.failureCount != 0)
let (message, proto) = try MessageReceiver.parse(db, self.data, openGroupMessageServerID: self.openGroupMessageServerID, isRetry: isRetry, using: transaction)
message.serverHash = self.serverHash
try MessageReceiver.handle(db, message, associatedWithProto: proto, openGroupID: self.openGroupID, isBackgroundPoll: self.isBackgroundPoll, using: transaction)
self.handleSuccess()
seal.fulfill(())
} catch {
if let error = error as? MessageReceiver.Error, !error.isRetryable {
SNLog("Message receive job permanently failed due to error: \(error).")
self.handlePermanentFailure(error: error)
} else {
SNLog("Couldn't receive message due to error: \(error).")
self.handleFailure(error: error)
}
seal.fulfill(()) // The promise is just used to keep track of when we're done
}
}, completion: { })
},
completion: { _, result in
switch result {
case .failure(let error): self.handleFailure(error: error)
default: break
}
seal.fulfill(()) // The promise is just used to keep track of when we're done
}
}, completion: { })
)
return promise
}

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Curve25519Kit
import SessionUtilitiesKit
@ -18,7 +19,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
// MARK: Kind
public enum Kind : CustomStringConvertible {
case new(publicKey: Data, name: String, encryptionKeyPair: ECKeyPair, members: [Data], admins: [Data], expirationTimer: UInt32)
case new(publicKey: Data, name: String, encryptionKeyPair: Box.KeyPair, members: [Data], admins: [Data], expirationTimer: UInt32)
/// An encryption key pair encrypted for each member individually.
///
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
@ -95,7 +96,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
switch kind {
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, let expirationTimer):
return !publicKey.isEmpty && !name.isEmpty && !encryptionKeyPair.publicKey.isEmpty
&& !encryptionKeyPair.privateKey.isEmpty && !members.isEmpty && !admins.isEmpty
&& !encryptionKeyPair.secretKey.isEmpty && !members.isEmpty && !admins.isEmpty
case .encryptionKeyPair: return true
case .nameChange(let name): return !name.isEmpty
case .membersAdded(let members): return !members.isEmpty
@ -113,11 +114,15 @@ public final class ClosedGroupControlMessage : ControlMessage {
case "new":
guard let publicKey = coder.decodeObject(forKey: "publicKey") as? Data,
let name = coder.decodeObject(forKey: "name") as? String,
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as? ECKeyPair,
let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as? SessionUtilitiesKit.Legacy.KeyPair,
let members = coder.decodeObject(forKey: "members") as? [Data],
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
let expirationTimer = coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0
self.kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair, members: members, admins: admins, expirationTimer: expirationTimer)
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
self.kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: keyPair, members: members, admins: admins, expirationTimer: expirationTimer)
case "encryptionKeyPair":
let publicKey = coder.decodeObject(forKey: "publicKey") as? Data
guard let wrappers = coder.decodeObject(forKey: "wrappers") as? [KeyPairWrapper] else { return nil }
@ -172,7 +177,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> ClosedGroupControlMessage? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ClosedGroupControlMessage? {
guard let closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage else { return nil }
let kind: Kind
switch closedGroupControlMessageProto.type {
@ -180,14 +185,9 @@ public final class ClosedGroupControlMessage : ControlMessage {
guard let publicKey = closedGroupControlMessageProto.publicKey, let name = closedGroupControlMessageProto.name,
let encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair else { return nil }
let expirationTimer = closedGroupControlMessageProto.expirationTimer
do {
let encryptionKeyPair = try ECKeyPair(publicKeyData: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded(), privateKeyData: encryptionKeyPairAsProto.privateKey)
kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair,
members: closedGroupControlMessageProto.members, admins: closedGroupControlMessageProto.admins, expirationTimer: expirationTimer)
} catch {
SNLog("Couldn't parse key pair.")
return nil
}
let encryptionKeyPair = Box.KeyPair(publicKey: encryptionKeyPairAsProto.publicKey.removing05PrefixIfNeeded().bytes, secretKey: encryptionKeyPairAsProto.privateKey.bytes)
kind = .new(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair,
members: closedGroupControlMessageProto.members, admins: closedGroupControlMessageProto.admins, expirationTimer: expirationTimer)
case .encryptionKeyPair:
let publicKey = closedGroupControlMessageProto.publicKey
let wrappers = closedGroupControlMessageProto.wrappers.compactMap { KeyPairWrapper.fromProto($0) }
@ -219,7 +219,7 @@ public final class ClosedGroupControlMessage : ControlMessage {
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .new)
closedGroupControlMessage.setPublicKey(publicKey)
closedGroupControlMessage.setName(name)
let encryptionKeyPairAsProto = SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey)
let encryptionKeyPairAsProto = SNProtoKeyPair.builder(publicKey: Data(encryptionKeyPair.publicKey), privateKey: Data(encryptionKeyPair.secretKey))
do {
closedGroupControlMessage.setEncryptionKeyPair(try encryptionKeyPairAsProto.build())
} catch {

View File

@ -1,88 +1,73 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
extension ConfigurationMessage {
public static func getCurrent(with transaction: YapDatabaseReadTransaction) -> ConfigurationMessage? {
let storage = Storage.shared
guard let user = storage.getUser(using: transaction) else { return nil }
public static func getCurrent(_ db: Database) throws -> ConfigurationMessage? {
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
let displayName = user.name
let profilePictureURL = user.profilePictureURL
let profileKey = user.profileEncryptionKey?.keyData
let displayName: String = profile.name
let profilePictureUrl: String? = profile.profilePictureUrl
let profileKey: Data? = profile.profileEncryptionKey?.keyData
var closedGroups: Set<ClosedGroup> = []
var openGroups: Set<String> = []
var contacts: Set<ConfigurationMessage.Contact> = []
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSGroupThread else { return }
switch thread.groupModel.groupType {
case .closedGroup:
guard thread.isCurrentUserMemberInGroup() else { return }
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
guard
storage.isClosedGroup(groupPublicKey, using: transaction),
let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction)
else {
return
}
let closedGroup = ClosedGroup(
publicKey: groupPublicKey,
name: (thread.groupModel.groupName ?? ""),
encryptionKeyPair: encryptionKeyPair,
members: Set(thread.groupModel.groupMemberIds),
admins: Set(thread.groupModel.groupAdminIds),
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
)
closedGroups.insert(closedGroup)
case .openGroup:
if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
}
Storage.read { transaction in
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard let thread = object as? TSGroupThread else { return }
switch thread.groupModel.groupType {
case .closedGroup:
guard thread.isCurrentUserMemberInGroup() else { return }
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
guard
Storage.shared.isClosedGroup(groupPublicKey, using: transaction),
let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction)
else {
return
}
let closedGroup = ClosedGroup(
publicKey: groupPublicKey,
name: (thread.groupModel.groupName ?? ""),
encryptionKeyPair: encryptionKeyPair,
members: Set(thread.groupModel.groupMemberIds),
admins: Set(thread.groupModel.groupAdminIds),
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
)
closedGroups.insert(closedGroup)
case .openGroup:
if let threadId: String = thread.uniqueId, let v2OpenGroup = Storage.shared.getV2OpenGroup(for: threadId) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
}
default: break
default: break
}
}
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
contacts = storage.getAllContacts(with: transaction)
.compactMap { contact -> ConfigurationMessage.Contact? in
let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
guard
// Skip the current user
contact.sessionID != currentUserPublicKey &&
// Contacts which have visible threads
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && (
// Include already approved contacts
contact.isApproved ||
contact.didApproveMe ||
// Sync blocked contacts
contact.isBlocked ||
contact.hasBeenBlocked
)
else {
return nil
}
let contacts: Set<CMContact> = try Contact.fetchAll(db)
.compactMap { contact -> CMContact? in
guard contact.id != currentUserPublicKey else { return nil }
// Can just default the 'hasX' values to true as they will be set to this
// when converting to proto anyway
let profilePictureURL = contact.profilePictureURL
let profileKey = contact.profileEncryptionKey?.keyData
let profile: Profile? = try? Profile.fetchOne(db, id: contact.id)
return ConfigurationMessage.Contact(
publicKey: contact.sessionID,
displayName: (contact.name ?? contact.sessionID),
profilePictureURL: profilePictureURL,
profileKey: profileKey,
return CMContact(
publicKey: contact.id,
displayName: (profile?.name ?? contact.id),
profilePictureURL: profile?.profilePictureUrl,
profileKey: profile?.profileEncryptionKey?.keyData,
hasIsApproved: true,
isApproved: contact.isApproved,
hasIsBlocked: true,
@ -95,7 +80,7 @@ extension ConfigurationMessage {
return ConfigurationMessage(
displayName: displayName,
profilePictureURL: profilePictureURL,
profilePictureURL: profilePictureUrl,
profileKey: profileKey,
closedGroups: closedGroups,
openGroups: openGroups,

View File

@ -11,14 +11,14 @@ public final class ConfigurationMessage : ControlMessage {
public var displayName: String?
public var profilePictureURL: String?
public var profileKey: Data?
public var contacts: Set<Contact> = []
public var contacts: Set<CMContact> = []
public override var isSelfSendValid: Bool { true }
// MARK: Initialization
public override init() { super.init() }
public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<Contact>) {
public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<CMContact>) {
super.init()
self.displayName = displayName
self.profilePictureURL = profilePictureURL
@ -36,7 +36,7 @@ public final class ConfigurationMessage : ControlMessage {
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
if let contacts = coder.decodeObject(forKey: "contacts") as! Set<Contact>? { self.contacts = contacts }
if let contacts = coder.decodeObject(forKey: "contacts") as! Set<CMContact>? { self.contacts = contacts }
}
public override func encode(with coder: NSCoder) {
@ -50,14 +50,14 @@ public final class ConfigurationMessage : ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> ConfigurationMessage? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ConfigurationMessage? {
guard let configurationProto = proto.configurationMessage else { return nil }
let displayName = configurationProto.displayName
let profilePictureURL = configurationProto.profilePicture
let profileKey = configurationProto.profileKey
let closedGroups = Set(configurationProto.closedGroups.compactMap { ClosedGroup.fromProto($0) })
let openGroups = Set(configurationProto.openGroups)
let contacts = Set(configurationProto.contacts.compactMap { Contact.fromProto($0) })
let contacts = Set(configurationProto.contacts.compactMap { CMContact.fromProto($0) })
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
}
@ -89,7 +89,7 @@ public final class ConfigurationMessage : ControlMessage {
displayName: \(displayName ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null"),
profileKey: \(profileKey?.toHexString() ?? "null"),
contacts: \([Contact](contacts).prettifiedDescription)
contacts: \([CMContact](contacts).prettifiedDescription)
)
"""
}
@ -192,7 +192,7 @@ extension ConfigurationMessage {
extension ConfigurationMessage {
@objc(SNConfigurationMessageContact)
public final class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public final class CMContact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public var publicKey: String?
public var displayName: String?
public var profilePictureURL: String?
@ -259,8 +259,8 @@ extension ConfigurationMessage {
coder.encode(didApproveMe, forKey: "didApproveMe")
}
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> Contact? {
let result: Contact = Contact(
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> CMContact? {
let result: CMContact = CMContact(
publicKey: proto.publicKey.toHexString(),
displayName: proto.name,
profilePictureURL: proto.profilePicture,

View File

@ -60,7 +60,7 @@ public final class DataExtractionNotification : ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> DataExtractionNotification? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> DataExtractionNotification? {
guard let dataExtractionNotification = proto.dataExtractionNotification else { return nil }
let kind: Kind
switch dataExtractionNotification.type {

View File

@ -39,7 +39,7 @@ public final class ExpirationTimerUpdate : ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> ExpirationTimerUpdate? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ExpirationTimerUpdate? {
guard let dataMessageProto = proto.dataMessage else { return nil }
let isExpirationTimerUpdate = (dataMessageProto.flags & UInt32(SNProtoDataMessage.SNProtoDataMessageFlags.expirationTimerUpdate.rawValue)) != 0
guard isExpirationTimerUpdate else { return nil }

View File

@ -30,7 +30,7 @@ public final class MessageRequestResponse: ControlMessage {
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> MessageRequestResponse? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> MessageRequestResponse? {
guard let messageRequestResponseProto = proto.messageRequestResponse else { return nil }
let isApproved = messageRequestResponseProto.isApproved

View File

@ -31,7 +31,7 @@ public final class ReadReceipt : ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> ReadReceipt? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ReadReceipt? {
guard let receiptProto = proto.receiptMessage, receiptProto.type == .read else { return nil }
let timestamps = receiptProto.timestamp
guard !timestamps.isEmpty else { return nil }

View File

@ -58,7 +58,7 @@ public final class TypingIndicator : ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> TypingIndicator? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> TypingIndicator? {
guard let typingIndicatorProto = proto.typingMessage else { return nil }
let kind = Kind.fromProto(typingIndicatorProto.action)
return TypingIndicator(kind: kind)

View File

@ -36,7 +36,7 @@ public final class UnsendRequest: ControlMessage {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> UnsendRequest? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> UnsendRequest? {
guard let unsendRequestProto = proto.unsendRequest else { return nil }
let timestamp = unsendRequestProto.timestamp
let author = unsendRequestProto.author

View File

@ -53,8 +53,8 @@ public class Message : NSObject, NSCoding { // NSObject/NSCoding conformance is
}
// MARK: Proto Conversion
public class func fromProto(_ proto: SNProtoContent) -> Self? {
preconditionFailure("fromProto(_:) is abstract and must be overridden.")
public class func fromProto(_ proto: SNProtoContent, sender: String) -> Self? {
preconditionFailure("fromProto(_:sender:) is abstract and must be overridden.")
}
public func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {

View File

@ -144,7 +144,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
- (void)setExpiresInSeconds:(uint32_t)expiresInSeconds
{
uint32_t maxExpirationDuration = [OWSDisappearingMessagesConfiguration maxDurationSeconds];
uint32_t maxExpirationDuration = [SMKDisappearingMessagesConfiguration maxDurationSeconds];
_expiresInSeconds = MIN(expiresInSeconds, maxExpirationDuration);
[self updateExpiresAt];

View File

@ -1,3 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
@objc public extension TSOutgoingMessage {
@ -9,11 +12,11 @@ import SessionUtilitiesKit
static func from(_ visibleMessage: VisibleMessage, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction? = nil) -> TSOutgoingMessage {
var expiration: UInt32 = 0
let disappearingMessagesConfigurationOrNil: OWSDisappearingMessagesConfiguration?
let disappearingMessagesConfigurationOrNil: Legacy.DisappearingMessagesConfiguration?
if let transaction = transaction {
disappearingMessagesConfigurationOrNil = OWSDisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!, transaction: transaction)
disappearingMessagesConfigurationOrNil = Legacy.DisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!, transaction: transaction)
} else {
disappearingMessagesConfigurationOrNil = OWSDisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!)
disappearingMessagesConfigurationOrNil = Legacy.DisappearingMessagesConfiguration.fetch(uniqueId: thread.uniqueId!)
}
if let disappearingMessagesConfiguration = disappearingMessagesConfigurationOrNil {
expiration = disappearingMessagesConfiguration.isEnabled ? disappearingMessagesConfiguration.durationSeconds : 0

View File

@ -1,8 +1,7 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SignalCoreKit
@objc(OWSTypingIndicatorInteraction)
public class TypingIndicatorInteraction: TSInteraction {

View File

@ -1,72 +1,72 @@
import SessionUtilitiesKit
public extension VisibleMessage {
@objc(SNProfile)
class Profile : NSObject, NSCoding {
public var displayName: String?
public var profileKey: Data?
public var profilePictureURL: String?
internal init(displayName: String, profileKey: Data? = nil, profilePictureURL: String? = nil) {
self.displayName = displayName
self.profileKey = profileKey
self.profilePictureURL = profilePictureURL
}
public required init?(coder: NSCoder) {
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
}
public func encode(with coder: NSCoder) {
coder.encode(displayName, forKey: "displayName")
coder.encode(profileKey, forKey: "profileKey")
coder.encode(profilePictureURL, forKey: "profilePictureURL")
}
public static func fromProto(_ proto: SNProtoDataMessage) -> Profile? {
guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
let profileKey = proto.profileKey
let profilePictureURL = profileProto.profilePicture
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
return Profile(displayName: displayName, profileKey: profileKey, profilePictureURL: profilePictureURL)
} else {
return Profile(displayName: displayName)
}
}
public func toProto() -> SNProtoDataMessage? {
guard let displayName = displayName else {
SNLog("Couldn't construct profile proto from: \(self).")
return nil
}
let dataMessageProto = SNProtoDataMessage.builder()
let profileProto = SNProtoDataMessageLokiProfile.builder()
profileProto.setDisplayName(displayName)
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
dataMessageProto.setProfileKey(profileKey)
profileProto.setProfilePicture(profilePictureURL)
}
do {
dataMessageProto.setProfile(try profileProto.build())
return try dataMessageProto.build()
} catch {
SNLog("Couldn't construct profile proto from: \(self).")
return nil
}
}
// MARK: Description
public override var description: String {
"""
Profile(
displayName: \(displayName ?? "null"),
profileKey: \(profileKey?.description ?? "null"),
profilePictureURL: \(profilePictureURL ?? "null")
)
"""
}
}
}
//import SessionUtilitiesKit
//
//public extension VisibleMessage {
//
// @objc(SNProfile)
// class Profile : NSObject, NSCoding {
// public var displayName: String?
// public var profileKey: Data?
// public var profilePictureURL: String?
//
// internal init(displayName: String, profileKey: Data? = nil, profilePictureURL: String? = nil) {
// self.displayName = displayName
// self.profileKey = profileKey
// self.profilePictureURL = profilePictureURL
// }
//
// public required init?(coder: NSCoder) {
// if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
// if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
// if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
// }
//
// public func encode(with coder: NSCoder) {
// coder.encode(displayName, forKey: "displayName")
// coder.encode(profileKey, forKey: "profileKey")
// coder.encode(profilePictureURL, forKey: "profilePictureURL")
// }
//
// public static func fromProto(_ proto: SNProtoDataMessage, sessionId: String) -> Profile? {
// guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
// let profileKey = proto.profileKey
// let profilePictureURL = profileProto.profilePicture
// if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
// return Profile(displayName: displayName, profileKey: profileKey, profilePictureURL: profilePictureURL)
// } else {
// return Profile(displayName: displayName)
// }
// }
//
// public func toProto() -> SNProtoDataMessage? {
// guard let displayName = displayName else {
// SNLog("Couldn't construct profile proto from: \(self).")
// return nil
// }
// let dataMessageProto = SNProtoDataMessage.builder()
// let profileProto = SNProtoDataMessageLokiProfile.builder()
// profileProto.setDisplayName(displayName)
// if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
// dataMessageProto.setProfileKey(profileKey)
// profileProto.setProfilePicture(profilePictureURL)
// }
// do {
// dataMessageProto.setProfile(try profileProto.build())
// return try dataMessageProto.build()
// } catch {
// SNLog("Couldn't construct profile proto from: \(self).")
// return nil
// }
// }
//
// // MARK: Description
// public override var description: String {
// """
// Profile(
// displayName: \(displayName ?? "null"),
// profileKey: \(profileKey?.description ?? "null"),
// profilePictureURL: \(profilePictureURL ?? "null")
// )
// """
// }
// }
//}

View File

@ -10,8 +10,8 @@ public final class VisibleMessage : Message {
@objc public var attachmentIDs: [String] = []
@objc public var quote: Quote?
@objc public var linkPreview: LinkPreview?
@objc public var contact: Contact?
@objc public var profile: Profile?
@objc public var contact: Legacy.Contact?
@objc public var profile: Legacy.Profile?
@objc public var openGroupInvitation: OpenGroupInvitation?
public override var isSelfSendValid: Bool { true }
@ -37,7 +37,7 @@ public final class VisibleMessage : Message {
if let quote = coder.decodeObject(forKey: "quote") as! Quote? { self.quote = quote }
if let linkPreview = coder.decodeObject(forKey: "linkPreview") as! LinkPreview? { self.linkPreview = linkPreview }
// TODO: Contact
if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile }
if let profile = coder.decodeObject(forKey: "profile") as! Legacy.Profile? { self.profile = profile }
if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation }
}
@ -54,7 +54,7 @@ public final class VisibleMessage : Message {
}
// MARK: Proto Conversion
public override class func fromProto(_ proto: SNProtoContent) -> VisibleMessage? {
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> VisibleMessage? {
guard let dataMessage = proto.dataMessage else { return nil }
let result = VisibleMessage()
result.text = dataMessage.body
@ -62,7 +62,7 @@ public final class VisibleMessage : Message {
if let quoteProto = dataMessage.quote, let quote = Quote.fromProto(quoteProto) { result.quote = quote }
if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview }
// TODO: Contact
if let profile = Profile.fromProto(dataMessage) { result.profile = profile }
if let profile = Legacy.Profile.fromProto(dataMessage) { result.profile = profile }
if let openGroupInvitationProto = dataMessage.openGroupInvitation,
let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation }
result.syncTarget = dataMessage.syncTarget

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
@objc(SNOpenGroupAPIV2)
public final class OpenGroupAPIV2 : NSObject {
private static var authTokenPromises: [String: Promise<String>] = [:]
private static var authTokenPromises: Atomic<[String: Promise<String>]> = Atomic([:])
private static var hasPerformedInitialPoll: [String: Bool] = [:]
private static var hasUpdatedLastOpenDate = false
public static let workQueue = DispatchQueue(label: "OpenGroupAPIV2.workQueue", qos: .userInitiated) // It's important that this is a serial queue
@ -215,7 +215,7 @@ public final class OpenGroupAPIV2 : NSObject {
if let authToken = storage.getAuthToken(for: room, on: server) {
return Promise.value(authToken)
} else {
if let authTokenPromise = authTokenPromises["\(server).\(room)"] {
if let authTokenPromise = authTokenPromises.wrappedValue["\(server).\(room)"] {
return authTokenPromise
} else {
let promise = requestNewAuthToken(for: room, on: server)
@ -230,11 +230,11 @@ public final class OpenGroupAPIV2 : NSObject {
return promise
}
promise.done(on: OpenGroupAPIV2.workQueue) { _ in
authTokenPromises["\(server).\(room)"] = nil
authTokenPromises.mutate { $0["\(server).\(room)"] = nil }
}.catch(on: OpenGroupAPIV2.workQueue) { _ in
authTokenPromises["\(server).\(room)"] = nil
authTokenPromises.mutate { $0["\(server).\(room)"] = nil }
}
authTokenPromises["\(server).\(room)"] = promise
authTokenPromises.mutate { $0["\(server).\(room)"] = promise }
return promise
}
}
@ -251,7 +251,7 @@ public final class OpenGroupAPIV2 : NSObject {
let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else {
throw Error.parsingFailed
}
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey)
let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: Data(userKeyPair.secretKey))
guard let tokenAsData = try? AESGCM.decrypt(ciphertext, with: symmetricKey) else { throw Error.decryptionFailed }
return tokenAsData.toHexString()
}

View File

@ -17,10 +17,11 @@ public struct OpenGroupMessageV2 {
public func sign() -> OpenGroupMessageV2? {
guard
let userKeyPair = Identity.fetchUserKeyPair(),
let legacyKeyPair: ECKeyPair = try? ECKeyPair(publicKeyData: Data(userKeyPair.publicKey), privateKeyData: Data(userKeyPair.secretKey)),
let data: Data = Data(base64Encoded: base64EncodedData)
else { return nil }
guard let signature = try? Ed25519.sign(data, with: userKeyPair) else {
guard let signature = try? Ed25519.sign(data, with: legacyKeyPair) else {
SNLog("Failed to sign open group message.")
return nil
}

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
@objc(SNDataExtractionNotificationInfoMessage)
final class DataExtractionNotificationInfoMessage : TSInfoMessage {
@ -10,20 +15,24 @@ final class DataExtractionNotificationInfoMessage : TSInfoMessage {
super.init(coder: coder)
}
required init(dictionary dictionaryValue: [String:Any]!) throws {
required init(dictionary dictionaryValue: [String: Any]!) throws {
try super.init(dictionary: dictionaryValue)
}
override func previewText(with transaction: YapDatabaseReadTransaction) -> String {
guard let thread = thread as? TSContactThread else { return "" } // Should never occur
let sessionID = thread.contactSessionID()
let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID
let displayName = Profile.displayName(for: thread.contactSessionID())
switch messageType {
case .screenshotNotification: return String(format: NSLocalizedString("screenshot_taken", comment: ""), displayName)
case .mediaSavedNotification:
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
return String(format: NSLocalizedString("meida_saved", comment: ""), displayName)
default: preconditionFailure()
case .screenshotNotification:
return String(format: NSLocalizedString("screenshot_taken", comment: ""), displayName)
case .mediaSavedNotification:
// TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved
return String(format: NSLocalizedString("meida_saved", comment: ""), displayName)
default: preconditionFailure()
}
}
}

View File

@ -3,9 +3,10 @@
//
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
#import <SignalCoreKit/NSString+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSDisappearingConfigurationUpdateInfoMessage ()

View File

@ -1,35 +1,35 @@
////
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//#import <SessionUtilitiesKit/TSYapDatabaseObject.h>
//#import <SignalCoreKit/SignalCoreKit.h>
//
#import <SessionUtilitiesKit/TSYapDatabaseObject.h>
#import <SignalCoreKit/SignalCoreKit.h>
NS_ASSUME_NONNULL_BEGIN
#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval
@class YapDatabaseReadTransaction;
@interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject
- (instancetype)initDefaultWithThreadId:(NSString *)threadId;
- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds;
@property (nonatomic, getter=isEnabled) BOOL enabled;
@property (nonatomic) uint32_t durationSeconds;
@property (nonatomic, readonly) NSUInteger durationIndex;
@property (nonatomic, readonly) NSString *durationString;
@property (nonatomic, readonly) BOOL dictionaryValueDidChange;
@property (readonly, getter=isNewRecord) BOOL newRecord;
+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction;
+ (NSArray<NSNumber *> *)validDurationsSeconds;
+ (uint32_t)maxDurationSeconds;
@end
NS_ASSUME_NONNULL_END
//NS_ASSUME_NONNULL_BEGIN
//
//#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval
//
//@class YapDatabaseReadTransaction;
//
//@interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject
//
//- (instancetype)initDefaultWithThreadId:(NSString *)threadId;
//
//- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds;
//
//@property (nonatomic, getter=isEnabled) BOOL enabled;
//@property (nonatomic) uint32_t durationSeconds;
//@property (nonatomic, readonly) NSUInteger durationIndex;
//@property (nonatomic, readonly) NSString *durationString;
//@property (nonatomic, readonly) BOOL dictionaryValueDidChange;
//@property (readonly, getter=isNewRecord) BOOL newRecord;
//
//+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
// transaction:(YapDatabaseReadTransaction *)transaction;
//
//+ (NSArray<NSNumber *> *)validDurationsSeconds;
//+ (uint32_t)maxDurationSeconds;
//
//@end
//
//NS_ASSUME_NONNULL_END

View File

@ -1,130 +1,130 @@
////
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//#import "OWSDisappearingMessagesConfiguration.h"
//#import <SignalCoreKit/NSDate+OWS.h>
//#import <SignalCoreKit/NSString+OWS.h>
//
#import "OWSDisappearingMessagesConfiguration.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/NSString+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSDisappearingMessagesConfiguration ()
// Transient record lifecycle attributes.
@property (atomic) NSDictionary *originalDictionaryValue;
@property (atomic, getter=isNewRecord) BOOL newRecord;
@end
@implementation OWSDisappearingMessagesConfiguration
- (instancetype)initDefaultWithThreadId:(NSString *)threadId
{
return [self initWithThreadId:threadId
enabled:NO
durationSeconds:(NSTimeInterval)OWSDisappearingMessagesConfigurationDefaultExpirationDuration];
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
_originalDictionaryValue = [self dictionaryValue];
_newRecord = NO;
return self;
}
- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds
{
self = [super initWithUniqueId:threadId];
if (!self) {
return self;
}
_enabled = isEnabled;
_durationSeconds = seconds;
_newRecord = YES;
_originalDictionaryValue = self.dictionaryValue;
return self;
}
+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
transaction:(YapDatabaseReadTransaction *)transaction
{
OWSDisappearingMessagesConfiguration *savedConfiguration =
[self fetchObjectWithUniqueID:threadId transaction:transaction];
if (savedConfiguration) {
return savedConfiguration;
} else {
return [[self alloc] initDefaultWithThreadId:threadId];
}
}
+ (NSArray<NSNumber *> *)validDurationsSeconds
{
return @[
@(5 * kSecondInterval),
@(10 * kSecondInterval),
@(30 * kSecondInterval),
@(1 * kMinuteInterval),
@(5 * kMinuteInterval),
@(30 * kMinuteInterval),
@(1 * kHourInterval),
@(6 * kHourInterval),
@(12 * kHourInterval),
@(24 * kHourInterval),
@(1 * kWeekInterval)
];
}
+ (uint32_t)maxDurationSeconds
{
static uint32_t max;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
max = [[self.validDurationsSeconds valueForKeyPath:@"@max.intValue"] unsignedIntValue];
});
return max;
}
- (NSUInteger)durationIndex
{
return [[self.class validDurationsSeconds] indexOfObject:@(self.durationSeconds)];
}
- (NSString *)durationString
{
return [NSString formatDurationSeconds:self.durationSeconds useShortFormat:NO];
}
#pragma mark - Dirty Tracking
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
{
// Don't persist transient properties
if ([propertyKey isEqualToString:@"originalDictionaryValue"]
||[propertyKey isEqualToString:@"newRecord"]) {
return MTLPropertyStorageNone;
} else {
return [super storageBehaviorForPropertyWithKey:propertyKey];
}
}
- (BOOL)dictionaryValueDidChange
{
return ![self.originalDictionaryValue isEqual:[self dictionaryValue]];
}
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super saveWithTransaction:transaction];
self.originalDictionaryValue = [self dictionaryValue];
self.newRecord = NO;
}
@end
NS_ASSUME_NONNULL_END
//NS_ASSUME_NONNULL_BEGIN
//
//@interface OWSDisappearingMessagesConfiguration ()
//
//// Transient record lifecycle attributes.
//@property (atomic) NSDictionary *originalDictionaryValue;
//@property (atomic, getter=isNewRecord) BOOL newRecord;
//
//@end
//
//@implementation OWSDisappearingMessagesConfiguration
//
//- (instancetype)initDefaultWithThreadId:(NSString *)threadId
//{
// return [self initWithThreadId:threadId
// enabled:NO
// durationSeconds:(NSTimeInterval)OWSDisappearingMessagesConfigurationDefaultExpirationDuration];
//}
//
//- (nullable instancetype)initWithCoder:(NSCoder *)coder
//{
// self = [super initWithCoder:coder];
//
// _originalDictionaryValue = [self dictionaryValue];
// _newRecord = NO;
//
// return self;
//}
//
//- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds
//{
// self = [super initWithUniqueId:threadId];
// if (!self) {
// return self;
// }
//
// _enabled = isEnabled;
// _durationSeconds = seconds;
// _newRecord = YES;
// _originalDictionaryValue = self.dictionaryValue;
//
// return self;
//}
//
//+ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId
// transaction:(YapDatabaseReadTransaction *)transaction
//{
// OWSDisappearingMessagesConfiguration *savedConfiguration =
// [self fetchObjectWithUniqueID:threadId transaction:transaction];
// if (savedConfiguration) {
// return savedConfiguration;
// } else {
// return [[self alloc] initDefaultWithThreadId:threadId];
// }
//}
//
//+ (NSArray<NSNumber *> *)validDurationsSeconds
//{
// return @[
// @(5 * kSecondInterval),
// @(10 * kSecondInterval),
// @(30 * kSecondInterval),
// @(1 * kMinuteInterval),
// @(5 * kMinuteInterval),
// @(30 * kMinuteInterval),
// @(1 * kHourInterval),
// @(6 * kHourInterval),
// @(12 * kHourInterval),
// @(24 * kHourInterval),
// @(1 * kWeekInterval)
// ];
//}
//
//+ (uint32_t)maxDurationSeconds
//{
// static uint32_t max;
// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
// max = [[self.validDurationsSeconds valueForKeyPath:@"@max.intValue"] unsignedIntValue];
// });
//
// return max;
//}
//
//- (NSUInteger)durationIndex
//{
// return [[self.class validDurationsSeconds] indexOfObject:@(self.durationSeconds)];
//}
//
//- (NSString *)durationString
//{
// return [NSString formatDurationSeconds:self.durationSeconds useShortFormat:NO];
//}
//
//#pragma mark - Dirty Tracking
//
//+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
//{
// // Don't persist transient properties
// if ([propertyKey isEqualToString:@"originalDictionaryValue"]
// ||[propertyKey isEqualToString:@"newRecord"]) {
// return MTLPropertyStorageNone;
// } else {
// return [super storageBehaviorForPropertyWithKey:propertyKey];
// }
//}
//
//- (BOOL)dictionaryValueDidChange
//{
// return ![self.originalDictionaryValue isEqual:[self dictionaryValue]];
//}
//
//- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
//{
// [super saveWithTransaction:transaction];
// self.originalDictionaryValue = [self dictionaryValue];
// self.newRecord = NO;
//}
//
//@end
//
//NS_ASSUME_NONNULL_END

View File

@ -188,8 +188,7 @@ void AssertIsOnDisappearingMessagesQueue()
NSString *_Nullable remoteContactName = nil;
if (remoteRecipientId) {
SNContactContext context = [SNContact contextForThread:thread];
remoteContactName = [[LKStorage.shared getContactWithSessionID:remoteRecipientId] displayNameFor:context] ?: remoteRecipientId;
remoteContactName = [SMKProfile displayNameWithId:remoteRecipientId thread:thread];
}
// Become eventually consistent in the case that the remote changed their settings at the same time.
@ -198,9 +197,9 @@ void AssertIsOnDisappearingMessagesQueue()
[thread disappearingMessagesConfigurationWithTransaction:transaction];
if (duration == 0) {
disappearingMessagesConfiguration.enabled = NO;
disappearingMessagesConfiguration.isEnabled = NO;
} else {
disappearingMessagesConfiguration.enabled = YES;
disappearingMessagesConfiguration.isEnabled = YES;
disappearingMessagesConfiguration.durationSeconds = duration;
}

View File

@ -1,10 +1,9 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import AFNetworking
import Foundation
import PromiseKit
import SignalCoreKit
@objc
public enum LinkPreviewError: Int, Error {

View File

@ -33,9 +33,9 @@ public final class MentionsManager : NSObject {
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
storage.dbReadConnection.read { transaction in
candidates = cache.compactMap { publicKey in
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
let displayNameOrNil = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
guard let displayName = displayNameOrNil else { return nil }
guard let displayName: String = Profile.displayNameNoFallback(for: publicKey, context: (openGroupV2 != nil ? .openGroup : .regular)) else {
return nil
}
guard !displayName.hasPrefix("Anonymous") else { return nil }
return Mention(publicKey: publicKey, displayName: displayName)
}

View File

@ -8,9 +8,9 @@ import SessionUtilitiesKit
extension MessageReceiver {
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: ECKeyPair) throws -> (plaintext: Data, senderX25519PublicKey: String) {
let recipientX25519PrivateKey = x25519KeyPair.privateKey
let recipientX25519PublicKey = Data(hex: x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: Box.KeyPair) throws -> (plaintext: Data, senderX25519PublicKey: String) {
let recipientX25519PrivateKey = x25519KeyPair.secretKey
let recipientX25519PublicKey = x25519KeyPair.publicKey
let sodium = Sodium()
let signatureSize = sodium.sign.Bytes
let ed25519PublicKeySize = sodium.sign.PublicKeyBytes

View File

@ -1,23 +1,25 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import Curve25519Kit
import SignalCoreKit
import SessionSnodeKit
extension MessageReceiver {
public static func handle(_ message: Message, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws {
public static func handle(_ db: Database, _ message: Message, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws {
switch message {
case let message as ReadReceipt: handleReadReceipt(message, using: transaction)
case let message as TypingIndicator: handleTypingIndicator(message, using: transaction)
case let message as ClosedGroupControlMessage: handleClosedGroupControlMessage(message, using: transaction)
case let message as ClosedGroupControlMessage: handleClosedGroupControlMessage(db, message, using: transaction)
case let message as DataExtractionNotification: handleDataExtractionNotification(message, using: transaction)
case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(message, using: transaction)
case let message as ConfigurationMessage: handleConfigurationMessage(db, message, using: transaction)
case let message as UnsendRequest: handleUnsendRequest(message, using: transaction)
case let message as MessageRequestResponse: handleMessageRequestResponse(message, using: transaction)
case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
case let message as MessageRequestResponse: handleMessageRequestResponse(db, message, using: transaction)
case let message as VisibleMessage: try handleVisibleMessage(db, message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
default: fatalError()
}
@ -145,11 +147,11 @@ extension MessageReceiver {
threadOrNil = TSContactThread.getWithContactSessionID(syncTarget ?? senderPublicKey, transaction: transaction)
}
guard let thread = threadOrNil else { return }
let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: true, durationSeconds: duration)
let configuration = Legacy.DisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: true, durationSeconds: duration)
configuration.save(with: transaction)
var senderDisplayName: String? = nil
if senderPublicKey != getUserHexEncodedPublicKey() {
senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
senderDisplayName = Profile.displayName(for: senderPublicKey)
}
let message = OWSDisappearingConfigurationUpdateInfoMessage(timestamp: messageSentTimestamp, thread: thread,
configuration: configuration, createdByRemoteName: senderDisplayName, createdInExistingGroup: false)
@ -168,11 +170,11 @@ extension MessageReceiver {
threadOrNil = TSContactThread.getWithContactSessionID(syncTarget ?? senderPublicKey, transaction: transaction)
}
guard let thread = threadOrNil else { return }
let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: false, durationSeconds: 24 * 60 * 60)
let configuration = Legacy.DisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: false, durationSeconds: 24 * 60 * 60)
configuration.save(with: transaction)
var senderDisplayName: String? = nil
if senderPublicKey != getUserHexEncodedPublicKey() {
senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
senderDisplayName = Profile.displayName(for: senderPublicKey)
}
let message = OWSDisappearingConfigurationUpdateInfoMessage(timestamp: messageSentTimestamp, thread: thread,
configuration: configuration, createdByRemoteName: senderDisplayName, createdInExistingGroup: false)
@ -184,7 +186,7 @@ extension MessageReceiver {
// MARK: - Configuration Messages
private static func handleConfigurationMessage(_ message: ConfigurationMessage, using transaction: Any) {
private static func handleConfigurationMessage(_ db: Database, _ message: ConfigurationMessage, using transaction: Any) {
let userPublicKey = getUserHexEncodedPublicKey()
guard message.sender == userPublicKey else { return }
SNLog("Configuration message received.")
@ -195,10 +197,14 @@ extension MessageReceiver {
let lastConfigTimestamp: TimeInterval = (UserDefaults.standard[.lastConfigurationSync]?.timeIntervalSince1970 ?? Date(timeIntervalSince1970: 0).timeIntervalSince1970)
// Profile
var userProfileKey: OWSAES256Key? = nil
if let profileKey = message.profileKey { userProfileKey = OWSAES256Key(data: profileKey) }
updateProfileIfNeeded(publicKey: userPublicKey, name: message.displayName, profilePictureURL: message.profilePictureURL,
profileKey: userProfileKey, sentTimestamp: message.sentTimestamp!, transaction: transaction)
updateProfileIfNeeded(
publicKey: userPublicKey,
name: message.displayName,
profilePictureURL: message.profilePictureURL,
profileKey: OWSAES256Key(data: message.profileKey),
sentTimestamp: message.sentTimestamp!,
transaction: transaction
)
if isInitialSync || messageSentTimestamp > lastConfigTimestamp {
if isInitialSync {
@ -211,11 +217,18 @@ extension MessageReceiver {
// Contacts
for contactInfo in message.contacts {
let sessionID = contactInfo.publicKey!
let contact = (Storage.shared.getContact(with: sessionID, using: transaction) ?? Contact(sessionID: sessionID))
let contactWasBlocked: Bool = contact.isBlocked
if let profileKey = contactInfo.profileKey { contact.profileEncryptionKey = OWSAES256Key(data: profileKey) }
contact.profilePictureURL = contactInfo.profilePictureURL
contact.name = contactInfo.displayName
let contact: Contact = Contact.fetchOrCreate(db, id: sessionID)
let profile: Profile = Profile.fetchOrCreate(db, id: sessionID)
try? profile
.with(
name: contactInfo.displayName,
profilePictureUrl: .updateIf(contactInfo.profilePictureURL),
profileEncryptionKey: .updateIf(
contactInfo.profileKey.map { OWSAES256Key(data: $0) }
)
)
.save(db)
// Note: We only update these values if the proto actually has values for them (this is to
// prevent an edge case where an old client could override the values with default values
@ -225,12 +238,22 @@ extension MessageReceiver {
// config message setting *isApproved* and *didApproveMe* to true. This may prevent some
// weird edge cases where a config message swapping *isApproved* and *didApproveMe* to
// false.
if contactInfo.hasIsApproved && contactInfo.isApproved { contact.isApproved = true }
if contactInfo.hasDidApproveMe && contactInfo.didApproveMe { contact.didApproveMe = true }
if contactInfo.hasIsBlocked { contact.isBlocked = contactInfo.isBlocked }
Storage.shared.setContact(contact, using: transaction)
try? contact
.with(
isApproved: (contactInfo.hasIsApproved && contactInfo.isApproved ?
.existing :
true
),
isBlocked: (contactInfo.hasIsBlocked && contactInfo.isBlocked ?
.existing :
true
),
didApproveMe: (contactInfo.hasDidApproveMe && contactInfo.didApproveMe ?
.existing :
true
)
)
.save(db)
// If the contact is blocked
if contactInfo.hasIsBlocked && contactInfo.isBlocked {
@ -238,7 +261,7 @@ extension MessageReceiver {
// associated with them that is a message request thread then delete it (assume
// that the current user had deleted that message request)
if
contactInfo.isBlocked != contactWasBlocked,
contactInfo.isBlocked != contact.isBlocked,
let thread: TSContactThread = TSContactThread.getWithContactSessionID(sessionID, transaction: transaction),
thread.isMessageRequest(using: transaction)
{
@ -258,7 +281,11 @@ extension MessageReceiver {
let allClosedGroupPublicKeys = storage.getUserClosedGroupPublicKeys()
for closedGroup in message.closedGroups {
guard !allClosedGroupPublicKeys.contains(closedGroup.publicKey) else { continue }
handleNewClosedGroup(groupPublicKey: closedGroup.publicKey, name: closedGroup.name, encryptionKeyPair: closedGroup.encryptionKeyPair,
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: closedGroup.encryptionKeyPair.publicKey.bytes,
secretKey: closedGroup.encryptionKeyPair.privateKey.bytes
)
handleNewClosedGroup(db, groupPublicKey: closedGroup.publicKey, name: closedGroup.name, encryptionKeyPair: keyPair,
members: [String](closedGroup.members), admins: [String](closedGroup.admins), expirationTimer: closedGroup.expirationTimer,
messageSentTimestamp: message.sentTimestamp!, using: transaction)
}
@ -313,7 +340,8 @@ extension MessageReceiver {
// MARK: - Visible Messages
@discardableResult
public static func handleVisibleMessage(_ message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws -> String {
public static func handleVisibleMessage(_ db: Database, _ message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupID: String?, isBackgroundPoll: Bool, using transaction: Any) throws -> String {
let sender: String = message.sender!
let storage = SNMessagingKitConfiguration.shared.storage
let transaction = transaction as! YapDatabaseReadWriteTransaction
var isMainAppAndActive = false
@ -337,7 +365,7 @@ extension MessageReceiver {
profileKey: contactProfileKey, sentTimestamp: message.sentTimestamp!, transaction: transaction)
}
// Get or create thread
guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? sender, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
// Parse quote if needed
var tsQuotedMessage: TSQuotedMessage? = nil
if message.quote != nil && proto.dataMessage?.quote != nil, let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) {
@ -359,7 +387,9 @@ extension MessageReceiver {
groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.duplicateMessage }
message.threadID = threadID
// Start attachment downloads if needed
let isContactTrusted = Storage.shared.getContact(with: message.sender!)?.isTrusted ?? false
// TODO: Swap this back
let isContactTrusted: Bool = (GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: sender) })?.isTrusted ?? false)
// let isContactTrusted: Bool = ((try? Contact.fetchOne(db, id: sender))?.isTrusted ?? false)
let isGroup = message.groupPublicKey != nil || openGroupID != nil
attachmentsToDownload.forEach { attachmentID in
let downloadJob = AttachmentDownloadJob(attachmentID: attachmentID, tsMessageID: tsMessageID, threadID: threadID)
@ -403,10 +433,10 @@ extension MessageReceiver {
// by using the approval process
if !isGroup, let senderSessionId: String = message.sender {
updateContactApprovalStatusIfNeeded(
db,
senderSessionId: senderSessionId,
threadId: message.threadID,
forceConfigSync: false,
using: transaction
forceConfigSync: false
)
}
@ -427,9 +457,10 @@ extension MessageReceiver {
profileKey: OWSAES256Key?, sentTimestamp: UInt64, transaction: YapDatabaseReadWriteTransaction) {
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey())
let userDefaults = UserDefaults.standard
let contact = Storage.shared.getContact(with: publicKey) ?? Contact(sessionID: publicKey) // New API
var profile: Profile = Profile.fetchOrCreate(id: publicKey)
// Name
if let name = name, name != contact.name {
if let name = name, name != profile.name {
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(userDefaults[.lastDisplayNameUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true
@ -440,40 +471,54 @@ extension MessageReceiver {
if isCurrentUser {
userDefaults[.lastDisplayNameUpdate] = Date(timeIntervalSince1970: TimeInterval(sentTimestamp / 1000))
}
contact.name = name
profile = profile.with(name: name)
}
}
// Profile picture & profile key
if let profileKey = profileKey, let profilePictureURL = profilePictureURL,
profileKey.keyData.count == kAES256_KeyByteLength, profileKey != contact.profileEncryptionKey {
if
let profileKey = profileKey,
let profilePictureURL = profilePictureURL,
profileKey.keyData.count == kAES256_KeyByteLength,
profileKey != profile.profileEncryptionKey
{
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(userDefaults[.lastProfilePictureUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true
} else {
shouldUpdate = true
}
if shouldUpdate {
if isCurrentUser {
userDefaults[.lastProfilePictureUpdate] = Date(timeIntervalSince1970: TimeInterval(sentTimestamp / 1000))
}
contact.profilePictureURL = profilePictureURL
contact.profileEncryptionKey = profileKey
profile = profile.with(
profilePictureUrl: .update(profilePictureURL),
profileEncryptionKey: .update(profileKey)
)
}
}
// Persist changes
Storage.shared.setContact(contact, using: transaction)
GRDBStorage.shared.write { db in
try profile.save(db)
}
// Download the profile picture if needed
transaction.addCompletionQueue(DispatchQueue.main) {
SSKEnvironment.shared.profileManager.downloadAvatar(forUserProfile: contact)
ProfileManager.downloadAvatar(for: profile)
}
}
// MARK: - Closed Groups
public static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) {
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage, using transaction: Any) {
switch message.kind! {
case .new: handleNewClosedGroup(message, using: transaction)
case .new: handleNewClosedGroup(db, message, using: transaction)
case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction)
case .nameChange: handleClosedGroupNameChanged(message, using: transaction)
case .membersAdded: handleClosedGroupMembersAdded(message, using: transaction)
@ -483,16 +528,16 @@ extension MessageReceiver {
}
}
private static func handleNewClosedGroup(_ message: ClosedGroupControlMessage, using transaction: Any) {
private static func handleNewClosedGroup(_ db: Database, _ message: ClosedGroupControlMessage, using transaction: Any) {
guard case let .new(publicKeyAsData, name, encryptionKeyPair, membersAsData, adminsAsData, expirationTimer) = message.kind else { return }
let groupPublicKey = publicKeyAsData.toHexString()
let members = membersAsData.map { $0.toHexString() }
let admins = adminsAsData.map { $0.toHexString() }
handleNewClosedGroup(groupPublicKey: groupPublicKey, name: name, encryptionKeyPair: encryptionKeyPair,
handleNewClosedGroup(db, groupPublicKey: groupPublicKey, name: name, encryptionKeyPair: encryptionKeyPair,
members: members, admins: admins, expirationTimer: expirationTimer, messageSentTimestamp: message.sentTimestamp!, using: transaction)
}
private static func handleNewClosedGroup(groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: [String], admins: [String], expirationTimer: UInt32, messageSentTimestamp: UInt64, using transaction: Any) {
private static func handleNewClosedGroup(_ db: Database, groupPublicKey: String, name: String, encryptionKeyPair: Box.KeyPair, members: [String], admins: [String], expirationTimer: UInt32, messageSentTimestamp: UInt64, using transaction: Any) {
let transaction = transaction as! YapDatabaseReadWriteTransaction
// With new closed groups we only want to create them if the admin creating the closed group is an
@ -501,7 +546,7 @@ extension MessageReceiver {
var hasApprovedAdmin: Bool = false
for adminId in admins {
if let contact: Contact = Storage.shared.getContact(with: adminId), contact.isApproved {
if let contact: Contact = try? Contact.fetchOne(db, id: adminId), contact.isApproved {
hasApprovedAdmin = true
break
}
@ -533,7 +578,7 @@ extension MessageReceiver {
let isExpirationTimerEnabled = (expirationTimer > 0)
let expirationTimerDuration = (isExpirationTimerEnabled ? expirationTimer : 24 * 60 * 60)
let configuration = OWSDisappearingMessagesConfiguration(
let configuration = Legacy.DisappearingMessagesConfiguration(
threadId: thread.uniqueId!,
enabled: isExpirationTimerEnabled,
durationSeconds: expirationTimerDuration
@ -560,7 +605,7 @@ extension MessageReceiver {
let groupPublicKey = explicitGroupPublicKey?.toHexString() ?? message.groupPublicKey else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
let userPublicKey = getUserHexEncodedPublicKey()
guard let userKeyPair = Identity.fetchUserKeyPair() else {
guard let userKeyPair: Box.KeyPair = Identity.fetchUserKeyPair() else {
return SNLog("Couldn't find user X25519 key pair.")
}
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
@ -586,12 +631,10 @@ extension MessageReceiver {
} catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
let keyPair: ECKeyPair
do {
keyPair = try ECKeyPair(publicKeyData: proto.publicKey.removing05PrefixIfNeeded(), privateKeyData: proto.privateKey)
} catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: proto.publicKey.removing05PrefixIfNeeded().bytes,
secretKey: proto.privateKey.bytes
)
// Store it if needed
let closedGroupEncryptionKeyPairs = Storage.shared.getClosedGroupEncryptionKeyPairs(for: groupPublicKey)
guard !closedGroupEncryptionKeyPairs.contains(keyPair) else {
@ -711,6 +754,8 @@ extension MessageReceiver {
/// Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
private static func handleClosedGroupMemberLeft(_ message: ClosedGroupControlMessage, using transaction: Any) {
guard case .memberLeft = message.kind else { return }
let sender: String = message.sender!
let transaction = transaction as! YapDatabaseReadWriteTransaction
guard let groupPublicKey = message.groupPublicKey else { return }
performIfValid(for: message, using: transaction) { groupID, thread, group in
@ -731,13 +776,16 @@ extension MessageReceiver {
thread.setGroupModel(newGroupModel, with: transaction)
// Notify the user if needed
guard members != Set(group.groupMemberIds) else { return }
let contact = Storage.shared.getContact(with: message.sender!)
let updateInfo: String
if let displayName = contact?.displayName(for: Contact.Context.regular) {
if let displayName = Profile.displayNameNoFallback(for: sender) {
updateInfo = String(format: NSLocalizedString("GROUP_MEMBER_LEFT", comment: ""), displayName)
} else {
}
else {
updateInfo = NSLocalizedString("GROUP_UPDATED", comment: "")
}
let infoMessage = TSInfoMessage(timestamp: message.sentTimestamp!, in: thread, messageType: .groupUpdated, customMessage: updateInfo)
infoMessage.save(with: transaction)
}
@ -787,14 +835,12 @@ extension MessageReceiver {
// MARK: - Message Requests
private static func updateContactApprovalStatusIfNeeded(
_ db: Database,
senderSessionId: String,
threadId: String?,
forceConfigSync: Bool,
using transaction: Any
forceConfigSync: Bool
) {
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return }
let userPublicKey: String = getUserHexEncodedPublicKey()
let userPublicKey: String = getUserHexEncodedPublicKey(db)
// If the sender of the message was the current user
if senderSessionId == userPublicKey {
@ -824,8 +870,8 @@ extension MessageReceiver {
MessageSender.syncConfiguration(forceSyncNow: true).retainUntilComplete()
}
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
let userPublicKey = getUserHexEncodedPublicKey()
public static func handleMessageRequestResponse(_ db: Database, _ message: MessageRequestResponse, using transaction: Any) {
let userPublicKey = getUserHexEncodedPublicKey(db)
// Ignore messages which were sent from the current user
guard message.sender != userPublicKey else { return }
@ -842,10 +888,10 @@ extension MessageReceiver {
}
updateContactApprovalStatusIfNeeded(
db,
senderSessionId: senderId,
threadId: nil,
forceConfigSync: true,
using: transaction
forceConfigSync: true
)
}
}

View File

@ -1,6 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtilitiesKit
public enum MessageReceiver {
@ -51,7 +53,7 @@ public enum MessageReceiver {
}
}
public static func parse(_ data: Data, openGroupMessageServerID: UInt64?, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
public static func parse(_ db: Database, _ data: Data, openGroupMessageServerID: UInt64?, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
let userPublicKey = getUserHexEncodedPublicKey()
let isOpenGroupMessage = (openGroupMessageServerID != nil)
// Parse the envelope
@ -67,7 +69,7 @@ public enum MessageReceiver {
} else {
switch envelope.type {
case .sessionMessage:
guard let userX25519KeyPair = Identity.fetchUserKeyPair() else { throw Error.noUserX25519KeyPair }
guard let userX25519KeyPair: Box.KeyPair = Identity.fetchUserKeyPair() else { throw Error.noUserX25519KeyPair }
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
case .closedGroupMessage:
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else { throw Error.invalidGroupPublicKey }
@ -111,7 +113,8 @@ public enum MessageReceiver {
}
// Don't process the envelope any further if the sender is blocked
guard Storage.shared.getContact(with: sender, using: transaction)?.isBlocked != true else {
guard GRDBStorage.shared.read({ db in try Contact.fetchOne(db, id: sender) })?.isBlocked != true else {
// guard (try? Contact.fetchOne(db, id: sender))?.isBlocked != true else {
throw Error.senderBlocked
}
@ -125,15 +128,15 @@ public enum MessageReceiver {
}
// Parse the message
let message: Message? = {
if let readReceipt = ReadReceipt.fromProto(proto) { return readReceipt }
if let typingIndicator = TypingIndicator.fromProto(proto) { return typingIndicator }
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto) { return closedGroupControlMessage }
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto) { return dataExtractionNotification }
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto) { return expirationTimerUpdate }
if let configurationMessage = ConfigurationMessage.fromProto(proto) { return configurationMessage }
if let unsendRequest = UnsendRequest.fromProto(proto) { return unsendRequest }
if let messageRequestResponse = MessageRequestResponse.fromProto(proto) { return messageRequestResponse }
if let visibleMessage = VisibleMessage.fromProto(proto) { return visibleMessage }
if let readReceipt = ReadReceipt.fromProto(proto, sender: sender) { return readReceipt }
if let typingIndicator = TypingIndicator.fromProto(proto, sender: sender) { return typingIndicator }
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto, sender: sender) { return closedGroupControlMessage }
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto, sender: sender) { return dataExtractionNotification }
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto, sender: sender) { return expirationTimerUpdate }
if let configurationMessage = ConfigurationMessage.fromProto(proto, sender: sender) { return configurationMessage }
if let unsendRequest = UnsendRequest.fromProto(proto, sender: sender) { return unsendRequest }
if let messageRequestResponse = MessageRequestResponse.fromProto(proto, sender: sender) { return messageRequestResponse }
if let visibleMessage = VisibleMessage.fromProto(proto, sender: sender) { return visibleMessage }
return nil
}()
if let message = message {

View File

@ -1,11 +1,12 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Curve25519Kit
import PromiseKit
extension MessageSender {
public static var distributingClosedGroupEncryptionKeyPairs: [String: [ECKeyPair]] = [:]
public static var distributingClosedGroupEncryptionKeyPairs: [String: [Box.KeyPair]] = [:]
public static func createClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> Promise<TSGroupThread> {
// Prepare
@ -30,8 +31,12 @@ extension MessageSender {
for member in members {
let thread = TSContactThread.getOrCreateThread(withContactSessionID: member, transaction: transaction)
thread.save(with: transaction)
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
let closedGroupControlMessageKind = ClosedGroupControlMessage.Kind.new(publicKey: Data(hex: groupPublicKey), name: name,
encryptionKeyPair: encryptionKeyPair, members: membersAsData, admins: adminsAsData, expirationTimer: 0)
encryptionKeyPair: keyPair, members: membersAsData, admins: adminsAsData, expirationTimer: 0)
let closedGroupControlMessage = ClosedGroupControlMessage(kind: closedGroupControlMessageKind)
// Sending this non-durably is okay because we show a loader to the user. If they close the app while the
// loader is still showing, it's within expectation that the group creation might be incomplete.
@ -41,7 +46,11 @@ extension MessageSender {
// Add the group to the user's set of public keys to poll for
Storage.shared.addClosedGroupPublicKey(groupPublicKey, using: transaction)
// Store the key pair
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: groupPublicKey, using: transaction)
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: groupPublicKey, using: transaction)
// Notify the PN server
promises.append(PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: userPublicKey))
// Notify the user
@ -72,10 +81,14 @@ extension MessageSender {
return Promise(error: Error.invalidClosedGroupUpdate)
}
// Generate the new encryption key pair
let newKeyPair = Curve25519.generateKeyPair()
let newLegacyKeyPair = Curve25519.generateKeyPair()
let newKeyPair: Box.KeyPair = Box.KeyPair(
publicKey: newLegacyKeyPair.publicKey.bytes,
secretKey: newLegacyKeyPair.privateKey.bytes
)
// Distribute it
let proto = try! SNProtoKeyPair.builder(publicKey: newKeyPair.publicKey,
privateKey: newKeyPair.privateKey).build()
let proto = try! SNProtoKeyPair.builder(publicKey: Data(newKeyPair.publicKey),
privateKey: Data(newKeyPair.secretKey)).build()
let plaintext = try! proto.serializedData()
let wrappers = targetMembers.compactMap { publicKey -> ClosedGroupControlMessage.KeyPairWrapper in
let ciphertext = try! MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey)
@ -329,8 +342,8 @@ extension MessageSender {
guard let encryptionKeyPair = distributingClosedGroupEncryptionKeyPairs[groupPublicKey]?.last
?? Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { return }
// Send it
guard let proto = try? SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey,
privateKey: encryptionKeyPair.privateKey).build(), let plaintext = try? proto.serializedData() else { return }
guard let proto = try? SNProtoKeyPair.builder(publicKey: Data(encryptionKeyPair.publicKey),
privateKey: Data(encryptionKeyPair.secretKey)).build(), let plaintext = try? proto.serializedData() else { return }
let contactThread = TSContactThread.getOrCreateThread(withContactSessionID: publicKey, transaction: transaction)
guard let ciphertext = try? MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey) else { return }
SNLog("Sending latest encryption key pair to: \(publicKey).")

View File

@ -171,7 +171,7 @@ public final class MessageSender : NSObject {
case .contact(let publicKey): ciphertext = try encryptWithSessionProtocol(plaintext, for: publicKey)
case .closedGroup(let groupPublicKey):
guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { throw Error.noKeyPair }
ciphertext = try encryptWithSessionProtocol(plaintext, for: encryptionKeyPair.hexEncodedPublicKey)
ciphertext = try encryptWithSessionProtocol(plaintext, for: "05\(encryptionKeyPair.publicKey.toHexString())")
case .openGroup(_, _), .openGroupV2(_, _): preconditionFailure()
}
} catch {

View File

@ -65,20 +65,22 @@ public final class OpenGroupPollerV2 : NSObject {
// Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages
let openGroupID = "\(server).\(body.room)"
let messages = body.messages.sorted { $0.serverID! < $1.serverID! } // Safe because messages with a nil serverID are filtered out
storage.write { transaction in
messages.forEach { message in
guard let data = Data(base64Encoded: message.base64EncodedData) else {
return SNLog("Ignoring open group message with invalid encoding.")
}
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.sentTimestamp)
envelope.setContent(data)
envelope.setSource(message.sender!) // Safe because messages with a nil sender are filtered out
do {
let data = try envelope.buildSerializedData()
let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: UInt64(message.serverID!), isRetry: false, using: transaction)
try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
} catch {
SNLog("Couldn't receive open group message due to error: \(error).")
GRDBStorage.shared.write { db in
storage.write { transaction in
messages.forEach { message in
guard let data = Data(base64Encoded: message.base64EncodedData) else {
return SNLog("Ignoring open group message with invalid encoding.")
}
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.sentTimestamp)
envelope.setContent(data)
envelope.setSource(message.sender!) // Safe because messages with a nil sender are filtered out
do {
let data = try envelope.buildSerializedData()
let (message, proto) = try MessageReceiver.parse(db, data, openGroupMessageServerID: UInt64(message.serverID!), isRetry: false, using: transaction)
try MessageReceiver.handle(db, message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction)
} catch {
SNLog("Couldn't receive open group message due to error: \(error).")
}
}
}
}

View File

@ -12,13 +12,6 @@ public protocol SessionMessagingKitStorageProtocol {
func write(with block: @escaping (Any) -> Void, completion: @escaping () -> Void) -> Promise<Void>
func writeSync(with block: @escaping (Any) -> Void)
// MARK: - General
func getUser() -> Contact?
func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact?
func getAllContacts() -> Set<Contact>
func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set<Contact>
// MARK: - Closed Groups
func getUserClosedGroupPublicKeys() -> Set<String>
@ -95,4 +88,4 @@ public protocol SessionMessagingKitStorageProtocol {
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any)
}
extension Storage: SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {}
extension Storage: SessionMessagingKitStorageProtocol {}

View File

@ -59,7 +59,7 @@ NSString *const TSContactThreadPrefix = @"c";
- (BOOL)isMessageRequest {
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
return (
self.shouldBeVisible &&
@ -72,7 +72,7 @@ NSString *const TSContactThreadPrefix = @"c";
- (BOOL)isMessageRequestUsingTransaction:(YapDatabaseReadTransaction *)transaction {
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
return (
self.shouldBeVisible &&
@ -85,14 +85,14 @@ NSString *const TSContactThreadPrefix = @"c";
- (BOOL)isBlocked {
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
return (contact.isBlocked == YES);
}
- (BOOL)isBlockedUsingTransaction:(YapDatabaseReadTransaction *)transaction {
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
SMKContact *contact = [SMKContact fetchOrCreateWithId: sessionID];
return (contact.isBlocked == YES);
}
@ -105,15 +105,13 @@ NSString *const TSContactThreadPrefix = @"c";
- (NSString *)name
{
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID];
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
return [SMKProfile displayNameWithId:sessionID];
}
- (NSString *)nameWithTransaction:(YapDatabaseReadTransaction *)transaction
{
NSString *sessionID = self.contactSessionID;
SNContact *contact = [LKStorage.shared getContactWithSessionID:sessionID using:transaction];
return [contact displayNameFor:SNContactContextRegular] ?: sessionID;
return [SMKProfile displayNameWithId:sessionID];
}
+ (NSString *)threadIDFromContactSessionID:(NSString *)contactSessionID {

View File

@ -123,8 +123,7 @@ const int32_t kGroupIdLength = 16;
if (removedMembersMinusSelf.count > 0) {
NSArray *removedMemberNames = [removedMembers.allObjects map:^NSString *(NSString *publicKey) {
SNContact *contact = [LKStorage.shared getContactWithSessionID:publicKey];
return [contact displayNameFor:SNContactContextRegular] ?: publicKey;
return [SMKProfile displayNameWithId:publicKey];
}];
NSString *format = removedMembers.count > 1 ? NSLocalizedString(@"GROUP_MEMBERS_REMOVED", @"") : NSLocalizedString(@"GROUP_MEMBER_REMOVED", @"");
updatedGroupInfoString = [updatedGroupInfoString
@ -135,8 +134,7 @@ const int32_t kGroupIdLength = 16;
if (addedMembers.count > 0) {
NSArray *addedMemberNames = [[addedMembers allObjects] map:^NSString*(NSString* publicKey) {
SNContact *contact = [LKStorage.shared getContactWithSessionID:publicKey];
return [contact displayNameFor:SNContactContextRegular] ?: publicKey;
return [SMKProfile displayNameWithId:publicKey];
}];
updatedGroupInfoString = [updatedGroupInfoString
stringByAppendingString:[NSString

View File

@ -22,6 +22,7 @@ BOOL IsNoteToSelfEnabled(void);
@property (nonatomic, readonly, nullable) NSDate *lastInteractionDate;
@property (nonatomic, readonly) TSInteraction *lastInteraction;
@property (atomic, readonly) BOOL isMuted;
@property (nonatomic, copy, nullable) NSString *messageDraft;
@property (atomic, readonly, nullable) NSDate *mutedUntilDate;
/**

View File

@ -23,7 +23,6 @@ BOOL IsNoteToSelfEnabled(void)
@property (nonatomic) NSDate *creationDate;
@property (nonatomic, nullable) NSDate *lastInteractionDate;
@property (nonatomic, nullable) NSNumber *archivedAsOfMessageSortId;
@property (nonatomic, copy, nullable) NSString *messageDraft;
@property (atomic, nullable) NSDate *mutedUntilDate;
@end

View File

@ -6,9 +6,9 @@
NS_ASSUME_NONNULL_BEGIN
extern NSString *const kNSNotificationName_LocalProfileDidChange;
extern NSString *const kNSNotificationName_OtherUsersProfileDidChange;
extern NSString *const kNSNotificationKey_ProfileRecipientId;
//extern NSString *const kNSNotificationName_LocalProfileDidChange;
//extern NSString *const kNSNotificationName_OtherUsersProfileDidChange;
//extern NSString *const kNSNotificationKey_ProfileRecipientId;
@interface OWSUserProfile : TSYapDatabaseObject
@ -18,7 +18,7 @@ extern NSString *const kNSNotificationKey_ProfileRecipientId;
+ (NSString *)sharedDataProfileAvatarsDirPath;
+ (NSString *)profileAvatarsDirPath;
+ (void)resetProfileStorage;
+ (NSSet<NSString *> *)allProfileAvatarFilePaths;
//+ (NSSet<NSString *> *)allProfileAvatarFilePaths;
@end

View File

@ -18,10 +18,6 @@
NS_ASSUME_NONNULL_BEGIN
NSString *const kNSNotificationName_LocalProfileDidChange = @"kNSNotificationName_LocalProfileDidChange";
NSString *const kNSNotificationName_OtherUsersProfileDidChange = @"kNSNotificationName_OtherUsersProfileDidChange";
NSString *const kNSNotificationKey_ProfileRecipientId = @"kNSNotificationKey_ProfileRecipientId";
@interface OWSUserProfile ()
@end
@ -69,21 +65,21 @@ NSString *const kNSNotificationKey_ProfileRecipientId = @"kNSNotificationKey_Pro
[[NSFileManager defaultManager] removeItemAtPath:[self profileAvatarsDirPath] error:&error];
}
+ (NSSet<NSString *> *)allProfileAvatarFilePaths
{
NSString *profileAvatarsDirPath = self.profileAvatarsDirPath;
NSMutableSet<NSString *> *profileAvatarFilePaths = [NSMutableSet new];
NSSet<SNContact *> *allContacts = [LKStorage.shared getAllContacts];
for (SNContact *contact in allContacts) {
if (contact.profilePictureFileName == nil) { continue; }
NSString *filePath = [profileAvatarsDirPath stringByAppendingPathComponent:contact.profilePictureFileName];
[profileAvatarFilePaths addObject:filePath];
}
return [profileAvatarFilePaths copy];
}
//+ (NSSet<NSString *> *)allProfileAvatarFilePaths
//{
// NSString *profileAvatarsDirPath = self.profileAvatarsDirPath;
// NSMutableSet<NSString *> *profileAvatarFilePaths = [NSMutableSet new];
//
// NSSet<SNContact *> *allContacts = [LKStorage.shared getAllContacts];
//
// for (SNContact *contact in allContacts) {
// if (contact.profilePictureFileName == nil) { continue; }
// NSString *filePath = [profileAvatarsDirPath stringByAppendingPathComponent:contact.profilePictureFileName];
// [profileAvatarFilePaths addObject:filePath];
// }
//
// return [profileAvatarFilePaths copy];
//}
@end

View File

@ -1,30 +1,30 @@
////
//// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
////
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//@class OWSAES256Key;
//@class TSThread;
//@class YapDatabaseReadWriteTransaction;
//@class SNContact;
//
@class OWSAES256Key;
@class TSThread;
@class YapDatabaseReadWriteTransaction;
@class SNContact;
NS_ASSUME_NONNULL_BEGIN
@protocol ProfileManagerProtocol <NSObject>
#pragma mark - Local Profile
- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL;
#pragma mark - Other User's Profiles
- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId avatarURL:(nullable NSString *)avatarURL;
#pragma mark - Other
- (void)downloadAvatarForUserProfile:(SNContact *)userProfile;
@end
NS_ASSUME_NONNULL_END
//NS_ASSUME_NONNULL_BEGIN
//
//@protocol ProfileManagerProtocol <NSObject>
//
//#pragma mark - Local Profile
//
//- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL;
//
//#pragma mark - Other User's Profiles
//
//- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId;
//- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;
//- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId avatarURL:(nullable NSString *)avatarURL;
//
//#pragma mark - Other
//
//- (void)downloadAvatarForUserProfile:(SNContact *)userProfile;
//
//@end
//
//NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,12 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
extension Box.KeyPair: Equatable {
public static func == (lhs: Box.KeyPair, rhs: Box.KeyPair) -> Bool {
return (
lhs.publicKey == rhs.publicKey &&
lhs.secretKey == rhs.secretKey
)
}
}

View File

@ -178,12 +178,16 @@ public class FullTextSearchFinder: NSObject {
}
private static let recipientIndexer: SearchIndexer<String> = SearchIndexer { (recipientId: String, transaction: YapDatabaseReadTransaction) in
var result = "\(recipientId)"
if let contact = Storage.shared.getContact(with: recipientId) {
if let name = contact.name { result += " \(name)" }
if let nickname = contact.nickname { result += " \(nickname)" }
}
return result
let profile: Profile? = GRDBStorage.shared.read { db in try Profile.fetchOne(db, id: recipientId) }
return [
recipientId,
profile?.name,
profile?.nickname
]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: " ")
}
private static let messageIndexer: SearchIndexer<TSMessage> = SearchIndexer { (message: TSMessage, transaction: YapDatabaseReadTransaction) in

View File

@ -0,0 +1,12 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SignalCoreKit
public extension OWSAES256Key {
convenience init?(data: Data?) {
guard let existingData: Data = data else { return nil }
self.init(data: existingData)
}
}

View File

@ -1,9 +1,8 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import AVFoundation
import SignalCoreKit
@objc(OWSAudioActivity)
public class AudioActivity: NSObject {

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SignalCoreKit
extension OWSSound {
public func notificationSound(isQuiet: Bool) -> UNNotificationSound {

View File

@ -0,0 +1,351 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
public struct ProfileManager {
public enum Error: LocalizedError {
case avatarImageTooLarge
case avatarWriteFailed
case avatarEncryptionFailed
case avatarUploadFailed
case avatarUploadMaxFileSizeExceeded
var localizedDescription: String {
switch self {
case .avatarImageTooLarge: return "Avatar image too large."
case .avatarWriteFailed: return "Avatar write failed."
case .avatarEncryptionFailed: return "Avatar encryption failed."
case .avatarUploadFailed: return "Avatar upload failed."
case .avatarUploadMaxFileSizeExceeded: return "Maximum file size exceeded."
}
}
}
// The max bytes for a user's profile name, encoded in UTF8.
// Before encrypting and submitting we NULL pad the name data to this length.
private static let nameDataLength: UInt = 26
public static let maxAvatarDiameter: CGFloat = 640
private static var profileAvatarCache: Atomic<[String: UIImage]> = Atomic([:])
private static var currentAvatarDownloads: Atomic<Set<String>> = Atomic([])
// MARK: - Functions
public static func isToLong(profileName: String) -> Bool {
return ((profileName.data(using: .utf8)?.count ?? 0) > nameDataLength)
}
public static func profileAvatar(for id: String) -> UIImage? {
guard let profile: Profile = GRDBStorage.shared.read({ db in try Profile.fetchOne(db, id: id) }) else {
return nil
}
if let profileFileName: String = profile.profilePictureFileName, !profileFileName.isEmpty {
return loadProfileAvatar(for: profileFileName)
}
if let profilePictureUrl: String = profile.profilePictureUrl, !profilePictureUrl.isEmpty {
downloadAvatar(for: profile)
}
return nil
}
private static func loadProfileAvatar(for fileName: String) -> UIImage? {
if let cachedImage: UIImage = profileAvatarCache.wrappedValue[fileName] {
return cachedImage
}
guard
!fileName.isEmpty,
let data: Data = loadProfileData(with: fileName),
data.isValidImage,
let image: UIImage = UIImage(data: data)
else {
return nil
}
profileAvatarCache.mutate { $0[fileName] = image }
return image
}
private static func loadProfileData(with fileName: String) -> Data? {
let filePath: String = OWSUserProfile.profileAvatarFilepath(withFilename: fileName)
return try? Data(contentsOf: URL(fileURLWithPath: filePath))
}
// MARK: - Profile Encryption
private static func encryptProfileData(data: Data, key: OWSAES256Key) -> Data? {
guard key.keyData.count == kAES256_KeyByteLength else { return nil }
return Cryptography.encryptAESGCMProfileData(plainTextData: data, key: key)
}
private static func decryptProfileData(data: Data, key: OWSAES256Key) -> Data? {
guard key.keyData.count == kAES256_KeyByteLength else { return nil }
return Cryptography.decryptAESGCMProfileData(encryptedData: data, key: key)
}
// MARK: - Other Users' Profiles
public static func downloadAvatar(for profile: Profile, funcName: String = #function) {
guard !currentAvatarDownloads.wrappedValue.contains(profile.id) else {
// Download already in flight; ignore
return
}
guard
let profileUrlStringAtStart: String = profile.profilePictureUrl,
let profileUrlAtStart: URL = URL(string: profileUrlStringAtStart)
else {
SNLog("Skipping downloading avatar for \(profile.id) because url is not set")
return
}
guard
let fileId: UInt64 = UInt64(profileUrlAtStart.lastPathComponent),
let profileKeyAtStart: OWSAES256Key = profile.profileEncryptionKey,
profileKeyAtStart.keyData.count > 0
else {
return
}
let fileName: String = UUID().uuidString.appendingFileExtension("jpg")
let filePath: String = OWSUserProfile.profileAvatarFilepath(withFilename: fileName)
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: funcName)
DispatchQueue.global(qos: .default).async {
OWSLogger.verbose("downloading profile avatar: \(profile.id)")
currentAvatarDownloads.mutate { $0.insert(profile.id) }
let useOldServer: Bool = (profileUrlStringAtStart.contains(FileServerAPIV2.oldServer))
FileServerAPIV2
.download(fileId, useOldServer: useOldServer)
.done { data in
currentAvatarDownloads.mutate { $0.remove(profile.id) }
GRDBStorage.shared.write { db in
guard let latestProfile: Profile = try Profile.fetchOne(db, id: profile.id) else {
return
}
guard
let latestProfileKey: OWSAES256Key = latestProfile.profileEncryptionKey,
!latestProfileKey.keyData.isEmpty,
latestProfileKey == profileKeyAtStart
else {
OWSLogger.warn("Ignoring avatar download for obsolete user profile.")
return
}
guard profileUrlStringAtStart == latestProfile.profilePictureUrl else {
OWSLogger.warn("Avatar url has changed during download.")
if latestProfile.profilePictureUrl?.isEmpty == false {
self.downloadAvatar(for: latestProfile)
}
return
}
guard let decryptedData: Data = decryptProfileData(data: data, key: profileKeyAtStart) else {
OWSLogger.warn("Avatar data for \(profile.id) could not be decrypted.")
return
}
try? decryptedData.write(to: URL(fileURLWithPath: filePath), options: [.atomic])
guard let image: UIImage = UIImage(contentsOfFile: filePath) else {
OWSLogger.warn("Avatar image for \(profile.id) could not be loaded.")
return
}
try? latestProfile
.with(profilePictureFileName: .update(fileName))
.update(db)
profileAvatarCache.mutate { $0[fileName] = image }
}
// Redundant but without reading 'backgroundTask' it will warn that the variable
// isn't used
if backgroundTask != nil { backgroundTask = nil }
}
.retainUntilComplete()
}
}
// MARK: - Current User Profile
public static func updateLocal(
profileName: String,
avatarImage: UIImage?,
requiredSync: Bool,
success: (() -> ())? = nil,
failure: ((Error) -> ())? = nil
) {
DispatchQueue.global(qos: .default).async {
// If the profile avatar was updated or removed then encrypt with a new profile key
// to ensure that other users know that our profile picture was updated
let newProfileKey: OWSAES256Key = OWSAES256Key.generateRandom()
guard let avatarImage: UIImage = avatarImage else {
// If we have no image then we need to make sure to remove it from the profile
GRDBStorage.shared.writeAsync(
updates: { db in
let existingProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
OWSLogger.verbose(existingProfile.profilePictureUrl != nil ?
"Updating local profile on service with cleared avatar." :
"Updating local profile on service with no avatar."
)
try? existingProfile
.with(
name: profileName,
profilePictureUrl: nil,
profilePictureFileName: nil,
profileEncryptionKey: (existingProfile.profilePictureUrl != nil ?
.update(newProfileKey) :
.existing
)
)
.save(db)
// Remove any cached avatar image value
if let fileName: String = existingProfile.profilePictureFileName {
profileAvatarCache.mutate { $0[fileName] = nil }
}
},
completion: { _, _ in
SNLog("Successfully updated service with profile.")
DispatchQueue.main.async {
success?()
}
}
)
return
}
// If we have a new avatar image, we must first:
//
// * Encode it to JPEG.
// * Write it to disk.
// * Encrypt it
// * Upload it to asset service
// * Send asset service info to Signal Service
OWSLogger.verbose("Updating local profile on service with new avatar.")
let maxAvatarBytes: UInt = (5 * 1000 * 1000)
var image: UIImage = avatarImage
if image.size.width != maxAvatarDiameter || image.size.height != maxAvatarDiameter {
// To help ensure the user is being shown the same cropping of their avatar as
// everyone else will see, we want to be sure that the image was resized before this point.
SNLog("Avatar image should have been resized before trying to upload")
image = image.resizedImage(toFillPixelSize: CGSize(width: maxAvatarDiameter, height: maxAvatarDiameter))
}
guard let data: Data = image.jpegData(compressionQuality: 0.95) else {
DispatchQueue.main.async {
SNLog("Updating service with profile failed.")
failure?(.avatarWriteFailed)
}
return
}
guard data.count <= maxAvatarBytes else {
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
// be able to fit our profile photo (eg. generating pure noise at our resolution
// compresses to ~200k)
DispatchQueue.main.async {
SNLog("Suprised to find profile avatar was too large. Was it scaled properly? image: \(image)")
SNLog("Updating service with profile failed.")
failure?(.avatarImageTooLarge)
}
return
}
let fileName: String = UUID().uuidString.appendingFileExtension("jpg")
let filePath: String = OWSUserProfile.profileAvatarFilepath(withFilename: fileName)
// Write the avatar to disk
do { try data.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) }
catch {
DispatchQueue.main.async {
SNLog("Updating service with profile failed.")
failure?(.avatarWriteFailed)
}
return
}
// Encrypt the avatar for upload
guard let encryptedAvatarData: Data = encryptProfileData(data: data, key: newProfileKey) else {
DispatchQueue.main.async {
SNLog("Updating service with profile failed.")
failure?(.avatarEncryptionFailed)
}
return
}
// Upload the avatar to the FileServer
FileServerAPIV2
.upload(encryptedAvatarData)
.done { fileId in
let downloadUrl: String = "\(FileServerAPIV2.server)/files/\(fileId)"
UserDefaults.standard[.lastProfilePictureUpload] = Date()
GRDBStorage.shared.writeAsync(
updates: { db in
try? Profile
.fetchOrCreateCurrentUser(db)
.with(
name: profileName,
profilePictureUrl: .update(downloadUrl),
profilePictureFileName: .update(fileName),
profileEncryptionKey: .update(newProfileKey)
)
.save(db)
},
completion: { _, _ in
// Update the cached avatar image value
profileAvatarCache.mutate { $0[fileName] = avatarImage }
DispatchQueue.main.async {
SNLog("Successfully updated service with profile.")
success?()
}
}
)
}
.recover { error in
DispatchQueue.main.async {
SNLog("Updating service with profile failed.")
let isMaxFileSizeExceeded: Bool = ((error as? FileServerAPIV2.Error) == FileServerAPIV2.Error.maxFileSizeExceeded)
failure?(isMaxFileSizeExceeded ?
.avatarUploadMaxFileSizeExceeded :
.avatarUploadFailed
)
}
}
.retainUntilComplete()
}
}
}
// MARK: - Objective-C Support
@objc(SMKProfileManager)
public class SMKProfileManager: NSObject {
@objc public static func profileAvatar(recipientId: String) -> UIImage? {
return ProfileManager.profileAvatar(for: recipientId)
}
@objc public static func updateLocal(profileName: String, avatarImage: UIImage?, requiresSync: Bool) {
ProfileManager.updateLocal(profileName: profileName, avatarImage: avatarImage, requiredSync: requiresSync)
}
}

Some files were not shown because too many files have changed in this diff Show More