diff --git a/Podfile b/Podfile index ae568f035..7e448b202 100644 --- a/Podfile +++ b/Podfile @@ -66,6 +66,9 @@ abstract_target 'GlobalDependencies' do pod 'Quick' pod 'Nimble' + + # Need to include this for the tests because otherwise it won't actually build + pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage' end end diff --git a/Podfile.lock b/Podfile.lock index 37f4ac9c5..1315eb501 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -242,6 +242,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: f0857369c4831b2e5c1946345e76e493f3286805 +PODFILE CHECKSUM: 0e694576fbda3c10bbc762998183d97142b85896 COCOAPODS: 1.11.3 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c568bb312..e77691409 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -645,6 +645,9 @@ FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; }; FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; }; + FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; + FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; }; + FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; }; FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; @@ -1685,6 +1688,9 @@ FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = ""; }; + FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = ""; }; + FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = ""; }; + FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = ""; }; FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; @@ -3457,6 +3463,7 @@ FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, + FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, ); path = Migrations; sourceTree = ""; @@ -3523,6 +3530,7 @@ FD17D7B727F51ECA00122BE0 /* Migration.swift */, FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */, + FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */, FD7162DA281B6C440060647B /* TypedTableAlias.swift */, FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */, ); @@ -3570,6 +3578,22 @@ path = LegacyDatabase; sourceTree = ""; }; + FD37EA1228AB3F60003AE748 /* Database */ = { + isa = PBXGroup; + children = ( + FD37EA1328AB42C1003AE748 /* Models */, + ); + path = Database; + sourceTree = ""; + }; + FD37EA1328AB42C1003AE748 /* Models */ = { + isa = PBXGroup; + children = ( + FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */, + ); + path = Models; + sourceTree = ""; + }; FD3C905D27E410DB00CD579F /* Common Networking */ = { isa = PBXGroup; children = ( @@ -3659,6 +3683,7 @@ FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */ = { isa = PBXGroup; children = ( + FD37EA1228AB3F60003AE748 /* Database */, FD83B9B927CF20A5005E1583 /* General */, ); path = SessionUtilitiesKitTests; @@ -5018,6 +5043,7 @@ FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, + FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, @@ -5125,6 +5151,7 @@ 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */, FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, + FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, C3D9E3BF25676AD70040E4F3 /* (null) in Sources */, B8BF43BA26CC95FB007828D1 /* WebRTC+Utilities.swift in Sources */, FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, @@ -5444,6 +5471,7 @@ FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, + FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */, FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index 9b0b12090..457a50b89 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -256,7 +256,8 @@ enum MockDataGenerator { _ = try! GroupMember( groupId: randomGroupPublicKey, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ) .saved(db) } @@ -264,7 +265,8 @@ enum MockDataGenerator { _ = try! GroupMember( groupId: randomGroupPublicKey, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ) .saved(db) } diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 928133641..2230a04de 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -17,7 +17,8 @@ public enum SNMessagingKit { // Just to make the external API nice _004_RemoveLegacyYDB.self ], [ - _005_FixDeletedMessageReadState.self + _005_FixDeletedMessageReadState.self, + _006_FixHiddenModAdminSupport.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index e1c8d8b4e..b748da16d 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -650,7 +650,8 @@ enum _003_YDBToGRDBMigration: Migration { try GroupMember( groupId: threadId, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).insert(db) if !validProfileIds.contains(memberId) { @@ -662,7 +663,8 @@ enum _003_YDBToGRDBMigration: Migration { try GroupMember( groupId: threadId, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).insert(db) if !validProfileIds.contains(adminId) { @@ -674,7 +676,8 @@ enum _003_YDBToGRDBMigration: Migration { try GroupMember( groupId: threadId, profileId: zombieId, - role: .zombie + role: .zombie, + isHidden: false ).insert(db) if !validProfileIds.contains(zombieId) { diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift new file mode 100644 index 000000000..c1097eb94 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift @@ -0,0 +1,30 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration fixes an issue where hidden mods/admins weren't getting recognised as mods/admins, it reset's the `info_updates` +/// for open groups so they will fully re-fetch their mod/admin lists +enum _006_FixHiddenModAdminSupport: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "FixHiddenModAdminSupport" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + + static func migrate(_ db: Database) throws { + try db.alter(table: GroupMember.self) { t in + t.add(.isHidden, .boolean) + .notNull() + .defaults(to: false) + } + + // When modifying OpenGroup behaviours we should always look to reset the `infoUpdates` + // value for all OpenGroups to ensure they all have the correct state for newly + // added/changed fields + _ = try OpenGroup + .updateAll(db, OpenGroup.Columns.infoUpdates.set(to: 0)) + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/GroupMember.swift b/SessionMessagingKit/Database/Models/GroupMember.swift index a59bfc417..4cfe0abd4 100644 --- a/SessionMessagingKit/Database/Models/GroupMember.swift +++ b/SessionMessagingKit/Database/Models/GroupMember.swift @@ -17,6 +17,7 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor case groupId case profileId case role + case isHidden } public enum Role: Int, Codable, DatabaseValueConvertible { @@ -29,6 +30,7 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor public let groupId: String public let profileId: String public let role: Role + public let isHidden: Bool // MARK: - Relationships @@ -49,11 +51,13 @@ public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecor public init( groupId: String, profileId: String, - role: Role + role: Role, + isHidden: Bool ) { self.groupId = groupId self.profileId = profileId self.role = role + self.isHidden = isHidden } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index f1c1c6fcf..3e8370ddb 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -422,17 +422,41 @@ public final class OpenGroupManager: NSObject { _ = try GroupMember( groupId: threadId, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).saved(db) } + try roomDetails.hiddenAdmins + .defaulting(to: []) + .forEach { adminId in + _ = try GroupMember( + groupId: threadId, + profileId: adminId, + role: .admin, + isHidden: true + ).saved(db) + } + try roomDetails.moderators.forEach { moderatorId in _ = try GroupMember( groupId: threadId, profileId: moderatorId, - role: .moderator + role: .moderator, + isHidden: false ).saved(db) } + + try roomDetails.hiddenModerators + .defaulting(to: []) + .forEach { moderatorId in + _ = try GroupMember( + groupId: threadId, + profileId: moderatorId, + role: .moderator, + isHidden: true + ).saved(db) + } } db.afterNextTransactionCommit { db in diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index e2312383d..20d52e23f 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -89,7 +89,8 @@ extension MessageReceiver { try GroupMember( groupId: groupPublicKey, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).save(db) } @@ -97,7 +98,8 @@ extension MessageReceiver { try GroupMember( groupId: groupPublicKey, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).save(db) } @@ -254,7 +256,8 @@ extension MessageReceiver { try GroupMember( groupId: id, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).insert(db) } @@ -440,7 +443,8 @@ extension MessageReceiver { try GroupMember( groupId: id, profileId: sender, - role: .zombie + role: .zombie, + isHidden: false ).insert(db) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index ff813332e..bc8d52a0b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -37,7 +37,8 @@ extension MessageSender { try GroupMember( groupId: groupPublicKey, profileId: adminId, - role: .admin + role: .admin, + isHidden: false ).insert(db) } @@ -48,7 +49,8 @@ extension MessageSender { try GroupMember( groupId: groupPublicKey, profileId: memberId, - role: .standard + role: .standard, + isHidden: false ).insert(db) } @@ -374,7 +376,8 @@ extension MessageSender { try GroupMember( groupId: closedGroup.id, profileId: member, - role: .standard + role: .standard, + isHidden: false ).insert(db) } } diff --git a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift index 707f8852d..220c9bad2 100644 --- a/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/CapabilitiesSpec.swift @@ -37,7 +37,7 @@ class CapabilitiesSpec: QuickSpec { describe("a Capability") { context("when initializing") { it("succeeeds with a valid case") { - let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability( + let capability: Capability.Variant = Capability.Variant( from: "sogs" ) @@ -45,7 +45,7 @@ class CapabilitiesSpec: QuickSpec { } it("wraps an unknown value in the unsupported case") { - let capability: OpenGroupAPI.Capabilities.Capability = OpenGroupAPI.Capabilities.Capability( + let capability: Capability.Variant = Capability.Variant( from: "test" ) @@ -55,12 +55,12 @@ class CapabilitiesSpec: QuickSpec { context("when accessing the rawValue") { it("provides known cases exactly") { - expect(OpenGroupAPI.Capabilities.Capability.sogs.rawValue).to(equal("sogs")) - expect(OpenGroupAPI.Capabilities.Capability.blind.rawValue).to(equal("blind")) + expect(Capability.Variant.sogs.rawValue).to(equal("sogs")) + expect(Capability.Variant.blind.rawValue).to(equal("blind")) } it("provides the wrapped value for unsupported cases") { - expect(OpenGroupAPI.Capabilities.Capability.unsupported("test").rawValue).to(equal("test")) + expect(Capability.Variant.unsupported("test").rawValue).to(equal("test")) } } @@ -68,14 +68,14 @@ class CapabilitiesSpec: QuickSpec { it("decodes known cases exactly") { expect( try? JSONDecoder().decode( - OpenGroupAPI.Capabilities.Capability.self, + Capability.Variant.self, from: "\"sogs\"".data(using: .utf8)! ) ) .to(equal(.sogs)) expect( try? JSONDecoder().decode( - OpenGroupAPI.Capabilities.Capability.self, + Capability.Variant.self, from: "\"blind\"".data(using: .utf8)! ) ) @@ -85,7 +85,7 @@ class CapabilitiesSpec: QuickSpec { it("decodes unknown cases into the unsupported case") { expect( try? JSONDecoder().decode( - OpenGroupAPI.Capabilities.Capability.self, + Capability.Variant.self, from: "\"test\"".data(using: .utf8)! ) ) diff --git a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift index 966a270c4..2b8e7c858 100644 --- a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift @@ -76,7 +76,7 @@ class OpenGroupSpec: QuickSpec { ) expect(openGroup.debugDescription) - .to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0)")) + .to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0, pollFailureCount: 0)")) } } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index a67c9c3ee..e4edc8803 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -1237,6 +1237,7 @@ class OpenGroupAPISpec: QuickSpec { sender: "testSender", posted: 321, edited: nil, + deleted: nil, seqNo: 10, whisper: false, whisperMods: false, @@ -1605,6 +1606,7 @@ class OpenGroupAPISpec: QuickSpec { sender: "testSender", posted: 321, edited: nil, + deleted: nil, seqNo: 10, whisper: false, whisperMods: false, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index e14a1099f..9a598a15b 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -186,6 +186,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -1277,7 +1278,12 @@ class OpenGroupManagerSpec: QuickSpec { defaultWrite: nil, upload: false, defaultUpload: nil, - details: TestCapabilitiesAndRoomApi.roomData.with(moderators: ["TestMod"], admins: []) + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: ["TestMod"], + hiddenModerators: [], + admins: [], + hiddenAdmins: [] + ) ) mockStorage.write { db in @@ -1308,7 +1314,67 @@ class OpenGroupManagerSpec: QuickSpec { server: "testServer" ), profileId: "TestMod", - role: .moderator + role: .moderator, + isHidden: false + ) + )) + } + + it("updates for hidden moderators") { + var didComplete: Bool = false // Prevent multi-threading test bugs + + testPollInfo = OpenGroupAPI.RoomPollInfo( + token: "testRoom", + activeUsers: 10, + admin: false, + globalAdmin: false, + moderator: false, + globalModerator: false, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil, + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: [], + hiddenModerators: ["TestMod2"], + admins: [], + hiddenAdmins: [] + ) + ) + + mockStorage.write { db in + try OpenGroupManager.handlePollInfo( + db, + pollInfo: testPollInfo, + publicKey: TestConstants.publicKey, + for: "testRoom", + on: "testServer", + dependencies: dependencies + ) { didComplete = true } + } + + expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50)) + expect( + mockStorage.read { db in + try GroupMember + .filter(GroupMember.Columns.groupId == OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + )) + .fetchOne(db) + } + ).to(equal( + GroupMember( + groupId: OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + ), + profileId: "TestMod2", + role: .moderator, + isHidden: true ) )) } @@ -1368,7 +1434,12 @@ class OpenGroupManagerSpec: QuickSpec { defaultWrite: nil, upload: false, defaultUpload: nil, - details: TestCapabilitiesAndRoomApi.roomData.with(moderators: [], admins: ["TestAdmin"]) + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: [], + hiddenModerators: [], + admins: ["TestAdmin"], + hiddenAdmins: [] + ) ) mockStorage.write { db in @@ -1399,7 +1470,67 @@ class OpenGroupManagerSpec: QuickSpec { server: "testServer" ), profileId: "TestAdmin", - role: .admin + role: .admin, + isHidden: false + ) + )) + } + + it("updates for hidden admins") { + var didComplete: Bool = false // Prevent multi-threading test bugs + + testPollInfo = OpenGroupAPI.RoomPollInfo( + token: "testRoom", + activeUsers: 10, + admin: false, + globalAdmin: false, + moderator: false, + globalModerator: false, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil, + details: TestCapabilitiesAndRoomApi.roomData.with( + moderators: [], + hiddenModerators: [], + admins: [], + hiddenAdmins: ["TestAdmin2"] + ) + ) + + mockStorage.write { db in + try OpenGroupManager.handlePollInfo( + db, + pollInfo: testPollInfo, + publicKey: TestConstants.publicKey, + for: "testRoom", + on: "testServer", + dependencies: dependencies + ) { didComplete = true } + } + + expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50)) + expect( + mockStorage.read { db in + try GroupMember + .filter(GroupMember.Columns.groupId == OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + )) + .fetchOne(db) + } + ).to(equal( + GroupMember( + groupId: OpenGroup.idFor( + roomToken: "testRoom", + server: "testServer" + ), + profileId: "TestAdmin2", + role: .admin, + isHidden: true ) )) } @@ -1978,6 +2109,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: nil, posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -2039,6 +2171,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: nil, posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -2071,6 +2204,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 124, whisper: false, whisperMods: false, @@ -2114,6 +2248,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 122, edited: nil, + deleted: nil, seqNo: 123, whisper: false, whisperMods: false, @@ -2152,6 +2287,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 123, whisper: false, whisperMods: false, @@ -2180,6 +2316,7 @@ class OpenGroupManagerSpec: QuickSpec { sender: "05\(TestConstants.publicKey)", posted: 123, edited: nil, + deleted: nil, seqNo: 123, whisper: false, whisperMods: false, @@ -2328,6 +2465,10 @@ class OpenGroupManagerSpec: QuickSpec { mockSodium .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } .thenReturn(Data(hex: testDirectMessage.sender.removingIdPrefixIfNeeded()).bytes) + + mockSodium + .when { $0.sessionId(any(), matchesBlindedId: any(), serverPublicKey: any(), genericHash: mockGenericHash) } + .thenReturn(false) } it("updates the inbox latest message id") { @@ -2422,6 +2563,10 @@ class OpenGroupManagerSpec: QuickSpec { mockSodium .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } .thenReturn(Data(hex: testDirectMessage.recipient.removingIdPrefixIfNeeded()).bytes) + + mockSodium + .when { $0.sessionId(any(), matchesBlindedId: any(), serverPublicKey: any(), genericHash: mockGenericHash) } + .thenReturn(false) } it("updates the outbox latest message id") { @@ -2602,7 +2747,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(TestConstants.publicKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) } @@ -2621,7 +2767,48 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(TestConstants.publicKey)", - role: .admin + role: .admin, + isHidden: false + ).insert(db) + } + + expect( + OpenGroupManager.isUserModeratorOrAdmin( + "05\(TestConstants.publicKey)", + for: "testRoom", + on: "testServer", + using: dependencies + ) + ).to(beTrue()) + } + + it("returns true if the moderator is hidden") { + mockStorage.write { db in + try GroupMember( + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + profileId: "05\(TestConstants.publicKey)", + role: .moderator, + isHidden: true + ).insert(db) + } + + expect( + OpenGroupManager.isUserModeratorOrAdmin( + "05\(TestConstants.publicKey)", + for: "testRoom", + on: "testServer", + using: dependencies + ) + ).to(beTrue()) + } + + it("returns true if the admin is hidden") { + mockStorage.write { db in + try GroupMember( + groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), + profileId: "05\(TestConstants.publicKey)", + role: .admin, + isHidden: true ).insert(db) } @@ -2672,7 +2859,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "00\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .ed25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db) @@ -2709,7 +2897,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "15\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) } @@ -2766,7 +2955,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db) @@ -2805,7 +2995,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "15\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db) @@ -2911,7 +3102,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "05\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: otherKey)!).save(db) @@ -2951,7 +3143,8 @@ class OpenGroupManagerSpec: QuickSpec { try GroupMember( groupId: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), profileId: "00\(otherKey)", - role: .moderator + role: .moderator, + isHidden: false ).insert(db) try Identity(variant: .x25519PublicKey, data: Data.data(fromHex: TestConstants.publicKey)!).save(db) @@ -2977,6 +3170,7 @@ class OpenGroupManagerSpec: QuickSpec { context("when getting the default rooms if needed") { beforeEach { class TestRoomsApi: TestOnionRequestAPI { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) static let roomsData: [OpenGroupAPI.Room] = [ TestCapabilitiesAndRoomApi.roomData, OpenGroupAPI.Room( @@ -3009,7 +3203,26 @@ class OpenGroupManagerSpec: QuickSpec { ] override class var mockResponse: Data? { - return try! JSONEncoder().encode(roomsData) + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomsData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } dependencies = dependencies.with(onionApi: TestRoomsApi.self) @@ -3178,6 +3391,7 @@ class OpenGroupManagerSpec: QuickSpec { it("fetches the image for any rooms with images") { class TestRoomsApi: TestOnionRequestAPI { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) static let roomsData: [OpenGroupAPI.Room] = [ OpenGroupAPI.Room( token: "test2", @@ -3209,7 +3423,26 @@ class OpenGroupManagerSpec: QuickSpec { ] override class var mockResponse: Data? { - return try! JSONEncoder().encode(roomsData) + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomsData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) } } let testDate: Date = Date(timeIntervalSince1970: 1234567890) @@ -3218,7 +3451,9 @@ class OpenGroupManagerSpec: QuickSpec { date: testDate ) - OpenGroupManager.getDefaultRoomsIfNeeded(using: dependencies) + OpenGroupManager + .getDefaultRoomsIfNeeded(using: dependencies) + .retainUntilComplete() expect(mockUserDefaults) .toEventually( @@ -3674,7 +3909,12 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - Room Convenience Extensions extension OpenGroupAPI.Room { - func with(moderators: [String], admins: [String]) -> OpenGroupAPI.Room { + func with( + moderators: [String], + hiddenModerators: [String], + admins: [String], + hiddenAdmins: [String] + ) -> OpenGroupAPI.Room { return OpenGroupAPI.Room( token: self.token, name: self.name, @@ -3689,11 +3929,11 @@ extension OpenGroupAPI.Room { admin: self.admin, globalAdmin: self.globalAdmin, admins: admins, - hiddenAdmins: self.hiddenAdmins, + hiddenAdmins: hiddenAdmins, moderator: self.moderator, globalModerator: self.globalModerator, moderators: moderators, - hiddenModerators: self.hiddenModerators, + hiddenModerators: hiddenModerators, read: self.read, defaultRead: self.defaultRead, defaultAccessible: self.defaultAccessible, diff --git a/SessionUtilitiesKit/Database/Models/Identity.swift b/SessionUtilitiesKit/Database/Models/Identity.swift index cc9749ce8..9d08731f5 100644 --- a/SessionUtilitiesKit/Database/Models/Identity.swift +++ b/SessionUtilitiesKit/Database/Models/Identity.swift @@ -129,14 +129,16 @@ public extension Identity { ) } - static func fetchHexEncodedSeed() -> String? { - return Storage.shared.read { db in - guard let data: Data = try? Identity.fetchOne(db, id: .seed)?.data else { - return nil - } - - return data.toHexString() + static func fetchHexEncodedSeed(_ db: Database? = nil) -> String? { + guard let db: Database = db else { + return Storage.shared.read { db in fetchHexEncodedSeed(db) } } + + guard let data: Data = try? Identity.fetchOne(db, id: .seed)?.data else { + return nil + } + + return data.toHexString() } } diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift new file mode 100644 index 000000000..4c21fb753 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlteration.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +/// This is a convenience wrapper around the GRDB `TableAlteration` class which allows for shorthand +/// when creating tables +public class TypedTableAlteration where T: TableRecord, T: ColumnExpressible { + let alteration: TableAlteration + + init(alteration: TableAlteration) { + self.alteration = alteration + } + + @discardableResult public func add(_ key: T.Columns, _ type: Database.ColumnType? = nil) -> ColumnDefinition { + return alteration.add(column: key.name, type) + } + + public func rename(column: String, to key: T.Columns) { + alteration.rename(column: column, to: key.name) + } + + public func drop(_ key: T.Columns) { + return alteration.drop(column: key.name) + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift index 09a6cb7a5..278f52766 100644 --- a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift @@ -16,6 +16,17 @@ public extension Database { } } + func alter( + table: T.Type, + body: (TypedTableAlteration) -> Void + ) throws where T: TableRecord, T: ColumnExpressible { + try alter(table: T.databaseTableName) { tableAlteration in + let typedAlteration: TypedTableAlteration = TypedTableAlteration(alteration: tableAlteration) + + body(typedAlteration) + } + } + func makeFTS5Pattern(rawPattern: String, forTable table: T.Type) throws -> FTS5Pattern where T: TableRecord, T: ColumnExpressible { return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName) } diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift new file mode 100644 index 000000000..135e80dc9 --- /dev/null +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -0,0 +1,105 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +import Quick +import Nimble + +@testable import SessionUtilitiesKit + +class IdentitySpec: QuickSpec { + // MARK: - Spec + + override func spec() { + var mockStorage: Storage! + + describe("an Identity") { + beforeEach { + mockStorage = Storage( + customWriter: DatabaseQueue(), + customMigrations: [ + SNUtilitiesKit.migrations() + ] + ) + } + + it("correctly retrieves the user user public key") { + mockStorage.write { db in + try Identity(variant: .x25519PublicKey, data: "Test1".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.fetchUserPublicKey(db)) + .to(equal("Test1".data(using: .utf8))) + } + } + + it("correctly retrieves the user private key") { + mockStorage.write { db in + try Identity(variant: .x25519PrivateKey, data: "Test2".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.fetchUserPrivateKey(db)) + .to(equal("Test2".data(using: .utf8))) + } + } + + it("correctly retrieves the user key pair") { + mockStorage.write { db in + try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db) + try Identity(variant: .x25519PrivateKey, data: "Test4".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + let keyPair = Identity.fetchUserKeyPair(db) + + expect(keyPair?.publicKey) + .to(equal("Test3".data(using: .utf8)?.bytes)) + expect(keyPair?.secretKey) + .to(equal("Test4".data(using: .utf8)?.bytes)) + } + } + + it("correctly determines if the user exists") { + mockStorage.write { db in + try Identity(variant: .x25519PublicKey, data: "Test3".data(using: .utf8)!).insert(db) + try Identity(variant: .x25519PrivateKey, data: "Test4".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.userExists(db)) + .to(equal(true)) + } + } + + it("correctly retrieves the user ED25519 key pair") { + mockStorage.write { db in + try Identity(variant: .ed25519PublicKey, data: "Test5".data(using: .utf8)!).insert(db) + try Identity(variant: .ed25519SecretKey, data: "Test6".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + let keyPair = Identity.fetchUserEd25519KeyPair(db) + + expect(keyPair?.publicKey) + .to(equal("Test5".data(using: .utf8)?.bytes)) + expect(keyPair?.secretKey) + .to(equal("Test6".data(using: .utf8)?.bytes)) + } + } + + it("correctly retrieves the hex encoded seed") { + mockStorage.write { db in + try Identity(variant: .seed, data: "Test7".data(using: .utf8)!).insert(db) + } + + mockStorage.read { db in + expect(Identity.fetchHexEncodedSeed(db)) + .to(equal("5465737437")) + } + } + } + } +}