diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b671fabb6..390444359 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -791,22 +791,22 @@ FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; FDC4381A27B34EBA00C60D73 /* LegacyCompactPollBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */; }; - FDC4381C27B354AC00C60D73 /* PublicKeyBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */; }; + FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */; }; FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* Endpoint.swift */; }; - FDC4382627B37F6900C60D73 /* DeletedMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */; }; - FDC4382827B37FD300C60D73 /* ModeratorsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */; }; + FDC4382627B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */; }; + FDC4382827B37FD300C60D73 /* LegacyModeratorsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */; }; FDC4382A27B3802D00C60D73 /* LegacyRoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */; }; - FDC4382C27B380E300C60D73 /* MemberCountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */; }; + FDC4382C27B380E300C60D73 /* LegacyMemberCountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382B27B380E300C60D73 /* LegacyMemberCountResponse.swift */; }; FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */; }; FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383027B3841C00C60D73 /* RegisterResponse.swift */; }; FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; }; - FDC4383A27B4696200C60D73 /* AuthTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */; }; + FDC4383A27B4696200C60D73 /* LegacyAuthTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383927B4696200C60D73 /* LegacyAuthTokenResponse.swift */; }; FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; }; FDC4384027B4746D00C60D73 /* LegacyGetInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383F27B4746D00C60D73 /* LegacyGetInfoResponse.swift */; }; - FDC4384727B47F4D00C60D73 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */; }; + FDC4384727B47F4D00C60D73 /* LegacyOpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384327B47F4D00C60D73 /* LegacyOpenGroupMessageV2.swift */; }; FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */; }; FDC4384927B47F4D00C60D73 /* LegacyCompactPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */; }; - FDC4384A27B47F4D00C60D73 /* Deletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384627B47F4D00C60D73 /* Deletion.swift */; }; + FDC4384A27B47F4D00C60D73 /* LegacyDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384627B47F4D00C60D73 /* LegacyDeletion.swift */; }; FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */; }; FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; }; FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; }; @@ -846,6 +846,8 @@ FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C227BB512200C60D73 /* SodiumProtocols.swift */; }; FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; }; FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; }; + FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; }; + FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1925,22 +1927,22 @@ FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator16Byte.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollBody.swift; sourceTree = ""; }; - FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyBody.swift; sourceTree = ""; }; + FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPublicKeyBody.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; - FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessagesResponse.swift; sourceTree = ""; }; - FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeratorsResponse.swift; sourceTree = ""; }; + FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDeletedMessagesResponse.swift; sourceTree = ""; }; + FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyModeratorsResponse.swift; sourceTree = ""; }; FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyRoomsResponse.swift; sourceTree = ""; }; - FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberCountResponse.swift; sourceTree = ""; }; + FDC4382B27B380E300C60D73 /* LegacyMemberCountResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyMemberCountResponse.swift; sourceTree = ""; }; FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnregisterResponse.swift; sourceTree = ""; }; FDC4383027B3841C00C60D73 /* RegisterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = ""; }; - FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTokenResponse.swift; sourceTree = ""; }; + FDC4383927B4696200C60D73 /* LegacyAuthTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyAuthTokenResponse.swift; sourceTree = ""; }; FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; FDC4383F27B4746D00C60D73 /* LegacyGetInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGetInfoResponse.swift; sourceTree = ""; }; - FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = ""; }; + FDC4384327B47F4D00C60D73 /* LegacyOpenGroupMessageV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyOpenGroupMessageV2.swift; sourceTree = ""; }; FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyRoomInfo.swift; sourceTree = ""; }; FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollResponse.swift; sourceTree = ""; }; - FDC4384627B47F4D00C60D73 /* Deletion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Deletion.swift; sourceTree = ""; }; + FDC4384627B47F4D00C60D73 /* LegacyDeletion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyDeletion.swift; sourceTree = ""; }; FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupV2.swift; sourceTree = ""; }; FDC4384E27B4804F00C60D73 /* Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Header.swift; sourceTree = ""; }; FDC4385027B4807400C60D73 /* QueryParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParam.swift; sourceTree = ""; }; @@ -1978,6 +1980,8 @@ FDC438C227BB512200C60D73 /* SodiumProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumProtocols.swift; sourceTree = ""; }; FDC438C627BB6DF000C60D73 /* DirectMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessage.swift; sourceTree = ""; }; FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequest.swift; sourceTree = ""; }; + FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = ""; }; + FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2500,6 +2504,7 @@ C33FDB8A255A581200E217F9 /* AppContext.h */, C33FDB85255A581100E217F9 /* AppContext.m */, C3C2A5D12553860800C340D1 /* Array+Utilities.swift */, + FDC438CC27BC641200C60D73 /* Set+Utilities.swift */, C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */, B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */, @@ -3836,7 +3841,6 @@ isa = PBXGroup; children = ( FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */, - FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */, FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */, FDC4386627B4E10E00C60D73 /* Capabilities.swift */, FDC4385C27B4C18900C60D73 /* Room.swift */, @@ -3844,6 +3848,7 @@ FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */, FDC4386027B4CDDF00C60D73 /* FileResponse.swift */, FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */, + FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */, FDC4386227B4D94E00C60D73 /* OGMessage.swift */, FDC438C627BB6DF000C60D73 /* DirectMessage.swift */, FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */, @@ -3853,17 +3858,18 @@ FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, FDC438AB27BB145200C60D73 /* UserDeleteMessagesRequest.swift */, FDC438AD27BB148700C60D73 /* UserDeleteMessagesResponse.swift */, - FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */, - FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */, - FDC4384627B47F4D00C60D73 /* Deletion.swift */, - FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */, + FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */, + FDC4383927B4696200C60D73 /* LegacyAuthTokenResponse.swift */, FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */, FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */, FDC4383F27B4746D00C60D73 /* LegacyGetInfoResponse.swift */, FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */, FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */, - FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */, - FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */, + FDC4382B27B380E300C60D73 /* LegacyMemberCountResponse.swift */, + FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */, + FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */, + FDC4384627B47F4D00C60D73 /* LegacyDeletion.swift */, + FDC4384327B47F4D00C60D73 /* LegacyOpenGroupMessageV2.swift */, ); path = Models; sourceTree = ""; @@ -5048,6 +5054,7 @@ C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */, C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */, + FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */, C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, B8AE75A425A6C6A6001A84D2 /* Data+Utilities.swift in Sources */, @@ -5151,14 +5158,14 @@ B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, - FDC4384A27B47F4D00C60D73 /* Deletion.swift in Sources */, + FDC4384A27B47F4D00C60D73 /* LegacyDeletion.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */, - FDC4382827B37FD300C60D73 /* ModeratorsResponse.swift in Sources */, + FDC4382827B37FD300C60D73 /* LegacyModeratorsResponse.swift in Sources */, C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, FD5D202227B1D74F00FEA984 /* ECKeyPair+Conversion.swift in Sources */, - FDC4381C27B354AC00C60D73 /* PublicKeyBody.swift in Sources */, + FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */, FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */, FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */, C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */, @@ -5173,7 +5180,7 @@ C32C5D9C256DD6DC003C73A2 /* OWSOutgoingReceiptManager.m in Sources */, B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */, C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */, - FDC4384727B47F4D00C60D73 /* OpenGroupMessageV2.swift in Sources */, + FDC4384727B47F4D00C60D73 /* LegacyOpenGroupMessageV2.swift in Sources */, FDC4384F27B4804F00C60D73 /* Header.swift in Sources */, C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */, B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */, @@ -5185,7 +5192,7 @@ C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */, FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */, C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */, - FDC4382C27B380E300C60D73 /* MemberCountResponse.swift in Sources */, + FDC4382C27B380E300C60D73 /* LegacyMemberCountResponse.swift in Sources */, B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */, B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */, C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */, @@ -5205,7 +5212,7 @@ C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */, C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */, FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */, - FDC4382627B37F6900C60D73 /* DeletedMessagesResponse.swift in Sources */, + FDC4382627B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift in Sources */, B88FA7B826045D100049422F /* OpenGroupAPI.swift in Sources */, C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, @@ -5214,7 +5221,7 @@ B8856E94256F1C37001CE70E /* OWSSounds.m in Sources */, C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */, C3A3A0EC256E1949004D228D /* OWSRecipientIdentity.m in Sources */, - FDC4383A27B4696200C60D73 /* AuthTokenResponse.swift in Sources */, + FDC4383A27B4696200C60D73 /* LegacyAuthTokenResponse.swift in Sources */, B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */, C32C5AB2256DBE8F003C73A2 /* TSMessage.m in Sources */, C3A3A0FE256E1A3C004D228D /* TSDatabaseSecondaryIndexes.m in Sources */, @@ -5227,6 +5234,7 @@ C3D9E3C025676AD70040E4F3 /* TSAttachment.m in Sources */, FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, + FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index 2383ad12e..3f9cd0749 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -26,7 +26,9 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index 8afcf5442..51b199501 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -1006,7 +1006,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) // If it's an incoming message the user must have moderator status if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) { NSString *userPublicKey = [LKStorage.shared getUserPublicKey]; - if (![SNOpenGroupAPI isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; } + if (![SNOpenGroupManager isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; } } // Delete the message @@ -1060,7 +1060,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) { NSString *userPublicKey = [LKStorage.shared getUserPublicKey]; if (openGroupV2 != nil) { - if (![SNOpenGroupAPI isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; } + if (![SNOpenGroupManager isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; } } } @@ -1133,7 +1133,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) if (interationType == OWSInteractionType_IncomingMessage) { // Only allow deletion on incoming messages if the user has moderation permission if (openGroupV2 != nil) { - return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server]; + return [SNOpenGroupManager isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server]; } } else { return YES; @@ -1155,7 +1155,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) // Check that we're a moderator if (openGroupV2 != nil) { - return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server]; + return [SNOpenGroupManager isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server]; } } diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 580710569..a5cc636c4 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -163,7 +163,7 @@ private extension MentionSelectionView { profilePictureView.publicKey = mentionCandidate.publicKey profilePictureView.update() if let server = openGroupServer, let room = openGroupRoom { - let isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, for: room, on: server) + let isUserModerator = OpenGroupManager.isUserModerator(mentionCandidate.publicKey, for: room, on: server) moderatorIconImageView.isHidden = !isUserModerator } else { moderatorIconImageView.isHidden = true diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 65584efbd..96703a52b 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -219,7 +219,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } if let senderSessionID = senderSessionID, message.isOpenGroupMessage { if let openGroupV2 = Storage.shared.getV2OpenGroup(for: message.uniqueThreadId) { - let isUserModerator = OpenGroupAPI.isUserModerator(senderSessionID, for: openGroupV2.room, on: openGroupV2.server) + let isUserModerator = OpenGroupManager.isUserModerator(senderSessionID, for: openGroupV2.room, on: openGroupV2.server) moderatorIconImageView.isHidden = !isUserModerator || profilePictureView.isHidden } else { moderatorIconImageView.isHidden = true diff --git a/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift b/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift index 6b51dc2e6..fc32e51f9 100644 --- a/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift +++ b/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift @@ -69,9 +69,10 @@ final class JoinOpenGroupModal : Modal { return presentingViewController!.present(alert, animated: true, completion: nil) } presentingViewController!.dismiss(animated: true, completion: nil) + Storage.shared.write { [presentingViewController = self.presentingViewController!] transaction in OpenGroupManager.shared - .add(room: room, server: server, publicKey: publicKey, using: transaction) + .add(roomToken: room, server: server, publicKey: publicKey, using: transaction) .done(on: DispatchQueue.main) { _ in let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index cf4597575..8973ce1aa 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -128,7 +128,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView // A V2 open group URL will look like: + + + + // The host doesn't parse if no explicit scheme is provided if let (room, server, publicKey) = OpenGroupManager.parseV2OpenGroup(from: string) { - joinV2OpenGroup(room: room, server: server, publicKey: publicKey) + joinV2OpenGroup(roomToken: room, server: server, publicKey: publicKey) } else { let title = NSLocalizedString("invalid_url", comment: "") let message = "Please check the URL you entered and try again." @@ -136,24 +136,25 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView } } - fileprivate func joinV2OpenGroup(room: String, server: String, publicKey: String) { + fileprivate func joinV2OpenGroup(roomToken: String, server: String, publicKey: String) { guard !isJoining else { return } isJoining = true ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in Storage.shared.write { transaction in - OpenGroupManager.shared.add(room: room, server: server, publicKey: publicKey, using: transaction) - .done(on: DispatchQueue.main) { [weak self] _ in - self?.presentingViewController?.dismiss(animated: true, completion: nil) - let appDelegate = UIApplication.shared.delegate as! AppDelegate - appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) - } - .catch(on: DispatchQueue.main) { [weak self] error in - self?.dismiss(animated: true, completion: nil) // Dismiss the loader - let title = "Couldn't Join" - let message = error.localizedDescription - self?.isJoining = false - self?.showError(title: title, message: message) - } + OpenGroupManager.shared + .add(roomToken: roomToken, server: server, publicKey: publicKey, using: transaction) + .done(on: DispatchQueue.main) { [weak self] _ in + self?.presentingViewController?.dismiss(animated: true, completion: nil) + let appDelegate = UIApplication.shared.delegate as! AppDelegate + appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) + } + .catch(on: DispatchQueue.main) { [weak self] error in + self?.dismiss(animated: true, completion: nil) // Dismiss the loader + let title = "Couldn't Join" + let message = error.localizedDescription + self?.isJoining = false + self?.showError(title: title, message: message) + } } } } @@ -166,10 +167,11 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView } } -private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate { +private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate { weak var joinOpenGroupVC: JoinOpenGroupVC! - // MARK: Components + // MARK: - Components + private lazy var urlTextView: TextView = { let result = TextView(placeholder: NSLocalizedString("vc_enter_chat_url_text_field_hint", comment: "")) result.keyboardType = .URL @@ -185,7 +187,8 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, return result }() - // MARK: Lifecycle + // MARK: - Lifecycle + override func viewDidLoad() { // Remove background color view.backgroundColor = .clear @@ -223,7 +226,8 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, view.addGestureRecognizer(tapGestureRecognizer) } - // MARK: General + // MARK: - General + func constrainHeight(to height: CGFloat) { view.set(.height, to: height) } @@ -232,14 +236,15 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, urlTextView.resignFirstResponder() } - // MARK: Interaction + // MARK: - Interaction + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { let location = gestureRecognizer.location(in: view) return !suggestionGrid.frame.contains(location) } - func join(_ room: OpenGroupAPI.LegacyRoomInfo) { - joinOpenGroupVC.joinV2OpenGroup(room: room.id, server: OpenGroupAPI.defaultServer, publicKey: OpenGroupAPI.defaultServerPublicKey) + func join(_ room: OpenGroupAPI.Room) { + joinOpenGroupVC.joinV2OpenGroup(roomToken: room.token, server: OpenGroupAPI.defaultServer, publicKey: OpenGroupAPI.defaultServerPublicKey) } @objc private func joinOpenGroup() { diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index 3ac1f5e69..58e7633d7 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -3,11 +3,12 @@ import NVActivityIndicatorView final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let maxWidth: CGFloat - private var rooms: [OpenGroupAPI.LegacyRoomInfo] = [] { didSet { update() } } + private var rooms: [OpenGroupAPI.Room] = [] { didSet { update() } } private var heightConstraint: NSLayoutConstraint! var delegate: OpenGroupSuggestionGridDelegate? - // MARK: UI Components + // MARK: - UI + private lazy var layout: UICollectionViewFlowLayout = { let result = UICollectionViewFlowLayout() result.minimumLineSpacing = 0 @@ -32,11 +33,13 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl return result }() - // MARK: Settings + // MARK: - Settings + private static let cellHeight: CGFloat = 40 private static let separatorWidth = 1 / UIScreen.main.scale - // MARK: Initialization + // MARK: - Initialization + init(maxWidth: CGFloat) { self.maxWidth = maxWidth super.init(frame: CGRect.zero) @@ -59,16 +62,15 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl spinner.startAnimating() heightConstraint = set(.height, to: OpenGroupSuggestionGrid.cellHeight) widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true - if OpenGroupAPI.defaultRoomsPromise == nil { - OpenGroupAPI.legacyGetDefaultRoomsIfNeeded() - } - let _ = OpenGroupAPI.legacyDefaultRoomsPromise?.done { [weak self] rooms in - // TODO: Update this for the new rooms API + + OpenGroupManager.getDefaultRoomsIfNeeded() + _ = OpenGroupManager.defaultRoomsPromise?.done { [weak self] rooms in self?.rooms = rooms } } - // MARK: Updating + // MARK: - Updating + private func update() { spinner.stopAnimating() spinner.isHidden = true @@ -78,12 +80,14 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl collectionView.reloadData() } - // MARK: Layout + // MARK: - Layout + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: maxWidth / 2, height: OpenGroupSuggestionGrid.cellHeight) } - // MARK: Data Source + // MARK: - Data Source + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2) } @@ -94,18 +98,20 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl return cell } - // MARK: Interaction + // MARK: - Interaction + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let room = rooms[indexPath.item] delegate?.join(room) } } -// MARK: Cell +// MARK: - Cell + extension OpenGroupSuggestionGrid { fileprivate final class Cell : UICollectionViewCell { - var room: OpenGroupAPI.LegacyRoomInfo? { didSet { update() } } + var room: OpenGroupAPI.Room? { didSet { update() } } static let identifier = "OpenGroupSuggestionGridCell" @@ -172,17 +178,24 @@ extension OpenGroupSuggestionGrid { } private func update() { - guard let room = room else { return } - let promise = OpenGroupAPI.legacyGetGroupImage(for: room.id, on: OpenGroupAPI.defaultServer) - imageView.image = given(promise.value) { UIImage(data: $0)! } - imageView.isHidden = (imageView.image == nil) + guard let room: OpenGroupAPI.Room = room else { return } + label.text = room.name + + if let imageId: Int64 = room.imageId { + let promise = OpenGroupManager.roomImage(imageId, for: room.token, on: OpenGroupAPI.defaultServer) + imageView.image = given(promise.value) { UIImage(data: $0)! } + imageView.isHidden = (imageView.image == nil) + } + else { + imageView.isHidden = true + } } } } -// MARK: Delegate +// MARK: - Delegate + protocol OpenGroupSuggestionGridDelegate { - - func join(_ room: OpenGroupAPI.LegacyRoomInfo) + func join(_ room: OpenGroupAPI.Room) } diff --git a/SessionMessagingKit/Database/Storage+OpenGroups.swift b/SessionMessagingKit/Database/Storage+OpenGroups.swift index a769b2fd7..9ef34143b 100644 --- a/SessionMessagingKit/Database/Storage+OpenGroups.swift +++ b/SessionMessagingKit/Database/Storage+OpenGroups.swift @@ -2,6 +2,12 @@ public protocol SessionMessagingKitOpenGroupStorageProtocol { func getOpenGroupImage(for room: String, on server: String) -> Data? func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) + + func getV2OpenGroup(for threadID: String) -> OpenGroupV2? + func setV2OpenGroup(_ openGroup: OpenGroupV2, for threadID: String, using transaction: Any) + + func getUserCount(forV2OpenGroupWithID openGroupID: String) -> UInt64? + func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) } extension Storage: SessionMessagingKitOpenGroupStorageProtocol { diff --git a/SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift similarity index 90% rename from SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift index 0823b3016..1f02b8ac8 100644 --- a/SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - struct AuthTokenResponse: Codable { + struct LegacyAuthTokenResponse: Codable { struct Challenge: Codable { enum CodingKeys: String, CodingKey { case ciphertext = "ciphertext" @@ -20,7 +20,7 @@ extension OpenGroupAPI { // MARK: - Codable -extension OpenGroupAPI.AuthTokenResponse.Challenge { +extension OpenGroupAPI.LegacyAuthTokenResponse.Challenge { init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -31,7 +31,7 @@ extension OpenGroupAPI.AuthTokenResponse.Challenge { throw OpenGroupAPI.Error.parsingFailed } - self = OpenGroupAPI.AuthTokenResponse.Challenge( + self = OpenGroupAPI.LegacyAuthTokenResponse.Challenge( ciphertext: ciphertext, ephemeralPublicKey: ephemeralPublicKey ) diff --git a/SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift index 963ef1449..f699b3206 100644 --- a/SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift @@ -15,8 +15,8 @@ extension OpenGroupAPI { public let room: String public let statusCode: UInt - public let messages: [OpenGroupMessageV2]? - public let deletions: [Deletion]? + public let messages: [LegacyOpenGroupMessageV2]? + public let deletions: [LegacyDeletion]? public let moderators: [String]? } diff --git a/SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyDeletedMessagesResponse.swift similarity index 69% rename from SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyDeletedMessagesResponse.swift index a701594b4..f063cc08c 100644 --- a/SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyDeletedMessagesResponse.swift @@ -3,11 +3,11 @@ import Foundation extension OpenGroupAPI { - struct DeletedMessagesResponse: Codable { + struct LegacyDeletedMessagesResponse: Codable { enum CodingKeys: String, CodingKey { case deletions = "ids" } - let deletions: [Deletion] + let deletions: [LegacyDeletion] } } diff --git a/SessionMessagingKit/Open Groups/Models/Deletion.swift b/SessionMessagingKit/Open Groups/Models/LegacyDeletion.swift similarity index 72% rename from SessionMessagingKit/Open Groups/Models/Deletion.swift rename to SessionMessagingKit/Open Groups/Models/LegacyDeletion.swift index 407fcec41..03fc1ae0a 100644 --- a/SessionMessagingKit/Open Groups/Models/Deletion.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyDeletion.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct Deletion: Codable { + public struct LegacyDeletion: Codable { enum CodingKeys: String, CodingKey { case id case deletedMessageID = "deleted_message_id" @@ -12,12 +12,12 @@ extension OpenGroupAPI { let id: Int64 let deletedMessageID: Int64 - public static func from(_ json: JSON) -> Deletion? { + public static func from(_ json: JSON) -> LegacyDeletion? { guard let id = json["id"] as? Int64, let deletedMessageID = json["deleted_message_id"] as? Int64 else { return nil } - return Deletion(id: id, deletedMessageID: deletedMessageID) + return LegacyDeletion(id: id, deletedMessageID: deletedMessageID) } } } diff --git a/SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyMemberCountResponse.swift similarity index 84% rename from SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyMemberCountResponse.swift index 8ca0d8e43..1f7c13cfb 100644 --- a/SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyMemberCountResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - struct MemberCountResponse: Codable { + struct LegacyMemberCountResponse: Codable { enum CodingKeys: String, CodingKey { case memberCount = "member_count" } diff --git a/SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyModeratorsResponse.swift similarity index 75% rename from SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyModeratorsResponse.swift index 40b8fb08a..4846a5faf 100644 --- a/SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyModeratorsResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - struct ModeratorsResponse: Codable { + struct LegacyModeratorsResponse: Codable { let moderators: [String] } } diff --git a/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift b/SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift similarity index 91% rename from SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift rename to SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift index c7a89ea83..3c87036e0 100644 --- a/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift @@ -1,7 +1,7 @@ import Foundation import SessionUtilitiesKit -public struct OpenGroupMessageV2: Codable { +public struct LegacyOpenGroupMessageV2: Codable { enum CodingKeys: String, CodingKey { case serverID = "server_id" case sender = "public_key" @@ -19,7 +19,7 @@ public struct OpenGroupMessageV2: Codable { /// a receiving user can verify that the message wasn't tampered with. public let base64EncodedSignature: String? - public func sign(with publicKey: String) -> OpenGroupMessageV2? { + public func sign(with publicKey: String) -> LegacyOpenGroupMessageV2? { guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return nil } guard let data = Data(base64Encoded: base64EncodedData) else { return nil } guard let signature = try? Ed25519.sign(data, with: userKeyPair) else { @@ -27,7 +27,7 @@ public struct OpenGroupMessageV2: Codable { return nil } - return OpenGroupMessageV2( + return LegacyOpenGroupMessageV2( serverID: serverID, sender: sender, sentTimestamp: sentTimestamp, @@ -39,7 +39,7 @@ public struct OpenGroupMessageV2: Codable { // MARK: - Decoder -extension OpenGroupMessageV2 { +extension LegacyOpenGroupMessageV2 { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) @@ -60,7 +60,7 @@ extension OpenGroupMessageV2 { throw OpenGroupAPI.Error.parsingFailed } - self = OpenGroupMessageV2( + self = LegacyOpenGroupMessageV2( serverID: try? container.decode(Int64.self, forKey: .serverID), sender: sender, sentTimestamp: try container.decode(UInt64.self, forKey: .sentTimestamp), diff --git a/SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift b/SessionMessagingKit/Open Groups/Models/LegacyPublicKeyBody.swift similarity index 85% rename from SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift rename to SessionMessagingKit/Open Groups/Models/LegacyPublicKeyBody.swift index b10e6bdbc..572bdbdbf 100644 --- a/SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyPublicKeyBody.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - struct PublicKeyBody: Codable { + struct LegacyPublicKeyBody: Codable { enum CodingKeys: String, CodingKey { case publicKey = "public_key" } diff --git a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift index 01803253c..85f0841d8 100644 --- a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift @@ -71,3 +71,37 @@ extension OpenGroupAPI { public let details: Room? } } + +// MARK: - Convenience + +extension OpenGroupAPI.RoomPollInfo { + init(room: OpenGroupAPI.Room) { + self.init( + token: room.token, + created: room.created, + name: room.name, + description: room.description, + imageId: room.imageId, + infoUpdates: room.infoUpdates, + messageSequence: room.messageSequence, + activeUsers: room.activeUsers, + activeUsersCutoff: room.activeUsersCutoff, + pinnedMessages: room.pinnedMessages, + admin: room.admin, + globalAdmin: room.globalAdmin, + admins: room.admins, + hiddenAdmins: room.hiddenAdmins, + moderator: room.moderator, + globalModerator: room.globalModerator, + moderators: room.moderators, + hiddenModerators: room.hiddenModerators, + read: room.read, + defaultRead: room.defaultRead, + write: room.write, + defaultWrite: room.defaultWrite, + upload: room.upload, + defaultUpload: room.defaultUpload, + details: nil + ) + } +} diff --git a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift b/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift index 068575d81..92e8db8ae 100644 --- a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift +++ b/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift @@ -45,23 +45,5 @@ extension OpenGroupAPI { try container.encode(whisperMods, forKey: .whisperMods) try container.encodeIfPresent(fileIds, forKey: .fileIds) } - - // MARK: - Signing - - public static func sign(message: Data, for idType: IdPrefix, with publicKey: String) -> (data: Data, signature: Data)? { - guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { - return nil - } - guard let targetKeyPair: ECKeyPair = try? userKeyPair.convert(to: idType, with: publicKey) else { - return nil - } - - guard let signature = try? Ed25519.sign(message, with: targetKeyPair) else { - SNLog("Failed to sign open group message.") - return nil - } - - return (message, signature) - } } } diff --git a/SessionMessagingKit/Open Groups/Models/UpdateMessageRequest.swift b/SessionMessagingKit/Open Groups/Models/UpdateMessageRequest.swift new file mode 100644 index 000000000..aadff442b --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/UpdateMessageRequest.swift @@ -0,0 +1,19 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPI { + public struct UpdateMessageRequest: Codable { + let data: Data + let signature: Data + + // MARK: - Encodable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(data.base64EncodedString(), forKey: .data) + try container.encode(signature.base64EncodedString(), forKey: .signature) + } + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift index 89ba6e8c1..13ce1c4e1 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift @@ -7,11 +7,6 @@ extension OpenGroupAPI { // TODO: Upgrade this to use the non-legacy version. return AnyPromise.from(legacyDeleteMessage(with: serverID, from: room, on: server)) } - - @objc(isUserModerator:forRoom:onServer:) - public static func objc_isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool { - return isUserModerator(publicKey, for: room, on: server) - } @objc(legacyGetDefaultRoomsIfNeeded) public static func objc_legacyGetDefaultRoomsIfNeeded() { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index ecf87d375..a077e01fd 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -11,42 +11,46 @@ public final class OpenGroupAPI: NSObject { public static let defaultServer = "http://116.203.70.33" public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - // MARK: - Cache - - private static var authTokenPromises: Atomic<[String: Promise]> = Atomic([:]) - private static var hasPerformedInitialPoll: [String: Bool] = [:] - private static var hasUpdatedLastOpenDate = false public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue - public static var moderators: [String: [String: Set]] = [:] // Server URL to room ID to set of moderator IDs - public static var defaultRoomsPromise: Promise<[Room]>? - public static var groupImagePromises: [String: Promise] = [:] + + // MARK: - Polling State + + private static var hasPerformedInitialPoll: [String: Bool] = [:] + private static var timeSinceLastPoll: [String: TimeInterval] = [:] + private static var lastPollTime: TimeInterval = .greatestFiniteMagnitude private static let timeSinceLastOpen: TimeInterval = { guard let lastOpen = UserDefaults.standard[.lastOpen] else { return .greatestFiniteMagnitude } return Date().timeIntervalSince(lastOpen) }() + + + // TODO: Remove these + private static var legacyAuthTokenPromises: Atomic<[String: Promise]> = Atomic([:]) + private static var legacyHasUpdatedLastOpenDate = false + private static var legacyGroupImagePromises: [String: Promise] = [:] + // MARK: - Batching & Polling - /// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open Group + /// This is a convenience method which calls `/batch` with a pre-defined set of requests used to update an Open + /// Group, currently this will retrieve: + /// - Capabilities for the server + /// - For each room: + /// - Poll Info + /// - Messages (includes additions and deletions) public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> { - // TODO: Remove comments - // Capabilities - // Fetch each room - // Poll Info - // /room//pollInfo/ instead? - // Fetch messages for each room - // /room/{roomToken}/messages/since/{messageSequence}: - // Fetch deletions for each room (included in messages) + // Store a local copy of the cached state for this server + let hadPerformedInitialPoll: Bool = (hasPerformedInitialPoll[server] == true) + let originalTimeSinceLastPoll: TimeInterval = (timeSinceLastPoll[server] ?? min(lastPollTime, timeSinceLastOpen)) - // old compact_poll data -// public let room: String -// public let statusCode: UInt -// public let messages: [OpenGroupMessageV2]? -// public let deletions: [Deletion]? -// public let moderators: [String]? + // Update the cached state for this server + hasPerformedInitialPoll[server] = true + lastPollTime = min(lastPollTime, timeSinceLastOpen) + UserDefaults.standard[.lastOpen] = Date() + // Generate the requests let requestResponseType: [BatchRequestInfo] = [ BatchRequestInfo( request: Request( @@ -59,27 +63,37 @@ public final class OpenGroupAPI: NSObject { ] .appending( dependencies.storage.getAllV2OpenGroups().values - .filter { $0.server == server.lowercased() } // Note: The `OpenGroupV2` converts the server value to lowercase during init + .filter { $0.server == server.lowercased() } // Note: The `OpenGroupV2` type converts to lowercase in init .flatMap { openGroup -> [BatchRequestInfo] in let lastSeqNo: Int64? = dependencies.storage.getLastMessageServerID(for: openGroup.room, on: server) let targetSeqNo: Int64 = (lastSeqNo ?? 0) + let shouldRetrieveRecentMessages: Bool = ( + lastSeqNo == nil || ( + // If it's the first poll for this launch and it's been longer than + // 'maxInactivityPeriod' then just retrieve recent messages instead + // of trying to get all messages since the last one retrieved + !hadPerformedInitialPoll && + originalTimeSinceLastPoll > OpenGroupAPI.Poller.maxInactivityPeriod + ) + ) return [ BatchRequestInfo( request: Request( server: server, - // TODO: Source the '0' from the open group (will need to add a new field and default to 0) - endpoint: .roomPollInfo(openGroup.room, 0) + endpoint: .roomPollInfo(openGroup.room, openGroup.infoUpdates) ), responseType: RoomPollInfo.self ), BatchRequestInfo( request: Request( server: server, - endpoint: (lastSeqNo == nil ? + endpoint: (shouldRetrieveRecentMessages ? .roomMessagesRecent(openGroup.room) : .roomMessagesSince(openGroup.room, seqNo: targetSeqNo) ) + // TODO: Limit? +// queryParameters: [ .limit: 256 ] ), responseType: [Message].self ) @@ -87,7 +101,6 @@ public final class OpenGroupAPI: NSObject { } ) - // TODO: Handle response (maybe in the poller or the OpenGroupManager?) return batch(server, requests: requestResponseType, using: dependencies) } @@ -121,64 +134,35 @@ public final class OpenGroupAPI: NSObject { } } - // TODO: `/sequence` request. - - public static func compactPoll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise { - let rooms: [String] = dependencies.storage.getAllV2OpenGroups().values - .filter { $0.server == server } - .map { $0.room } - let useMessageLimit = (hasPerformedInitialPoll[server] != true && timeSinceLastOpen > OpenGroupAPI.Poller.maxInactivityPeriod) - - hasPerformedInitialPoll[server] = true - - if !hasUpdatedLastOpenDate { - UserDefaults.standard[.lastOpen] = dependencies.date - hasUpdatedLastOpenDate = true - } - - let requestBody: LegacyCompactPollBody = LegacyCompactPollBody( - requests: rooms - .map { roomId -> LegacyCompactPollBody.Room in - LegacyCompactPollBody.Room( - id: roomId, - fromMessageServerId: (useMessageLimit ? nil : - dependencies.storage.getLastMessageServerID(for: roomId, on: server) - ), - fromDeletionServerId: (useMessageLimit ? nil : - dependencies.storage.getLastDeletionServerID(for: roomId, on: server) - ), - legacyAuthToken: nil - ) - } - ) + /// The requests are guaranteed to be performed sequentially in the order given in the request and will abort if any request does not return a status-`2xx` response. + /// + /// For example, this can be used to ban and delete all of a user's messages by sequencing the ban followed by the `delete_all`: if the ban fails (e.g. because + /// permission is denied) then the `delete_all` will not occur. The batch body and response are identical to the `/batch` endpoint; requests that are not + /// carried out because of an earlier failure will have a response code of `412` (Precondition Failed)." + private static func sequence(_ server: String, requests: [BatchRequestInfo], using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable)]> { + let requestBody: BatchRequest = requests.map { BatchSubRequest(request: $0.request) } + let responseTypes = requests.map { $0.responseType } guard let body: Data = try? JSONEncoder().encode(requestBody) else { return Promise(error: HTTP.Error.invalidJSON) } - - let request = Request( + + let request: Request = Request( method: .post, server: server, - endpoint: .legacyCompactPoll(legacyAuth: false), + endpoint: .sequence, body: body ) + // TODO: Handle a `412` response (ie. a required capability isn't supported) return send(request, using: dependencies) - .then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise in - guard let data: Data = maybeData else { throw Error.parsingFailed } - - let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed) - - return when( - fulfilled: response.results - .map { (result: LegacyCompactPollResponse.Result) in - legacyProcess(messages: result.messages, for: result.room, on: server) - .then(on: OpenGroupAPI.workQueue) { _ in - process(deletions: result.deletions, for: result.room, on: server) - } - } - ).then(on: OpenGroupAPI.workQueue) { _ in Promise.value(response) } - } + .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) + .map { result in + result.enumerated() + .reduce(into: [:]) { prev, next in + prev[requests[next.offset].request.endpoint] = next.element + } + } } // MARK: - Capabilities @@ -239,13 +223,13 @@ public final class OpenGroupAPI: NSObject { using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Message)> { // TODO: Change this to use '.blinded' once it's working. - guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else { + guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, for: .standard, with: serverPublicKey) else { return Promise(error: Error.signingFailed) } let requestBody: SendMessageRequest = SendMessageRequest( - data: signedRequest.data, - signature: signedRequest.signature, + data: signedMessage.data, + signature: signedMessage.signature, whisperTo: whisperTo, whisperMods: whisperMods, fileIds: nil // TODO: Add support for 'fileIds'. @@ -265,62 +249,97 @@ public final class OpenGroupAPI: NSObject { return send(request, using: dependencies) .decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) } + + public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> { + let request: Request = Request( + server: server, + endpoint: .roomMessageIndividual(roomToken, id: id) + ) + return send(request, using: dependencies) + .decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) + } + + public static func messageUpdate( + _ id: Int64, + plaintext: Data, + in roomToken: String, + on server: String, + with serverPublicKey: String, + using dependencies: Dependencies = Dependencies() + ) -> Promise<(OnionRequestResponseInfoType, Data?)> { + // TODO: Change this to use '.blinded' once it's working. + guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, for: .standard, with: serverPublicKey) else { + return Promise(error: Error.signingFailed) + } + + let requestBody: UpdateMessageRequest = UpdateMessageRequest( + data: signedMessage.data, + signature: signedMessage.signature + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: Error.parsingFailed) + } + + let request: Request = Request( + method: .put, + server: server, + endpoint: .roomMessageIndividual(roomToken, id: id), + body: body + ) + + // TODO: Handle custom response info? + return send(request, using: dependencies) + } + + /// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()` + /// method, if the logic should change then remove the `@available` line and make sure to route the response of this method to + /// the `OpenGroupManager` `handleMessages` method (otherwise the logic may not work correctly) + @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") public static func recentMessages(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { - // TODO: Recent vs. Since?. let request: Request = Request( server: server, endpoint: .roomMessagesRecent(roomToken) // TODO: Limit?. -// queryParameters: [ -// .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } -// ].compactMapValues { $0 } +// queryParameters: [ .limit: 50 ] ) return send(request, using: dependencies) .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) - .then(on: OpenGroupAPI.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in - process(messages: messages, for: roomToken, on: server, using: dependencies) - .map { processedMessages in (responseInfo, processedMessages) } - } } + /// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()` + /// method, if the logic should change then remove the `@available` line and make sure to route the response of this method to + /// the `OpenGroupManager` `handleMessages` method (otherwise the logic may not work correctly) + @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { - // TODO: Recent vs. Since?. + // TODO: Do we need to be able to load old messages? let request: Request = Request( server: server, endpoint: .roomMessagesBefore(roomToken, id: messageId) // TODO: Limit?. -// queryParameters: [ -// .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } -// ].compactMapValues { $0 } +// queryParameters: [ .limit: 50 ] ) return send(request, using: dependencies) .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) - .then(on: OpenGroupAPI.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in - process(messages: messages, for: roomToken, on: server, using: dependencies) - .map { processedMessages in (responseInfo, processedMessages) } - } } + /// This is the direct request to retrieve recent messages from an Open Group so should be retrieved automatically from the `poll()` + /// method, if the logic should change then remove the `@available` line and make sure to route the response of this method to + /// the `OpenGroupManager` `handleMessages` method (otherwise the logic may not work correctly) + @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { - // TODO: Recent vs. Since?. let request: Request = Request( server: server, endpoint: .roomMessagesSince(roomToken, seqNo: seqNo) // TODO: Limit?. -// queryParameters: [ -// .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } -// ].compactMapValues { $0 } +// queryParameters: [ .limit: 50 ] ) return send(request, using: dependencies) .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) - .then(on: OpenGroupAPI.workQueue) { responseInfo, messages -> Promise<(OnionRequestResponseInfoType, [Message])> in - process(messages: messages, for: roomToken, on: server, using: dependencies) - .map { processedMessages in (responseInfo, processedMessages) } - } } // MARK: - Pinning @@ -360,45 +379,6 @@ public final class OpenGroupAPI: NSObject { // MARK: - Files - // TODO: Shift this logic to the `OpenGroupManager` (makes more sense since it's not API logic). - public static func roomImage(_ fileId: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { - // Normally the image for a given group is stored with the group thread, so it's only - // fetched once. However, on the join open group screen we show images for groups the - // user * hasn't * joined yet. We don't want to re-fetch these images every time the - // user opens the app because that could slow the app down or be data-intensive. So - // instead we assume that these images don't change that often and just fetch them once - // a week. We also assume that they're all fetched at the same time as well, so that - // we only need to maintain one date in user defaults. On top of all of this we also - // don't double up on fetch requests by storing the existing request as a promise if - // there is one. - let lastOpenGroupImageUpdate: Date? = UserDefaults.standard[.lastOpenGroupImageUpdate] - let now: Date = dependencies.date - let timeSinceLastUpdate: TimeInterval = (given(lastOpenGroupImageUpdate) { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude) - let updateInterval: TimeInterval = (7 * 24 * 60 * 60) - - if let data = dependencies.storage.getOpenGroupImage(for: roomToken, on: server), server == defaultServer, timeSinceLastUpdate < updateInterval { - return Promise.value(data) - } - - if let promise = groupImagePromises["\(server).\(roomToken)"] { - return promise - } - - let promise: Promise = downloadFile(fileId, from: roomToken, on: server, using: dependencies) - .map { _, data in data } - _ = promise.done(on: OpenGroupAPI.workQueue) { imageData in - if server == defaultServer { - dependencies.storage.write { transaction in - dependencies.storage.setOpenGroupImage(to: imageData, for: roomToken, on: server, using: transaction) - } - UserDefaults.standard[.lastOpenGroupImageUpdate] = now - } - } - groupImagePromises["\(server).\(roomToken)"] = promise - - return promise - } - public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { let request: Request = Request( method: .post, @@ -475,13 +455,13 @@ public final class OpenGroupAPI: NSObject { public static func sendMessageRequest(_ plaintext: Data, to sessionId: String, on server: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> { // TODO: Change this to use '.blinded' once it's working - guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else { + guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, for: .standard, with: serverPublicKey) else { return Promise(error: Error.signingFailed) } let requestBody: SendDirectMessageRequest = SendDirectMessageRequest( - data: signedRequest.data, - signature: signedRequest.signature + data: signedMessage.data, + signature: signedMessage.signature ) guard let body: Data = try? JSONEncoder().encode(requestBody) else { @@ -609,96 +589,24 @@ public final class OpenGroupAPI: NSObject { .decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed) } - // MARK: - Processing - // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API). - - private static func process(messages: [Message]?, for room: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Message]> { - guard let messages: [Message] = messages, !messages.isEmpty else { return Promise.value([]) } - - let seqNo: Int64 = (messages.compactMap { $0.seqNo }.max() ?? 0) - let lastMessageSeqNo: Int64 = (dependencies.storage.getLastMessageServerID(for: room, on: server) ?? 0) - - if seqNo > lastMessageSeqNo { - let (promise, seal) = Promise<[Message]>.pending() - - dependencies.storage.write( - with: { transaction in - dependencies.storage.setLastMessageServerID(for: room, on: server, to: seqNo, using: transaction) - }, - completion: { - seal.fulfill(messages) - } - ) - - return promise - } - - return Promise.value(messages) - } - - private static func process(deletions: [Deletion]?, for room: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Deletion]> { - guard let deletions: [Deletion] = deletions else { return Promise.value([]) } - - let serverID: Int64 = (deletions.compactMap { $0.id }.max() ?? 0) - let lastDeletionServerID: Int64 = (dependencies.storage.getLastDeletionServerID(for: room, on: server) ?? 0) - - if serverID > lastDeletionServerID { - let (promise, seal) = Promise<[Deletion]>.pending() - - dependencies.storage.write( - with: { transaction in - dependencies.storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction) - }, - completion: { - seal.fulfill(deletions) - } - ) - - return promise - } - - return Promise.value(deletions) - } - - public static func isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool { - return moderators[server]?[room]?.contains(publicKey) ?? false - } - - // MARK: - General - - // TODO: Shift this to the OpenGroupManager? (seems more at place there than in the API) - public static func getDefaultRoomsIfNeeded(using dependencies: Dependencies = Dependencies()) { - Storage.shared.write( - with: { transaction in - dependencies.storage.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) - }, - completion: { - let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { - OpenGroupAPI.rooms(for: defaultServer, using: dependencies) - .map { _, data in data } - } - _ = promise.done(on: OpenGroupAPI.workQueue) { items in - items - .compactMap { room -> (Int64, String)? in - guard let imageId: Int64 = room.imageId else { return nil} - - return (imageId, room.token) - } - .forEach { imageId, roomToken in - roomImage(imageId, for: roomToken, on: defaultServer, using: dependencies) - .retainUntilComplete() - } - } - promise.catch(on: OpenGroupAPI.workQueue) { _ in - OpenGroupAPI.defaultRoomsPromise = nil - } - defaultRoomsPromise = promise - } - ) - } - // MARK: - Authentication + public static func sign(message: Data, for idType: IdPrefix, with publicKey: String, using dependencies: Dependencies = Dependencies()) -> (data: Data, signature: Data)? { + guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else { + return nil + } + guard let targetKeyPair: ECKeyPair = try? userKeyPair.convert(to: idType, with: publicKey) else { + return nil + } + + guard let signature = try? Ed25519.sign(message, with: targetKeyPair) else { + SNLog("Failed to sign open group message.") + return nil + } + + return (message, signature) + } + private static func sign(_ request: URLRequest, with publicKey: String, using dependencies: Dependencies = Dependencies()) -> URLRequest? { guard let url: URL = request.url else { return nil } @@ -812,7 +720,7 @@ public final class OpenGroupAPI: NSObject { return Promise.value(authToken) } - if let authTokenPromise: Promise = authTokenPromises.wrappedValue["\(server).\(room)"] { + if let authTokenPromise: Promise = legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] { return authTokenPromise } @@ -830,13 +738,13 @@ public final class OpenGroupAPI: NSObject { promise .done(on: OpenGroupAPI.workQueue) { _ in - authTokenPromises.wrappedValue["\(server).\(room)"] = nil + legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] = nil } .catch(on: OpenGroupAPI.workQueue) { _ in - authTokenPromises.wrappedValue["\(server).\(room)"] = nil + legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] = nil } - authTokenPromises.wrappedValue["\(server).\(room)"] = promise + legacyAuthTokenPromises.wrappedValue["\(server).\(room)"] = promise return promise } @@ -859,7 +767,7 @@ public final class OpenGroupAPI: NSObject { return legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in guard let data: Data = maybeData else { throw Error.parsingFailed } - let response = try data.decoded(as: AuthTokenResponse.self, customError: Error.parsingFailed) + let response = try data.decoded(as: LegacyAuthTokenResponse.self, customError: Error.parsingFailed) let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey) guard let tokenAsData = try? AESGCM.decrypt(response.challenge.ciphertext, with: symmetricKey) else { @@ -872,7 +780,7 @@ public final class OpenGroupAPI: NSObject { @available(*, deprecated, message: "Use request signing instead") public static func legacyClaimAuthToken(_ authToken: String, for room: String, on server: String) -> Promise { - let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + let requestBody: LegacyPublicKeyBody = LegacyPublicKeyBody(publicKey: getUserHexEncodedPublicKey()) guard let body: Data = try? JSONEncoder().encode(requestBody) else { return Promise(error: HTTP.Error.invalidJSON) @@ -926,9 +834,9 @@ public final class OpenGroupAPI: NSObject { hasPerformedInitialPoll[server] = true - if !hasUpdatedLastOpenDate { + if !legacyHasUpdatedLastOpenDate { UserDefaults.standard[.lastOpen] = Date() - hasUpdatedLastOpenDate = true + legacyHasUpdatedLastOpenDate = true } for room in rooms { @@ -985,7 +893,7 @@ public final class OpenGroupAPI: NSObject { return when( fulfilled: response.results - .compactMap { (result: LegacyCompactPollResponse.Result) -> Promise<[Deletion]>? in + .compactMap { (result: LegacyCompactPollResponse.Result) -> Promise<[LegacyDeletion]>? in // A 401 means that we didn't provide a (valid) auth token for a route that // required one. We use this as an indication that the token we're using has // expired. Note that a 403 has a different meaning; it means that we provided @@ -1000,7 +908,7 @@ public final class OpenGroupAPI: NSObject { } return legacyProcess(messages: result.messages, for: result.room, on: server) - .then(on: OpenGroupAPI.workQueue) { _ -> Promise<[Deletion]> in + .then(on: OpenGroupAPI.workQueue) { _ -> Promise<[LegacyDeletion]> in legacyProcess(deletions: result.deletions, for: result.room, on: server) } } @@ -1023,7 +931,7 @@ public final class OpenGroupAPI: NSObject { items.forEach { legacyGetGroupImage(for: $0.id, on: defaultServer).retainUntilComplete() } } promise.catch(on: OpenGroupAPI.workQueue) { _ in - OpenGroupAPI.defaultRoomsPromise = nil + OpenGroupAPI.legacyDefaultRoomsPromise = nil } legacyDefaultRoomsPromise = promise } @@ -1085,7 +993,7 @@ public final class OpenGroupAPI: NSObject { return Promise.value(data) } - if let promise = groupImagePromises["\(server).\(room)"] { + if let promise = legacyGroupImagePromises["\(server).\(room)"] { return promise } @@ -1109,7 +1017,7 @@ public final class OpenGroupAPI: NSObject { return response.data } - groupImagePromises["\(server).\(room)"] = promise + legacyGroupImagePromises["\(server).\(room)"] = promise return promise } @@ -1125,7 +1033,7 @@ public final class OpenGroupAPI: NSObject { return legacySend(request) .map(on: OpenGroupAPI.workQueue) { _, maybeData in guard let data: Data = maybeData else { throw Error.parsingFailed } - let response: MemberCountResponse = try data.decoded(as: MemberCountResponse.self, customError: Error.parsingFailed) + let response: LegacyMemberCountResponse = try data.decoded(as: LegacyMemberCountResponse.self, customError: Error.parsingFailed) let storage = SNMessagingKitConfiguration.shared.storage storage.write { transaction in @@ -1171,7 +1079,7 @@ public final class OpenGroupAPI: NSObject { // MARK: - Legacy Message Sending & Receiving @available(*, deprecated, message: "Use send(_:to:on:whisperTo:whisperMods:with:) instead") - public static func legacySend(_ message: OpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise { + public static func legacySend(_ message: LegacyOpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise { guard let signedMessage = message.sign(with: publicKey) else { return Promise(error: Error.signingFailed) } guard let body: Data = try? JSONEncoder().encode(signedMessage) else { return Promise(error: Error.parsingFailed) @@ -1180,7 +1088,7 @@ public final class OpenGroupAPI: NSObject { return legacySend(request).map(on: OpenGroupAPI.workQueue) { _, maybeData in guard let data: Data = maybeData else { throw Error.parsingFailed } - let message: OpenGroupMessageV2 = try data.decoded(as: OpenGroupMessageV2.self, customError: Error.parsingFailed) + let message: LegacyOpenGroupMessageV2 = try data.decoded(as: LegacyOpenGroupMessageV2.self, customError: Error.parsingFailed) Storage.shared.write { transaction in Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction) } @@ -1189,7 +1097,7 @@ public final class OpenGroupAPI: NSObject { } @available(*, deprecated, message: "Use recentMessages(in:on:) or messagesSince(seqNo:in:on:) instead") - public static func legacyGetMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { + public static func legacyGetMessages(for room: String, on server: String) -> Promise<[LegacyOpenGroupMessageV2]> { let storage = SNMessagingKitConfiguration.shared.storage let request: Request = Request( server: server, @@ -1200,9 +1108,9 @@ public final class OpenGroupAPI: NSObject { ].compactMapValues { $0 } ) - return legacySend(request).then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<[OpenGroupMessageV2]> in + return legacySend(request).then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<[LegacyOpenGroupMessageV2]> in guard let data: Data = maybeData else { throw Error.parsingFailed } - let messages: [OpenGroupMessageV2] = try data.decoded(as: [OpenGroupMessageV2].self, customError: Error.parsingFailed) + let messages: [LegacyOpenGroupMessageV2] = try data.decoded(as: [LegacyOpenGroupMessageV2].self, customError: Error.parsingFailed) return legacyProcess(messages: messages, for: room, on: server) } @@ -1224,7 +1132,7 @@ public final class OpenGroupAPI: NSObject { } @available(*, deprecated, message: "Use v4 endpoint instead") - public static func legacyGetDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> { + public static func legacyGetDeletedMessages(for room: String, on server: String) -> Promise<[LegacyDeletion]> { let storage = SNMessagingKitConfiguration.shared.storage let request: Request = Request( @@ -1236,11 +1144,11 @@ public final class OpenGroupAPI: NSObject { ].compactMapValues { $0 } ) - return legacySend(request).then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<[Deletion]> in + return legacySend(request).then(on: OpenGroupAPI.workQueue) { _, maybeData -> Promise<[LegacyDeletion]> in guard let data: Data = maybeData else { throw Error.parsingFailed } - let response: DeletedMessagesResponse = try data.decoded(as: DeletedMessagesResponse.self, customError: Error.parsingFailed) + let response: LegacyDeletedMessagesResponse = try data.decoded(as: LegacyDeletedMessagesResponse.self, customError: Error.parsingFailed) - return process(deletions: response.deletions, for: room, on: server) + return legacyProcess(deletions: response.deletions, for: room, on: server) } } @@ -1257,7 +1165,7 @@ public final class OpenGroupAPI: NSObject { return legacySend(request) .map(on: OpenGroupAPI.workQueue) { _, maybeData in guard let data: Data = maybeData else { throw Error.parsingFailed } - let response: ModeratorsResponse = try data.decoded(as: ModeratorsResponse.self, customError: Error.parsingFailed) + let response: LegacyModeratorsResponse = try data.decoded(as: LegacyModeratorsResponse.self, customError: Error.parsingFailed) if var x = self.moderators[server] { x[room] = Set(response.moderators) @@ -1273,7 +1181,7 @@ public final class OpenGroupAPI: NSObject { @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyBan(_ publicKey: String, from room: String, on server: String) -> Promise { - let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + let requestBody: LegacyPublicKeyBody = LegacyPublicKeyBody(publicKey: getUserHexEncodedPublicKey()) guard let body: Data = try? JSONEncoder().encode(requestBody) else { return Promise(error: HTTP.Error.invalidJSON) @@ -1292,7 +1200,7 @@ public final class OpenGroupAPI: NSObject { @available(*, deprecated, message: "Use v4 endpoint instead") public static func legacyBanAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise { - let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + let requestBody: LegacyPublicKeyBody = LegacyPublicKeyBody(publicKey: getUserHexEncodedPublicKey()) guard let body: Data = try? JSONEncoder().encode(requestBody) else { return Promise(error: HTTP.Error.invalidJSON) @@ -1325,15 +1233,15 @@ public final class OpenGroupAPI: NSObject { // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API) @available(*, deprecated, message: "Use v4 endpoint instead") - private static func legacyProcess(messages: [OpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { - guard let messages: [OpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) } + private static func legacyProcess(messages: [LegacyOpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[LegacyOpenGroupMessageV2]> { + guard let messages: [LegacyOpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) } let storage = SNMessagingKitConfiguration.shared.storage let serverID: Int64 = (messages.compactMap { $0.serverID }.max() ?? 0) let lastMessageServerID: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0) if serverID > lastMessageServerID { - let (promise, seal) = Promise<[OpenGroupMessageV2]>.pending() + let (promise, seal) = Promise<[LegacyOpenGroupMessageV2]>.pending() storage.write( with: { transaction in @@ -1351,15 +1259,15 @@ public final class OpenGroupAPI: NSObject { } @available(*, deprecated, message: "Use v4 endpoint instead") - private static func legacyProcess(deletions: [Deletion]?, for room: String, on server: String) -> Promise<[Deletion]> { - guard let deletions: [Deletion] = deletions else { return Promise.value([]) } + private static func legacyProcess(deletions: [LegacyDeletion]?, for room: String, on server: String) -> Promise<[LegacyDeletion]> { + guard let deletions: [LegacyDeletion] = deletions else { return Promise.value([]) } let storage = SNMessagingKitConfiguration.shared.storage let serverID: Int64 = (deletions.compactMap { $0.id }.max() ?? 0) let lastDeletionServerID: Int64 = (storage.getLastDeletionServerID(for: room, on: server) ?? 0) if serverID > lastDeletionServerID { - let (promise, seal) = Promise<[Deletion]>.pending() + let (promise, seal) = Promise<[LegacyDeletion]>.pending() storage.write( with: { transaction in diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index fee3e6b14..c6ac5b239 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -6,8 +6,15 @@ public final class OpenGroupManager: NSObject { private var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server private var isPolling = false + + // MARK: - Cache + + public static var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>? + private static var groupImagePromises: [String: Promise] = [:] + private static var moderators: [String: [String: Set]] = [:] // Server URL to room ID to set of moderator IDs // MARK: - Polling + @objc public func startPolling() { guard !isPolling else { return } @@ -30,13 +37,13 @@ public final class OpenGroupManager: NSObject { // MARK: - Adding & Removing - public func add(room: String, server: String, publicKey: String, using transaction: Any) -> Promise { + public func add(roomToken: String, server: String, publicKey: String, using transaction: Any) -> Promise { let storage = Storage.shared // Clear any existing data if needed - storage.removeLastMessageServerID(for: room, on: server, using: transaction) - storage.removeLastDeletionServerID(for: room, on: server, using: transaction) - storage.removeAuthToken(for: room, on: server, using: transaction) + storage.removeLastMessageServerID(for: roomToken, on: server, using: transaction) + storage.removeLastDeletionServerID(for: roomToken, on: server, using: transaction) + storage.removeAuthToken(for: roomToken, on: server, using: transaction) // Store the public key storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) @@ -45,111 +52,17 @@ public final class OpenGroupManager: NSObject { let transaction = transaction as! YapDatabaseReadWriteTransaction transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) { - // Get the group info - // TODO: Remove this legacy method -// OpenGroupAPI.legacyGetRoomInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { info in -// // Create the open group model and the thread -// let openGroup = OpenGroupV2(server: server, room: room, name: info.name, publicKey: publicKey, imageID: info.imageID) -// let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) -// let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) -// // Store everything -// storage.write(with: { transaction in -// let transaction = transaction as! YapDatabaseReadWriteTransaction -// let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) -// thread.shouldBeVisible = true -// thread.save(with: transaction) -// storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) -// }, completion: { -// // Start the poller if needed -// if OpenGroupManager.shared.pollers[server] == nil { -// let poller = OpenGroupPollerV2(for: server) -// poller.startIfNeeded() -// OpenGroupManager.shared.pollers[server] = poller -// } -// // Fetch the group image -// OpenGroupAPI.legacyGetGroupImage(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in -// storage.write { transaction in -// // Update the thread -// let transaction = transaction as! YapDatabaseReadWriteTransaction -// let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) -// thread.groupModel.groupImage = UIImage(data: data) -// thread.save(with: transaction) -// } -// }.retainUntilComplete() -// // Finish -// seal.fulfill(()) -// }) -// }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in -// seal.reject(error) -// } - - OpenGroupAPI.room(for: room, on: server) + OpenGroupAPI.room(for: roomToken, on: server) .done(on: DispatchQueue.global(qos: .userInitiated)) { _, room in - // Create the open group model and the thread - let openGroup: OpenGroupV2 = OpenGroupV2( - server: server, - room: room.token, - name: room.name, + OpenGroupManager.handleRoom( + room, publicKey: publicKey, - imageID: room.imageId.map { "\($0)" } // TODO: Update this? - ) - - let groupID: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) - let model: TSGroupModel = TSGroupModel( - title: openGroup.name, - memberIds: [ getUserHexEncodedPublicKey() ], - image: nil, - groupId: groupID, - groupType: .openGroup, - adminIds: [] // TODO: This is part of the 'room' object - ) - - // Store everything - storage.write( - with: { transaction in - let transaction = transaction as! YapDatabaseReadWriteTransaction - let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) - thread.shouldBeVisible = true - thread.save(with: transaction) - storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) - }, - completion: { - // Start the poller if needed - if OpenGroupManager.shared.pollers[server] == nil { - let poller = OpenGroupAPI.Poller(for: server) - poller.startIfNeeded() - OpenGroupManager.shared.pollers[server] = poller - } - - // Fetch the group image (if there is one) - // TODO: Need to test this. - // TODO: Clean this up (can we avoid the if/else with fancy promise wrangling?). - if let imageId: Int64 = room.imageId { - OpenGroupAPI.roomImage(imageId, for: room.token, on: server) - .done(on: DispatchQueue.global(qos: .userInitiated)) { data in - storage.write { transaction in - // Update the thread - let transaction = transaction as! YapDatabaseReadWriteTransaction - let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) - thread.groupModel.groupImage = UIImage(data: data) - thread.save(with: transaction) - } - } - .retainUntilComplete() - } - else { - storage.write { transaction in - // Update the thread - let transaction = transaction as! YapDatabaseReadWriteTransaction - let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) - thread.save(with: transaction) - } - } - - // Finish - seal.fulfill(()) - } - ) + for: roomToken, + on: server, + isBackgroundPoll: false + ) { + seal.fulfill(()) + } } .catch(on: DispatchQueue.global(qos: .userInitiated)) { error in seal.reject(error) @@ -192,7 +105,279 @@ public final class OpenGroupManager: NSObject { } } - // MARK: Convenience + // MARK: - Response Processing + + internal static func handleMessages( + _ messages: [OpenGroupAPI.Message], + for roomToken: String, + on server: String, + isBackgroundPoll: Bool, + using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() + ) { + // Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages + let openGroupID = "\(server).\(roomToken)" + let sortedMessages: [OpenGroupAPI.Message] = messages + .sorted { lhs, rhs in lhs.seqNo < rhs.seqNo } + let seqNo: Int64 = (sortedMessages.last?.seqNo ?? 0) + let lastMessageSeqNo: Int64 = (dependencies.storage.getLastMessageServerID(for: roomToken, on: server) ?? 0) + + dependencies.storage.write { transaction in + var messageServerIDsToRemove: [UInt64] = [] + + // Update the 'lastMessageServerId' value if we've gotten a newer message + if seqNo > lastMessageSeqNo { + dependencies.storage.setLastMessageServerID(for: roomToken, on: server, to: seqNo, using: transaction) + } + + // Process the messages + sortedMessages.forEach { message in + guard let base64EncodedString: String = message.base64EncodedData, let data = Data(base64Encoded: base64EncodedString), let sender: String = message.sender else { + // A message with no data has been deleted so add it to the list to remove + messageServerIDsToRemove.append(UInt64(message.seqNo)) + return + } + + let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted))) + envelope.setContent(data) + envelope.setSource(sender) + + do { + let data = try envelope.buildSerializedData() + let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: UInt64(message.seqNo), isRetry: false, using: transaction) + try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction) + } + catch { + SNLog("Couldn't receive open group message due to error: \(error).") + } + } + + // Handle any deletions that are needed + guard !messageServerIDsToRemove.isEmpty else { return } + guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return } + guard let threadID = dependencies.storage.v2GetThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + return + } + + var messagesToRemove: [TSMessage] = [] + + thread.enumerateInteractions(with: transaction) { interaction, stop in + guard let message: TSMessage = interaction as? TSMessage, messageServerIDsToRemove.contains(message.openGroupServerMessageID) else { return } + messagesToRemove.append(message) + } + + messagesToRemove.forEach { $0.remove(with: transaction) } + } + } + + internal static func handleRoom( + _ room: OpenGroupAPI.Room, + publicKey: String, + for roomToken: String, + on server: String, + isBackgroundPoll: Bool, + using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies(), + completion: (() -> ())? = nil + ) { + OpenGroupManager.handlePollInfo( + OpenGroupAPI.RoomPollInfo(room: room), + publicKey: publicKey, + for: roomToken, + on: server, + isBackgroundPoll: isBackgroundPoll, + using: dependencies, + completion: completion + ) + } + + internal static func handlePollInfo( + _ pollInfo: OpenGroupAPI.RoomPollInfo, + publicKey maybePublicKey: String?, + for roomToken: String, + on server: String, + isBackgroundPoll: Bool, + using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies(), + completion: (() -> ())? = nil + ) { + // Create the open group model and get or create the thread + let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)") + let userPublicKey: String = getUserHexEncodedPublicKey() + let initialModel: TSGroupModel = TSGroupModel( + title: pollInfo.name, + memberIds: [ userPublicKey ], + image: nil, + groupId: groupId, + groupType: .openGroup, + adminIds: (pollInfo.admins ?? []) + ) + + // Store/Update everything + dependencies.storage.write( + with: { transaction in + let transaction = transaction as! YapDatabaseReadWriteTransaction + let thread = TSGroupThread.getOrCreateThread(with: initialModel, transaction: transaction) + let existingOpenGroup: OpenGroupV2? = thread.uniqueId.flatMap { uniqueId -> OpenGroupV2? in + dependencies.storage.getV2OpenGroup(for: uniqueId) + } + + guard let threadUniqueId: String = thread.uniqueId else { return } + guard let publicKey: String = (maybePublicKey ?? existingOpenGroup?.publicKey) else { return } + + let updatedModel: TSGroupModel = TSGroupModel( + title: (pollInfo.name ?? thread.groupModel.groupName), + memberIds: Array(Set(thread.groupModel.groupMemberIds).inserting(userPublicKey)), + image: thread.groupModel.groupImage, + groupId: groupId, + groupType: .openGroup, + adminIds: (pollInfo.admins ?? thread.groupModel.groupAdminIds) + ) + let updatedOpenGroup: OpenGroupV2 = OpenGroupV2( + server: server, + room: (pollInfo.token ?? roomToken), + publicKey: publicKey, + name: (pollInfo.name ?? thread.name()), + groupDescription: (pollInfo.description ?? existingOpenGroup?.description), + imageID: (pollInfo.imageId.map { "\($0)" } ?? existingOpenGroup?.imageID), + infoUpdates: ((pollInfo.infoUpdates ?? existingOpenGroup?.infoUpdates) ?? 0) + ) + let existingUserCount: UInt64? = dependencies.storage.getUserCount(forV2OpenGroupWithID: updatedOpenGroup.id) + + // - Thread changes + thread.shouldBeVisible = true + thread.groupModel = updatedModel + thread.save(with: transaction) + + // - Open Group changes + dependencies.storage.setV2OpenGroup(updatedOpenGroup, for: threadUniqueId, using: transaction) + + // - User Count + dependencies.storage.setUserCount( + to: ((pollInfo.activeUsers.map { UInt64($0) } ?? existingUserCount) ?? 0), + forV2OpenGroupWithID: updatedOpenGroup.id, + using: transaction + ) + }, + completion: { + // Start the poller if needed + if OpenGroupManager.shared.pollers[server] == nil { + OpenGroupManager.shared.pollers[server] = OpenGroupAPI.Poller(for: server) + OpenGroupManager.shared.pollers[server]?.startIfNeeded() + } + + // - Moderators + if let moderators: [String] = pollInfo.moderators { + OpenGroupManager.moderators[server] = (OpenGroupManager.moderators[server] ?? [:]) + .setting(roomToken, Set(moderators)) + } + + // - Room image (if there is one) + if let imageId: Int64 = pollInfo.imageId { + OpenGroupManager.roomImage(imageId, for: roomToken, on: server) + .done(on: DispatchQueue.global(qos: .userInitiated)) { data in + dependencies.storage.write { transaction in + // Update the thread + let transaction = transaction as! YapDatabaseReadWriteTransaction + let thread = TSGroupThread.getOrCreateThread(with: initialModel, transaction: transaction) + thread.groupModel.groupImage = UIImage(data: data) + thread.save(with: transaction) + } + } + .retainUntilComplete() + } + + // Finish + completion?() + } + ) + } + + // MARK: - Convenience + + @objc(isUserModerator:forRoom:onServer:) + public static func isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool { + return (OpenGroupManager.moderators[server]?[room]?.contains(publicKey) ?? false) + } + + public static func getDefaultRoomsIfNeeded(using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) { + // Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again + guard OpenGroupManager.defaultRoomsPromise == nil else { return } + + dependencies.storage.write( + with: { transaction in + dependencies.storage.setOpenGroupPublicKey( + for: OpenGroupAPI.defaultServer, + to: OpenGroupAPI.defaultServerPublicKey, + using: transaction + ) + }, + completion: { + OpenGroupManager.defaultRoomsPromise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { + OpenGroupAPI.rooms(for: OpenGroupAPI.defaultServer, using: dependencies) + .map { _, data in data } + } + OpenGroupManager.defaultRoomsPromise? + .done(on: OpenGroupAPI.workQueue) { items in + items + .compactMap { room -> (Int64, String)? in + guard let imageId: Int64 = room.imageId else { return nil} + + return (imageId, room.token) + } + .forEach { imageId, roomToken in + roomImage(imageId, for: roomToken, on: OpenGroupAPI.defaultServer, using: dependencies) + .retainUntilComplete() + } + } + .catch(on: OpenGroupAPI.workQueue) { _ in + OpenGroupManager.defaultRoomsPromise = nil + } + } + ) + } + + public static func roomImage( + _ fileId: Int64, + for roomToken: String, + on server: String, + using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() + ) -> Promise { + // Normally the image for a given group is stored with the group thread, so it's only + // fetched once. However, on the join open group screen we show images for groups the + // user * hasn't * joined yet. We don't want to re-fetch these images every time the + // user opens the app because that could slow the app down or be data-intensive. So + // instead we assume that these images don't change that often and just fetch them once + // a week. We also assume that they're all fetched at the same time as well, so that + // we only need to maintain one date in user defaults. On top of all of this we also + // don't double up on fetch requests by storing the existing request as a promise if + // there is one. + let lastOpenGroupImageUpdate: Date? = UserDefaults.standard[.lastOpenGroupImageUpdate] + let now: Date = dependencies.date + let timeSinceLastUpdate: TimeInterval = (lastOpenGroupImageUpdate.map { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude) + let updateInterval: TimeInterval = (7 * 24 * 60 * 60) + + if let data = dependencies.storage.getOpenGroupImage(for: roomToken, on: server), server == OpenGroupAPI.defaultServer, timeSinceLastUpdate < updateInterval { + return Promise.value(data) + } + + if let promise = OpenGroupManager.groupImagePromises["\(server).\(roomToken)"] { + return promise + } + + let promise: Promise = OpenGroupAPI + .downloadFile(fileId, from: roomToken, on: server, using: dependencies) + .map { _, data in data } + _ = promise.done(on: OpenGroupAPI.workQueue) { imageData in + if server == OpenGroupAPI.defaultServer { + dependencies.storage.write { transaction in + dependencies.storage.setOpenGroupImage(to: imageData, for: roomToken, on: server, using: transaction) + } + UserDefaults.standard[.lastOpenGroupImageUpdate] = now + } + } + OpenGroupManager.groupImagePromises["\(server).\(roomToken)"] = promise + + return promise + } + public static func parseV2OpenGroup(from string: String) -> (room: String, server: String, publicKey: String)? { guard let url = URL(string: string), let host = url.host ?? given(string.split(separator: "/").first, { String($0) }), let query = url.query else { return nil } // Inputs that should work: diff --git a/SessionMessagingKit/Open Groups/Types/Endpoint.swift b/SessionMessagingKit/Open Groups/Types/Endpoint.swift index 5cdc19785..d7e70b4d1 100644 --- a/SessionMessagingKit/Open Groups/Types/Endpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/Endpoint.swift @@ -20,7 +20,7 @@ extension OpenGroupAPI { // Messages case roomMessage(String) - case roomMessageIndividual(String, String) + case roomMessageIndividual(String, id: Int64) case roomMessagesRecent(String) case roomMessagesBefore(String, id: Int64) case roomMessagesSince(String, seqNo: Int64) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index eedaa0e84..0adea4a7b 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -234,7 +234,7 @@ extension MessageReceiver { // Open groups for openGroupURL in message.openGroups { if let (room, server, publicKey) = OpenGroupManager.parseV2OpenGroup(from: openGroupURL) { - OpenGroupManager.shared.add(room: room, server: server, publicKey: publicKey, using: transaction).retainUntilComplete() + OpenGroupManager.shared.add(roomToken: room, server: server, publicKey: publicKey, using: transaction).retainUntilComplete() } } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 38097ce7c..384589e39 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -80,122 +80,40 @@ extension OpenGroupAPI { } private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable)], isBackgroundPoll: Bool) { - let storage = SNMessagingKitConfiguration.shared.storage - response.forEach { endpoint, response in switch endpoint { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard let responseData: [OpenGroupAPI.Message] = response.data as? [OpenGroupAPI.Message] else { - //SNLog("Open group polling failed due to error: \(error).") - return // TODO: Throw error? + SNLog("Open group polling failed due to invalid data.") + return } - handleMessages(responseData, roomToken: roomToken, isBackgroundPoll: isBackgroundPoll, using: storage) + OpenGroupManager.handleMessages( + responseData, + for: roomToken, + on: server, + isBackgroundPoll: isBackgroundPoll + ) case .roomPollInfo(let roomToken, _): guard let responseData: OpenGroupAPI.RoomPollInfo = response.data as? OpenGroupAPI.RoomPollInfo else { - //SNLog("Open group polling failed due to error: \(error).") - return // TODO: Throw error? + SNLog("Open group polling failed due to invalid data.") + return } - handlePollInfo(responseData, roomToken: roomToken, isBackgroundPoll: isBackgroundPoll, using: storage) + OpenGroupManager.handlePollInfo( + responseData, + publicKey: nil, + for: roomToken, + on: server, + isBackgroundPoll: isBackgroundPoll + ) default: break // No custom handling needed } } } - // MARK: - Custom response handling - // TODO: Shift this logic to the OpenGroupManager? (seems like the place it should belong?) - - private func handleMessages(_ messages: [OpenGroupAPI.Message], roomToken: String, isBackgroundPoll: Bool, using storage: SessionMessagingKitStorageProtocol) { - // Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages - let openGroupID = "\(server).\(roomToken)" - let sortedMessages: [OpenGroupAPI.Message] = messages - .sorted { lhs, rhs in lhs.seqNo < rhs.seqNo } - - storage.write { transaction in - var messageServerIDsToRemove: [UInt64] = [] - - sortedMessages.forEach { message in - guard let base64EncodedString: String = message.base64EncodedData, let data = Data(base64Encoded: base64EncodedString), let sender: String = message.sender else { - // A message with no data has been deleted so add it to the list to remove - messageServerIDsToRemove.append(UInt64(message.seqNo)) - return - } - - let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted))) - envelope.setContent(data) - envelope.setSource(sender) - - do { - let data = try envelope.buildSerializedData() - let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: UInt64(message.seqNo), isRetry: false, using: transaction) - try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: openGroupID, isBackgroundPoll: isBackgroundPoll, using: transaction) - } - catch { - SNLog("Couldn't receive open group message due to error: \(error).") - } - } - - // Handle any deletions that are needed - guard !messageServerIDsToRemove.isEmpty else { return } - guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return } - guard let threadID = storage.v2GetThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { - return - } - - var messagesToRemove: [TSMessage] = [] - - thread.enumerateInteractions(with: transaction) { interaction, stop in - guard let message: TSMessage = interaction as? TSMessage, messageServerIDsToRemove.contains(message.openGroupServerMessageID) else { return } - messagesToRemove.append(message) - } - - messagesToRemove.forEach { $0.remove(with: transaction) } - } - } - - private func handlePollInfo(_ pollInfo: OpenGroupAPI.RoomPollInfo, roomToken: String, isBackgroundPoll: Bool, using storage: SessionMessagingKitStorageProtocol) { - // TODO: Handle other properties???. - - // public let token: String? - // public let created: TimeInterval? - // public let name: String? - // public let description: String? - // public let imageId: Int64? - // - // public let infoUpdates: Int64? - // public let messageSequence: Int64? - // public let activeUsers: Int64? - // public let activeUsersCutoff: Int64? - // public let pinnedMessages: [PinnedMessage]? - // - // public let admin: Bool? - // public let globalAdmin: Bool? - // public let admins: [String]? - // public let hiddenAdmins: [String]? - // - // public let moderator: Bool? - // public let globalModerator: Bool? - // public let moderators: [String]? - // public let hiddenModerators: [String]? - - // - Moderators - OpenGroupAPI.moderators[server] = (OpenGroupAPI.moderators[server] ?? [:]) - .setting(roomToken, Set(pollInfo.moderators ?? [])) - - // public let read: Bool? - // public let defaultRead: Bool? - // public let write: Bool? - // public let defaultWrite: Bool? - // public let upload: Bool? - // public let defaultUpload: Bool? - // - // /// Only populated and different if the `info_updates` counter differs from the provided `info_updated` value - // public let details: Room? - } - // MARK: - Legacy Handling private func handleCompactPollBody(_ body: OpenGroupAPI.LegacyCompactPollResponse.Result, isBackgroundPoll: Bool) { diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index fab20bd78..8df85e35f 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -67,10 +67,6 @@ public protocol SessionMessagingKitStorageProtocol: SessionMessagingKitOpenGroup func setLastDeletionServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) func removeLastDeletionServerID(for room: String, on server: String, using transaction: Any) - // MARK: - Open Group Metadata - - func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) - // MARK: - Message Handling func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift index 5275bc9c6..4914f102c 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPIV2Tests.swift @@ -81,9 +81,19 @@ class OpenGroupAPITests: XCTestCase { ) testStorage.mockData[.allV2OpenGroups] = [ - "0": OpenGroupV2(server: "testServer", room: "testRoom", name: "Test", publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d", imageID: nil) + "0": OpenGroupV2( + server: "testServer", + room: "testRoom", + publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d", + name: "Test", + groupDescription: nil, + imageID: nil, + infoUpdates: 0 + ) + ] + testStorage.mockData[.openGroupPublicKeys] = [ + "testServer": "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d" ] - testStorage.mockData[.openGroupPublicKeys] = ["testServer": "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"] // Test Private key, not actually used (from here https://www.notion.so/oxen/SOGS-Authentication-dc64cc846cb24b2abbf7dd4bfd74abbb) testStorage.mockData[.userKeyPair] = try! ECKeyPair( diff --git a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift index cc5ad174b..fa697392c 100644 --- a/SessionMessagingKitTests/_TestUtilities/TestStorage.swift +++ b/SessionMessagingKitTests/_TestUtilities/TestStorage.swift @@ -13,7 +13,9 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { case allV2OpenGroups case openGroupPublicKeys case userKeyPair + case openGroup case openGroupImage + case openGroupUserCount } typealias Key = DataKey @@ -71,7 +73,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { // MARK: - Open Groups func getAllV2OpenGroups() -> [String: OpenGroupV2] { return (mockData[.allV2OpenGroups] as! [String: OpenGroupV2]) } - func getV2OpenGroup(for threadID: String) -> OpenGroupV2? { return nil } + func getV2OpenGroup(for threadID: String) -> OpenGroupV2? { return (mockData[.openGroup] as? OpenGroupV2) } func v2GetThreadID(for v2OpenGroupID: String) -> String? { return nil } func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set, using transaction: Any) {} @@ -99,10 +101,6 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { func setLastDeletionServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) {} func removeLastDeletionServerID(for room: String, on server: String, using transaction: Any) {} - // MARK: - Open Group Metadata - - func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) {} - // MARK: - Message Handling func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] { return [] } @@ -118,5 +116,19 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable { extension TestStorage: SessionMessagingKitOpenGroupStorageProtocol { func getOpenGroupImage(for room: String, on server: String) -> Data? { return (mockData[.openGroupImage] as? Data) } - func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) {} + func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) { + mockData[.openGroupImage] = data + } + + func setV2OpenGroup(_ openGroup: OpenGroupV2, for threadID: String, using transaction: Any) { + mockData[.openGroup] = openGroup + } + + func getUserCount(forV2OpenGroupWithID openGroupID: String) -> UInt64? { + return (mockData[.openGroupUserCount] as? UInt64) + } + + func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) { + mockData[.openGroupUserCount] = newValue + } } diff --git a/SessionUtilitiesKit/General/Set+Utilities.swift b/SessionUtilitiesKit/General/Set+Utilities.swift new file mode 100644 index 000000000..9c4b8eaca --- /dev/null +++ b/SessionUtilitiesKit/General/Set+Utilities.swift @@ -0,0 +1,12 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Set { + func inserting(_ other: Element) -> Set { + var updatedSet: Set = self + updatedSet.insert(other) + + return updatedSet + } +}