Separate streams for attachment upload/download and bug fixes

• Updated the code to stop sending legacy PNs outside of legacy group conversations
• Updated the logger logic to clean things up and use the local date/time (with time zone info) to ease debugging user reports
• Fixed an issue where messages in a community could incorrectly accept disappearing message settings
• Fixed an issue where duplicate messages could be sent in some cases
• Fixed an issue where the conversation might not scroll to the bottom after sending an attachment
• Fixed an issue where attachment encryption was happening in a db write thread
pull/976/head
Morgan Pretty 11 months ago
parent 6751a9c5ff
commit a91024f0bb

@ -1 +1 @@
Subproject commit b20b0ffce08da126edbad7693a4411170a00b0ce
Subproject commit 702302626d401d6954e67b60b28805c785be7a7c

@ -529,7 +529,7 @@
FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */; };
FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; };
FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */; };
FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* NetworkType.swift */; };
FD23CE1B2A651E6D0000B97C /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* Network.swift */; };
FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1E2A65269C0000B97C /* Crypto.swift */; };
FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */; };
FD23CE242A675C440000B97C /* Crypto+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */; };
@ -571,7 +571,6 @@
FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A348255781F400338F3E /* AttachmentDownloadJob.swift */; };
FD245C682850666300B966DD /* Message+Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A30825574D8400338F3E /* Message+Destination.swift */; };
FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */; };
FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87EF17026367CF800124B3C /* FileServerAPI.swift */; };
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
@ -655,6 +654,8 @@
FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C7306284F103B0029977D /* MessageReceiver+MessageRequests.swift */; };
FD5C7309285007920029977D /* BlindedIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C7308285007920029977D /* BlindedIdLookup.swift */; };
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; };
FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* AppVersionResponse.swift */; };
FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */; };
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; };
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
@ -754,7 +755,6 @@
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */; };
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; };
FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; };
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; };
FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; };
FD83DCDD2A739D350065FFAE /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; };
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; };
@ -834,7 +834,6 @@
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; };
FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; };
FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */; };
FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; };
FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; };
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; };
FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; };
@ -842,7 +841,6 @@
FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* Capabilities.swift */; };
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386827B4E6B700C60D73 /* String+Utlities.swift */; };
FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; };
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; };
FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; };
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */; };
@ -942,7 +940,7 @@
FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */; };
FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */; };
FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */; };
FDF848E629405D6E007DCAE5 /* OnionRequestAPIDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */; };
FDF848E629405D6E007DCAE5 /* Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E129405D6E007DCAE5 /* Destination.swift */; };
FDF848EB29405E4F007DCAE5 /* Network+OnionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E829405E4E007DCAE5 /* Network+OnionRequest.swift */; };
FDF848EF294067E4007DCAE5 /* URLResponse+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */; };
FDF848F129406A30007DCAE5 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F029406A30007DCAE5 /* Format.swift */; };
@ -1413,7 +1411,6 @@
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = "<group>"; };
B879D448247E1BE300DB3608 /* PathVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathVC.swift; sourceTree = "<group>"; };
B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = "<group>"; };
B87EF17026367CF800124B3C /* FileServerAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = "<group>"; };
B87EF18026377A1D00124B3C /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.swift; sourceTree = "<group>"; };
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = "<group>"; };
B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = "<group>"; };
@ -1735,7 +1732,7 @@
FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Utilities.swift"; sourceTree = "<group>"; };
FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = "<group>"; };
FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationRequirement.swift; sourceTree = "<group>"; };
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = "<group>"; };
FD23CE1A2A651E6D0000B97C /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = "<group>"; };
FD23CE1E2A65269C0000B97C /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = "<group>"; };
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+Crypto.swift"; sourceTree = "<group>"; };
FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionMessagingKit.swift"; sourceTree = "<group>"; };
@ -1901,7 +1898,6 @@
FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSpec.swift; sourceTree = "<group>"; };
FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesSpec.swift; sourceTree = "<group>"; };
FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = "<group>"; };
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = "<group>"; };
FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = "<group>"; };
FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryWithDependencies.swift; sourceTree = "<group>"; };
FD848B86283B844B000E298B /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; };
@ -2019,7 +2015,7 @@
FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = "<group>"; };
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = "<group>"; };
FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = "<group>"; };
FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = "<group>"; };
FDC4383727B3863200C60D73 /* AppVersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionResponse.swift; sourceTree = "<group>"; };
FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = "<group>"; };
FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessage.swift; sourceTree = "<group>"; };
@ -2130,7 +2126,7 @@
FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = "<group>"; };
FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = "<group>"; };
FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIError.swift; sourceTree = "<group>"; };
FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIDestination.swift; sourceTree = "<group>"; };
FDF848E129405D6E007DCAE5 /* Destination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Destination.swift; sourceTree = "<group>"; };
FDF848E829405E4E007DCAE5 /* Network+OnionRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Network+OnionRequest.swift"; sourceTree = "<group>"; };
FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = "<group>"; };
FDF848F029406A30007DCAE5 /* Format.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Format.swift; path = "SessionUIKit/Style Guide/Format.swift"; sourceTree = SOURCE_ROOT; };
@ -2693,7 +2689,7 @@
FDC438B227BB15B400C60D73 /* ResponseInfo.swift */,
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */,
FDF8487629405906007DCAE5 /* NetworkError.swift */,
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */,
FD23CE1A2A651E6D0000B97C /* Network.swift */,
FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */,
);
path = Networking;
@ -3331,16 +3327,6 @@
path = "Open Groups";
sourceTree = "<group>";
};
C3A7215C2558C0AC0043A11F /* File Server */ = {
isa = PBXGroup;
children = (
FDC4383227B385B200C60D73 /* Models */,
FD83B9CA27D179AF005E1583 /* Types */,
B87EF17026367CF800124B3C /* FileServerAPI.swift */,
);
path = "File Server";
sourceTree = "<group>";
};
C3BBE0B32554F0D30050F1E3 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -3435,12 +3421,10 @@
children = (
C3C2A7802553AA6300C340D1 /* Protos */,
C3C2A70A25539DF900C340D1 /* Meta */,
FDC4384D27B47FD600C60D73 /* Common Networking */,
B8DE1FB226C22F1F0079C9CE /* Calls */,
C32C5BCB256DC818003C73A2 /* Database */,
C300A5BB2554AFFB00555489 /* Messages */,
C352A2F325574B3300338F3E /* Jobs */,
C3A7215C2558C0AC0043A11F /* File Server */,
C3A721332558BDDF0043A11F /* Open Groups */,
C300A5F02554B08500555489 /* Sending & Receiving */,
FD8ECF7529340F4800C0D1BB /* LibSession */,
@ -4212,14 +4196,6 @@
path = Models;
sourceTree = "<group>";
};
FD83B9CA27D179AF005E1583 /* Types */ = {
isa = PBXGroup;
children = (
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */,
);
path = Types;
sourceTree = "<group>";
};
FD8ECF7529340F4800C0D1BB /* LibSession */ = {
isa = PBXGroup;
children = (
@ -4329,6 +4305,7 @@
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */,
FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */,
FDC4381627B32EC700C60D73 /* Personalization.swift */,
7B81682228A4C1210069F315 /* UpdateTypes.swift */,
);
path = Types;
sourceTree = "<group>";
@ -4373,31 +4350,6 @@
path = Models;
sourceTree = "<group>";
};
FDC4383227B385B200C60D73 /* Models */ = {
isa = PBXGroup;
children = (
FDC4383727B3863200C60D73 /* VersionResponse.swift */,
);
path = Models;
sourceTree = "<group>";
};
FDC4384D27B47FD600C60D73 /* Common Networking */ = {
isa = PBXGroup;
children = (
FDC4385527B484AE00C60D73 /* Models */,
7B81682228A4C1210069F315 /* UpdateTypes.swift */,
);
path = "Common Networking";
sourceTree = "<group>";
};
FDC4385527B484AE00C60D73 /* Models */ = {
isa = PBXGroup;
children = (
FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */,
);
path = Models;
sourceTree = "<group>";
};
FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */ = {
isa = PBXGroup;
children = (
@ -4514,7 +4466,6 @@
FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */,
FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */,
FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */,
FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */,
FD7F74832BB283DF006DDFD8 /* SwarmDrainBehaviour.swift */,
FD7F74812BB283CE006DDFD8 /* UpdatableTimestamp.swift */,
FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */,
@ -4525,6 +4476,7 @@
FDF8489229405C1B007DCAE5 /* Networking */ = {
isa = PBXGroup;
children = (
FDF848E129405D6E007DCAE5 /* Destination.swift */,
FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */,
FD7F747F2BB283A9006DDFD8 /* Request+SnodeAPI.swift */,
FD7F74852BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift */,
@ -4569,6 +4521,8 @@
FDF848A029405C5A007DCAE5 /* OxenDaemonRPCRequest.swift */,
FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */,
FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */,
FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */,
FDC4383727B3863200C60D73 /* AppVersionResponse.swift */,
);
path = Models;
sourceTree = "<group>";
@ -5909,12 +5863,13 @@
FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */,
FD7F74862BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift in Sources */,
FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */,
FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */,
FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */,
FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */,
FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */,
FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */,
FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */,
FDF848E629405D6E007DCAE5 /* OnionRequestAPIDestination.swift in Sources */,
FDF848E629405D6E007DCAE5 /* Destination.swift in Sources */,
FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */,
FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */,
FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */,
@ -5940,6 +5895,7 @@
FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */,
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */,
FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */,
FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */,
FDF848EB29405E4F007DCAE5 /* Network+OnionRequest.swift in Sources */,
FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */,
);
@ -5961,7 +5917,7 @@
FD7F746E2BB2766D006DDFD8 /* PreparedRequest.swift in Sources */,
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */,
FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */,
FD23CE1B2A651E6D0000B97C /* Network.swift in Sources */,
FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */,
FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */,
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
@ -6127,7 +6083,6 @@
7B7AD41F2A5512CA00469FB1 /* GetExpirationJob.swift in Sources */,
FD7F747A2BB277E1006DDFD8 /* Request+OpenGroupAPI.swift in Sources */,
FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */,
FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */,
FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */,
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */,
@ -6198,7 +6153,6 @@
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */,
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */,
FD43EE9D297A5190009C87C5 /* LibSession+UserGroups.swift in Sources */,
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */,
FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */,
FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */,
FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */,
@ -6217,7 +6171,6 @@
B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */,
FD245C53285065DB00B966DD /* ProximityMonitoringManager.swift in Sources */,
FD245C55285065E500B966DD /* OpenGroupManager.swift in Sources */,
FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */,
C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */,
FD716E682850318E00C96BF4 /* CallMode.swift in Sources */,
FD09799527FE7B8E00936362 /* Interaction.swift in Sources */,
@ -6270,7 +6223,6 @@
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */,
FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */,
B806ECA126C4A7E4008BDA44 /* WebRTCSession+UI.swift in Sources */,
FD72BD9A2BDF5EEA00CF6CF6 /* Message+Origin.swift in Sources */,
FD432432299C6933008A0213 /* _011_AddPendingReadReceipts.swift in Sources */,

@ -233,6 +233,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
let interaction: Interaction? = try? Interaction(
messageUuid: self.uuid,
threadId: sessionId,
threadVariant: thread.variant,
authorId: getUserHexEncodedPublicKey(db),
variant: .infoCall,
body: String(data: messageInfoData, encoding: .utf8),

@ -907,9 +907,11 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
// If we just sent a message then we want to jump to the bottom of the conversation instantly
if didSendMessageBeforeUpdate {
// We need to dispatch to the next run loop because it seems trying to scroll immediately after
// triggering a 'reloadData' doesn't work
DispatchQueue.main.async { [weak self] in
// We need to dispatch to the next run loop after a slight delay because it seems trying to scroll
// immediately after triggering a 'reloadData' doesn't work and it's possible (eg. when uploading)
// for two updates to come through in rapid succession which will result in two updates, the second
// which stops the scroll from working
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in
self?.tableView.layoutIfNeeded()
self?.scrollToBottom(isAnimated: false)

@ -544,6 +544,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser()
let interaction: Interaction = Interaction(
threadId: threadData.threadId,
threadVariant: threadData.threadVariant,
authorId: (threadData.currentUserBlinded15PublicKey ?? threadData.currentUserPublicKey),
variant: .standardOutgoing,
body: text,

@ -785,6 +785,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi
let interaction: Interaction = try Interaction(
threadId: thread.id,
threadVariant: thread.variant,
authorId: currentUserSessionId,
variant: .standardOutgoing,
timestampMs: SnodeAPI.currentOffsetTimestampMs(),

@ -503,7 +503,7 @@ class CaptureOutput {
movieOutput.movieFragmentInterval = CMTime.invalid
// Ensure the recorded movie can't go over the maximum file server size
movieOutput.maxRecordedFileSize = Int64(FileServerAPI.maxFileSize)
movieOutput.maxRecordedFileSize = Int64(Network.maxFileSize)
}
var photoOutput: AVCaptureOutput? {

@ -135,7 +135,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func applicationWillEnterForeground(_ application: UIApplication) {
Log.enterForeground()
Log.appResumedExecution()
Log.info("[AppDelegate] applicationWillEnterForeground.")
/// **Note:** We _shouldn't_ need to call this here but for some reason the OS doesn't seems to
@ -221,7 +221,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
Log.info("applicationDidReceiveMemoryWarning")
Log.warn("applicationDidReceiveMemoryWarning")
}
func applicationWillTerminate(_ application: UIApplication) {
@ -281,6 +281,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// MARK: - Background Fetching
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Log.appResumedExecution()
Log.info("Starting background fetch.")
Storage.resumeDatabaseAccess()
LibSession.resumeNetworkAccess()

@ -510,6 +510,7 @@ class NotificationActionHandler {
.writePublisher { db in
let interaction: Interaction = try Interaction(
threadId: threadId,
threadVariant: thread.variant,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: replyText,

@ -312,12 +312,13 @@ public enum PushRegistrationError: Error {
let interaction: Interaction = try Interaction(
messageUuid: uuid,
threadId: thread.id,
threadVariant: thread.variant,
authorId: caller,
variant: .infoCall,
body: messageInfoString,
timestampMs: timestampMs
)
.withDisappearingMessagesConfiguration(db)
.withDisappearingMessagesConfiguration(db, threadVariant: thread.variant)
.inserted(db)
call.callInteractionId = interaction.id

@ -29,7 +29,7 @@ class ScreenLockUI {
return
}
Logger.info("unlockButtonWasTapped")
Log.info("unlockButtonWasTapped")
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
@ -88,17 +88,17 @@ class ScreenLockUI {
private var desiredUIState: ScreenLockViewController.State {
if isScreenLockLocked {
if appIsInactiveOrBackground {
Logger.verbose("desiredUIState: screen protection 1.")
Log.trace("desiredUIState: screen protection 1.")
return .protection
}
Logger.verbose("desiredUIState: screen lock 2.")
Log.trace("desiredUIState: screen lock 2.")
return (isShowingScreenLockUI ? .protection : .lock)
}
if !self.appIsInactiveOrBackground {
// App is inactive or background.
Logger.verbose("desiredUIState: none 3.");
Log.trace("desiredUIState: none 3.");
return .none;
}
@ -106,7 +106,7 @@ class ScreenLockUI {
return .none;
}
Logger.verbose("desiredUIState: screen protection 4.")
Log.trace("desiredUIState: screen protection 4.")
return .protection;
}
@ -181,17 +181,17 @@ class ScreenLockUI {
//
// We don't need to try to lock the screen lock;
// It will be initialized by `setupWithRootWindow`.
Logger.verbose("tryToActivateScreenLockUponBecomingActive NO 0")
Log.trace("tryToActivateScreenLockUponBecomingActive NO 0")
return
}
guard Storage.shared[.isScreenLockEnabled] else {
// Screen lock is not enabled.
Logger.verbose("tryToActivateScreenLockUponBecomingActive NO 1")
Log.trace("tryToActivateScreenLockUponBecomingActive NO 1")
return;
}
guard !isScreenLockLocked else {
// Screen lock is already activated.
Logger.verbose("tryToActivateScreenLockUponBecomingActive NO 2")
Log.trace("tryToActivateScreenLockUponBecomingActive NO 2")
return;
}
@ -211,7 +211,7 @@ class ScreenLockUI {
}
let desiredUIState: ScreenLockViewController.State = self.desiredUIState
Logger.verbose("ensureUI: \(desiredUIState)")
Log.trace("ensureUI: \(desiredUIState)")
// Show the "iOS auth UI to unlock" if necessary.
if desiredUIState == .lock && !didLastUnlockAttemptFail {
@ -227,25 +227,25 @@ class ScreenLockUI {
guard !isShowingScreenLockUI else { return } // We're already showing the auth UI; abort
guard !appIsInactiveOrBackground else { return } // Never show the auth UI unless active
Logger.info("try to unlock screen lock")
Log.info("try to unlock screen lock")
isShowingScreenLockUI = true
ScreenLock.shared.tryToUnlockScreenLock(
success: { [weak self] in
Logger.info("unlock screen lock succeeded.")
Log.info("unlock screen lock succeeded.")
self?.isShowingScreenLockUI = false
self?.isScreenLockLocked = false
self?.didUnlockJustSucceed = true
self?.ensureUI()
},
failure: { [weak self] error in
Logger.info("unlock screen lock failed.")
Log.info("unlock screen lock failed.")
self?.clearAuthUIWhenActive()
self?.didLastUnlockAttemptFail = true
self?.showScreenLockFailureAlert(message: error.localizedDescription)
},
unexpectedFailure: { [weak self] error in
Logger.info("unlock screen lock unexpectedly failed.")
Log.info("unlock screen lock unexpectedly failed.")
// Local Authentication isn't working properly.
// This isn't covered by the docs or the forums but in practice
@ -255,7 +255,7 @@ class ScreenLockUI {
}
},
cancel: { [weak self] in
Logger.info("unlock screen lock cancelled.")
Log.info("unlock screen lock cancelled.")
self?.clearAuthUIWhenActive()
self?.didLastUnlockAttemptFail = true
@ -300,7 +300,7 @@ class ScreenLockUI {
return
}
Logger.info("unlockButtonWasTapped")
Log.info("unlockButtonWasTapped")
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
@ -360,7 +360,7 @@ class ScreenLockUI {
/// Whenever the device date/time is edited by the user, trigger screen lock immediately if enabled.
@objc private func clockDidChange() {
Logger.info("clock did change")
Log.info("clock did change")
guard Singleton.appReadiness.isAppReady else {
// It's not safe to access OWSScreenLock.isScreenLockEnabled
@ -368,7 +368,7 @@ class ScreenLockUI {
//
// We don't need to try to lock the screen lock;
// It will be initialized by `setupWithRootWindow`.
Logger.verbose("clockDidChange 0")
Log.trace("clockDidChange 0")
return;
}

@ -115,6 +115,7 @@ enum MockDataGenerator {
_ = try! Interaction(
threadId: thread.id,
threadVariant: thread.variant,
authorId: (isIncoming ? randomSessionId : userSessionId),
variant: (isIncoming ? .standardIncoming : .standardOutgoing),
body: (0..<messageWords)
@ -238,6 +239,7 @@ enum MockDataGenerator {
_ = try! Interaction(
threadId: thread.id,
threadVariant: thread.variant,
authorId: senderId,
variant: (senderId != userSessionId ? .standardIncoming : .standardOutgoing),
body: (0..<messageWords)
@ -363,6 +365,7 @@ enum MockDataGenerator {
_ = try! Interaction(
threadId: thread.id,
threadVariant: thread.variant,
authorId: senderId,
variant: (senderId != userSessionId ? .standardIncoming : .standardOutgoing),
body: (0..<messageWords)

@ -1,7 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
enum UpdateTypes: String {
case reaction = "r"
}

@ -219,17 +219,4 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
let contact: Contact
let profile: Profile?
}
struct GroupInfo: FetchableRecord, Decodable, ColumnExpressible {
typealias Columns = CodingKeys
enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case closedGroup
case disappearingMessagesConfiguration
case groupMembers
}
let closedGroup: ClosedGroup
let disappearingMessagesConfiguration: DisappearingMessagesConfiguration?
let groupMembers: [GroupMember]
}
}

@ -1065,7 +1065,7 @@ extension Attachment {
// dependant on the attachment being uploaded (in this case the attachment has
// already been uploaded so just succeed)
guard state != .uploaded else {
return Just(Attachment.fileId(for: self.downloadUrl))
return Just(self.downloadUrl)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -1079,8 +1079,8 @@ extension Attachment {
let attachmentId: String = self.id
return Storage.shared
.writePublisher { db -> (Network.PreparedRequest<FileUploadResponse>?, String?, Data?, Data?) in
return Just(())
.tryFlatMap { _ -> AnyPublisher<(Network.Destination?, String?, Data?, Data?), Error> in
// If the attachment is a downloaded attachment, check if it came from
// the server and if so just succeed immediately (no use re-uploading
// an attachment that is already present on the server) - or if we want
@ -1096,11 +1096,13 @@ extension Attachment {
digest == nil
else {
// Save the final upload info
_ = try? Attachment
.filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploaded))
return (nil, Attachment.fileId(for: self.downloadUrl), nil, nil)
return Storage.shared.writePublisher { db -> (Network.Destination?, String?, Data?, Data?) in
_ = try? Attachment
.filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploaded))
return (nil, self.downloadUrl, nil, nil)
}
}
var encryptionKey: NSData = NSData()
@ -1118,38 +1120,35 @@ extension Attachment {
// Check the file size
SNLog("File size: \(data.count) bytes.")
if data.count > FileServerAPI.maxFileSize { throw NetworkError.maxFileSizeExceeded }
// Update the attachment to the 'uploading' state
_ = try? Attachment
.filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading))
if data.count > Network.maxFileSize { throw NetworkError.maxFileSizeExceeded }
// We need database access for OpenGroup uploads so generate prepared data
let preparedSendData: Network.PreparedRequest<FileUploadResponse>? = try {
return Storage.shared.writePublisher { db -> (Network.Destination?, String?, Data?, Data?) in
// Update the attachment to the 'uploading' state
_ = try? Attachment
.filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading))
switch destination {
case .openGroup(let openGroup):
return try OpenGroupAPI
.preparedUploadFile(
return (
try OpenGroupAPI.uploadDestination(
db,
bytes: data.bytes,
to: openGroup.roomToken,
on: openGroup.server,
data: data,
openGroup: openGroup,
using: dependencies
)
),
nil,
nil,
nil
)
default: return nil
default:
return (.fileServer, nil, encryptionKey as Data, digest as Data)
}
}()
return (
preparedSendData,
nil,
(destination.shouldEncrypt ? encryptionKey as Data : nil),
(destination.shouldEncrypt ? digest as Data : nil)
)
}
}
.flatMap { preparedRequest, existingFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in
.tryFlatMap { maybeDestination, existingFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in
// No need to upload if the file was already uploaded
if let fileId: String = existingFileId {
return Just((fileId, encryptionKey, digest))
@ -1157,24 +1156,31 @@ extension Attachment {
.eraseToAnyPublisher()
}
switch destination {
case .openGroup:
return preparedRequest.send(using: dependencies)
.map { _, response -> (String, Data?, Data?) in (response.id, encryptionKey, digest) }
.eraseToAnyPublisher()
case .fileServer:
return FileServerAPI.upload(data)
.map { response -> (String, Data?, Data?) in (response.id, encryptionKey, digest) }
.eraseToAnyPublisher()
}
guard let destination: Network.Destination = maybeDestination else { throw NetworkError.invalidURL }
return LibSession.uploadToServer(data, to: destination, fileName: nil, using: dependencies)
.map { _, response -> (String, Data?, Data?) in (response.id, encryptionKey, digest) }
.eraseToAnyPublisher()
}
.flatMap { fileId, encryptionKey, digest -> AnyPublisher<String?, Error> in
.tryFlatMap { fileId, encryptionKey, digest -> AnyPublisher<String?, Error> in
let downloadUrl: URL? = try fileId.map { fileId in
switch destination {
case .fileServer: return try Network.fileServerDownloadUrlFor(fileId: fileId)
case .openGroup(let openGroup):
return try OpenGroupAPI
.downloadUrlFor(
fileId: fileId,
server: openGroup.server,
roomToken: openGroup.roomToken
)
}
}
/// Save the final upload info
///
/// **Note:** We **MUST** use the `.with` function here to ensure the `isValid` flag is
/// updated correctly
Storage.shared
return Storage.shared
.writePublisher { db in
try self
.with(
@ -1184,13 +1190,13 @@ extension Attachment {
self.creationTimestamp ??
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
),
downloadUrl: fileId.map { "\(FileServerAPI.server)/file/\($0)" },
downloadUrl: downloadUrl?.absoluteString,
encryptionKey: encryptionKey,
digest: digest
)
.saved(db)
}
.map { _ in fileId }
.map { _ in downloadUrl?.absoluteString }
.eraseToAnyPublisher()
}
.handleEvents(

@ -297,7 +297,8 @@ public extension DisappearingMessagesConfiguration {
)
)
let messageExpirationInfo: Message.MessageExpirationInfo = Message.getMessageExpirationInfo(
wasRead: wasRead,
threadVariant: threadVariant,
wasRead: wasRead,
serverExpirationTimestamp: serverExpirationTimestamp,
expiresInSeconds: self.durationSeconds,
expiresStartedAtMs: (self.type == .disappearAfterSend) ? Double(timestampMs) : nil
@ -305,6 +306,7 @@ public extension DisappearingMessagesConfiguration {
let interaction = try Interaction(
serverHash: serverHash,
threadId: threadId,
threadVariant: threadVariant,
authorId: authorId,
variant: .infoDisappearingMessagesUpdate,
body: self.messageInfoString(
@ -321,6 +323,7 @@ public extension DisappearingMessagesConfiguration {
Message.updateExpiryForDisappearAfterReadMessages(
db,
threadId: threadId,
threadVariant: threadVariant,
serverHash: serverHash,
expiresInSeconds: messageExpirationInfo.expiresInSeconds,
expiresStartedAtMs: messageExpirationInfo.expiresStartedAtMs

@ -316,6 +316,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
serverHash: String? = nil,
messageUuid: String? = nil,
threadId: String,
threadVariant: SessionThread.Variant,
authorId: String,
variant: Variant,
body: String? = nil,
@ -346,8 +347,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
}()
self.wasRead = (wasRead || !variant.canBeUnread)
self.hasMention = hasMention
self.expiresInSeconds = expiresInSeconds
self.expiresStartedAtMs = expiresStartedAtMs
self.expiresInSeconds = (threadVariant != .community ? expiresInSeconds : nil)
self.expiresStartedAtMs = (threadVariant != .community ? expiresStartedAtMs : nil)
self.linkPreviewUrl = linkPreviewUrl
self.openGroupServerMessageId = openGroupServerMessageId
self.openGroupWhisperMods = openGroupWhisperMods
@ -477,30 +478,18 @@ public extension Interaction {
)
}
func withDisappearAfterReadIfNeeded(_ db: Database) -> Interaction {
if let config = try? DisappearingMessagesConfiguration.fetchOne(db, id: self.threadId) {
return self.withDisappearingMessagesConfiguration(
config: config.with(type: .disappearAfterRead)
)
}
func withDisappearingMessagesConfiguration(_ db: Database, threadVariant: SessionThread.Variant) -> Interaction {
guard threadVariant != .community else { return self }
return self
}
func withDisappearingMessagesConfiguration(_ db: Database) -> Interaction {
if let config = try? DisappearingMessagesConfiguration.fetchOne(db, id: self.threadId) {
return self.withDisappearingMessagesConfiguration(config: config)
return self.with(
expiresInSeconds: config.durationSeconds,
expiresStartedAtMs: (config.type == .disappearAfterSend ? Double(self.timestampMs) : nil)
)
}
return self
}
func withDisappearingMessagesConfiguration(config: DisappearingMessagesConfiguration?) -> Interaction {
return self.with(
expiresInSeconds: config?.durationSeconds,
expiresStartedAtMs: (config?.type == .disappearAfterSend ? Double(self.timestampMs) : nil)
)
}
}
// MARK: - GRDB Interactions

@ -1,122 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine
import SessionSnodeKit
import SessionUtilitiesKit
public enum FileServerAPI {
// MARK: - Settings
public static let oldServer = "http://88.99.175.227"
public static let oldServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
public static let server = "http://filev2.getsession.org"
public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
/// **Note:** The max file size is 10,000,000 bytes (rather than 10MiB which would be `(10 * 1024 * 1024)`), 10,000,000
/// exactly will be fine but a single byte more will result in an error
public static let maxFileSize = 10_000_000
/// Standard timeout is 10 seconds which is a little too short for file upload/download with slightly larger files
public static let fileDownloadTimeout: TimeInterval = 30
public static let fileUploadTimeout: TimeInterval = 60
// MARK: - File Storage
public static func upload(
_ file: Data,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<FileUploadResponse, Error> {
do {
return try prepareRequest(
request: Request(
method: .post,
server: server,
endpoint: Endpoint.file,
headers: [
.contentDisposition: "attachment",
.contentType: "application/octet-stream"
],
x25519PublicKey: serverPublicKey,
body: Array(file)
),
responseType: FileUploadResponse.self,
timeout: FileServerAPI.fileUploadTimeout,
using: dependencies
)
.send(using: dependencies)
.map { _, response in response }
.eraseToAnyPublisher()
}
catch { return Fail(error: error).eraseToAnyPublisher() }
}
public static func download(
fileId: String,
useOldServer: Bool,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Data, Error> {
do {
return try prepareRequest(
request: Request<NoBody, Endpoint>(
server: (useOldServer ? oldServer : server),
endpoint: .fileIndividual(fileId: fileId),
x25519PublicKey: (useOldServer ? oldServerPublicKey : serverPublicKey)
),
responseType: Data.self,
timeout: FileServerAPI.fileDownloadTimeout,
using: dependencies
)
.send(using: dependencies)
.map { _, data in data }
.eraseToAnyPublisher()
}
catch { return Fail(error: error).eraseToAnyPublisher() }
}
public static func getVersion(
_ platform: String,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<String, Error> {
do {
return try prepareRequest(
request: Request<NoBody, Endpoint>(
server: server,
endpoint: .sessionVersion,
queryParameters: [
.platform: platform
],
x25519PublicKey: serverPublicKey
),
responseType: VersionResponse.self,
timeout: Network.defaultTimeout,
using: dependencies
)
.send(using: dependencies)
.map { _, response in response.version }
.eraseToAnyPublisher()
}
catch { return Fail(error: error).eraseToAnyPublisher() }
}
// MARK: - Convenience
private static func prepareRequest<T: Encodable, R: Decodable>(
request: Request<T, Endpoint>,
responseType: R.Type,
retryCount: Int = 0,
timeout: TimeInterval,
using dependencies: Dependencies
) throws -> Network.PreparedRequest<R> {
return Network.PreparedRequest<R>(
request: request,
urlRequest: try request.generateUrlRequest(using: dependencies),
responseType: responseType,
retryCount: retryCount,
timeout: timeout
)
}
}

@ -1,13 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension FileServerAPI {
struct VersionResponse: Codable {
enum CodingKeys: String, CodingKey {
case version = "version"
}
public let version: String
}
}

@ -1,24 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit
extension FileServerAPI {
public enum Endpoint: EndpointType {
case file
case fileIndividual(fileId: String)
case sessionVersion
public static var name: String { "FileServerAPI.Endpoint" }
public var path: String {
switch self {
case .file: return "file"
case .fileIndividual(let fileId): return "file/\(fileId)"
case .sessionVersion: return "session_version"
}
}
}
}

@ -89,40 +89,28 @@ public enum AttachmentDownloadJob: JobExecutor {
Just(attachment.downloadUrl)
.setFailureType(to: Error.self)
.tryFlatMap { maybeDownloadUrl -> AnyPublisher<Data, Error> in
guard
let downloadUrl: String = maybeDownloadUrl,
let fileId: String = Attachment.fileId(for: downloadUrl)
else { throw AttachmentDownloadError.invalidUrl }
guard let downloadUrl: URL = maybeDownloadUrl.map({ URL(string: $0) }) else {
throw AttachmentDownloadError.invalidUrl
}
return Storage.shared
.readPublisher { db -> Network.PreparedRequest<Data>? in
try OpenGroup.fetchOne(db, id: threadId)
.map { openGroup in
try OpenGroupAPI
.preparedDownloadFile(
db,
fileId: fileId,
from: openGroup.roomToken,
on: openGroup.server,
using: dependencies
)
}
}
.flatMap { maybePreparedRequest -> AnyPublisher<Data, Error> in
guard let preparedRequest: Network.PreparedRequest<Data> = maybePreparedRequest else {
return FileServerAPI
.download(
fileId: fileId,
useOldServer: downloadUrl.contains(FileServerAPI.oldServer)
.readPublisher { db -> Network.Destination in
switch try? OpenGroup.fetchOne(db, id: threadId) {
case .some(let openGroup):
return try OpenGroupAPI.downloadDestination(
db,
url: downloadUrl,
openGroup: openGroup,
using: dependencies
)
.eraseToAnyPublisher()
case .none: return .fileServer(downloadUrl: downloadUrl)
}
return preparedRequest
.send(using: dependencies)
.map { _, data in data }
.eraseToAnyPublisher()
}
.flatMap { downloadDestination -> AnyPublisher<(ResponseInfoType, Data), Error> in
LibSession.downloadFile(from: downloadDestination)
}
.map { _, data in data }
.eraseToAnyPublisher()
}
.subscribe(on: queue)

@ -24,7 +24,7 @@ public enum MessageSendJob: JobExecutor {
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
SNLog("[MessageSendJob] Failing due to missing details")
Log.error("[MessageSendJob] Failing due to missing details")
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
@ -45,7 +45,7 @@ public enum MessageSendJob: JobExecutor {
let jobId: Int64 = job.id,
let interactionId: Int64 = job.interactionId
else {
SNLog("[MessageSendJob] Failing due to missing details")
Log.error("[MessageSendJob] Failing due to missing details")
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
@ -57,7 +57,7 @@ public enum MessageSendJob: JobExecutor {
// If the original interaction no longer exists then don't bother sending the message (ie. the
// message was deleted before it even got sent)
guard try Interaction.exists(db, id: interactionId) else {
SNLog("[MessageSendJob] Failing due to missing interaction")
Log.warn("[MessageSendJob] Failing due to missing interaction")
return (StorageError.objectNotFound, [], [])
}
@ -73,7 +73,7 @@ public enum MessageSendJob: JobExecutor {
// If there were failed attachments then this job should fail (can't send a
// message which has associated attachments if the attachments fail to upload)
guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else {
SNLog("[MessageSendJob] Failing due to failed attachment upload")
Log.info("[MessageSendJob] Failing due to failed attachment upload")
return (AttachmentError.notUploaded, [], fileIds)
}
@ -152,7 +152,7 @@ public enum MessageSendJob: JobExecutor {
}
}
SNLog("[MessageSendJob] Deferring due to pending attachment uploads")
Log.info("[MessageSendJob] Deferring due to pending attachment uploads")
return deferred(job, dependencies)
}
@ -162,7 +162,7 @@ public enum MessageSendJob: JobExecutor {
// Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error
let originalSentTimestamp: UInt64? = details.message.sentTimestamp
let startTime: CFTimeInterval = CACurrentMediaTime()
let startTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
/// Perform the actual message sending - this will timeout if the entire process takes longer than `HTTP.defaultTimeout * 2`
/// which can occur if it needs to build a new onion path (which doesn't actually have any limits so can take forever in rare cases)
@ -184,34 +184,15 @@ public enum MessageSendJob: JobExecutor {
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: queue, using: dependencies)
.receive(on: queue, using: dependencies)
.timeout(.milliseconds(Int(Network.defaultTimeout * 2 * 1000)), scheduler: queue, customError: {
MessageSenderError.sendJobTimeout
})
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: success(job, false, dependencies)
case .finished:
Log.info("[MessageSendJob] Completed sending \(type(of: details.message)) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s).")
success(job, false, dependencies)
case .failure(let error):
switch error {
case MessageSenderError.sendJobTimeout:
SNLog("[MessageSendJob] Failed after \(CACurrentMediaTime() - startTime)s: \(error).")
// In this case the `MessageSender` process gets cancelled so we need to
// call `handleFailedMessageSend` to update the statuses correctly
dependencies.storage.write(using: dependencies) { db in
MessageSender.handleFailedMessageSend(
db,
message: details.message,
destination: details.destination,
with: .other(error),
interactionId: job.interactionId,
using: dependencies
)
}
default:
SNLog("[MessageSendJob] Couldn't send message due to error: \(error)")
}
Log.info("[MessageSendJob] Failed to send \(type(of: details.message)) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s) due to error: \(error).")
// Actual error handling
switch (error, details.message) {
@ -222,18 +203,15 @@ public enum MessageSendJob: JobExecutor {
failure(job, error, true, dependencies)
case (SnodeAPIError.clockOutOfSync, _):
SNLog("[MessageSendJob] \(originalSentTimestamp != nil ? "Permanently Failing" : "Failing") to send \(type(of: details.message)) due to clock out of sync issue.")
Log.error("[MessageSendJob] \(originalSentTimestamp != nil ? "Permanently Failing" : "Failing") to send \(type(of: details.message)) due to clock out of sync issue.")
failure(job, error, (originalSentTimestamp != nil), dependencies)
// Don't bother retrying (it can just send a new one later but allowing retries
// can result in a large number of `MessageSendJobs` backing up)
case (_, is TypingIndicator):
SNLog("[MessageSendJob] Failed to send \(type(of: details.message)).")
failure(job, error, true, dependencies)
default:
SNLog("[MessageSendJob] Failed to send \(type(of: details.message)).")
if details.message is VisibleMessage {
guard
let interactionId: Int64 = job.interactionId,

@ -14,6 +14,7 @@ extension Message {
}
public static func getMessageExpirationInfo(
threadVariant: SessionThread.Variant,
wasRead: Bool,
serverExpirationTimestamp: TimeInterval?,
expiresInSeconds: TimeInterval?,
@ -21,6 +22,8 @@ extension Message {
) -> MessageExpirationInfo {
var shouldUpdateExpiry: Bool = false
let expiresStartedAtMs: Double? = {
guard threadVariant != .community else { return nil }
// Disappear after sent
guard expiresStartedAtMs == nil else {
return expiresStartedAtMs
@ -60,18 +63,18 @@ extension Message {
public static func getExpirationForOutgoingDisappearingMessages(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
variant: Interaction.Variant,
serverHash: String?,
expireInSeconds: TimeInterval?
) {
guard
threadVariant != .community,
variant == .standardOutgoing,
let serverHash: String = serverHash,
let expireInSeconds: TimeInterval = expireInSeconds,
expireInSeconds > 0
else {
return
}
else { return }
let startedAtTimestampMs: Double = Double(SnodeAPI.currentOffsetTimestampMs())
@ -92,17 +95,17 @@ extension Message {
public static func updateExpiryForDisappearAfterReadMessages(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
serverHash: String?,
expiresInSeconds: TimeInterval?,
expiresStartedAtMs: Double?
) {
guard
threadVariant != .community,
let serverHash: String = serverHash,
let expiresInSeconds: TimeInterval = expiresInSeconds,
let expiresStartedAtMs: Double = expiresStartedAtMs
else {
return
}
else { return }
let expirationTimestampMs: Int64 = Int64(expiresStartedAtMs + expiresInSeconds * 1000)
JobRunner.add(

@ -846,65 +846,71 @@ public enum OpenGroupAPI {
// MARK: - Files
/// Uploads a file to a room.
///
/// Takes the request as binary in the body and takes other properties (specifically the suggested filename) via submitted headers.
///
/// The user must have upload and posting permissions for the room. The file will have a default lifetime of 1 hour, which is extended
/// to 15 days (by default) when a post referencing the uploaded file is posted or edited.
public static func preparedUploadFile(
public static func uploadDestination(
_ db: Database,
bytes: [UInt8],
fileName: String? = nil,
to roomToken: String,
on server: String,
data: Data,
openGroup: OpenGroup,
using dependencies: Dependencies
) throws -> Network.PreparedRequest<FileUploadResponse> {
return try OpenGroupAPI
.prepareRequest(
request: Request(
db,
method: .post,
server: server,
endpoint: Endpoint.roomFile(roomToken),
headers: [
.contentDisposition: [ "attachment", fileName.map { "filename=\"\($0)\"" } ]
.compactMap{ $0 }
.joined(separator: "; "),
.contentType: "application/octet-stream"
],
body: bytes
),
responseType: FileUploadResponse.self,
timeout: FileServerAPI.fileUploadTimeout,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
) throws -> Network.Destination {
guard let url: URL = URL(string: "\(openGroup.server)/\(Endpoint.roomFile(openGroup.roomToken).path)") else {
throw NetworkError.invalidURL
}
return try .server(
url: url,
method: .post,
headers: nil,
x25519PublicKey: openGroup.publicKey
)
.signed(db, server: openGroup.server, data: data, using: dependencies)
}
/// Retrieves a file uploaded to the room.
///
/// Retrieves a file via its numeric id from the room, returning the file content directly as the binary response body. The file's suggested
/// filename (as provided by the uploader) is provided in the Content-Disposition header, if available.
public static func preparedDownloadFile(
public static func downloadUrlFor(
fileId: String,
server: String,
roomToken: String
) throws -> URL {
return (
try URL(string: "\(server)/\(Endpoint.roomFileIndividual(roomToken, fileId).path)") ??
{ throw NetworkError.invalidURL }()
)
}
public static func downloadDestination(
_ db: Database,
fileId: String,
from roomToken: String,
on server: String,
openGroup: OpenGroup,
using dependencies: Dependencies
) throws -> Network.PreparedRequest<Data> {
return try OpenGroupAPI
.prepareRequest(
request: Request<NoBody, Endpoint>(
db,
server: server,
endpoint: .roomFileIndividual(roomToken, fileId)
),
responseType: Data.self,
timeout: FileServerAPI.fileDownloadTimeout,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
) throws -> Network.Destination {
return try downloadDestination(
db,
url: try downloadUrlFor(fileId: fileId, server: openGroup.server, roomToken: openGroup.roomToken),
openGroup: openGroup,
using: dependencies
)
}
public static func downloadDestination(
_ db: Database,
url: URL,
openGroup: OpenGroup,
using dependencies: Dependencies
) throws -> Network.Destination {
return try .server(
url: try {
// FIXME: Remove this logic once the 'downloadUrl' for SOGS is being set correctly
guard
Network.isFileServerUrl(url: url),
let fileId: String = Attachment.fileId(for: url.absoluteString)
else { return url }
return try downloadUrlFor(fileId: fileId, server: openGroup.server, roomToken: openGroup.roomToken)
}(),
method: .get,
headers: nil,
x25519PublicKey: openGroup.publicKey
)
.signed(db, server: openGroup.server, data: nil, using: dependencies)
}
// MARK: - Inbox/Outbox (Message Requests)
@ -1273,6 +1279,68 @@ public enum OpenGroupAPI {
// MARK: - Authentication
fileprivate static func signatureHeaders(
_ db: Database,
url: URL,
method: HTTPMethod,
server: String,
serverPublicKey: String,
body: Data?,
forceBlinded: Bool,
using dependencies: Dependencies
) throws -> [HTTPHeader: String] {
let path: String = url.path
.appending(url.query.map { value in "?\(value)" })
let method: String = method.rawValue
let timestamp: Int = Int(floor(dependencies.dateNow.timeIntervalSince1970))
let serverPublicKeyData: Data = Data(hex: serverPublicKey)
guard
!serverPublicKeyData.isEmpty,
let nonce: [UInt8] = (try? dependencies.crypto.perform(.generateNonce16())),
let timestampBytes: [UInt8] = "\(timestamp)".data(using: .ascii).map({ Array($0) })
else { throw OpenGroupAPIError.signingFailed }
/// Get a hash of any body content
let bodyHash: [UInt8]? = {
guard let body: Data = body else { return nil }
return try? dependencies.crypto.perform(.hash(message: body.bytes, outputLength: 64))
}()
/// Generate the signature message
/// "ServerPubkey || Nonce || Timestamp || Method || Path || Blake2b Hash(Body)
/// `ServerPubkey`
/// `Nonce`
/// `Timestamp` is the bytes of an ascii decimal string
/// `Method`
/// `Path`
/// `Body` is a Blake2b hash of the data (if there is a body)
let messageBytes: [UInt8] = serverPublicKeyData.bytes
.appending(contentsOf: nonce)
.appending(contentsOf: timestampBytes)
.appending(contentsOf: method.bytes)
.appending(contentsOf: path.bytes)
.appending(contentsOf: bodyHash ?? [])
/// Sign the above message
let signResult: (publicKey: String, signature: [UInt8]) = try sign(
db,
messageBytes: messageBytes,
for: server,
fallbackSigningType: .unblinded,
forceBlinded: forceBlinded,
using: dependencies
)
return [
HTTPHeader.sogsPubKey: signResult.publicKey,
HTTPHeader.sogsTimestamp: "\(timestamp)",
HTTPHeader.sogsNonce: Data(nonce).base64EncodedString(),
HTTPHeader.sogsSignature: signResult.signature.toBase64()
]
}
/// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
private static func sign(
_ db: Database,
@ -1357,57 +1425,19 @@ public enum OpenGroupAPI {
else { throw OpenGroupAPIError.signingFailed }
var updatedRequest: URLRequest = preparedRequest.request
let path: String = url.path
.appending(url.query.map { value in "?\(value)" })
let method: String = preparedRequest.method.rawValue
let timestamp: Int = Int(floor(dependencies.dateNow.timeIntervalSince1970))
let serverPublicKeyData: Data = Data(hex: target.serverPublicKey)
guard
!serverPublicKeyData.isEmpty,
let nonce: [UInt8] = (try? dependencies.crypto.perform(.generateNonce16())),
let timestampBytes: [UInt8] = "\(timestamp)".data(using: .ascii).map({ Array($0) })
else { throw OpenGroupAPIError.signingFailed }
/// Get a hash of any body content
let bodyHash: [UInt8]? = {
guard let body: Data = preparedRequest.request.httpBody else { return nil }
return try? dependencies.crypto.perform(.hash(message: body.bytes, outputLength: 64))
}()
/// Generate the signature message
/// "ServerPubkey || Nonce || Timestamp || Method || Path || Blake2b Hash(Body)
/// `ServerPubkey`
/// `Nonce`
/// `Timestamp` is the bytes of an ascii decimal string
/// `Method`
/// `Path`
/// `Body` is a Blake2b hash of the data (if there is a body)
let messageBytes: [UInt8] = serverPublicKeyData.bytes
.appending(contentsOf: nonce)
.appending(contentsOf: timestampBytes)
.appending(contentsOf: method.bytes)
.appending(contentsOf: path.bytes)
.appending(contentsOf: bodyHash ?? [])
/// Sign the above message
let signResult: (publicKey: String, signature: [UInt8]) = try sign(
db,
messageBytes: messageBytes,
for: target.server,
fallbackSigningType: .unblinded,
forceBlinded: target.forceBlinded,
using: dependencies
)
updatedRequest.allHTTPHeaderFields = (preparedRequest.request.allHTTPHeaderFields ?? [:])
.updated(with: [
HTTPHeader.sogsPubKey: signResult.publicKey,
HTTPHeader.sogsTimestamp: "\(timestamp)",
HTTPHeader.sogsNonce: Data(nonce).base64EncodedString(),
HTTPHeader.sogsSignature: signResult.signature.toBase64()
])
.updated(
with: try signatureHeaders(
db,
url: url,
method: preparedRequest.method,
server: target.server,
serverPublicKey: target.serverPublicKey,
body: preparedRequest.request.httpBody,
forceBlinded: target.forceBlinded,
using: dependencies
)
)
return updatedRequest
}
@ -1431,3 +1461,29 @@ public enum OpenGroupAPI {
)
}
}
private extension Network.Destination {
func signed(_ db: Database, server: String, data: Data?, using dependencies: Dependencies) throws -> Network.Destination {
switch self {
case .snode: throw NetworkError.invalidURL
case .server(let url, let method, let headers, let x25519PublicKey):
return .server(
url: url,
method: method,
headers: (headers ?? [:])
.updated(with: try OpenGroupAPI.signatureHeaders(
db,
url: url,
method: method,
server: server,
serverPublicKey: x25519PublicKey,
body: data,
forceBlinded: false,
using: dependencies
)),
x25519PublicKey: x25519PublicKey
)
}
}
}

@ -1134,7 +1134,7 @@ public final class OpenGroupManager {
DispatchQueue.global(qos: .background).async(using: dependencies) {
// Hold on to the publisher until it has completed at least once
dependencies.storage
.readPublisher { db -> (Data?, Network.PreparedRequest<Data>?) in
.readPublisher { db -> (Data?, Network.Destination) in
if canUseExistingImage {
let maybeExistingData: Data? = try? OpenGroup
.select(.imageData)
@ -1143,37 +1143,36 @@ public final class OpenGroupManager {
.fetchOne(db)
if let existingData: Data = maybeExistingData {
return (existingData, nil)
return (existingData, .fileServer)
}
}
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
throw StorageError.objectNotFound
}
return (
nil,
try OpenGroupAPI
.preparedDownloadFile(
db,
fileId: fileId,
from: roomToken,
on: server,
using: dependencies
)
try OpenGroupAPI.downloadDestination(
db,
fileId: fileId,
openGroup: openGroup,
using: dependencies
)
)
}
.flatMap { info in
switch info {
case (.some(let existingData), _):
.flatMap { existingData, destination in
switch existingData {
case .some(let existingData):
return Just(existingData)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
case (_, .some(let preparedRequest)):
return preparedRequest.send(using: dependencies)
case .none:
return LibSession
.downloadFile(from: destination)
.map { _, imageData in imageData }
.eraseToAnyPublisher()
default:
return Fail(error: NetworkError.invalidPreparedRequest)
.eraseToAnyPublisher()
}
}
.sinkUntilComplete(

@ -0,0 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension OpenGroupAPI {
enum UpdateTypes: String {
case reaction = "r"
}
}

@ -14,7 +14,6 @@ public enum MessageSenderError: Error, CustomStringConvertible, Equatable {
case noUsername
case attachmentsNotUploaded
case blindingFailed
case sendJobTimeout
// Closed groups
case noThread
@ -44,7 +43,6 @@ public enum MessageSenderError: Error, CustomStringConvertible, Equatable {
case .noUsername: return "Missing username (MessageSenderError.noUsername)."
case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded (MessageSenderError.attachmentsNotUploaded)."
case .blindingFailed: return "Couldn't blind the sender (MessageSenderError.blindingFailed)."
case .sendJobTimeout: return "Send job timeout (MessageSenderError.sendJobTimeout)."
// Closed groups
case .noThread: return "Couldn't find a thread associated with the given group public key (MessageSenderError.noThread)."
@ -68,7 +66,6 @@ public enum MessageSenderError: Error, CustomStringConvertible, Equatable {
case (.noKeyPair, .noKeyPair): return true
case (.invalidClosedGroupUpdate, .invalidClosedGroupUpdate): return true
case (.blindingFailed, .blindingFailed): return true
case (.sendJobTimeout, .sendJobTimeout): return true
case (.other(let lhsError), .other(let rhsError)):
// Not ideal but the best we can do

@ -219,6 +219,7 @@ extension MessageReceiver {
serverHash: message.serverHash,
messageUuid: message.uuid,
threadId: thread.id,
threadVariant: thread.variant,
authorId: caller,
variant: .infoCall,
body: String(data: messageInfoData, encoding: .utf8),
@ -298,6 +299,7 @@ extension MessageReceiver {
serverHash: message.serverHash,
messageUuid: message.uuid,
threadId: thread.id,
threadVariant: thread.variant,
authorId: sender,
variant: .infoCall,
body: String(data: messageInfoData, encoding: .utf8),

@ -674,6 +674,7 @@ extension MessageReceiver {
_ = try Interaction(
serverHash: message.serverHash,
threadId: threadId,
threadVariant: threadVariant,
authorId: sender,
variant: infoMessageVariant,
body: messageKind

@ -32,6 +32,7 @@ extension MessageReceiver {
openGroup: nil
)
let messageExpirationInfo: Message.MessageExpirationInfo = Message.getMessageExpirationInfo(
threadVariant: threadVariant,
wasRead: wasRead,
serverExpirationTimestamp: serverExpirationTimestamp,
expiresInSeconds: message.expiresInSeconds,
@ -40,6 +41,7 @@ extension MessageReceiver {
_ = try Interaction(
serverHash: message.serverHash,
threadId: threadId,
threadVariant: threadVariant,
authorId: sender,
variant: {
switch messageKind {
@ -58,6 +60,7 @@ extension MessageReceiver {
Message.updateExpiryForDisappearAfterReadMessages(
db,
threadId: threadId,
threadVariant: threadVariant,
serverHash: message.serverHash,
expiresInSeconds: messageExpirationInfo.expiresInSeconds,
expiresStartedAtMs: messageExpirationInfo.expiresStartedAtMs

@ -114,6 +114,7 @@ extension MessageReceiver {
_ = try Interaction(
serverHash: nil, // Intentionally null so sync messages are seen as duplicates
threadId: threadId,
threadVariant: threadVariant,
authorId: sender,
variant: .infoDisappearingMessagesUpdate,
body: updatedConfig.messageInfoString(
@ -141,12 +142,11 @@ extension MessageReceiver {
) throws {
guard proto.hasExpirationType || proto.hasExpirationTimer else { return }
guard
threadVariant != .community,
let sender: String = message.sender,
let timestampMs: UInt64 = message.sentTimestamp,
Features.useNewDisappearingMessagesConfig
else {
return
}
else { return }
let localConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration
.fetchOne(db, id: threadId)

@ -142,6 +142,7 @@ extension MessageReceiver {
_ = try Interaction(
serverHash: message.serverHash,
threadId: unblindedThread.id,
threadVariant: unblindedThread.variant,
authorId: senderId,
variant: .infoMessageRequestAccepted,
timestampMs: (

@ -152,6 +152,7 @@ extension MessageReceiver {
)
)
let messageExpirationInfo: Message.MessageExpirationInfo = Message.getMessageExpirationInfo(
threadVariant: thread.variant,
wasRead: wasRead,
serverExpirationTimestamp: serverExpirationTimestamp,
expiresInSeconds: message.expiresInSeconds,
@ -161,6 +162,7 @@ extension MessageReceiver {
interaction = try Interaction(
serverHash: message.serverHash, // Keep track of server hash
threadId: thread.id,
threadVariant: thread.variant,
authorId: sender,
variant: variant,
body: message.text,
@ -219,6 +221,7 @@ extension MessageReceiver {
Message.getExpirationForOutgoingDisappearingMessages(
db,
threadId: threadId,
threadVariant: threadVariant,
variant: variant,
serverHash: message.serverHash,
expireInSeconds: message.expiresInSeconds
@ -246,6 +249,7 @@ extension MessageReceiver {
Message.updateExpiryForDisappearAfterReadMessages(
db,
threadId: threadId,
threadVariant: threadVariant,
serverHash: message.serverHash,
expiresInSeconds: message.expiresInSeconds,
expiresStartedAtMs: message.expiresStartedAtMs
@ -255,6 +259,7 @@ extension MessageReceiver {
Message.getExpirationForOutgoingDisappearingMessages(
db,
threadId: threadId,
threadVariant: threadVariant,
variant: variant,
serverHash: message.serverHash,
expireInSeconds: message.expiresInSeconds

@ -289,6 +289,7 @@ extension MessageSender {
// Notify the user
let interaction: Interaction = try Interaction(
threadId: groupPublicKey,
threadVariant: .legacyGroup,
authorId: userPublicKey,
variant: .infoClosedGroupUpdated,
body: ClosedGroupControlMessage.Kind
@ -403,6 +404,7 @@ extension MessageSender {
// Notify the user
let interaction: Interaction = try Interaction(
threadId: closedGroup.threadId,
threadVariant: .legacyGroup,
authorId: userPublicKey,
variant: .infoClosedGroupUpdated,
body: ClosedGroupControlMessage.Kind
@ -525,6 +527,7 @@ extension MessageSender {
if !removedMembers.subtracting(groupZombieIds).isEmpty {
let interaction: Interaction = try Interaction(
threadId: closedGroup.threadId,
threadVariant: .legacyGroup,
authorId: userPublicKey,
variant: .infoClosedGroupUpdated,
body: ClosedGroupControlMessage.Kind
@ -591,6 +594,7 @@ extension MessageSender {
// Notify the user
let interaction: Interaction = try Interaction(
threadId: groupPublicKey,
threadVariant: .legacyGroup,
authorId: userPublicKey,
variant: .infoClosedGroupCurrentUserLeaving,
body: "group_you_leaving".localized(),

@ -168,7 +168,11 @@ public enum MessageReceiver {
message.sentTimestamp = sentTimestamp
message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
message.openGroupServerMessageId = openGroupServerMessageId
message.attachDisappearingMessagesConfiguration(from: proto)
// Ignore disappearing message settings in communities (in case of modified clients)
if threadVariant != .community {
message.attachDisappearingMessagesConfiguration(from: proto)
}
// Don't process the envelope any further if the sender is blocked
guard (try? Contact.fetchOne(db, id: sender))?.isBlocked != true || message.processWithBlockedSender else {
@ -332,12 +336,13 @@ public enum MessageReceiver {
}
// Perform any required post-handling logic
try MessageReceiver.postHandleMessage(db, threadId: threadId, message: message)
try MessageReceiver.postHandleMessage(db, threadId: threadId, threadVariant: threadVariant, message: message)
}
public static func postHandleMessage(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: Message
) throws {
// When handling any message type which has related UI we want to make sure the thread becomes
@ -368,15 +373,17 @@ public enum MessageReceiver {
// Start the disappearing messages timer if needed
// For disappear after send, this is necessary so the message will disappear even if it is not read
db.afterNextTransactionNestedOnce(
dedupeId: "PostInsertDisappearingMessagesJob", // stringlint:disable
onCommit: { db in
JobRunner.upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(db)
)
}
)
if threadVariant != .community {
db.afterNextTransactionNestedOnce(
dedupeId: "PostInsertDisappearingMessagesJob", // stringlint:disable
onCommit: { db in
JobRunner.upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(db)
)
}
)
}
guard !isCurrentlyVisible else { return }

@ -185,7 +185,7 @@ extension MessageSender {
.map { results -> PreparedSendData in
// Once the attachments are processed then update the PreparedSendData with
// the fileIds associated to the message
let fileIds: [String] = results.compactMap { result -> String? in result }
let fileIds: [String] = results.compactMap { result -> String? in Attachment.fileId(for: result) }
return preparedSendData.with(fileIds: fileIds)
}

@ -671,25 +671,20 @@ public final class MessageSender {
let updatedMessage: Message = message
updatedMessage.serverHash = response.hash
let job: Job? = Job(
variant: .notifyPushServer,
behaviour: .runOnce,
details: NotifyPushServerJob.Details(message: snodeMessage)
)
let shouldNotify: Bool = {
switch (updatedMessage, data.destination) {
case (is VisibleMessage, .syncMessage), (is UnsendRequest, .syncMessage): return false
case (is VisibleMessage, _), (is UnsendRequest, _): return true
case (let callMessage as CallMessage, _):
// Note: Other 'CallMessage' types are too big to send as push notifications
// so only send the 'preOffer' message as a notification
switch callMessage.kind {
case .preOffer: return true
default: return false
}
default: return false
}
// Only legacy groups need to manually trigger push notifications now so only create the job
// if the destination is a legacy group (ie. a group destination with a standard pubkey prefix)
let notifyPushServerJob: Job? = {
guard
case .closedGroup(let groupPublicKey) = data.destination,
let groupId: SessionId = try? SessionId(from: groupPublicKey),
groupId.prefix == .standard
else { return nil }
return Job(
variant: .notifyPushServer,
behaviour: .runOnce,
details: NotifyPushServerJob.Details(message: snodeMessage)
)
}()
return dependencies.storage
@ -702,21 +697,16 @@ public final class MessageSender {
using: dependencies
)
guard shouldNotify else { return () }
guard notifyPushServerJob != nil else { return () }
dependencies.jobRunner.add(db, job: job, canStartJob: true, using: dependencies)
dependencies.jobRunner.add(db, job: notifyPushServerJob, canStartJob: true, using: dependencies)
return ()
}
.flatMap { _ -> AnyPublisher<Void, Error> in
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive])
.defaulting(to: false)
guard shouldNotify && !isMainAppActive else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
guard let job: Job = job else {
guard !isMainAppActive, let notifyPushServerJob: Job = notifyPushServerJob else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
@ -725,7 +715,7 @@ public final class MessageSender {
return Deferred {
Future<Void, Error> { resolver in
NotifyPushServerJob.run(
job,
notifyPushServerJob,
queue: .global(qos: .default),
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, _, _, _ in

@ -76,7 +76,6 @@ public final class ClosedGroupPoller: Poller {
let limit: Double = (12 * 60 * 60)
let a: TimeInterval = ((ClosedGroupPoller.maxPollInterval - minPollInterval) / limit)
let nextPollInterval: TimeInterval = a * min(timeSinceLastMessage, limit) + minPollInterval
SNLog("Next poll interval for closed group with public key: \(publicKey) is \(nextPollInterval) s.")
return nextPollInterval
}

@ -10,6 +10,13 @@ import SessionSnodeKit
import SessionUtilitiesKit
public class Poller {
public typealias PollResponse = (
messages: [ProcessedMessage],
rawMessageCount: Int,
validMessageCount: Int,
hadValidHashUpdate: Bool
)
private var cancellables: Atomic<[String: AnyCancellable]> = Atomic([:])
internal var isPolling: Atomic<[String: Bool]> = Atomic([:])
internal var pollCount: Atomic<[String: Int]> = Atomic([:])
@ -95,6 +102,7 @@ public class Poller {
) {
guard isPolling.wrappedValue[swarmPublicKey] == true else { return }
let pollerName: String = pollerName(for: swarmPublicKey)
let namespaces: [SnodeAPI.Namespace] = self.namespaces
let pollerQueue: DispatchQueue = self.pollerQueue
let lastPollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970
@ -109,8 +117,17 @@ public class Poller {
)
.subscribe(on: pollerQueue, using: dependencies)
.receive(on: pollerQueue, using: dependencies)
// FIXME: In iOS 14.0 a `flatMap` was added where the error type in `Never`, we should use that here
.map { response -> Result<PollResponse, Error> in Result.success(response) }
.catch { error -> AnyPublisher<Result<PollResponse, Error>, Error> in
Just(Result.failure(error)).setFailureType(to: Error.self).eraseToAnyPublisher()
}
.sink(
receiveCompletion: { result in
receiveCompletion: { _ in }, // Never called
receiveValue: { result in
let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
// Log information about the poll
switch result {
case .failure(let error):
// Determine if the error should stop us from polling anymore
@ -118,16 +135,28 @@ public class Poller {
return
}
case .finished: break
Log.error("\(pollerName) failed to process any messages due to error: \(error)")
case .success(let response):
let duration: TimeUnit = .seconds(endTime - lastPollStart)
switch (response.rawMessageCount, response.validMessageCount, response.hadValidHashUpdate) {
case (0, _, _):
Log.info("Received no new messages in \(pollerName) after \(duration, unit: .s).")
case (_, 0, false):
Log.info("Received \(response.rawMessageCount) new message\(plural: response.rawMessageCount) in \(pollerName) after \(duration, unit: .s), all duplicates - marked the hash we polled with as invalid")
default:
Log.info("Received \(response.validMessageCount) new message\(plural: response.validMessageCount) in \(pollerName) after \(duration, unit: .s) (duplicates: \(response.rawMessageCount - response.validMessageCount))")
}
}
// Calculate the remaining poll delay and schedule the next poll
let currentTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
guard
self != nil,
let remainingInterval: TimeInterval = (self?.nextPollDelay(for: swarmPublicKey, using: dependencies))
.map({ nextPollInterval in max(0, nextPollInterval - (currentTime - lastPollStart)) }),
.map({ nextPollInterval in max(0, nextPollInterval - (endTime - lastPollStart)) }),
remainingInterval > 0
else {
return pollerQueue.async(using: dependencies) {
@ -138,8 +167,7 @@ public class Poller {
pollerQueue.asyncAfter(deadline: .now() + .milliseconds(Int(remainingInterval * 1000)), qos: .default, using: dependencies) {
self?.pollRecursively(for: swarmPublicKey, drainBehaviour: drainBehaviour, using: dependencies)
}
},
receiveValue: { _ in }
}
)
}
}
@ -156,21 +184,19 @@ public class Poller {
isBackgroundPollValid: @escaping (() -> Bool) = { true },
drainBehaviour: Atomic<SwarmDrainBehaviour>,
using dependencies: Dependencies
) -> AnyPublisher<[ProcessedMessage], Error> {
) -> AnyPublisher<PollResponse, Error> {
// If the polling has been cancelled then don't continue
guard
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
isPolling.wrappedValue[swarmPublicKey] == true
else {
return Just([])
return Just(([], 0, 0, false))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
let pollerName: String = pollerName(for: swarmPublicKey)
let pollerQueue: DispatchQueue = self.pollerQueue
let configHashes: [String] = LibSession.configHashes(for: swarmPublicKey)
let pollStartTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
// Fetch the messages
return LibSession.getSwarm(swarmPublicKey: swarmPublicKey)
@ -183,12 +209,12 @@ public class Poller {
using: dependencies
)
}
.flatMap { [weak self] namespacedResults -> AnyPublisher<[ProcessedMessage], Error> in
.flatMap { [weak self] namespacedResults -> AnyPublisher<PollResponse, Error> in
guard
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
self?.isPolling.wrappedValue[swarmPublicKey] == true
else {
return Just([])
return Just(([], 0, 0, false))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -201,12 +227,7 @@ public class Poller {
// No need to do anything if there are no messages
guard rawMessageCount > 0 else {
if !calledFromBackgroundPoller {
let pollEndTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
SNLog("Received no new messages in \(pollerName) after \(.seconds(pollEndTime - pollStartTime), unit: .s).")
}
return Just([])
return Just(([], 0, 0, false))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -223,7 +244,6 @@ public class Poller {
var hadValidHashUpdate: Bool = false
var configMessageJobsToRun: [Job] = []
var standardMessageJobsToRun: [Job] = []
var pollerLogOutput: String = "\(pollerName) failed to process any messages" // stringlint:disable
dependencies.storage.write { db in
let allProcessedMessages: [ProcessedMessage] = sortedMessages
@ -257,11 +277,11 @@ public class Poller {
/// In the background ignore 'SQLITE_ABORT' (it generally means the
/// BackgroundPoller has timed out
if !calledFromBackgroundPoller {
SNLog("Failed to the database being suspended (running in background with no background task).")
Log.warn("Failed to the database being suspended (running in background with no background task).")
}
break
default: SNLog("Failed to deserialize envelope due to error: \(error).")
default: Log.error("Failed to deserialize envelope due to error: \(error).")
}
return nil
@ -285,7 +305,7 @@ public class Poller {
publicKey: swarmPublicKey
)
}
catch { SNLog("Failed to handle processed message to error: \(error).") }
catch { Log.error("Failed to handle processed config message due to error: \(error).") }
}
else {
/// Individually process non-config messages
@ -304,7 +324,7 @@ public class Poller {
associatedWithProto: proto
)
}
catch { SNLog("Failed to handle processed message to error: \(error).") }
catch { Log.error("Failed to handle processed message due to error: \(error).") }
}
}
@ -388,19 +408,13 @@ public class Poller {
}
}
catch {
SNLog("Failed to add dependency between config processing and non-config processing messageReceive jobs.")
Log.warn("Failed to add dependency between config processing and non-config processing messageReceive jobs.")
}
}
}
// Set the output for logging
let pollEndTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
pollerLogOutput = "Received \(messageCount) new message\(messageCount == 1 ? "" : "s") in \(pollerName) after \(.seconds(pollEndTime - pollStartTime), unit: .s) (duplicates: \(rawMessageCount - messageCount))" // stringlint:disable
// Clean up message hashes and add some logs about the poll results
if sortedMessages.isEmpty && !hadValidHashUpdate {
pollerLogOutput = "Received \(rawMessageCount) new message\(rawMessageCount == 1 ? "" : "s") in \(pollerName) after \(.seconds(pollEndTime - pollStartTime), unit: .s), all duplicates - marking the hash we polled with as invalid" // stringlint:disable
// Update the cached validity of the messages
try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash(
db,
@ -410,14 +424,9 @@ public class Poller {
}
}
// Only output logs if it isn't the background poller
if !calledFromBackgroundPoller {
SNLog(pollerLogOutput)
}
// If we aren't runing in a background poller then just finish immediately
guard calledFromBackgroundPoller else {
return Just(processedMessages)
return Just((processedMessages, rawMessageCount, messageCount, hadValidHashUpdate))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -465,7 +474,7 @@ public class Poller {
)
.collect()
}
.map { _ in processedMessages }
.map { _ in (processedMessages, rawMessageCount, messageCount, hadValidHashUpdate) }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()

@ -150,7 +150,7 @@ public struct ProfileManager {
public static let sharedDataProfileAvatarsDirPath: String = {
let path: String = URL(fileURLWithPath: OWSFileSystem.appSharedDataDirectoryPath())
.appendingPathComponent("ProfileAvatars")
.appendingPathComponent("ProfileAvatars") // stringlint:disable
.path
OWSFileSystem.ensureDirectoryExists(path)
@ -197,29 +197,24 @@ public struct ProfileManager {
// Download already in flight; ignore
return
}
guard let profileUrlStringAtStart: String = profile.profilePictureUrl else {
SNLog("Skipping downloading avatar for \(profile.id) because url is not set")
return
}
guard
let fileId: String = Attachment.fileId(for: profileUrlStringAtStart),
let profileUrlStringAtStart: String = profile.profilePictureUrl,
let profileUrl: URL = URL(string: profileUrlStringAtStart)
else { return SNLog("Skipping downloading avatar for \(profile.id) because url is not set") }
guard
let profileKeyAtStart: Data = profile.profileEncryptionKey,
profileKeyAtStart.count > 0
else {
return
}
else { return }
let fileName: String = UUID().uuidString.appendingFileExtension("jpg")
let fileName: String = UUID().uuidString.appendingFileExtension("jpg") // stringlint:disable
let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName)
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: funcName)
Log.trace("downloading profile avatar: \(profile.id)")
currentAvatarDownloads.mutate { $0.insert(profile.id) }
let useOldServer: Bool = (profileUrlStringAtStart.contains(FileServerAPI.oldServer))
FileServerAPI
.download(fileId: fileId, useOldServer: useOldServer)
LibSession
.downloadFile(from: .fileServer(downloadUrl: profileUrl))
.subscribe(on: DispatchQueue.global(qos: .background))
.receive(on: DispatchQueue.global(qos: .background))
.sinkUntilComplete(
@ -230,7 +225,7 @@ public struct ProfileManager {
// isn't used
if backgroundTask != nil { backgroundTask = nil }
},
receiveValue: { data in
receiveValue: { _, data in
guard let latestProfile: Profile = Storage.shared.read({ db in try Profile.fetchOne(db, id: profile.id) }) else {
return
}
@ -318,8 +313,8 @@ public struct ProfileManager {
}
Log.debug(existingProfileUrl != nil ?
"Updating local profile on service with cleared avatar." :
"Updating local profile on service with no avatar."
"Updating local profile on service with cleared avatar." : // stringlint:disable
"Updating local profile on service with no avatar." // stringlint:disable
)
}
@ -462,8 +457,9 @@ public struct ProfileManager {
}
// Upload the avatar to the FileServer
FileServerAPI
.upload(encryptedAvatarData)
LibSession
.uploadToServer(encryptedAvatarData, to: .fileServer, fileName: nil)
.tryMap { _, fileUploadResponse in try Network.fileServerDownloadUrlFor(fileId: fileUploadResponse.id) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: queue)
.sinkUntilComplete(
@ -480,15 +476,13 @@ public struct ProfileManager {
)
}
},
receiveValue: { fileUploadResponse in
let downloadUrl: String = "\(FileServerAPI.server)/file/\(fileUploadResponse.id)"
receiveValue: { downloadUrl in
// Update the cached avatar image value
profileAvatarCache.mutate { $0[fileName] = avatarImageData }
UserDefaults.standard[.lastProfilePictureUpload] = Date()
SNLog("Successfully uploaded avatar image.")
success((downloadUrl, fileName, newProfileKey))
success((downloadUrl.absoluteString, fileName, newProfileKey))
}
)
}
@ -586,7 +580,7 @@ public struct ProfileManager {
// Download the profile picture if needed
guard avatarNeedsDownload else { return }
let dedupeIdentifier: String = "AvatarDownload-\(publicKey)-\(targetAvatarUrl ?? "remove")"
let dedupeIdentifier: String = "AvatarDownload-\(publicKey)-\(targetAvatarUrl ?? "remove")" // stringlint:disable
db.afterNextTransactionNestedOnce(dedupeId: dedupeIdentifier) { db in
// Need to refetch to ensure the db changes have occurred

@ -21,6 +21,7 @@ class OpenGroupManagerSpec: QuickSpec {
serverHash: "TestServerHash",
messageUuid: nil,
threadId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
threadVariant: .community,
authorId: "TestAuthorId",
variant: .standardOutgoing,
body: "Test",

@ -47,7 +47,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing])
.defaulting(to: false)
let lastCallPreOffer: Date? = UserDefaults.sharedLokiProject?[.lastCallPreOffer]
// Perform main setup
Storage.resumeDatabaseAccess()
@ -187,6 +186,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
try MessageReceiver.postHandleMessage(
db,
threadId: threadId,
threadVariant: threadVariant,
message: messageInfo.message
)

@ -158,6 +158,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
Log.appResumedExecution()
Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in
AssertIsOnMainThread()
self?.showLockScreenOrMainContent()

@ -260,6 +260,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
// Create the interaction
let interaction: Interaction = try Interaction(
threadId: threadId,
threadVariant: threadVariant,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: body,

@ -21,7 +21,7 @@ public extension LibSession {
static var hasPaths: Bool { !lastPaths.wrappedValue.isEmpty }
static var pathsDescription: String { lastPaths.wrappedValue.prettifiedDescription }
private class CallbackWrapper<Output> {
fileprivate class CallbackWrapper<Output> {
public let resultPublisher: CurrentValueSubject<Output?, Error> = CurrentValueSubject(nil)
private var pointersToDeallocate: [UnsafeRawPointer?] = []
@ -116,6 +116,7 @@ public extension LibSession {
}
static func suspendNetworkAccess() {
Log.info("[LibSession] suspendNetworkAccess called.")
isSuspended.mutate { $0 = true }
guard let network: UnsafeMutablePointer<network_object> = networkCache.wrappedValue else { return }
@ -125,6 +126,7 @@ public extension LibSession {
static func resumeNetworkAccess() {
isSuspended.mutate { $0 = false }
Log.info("[LibSession] resumeNetworkAccess called.")
}
static func clearSnodeCache() {
@ -193,7 +195,7 @@ public extension LibSession {
}
static func sendOnionRequest<T: Encodable>(
to destination: OnionRequestAPIDestination,
to destination: Network.Destination,
body: T?,
swarmPublicKey: String?,
timeout: TimeInterval,
@ -243,77 +245,10 @@ public extension LibSession {
wrapper.unsafePointer()
)
case .server(let method, let scheme, let host, let endpoint, let port, let headers, let x25519PublicKey):
let headerInfo: [(key: String, value: String)]? = headers?.map { ($0.key, $0.value) }
// Handle the more complicated type conversions first
let cHeaderKeysContent: [UnsafePointer<CChar>?] = (try? ((headerInfo ?? [])
.map { $0.key.cString(using: .utf8) }
.unsafeCopyCStringArray()))
.defaulting(to: [])
let cHeaderValuesContent: [UnsafePointer<CChar>?] = (try? ((headerInfo ?? [])
.map { $0.value.cString(using: .utf8) }
.unsafeCopyCStringArray()))
.defaulting(to: [])
guard
cHeaderKeysContent.count == cHeaderValuesContent.count,
cHeaderKeysContent.allSatisfy({ $0 != nil }),
cHeaderValuesContent.allSatisfy({ $0 != nil })
else {
cHeaderKeysContent.forEach { $0?.deallocate() }
cHeaderValuesContent.forEach { $0?.deallocate() }
throw LibSessionError.invalidCConversion
}
// Convert the other types
let targetScheme: String = (scheme ?? "https")
let cMethod: UnsafePointer<CChar>? = (method ?? "GET")
.cString(using: .utf8)?
.unsafeCopy()
let cTargetScheme: UnsafePointer<CChar>? = targetScheme
.cString(using: .utf8)?
.unsafeCopy()
let cHost: UnsafePointer<CChar>? = host
.cString(using: .utf8)?
.unsafeCopy()
let cEndpoint: UnsafePointer<CChar>? = endpoint
.cString(using: .utf8)?
.unsafeCopy()
let cX25519Pubkey: UnsafePointer<CChar>? = x25519PublicKey
.suffix(64) // Quick way to drop '05' prefix if present
.cString(using: .utf8)?
.unsafeCopy()
let cHeaderKeys: UnsafeMutablePointer<UnsafePointer<CChar>?>? = cHeaderKeysContent
.unsafeCopy()
let cHeaderValues: UnsafeMutablePointer<UnsafePointer<CChar>?>? = cHeaderValuesContent
.unsafeCopy()
let cServerDestination = network_server_destination(
method: cMethod,
protocol: cTargetScheme,
host: cHost,
endpoint: cEndpoint,
port: (port ?? (targetScheme == "https" ? 443 : 80)),
x25519_pubkey: cX25519Pubkey,
headers: cHeaderKeys,
header_values: cHeaderValues,
headers_size: (headerInfo ?? []).count
)
// Add a cleanup callback to deallocate the header arrays
wrapper.addUnsafePointerToCleanup(cMethod)
wrapper.addUnsafePointerToCleanup(cTargetScheme)
wrapper.addUnsafePointerToCleanup(cHost)
wrapper.addUnsafePointerToCleanup(cEndpoint)
wrapper.addUnsafePointerToCleanup(cX25519Pubkey)
cHeaderKeysContent.forEach { wrapper.addUnsafePointerToCleanup($0) }
cHeaderValuesContent.forEach { wrapper.addUnsafePointerToCleanup($0) }
wrapper.addUnsafePointerToCleanup(cHeaderKeys)
wrapper.addUnsafePointerToCleanup(cHeaderValues)
case .server:
network_send_onion_request_to_server_destination(
network,
cServerDestination,
try wrapper.cServerDestination(destination),
cPayloadBytes,
cPayloadBytes.count,
Int64(floor(timeout * 1000)),
@ -326,13 +261,116 @@ public extension LibSession {
}
}
.tryMap { success, timeout, statusCode, data -> (any ResponseInfoType, Data?) in
try throwErrorIfNeeded(success, timeout, statusCode, data, using: dependencies)
try throwErrorIfNeeded(success, timeout, statusCode, data)
return (Network.ResponseInfo(code: statusCode), data)
}
}
.eraseToAnyPublisher()
}
static func uploadToServer(
_ data: Data,
to server: Network.Destination,
fileName: String?,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, FileUploadResponse), Error> {
typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?)
return getOrCreateNetwork()
.tryFlatMap { network in
CallbackWrapper<Output>
.create { wrapper in
network_upload_to_server(
network,
try wrapper.cServerDestination(server),
Array(data),
data.count,
fileName?.cString(using: .utf8),
Int64(floor(Network.fileUploadTimeout * 1000)),
{ success, timeout, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
CallbackWrapper<Output>.run(ctx, (success, timeout, Int(statusCode), data))
},
wrapper.unsafePointer()
)
}
.tryMap { success, timeout, statusCode, maybeData -> (any ResponseInfoType, FileUploadResponse) in
try throwErrorIfNeeded(success, timeout, statusCode, maybeData)
guard let data: Data = maybeData else { throw NetworkError.parsingFailed }
return (
Network.ResponseInfo(code: statusCode),
try FileUploadResponse.decoded(from: data, using: dependencies)
)
}
}
}
static func downloadFile(from server: Network.Destination) -> AnyPublisher<(ResponseInfoType, Data), Error> {
typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?)
return getOrCreateNetwork()
.tryFlatMap { network in
return CallbackWrapper<Output>
.create { wrapper in
network_download_from_server(
network,
try wrapper.cServerDestination(server),
Int64(floor(Network.fileDownloadTimeout * 1000)),
{ success, timeout, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
CallbackWrapper<Output>.run(ctx, (success, timeout, Int(statusCode), data))
},
wrapper.unsafePointer()
)
}
.tryMap { success, timeout, statusCode, maybeData -> (any ResponseInfoType, Data) in
try throwErrorIfNeeded(success, timeout, statusCode, maybeData)
guard let data: Data = maybeData else { throw NetworkError.parsingFailed }
return (
Network.ResponseInfo(code: statusCode),
data
)
}
}
}
static func checkClientVersion(
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> {
typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?)
return getOrCreateNetwork()
.tryFlatMap { network in
return CallbackWrapper<Output>
.create { wrapper in
network_get_client_version(
network,
CLIENT_PLATFORM_IOS,
Int64(floor(Network.fileDownloadTimeout * 1000)),
{ success, timeout, statusCode, dataPtr, dataLen, ctx in
let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) }
CallbackWrapper<Output>.run(ctx, (success, timeout, Int(statusCode), data))
},
wrapper.unsafePointer()
)
}
.tryMap { success, timeout, statusCode, maybeData -> (any ResponseInfoType, AppVersionResponse) in
try throwErrorIfNeeded(success, timeout, statusCode, maybeData)
guard let data: Data = maybeData else { throw NetworkError.parsingFailed }
return (
Network.ResponseInfo(code: statusCode),
try AppVersionResponse.decoded(from: data, using: dependencies)
)
}
}
}
// MARK: - Internal Functions
private static func getOrCreateNetwork() -> AnyPublisher<UnsafeMutablePointer<network_object>?, Error> {
@ -460,17 +498,14 @@ public extension LibSession {
_ success: Bool,
_ timeout: Bool,
_ statusCode: Int,
_ data: Data?,
using dependencies: Dependencies
_ data: Data?
) throws {
guard !success || statusCode < 200 || statusCode > 299 else { return }
guard !timeout else { throw NetworkError.timeout }
/// Handle status codes with specific meanings
switch (statusCode, data.map { String(data: $0, encoding: .ascii) }) {
case (400, .none):
throw NetworkError.badRequest(error: NetworkError.unknown.errorDescription ?? "Bad Request", rawData: data)
case (400, .none): throw NetworkError.badRequest(error: "\(NetworkError.unknown)", rawData: data)
case (400, .some(let responseString)): throw NetworkError.badRequest(error: responseString, rawData: data)
case (401, _):
@ -486,7 +521,16 @@ public extension LibSession {
case (421, _): throw SnodeAPIError.unassociatedPubkey
case (429, _): throw SnodeAPIError.rateLimited
case (500, _), (502, _), (503, _): throw SnodeAPIError.internalServerError
case (500, _): throw NetworkError.internalServerError
case (503, _): throw NetworkError.serviceUnavailable
case (502, .none): throw NetworkError.badGateway
case (502, .some(let responseString)):
guard responseString.count >= 64 && Hex.isValid(String(responseString.suffix(64))) else {
throw NetworkError.badGateway
}
throw SnodeAPIError.nodeNotFound(String(responseString.suffix(64)))
case (_, .none): throw NetworkError.unknown
case (_, .some(let responseString)): throw NetworkError.requestFailed(error: responseString, rawData: data)
}
@ -553,3 +597,102 @@ extension LibSession {
}
}
}
// MARK: - Convenience
public extension Network.Destination {
static var fileServer: Network.Destination = .server(
url: try! Network.fileServerUploadUrl(),
method: .post,
headers: nil,
x25519PublicKey: Network.fileServerPubkey()
)
static func fileServer(downloadUrl: URL) -> Network.Destination {
return .server(
url: downloadUrl,
method: .get,
headers: nil,
x25519PublicKey: Network.fileServerPubkey(url: downloadUrl.absoluteString)
)
}
}
private extension LibSession.CallbackWrapper {
func cServerDestination(_ destination: Network.Destination) throws -> network_server_destination {
guard
case .server(let url, let method, let headers, let x25519PublicKey) = destination,
let host: String = url.host
else { throw NetworkError.invalidURL }
let headerInfo: [(key: String, value: String)]? = headers?.map { ($0.key, $0.value) }
// Handle the more complicated type conversions first
let cHeaderKeysContent: [UnsafePointer<CChar>?] = (try? ((headerInfo ?? [])
.map { $0.key.cString(using: .utf8) }
.unsafeCopyCStringArray()))
.defaulting(to: [])
let cHeaderValuesContent: [UnsafePointer<CChar>?] = (try? ((headerInfo ?? [])
.map { $0.value.cString(using: .utf8) }
.unsafeCopyCStringArray()))
.defaulting(to: [])
guard
cHeaderKeysContent.count == cHeaderValuesContent.count,
cHeaderKeysContent.allSatisfy({ $0 != nil }),
cHeaderValuesContent.allSatisfy({ $0 != nil })
else {
cHeaderKeysContent.forEach { $0?.deallocate() }
cHeaderValuesContent.forEach { $0?.deallocate() }
throw LibSessionError.invalidCConversion
}
// Convert the other types
let targetScheme: String = (url.scheme ?? "https")
let cMethod: UnsafePointer<CChar>? = method.rawValue
.cString(using: .utf8)?
.unsafeCopy()
let cTargetScheme: UnsafePointer<CChar>? = targetScheme
.cString(using: .utf8)?
.unsafeCopy()
let cHost: UnsafePointer<CChar>? = host
.cString(using: .utf8)?
.unsafeCopy()
let cEndpoint: UnsafePointer<CChar>? = url.path
.appending(url.query.map { value in "?\(value)" })
.cString(using: .utf8)?
.unsafeCopy()
let cX25519Pubkey: UnsafePointer<CChar>? = x25519PublicKey
.suffix(64) // Quick way to drop '05' prefix if present
.cString(using: .utf8)?
.unsafeCopy()
let cHeaderKeys: UnsafeMutablePointer<UnsafePointer<CChar>?>? = cHeaderKeysContent
.unsafeCopy()
let cHeaderValues: UnsafeMutablePointer<UnsafePointer<CChar>?>? = cHeaderValuesContent
.unsafeCopy()
let cServerDestination = network_server_destination(
method: cMethod,
protocol: cTargetScheme,
host: cHost,
endpoint: cEndpoint,
port: UInt16(url.port ?? (targetScheme == "https" ? 443 : 80)),
x25519_pubkey: cX25519Pubkey,
headers: cHeaderKeys,
header_values: cHeaderValues,
headers_size: (headerInfo ?? []).count
)
// Add a cleanup callback to deallocate the header arrays
self.addUnsafePointerToCleanup(cMethod)
self.addUnsafePointerToCleanup(cTargetScheme)
self.addUnsafePointerToCleanup(cHost)
self.addUnsafePointerToCleanup(cEndpoint)
self.addUnsafePointerToCleanup(cX25519Pubkey)
cHeaderKeysContent.forEach { self.addUnsafePointerToCleanup($0) }
cHeaderValuesContent.forEach { self.addUnsafePointerToCleanup($0) }
self.addUnsafePointerToCleanup(cHeaderKeys)
self.addUnsafePointerToCleanup(cHeaderValues)
return cServerDestination
}
}

@ -0,0 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
struct AppVersionResponse: Codable {
enum CodingKeys: String, CodingKey {
case version = "result"
}
public let version: String
}

@ -0,0 +1,25 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit
public extension Network {
enum Destination: CustomStringConvertible {
case snode(LibSession.Snode)
case server(
url: URL,
method: HTTPMethod,
headers: [HTTPHeader: String]?,
x25519PublicKey: String
)
public var description: String {
switch self {
case .snode(let snode): return "Service node \(snode.address)"
case .server(let url, _, _, _): return url.host.defaulting(to: "Unknown Host")
}
}
}
}

@ -21,7 +21,7 @@ public extension Network.RequestType {
args: [payload, snode, swarmPublicKey, timeout]
) { dependencies in
LibSession.sendOnionRequest(
to: OnionRequestAPIDestination.snode(snode),
to: Network.Destination.snode(snode),
body: payload,
swarmPublicKey: swarmPublicKey,
timeout: timeout,
@ -49,12 +49,9 @@ public extension Network.RequestType {
}
return LibSession.sendOnionRequest(
to: OnionRequestAPIDestination.server(
method: request.httpMethod,
scheme: url.scheme,
host: host,
endpoint: url.path,
port: url.port.map { UInt16($0) },
to: Network.Destination.server(
url: url,
method: (request.httpMethod.map { HTTPMethod(rawValue: $0) } ?? .get),
headers: request.allHTTPHeaderFields,
x25519PublicKey: x25519PublicKey
),

@ -1,26 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit
public enum OnionRequestAPIDestination: CustomStringConvertible {
case snode(LibSession.Snode)
case server(
method: String?,
scheme: String?,
host: String,
endpoint: String,
port: UInt16?,
headers: [HTTPHeader: String]?,
x25519PublicKey: String
)
public var description: String {
switch self {
case .snode(let snode): return "Service node \(snode.address)"
case .server(_, _, let host, _, _, _, _): return host
}
}
}

@ -32,7 +32,7 @@ public enum SnodeAPIError: Error, CustomStringConvertible {
case invalidNetwork
case invalidPayload
case missingSecretKey
case internalServerError
case nodeNotFound(String)
case unassociatedPubkey
case unableToRetrieveSwarm
@ -70,7 +70,7 @@ public enum SnodeAPIError: Error, CustomStringConvertible {
case .invalidNetwork: return "Unable to create network (SnodeAPIError.invalidNetwork)."
case .invalidPayload: return "Invalid payload (SnodeAPIError.invalidPayload)."
case .missingSecretKey: return "Missing secret key (SnodeAPIError.missingSecretKey)."
case .internalServerError: return "The service node is unreachable (SnodeAPIError.internalServerError)."
case .nodeNotFound(let nodePubkey): return "Next node was not found: \(nodePubkey) (SnodeAPIError.nodeNotFound)."
case .unassociatedPubkey: return "The service node is no longer associated with the public key (SnodeAPIError.unassociatedPubkey)."
case .unableToRetrieveSwarm: return "Unable to retrieve the swarm for the given public key (SnodeAPIError.unableToRetrieveSwarm)."
}

@ -408,6 +408,7 @@ open class Storage {
/// with extensions - this may be possible but will require significant refactoring and a potentially painful migration to move the
/// database and other files into the App folder
public static func suspendDatabaseAccess(using dependencies: Dependencies = Dependencies()) {
Log.info("[Storage] suspendDatabaseAccess called.")
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
if Storage.hasCreatedValidInstance { dependencies.storage.isSuspendedUnsafe = true }
}
@ -417,6 +418,7 @@ open class Storage {
public static func resumeDatabaseAccess(using dependencies: Dependencies = Dependencies()) {
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
if Storage.hasCreatedValidInstance { dependencies.storage.isSuspendedUnsafe = false }
Log.info("[Storage] resumeDatabaseAccess called.")
}
public static func resetAllStorage() {

@ -34,7 +34,7 @@ public enum Log {
Log.logger.mutate { $0 = logger }
}
public static func enterForeground() {
public static func appResumedExecution() {
guard logger.wrappedValue != nil else { return }
Log.empty()
@ -161,6 +161,17 @@ public class Logger {
self.fileLogger = DDFileLogger(logFileManager: logFileManager)
}
// We want to use the local datetime and show the timezone offset because it'll make
// it easier to debug when users provide logs and specify that something happened at
// a certain time (the default is UTC so we'd need to know the users timezone in order
// to convert and debug effectively)
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.formatterBehavior = .behavior10_4 // 10.4+ style
dateFormatter.locale = NSLocale.current // Use the current locale and include the timezone instead of UTC
dateFormatter.timeZone = NSTimeZone.local
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss:SSS ZZZZZ"
self.fileLogger.logFormatter = DDLogFileFormatterDefault(dateFormatter: dateFormatter)
self.fileLogger.rollingFrequency = kDayInterval // Refresh everyday
self.fileLogger.logFileManager.maximumNumberOfLogFiles = 3 // Save 3 days' log files
DDLog.add(self.fileLogger)

@ -78,6 +78,10 @@ public extension String {
// MARK: - Formatting
public extension String.StringInterpolation {
mutating func appendInterpolation(plural value: Int) {
appendInterpolation(value == 1 ? "" : "s")
}
mutating func appendInterpolation(_ value: TimeUnit, unit: TimeUnit.Unit, resolution: Int = 2) {
appendLiteral("\(TimeUnit(value, unit: unit, resolution: resolution))")
}

@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtil
@ -31,13 +33,29 @@ extension LibSession {
}
/// Finally register the actual logger callback
session_add_logger_full({ msgPtr, msgLen, _, _, lvl in
guard let msg: String = String(pointer: msgPtr, length: msgLen, encoding: .utf8) else { return }
session_add_logger_full({ msgPtr, msgLen, catPtr, catLen, lvl in
guard
let msg: String = String(pointer: msgPtr, length: msgLen, encoding: .utf8),
let cat: String = String(pointer: catPtr, length: catLen, encoding: .utf8)
else { return }
/// Logs from libSession come through in the format:
/// `[yyyy-MM-dd hh:mm:ss] [+{lifetime}s] [{cat}:{lvl}|log.hpp:{line}] {message}`
/// We want to remove the extra data because it doesn't help the logs
let processedMessage: String = {
let logParts: [String] = msg.components(separatedBy: "] ")
guard logParts.count == 4 else { return msg.trimmingCharacters(in: .whitespacesAndNewlines) }
let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines)
return "[libSession:\(cat)] \(logParts[1])] \(message)"
}()
Log.custom(
Log.Level(lvl),
msg.trimmingCharacters(in: .whitespacesAndNewlines),
withPrefixes: false,
processedMessage,
withPrefixes: true,
silenceForTests: false
)
})

@ -0,0 +1,123 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine
public protocol NetworkType {
func send<T>(_ request: Network.RequestType<T>, using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error>
}
public class Network: NetworkType {
public static let defaultTimeout: TimeInterval = 10
public static let fileUploadTimeout: TimeInterval = 60
public static let fileDownloadTimeout: TimeInterval = 30
/// **Note:** The max file size is 10,000,000 bytes (rather than 10MiB which would be `(10 * 1024 * 1024)`), 10,000,000
/// exactly will be fine but a single byte more will result in an error
public static let maxFileSize = 10_000_000
}
// MARK: - RequestType
public extension Network {
struct RequestType<T> {
public let id: String
public let url: String?
public let method: String?
public let headers: [String: String]?
public let body: Data?
public let args: [Any?]
public let generatePublisher: (Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error>
public init(
id: String,
url: String? = nil,
method: String? = nil,
headers: [String: String]? = nil,
body: Data? = nil,
args: [Any?] = [],
generatePublisher: @escaping (Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error>
) {
self.id = id
self.url = url
self.method = method
self.headers = headers
self.body = body
self.args = args
self.generatePublisher = generatePublisher
}
}
func send<T>(_ request: RequestType<T>, using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error> {
return request.generatePublisher(dependencies)
}
}
// MARK: - FileServer Convenience
public extension Network {
private static let fileServer = "http://filev2.getsession.org"
private static let fileServerPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
private static let legacyFileServer = "http://88.99.175.227"
private static let legacyFileServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
private enum Endpoint: EndpointType {
case file
case fileIndividual(String)
case sessionVersion
public static var name: String { "FileServerAPI.Endpoint" }
public var path: String {
switch self {
case .file: return "file"
case .fileIndividual(let fileId): return "file/\(fileId)"
case .sessionVersion: return "session_version"
}
}
}
static func fileServerPubkey(url: String? = nil) -> String {
switch url?.contains(legacyFileServer) {
case true: return legacyFileServerPublicKey
default: return fileServerPublicKey
}
}
static func isFileServerUrl(url: URL) -> Bool {
return (
url.absoluteString.starts(with: fileServer) ||
url.absoluteString.starts(with: legacyFileServer)
)
}
static func fileServerUploadUrl() throws -> URL {
return (
try URL(string: "\(fileServer)/\(Endpoint.file.path)") ??
{ throw NetworkError.invalidURL }()
)
}
static func fileServerDownloadUrlFor(fileId: String) throws -> URL {
return (
try URL(string: "\(fileServer)/\(Endpoint.fileIndividual(fileId).path)") ??
{ throw NetworkError.invalidURL }()
)
}
//case file
//case fileIndividual(fileId: String)
//case sessionVersion
//
//public static var name: String { "FileServerAPI.Endpoint" }
//
//public var path: String {
// switch self {
// case .file: return "file"
// case .fileIndividual(let fileId): return "file/\(fileId)"
// case .sessionVersion: return "session_version"
// }
//}
}

@ -4,7 +4,7 @@
import Foundation
public enum NetworkError: LocalizedError, Equatable {
public enum NetworkError: Error, Equatable, CustomStringConvertible {
case invalidURL
case invalidPreparedRequest
case notFound
@ -12,24 +12,31 @@ public enum NetworkError: LocalizedError, Equatable {
case invalidResponse
case maxFileSizeExceeded
case unauthorised
case internalServerError
case badGateway
case serviceUnavailable
case badRequest(error: String, rawData: Data?)
case requestFailed(error: String, rawData: Data?)
case timeout
case suspended
case unknown
public var errorDescription: String? {
public var description: String {
switch self {
case .invalidURL: return "Invalid URL."
case .invalidPreparedRequest: return "Invalid PreparedRequest provided."
case .notFound: return "Not Found."
case .parsingFailed, .invalidResponse: return "Invalid response."
case .maxFileSizeExceeded: return "Maximum file size exceeded."
case .unauthorised: return "Unauthorised (Failed to verify the signature)."
case .invalidURL: return "Invalid URL (NetworkError.invalidURL)."
case .invalidPreparedRequest: return "Invalid PreparedRequest provided (NetworkError.invalidPreparedRequest)."
case .notFound: return "Not Found (NetworkError.notFound)."
case .parsingFailed: return "Invalid response (NetworkError.parsingFailed)."
case .invalidResponse: return "Invalid response (NetworkError.invalidResponse)."
case .maxFileSizeExceeded: return "Maximum file size exceeded (NetworkError.maxFileSizeExceeded)."
case .unauthorised: return "Unauthorised (Failed to verify the signature - NetworkError.unauthorised)."
case .internalServerError: return "Internal server error (NetworkError.internalServerError)."
case .badGateway: return "Bad gateway (NetworkError.badGateway)."
case .serviceUnavailable: return "Service unavailable (NetworkError.serviceUnavailable)."
case .badRequest(let error, _), .requestFailed(let error, _): return error
case .timeout: return "The request timed out."
case .suspended: return "Network requests are suspended."
case .unknown: return "An unknown error occurred."
case .timeout: return "The request timed out (NetworkError.timeout)."
case .suspended: return "Network requests are suspended (NetworkError.suspended)."
case .unknown: return "An unknown error occurred (NetworkError.unknown)."
}
}
}

@ -1,44 +0,0 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
public protocol NetworkType {
func send<T>(_ request: Network.RequestType<T>, using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error>
}
public class Network: NetworkType {
public static let defaultTimeout: TimeInterval = 10
public struct RequestType<T> {
public let id: String
public let url: String?
public let method: String?
public let headers: [String: String]?
public let body: Data?
public let args: [Any?]
public let generatePublisher: (Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error>
public init(
id: String,
url: String? = nil,
method: String? = nil,
headers: [String: String]? = nil,
body: Data? = nil,
args: [Any?] = [],
generatePublisher: @escaping (Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error>
) {
self.id = id
self.url = url
self.method = method
self.headers = headers
self.body = body
self.args = args
self.generatePublisher = generatePublisher
}
}
public func send<T>(_ request: RequestType<T>, using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, T), Error> {
return request.generatePublisher(dependencies)
}
}

@ -9,7 +9,7 @@ import SessionUtilitiesKit
public enum Configuration {
public static func performMainSetup() {
// Need to do this first to ensure the legacy database exists
SNUtilitiesKit.configure(maxFileSize: UInt(FileServerAPI.maxFileSize))
SNUtilitiesKit.configure(maxFileSize: UInt(Network.maxFileSize))
SNMessagingKit.configure()
SNUIKit.configure()

Loading…
Cancel
Save