Further work on unit tests (and a couple of bug fixes found when testing)

Removed a couple remaining TODOs
Added 'standardUserDefaults' to the 'Dependencies' type
Tweaked the OpenGroupAPI to only update the 'lastOpen' timestamp if it successfully polls
Refactored a couple of methods in the ConversationViewItem into swift so we can clean up the OpenGroupAPI more
Updated the OpenGroupAPI so it no longer has static variables for state (shifted to the OpenGroupManager and made them instance variables)
Fixed an encoding issue with the Capabilities.Capability
This commit is contained in:
Morgan Pretty 2022-03-07 17:43:30 +11:00
parent f9468219d9
commit 17a9e510c5
19 changed files with 1118 additions and 480 deletions

View file

@ -753,7 +753,6 @@
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
C3DB66AC260ACA42001EFC55 /* OpenGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */; };
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */; };
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66CB260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift */; };
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */; };
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */; };
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ECBF7A257056B700EA7FCE /* Threading.swift */; };
@ -795,6 +794,8 @@
FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; };
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; };
FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; };
FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* TestUserDefaults.swift */; };
FD83B9D427D5A7D5005E1583 /* ConversationViewItem+Refactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */; };
FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; };
FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* TestSodium.swift */; };
FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* TestSign.swift */; };
@ -990,13 +991,6 @@
remoteGlobalIDString = C3C2A678255388CC00C340D1;
remoteInfo = SessionUtilitiesKit;
};
FDC438BA27BB276F00C60D73 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D221A080169C9E5E00537ABF /* Project object */;
proxyType = 1;
remoteGlobalIDString = D221A088169C9E5E00537ABF;
remoteInfo = Session;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -1866,7 +1860,6 @@
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = "<group>"; };
C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManager.swift; sourceTree = "<group>"; };
C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupPoller.swift; sourceTree = "<group>"; };
C3DB66CB260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+ObjC.swift"; sourceTree = "<group>"; };
C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sheet.swift; sourceTree = "<group>"; };
C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditClosedGroupVC.swift; sourceTree = "<group>"; };
C3E7134E251C867C009649BB /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
@ -1933,6 +1926,8 @@
FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = "<group>"; };
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = "<group>"; };
FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
FD83B9D127D59495005E1583 /* TestUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUserDefaults.swift; sourceTree = "<group>"; };
FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewItem+Refactor.swift"; sourceTree = "<group>"; };
FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = "<group>"; };
FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = "<group>"; };
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
@ -2410,6 +2405,7 @@
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */,
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */,
341341ED2187467900192D59 /* ConversationViewModel.h */,
341341EE2187467900192D59 /* ConversationViewModel.m */,
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
@ -3378,7 +3374,6 @@
FDC4381827B34EAD00C60D73 /* Models */,
FDC4380727B31D3A00C60D73 /* Types */,
B88FA7B726045D100049422F /* OpenGroupAPI.swift */,
C3DB66CB260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift */,
C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */,
);
path = "Open Groups";
@ -3992,6 +3987,7 @@
FD859EF727C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift */,
FD859EF927C2F5C500510D0C /* TestGenericHash.swift */,
FD859EFB27C2F60700510D0C /* TestEd25519.swift */,
FD83B9D127D59495005E1583 /* TestUserDefaults.swift */,
);
path = _TestUtilities;
sourceTree = "<group>";
@ -4376,7 +4372,6 @@
);
dependencies = (
FDC4389427B9FFC700C60D73 /* PBXTargetDependency */,
FDC438BB27BB276F00C60D73 /* PBXTargetDependency */,
);
name = SessionMessagingKitTests;
productName = SessionMessagingKitTests;
@ -5327,7 +5322,6 @@
C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */,
FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */,
FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */,
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */,
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
FDC4384C27B47F7700C60D73 /* OpenGroup.swift in Sources */,
@ -5547,6 +5541,7 @@
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */,
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */,
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */,
FD83B9D427D5A7D5005E1583 /* ConversationViewItem+Refactor.swift in Sources */,
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
@ -5594,6 +5589,7 @@
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */,
FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */,
FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5703,11 +5699,6 @@
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
targetProxy = FDC438A027BA2B8A00C60D73 /* PBXContainerItemProxy */;
};
FDC438BB27BB276F00C60D73 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D221A088169C9E5E00537ABF /* Session */;
targetProxy = FDC438BA27BB276F00C60D73 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */

View file

@ -0,0 +1,126 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionSnodeKit
import SessionMessagingKit
extension ConversationViewItem {
func deleteLocallyAction() {
guard let message: TSMessage = self.interaction as? TSMessage else { return }
Storage.write { transaction in
MessageInvalidator.invalidate(message, with: transaction)
message.remove(with: transaction)
if message.interactionType() == .outgoingMessage {
Storage.shared.cancelPendingMessageSendJobIfNeeded(for: message.timestamp, using: transaction)
}
}
}
func deleteRemotelyAction() {
guard let message: TSMessage = self.interaction as? TSMessage else { return }
if isGroupThread {
guard let groupThread: TSGroupThread = message.thread as? TSGroupThread else { return }
// Only allow deletion on incoming and outgoing messages
guard message.interactionType() == .incomingMessage || message.interactionType() == .outgoingMessage else {
return
}
if groupThread.isOpenGroup {
// Make sure it's an open group message and get the open group
guard message.isOpenGroupMessage, let uniqueId: String = groupThread.uniqueId, let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: uniqueId) else {
return
}
// If it's an incoming message the user must have moderator status
if message.interactionType() == .incomingMessage {
guard let userPublicKey: String = Storage.shared.getUserPublicKey() else { return }
if !OpenGroupManager.isUserModeratorOrAdmin(userPublicKey, for: openGroup.room, on: openGroup.server) {
return
}
}
// Delete the message
OpenGroupAPI.messageDelete(message.openGroupServerMessageID, in: openGroup.room, on: openGroup.server)
.catch { _ in
// Roll back
message.save()
}
.retainUntilComplete()
}
else {
guard let serverHash: String = message.serverHash else { return }
let groupPublicKey: String = LKGroupUtilities.getDecodedGroupID(groupThread.groupModel.groupId)
SnodeAPI.deleteMessage(publicKey: groupPublicKey, serverHashes: [serverHash])
.catch { _ in
// Roll back
message.save()
}
.retainUntilComplete()
}
}
else {
guard let contactThread: TSContactThread = message.thread as? TSContactThread, let serverHash: String = message.serverHash else {
return
}
SnodeAPI.deleteMessage(publicKey: contactThread.contactSessionID(), serverHashes: [serverHash])
.catch { _ in
// Roll back
message.save()
}
.retainUntilComplete()
}
}
// Remove this after the unsend request is enabled
func deleteAction() {
Storage.write { transaction in
self.interaction.remove(with: transaction)
if self.interaction.interactionType() == .outgoingMessage {
Storage.shared.cancelPendingMessageSendJobIfNeeded(for: self.interaction.timestamp, using: transaction)
}
}
if self.isGroupThread {
guard let message: TSMessage = self.interaction as? TSMessage, let groupThread: TSGroupThread = message.thread as? TSGroupThread else {
return
}
// Only allow deletion on incoming and outgoing messages
guard message.interactionType() == .incomingMessage || message.interactionType() == .outgoingMessage else {
return
}
// Make sure it's an open group message and get the open group
guard message.isOpenGroupMessage, let uniqueId: String = groupThread.uniqueId, let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: uniqueId) else {
return
}
// If it's an incoming message the user must have moderator status
if message.interactionType() == .incomingMessage {
guard let userPublicKey: String = Storage.shared.getUserPublicKey() else { return }
if !OpenGroupManager.isUserModeratorOrAdmin(userPublicKey, for: openGroup.room, on: openGroup.server) {
return
}
}
// Delete the message
OpenGroupAPI.messageDelete(message.openGroupServerMessageID, in: openGroup.room, on: openGroup.server)
.catch { _ in
// Roll back
message.save()
}
.retainUntilComplete()
}
}
}

View file

@ -133,10 +133,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
- (void)copyTextAction;
- (void)shareMediaAction;
- (void)saveMediaAction;
- (void)deleteLocallyAction;
- (void)deleteRemotelyAction;
- (void)deleteAction; // Remove this after the unsend request is enabled
- (BOOL)canCopyMedia;
- (BOOL)canSaveMedia;

View file

@ -972,109 +972,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return [self saveMediaAlbumItems:mediaAlbumItems];
}
- (void)deleteLocallyAction
{
TSMessage *message = (TSMessage *)self.interaction;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[MessageInvalidator invalidate:message with:transaction];
[self.interaction removeWithTransaction:transaction];
if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
[LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction];
}
}];
}
- (void)deleteRemotelyAction
{
TSMessage *message = (TSMessage *)self.interaction;
if (self.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
// Only allow deletion on incoming and outgoing messages
OWSInteractionType interationType = self.interaction.interactionType;
if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return;
if (groupThread.isOpenGroup) {
// Make sure it's an open group message
if (!message.isOpenGroupMessage) return;
// Get the open group
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil) return;
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
if (![SNOpenGroupManager isUserModeratorOrAdmin:userPublicKey forRoom:openGroup.room onServer:openGroup.server]) { return; }
}
// Delete the message
[[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroup.room onServer:openGroup.server].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
} else {
NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:groupThread.groupModel.groupId];
[[SNSnodeAPI deleteMessageForPublickKey:groupPublicKey serverHashes:@[message.serverHash]].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
}
} else {
TSContactThread *contactThread = (TSContactThread *)self.interaction.thread;
[[SNSnodeAPI deleteMessageForPublickKey:contactThread.contactSessionID serverHashes:@[message.serverHash]].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
}
}
// Remove this after the unsend request is enabled
- (void)deleteAction
{
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self.interaction removeWithTransaction:transaction];
if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
[LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction];
}
}];
if (self.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
// Only allow deletion on incoming and outgoing messages
OWSInteractionType interationType = self.interaction.interactionType;
if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return;
// Make sure it's an open group message
TSMessage *message = (TSMessage *)self.interaction;
if (!message.isOpenGroupMessage) return;
// Get the open group
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil && openGroup == nil) return;
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
if (openGroup != nil) {
if (![SNOpenGroupManager isUserModeratorOrAdmin:userPublicKey forRoom:openGroup.room onServer:openGroup.server]) { return; }
}
}
// Delete the message
BOOL wasSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
if (openGroup != nil) {
[[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroup.room onServer:openGroup.server].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
}
}
}
- (BOOL)hasBodyTextActionContent
{
return self.hasBodyText && self.displayableBodyText.fullText.length > 0;

View file

@ -1,5 +1,6 @@
import PromiseKit
import NVActivityIndicatorView
import SessionMessagingKit
final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private let maxWidth: CGFloat
@ -64,7 +65,7 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl
widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true
OpenGroupManager.getDefaultRoomsIfNeeded()
_ = OpenGroupManager.defaultRoomsPromise?.done { [weak self] rooms in
_ = OpenGroupManager.shared.cache.defaultRoomsPromise?.done { [weak self] rooms in
self?.rooms = rooms
}
}

View file

@ -3,7 +3,7 @@
import Foundation
extension OpenGroupAPI {
public struct Capabilities: Codable {
public struct Capabilities: Codable, Equatable {
public enum Capability: Equatable, CaseIterable, Codable {
public static var allCases: [Capability] {
[.sogs, .blind]
@ -54,4 +54,10 @@ extension OpenGroupAPI.Capabilities.Capability {
self = OpenGroupAPI.Capabilities.Capability(from: valueString)
}
public func encode(to encoder: Encoder) throws {
var container: SingleValueEncodingContainer = encoder.singleValueContainer()
try container.encode(rawValue)
}
}

View file

@ -3,7 +3,7 @@
import Foundation
extension OpenGroupAPI {
public struct PinnedMessage: Codable {
public struct PinnedMessage: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case id
case pinnedAt = "pinned_at"

View file

@ -3,7 +3,7 @@
import Foundation
extension OpenGroupAPI {
public struct Room: Codable {
public struct Room: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case token
case name

View file

@ -1,8 +0,0 @@
import PromiseKit
extension OpenGroupAPI {
@objc(deleteMessageWithServerID:fromRoom:onServer:)
public static func objc_deleteMessage(with serverID: Int64, from room: String, on server: String) -> AnyPromise {
return AnyPromise.from(messageDelete(serverID, in: room, on: server))
}
}

View file

@ -3,34 +3,14 @@ import SessionSnodeKit
import Sodium
import Curve25519Kit
@objc(SNOpenGroupAPI)
public final class OpenGroupAPI: NSObject {
public enum OpenGroupAPI {
// MARK: - Settings
public static let defaultServer = "http://116.203.70.33"
public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue
// MARK: - Polling State
private static var hasPerformedInitialPoll: Atomic<[String: Bool]> = Atomic([:])
private static var timeSinceLastPoll: Atomic<[String: TimeInterval]> = Atomic([:])
private static var lastPollTime: Atomic<TimeInterval> = Atomic(.greatestFiniteMagnitude)
private static let timeSinceLastOpen: Atomic<TimeInterval> = {
guard let lastOpen = UserDefaults.standard[.lastOpen] else { return Atomic(.greatestFiniteMagnitude) }
return Atomic(Date().timeIntervalSince(lastOpen))
}()
// TODO: Remove these
private static var legacyAuthTokenPromises: Atomic<[String: Promise<String>]> = Atomic([:])
private static var legacyHasUpdatedLastOpenDate = false
private static var legacyGroupImagePromises: [String: Promise<Data>] = [:]
// MARK: - Batching & Polling
@ -42,20 +22,17 @@ public final class OpenGroupAPI: NSObject {
/// - Messages (includes additions and deletions)
/// - Inbox for the server
/// - Outbox for the server
public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
// Store a local copy of the cached state for this server
let hadPerformedInitialPoll: Bool = (hasPerformedInitialPoll.wrappedValue[server] == true)
let originalTimeSinceLastPoll: TimeInterval = (timeSinceLastPoll.wrappedValue[server] ?? min(lastPollTime.wrappedValue, timeSinceLastOpen.wrappedValue))
public static func poll(
_ server: String,
hasPerformedInitialPoll: Bool,
timeSinceLastPoll: TimeInterval,
using dependencies: Dependencies = Dependencies()
) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> {
let maybeLastInboxMessageId: Int64? = dependencies.storage.getOpenGroupInboxLatestMessageId(for: server)
let maybeLastOutboxMessageId: Int64? = dependencies.storage.getOpenGroupOutboxLatestMessageId(for: server)
let lastInboxMessageId: Int64 = (maybeLastInboxMessageId ?? 0)
let lastOutboxMessageId: Int64 = (maybeLastOutboxMessageId ?? 0)
// Update the cached state for this server
hasPerformedInitialPoll.mutate { $0[server] = true }
lastPollTime.mutate { $0 = min($0, timeSinceLastOpen.wrappedValue)}
UserDefaults.standard[.lastOpen] = Date()
// Generate the requests
let requestResponseType: [BatchRequestInfoType] = [
BatchRequestInfo(
@ -78,8 +55,8 @@ public final class OpenGroupAPI: NSObject {
// 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
!hasPerformedInitialPoll &&
timeSinceLastPoll > OpenGroupAPI.Poller.maxInactivityPeriod
)
)
@ -180,7 +157,6 @@ public final class OpenGroupAPI: NSObject {
body: requestBody
)
// TODO: Handle a `412` response (ie. a required capability isn't supported)
return send(request, using: dependencies)
.decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies)
.map { result in
@ -203,11 +179,9 @@ public final class OpenGroupAPI: NSObject {
public static func capabilities(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Capabilities)> {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .capabilities,
queryParameters: [:] // TODO: Add any requirements '.required'.
endpoint: .capabilities
)
// TODO: Handle a `412` response (ie. a required capability isn't supported)
return send(request, using: dependencies)
.decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, using: dependencies)
}
@ -290,31 +264,21 @@ public final class OpenGroupAPI: NSObject {
]
return sequence(server, requests: requestResponseType, using: dependencies)
.map { response -> (capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?)) in
var capabilities: (OnionRequestResponseInfoType, Capabilities?)? = nil
var room: (OnionRequestResponseInfoType, Room?)? = nil
.map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?)) in
let maybeCapabilities: (OnionRequestResponseInfoType, Capabilities?)? = response[.capabilities]
.map { info, data in (info, (data as? BatchSubResponse<Capabilities>)?.body) }
let maybeRoomResponse: (OnionRequestResponseInfoType, Codable?)? = response
.first(where: { key, _ in
switch key {
case .room: return true
default: return false
}
})
.map { _, value in value }
let maybeRoom: (OnionRequestResponseInfoType, Room?)? = maybeRoomResponse
.map { info, data in (info, (data as? BatchSubResponse<Room>)?.body) }
try response.forEach { (endpoint: Endpoint, endpointResponse: (info: OnionRequestResponseInfoType, data: Codable?)) in
switch endpoint {
case .capabilities:
guard let responseData: BatchSubResponse<Capabilities> = endpointResponse.data as? BatchSubResponse<Capabilities>, let responseBody: Capabilities = responseData.body else {
throw HTTP.Error.parsingFailed
}
capabilities = (endpointResponse.info, responseBody)
case .room:
guard let responseData: OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Room> = endpointResponse.data as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Room>, let responseBody: OpenGroupAPI.Room = responseData.body else {
throw HTTP.Error.parsingFailed
}
room = (endpointResponse.info, responseBody)
default: break // No custom handling needed
}
}
guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = capabilities, let room: (OnionRequestResponseInfoType, Room?) = room else {
guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = maybeCapabilities, let room: (OnionRequestResponseInfoType, Room?) = maybeRoom else {
throw HTTP.Error.parsingFailed
}
@ -367,7 +331,7 @@ public final class OpenGroupAPI: NSObject {
}
/// Returns a single message by ID
public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> {
public static func message(_ id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessageIndividual(roomToken, id: id)
@ -381,7 +345,7 @@ public final class OpenGroupAPI: NSObject {
///
/// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room
public static func messageUpdate(
_ id: Int64,
_ id: UInt64,
plaintext: Data,
fileIds: [Int64]?,
in roomToken: String,
@ -405,12 +369,11 @@ public final class OpenGroupAPI: NSObject {
body: requestBody
)
// TODO: Handle custom response info?
return send(request, using: dependencies)
}
public static func messageDelete(
_ id: Int64,
_ id: UInt64,
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
@ -432,8 +395,6 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessagesRecent(roomToken)
// TODO: Limit?.
// queryParameters: [ .limit: 50 ]
)
return send(request, using: dependencies)
@ -444,13 +405,10 @@ public final class OpenGroupAPI: NSObject {
/// remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handleMessages`
/// method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
// TODO: Do we need to be able to load old messages?
public static func messagesBefore(messageId: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessagesBefore(roomToken, id: messageId)
// TODO: Limit?.
// queryParameters: [ .limit: 50 ]
)
return send(request, using: dependencies)
@ -465,8 +423,6 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request<NoBody, Endpoint>(
server: server,
endpoint: .roomMessagesSince(roomToken, seqNo: seqNo)
// TODO: Limit?.
// queryParameters: [ .limit: 50 ]
)
return send(request, using: dependencies)
@ -485,7 +441,7 @@ public final class OpenGroupAPI: NSObject {
/// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned
/// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the
/// order in which pinned messages should be displayed
public static func pinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
public static func pinMessage(id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
let request: Request = Request<NoBody, Endpoint>(
method: .post,
server: server,
@ -499,7 +455,7 @@ public final class OpenGroupAPI: NSObject {
/// Remove a message from this room's pinned message list
///
/// The user must have `admin` (not just `moderator`) permissions in the room
public static func unpinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
public static func unpinMessage(id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<OnionRequestResponseInfoType> {
let request: Request = Request<NoBody, Endpoint>(
method: .post,
server: server,

View file

@ -5,22 +5,44 @@ import SessionSnodeKit
@objc(SNOpenGroupManager)
public final class OpenGroupManager: NSObject {
@objc public static let shared = OpenGroupManager()
public class Cache {
public var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>?
fileprivate var groupImagePromises: [String: Promise<Data>] = [:]
/// Server URL to room ID to set of user IDs
fileprivate var moderators: [String: [String: Set<String>]] = [:]
fileprivate var admins: [String: [String: Set<String>]] = [:]
/// Server URL to value
public var hasPerformedInitialPoll: [String: Bool] = [:]
public var timeSinceLastPoll: [String: TimeInterval] = [:]
fileprivate var _timeSinceLastOpen: TimeInterval?
public func getTimeSinceLastOpen(using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> TimeInterval {
if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
return storedTimeSinceLastOpen
}
guard let lastOpen: Date = dependencies.standardUserDefaults[.lastOpen] else {
_timeSinceLastOpen = .greatestFiniteMagnitude
return .greatestFiniteMagnitude
}
_timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen)
return dependencies.date.timeIntervalSince(lastOpen)
}
}
// MARK: - Variables
@objc public static let shared: OpenGroupManager = OpenGroupManager()
public let mutableCache: Atomic<Cache> = Atomic(Cache())
public var cache: Cache { return mutableCache.wrappedValue }
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<Data>] = [:]
/// Server URL to room ID to set of moderator IDs
private static var moderators: Atomic<[String: [String: Set<String>]]> = Atomic([:])
/// Server URL to room ID to set of admin IDs
private static var admins: Atomic<[String: [String: Set<String>]]> = Atomic([:])
// MARK: - Polling
@objc public func startPolling() {
@ -247,15 +269,15 @@ public final class OpenGroupManager: NSObject {
// - Moderators
if let moderators: [String] = (pollInfo.details?.moderators ?? maybeUpdatedModel?.groupModeratorIds) {
OpenGroupManager.moderators.mutate {
$0[server] = ($0[server] ?? [:]).setting(roomToken, Set(moderators))
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.moderators[server] = (cache.moderators[server] ?? [:]).setting(roomToken, Set(moderators))
}
}
// - Admins
if let admins: [String] = (pollInfo.details?.admins ?? maybeUpdatedModel?.groupAdminIds) {
OpenGroupManager.admins.mutate {
$0[server] = ($0[server] ?? [:]).setting(roomToken, Set(admins))
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.admins[server] = (cache.admins[server] ?? [:]).setting(roomToken, Set(admins))
}
}
@ -458,8 +480,8 @@ public final class OpenGroupManager: NSObject {
}
public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Bool {
let modAndAdminKeys: Set<String> = (OpenGroupManager.moderators.wrappedValue[server]?[room] ?? Set())
.union(OpenGroupManager.admins.wrappedValue[server]?[room] ?? Set())
let modAndAdminKeys: Set<String> = (OpenGroupManager.shared.cache.moderators[server]?[room] ?? Set())
.union(OpenGroupManager.shared.cache.admins[server]?[room] ?? Set())
// If the publicKey is in the set then return immediately, otherwise only continue if it's the
// current user
@ -507,7 +529,7 @@ public final class OpenGroupManager: NSObject {
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 }
guard OpenGroupManager.shared.cache.defaultRoomsPromise == nil else { return }
dependencies.storage.write(
with: { transaction in
@ -518,11 +540,13 @@ public final class OpenGroupManager: NSObject {
)
},
completion: {
OpenGroupManager.defaultRoomsPromise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
OpenGroupAPI.rooms(for: OpenGroupAPI.defaultServer, using: dependencies)
.map { _, data in data }
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.defaultRoomsPromise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) {
OpenGroupAPI.rooms(for: OpenGroupAPI.defaultServer, using: dependencies)
.map { _, data in data }
}
}
OpenGroupManager.defaultRoomsPromise?
OpenGroupManager.shared.cache.defaultRoomsPromise?
.done(on: OpenGroupAPI.workQueue) { items in
items
.compactMap { room -> (UInt64, String)? in
@ -536,7 +560,9 @@ public final class OpenGroupManager: NSObject {
}
}
.catch(on: OpenGroupAPI.workQueue) { _ in
OpenGroupManager.defaultRoomsPromise = nil
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.defaultRoomsPromise = nil
}
}
}
)
@ -566,7 +592,7 @@ public final class OpenGroupManager: NSObject {
return Promise.value(data)
}
if let promise = OpenGroupManager.groupImagePromises["\(server).\(roomToken)"] {
if let promise = OpenGroupManager.shared.cache.groupImagePromises["\(server).\(roomToken)"] {
return promise
}
@ -581,7 +607,9 @@ public final class OpenGroupManager: NSObject {
UserDefaults.standard[.lastOpenGroupImageUpdate] = now
}
}
OpenGroupManager.groupImagePromises["\(server).\(roomToken)"] = promise
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.groupImagePromises["\(server).\(roomToken)"] = promise
}
return promise
}

View file

@ -3,6 +3,7 @@
import Foundation
import Sodium
import SessionSnodeKit
import SessionUtilitiesKit
// MARK: - Dependencies
@ -62,6 +63,12 @@ extension OpenGroupAPI {
set { _nonceGenerator24 = newValue }
}
private var _standardUserDefaults: UserDefaultsType?
public var standardUserDefaults: UserDefaultsType {
get { getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } }
set { _standardUserDefaults = newValue }
}
private var _date: Date?
public var date: Date {
get { getValueSettingIfNull(&_date) { Date() } }
@ -80,6 +87,7 @@ extension OpenGroupAPI {
ed25519: Ed25519Type.Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) {
_api = api
@ -91,6 +99,7 @@ extension OpenGroupAPI {
_ed25519 = ed25519
_nonceGenerator16 = nonceGenerator16
_nonceGenerator24 = nonceGenerator24
_standardUserDefaults = standardUserDefaults
_date = date
}
@ -106,6 +115,7 @@ extension OpenGroupAPI {
ed25519: Ed25519Type.Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) -> Dependencies {
return Dependencies(
@ -118,6 +128,7 @@ extension OpenGroupAPI {
ed25519: (ed25519 ?? self._ed25519),
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16),
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24),
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults),
date: (date ?? self._date)
)
}

View file

@ -20,15 +20,15 @@ extension OpenGroupAPI {
// Messages
case roomMessage(String)
case roomMessageIndividual(String, id: Int64)
case roomMessageIndividual(String, id: UInt64)
case roomMessagesRecent(String)
case roomMessagesBefore(String, id: Int64)
case roomMessagesBefore(String, id: UInt64)
case roomMessagesSince(String, seqNo: Int64)
// Pinning
case roomPinMessage(String, id: Int64)
case roomUnpinMessage(String, id: Int64)
case roomPinMessage(String, id: UInt64)
case roomUnpinMessage(String, id: UInt64)
case roomUnpinAll(String)
// Files

View file

@ -240,14 +240,6 @@ extension MessageReceiver {
thread.remove(with: transaction)
}
}
else if SessionId.Prefix(from: sessionID) != .blinded {
// Otherwise create and save the thread (if the contact isn't a blinded contact - we don't want to
// auto-create threads for blinded contacts if they have no messages)
// TODO: See what this will do with blinded->unblinded conversations?
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
}
}
// FIXME: 'OWSBlockingManager' manages it's own dbConnection and transactions so we have to dispatch this to prevent deadlocks
@ -891,7 +883,6 @@ extension MessageReceiver {
// Note: Pending `MessageSendJobs` _shouldn't_ be an issue as even if they are sent after the
// un-blinding of a thread, the logic when handling the sent messages should automatically
// assign them to the correct thread
// TODO: Validate the above note once `/outbox` has been implemented
view.enumerateRows(inGroup: blindedThreadId) { _, _, object, _, _, _ in
guard let interaction: TSInteraction = object as? TSInteraction else {
return

View file

@ -52,13 +52,28 @@ extension OpenGroupAPI {
guard !self.isPolling else { return Promise.value(()) }
self.isPolling = true
let server: String = self.server
let (promise, seal) = Promise<Void>.pending()
promise.retainUntilComplete()
OpenGroupAPI.poll(server)
OpenGroupAPI
.poll(
server,
hasPerformedInitialPoll: OpenGroupManager.shared.cache.hasPerformedInitialPoll[server] == true,
timeSinceLastPoll: (
OpenGroupManager.shared.cache.timeSinceLastPoll[server] ??
OpenGroupManager.shared.cache.getTimeSinceLastOpen()
)
)
.done(on: OpenGroupAPI.workQueue) { [weak self] response in
self?.isPolling = false
self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll)
OpenGroupManager.shared.mutableCache.mutate { cache in
cache.hasPerformedInitialPoll[server] = true
cache.timeSinceLastPoll[server] = Date().timeIntervalSince1970
UserDefaults.standard[.lastOpen] = Date()
}
seal.fulfill(())
}
.catch(on: OpenGroupAPI.workQueue) { [weak self] error in

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
class TestUserDefaults: UserDefaultsType {
var storage: [String: Any] = [:]
func object(forKey defaultName: String) -> Any? { return storage[defaultName] }
func string(forKey defaultName: String) -> String? { return storage[defaultName] as? String }
func array(forKey defaultName: String) -> [Any]? { return storage[defaultName] as? [Any] }
func dictionary(forKey defaultName: String) -> [String: Any]? { return storage[defaultName] as? [String: Any] }
func data(forKey defaultName: String) -> Data? { return storage[defaultName] as? Data }
func stringArray(forKey defaultName: String) -> [String]? { return storage[defaultName] as? [String] }
func integer(forKey defaultName: String) -> Int { return ((storage[defaultName] as? Int) ?? 0) }
func float(forKey defaultName: String) -> Float { return ((storage[defaultName] as? Float) ?? 0) }
func double(forKey defaultName: String) -> Double { return ((storage[defaultName] as? Double) ?? 0) }
func bool(forKey defaultName: String) -> Bool { return ((storage[defaultName] as? Bool) ?? false) }
func url(forKey defaultName: String) -> URL? { return storage[defaultName] as? URL }
func set(_ value: Any?, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Int, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Float, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Double, forKey defaultName: String) { storage[defaultName] = value }
func set(_ value: Bool, forKey defaultName: String) { storage[defaultName] = value }
func set(_ url: URL?, forKey defaultName: String) { storage[defaultName] = url }
}

View file

@ -308,9 +308,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
/// Sends an onion request to `server`. Builds new paths as needed.
public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: Version = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
guard version != .v4 || server == "https://chat.lokinet.dev" else { // TODO: Remove this
return sendOnionRequest(request, to: server, using: .v3, with: x25519PublicKey)
}
guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
let scheme: String? = url.scheme

View file

@ -1,5 +1,28 @@
import Foundation
public protocol UserDefaultsType: AnyObject {
func object(forKey defaultName: String) -> Any?
func string(forKey defaultName: String) -> String?
func array(forKey defaultName: String) -> [Any]?
func dictionary(forKey defaultName: String) -> [String : Any]?
func data(forKey defaultName: String) -> Data?
func stringArray(forKey defaultName: String) -> [String]?
func integer(forKey defaultName: String) -> Int
func float(forKey defaultName: String) -> Float
func double(forKey defaultName: String) -> Double
func bool(forKey defaultName: String) -> Bool
func url(forKey defaultName: String) -> URL?
func set(_ value: Any?, forKey defaultName: String)
func set(_ value: Int, forKey defaultName: String)
func set(_ value: Float, forKey defaultName: String)
func set(_ value: Double, forKey defaultName: String)
func set(_ value: Bool, forKey defaultName: String)
func set(_ url: URL?, forKey defaultName: String)
}
extension UserDefaults: UserDefaultsType {}
public enum SNUserDefaults {
public enum Bool : Swift.String {
@ -31,7 +54,7 @@ public enum SNUserDefaults {
}
}
public extension UserDefaults {
public extension UserDefaultsType {
subscript(bool: SNUserDefaults.Bool) -> Bool {
get { return self.bool(forKey: bool.rawValue) }