Updated to the latest version of libSession-util
Updated the SharedConfigMessage type to have a TTL of 30 days Updated the SnodeAPI to have a 'poll' method to be more consistent with the OpenGroupAPI (it also does multiple things now so is cleaner) Added logic to limit the number of config messages to be retrieved per poll Added the 'ValidatableResponse' protocol to standardise SnodeAPI response validation Added the libSession version to the logs Fixed an issue where the user profile pic wouldn't get synced correctly due to memory going out of scope Fixed some threading issues Refactored the thread variants to follow the updated terminology (will think about refactoring other code areas later) Cleaned up the Combine error handling Started fixing broken unit tests
This commit is contained in:
parent
345b693225
commit
f30b383bb8
|
@ -631,6 +631,8 @@
|
||||||
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; };
|
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; };
|
||||||
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; };
|
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; };
|
||||||
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; };
|
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; };
|
||||||
|
FD4324302999F0BC008A0213 /* ValidatableResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */; };
|
||||||
|
FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD432436299DEA38008A0213 /* TypeConversion+Utilities.swift */; };
|
||||||
FD43EE9D297A5190009C87C5 /* SessionUtil+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9C297A5190009C87C5 /* SessionUtil+Groups.swift */; };
|
FD43EE9D297A5190009C87C5 /* SessionUtil+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9C297A5190009C87C5 /* SessionUtil+Groups.swift */; };
|
||||||
FD43EE9F297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9E297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift */; };
|
FD43EE9F297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9E297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift */; };
|
||||||
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; };
|
FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; };
|
||||||
|
@ -806,6 +808,7 @@
|
||||||
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; };
|
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; };
|
||||||
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
|
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
|
||||||
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
|
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
|
||||||
|
FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */; };
|
||||||
FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; };
|
FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; };
|
||||||
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; };
|
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; };
|
||||||
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
|
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
|
||||||
|
@ -1756,6 +1759,8 @@
|
||||||
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = "<group>"; };
|
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = "<group>"; };
|
||||||
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
|
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
|
||||||
FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = "<group>"; };
|
FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatableResponse.swift; sourceTree = "<group>"; };
|
||||||
|
FD432436299DEA38008A0213 /* TypeConversion+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeConversion+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FD43EE9C297A5190009C87C5 /* SessionUtil+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Groups.swift"; sourceTree = "<group>"; };
|
FD43EE9C297A5190009C87C5 /* SessionUtil+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Groups.swift"; sourceTree = "<group>"; };
|
||||||
FD43EE9E297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+ConvoInfoVolatile.swift"; sourceTree = "<group>"; };
|
FD43EE9E297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+ConvoInfoVolatile.swift"; sourceTree = "<group>"; };
|
||||||
FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = "<group>"; };
|
FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1922,6 +1927,7 @@
|
||||||
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
|
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
|
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
|
||||||
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
|
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = "<group>"; };
|
||||||
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = "<group>"; };
|
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = "<group>"; };
|
FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = "<group>"; };
|
||||||
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = "<group>"; };
|
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = "<group>"; };
|
||||||
|
@ -3853,6 +3859,14 @@
|
||||||
path = "Shared Models";
|
path = "Shared Models";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
FD432435299DEA1C008A0213 /* Utilities */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FD432436299DEA38008A0213 /* TypeConversion+Utilities.swift */,
|
||||||
|
);
|
||||||
|
path = Utilities;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FD7115F528C8150600B47552 /* Combine */ = {
|
FD7115F528C8150600B47552 /* Combine */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -4030,6 +4044,7 @@
|
||||||
children = (
|
children = (
|
||||||
FD2B4B022949886900AB4848 /* Database */,
|
FD2B4B022949886900AB4848 /* Database */,
|
||||||
FD8ECF8E29381FB200C0D1BB /* Config Handling */,
|
FD8ECF8E29381FB200C0D1BB /* Config Handling */,
|
||||||
|
FD432435299DEA1C008A0213 /* Utilities */,
|
||||||
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */,
|
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */,
|
||||||
FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */,
|
FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */,
|
||||||
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */,
|
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */,
|
||||||
|
@ -4040,6 +4055,7 @@
|
||||||
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
|
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FDDC08F029A300D500BF9681 /* Utilities */,
|
||||||
FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */,
|
FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */,
|
||||||
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */,
|
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */,
|
||||||
FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */,
|
FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */,
|
||||||
|
@ -4203,6 +4219,14 @@
|
||||||
path = _TestUtilities;
|
path = _TestUtilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
FDDC08F029A300D500BF9681 /* Utilities */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */,
|
||||||
|
);
|
||||||
|
path = Utilities;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FDE7214E287E50D50093DF33 /* Scripts */ = {
|
FDE7214E287E50D50093DF33 /* Scripts */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -4259,6 +4283,7 @@
|
||||||
FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */,
|
FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */,
|
||||||
FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */,
|
FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */,
|
||||||
FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */,
|
FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */,
|
||||||
|
FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */,
|
||||||
);
|
);
|
||||||
path = Types;
|
path = Types;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -5430,6 +5455,7 @@
|
||||||
FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */,
|
FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */,
|
||||||
FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */,
|
FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */,
|
||||||
FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */,
|
FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */,
|
||||||
|
FD4324302999F0BC008A0213 /* ValidatableResponse.swift in Sources */,
|
||||||
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */,
|
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */,
|
||||||
FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */,
|
FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */,
|
||||||
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */,
|
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */,
|
||||||
|
@ -5762,6 +5788,7 @@
|
||||||
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
|
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
|
||||||
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
||||||
FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */,
|
FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */,
|
||||||
|
FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */,
|
||||||
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */,
|
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */,
|
||||||
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
|
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
|
||||||
FD245C56285065EA00B966DD /* SNProto.swift in Sources */,
|
FD245C56285065EA00B966DD /* SNProto.swift in Sources */,
|
||||||
|
@ -6023,6 +6050,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */,
|
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */,
|
||||||
|
FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */,
|
||||||
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
|
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
|
||||||
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
|
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
|
||||||
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,
|
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,
|
||||||
|
|
|
@ -326,7 +326,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
||||||
|
|
||||||
guard
|
guard
|
||||||
shouldMarkAsRead,
|
shouldMarkAsRead,
|
||||||
let threadVariant: SessionThread.Variant = try SessionThread
|
let threadVariant: SessionThread.Variant = try? SessionThread
|
||||||
.filter(id: interaction.threadId)
|
.filter(id: interaction.threadId)
|
||||||
.select(.variant)
|
.select(.variant)
|
||||||
.asRequest(of: SessionThread.Variant.self)
|
.asRequest(of: SessionThread.Variant.self)
|
||||||
|
@ -421,7 +421,9 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
||||||
let webRTCSession: WebRTCSession = self.webRTCSession
|
let webRTCSession: WebRTCSession = self.webRTCSession
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisherFlatMap { db in webRTCSession.sendOffer(db, to: sessionId, isRestartingICEConnection: true) }
|
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
|
webRTCSession.sendOffer(db, to: sessionId, isRestartingICEConnection: true)
|
||||||
|
}
|
||||||
.sinkUntilComplete()
|
.sinkUntilComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -462,18 +462,19 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
||||||
|
|
||||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in
|
ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap { db -> AnyPublisher<Void, Error> in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db -> AnyPublisher<Void, Error> in
|
||||||
if !updatedMemberIds.contains(userPublicKey) {
|
if !updatedMemberIds.contains(userPublicKey) {
|
||||||
return try MessageSender.leave(db, groupPublicKey: threadId)
|
return MessageSender.leave(db, groupPublicKey: threadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return try MessageSender.update(
|
return MessageSender.update(
|
||||||
db,
|
db,
|
||||||
groupPublicKey: threadId,
|
groupPublicKey: threadId,
|
||||||
with: updatedMemberIds,
|
with: updatedMemberIds,
|
||||||
name: updatedName
|
name: updatedName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sinkUntilComplete(
|
.sinkUntilComplete(
|
||||||
receiveCompletion: { [weak self] result in
|
receiveCompletion: { [weak self] result in
|
||||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||||
|
|
|
@ -333,10 +333,9 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
||||||
let message: String? = (selectedContacts.count > 20 ? "GROUP_CREATION_PLEASE_WAIT".localized() : nil)
|
let message: String? = (selectedContacts.count > 20 ? "GROUP_CREATION_PLEASE_WAIT".localized() : nil)
|
||||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in
|
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap { db in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
MessageSender.createClosedGroup(db, name: name, members: selectedContacts)
|
MessageSender.createClosedGroup(db, name: name, members: selectedContacts)
|
||||||
}
|
}
|
||||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sinkUntilComplete(
|
.sinkUntilComplete(
|
||||||
receiveCompletion: { result in
|
receiveCompletion: { result in
|
||||||
|
|
|
@ -158,20 +158,20 @@ extension ContextMenuVC {
|
||||||
)
|
)
|
||||||
let canCopySessionId: Bool = (
|
let canCopySessionId: Bool = (
|
||||||
cellViewModel.variant == .standardIncoming &&
|
cellViewModel.variant == .standardIncoming &&
|
||||||
cellViewModel.threadVariant != .openGroup
|
cellViewModel.threadVariant != .community
|
||||||
)
|
)
|
||||||
let canDelete: Bool = (
|
let canDelete: Bool = (
|
||||||
cellViewModel.threadVariant != .openGroup ||
|
cellViewModel.threadVariant != .community ||
|
||||||
currentUserIsOpenGroupModerator ||
|
currentUserIsOpenGroupModerator ||
|
||||||
cellViewModel.state == .failed
|
cellViewModel.state == .failed
|
||||||
)
|
)
|
||||||
let canBan: Bool = (
|
let canBan: Bool = (
|
||||||
cellViewModel.threadVariant == .openGroup &&
|
cellViewModel.threadVariant == .community &&
|
||||||
currentUserIsOpenGroupModerator
|
currentUserIsOpenGroupModerator
|
||||||
)
|
)
|
||||||
|
|
||||||
let shouldShowEmojiActions: Bool = {
|
let shouldShowEmojiActions: Bool = {
|
||||||
if cellViewModel.threadVariant == .openGroup {
|
if cellViewModel.threadVariant == .community {
|
||||||
return OpenGroupManager.isOpenGroupSupport(.reactions, on: cellViewModel.threadOpenGroupServer)
|
return OpenGroupManager.isOpenGroupSupport(.reactions, on: cellViewModel.threadOpenGroupServer)
|
||||||
}
|
}
|
||||||
return !currentThreadIsMessageRequest
|
return !currentThreadIsMessageRequest
|
||||||
|
|
|
@ -421,7 +421,7 @@ extension ConversationVC:
|
||||||
|
|
||||||
// Send the message
|
// Send the message
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { [weak self] db in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { [weak self] db in
|
||||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -545,7 +545,7 @@ extension ConversationVC:
|
||||||
|
|
||||||
// Send the message
|
// Send the message
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { [weak self] db in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { [weak self] db in
|
||||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1110,9 +1110,9 @@ extension ConversationVC:
|
||||||
guard
|
guard
|
||||||
cellViewModel.reactionInfo?.isEmpty == false &&
|
cellViewModel.reactionInfo?.isEmpty == false &&
|
||||||
(
|
(
|
||||||
self.viewModel.threadData.threadVariant == .legacyClosedGroup ||
|
self.viewModel.threadData.threadVariant == .legacyGroup ||
|
||||||
self.viewModel.threadData.threadVariant == .closedGroup ||
|
self.viewModel.threadData.threadVariant == .group ||
|
||||||
self.viewModel.threadData.threadVariant == .openGroup
|
self.viewModel.threadData.threadVariant == .community
|
||||||
),
|
),
|
||||||
let allMessages: [MessageViewModel] = self.viewModel.interactionData
|
let allMessages: [MessageViewModel] = self.viewModel.interactionData
|
||||||
.first(where: { $0.model == .messages })?
|
.first(where: { $0.model == .messages })?
|
||||||
|
@ -1173,10 +1173,10 @@ extension ConversationVC:
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) {
|
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) {
|
||||||
guard cellViewModel.threadVariant == .openGroup else { return }
|
guard cellViewModel.threadVariant == .community else { return }
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisherFlatMap { db -> AnyPublisher<(OpenGroupAPI.ReactionRemoveAllResponse, OpenGroupAPI.PendingChange), Error> in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<(OpenGroupAPI.ReactionRemoveAllResponse, OpenGroupAPI.PendingChange), Error> in
|
||||||
guard
|
guard
|
||||||
let openGroup: OpenGroup = try? OpenGroup
|
let openGroup: OpenGroup = try? OpenGroup
|
||||||
.fetchOne(db, id: cellViewModel.threadId),
|
.fetchOne(db, id: cellViewModel.threadId),
|
||||||
|
@ -1185,10 +1185,7 @@ extension ConversationVC:
|
||||||
.filter(id: cellViewModel.id)
|
.filter(id: cellViewModel.id)
|
||||||
.asRequest(of: Int64.self)
|
.asRequest(of: Int64.self)
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
else {
|
else { throw StorageError.objectNotFound }
|
||||||
return Fail(error: StorageError.objectNotFound)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
|
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
|
||||||
.addPendingReaction(
|
.addPendingReaction(
|
||||||
|
@ -1267,7 +1264,7 @@ extension ConversationVC:
|
||||||
// TODO: Need to test emoji reacts for both open groups and one-to-one to make sure this isn't broken
|
// TODO: Need to test emoji reacts for both open groups and one-to-one to make sure this isn't broken
|
||||||
// Perform the sending logic
|
// Perform the sending logic
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap { db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
||||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: cellViewModel.threadId) else {
|
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: cellViewModel.threadId) else {
|
||||||
return Just(nil)
|
return Just(nil)
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
|
@ -1360,10 +1357,7 @@ extension ConversationVC:
|
||||||
.filter(id: cellViewModel.id)
|
.filter(id: cellViewModel.id)
|
||||||
.asRequest(of: Int64.self)
|
.asRequest(of: Int64.self)
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
else {
|
else { throw MessageSenderError.invalidMessage }
|
||||||
return Fail(error: MessageSenderError.invalidMessage)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
|
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
|
||||||
.addPendingReaction(
|
.addPendingReaction(
|
||||||
|
@ -1552,7 +1546,7 @@ extension ConversationVC:
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap { db in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db in
|
||||||
OpenGroupManager.shared.add(
|
OpenGroupManager.shared.add(
|
||||||
db,
|
db,
|
||||||
roomToken: room,
|
roomToken: room,
|
||||||
|
@ -1674,9 +1668,11 @@ extension ConversationVC:
|
||||||
// Remote deletion logic
|
// Remote deletion logic
|
||||||
func deleteRemotely(from viewController: UIViewController?, request: AnyPublisher<Void, Error>, onComplete: (() -> ())?) {
|
func deleteRemotely(from viewController: UIViewController?, request: AnyPublisher<Void, Error>, onComplete: (() -> ())?) {
|
||||||
// Show a loading indicator
|
// Show a loading indicator
|
||||||
Future<Void, Error> { resolver in
|
Deferred {
|
||||||
ModalActivityIndicatorViewController.present(fromViewController: viewController, canCancel: false) { _ in
|
Future<Void, Error> { resolver in
|
||||||
resolver(Result.success(()))
|
ModalActivityIndicatorViewController.present(fromViewController: viewController, canCancel: false) { _ in
|
||||||
|
resolver(Result.success(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatMap { _ in request }
|
.flatMap { _ in request }
|
||||||
|
@ -1707,7 +1703,7 @@ extension ConversationVC:
|
||||||
// How we delete the message differs depending on the type of thread
|
// How we delete the message differs depending on the type of thread
|
||||||
switch cellViewModel.threadVariant {
|
switch cellViewModel.threadVariant {
|
||||||
// Handle open group messages the old way
|
// Handle open group messages the old way
|
||||||
case .openGroup:
|
case .community:
|
||||||
// If it's an incoming message the user must have moderator status
|
// If it's an incoming message the user must have moderator status
|
||||||
let result: (openGroupServerMessageId: Int64?, openGroup: OpenGroup?)? = Storage.shared.read { db -> (Int64?, OpenGroup?) in
|
let result: (openGroupServerMessageId: Int64?, openGroup: OpenGroup?)? = Storage.shared.read { db -> (Int64?, OpenGroup?) in
|
||||||
(
|
(
|
||||||
|
@ -1786,7 +1782,7 @@ extension ConversationVC:
|
||||||
// Delete the message from the open group
|
// Delete the message from the open group
|
||||||
deleteRemotely(
|
deleteRemotely(
|
||||||
from: self,
|
from: self,
|
||||||
request: Storage.shared.readPublisherFlatMap { db in
|
request: Storage.shared.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
OpenGroupAPI.messageDelete(
|
OpenGroupAPI.messageDelete(
|
||||||
db,
|
db,
|
||||||
id: openGroupServerMessageId,
|
id: openGroupServerMessageId,
|
||||||
|
@ -1800,7 +1796,7 @@ extension ConversationVC:
|
||||||
self?.showInputAccessoryView()
|
self?.showInputAccessoryView()
|
||||||
}
|
}
|
||||||
|
|
||||||
case .contact, .legacyClosedGroup, .closedGroup:
|
case .contact, .legacyGroup, .group:
|
||||||
let serverHash: String? = Storage.shared.read { db -> String? in
|
let serverHash: String? = Storage.shared.read { db -> String? in
|
||||||
try Interaction
|
try Interaction
|
||||||
.select(.serverHash)
|
.select(.serverHash)
|
||||||
|
@ -1859,7 +1855,7 @@ extension ConversationVC:
|
||||||
})
|
})
|
||||||
|
|
||||||
actionSheet.addAction(UIAlertAction(
|
actionSheet.addAction(UIAlertAction(
|
||||||
title: (cellViewModel.threadVariant == .legacyClosedGroup || cellViewModel.threadVariant == .closedGroup ?
|
title: (cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group ?
|
||||||
"delete_message_for_everyone".localized() :
|
"delete_message_for_everyone".localized() :
|
||||||
String(format: "delete_message_for_me_and_recipient".localized(), threadName)
|
String(format: "delete_message_for_me_and_recipient".localized(), threadName)
|
||||||
),
|
),
|
||||||
|
@ -1963,7 +1959,7 @@ extension ConversationVC:
|
||||||
}
|
}
|
||||||
|
|
||||||
func ban(_ cellViewModel: MessageViewModel) {
|
func ban(_ cellViewModel: MessageViewModel) {
|
||||||
guard cellViewModel.threadVariant == .openGroup else { return }
|
guard cellViewModel.threadVariant == .community else { return }
|
||||||
|
|
||||||
let threadId: String = self.viewModel.threadData.threadId
|
let threadId: String = self.viewModel.threadData.threadId
|
||||||
let modal: ConfirmationModal = ConfirmationModal(
|
let modal: ConfirmationModal = ConfirmationModal(
|
||||||
|
@ -1975,10 +1971,9 @@ extension ConversationVC:
|
||||||
cancelStyle: .alert_text,
|
cancelStyle: .alert_text,
|
||||||
onConfirm: { [weak self] _ in
|
onConfirm: { [weak self] _ in
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisherFlatMap { db -> AnyPublisher<Void, Error> in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.main) { db -> AnyPublisher<Void, Error> in
|
||||||
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
|
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
|
||||||
return Fail(error: StorageError.objectNotFound)
|
throw StorageError.objectNotFound
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return OpenGroupAPI
|
return OpenGroupAPI
|
||||||
|
@ -2020,7 +2015,7 @@ extension ConversationVC:
|
||||||
}
|
}
|
||||||
|
|
||||||
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) {
|
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) {
|
||||||
guard cellViewModel.threadVariant == .openGroup else { return }
|
guard cellViewModel.threadVariant == .community else { return }
|
||||||
|
|
||||||
let threadId: String = self.viewModel.threadData.threadId
|
let threadId: String = self.viewModel.threadData.threadId
|
||||||
let modal: ConfirmationModal = ConfirmationModal(
|
let modal: ConfirmationModal = ConfirmationModal(
|
||||||
|
@ -2032,10 +2027,9 @@ extension ConversationVC:
|
||||||
cancelStyle: .alert_text,
|
cancelStyle: .alert_text,
|
||||||
onConfirm: { [weak self] _ in
|
onConfirm: { [weak self] _ in
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisherFlatMap { db -> AnyPublisher<Void, Error> in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.main) { db -> AnyPublisher<Void, Error> in
|
||||||
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
|
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
|
||||||
return Fail(error: StorageError.objectNotFound)
|
throw StorageError.objectNotFound
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return OpenGroupAPI
|
return OpenGroupAPI
|
||||||
|
@ -2300,7 +2294,7 @@ extension ConversationVC {
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
// If we aren't creating a new thread (ie. sending a message request) then send a
|
// If we aren't creating a new thread (ie. sending a message request) then send a
|
||||||
// messageRequestResponse back to the sender (this allows the sender to know that
|
// messageRequestResponse back to the sender (this allows the sender to know that
|
||||||
// they have been approved and can now use this contact in closed groups)
|
// they have been approved and can now use this contact in closed groups)
|
||||||
|
|
|
@ -107,7 +107,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
threadId: self.threadId,
|
threadId: self.threadId,
|
||||||
threadVariant: self.initialThreadVariant,
|
threadVariant: self.initialThreadVariant,
|
||||||
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
|
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
|
||||||
currentUserIsClosedGroupMember: ((self.initialThreadVariant != .legacyClosedGroup && self.initialThreadVariant != .closedGroup) ?
|
currentUserIsClosedGroupMember: ((self.initialThreadVariant != .legacyGroup && self.initialThreadVariant != .group) ?
|
||||||
nil :
|
nil :
|
||||||
Storage.shared.read { db in
|
Storage.shared.read { db in
|
||||||
GroupMember
|
GroupMember
|
||||||
|
@ -342,7 +342,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
.read { db -> [MentionInfo] in
|
.read { db -> [MentionInfo] in
|
||||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
let pattern: FTS5Pattern? = try? SessionThreadViewModel.pattern(db, searchTerm: query, forTable: Profile.self)
|
let pattern: FTS5Pattern? = try? SessionThreadViewModel.pattern(db, searchTerm: query, forTable: Profile.self)
|
||||||
let capabilities: Set<Capability.Variant> = (threadData.threadVariant != .openGroup ?
|
let capabilities: Set<Capability.Variant> = (threadData.threadVariant != .community ?
|
||||||
nil :
|
nil :
|
||||||
try? Capability
|
try? Capability
|
||||||
.select(.variant)
|
.select(.variant)
|
||||||
|
|
|
@ -306,18 +306,18 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
let isGroupThread: Bool = (
|
let isGroupThread: Bool = (
|
||||||
cellViewModel.threadVariant == .openGroup ||
|
cellViewModel.threadVariant == .community ||
|
||||||
cellViewModel.threadVariant == .legacyClosedGroup ||
|
cellViewModel.threadVariant == .legacyGroup ||
|
||||||
cellViewModel.threadVariant == .closedGroup
|
cellViewModel.threadVariant == .group
|
||||||
)
|
)
|
||||||
|
|
||||||
// Profile picture view
|
// Profile picture view (should always be handled as a standard 'contact' profile picture)
|
||||||
profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0)
|
profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0)
|
||||||
profilePictureViewWidthConstraint.constant = (isGroupThread ? VisibleMessageCell.profilePictureSize : 0)
|
profilePictureViewWidthConstraint.constant = (isGroupThread ? VisibleMessageCell.profilePictureSize : 0)
|
||||||
profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil)
|
profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil)
|
||||||
profilePictureView.update(
|
profilePictureView.update(
|
||||||
publicKey: cellViewModel.authorId,
|
publicKey: cellViewModel.authorId,
|
||||||
threadVariant: cellViewModel.threadVariant,
|
threadVariant: .contact, // Should always be '.contact'
|
||||||
customImageData: nil,
|
customImageData: nil,
|
||||||
profile: cellViewModel.profile,
|
profile: cellViewModel.profile,
|
||||||
additionalProfile: nil
|
additionalProfile: nil
|
||||||
|
@ -710,9 +710,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
showingAllReactions: showExpandedReactions,
|
showingAllReactions: showExpandedReactions,
|
||||||
showNumbers: (
|
showNumbers: (
|
||||||
cellViewModel.threadVariant == .legacyClosedGroup ||
|
cellViewModel.threadVariant == .legacyGroup ||
|
||||||
cellViewModel.threadVariant == .closedGroup ||
|
cellViewModel.threadVariant == .group ||
|
||||||
cellViewModel.threadVariant == .openGroup
|
cellViewModel.threadVariant == .community
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -860,7 +860,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
||||||
|
|
||||||
if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile {
|
if profilePictureView.bounds.contains(profilePictureView.convert(location, from: self)), cellViewModel.shouldShowProfile {
|
||||||
// For open groups only attempt to start a conversation if the author has a blinded id
|
// For open groups only attempt to start a conversation if the author has a blinded id
|
||||||
guard cellViewModel.threadVariant != .openGroup else {
|
guard cellViewModel.threadVariant != .community else {
|
||||||
guard SessionId.Prefix(from: cellViewModel.authorId) == .blinded else { return }
|
guard SessionId.Prefix(from: cellViewModel.authorId) == .blinded else { return }
|
||||||
|
|
||||||
delegate?.startThread(
|
delegate?.startThread(
|
||||||
|
@ -1070,9 +1070,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
||||||
|
|
||||||
case .standardIncoming, .standardIncomingDeleted:
|
case .standardIncoming, .standardIncomingDeleted:
|
||||||
let isGroupThread = (
|
let isGroupThread = (
|
||||||
cellViewModel.threadVariant == .openGroup ||
|
cellViewModel.threadVariant == .community ||
|
||||||
cellViewModel.threadVariant == .legacyClosedGroup ||
|
cellViewModel.threadVariant == .legacyGroup ||
|
||||||
cellViewModel.threadVariant == .closedGroup
|
cellViewModel.threadVariant == .group
|
||||||
)
|
)
|
||||||
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
|
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
override var title: String {
|
override var title: String {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact: return "vc_settings_title".localized()
|
case .contact: return "vc_settings_title".localized()
|
||||||
case .legacyClosedGroup, .closedGroup, .openGroup: return "vc_group_settings_title".localized()
|
case .legacyGroup, .group, .community: return "vc_group_settings_title".localized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,8 +216,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
||||||
let currentUserIsClosedGroupMember: Bool = (
|
let currentUserIsClosedGroupMember: Bool = (
|
||||||
(
|
(
|
||||||
threadVariant == .legacyClosedGroup ||
|
threadVariant == .legacyGroup ||
|
||||||
threadVariant == .closedGroup
|
threadVariant == .group
|
||||||
) &&
|
) &&
|
||||||
threadViewModel.currentUserIsClosedGroupMember == true
|
threadViewModel.currentUserIsClosedGroupMember == true
|
||||||
)
|
)
|
||||||
|
@ -307,14 +307,14 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
SectionModel(
|
SectionModel(
|
||||||
model: .content,
|
model: .content,
|
||||||
elements: [
|
elements: [
|
||||||
(threadVariant == .legacyClosedGroup || threadVariant == .closedGroup ? nil :
|
(threadVariant == .legacyGroup || threadVariant == .group ? nil :
|
||||||
SessionCell.Info(
|
SessionCell.Info(
|
||||||
id: .copyThreadId,
|
id: .copyThreadId,
|
||||||
leftAccessory: .icon(
|
leftAccessory: .icon(
|
||||||
UIImage(named: "ic_copy")?
|
UIImage(named: "ic_copy")?
|
||||||
.withRenderingMode(.alwaysTemplate)
|
.withRenderingMode(.alwaysTemplate)
|
||||||
),
|
),
|
||||||
title: (threadVariant == .openGroup ?
|
title: (threadVariant == .community ?
|
||||||
"COPY_GROUP_URL".localized() :
|
"COPY_GROUP_URL".localized() :
|
||||||
"vc_conversation_settings_copy_session_id_button_title".localized()
|
"vc_conversation_settings_copy_session_id_button_title".localized()
|
||||||
),
|
),
|
||||||
|
@ -324,10 +324,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
),
|
),
|
||||||
onTap: {
|
onTap: {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact, .legacyClosedGroup, .closedGroup:
|
case .contact, .legacyGroup, .group:
|
||||||
UIPasteboard.general.string = threadId
|
UIPasteboard.general.string = threadId
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
guard
|
guard
|
||||||
let server: String = threadViewModel.openGroupServer,
|
let server: String = threadViewModel.openGroupServer,
|
||||||
let roomToken: String = threadViewModel.openGroupRoomToken,
|
let roomToken: String = threadViewModel.openGroupRoomToken,
|
||||||
|
@ -387,7 +387,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
(threadVariant != .openGroup ? nil :
|
(threadVariant != .community ? nil :
|
||||||
SessionCell.Info(
|
SessionCell.Info(
|
||||||
id: .addToOpenGroup,
|
id: .addToOpenGroup,
|
||||||
leftAccessory: .icon(
|
leftAccessory: .icon(
|
||||||
|
@ -414,7 +414,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
(threadVariant == .openGroup || threadViewModel.threadIsBlocked == true ? nil :
|
(threadVariant == .community || threadViewModel.threadIsBlocked == true ? nil :
|
||||||
SessionCell.Info(
|
SessionCell.Info(
|
||||||
id: .disappearingMessages,
|
id: .disappearingMessages,
|
||||||
leftAccessory: .icon(
|
leftAccessory: .icon(
|
||||||
|
@ -495,7 +495,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
),
|
),
|
||||||
onTap: { [weak self] in
|
onTap: { [weak self] in
|
||||||
dependencies.storage
|
dependencies.storage
|
||||||
.writePublisherFlatMap { db in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
MessageSender.leave(db, groupPublicKey: threadId)
|
MessageSender.leave(db, groupPublicKey: threadId)
|
||||||
}
|
}
|
||||||
.sinkUntilComplete()
|
.sinkUntilComplete()
|
||||||
|
@ -538,8 +538,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
),
|
),
|
||||||
isEnabled: (
|
isEnabled: (
|
||||||
(
|
(
|
||||||
threadViewModel.threadVariant != .legacyClosedGroup &&
|
threadViewModel.threadVariant != .legacyGroup &&
|
||||||
threadViewModel.threadVariant != .closedGroup
|
threadViewModel.threadVariant != .group
|
||||||
) ||
|
) ||
|
||||||
currentUserIsClosedGroupMember
|
currentUserIsClosedGroupMember
|
||||||
),
|
),
|
||||||
|
@ -576,8 +576,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
),
|
),
|
||||||
isEnabled: (
|
isEnabled: (
|
||||||
(
|
(
|
||||||
threadViewModel.threadVariant != .legacyClosedGroup &&
|
threadViewModel.threadVariant != .legacyGroup &&
|
||||||
threadViewModel.threadVariant != .closedGroup
|
threadViewModel.threadVariant != .group
|
||||||
) ||
|
) ||
|
||||||
currentUserIsClosedGroupMember
|
currentUserIsClosedGroupMember
|
||||||
),
|
),
|
||||||
|
|
|
@ -168,12 +168,12 @@ final class ConversationTitleView: UIView {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact: break
|
case .contact: break
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
subtitleLabel?.attributedText = NSAttributedString(
|
subtitleLabel?.attributedText = NSAttributedString(
|
||||||
string: "\(userCount) member\(userCount == 1 ? "" : "s")"
|
string: "\(userCount) member\(userCount == 1 ? "" : "s")"
|
||||||
)
|
)
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
subtitleLabel?.attributedText = NSAttributedString(
|
subtitleLabel?.attributedText = NSAttributedString(
|
||||||
string: "\(userCount) active member\(userCount == 1 ? "" : "s")"
|
string: "\(userCount) active member\(userCount == 1 ? "" : "s")"
|
||||||
)
|
)
|
||||||
|
|
|
@ -802,7 +802,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
||||||
// Delay the change to give the cell "unswipe" animation some time to complete
|
// Delay the change to give the cell "unswipe" animation some time to complete
|
||||||
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
try Contact
|
try Contact
|
||||||
.filter(id: threadViewModel.threadId)
|
.filter(id: threadViewModel.threadId)
|
||||||
.updateAllAndConfig(
|
.updateAllAndConfig(
|
||||||
|
|
|
@ -95,7 +95,7 @@ public class HomeViewModel {
|
||||||
joinToPagedType: {
|
joinToPagedType: {
|
||||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||||
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
||||||
let threadVariants: [SessionThread.Variant] = [.legacyClosedGroup, .closedGroup]
|
let threadVariants: [SessionThread.Variant] = [.legacyGroup, .group]
|
||||||
let targetRole: GroupMember.Role = GroupMember.Role.standard
|
let targetRole: GroupMember.Role = GroupMember.Role.standard
|
||||||
|
|
||||||
return SQL("""
|
return SQL("""
|
||||||
|
@ -367,12 +367,12 @@ public class HomeViewModel {
|
||||||
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
|
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.writeAsync { db in
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
MessageSender
|
MessageSender
|
||||||
.leave(db, groupPublicKey: threadId)
|
.leave(db, groupPublicKey: threadId)
|
||||||
.sinkUntilComplete()
|
.sinkUntilComplete()
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
OpenGroupManager.shared.delete(db, openGroupId: threadId)
|
OpenGroupManager.shared.delete(db, openGroupId: threadId)
|
||||||
|
|
||||||
default: break
|
default: break
|
||||||
|
|
|
@ -445,7 +445,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
|
|
||||||
return UISwipeActionsConfiguration(actions: [ delete, block ])
|
return UISwipeActionsConfiguration(actions: [ delete, block ])
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup, .openGroup:
|
case .legacyGroup, .group, .community:
|
||||||
return UISwipeActionsConfiguration(actions: [ delete ])
|
return UISwipeActionsConfiguration(actions: [ delete ])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,7 +469,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
let closedGroupThreadIds: [String] = (viewModel.threadData
|
let closedGroupThreadIds: [String] = (viewModel.threadData
|
||||||
.first { $0.model == .threads }?
|
.first { $0.model == .threads }?
|
||||||
.elements
|
.elements
|
||||||
.filter { $0.threadVariant == .legacyClosedGroup || $0.threadVariant == .closedGroup }
|
.filter { $0.threadVariant == .legacyGroup || $0.threadVariant == .group }
|
||||||
.map { $0.threadId })
|
.map { $0.threadId })
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
let alertVC: UIAlertController = UIAlertController(
|
let alertVC: UIAlertController = UIAlertController(
|
||||||
|
|
|
@ -186,12 +186,12 @@ public class MessageRequestsViewModel {
|
||||||
) { _ in
|
) { _ in
|
||||||
Storage.shared.write { db in
|
Storage.shared.write { db in
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact, .openGroup:
|
case .contact, .community:
|
||||||
_ = try SessionThread
|
_ = try SessionThread
|
||||||
.filter(id: threadId)
|
.filter(id: threadId)
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||||
db,
|
db,
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
|
|
|
@ -346,17 +346,14 @@ enum GiphyAPI {
|
||||||
// URLError codes are negative values
|
// URLError codes are negative values
|
||||||
return HTTPError.generic
|
return HTTPError.generic
|
||||||
}
|
}
|
||||||
.flatMap { data, _ -> AnyPublisher<[GiphyImageInfo], Error> in
|
.tryMap { data, _ -> [GiphyImageInfo] in
|
||||||
Logger.error("search request succeeded")
|
Logger.error("search request succeeded")
|
||||||
|
|
||||||
guard let imageInfos = self.parseGiphyImages(responseData: data) else {
|
guard let imageInfos = self.parseGiphyImages(responseData: data) else {
|
||||||
return Fail(error: HTTPError.invalidResponse)
|
throw HTTPError.invalidResponse
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Just(imageInfos)
|
return imageInfos
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,26 +86,17 @@ class PhotoCapture: NSObject {
|
||||||
return Just(())
|
return Just(())
|
||||||
.subscribe(on: sessionQueue)
|
.subscribe(on: sessionQueue)
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.flatMap { [weak self] _ -> AnyPublisher<Void, Error> in
|
.tryMap { [weak self] _ -> Void in
|
||||||
self?.session.beginConfiguration()
|
self?.session.beginConfiguration()
|
||||||
defer { self?.session.commitConfiguration() }
|
defer { self?.session.commitConfiguration() }
|
||||||
|
|
||||||
do {
|
try self?.updateCurrentInput(position: .back)
|
||||||
try self?.updateCurrentInput(position: .back)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return Fail(error: error)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let photoOutput = self?.captureOutput.photoOutput else {
|
guard
|
||||||
return Fail(error: PhotoCaptureError.initializationFailed)
|
let photoOutput = self?.captureOutput.photoOutput,
|
||||||
.eraseToAnyPublisher()
|
self?.session.canAddOutput(photoOutput) == true
|
||||||
}
|
else {
|
||||||
|
throw PhotoCaptureError.initializationFailed
|
||||||
guard self?.session.canAddOutput(photoOutput) == true else {
|
|
||||||
return Fail(error: PhotoCaptureError.initializationFailed)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let connection = photoOutput.connection(with: .video) {
|
if let connection = photoOutput.connection(with: .video) {
|
||||||
|
@ -130,9 +121,7 @@ class PhotoCapture: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Just(())
|
return ()
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveCompletion: { [weak self] result in
|
receiveCompletion: { [weak self] result in
|
||||||
|
@ -172,21 +161,12 @@ class PhotoCapture: NSObject {
|
||||||
return Just(())
|
return Just(())
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.subscribe(on: sessionQueue)
|
.subscribe(on: sessionQueue)
|
||||||
.flatMap { [weak self, newPosition = self.desiredPosition] _ -> AnyPublisher<Void, Error> in
|
.tryMap { [weak self, newPosition = self.desiredPosition] _ -> Void in
|
||||||
self?.session.beginConfiguration()
|
self?.session.beginConfiguration()
|
||||||
defer { self?.session.commitConfiguration() }
|
defer { self?.session.commitConfiguration() }
|
||||||
|
|
||||||
do {
|
try self?.updateCurrentInput(position: newPosition)
|
||||||
try self?.updateCurrentInput(position: newPosition)
|
return ()
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return Fail(error: error)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just(())
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,64 +137,68 @@ class PhotoCollectionContents {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestImageDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: DataSource, dataUTI: String), Error> {
|
private func requestImageDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: DataSource, dataUTI: String), Error> {
|
||||||
return Future { [weak self] resolver in
|
return Deferred {
|
||||||
|
Future { [weak self] resolver in
|
||||||
let options: PHImageRequestOptions = PHImageRequestOptions()
|
|
||||||
options.isNetworkAccessAllowed = true
|
let options: PHImageRequestOptions = PHImageRequestOptions()
|
||||||
|
options.isNetworkAccessAllowed = true
|
||||||
_ = self?.imageManager.requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in
|
|
||||||
|
_ = self?.imageManager.requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in
|
||||||
guard let imageData = imageData else {
|
|
||||||
resolver(Result.failure(PhotoLibraryError.assertionError(description: "imageData was unexpectedly nil")))
|
guard let imageData = imageData else {
|
||||||
return
|
resolver(Result.failure(PhotoLibraryError.assertionError(description: "imageData was unexpectedly nil")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let dataUTI = dataUTI else {
|
||||||
|
resolver(Result.failure(PhotoLibraryError.assertionError(description: "dataUTI was unexpectedly nil")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let dataSource = DataSourceValue.dataSource(with: imageData, utiType: dataUTI) else {
|
||||||
|
resolver(Result.failure(PhotoLibraryError.assertionError(description: "dataSource was unexpectedly nil")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver(Result.success((dataSource: dataSource, dataUTI: dataUTI)))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let dataUTI = dataUTI else {
|
|
||||||
resolver(Result.failure(PhotoLibraryError.assertionError(description: "dataUTI was unexpectedly nil")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let dataSource = DataSourceValue.dataSource(with: imageData, utiType: dataUTI) else {
|
|
||||||
resolver(Result.failure(PhotoLibraryError.assertionError(description: "dataSource was unexpectedly nil")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolver(Result.success((dataSource: dataSource, dataUTI: dataUTI)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestVideoDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: DataSource, dataUTI: String), Error> {
|
private func requestVideoDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: DataSource, dataUTI: String), Error> {
|
||||||
return Future { [weak self] resolver in
|
return Deferred {
|
||||||
|
Future { [weak self] resolver in
|
||||||
let options: PHVideoRequestOptions = PHVideoRequestOptions()
|
|
||||||
options.isNetworkAccessAllowed = true
|
let options: PHVideoRequestOptions = PHVideoRequestOptions()
|
||||||
|
options.isNetworkAccessAllowed = true
|
||||||
_ = self?.imageManager.requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, foo in
|
|
||||||
|
_ = self?.imageManager.requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, foo in
|
||||||
guard let exportSession = exportSession else {
|
|
||||||
resolver(Result.failure(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil")))
|
guard let exportSession = exportSession else {
|
||||||
return
|
resolver(Result.failure(PhotoLibraryError.assertionError(description: "exportSession was unexpectedly nil")))
|
||||||
}
|
|
||||||
|
|
||||||
exportSession.outputFileType = AVFileType.mp4
|
|
||||||
exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing()
|
|
||||||
|
|
||||||
let exportPath = OWSFileSystem.temporaryFilePath(withFileExtension: "mp4")
|
|
||||||
let exportURL = URL(fileURLWithPath: exportPath)
|
|
||||||
exportSession.outputURL = exportURL
|
|
||||||
|
|
||||||
Logger.debug("starting video export")
|
|
||||||
exportSession.exportAsynchronously {
|
|
||||||
Logger.debug("Completed video export")
|
|
||||||
|
|
||||||
guard let dataSource = DataSourcePath.dataSource(with: exportURL, shouldDeleteOnDeallocation: true) else {
|
|
||||||
resolver(Result.failure(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL")))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver(Result.success((dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)))
|
exportSession.outputFileType = AVFileType.mp4
|
||||||
|
exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing()
|
||||||
|
|
||||||
|
let exportPath = OWSFileSystem.temporaryFilePath(withFileExtension: "mp4")
|
||||||
|
let exportURL = URL(fileURLWithPath: exportPath)
|
||||||
|
exportSession.outputURL = exportURL
|
||||||
|
|
||||||
|
Logger.debug("starting video export")
|
||||||
|
exportSession.exportAsynchronously {
|
||||||
|
Logger.debug("Completed video export")
|
||||||
|
|
||||||
|
guard let dataSource = DataSourcePath.dataSource(with: exportURL, shouldDeleteOnDeallocation: true) else {
|
||||||
|
resolver(Result.failure(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver(Result.success((dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ let kAudioNotificationsThrottleInterval: TimeInterval = 5
|
||||||
|
|
||||||
protocol NotificationPresenterAdaptee: AnyObject {
|
protocol NotificationPresenterAdaptee: AnyObject {
|
||||||
|
|
||||||
func registerNotificationSettings() -> Future<Void, Never>
|
func registerNotificationSettings() -> AnyPublisher<Void, Never>
|
||||||
|
|
||||||
func notify(
|
func notify(
|
||||||
category: AppNotificationCategory,
|
category: AppNotificationCategory,
|
||||||
|
@ -150,7 +150,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
|
|
||||||
func registerNotificationSettings() -> AnyPublisher<Void, Never> {
|
func registerNotificationSettings() -> AnyPublisher<Void, Never> {
|
||||||
return adaptee.registerNotificationSettings()
|
return adaptee.registerNotificationSettings()
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
public func notifyUser(_ db: Database, for interaction: Interaction, in thread: SessionThread) {
|
||||||
|
@ -163,7 +162,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
|
|
||||||
// Try to group notifications for interactions from open groups
|
// Try to group notifications for interactions from open groups
|
||||||
let identifier: String = interaction.notificationIdentifier(
|
let identifier: String = interaction.notificationIdentifier(
|
||||||
shouldGroupMessagesForThread: (thread.variant == .openGroup)
|
shouldGroupMessagesForThread: (thread.variant == .community)
|
||||||
)
|
)
|
||||||
|
|
||||||
// While batch processing, some of the necessary changes have not been commited.
|
// While batch processing, some of the necessary changes have not been commited.
|
||||||
|
@ -203,7 +202,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
case .contact:
|
case .contact:
|
||||||
notificationTitle = (isMessageRequest ? "Session" : senderName)
|
notificationTitle = (isMessageRequest ? "Session" : senderName)
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup, .openGroup:
|
case .legacyGroup, .group, .community:
|
||||||
notificationTitle = String(
|
notificationTitle = String(
|
||||||
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
format: NotificationStrings.incomingGroupMessageTitleFormat,
|
||||||
senderName,
|
senderName,
|
||||||
|
@ -275,9 +274,9 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
// No call notifications for muted or group threads
|
// No call notifications for muted or group threads
|
||||||
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
||||||
guard
|
guard
|
||||||
thread.variant != .legacyClosedGroup &&
|
thread.variant != .legacyGroup &&
|
||||||
thread.variant != .closedGroup &&
|
thread.variant != .group &&
|
||||||
thread.variant != .openGroup
|
thread.variant != .community
|
||||||
else { return }
|
else { return }
|
||||||
guard
|
guard
|
||||||
interaction.variant == .infoCall,
|
interaction.variant == .infoCall,
|
||||||
|
@ -347,9 +346,9 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
// No reaction notifications for muted, group threads or message requests
|
// No reaction notifications for muted, group threads or message requests
|
||||||
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
|
||||||
guard
|
guard
|
||||||
thread.variant != .legacyClosedGroup &&
|
thread.variant != .legacyGroup &&
|
||||||
thread.variant != .closedGroup &&
|
thread.variant != .group &&
|
||||||
thread.variant != .openGroup
|
thread.variant != .community
|
||||||
else { return }
|
else { return }
|
||||||
guard !isMessageRequest else { return }
|
guard !isMessageRequest else { return }
|
||||||
|
|
||||||
|
@ -539,7 +538,7 @@ class NotificationActionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: DispatchQueue.main) { db in
|
||||||
let interaction: Interaction = try Interaction(
|
let interaction: Interaction = try Interaction(
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
authorId: getUserHexEncodedPublicKey(db),
|
authorId: getUserHexEncodedPublicKey(db),
|
||||||
|
@ -607,7 +606,7 @@ class NotificationActionHandler {
|
||||||
|
|
||||||
private func markAsRead(thread: SessionThread) -> AnyPublisher<Void, Error> {
|
private func markAsRead(thread: SessionThread) -> AnyPublisher<Void, Error> {
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
try Interaction.markAsRead(
|
try Interaction.markAsRead(
|
||||||
db,
|
db,
|
||||||
interactionId: try thread.interactions
|
interactionId: try thread.interactions
|
||||||
|
|
|
@ -54,10 +54,9 @@ public enum PushRegistrationError: Error {
|
||||||
return registerUserNotificationSettings()
|
return registerUserNotificationSettings()
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.receive(on: DispatchQueue.main) // MUST be on main thread
|
.receive(on: DispatchQueue.main) // MUST be on main thread
|
||||||
.flatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in
|
.tryFlatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
return Fail(error: PushRegistrationError.pushNotSupported(description: "Push not supported on simulators"))
|
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
|
||||||
.eraseToAnyPublisher()
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return self.registerForVanillaPushToken()
|
return self.registerForVanillaPushToken()
|
||||||
|
@ -101,7 +100,6 @@ public enum PushRegistrationError: Error {
|
||||||
public func registerUserNotificationSettings() -> AnyPublisher<Void, Never> {
|
public func registerUserNotificationSettings() -> AnyPublisher<Void, Never> {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
return notificationPresenter.registerNotificationSettings()
|
return notificationPresenter.registerNotificationSettings()
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,8 +140,10 @@ public enum PushRegistrationError: Error {
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
|
||||||
// No pending vanilla token yet; create a new publisher
|
// No pending vanilla token yet; create a new publisher
|
||||||
let publisher: AnyPublisher<Data, Error> = Future<Data, Error> { self.vanillaTokenResolver = $0 }
|
let publisher: AnyPublisher<Data, Error> = Deferred {
|
||||||
.eraseToAnyPublisher()
|
Future<Data, Error> { self.vanillaTokenResolver = $0 }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
self.vanillaTokenPublisher = publisher
|
self.vanillaTokenPublisher = publisher
|
||||||
|
|
||||||
return publisher
|
return publisher
|
||||||
|
@ -238,8 +238,10 @@ public enum PushRegistrationError: Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No pending voip token yet. Create a new publisher
|
// No pending voip token yet. Create a new publisher
|
||||||
let publisher: AnyPublisher<Data?, Error> = Future<Data?, Error> { self.voipTokenResolver = $0 }
|
let publisher: AnyPublisher<Data?, Error> = Deferred {
|
||||||
.eraseToAnyPublisher()
|
Future<Data?, Error> { self.voipTokenResolver = $0 }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
self.voipTokenPublisher = publisher
|
self.voipTokenPublisher = publisher
|
||||||
|
|
||||||
return publisher
|
return publisher
|
||||||
|
|
|
@ -73,14 +73,16 @@ public enum SyncPushTokensJob: JobExecutor {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future<Void, Error> { resolver in
|
return Deferred {
|
||||||
SyncPushTokensJob.registerForPushNotifications(
|
Future<Void, Error> { resolver in
|
||||||
pushToken: pushToken,
|
SyncPushTokensJob.registerForPushNotifications(
|
||||||
voipToken: voipToken,
|
pushToken: pushToken,
|
||||||
isForcedUpdate: shouldUploadTokens,
|
voipToken: voipToken,
|
||||||
success: { resolver(Result.success(())) },
|
isForcedUpdate: shouldUploadTokens,
|
||||||
failure: { resolver(Result.failure($0)) }
|
success: { resolver(Result.success(())) },
|
||||||
)
|
failure: { resolver(Result.failure($0)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.handleEvents(
|
.handleEvents(
|
||||||
receiveCompletion: { result in
|
receiveCompletion: { result in
|
||||||
|
|
|
@ -73,25 +73,27 @@ class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelega
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
||||||
func registerNotificationSettings() -> Future<Void, Never> {
|
func registerNotificationSettings() -> AnyPublisher<Void, Never> {
|
||||||
return Future { [weak self] resolver in
|
return Deferred {
|
||||||
self?.notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
|
Future { [weak self] resolver in
|
||||||
self?.notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories)
|
self?.notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
|
||||||
|
self?.notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories)
|
||||||
if granted {}
|
|
||||||
else if let error: Error = error {
|
if granted {}
|
||||||
Logger.error("failed with error: \(error)")
|
else if let error: Error = error {
|
||||||
|
Logger.error("failed with error: \(error)")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger.error("failed without error.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that the promise is fulfilled regardless of if notification permssions were
|
||||||
|
// granted. This promise only indicates that the user has responded, so we can
|
||||||
|
// proceed with requesting push tokens and complete registration.
|
||||||
|
resolver(Result.success(()))
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
Logger.error("failed without error.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that the promise is fulfilled regardless of if notification permssions were
|
|
||||||
// granted. This promise only indicates that the user has responded, so we can
|
|
||||||
// proceed with requesting push tokens and complete registration.
|
|
||||||
resolver(Result.success(()))
|
|
||||||
}
|
}
|
||||||
}
|
}.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(
|
func notify(
|
||||||
|
@ -114,7 +116,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
||||||
content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier)
|
content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier)
|
||||||
|
|
||||||
let shouldGroupNotification: Bool = (
|
let shouldGroupNotification: Bool = (
|
||||||
threadVariant == .openGroup &&
|
threadVariant == .community &&
|
||||||
replacingIdentifier == threadIdentifier
|
replacingIdentifier == threadIdentifier
|
||||||
)
|
)
|
||||||
let isAppActive = UIApplication.shared.applicationState == .active
|
let isAppActive = UIApplication.shared.applicationState == .active
|
||||||
|
|
|
@ -23,11 +23,8 @@ enum Onboarding {
|
||||||
return Atomic(
|
return Atomic(
|
||||||
SnodeAPI.getSwarm(for: userPublicKey)
|
SnodeAPI.getSwarm(for: userPublicKey)
|
||||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||||
.flatMap { swarm -> AnyPublisher<Void, Error> in
|
.tryFlatMap { swarm -> AnyPublisher<Void, Error> in
|
||||||
guard let snode = swarm.randomElement() else {
|
guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic }
|
||||||
return Fail(error: SnodeAPIError.generic)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return CurrentUserPoller.poll(
|
return CurrentUserPoller.poll(
|
||||||
namespaces: [.configUserProfile],
|
namespaces: [.configUserProfile],
|
||||||
|
@ -41,7 +38,7 @@ enum Onboarding {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.flatMap { _ -> AnyPublisher<String?, Error> in
|
.flatMap { _ -> AnyPublisher<String?, Error> in
|
||||||
Storage.shared.readPublisher { db in
|
Storage.shared.readPublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
try Profile
|
try Profile
|
||||||
.filter(id: userPublicKey)
|
.filter(id: userPublicKey)
|
||||||
.select(.name)
|
.select(.name)
|
||||||
|
|
|
@ -169,7 +169,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
|
||||||
|
|
||||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in
|
ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap { db in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db in
|
||||||
OpenGroupManager.shared.add(
|
OpenGroupManager.shared.add(
|
||||||
db,
|
db,
|
||||||
roomToken: roomToken,
|
roomToken: roomToken,
|
||||||
|
@ -194,7 +194,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
|
||||||
if shouldOpenCommunity {
|
if shouldOpenCommunity {
|
||||||
SessionApp.presentConversation(
|
SessionApp.presentConversation(
|
||||||
for: OpenGroup.idFor(roomToken: roomToken, server: server),
|
for: OpenGroup.idFor(roomToken: roomToken, server: server),
|
||||||
threadVariant: .openGroup,
|
threadVariant: .community,
|
||||||
isMessageRequest: false,
|
isMessageRequest: false,
|
||||||
action: .compose,
|
action: .compose,
|
||||||
focusInteractionInfo: nil,
|
focusInteractionInfo: nil,
|
||||||
|
|
|
@ -322,7 +322,7 @@ extension OpenGroupSuggestionGrid {
|
||||||
Publishers
|
Publishers
|
||||||
.MergeMany(
|
.MergeMany(
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisherFlatMap { db in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.main) { db in
|
||||||
OpenGroupManager
|
OpenGroupManager
|
||||||
.roomImage(db, fileId: imageId, for: room.token, on: OpenGroupAPI.defaultServer)
|
.roomImage(db, fileId: imageId, for: room.token, on: OpenGroupAPI.defaultServer)
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
||||||
) {
|
) {
|
||||||
let version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
|
let version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
|
||||||
.defaulting(to: "")
|
.defaulting(to: "")
|
||||||
OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)")
|
OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion), App: \(version), libSession: \(SessionUtil.libSessionVersion)")
|
||||||
DDLog.flushLog()
|
DDLog.flushLog()
|
||||||
|
|
||||||
let logFilePaths: [String] = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths
|
let logFilePaths: [String] = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths
|
||||||
|
|
|
@ -352,14 +352,14 @@ public final class FullConversationCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cellViewModel.threadVariant {
|
switch cellViewModel.threadVariant {
|
||||||
case .contact, .openGroup: bottomLabelStackView.isHidden = true
|
case .contact, .community: bottomLabelStackView.isHidden = true
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
bottomLabelStackView.isHidden = (cellViewModel.threadMemberNames ?? "").isEmpty
|
bottomLabelStackView.isHidden = (cellViewModel.threadMemberNames ?? "").isEmpty
|
||||||
|
|
||||||
ThemeManager.onThemeChange(observer: displayNameLabel) { [weak self, weak snippetLabel] theme, _ in
|
ThemeManager.onThemeChange(observer: displayNameLabel) { [weak self, weak snippetLabel] theme, _ in
|
||||||
guard let textColor: UIColor = theme.color(for: .textPrimary) else { return }
|
guard let textColor: UIColor = theme.color(for: .textPrimary) else { return }
|
||||||
if cellViewModel.threadVariant == .legacyClosedGroup || cellViewModel.threadVariant == .closedGroup {
|
if cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group {
|
||||||
snippetLabel?.attributedText = self?.getHighlightedSnippet(
|
snippetLabel?.attributedText = self?.getHighlightedSnippet(
|
||||||
content: (cellViewModel.threadMemberNames ?? ""),
|
content: (cellViewModel.threadMemberNames ?? ""),
|
||||||
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
||||||
|
@ -409,9 +409,9 @@ public final class FullConversationCell: UITableViewCell {
|
||||||
)
|
)
|
||||||
hasMentionView.isHidden = !(
|
hasMentionView.isHidden = !(
|
||||||
((cellViewModel.threadUnreadMentionCount ?? 0) > 0) && (
|
((cellViewModel.threadUnreadMentionCount ?? 0) > 0) && (
|
||||||
cellViewModel.threadVariant == .legacyClosedGroup ||
|
cellViewModel.threadVariant == .legacyGroup ||
|
||||||
cellViewModel.threadVariant == .closedGroup ||
|
cellViewModel.threadVariant == .group ||
|
||||||
cellViewModel.threadVariant == .openGroup
|
cellViewModel.threadVariant == .community
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
profilePictureView.update(
|
profilePictureView.update(
|
||||||
|
@ -514,7 +514,7 @@ public final class FullConversationCell: UITableViewCell {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
if cellViewModel.threadVariant == .legacyClosedGroup || cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup {
|
if cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group || cellViewModel.threadVariant == .community {
|
||||||
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
|
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
|
||||||
|
|
||||||
result.append(NSAttributedString(
|
result.append(NSAttributedString(
|
||||||
|
|
|
@ -67,11 +67,8 @@ public final class BackgroundPoller {
|
||||||
return SnodeAPI.getSwarm(for: userPublicKey)
|
return SnodeAPI.getSwarm(for: userPublicKey)
|
||||||
.subscribeOnMain(immediately: true)
|
.subscribeOnMain(immediately: true)
|
||||||
.receiveOnMain(immediately: true)
|
.receiveOnMain(immediately: true)
|
||||||
.flatMap { swarm -> AnyPublisher<Void, Error> in
|
.tryFlatMap { swarm -> AnyPublisher<Void, Error> in
|
||||||
guard let snode = swarm.randomElement() else {
|
guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic }
|
||||||
return Fail(error: SnodeAPIError.generic)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return CurrentUserPoller.poll(
|
return CurrentUserPoller.poll(
|
||||||
namespaces: CurrentUserPoller.namespaces,
|
namespaces: CurrentUserPoller.namespaces,
|
||||||
|
@ -104,10 +101,9 @@ public final class BackgroundPoller {
|
||||||
SnodeAPI.getSwarm(for: groupPublicKey)
|
SnodeAPI.getSwarm(for: groupPublicKey)
|
||||||
.subscribeOnMain(immediately: true)
|
.subscribeOnMain(immediately: true)
|
||||||
.receiveOnMain(immediately: true)
|
.receiveOnMain(immediately: true)
|
||||||
.flatMap { swarm -> AnyPublisher<Void, Error> in
|
.tryFlatMap { swarm -> AnyPublisher<Void, Error> in
|
||||||
guard let snode: Snode = swarm.randomElement() else {
|
guard let snode: Snode = swarm.randomElement() else {
|
||||||
return Fail(error: OnionRequestAPIError.insufficientSnodes)
|
throw OnionRequestAPIError.insufficientSnodes
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClosedGroupPoller.poll(
|
return ClosedGroupPoller.poll(
|
||||||
|
|
|
@ -241,7 +241,7 @@ enum MockDataGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
let thread: SessionThread = try! SessionThread
|
let thread: SessionThread = try! SessionThread
|
||||||
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .legacyClosedGroup)
|
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .legacyGroup)
|
||||||
.with(shouldBeVisible: true)
|
.with(shouldBeVisible: true)
|
||||||
.saved(db)
|
.saved(db)
|
||||||
_ = try! ClosedGroup(
|
_ = try! ClosedGroup(
|
||||||
|
@ -367,7 +367,7 @@ enum MockDataGenerator {
|
||||||
|
|
||||||
// Create the open group model and the thread
|
// Create the open group model and the thread
|
||||||
let thread: SessionThread = try! SessionThread
|
let thread: SessionThread = try! SessionThread
|
||||||
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .openGroup)
|
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .community)
|
||||||
.with(shouldBeVisible: true)
|
.with(shouldBeVisible: true)
|
||||||
.saved(db)
|
.saved(db)
|
||||||
_ = try! OpenGroup(
|
_ = try! OpenGroup(
|
||||||
|
|
|
@ -163,48 +163,50 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future<Void, Error> { [weak self] resolver in
|
return Deferred {
|
||||||
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
|
Future<Void, Error> { [weak self] resolver in
|
||||||
if let error = error {
|
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.peerConnection?.setLocalDescription(sdp) { error in
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Couldn't initiate call due to error: \(error).")
|
|
||||||
resolver(Result.failure(error))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
|
||||||
Storage.shared
|
preconditionFailure()
|
||||||
.writePublisher { db in
|
|
||||||
try MessageSender
|
|
||||||
.preparedSendData(
|
|
||||||
db,
|
|
||||||
message: CallMessage(
|
|
||||||
uuid: uuid,
|
|
||||||
kind: .offer,
|
|
||||||
sdps: [ sdp.sdp ],
|
|
||||||
sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
|
|
||||||
),
|
|
||||||
to: try Message.Destination.from(db, thread: thread),
|
|
||||||
interactionId: nil
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
|
||||||
.sinkUntilComplete(
|
self?.peerConnection?.setLocalDescription(sdp) { error in
|
||||||
receiveCompletion: { result in
|
if let error = error {
|
||||||
switch result {
|
print("Couldn't initiate call due to error: \(error).")
|
||||||
case .finished: resolver(Result.success(()))
|
resolver(Result.failure(error))
|
||||||
case .failure(let error): resolver(Result.failure(error))
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
Storage.shared
|
||||||
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
|
try MessageSender
|
||||||
|
.preparedSendData(
|
||||||
|
db,
|
||||||
|
message: CallMessage(
|
||||||
|
uuid: uuid,
|
||||||
|
kind: .offer,
|
||||||
|
sdps: [ sdp.sdp ],
|
||||||
|
sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||||
|
),
|
||||||
|
to: try Message.Destination.from(db, thread: thread),
|
||||||
|
interactionId: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||||
|
.sinkUntilComplete(
|
||||||
|
receiveCompletion: { result in
|
||||||
|
switch result {
|
||||||
|
case .finished: resolver(Result.success(()))
|
||||||
|
case .failure(let error): resolver(Result.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -216,10 +218,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
||||||
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
|
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.readPublisherFlatMap { db -> AnyPublisher<SessionThread, Error> in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<SessionThread, Error> in
|
||||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
|
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
|
||||||
return Fail(error: WebRTCSessionError.noThread)
|
throw WebRTCSessionError.noThread
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Just(thread)
|
return Just(thread)
|
||||||
|
@ -246,7 +247,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
try MessageSender
|
try MessageSender
|
||||||
.preparedSendData(
|
.preparedSendData(
|
||||||
db,
|
db,
|
||||||
|
@ -293,10 +294,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
||||||
self.queuedICECandidates.removeAll()
|
self.queuedICECandidates.removeAll()
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap { db in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
|
||||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else {
|
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else {
|
||||||
return Fail(error: WebRTCSessionError.noThread)
|
throw WebRTCSessionError.noThread
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")
|
SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")
|
||||||
|
|
|
@ -565,7 +565,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
|
|
||||||
switch legacyThread {
|
switch legacyThread {
|
||||||
case let groupThread as SMKLegacy._GroupThread:
|
case let groupThread as SMKLegacy._GroupThread:
|
||||||
threadVariant = (groupThread.isOpenGroup ? .openGroup : .legacyClosedGroup)
|
threadVariant = (groupThread.isOpenGroup ? .community : .legacyGroup)
|
||||||
onlyNotifyForMentions = groupThread.isOnlyNotifyingForMentions
|
onlyNotifyForMentions = groupThread.isOnlyNotifyingForMentions
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1036,7 +1036,7 @@ extension Attachment {
|
||||||
let attachmentId: String = self.id
|
let attachmentId: String = self.id
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.writePublisherFlatMap { db -> AnyPublisher<(String?, Data?, Data?), Error> in
|
.writePublisherFlatMap(receiveOn: queue) { db -> AnyPublisher<(String?, Data?, Data?), Error> in
|
||||||
// If the attachment is a downloaded attachment, check if it came from
|
// If the attachment is a downloaded attachment, check if it came from
|
||||||
// the server and if so just succeed immediately (no use re-uploading
|
// the server and if so just succeed immediately (no use re-uploading
|
||||||
// an attachment that is already present on the server) - or if we want
|
// an attachment that is already present on the server) - or if we want
|
||||||
|
@ -1068,8 +1068,7 @@ extension Attachment {
|
||||||
if destination.shouldEncrypt {
|
if destination.shouldEncrypt {
|
||||||
guard let ciphertext = Cryptography.encryptAttachmentData(data, shouldPad: true, outKey: &encryptionKey, outDigest: &digest) else {
|
guard let ciphertext = Cryptography.encryptAttachmentData(data, shouldPad: true, outKey: &encryptionKey, outDigest: &digest) else {
|
||||||
SNLog("Couldn't encrypt attachment.")
|
SNLog("Couldn't encrypt attachment.")
|
||||||
return Fail(error: AttachmentError.encryptionFailed)
|
throw AttachmentError.encryptionFailed
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data = ciphertext
|
data = ciphertext
|
||||||
|
@ -1077,10 +1076,7 @@ extension Attachment {
|
||||||
|
|
||||||
// Check the file size
|
// Check the file size
|
||||||
SNLog("File size: \(data.count) bytes.")
|
SNLog("File size: \(data.count) bytes.")
|
||||||
if data.count > FileServerAPI.maxFileSize {
|
if data.count > FileServerAPI.maxFileSize { throw HTTPError.maxFileSizeExceeded }
|
||||||
return Fail(error: HTTPError.maxFileSizeExceeded)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the attachment to the 'uploading' state
|
// Update the attachment to the 'uploading' state
|
||||||
_ = try? Attachment
|
_ = try? Attachment
|
||||||
|
@ -1131,13 +1127,14 @@ extension Attachment {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.receive(on: queue)
|
||||||
.flatMap { fileId, encryptionKey, digest -> AnyPublisher<String?, Error> in
|
.flatMap { fileId, encryptionKey, digest -> AnyPublisher<String?, Error> in
|
||||||
/// Save the final upload info
|
/// Save the final upload info
|
||||||
///
|
///
|
||||||
/// **Note:** We **MUST** use the `.with` function here to ensure the `isValid` flag is
|
/// **Note:** We **MUST** use the `.with` function here to ensure the `isValid` flag is
|
||||||
/// updated correctly
|
/// updated correctly
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: queue) { db in
|
||||||
try self
|
try self
|
||||||
.with(
|
.with(
|
||||||
serverId: fileId,
|
serverId: fileId,
|
||||||
|
|
|
@ -353,7 +353,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
state: .sending
|
state: .sending
|
||||||
).insert(db)
|
).insert(db)
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
let closedGroupMemberIds: Set<String> = (try? GroupMember
|
let closedGroupMemberIds: Set<String> = (try? GroupMember
|
||||||
.select(.profileId)
|
.select(.profileId)
|
||||||
.filter(GroupMember.Columns.groupId == threadId)
|
.filter(GroupMember.Columns.groupId == threadId)
|
||||||
|
@ -379,7 +379,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
).insert(db)
|
).insert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
// Since we use the 'RecipientState' type to manage the message state
|
// Since we use the 'RecipientState' type to manage the message state
|
||||||
// we need to ensure we have a state for all threads; so for open groups
|
// we need to ensure we have a state for all threads; so for open groups
|
||||||
// we just use the open group id as the 'recipientId' value
|
// we just use the open group id as the 'recipientId' value
|
||||||
|
|
|
@ -318,17 +318,12 @@ public extension LinkPreview {
|
||||||
.flatMap { data, response in
|
.flatMap { data, response in
|
||||||
parseLinkDataAndBuildDraft(linkData: data, response: response, linkUrlString: previewUrl)
|
parseLinkDataAndBuildDraft(linkData: data, response: response, linkUrlString: previewUrl)
|
||||||
}
|
}
|
||||||
.flatMap { linkPreviewDraft -> AnyPublisher<LinkPreviewDraft, Error> in
|
.tryMap { linkPreviewDraft -> LinkPreviewDraft in
|
||||||
guard linkPreviewDraft.isValid() else {
|
guard linkPreviewDraft.isValid() else { throw LinkPreviewError.noPreview }
|
||||||
return Fail(error: LinkPreviewError.noPreview)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
setCachedLinkPreview(linkPreviewDraft, forPreviewUrl: previewUrl)
|
setCachedLinkPreview(linkPreviewDraft, forPreviewUrl: previewUrl)
|
||||||
|
|
||||||
return Just(linkPreviewDraft)
|
return linkPreviewDraft
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -362,25 +357,18 @@ public extension LinkPreview {
|
||||||
.dataTaskPublisher(for: request)
|
.dataTaskPublisher(for: request)
|
||||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||||
.mapError { _ -> Error in HTTPError.generic } // URLError codes are negative values
|
.mapError { _ -> Error in HTTPError.generic } // URLError codes are negative values
|
||||||
.flatMap { data, response -> AnyPublisher<(Data, URLResponse), Error> in
|
.tryMap { data, response -> (Data, URLResponse) in
|
||||||
guard let urlResponse: HTTPURLResponse = response as? HTTPURLResponse else {
|
guard let urlResponse: HTTPURLResponse = response as? HTTPURLResponse else {
|
||||||
return Fail(error: LinkPreviewError.assertionFailure)
|
throw LinkPreviewError.assertionFailure
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
if let contentType: String = urlResponse.allHeaderFields["Content-Type"] as? String {
|
if let contentType: String = urlResponse.allHeaderFields["Content-Type"] as? String {
|
||||||
guard contentType.lowercased().hasPrefix("text/") else {
|
guard contentType.lowercased().hasPrefix("text/") else {
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
throw LinkPreviewError.invalidContent
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard data.count > 0 else {
|
guard data.count > 0 else { throw LinkPreviewError.invalidContent }
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just((data, response))
|
return (data, response)
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.catch { error -> AnyPublisher<(Data, URLResponse), Error> in
|
.catch { error -> AnyPublisher<(Data, URLResponse), Error> in
|
||||||
guard isRetryable(error: error), remainingRetries > 0 else {
|
guard isRetryable(error: error), remainingRetries > 0 else {
|
||||||
|
@ -496,63 +484,44 @@ public extension LinkPreview {
|
||||||
priority: .high,
|
priority: .high,
|
||||||
shouldIgnoreSignalProxy: true
|
shouldIgnoreSignalProxy: true
|
||||||
)
|
)
|
||||||
.flatMap { asset, _ -> AnyPublisher<Data, Error> in
|
.tryMap { asset, _ -> Data in
|
||||||
do {
|
let imageSize = NSData.imageSize(forFilePath: asset.filePath, mimeType: imageMimeType)
|
||||||
let imageSize = NSData.imageSize(forFilePath: asset.filePath, mimeType: imageMimeType)
|
|
||||||
|
guard imageSize.width > 0, imageSize.height > 0 else {
|
||||||
guard imageSize.width > 0, imageSize.height > 0 else {
|
throw LinkPreviewError.invalidContent
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = try Data(contentsOf: URL(fileURLWithPath: asset.filePath))
|
|
||||||
|
|
||||||
guard let srcImage = UIImage(data: data) else {
|
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loki: If it's a GIF then ensure its validity and don't download it as a JPG
|
|
||||||
if
|
|
||||||
imageMimeType == OWSMimeTypeImageGif &&
|
|
||||||
NSData(data: data).ows_isValidImage(withMimeType: OWSMimeTypeImageGif)
|
|
||||||
{
|
|
||||||
return Just(data)
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxImageSize: CGFloat = 1024
|
|
||||||
let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize
|
|
||||||
|
|
||||||
guard shouldResize else {
|
|
||||||
guard let dstData = srcImage.jpegData(compressionQuality: 0.8) else {
|
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just(dstData)
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let dstImage = srcImage.resized(withMaxDimensionPoints: maxImageSize) else {
|
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
guard let dstData = dstImage.jpegData(compressionQuality: 0.8) else {
|
|
||||||
return Fail(error: LinkPreviewError.invalidContent)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just(dstData)
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
return Fail(error: LinkPreviewError.assertionFailure)
|
guard let data: Data = try? Data(contentsOf: URL(fileURLWithPath: asset.filePath)) else {
|
||||||
.eraseToAnyPublisher()
|
throw LinkPreviewError.assertionFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let srcImage = UIImage(data: data) else { throw LinkPreviewError.invalidContent }
|
||||||
|
|
||||||
|
// Loki: If it's a GIF then ensure its validity and don't download it as a JPG
|
||||||
|
if
|
||||||
|
imageMimeType == OWSMimeTypeImageGif &&
|
||||||
|
NSData(data: data).ows_isValidImage(withMimeType: OWSMimeTypeImageGif)
|
||||||
|
{
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxImageSize: CGFloat = 1024
|
||||||
|
let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize
|
||||||
|
|
||||||
|
guard shouldResize else {
|
||||||
|
guard let dstData = srcImage.jpegData(compressionQuality: 0.8) else {
|
||||||
|
throw LinkPreviewError.invalidContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return dstData
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
let dstImage = srcImage.resized(withMaxDimensionPoints: maxImageSize),
|
||||||
|
let dstData = dstImage.jpegData(compressionQuality: 0.8)
|
||||||
|
else { throw LinkPreviewError.invalidContent }
|
||||||
|
|
||||||
|
return dstData
|
||||||
}
|
}
|
||||||
.mapError { _ -> Error in LinkPreviewError.couldNotDownload }
|
.mapError { _ -> Error in LinkPreviewError.couldNotDownload }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
|
@ -324,9 +324,9 @@ public extension Profile {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact, .legacyClosedGroup, .closedGroup: return name
|
case .contact, .legacyGroup, .group: return name
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
// In open groups, where it's more likely that multiple users have the same name,
|
// In open groups, where it's more likely that multiple users have the same name,
|
||||||
// we display a bit of the Session ID after a user's display name for added context
|
// we display a bit of the Session ID after a user's display name for added context
|
||||||
return "\(name) (\(Profile.truncated(id: id, truncating: .middle)))"
|
return "\(name) (\(Profile.truncated(id: id, truncating: .middle)))"
|
||||||
|
|
|
@ -37,9 +37,9 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
|
||||||
|
|
||||||
public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible {
|
public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible {
|
||||||
case contact
|
case contact
|
||||||
case legacyClosedGroup
|
case legacyGroup
|
||||||
case openGroup
|
case community
|
||||||
case closedGroup
|
case group
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unique identifier for a thread (formerly known as uniqueId)
|
/// Unique identifier for a thread (formerly known as uniqueId)
|
||||||
|
@ -312,8 +312,8 @@ public extension SessionThread {
|
||||||
profile: Profile? = nil
|
profile: Profile? = nil
|
||||||
) -> String {
|
) -> String {
|
||||||
switch variant {
|
switch variant {
|
||||||
case .legacyClosedGroup, .closedGroup: return (closedGroupName ?? "Unknown Group")
|
case .legacyGroup, .group: return (closedGroupName ?? "Unknown Group")
|
||||||
case .openGroup: return (openGroupName ?? "Unknown Group")
|
case .community: return (openGroupName ?? "Unknown Community")
|
||||||
case .contact:
|
case .contact:
|
||||||
guard !isNoteToSelf else { return "NOTE_TO_SELF".localized() }
|
guard !isNoteToSelf else { return "NOTE_TO_SELF".localized() }
|
||||||
guard let profile: Profile = profile else {
|
guard let profile: Profile = profile else {
|
||||||
|
@ -329,7 +329,7 @@ public extension SessionThread {
|
||||||
threadVariant: Variant
|
threadVariant: Variant
|
||||||
) -> String? {
|
) -> String? {
|
||||||
guard
|
guard
|
||||||
threadVariant == .openGroup,
|
threadVariant == .community,
|
||||||
let blindingInfo: (edkeyPair: Box.KeyPair?, publicKey: String?) = Storage.shared.read({ db in
|
let blindingInfo: (edkeyPair: Box.KeyPair?, publicKey: String?) = Storage.shared.read({ db in
|
||||||
return (
|
return (
|
||||||
Identity.fetchUserEd25519KeyPair(db),
|
Identity.fetchUserEd25519KeyPair(db),
|
||||||
|
|
|
@ -20,7 +20,7 @@ public struct ConfigDump: Codable, Equatable, Hashable, FetchableRecord, Persist
|
||||||
case userProfile
|
case userProfile
|
||||||
case contacts
|
case contacts
|
||||||
case convoInfoVolatile
|
case convoInfoVolatile
|
||||||
case groups
|
case userGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of config this dump is for
|
/// The type of config this dump is for
|
||||||
|
@ -66,14 +66,14 @@ public extension ConfigDump {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ConfigDump.Variant {
|
public extension ConfigDump.Variant {
|
||||||
static let userVariants: [ConfigDump.Variant] = [ .userProfile, .contacts, .convoInfoVolatile, .groups ]
|
static let userVariants: [ConfigDump.Variant] = [ .userProfile, .contacts, .convoInfoVolatile, .userGroups ]
|
||||||
|
|
||||||
var configMessageKind: SharedConfigMessage.Kind {
|
var configMessageKind: SharedConfigMessage.Kind {
|
||||||
switch self {
|
switch self {
|
||||||
case .userProfile: return .userProfile
|
case .userProfile: return .userProfile
|
||||||
case .contacts: return .contacts
|
case .contacts: return .contacts
|
||||||
case .convoInfoVolatile: return .convoInfoVolatile
|
case .convoInfoVolatile: return .convoInfoVolatile
|
||||||
case .groups: return .groups
|
case .userGroups: return .userGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ public extension ConfigDump.Variant {
|
||||||
case .userProfile: return SnodeAPI.Namespace.configUserProfile
|
case .userProfile: return SnodeAPI.Namespace.configUserProfile
|
||||||
case .contacts: return SnodeAPI.Namespace.configContacts
|
case .contacts: return SnodeAPI.Namespace.configContacts
|
||||||
case .convoInfoVolatile: return SnodeAPI.Namespace.configConvoInfoVolatile
|
case .convoInfoVolatile: return SnodeAPI.Namespace.configConvoInfoVolatile
|
||||||
case .groups: return SnodeAPI.Namespace.configGroups
|
case .userGroups: return SnodeAPI.Namespace.configUserGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,15 +79,10 @@ public enum FileServerAPI {
|
||||||
|
|
||||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout)
|
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout)
|
||||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||||
.flatMap { _, response -> AnyPublisher<Data, Error> in
|
.tryMap { _, response -> Data in
|
||||||
guard let response: Data = response else {
|
guard let response: Data = response else { throw HTTPError.parsingFailed }
|
||||||
return Fail(error: HTTPError.parsingFailed)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just(response)
|
return response
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,17 +87,14 @@ public enum AttachmentDownloadJob: JobExecutor {
|
||||||
|
|
||||||
Just(attachment.downloadUrl)
|
Just(attachment.downloadUrl)
|
||||||
.setFailureType(to: Error.self)
|
.setFailureType(to: Error.self)
|
||||||
.flatMap { maybeDownloadUrl -> AnyPublisher<Data, Error> in
|
.tryFlatMap { maybeDownloadUrl -> AnyPublisher<Data, Error> in
|
||||||
guard
|
guard
|
||||||
let downloadUrl: String = maybeDownloadUrl,
|
let downloadUrl: String = maybeDownloadUrl,
|
||||||
let fileId: String = Attachment.fileId(for: downloadUrl)
|
let fileId: String = Attachment.fileId(for: downloadUrl)
|
||||||
else {
|
else { throw AttachmentDownloadError.invalidUrl }
|
||||||
return Fail(error: AttachmentDownloadError.invalidUrl)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.readPublisher { db in try OpenGroup.fetchOne(db, id: threadId) }
|
.readPublisher(receiveOn: queue) { db in try OpenGroup.fetchOne(db, id: threadId) }
|
||||||
.flatMap { maybeOpenGroup -> AnyPublisher<Data, Error> in
|
.flatMap { maybeOpenGroup -> AnyPublisher<Data, Error> in
|
||||||
guard let openGroup: OpenGroup = maybeOpenGroup else {
|
guard let openGroup: OpenGroup = maybeOpenGroup else {
|
||||||
return FileServerAPI
|
return FileServerAPI
|
||||||
|
@ -109,7 +106,7 @@ public enum AttachmentDownloadJob: JobExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.readPublisherFlatMap { db in
|
.readPublisherFlatMap(receiveOn: queue) { db in
|
||||||
OpenGroupAPI
|
OpenGroupAPI
|
||||||
.downloadFile(
|
.downloadFile(
|
||||||
db,
|
db,
|
||||||
|
@ -123,41 +120,34 @@ public enum AttachmentDownloadJob: JobExecutor {
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.flatMap { data -> AnyPublisher<Void, Error> in
|
.receive(on: queue)
|
||||||
do {
|
.tryMap { data -> Void in
|
||||||
// Store the encrypted data temporarily
|
// Store the encrypted data temporarily
|
||||||
try data.write(to: temporaryFileUrl, options: .atomic)
|
try data.write(to: temporaryFileUrl, options: .atomic)
|
||||||
|
|
||||||
// Decrypt the data
|
// Decrypt the data
|
||||||
let plaintext: Data = try {
|
let plaintext: Data = try {
|
||||||
guard
|
guard
|
||||||
let key: Data = attachment.encryptionKey,
|
let key: Data = attachment.encryptionKey,
|
||||||
let digest: Data = attachment.digest,
|
let digest: Data = attachment.digest,
|
||||||
key.count > 0,
|
key.count > 0,
|
||||||
digest.count > 0
|
digest.count > 0
|
||||||
else { return data } // Open group attachments are unencrypted
|
else { return data } // Open group attachments are unencrypted
|
||||||
|
|
||||||
return try Cryptography.decryptAttachment(
|
return try Cryptography.decryptAttachment(
|
||||||
data,
|
data,
|
||||||
withKey: key,
|
withKey: key,
|
||||||
digest: digest,
|
digest: digest,
|
||||||
unpaddedSize: UInt32(attachment.byteCount)
|
unpaddedSize: UInt32(attachment.byteCount)
|
||||||
)
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Write the data to disk
|
// Write the data to disk
|
||||||
guard try attachment.write(data: plaintext) else {
|
guard try attachment.write(data: plaintext) else {
|
||||||
throw AttachmentDownloadError.failedToSaveFile
|
throw AttachmentDownloadError.failedToSaveFile
|
||||||
}
|
|
||||||
|
|
||||||
return Just(())
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return Fail(error: error)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ()
|
||||||
}
|
}
|
||||||
.sinkUntilComplete(
|
.sinkUntilComplete(
|
||||||
receiveCompletion: { result in
|
receiveCompletion: { result in
|
||||||
|
|
|
@ -39,18 +39,7 @@ public enum ConfigurationSyncJob: JobExecutor {
|
||||||
// fresh install due to the migrations getting run)
|
// fresh install due to the migrations getting run)
|
||||||
guard
|
guard
|
||||||
let pendingSwarmConfigChanges: [SingleDestinationChanges] = Storage.shared
|
let pendingSwarmConfigChanges: [SingleDestinationChanges] = Storage.shared
|
||||||
.read({ db -> [SessionUtil.OutgoingConfResult]? in
|
.read({ db in try SessionUtil.pendingChanges(db) })?
|
||||||
guard
|
|
||||||
Identity.userExists(db),
|
|
||||||
let ed25519SecretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
return try SessionUtil.pendingChanges(
|
|
||||||
db,
|
|
||||||
userPublicKey: getUserHexEncodedPublicKey(db),
|
|
||||||
ed25519SecretKey: ed25519SecretKey
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.grouped(by: { $0.destination })
|
.grouped(by: { $0.destination })
|
||||||
.map({ (destination: Message.Destination, value: [SessionUtil.OutgoingConfResult]) -> SingleDestinationChanges in
|
.map({ (destination: Message.Destination, value: [SessionUtil.OutgoingConfResult]) -> SingleDestinationChanges in
|
||||||
SingleDestinationChanges(
|
SingleDestinationChanges(
|
||||||
|
@ -75,7 +64,7 @@ public enum ConfigurationSyncJob: JobExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisher { db in
|
.readPublisher(receiveOn: queue) { db in
|
||||||
try pendingSwarmConfigChanges
|
try pendingSwarmConfigChanges
|
||||||
.map { (change: SingleDestinationChanges) -> (messages: [TargetedMessage], allOldHashes: Set<String>) in
|
.map { (change: SingleDestinationChanges) -> (messages: [TargetedMessage], allOldHashes: Set<String>) in
|
||||||
(
|
(
|
||||||
|
@ -96,8 +85,6 @@ public enum ConfigurationSyncJob: JobExecutor {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribe(on: queue)
|
|
||||||
.receive(on: queue)
|
|
||||||
.flatMap { (pendingSwarmChange: [(messages: [TargetedMessage], allOldHashes: Set<String>)]) -> AnyPublisher<[HTTP.BatchResponse], Error> in
|
.flatMap { (pendingSwarmChange: [(messages: [TargetedMessage], allOldHashes: Set<String>)]) -> AnyPublisher<[HTTP.BatchResponse], Error> in
|
||||||
Publishers
|
Publishers
|
||||||
.MergeMany(
|
.MergeMany(
|
||||||
|
@ -119,17 +106,17 @@ public enum ConfigurationSyncJob: JobExecutor {
|
||||||
.collect()
|
.collect()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
.flatMap { (responses: [HTTP.BatchResponse]) -> AnyPublisher<[SuccessfulChange], Error> in
|
.receive(on: queue)
|
||||||
|
.tryMap { (responses: [HTTP.BatchResponse]) -> [SuccessfulChange] in
|
||||||
// We make a sequence call for this so it's possible to get fewer responses than
|
// We make a sequence call for this so it's possible to get fewer responses than
|
||||||
// expected so if that happens fail and re-run later
|
// expected so if that happens fail and re-run later
|
||||||
guard responses.count == pendingSwarmConfigChanges.count else {
|
guard responses.count == pendingSwarmConfigChanges.count else {
|
||||||
return Fail(error: HTTPError.invalidResponse)
|
throw HTTPError.invalidResponse
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the response data into an easy to understand for (this isn't strictly
|
// Process the response data into an easy to understand for (this isn't strictly
|
||||||
// needed but the code gets convoluted without this)
|
// needed but the code gets convoluted without this)
|
||||||
let successfulChanges: [SuccessfulChange] = zip(responses, pendingSwarmConfigChanges)
|
return zip(responses, pendingSwarmConfigChanges)
|
||||||
.compactMap { (batchResponse: HTTP.BatchResponse, pendingSwarmChange: SingleDestinationChanges) -> [SuccessfulChange]? in
|
.compactMap { (batchResponse: HTTP.BatchResponse, pendingSwarmChange: SingleDestinationChanges) -> [SuccessfulChange]? in
|
||||||
let maybePublicKey: String? = {
|
let maybePublicKey: String? = {
|
||||||
switch pendingSwarmChange.destination {
|
switch pendingSwarmChange.destination {
|
||||||
|
@ -179,10 +166,6 @@ public enum ConfigurationSyncJob: JobExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatMap { $0 }
|
.flatMap { $0 }
|
||||||
|
|
||||||
return Just(successfulChanges)
|
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.map { (successfulChanges: [SuccessfulChange]) -> [ConfigDump] in
|
.map { (successfulChanges: [SuccessfulChange]) -> [ConfigDump] in
|
||||||
// Now that we have the successful changes, we need to mark them as pushed and
|
// Now that we have the successful changes, we need to mark them as pushed and
|
||||||
|
@ -213,8 +196,7 @@ public enum ConfigurationSyncJob: JobExecutor {
|
||||||
receiveCompletion: { result in
|
receiveCompletion: { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .finished: break
|
case .finished: break
|
||||||
case .failure(let error):
|
case .failure(let error): failure(job, error, false)
|
||||||
failure(job, error, false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
receiveValue: { (configDumps: [ConfigDump]) in
|
receiveValue: { (configDumps: [ConfigDump]) in
|
||||||
|
@ -354,7 +336,7 @@ public extension ConfigurationSyncJob {
|
||||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
guard Features.useSharedUtilForUserConfig else {
|
guard Features.useSharedUtilForUserConfig else {
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.writePublisher { db -> MessageSender.PreparedSendData in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> MessageSender.PreparedSendData in
|
||||||
// If we don't have a userKeyPair yet then there is no need to sync the configuration
|
// If we don't have a userKeyPair yet then there is no need to sync the configuration
|
||||||
// as the user doesn't exist yet (this will get triggered on the first launch of a
|
// as the user doesn't exist yet (this will get triggered on the first launch of a
|
||||||
// fresh install due to the migrations getting run)
|
// fresh install due to the migrations getting run)
|
||||||
|
@ -369,21 +351,21 @@ public extension ConfigurationSyncJob {
|
||||||
interactionId: nil
|
interactionId: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
|
||||||
.receive(on: DispatchQueue.global(qos: .userInitiated))
|
|
||||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger the job emitting the result when completed
|
// Trigger the job emitting the result when completed
|
||||||
return Future { resolver in
|
return Deferred {
|
||||||
ConfigurationSyncJob.run(
|
Future { resolver in
|
||||||
Job(variant: .configurationSync),
|
ConfigurationSyncJob.run(
|
||||||
queue: DispatchQueue.global(qos: .userInitiated),
|
Job(variant: .configurationSync),
|
||||||
success: { _, _ in resolver(Result.success(())) },
|
queue: DispatchQueue.global(qos: .userInitiated),
|
||||||
failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
|
success: { _, _ in resolver(Result.success(())) },
|
||||||
deferred: { _ in }
|
failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
|
||||||
)
|
deferred: { _ in }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ public enum GarbageCollectionJob: JobExecutor {
|
||||||
SELECT \(interaction.alias[Column.rowID])
|
SELECT \(interaction.alias[Column.rowID])
|
||||||
FROM \(Interaction.self)
|
FROM \(Interaction.self)
|
||||||
JOIN \(SessionThread.self) ON (
|
JOIN \(SessionThread.self) ON (
|
||||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
|
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
|
||||||
\(thread[.id]) = \(interaction[.threadId])
|
\(thread[.id]) = \(interaction[.threadId])
|
||||||
)
|
)
|
||||||
JOIN (
|
JOIN (
|
||||||
|
|
|
@ -165,7 +165,7 @@ public enum MessageSendJob: JobExecutor {
|
||||||
/// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job
|
/// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job
|
||||||
/// so we shouldn't get here until attachments have already been uploaded
|
/// so we shouldn't get here until attachments have already been uploaded
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: queue) { db in
|
||||||
try MessageSender.preparedSendData(
|
try MessageSender.preparedSendData(
|
||||||
db,
|
db,
|
||||||
message: details.message,
|
message: details.message,
|
||||||
|
@ -173,9 +173,9 @@ public enum MessageSendJob: JobExecutor {
|
||||||
interactionId: job.interactionId
|
interactionId: job.interactionId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.subscribe(on: queue)
|
|
||||||
.map { sendData in sendData.with(fileIds: messageFileIds) }
|
.map { sendData in sendData.with(fileIds: messageFileIds) }
|
||||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||||
|
.receive(on: queue)
|
||||||
.sinkUntilComplete(
|
.sinkUntilComplete(
|
||||||
receiveCompletion: { result in
|
receiveCompletion: { result in
|
||||||
switch result {
|
switch result {
|
||||||
|
|
|
@ -36,7 +36,7 @@ public enum SendReadReceiptsJob: JobExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisher { db in
|
.writePublisher(receiveOn: queue) { db in
|
||||||
try MessageSender.preparedSendData(
|
try MessageSender.preparedSendData(
|
||||||
db,
|
db,
|
||||||
message: ReadReceipt(
|
message: ReadReceipt(
|
||||||
|
@ -46,7 +46,6 @@ public enum SendReadReceiptsJob: JobExecutor {
|
||||||
interactionId: nil
|
interactionId: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.subscribe(on: queue)
|
|
||||||
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||||
.receive(on: queue)
|
.receive(on: queue)
|
||||||
.sinkUntilComplete(
|
.sinkUntilComplete(
|
||||||
|
@ -120,9 +119,9 @@ public extension SendReadReceiptsJob {
|
||||||
.joining(
|
.joining(
|
||||||
// Don't send read receipts in group threads
|
// Don't send read receipts in group threads
|
||||||
required: Interaction.thread
|
required: Interaction.thread
|
||||||
.filter(SessionThread.Columns.variant != SessionThread.Variant.legacyClosedGroup)
|
.filter(SessionThread.Columns.variant != SessionThread.Variant.legacyGroup)
|
||||||
.filter(SessionThread.Columns.variant != SessionThread.Variant.closedGroup)
|
.filter(SessionThread.Columns.variant != SessionThread.Variant.group)
|
||||||
.filter(SessionThread.Columns.variant != SessionThread.Variant.openGroup)
|
.filter(SessionThread.Columns.variant != SessionThread.Variant.community)
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,14 +42,17 @@ internal extension SessionUtil {
|
||||||
isBlocked: contact.blocked,
|
isBlocked: contact.blocked,
|
||||||
didApproveMe: contact.approved_me
|
didApproveMe: contact.approved_me
|
||||||
)
|
)
|
||||||
|
let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true)
|
||||||
let profileResult: Profile = Profile(
|
let profileResult: Profile = Profile(
|
||||||
id: contactId,
|
id: contactId,
|
||||||
name: (contact.name.map { String(cString: $0) } ?? ""),
|
name: (String(libSessionVal: contact.name) ?? ""),
|
||||||
nickname: contact.nickname.map { String(cString: $0) },
|
nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true),
|
||||||
profilePictureUrl: contact.profile_pic.url.map { String(cString: $0) },
|
profilePictureUrl: profilePictureUrl,
|
||||||
profileEncryptionKey: (contact.profile_pic.key != nil && contact.profile_pic.keylen > 0 ?
|
profileEncryptionKey: (profilePictureUrl == nil ? nil :
|
||||||
Data(bytes: contact.profile_pic.key, count: contact.profile_pic.keylen) :
|
Data(
|
||||||
nil
|
libSessionVal: contact.profile_pic.key,
|
||||||
|
count: ProfileManager.avatarAES256KeyByteLength
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -165,9 +168,7 @@ internal extension SessionUtil {
|
||||||
// Update the name
|
// Update the name
|
||||||
targetContacts
|
targetContacts
|
||||||
.forEach { (id, maybeContact, maybeProfile) in
|
.forEach { (id, maybeContact, maybeProfile) in
|
||||||
var sessionId: [CChar] = id
|
var sessionId: [CChar] = id.cArray
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
var contact: contacts_contact = contacts_contact()
|
var contact: contacts_contact = contacts_contact()
|
||||||
guard contacts_get_or_construct(conf, &contact, &sessionId) else {
|
guard contacts_get_or_construct(conf, &contact, &sessionId) else {
|
||||||
SNLog("Unable to upsert contact from Config Message")
|
SNLog("Unable to upsert contact from Config Message")
|
||||||
|
@ -179,60 +180,33 @@ internal extension SessionUtil {
|
||||||
contact.approved = updatedContact.isApproved
|
contact.approved = updatedContact.isApproved
|
||||||
contact.approved_me = updatedContact.didApproveMe
|
contact.approved_me = updatedContact.didApproveMe
|
||||||
contact.blocked = updatedContact.isBlocked
|
contact.blocked = updatedContact.isBlocked
|
||||||
|
|
||||||
|
// Store the updated contact (needs to happen before variables go out of scope)
|
||||||
|
contacts_set(conf, &contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the profile data (if there is one)
|
// Update the profile data (if there is one - users we have sent a message request to may
|
||||||
|
// not have profile info in certain situations)
|
||||||
if let updatedProfile: Profile = maybeProfile {
|
if let updatedProfile: Profile = maybeProfile {
|
||||||
/// Users we have sent a message request to may not have profile info in certain situations
|
let oldAvatarUrl: String? = String(libSessionVal: contact.profile_pic.url)
|
||||||
///
|
let oldAvatarKey: Data? = Data(
|
||||||
/// Note: We **MUST** store these in local variables rather than access them directly or they won't
|
libSessionVal: contact.profile_pic.key,
|
||||||
/// exist in memory long enough to actually be assigned in the C type
|
count: ProfileManager.avatarAES256KeyByteLength
|
||||||
let updatedName: [CChar]? = (updatedProfile.name.isEmpty ?
|
|
||||||
nil :
|
|
||||||
updatedProfile.name
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
)
|
)
|
||||||
let updatedNickname: [CChar]? = updatedProfile.nickname?
|
|
||||||
.bytes
|
contact.name = updatedProfile.name.toLibSession()
|
||||||
.map { CChar(bitPattern: $0) }
|
contact.nickname = updatedProfile.nickname.toLibSession()
|
||||||
let updatedAvatarUrl: [CChar]? = updatedProfile.profilePictureUrl?
|
contact.profile_pic.url = updatedProfile.profilePictureUrl.toLibSession()
|
||||||
.bytes
|
contact.profile_pic.key = updatedProfile.profileEncryptionKey.toLibSession()
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let updatedAvatarKey: [UInt8]? = updatedProfile.profileEncryptionKey?
|
|
||||||
.bytes
|
|
||||||
let oldAvatarUrl: String? = contact.profile_pic.url.map { String(cString: $0) }
|
|
||||||
let oldAvatarKey: Data? = (contact.profile_pic.key != nil && contact.profile_pic.keylen > 0 ?
|
|
||||||
Data(bytes: contact.profile_pic.key, count: contact.profile_pic.keylen) :
|
|
||||||
nil
|
|
||||||
)
|
|
||||||
updatedName?.withUnsafeBufferPointer { contact.name = $0.baseAddress }
|
|
||||||
(updatedNickname == nil ?
|
|
||||||
contact.nickname = nil :
|
|
||||||
updatedNickname?.withUnsafeBufferPointer { contact.nickname = $0.baseAddress }
|
|
||||||
)
|
|
||||||
(updatedAvatarUrl == nil ?
|
|
||||||
contact.profile_pic.url = nil :
|
|
||||||
updatedAvatarUrl?.withUnsafeBufferPointer {
|
|
||||||
contact.profile_pic.url = $0.baseAddress
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(updatedAvatarKey == nil ?
|
|
||||||
contact.profile_pic.key = nil :
|
|
||||||
updatedAvatarKey?.withUnsafeBufferPointer {
|
|
||||||
contact.profile_pic.key = $0.baseAddress
|
|
||||||
}
|
|
||||||
)
|
|
||||||
contact.profile_pic.keylen = (updatedAvatarKey?.count ?? 0)
|
|
||||||
|
|
||||||
// Download the profile picture if needed
|
// Download the profile picture if needed
|
||||||
if oldAvatarUrl != updatedProfile.profilePictureUrl || oldAvatarKey != updatedProfile.profileEncryptionKey {
|
if oldAvatarUrl != updatedProfile.profilePictureUrl || oldAvatarKey != updatedProfile.profileEncryptionKey {
|
||||||
ProfileManager.downloadAvatar(for: updatedProfile)
|
ProfileManager.downloadAvatar(for: updatedProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the updated contact (needs to happen before variables go out of scope)
|
||||||
|
contacts_set(conf, &contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the updated contact
|
|
||||||
contacts_set(conf, &contact)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfResult(
|
return ConfResult(
|
||||||
|
|
|
@ -21,8 +21,8 @@ internal extension SessionUtil {
|
||||||
let volatileThreadInfo: [VolatileThreadInfo] = atomicConf.mutate { conf -> [VolatileThreadInfo] in
|
let volatileThreadInfo: [VolatileThreadInfo] = atomicConf.mutate { conf -> [VolatileThreadInfo] in
|
||||||
var volatileThreadInfo: [VolatileThreadInfo] = []
|
var volatileThreadInfo: [VolatileThreadInfo] = []
|
||||||
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||||
var openGroup: convo_info_volatile_open = convo_info_volatile_open()
|
var community: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
|
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
|
||||||
|
|
||||||
while !convo_info_volatile_iterator_done(convoIterator) {
|
while !convo_info_volatile_iterator_done(convoIterator) {
|
||||||
|
@ -43,23 +43,23 @@ internal extension SessionUtil {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if convo_info_volatile_it_is_open(convoIterator, &openGroup) {
|
else if convo_info_volatile_it_is_community(convoIterator, &community) {
|
||||||
let server: String = String(cString: withUnsafeBytes(of: openGroup.base_url) { [UInt8]($0) }
|
let server: String = String(cString: withUnsafeBytes(of: community.base_url) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
)
|
)
|
||||||
let roomToken: String = String(cString: withUnsafeBytes(of: openGroup.room) { [UInt8]($0) }
|
let roomToken: String = String(cString: withUnsafeBytes(of: community.room) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
)
|
)
|
||||||
let publicKey: String = withUnsafePointer(to: openGroup.pubkey, { pubkeyBytes in
|
let publicKey: String = withUnsafePointer(to: community.pubkey, { pubkeyBytes in
|
||||||
Data(bytes: pubkeyBytes, count: 32).toHexString()
|
Data(bytes: pubkeyBytes, count: 32).toHexString()
|
||||||
})
|
})
|
||||||
|
|
||||||
volatileThreadInfo.append(
|
volatileThreadInfo.append(
|
||||||
VolatileThreadInfo(
|
VolatileThreadInfo(
|
||||||
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
||||||
variant: .openGroup,
|
variant: .community,
|
||||||
openGroupUrlInfo: VolatileThreadInfo.OpenGroupUrlInfo(
|
openGroupUrlInfo: VolatileThreadInfo.OpenGroupUrlInfo(
|
||||||
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
|
||||||
server: server,
|
server: server,
|
||||||
|
@ -67,14 +67,14 @@ internal extension SessionUtil {
|
||||||
publicKey: publicKey
|
publicKey: publicKey
|
||||||
),
|
),
|
||||||
changes: [
|
changes: [
|
||||||
.markedAsUnread(openGroup.unread),
|
.markedAsUnread(community.unread),
|
||||||
.lastReadTimestampMs(openGroup.last_read)
|
.lastReadTimestampMs(community.last_read)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if convo_info_volatile_it_is_legacy_closed(convoIterator, &legacyClosedGroup) {
|
else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) {
|
||||||
let groupId: String = String(cString: withUnsafeBytes(of: legacyClosedGroup.group_id) { [UInt8]($0) }
|
let groupId: String = String(cString: withUnsafeBytes(of: legacyGroup.group_id) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
)
|
)
|
||||||
|
@ -82,10 +82,10 @@ internal extension SessionUtil {
|
||||||
volatileThreadInfo.append(
|
volatileThreadInfo.append(
|
||||||
VolatileThreadInfo(
|
VolatileThreadInfo(
|
||||||
threadId: groupId,
|
threadId: groupId,
|
||||||
variant: .legacyClosedGroup,
|
variant: .legacyGroup,
|
||||||
changes: [
|
changes: [
|
||||||
.markedAsUnread(legacyClosedGroup.unread),
|
.markedAsUnread(legacyGroup.unread),
|
||||||
.lastReadTimestampMs(legacyClosedGroup.last_read)
|
.lastReadTimestampMs(legacyGroup.last_read)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -183,11 +183,13 @@ internal extension SessionUtil {
|
||||||
convoInfoVolatileChanges: [VolatileThreadInfo],
|
convoInfoVolatileChanges: [VolatileThreadInfo],
|
||||||
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>
|
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>
|
||||||
) throws -> ConfResult {
|
) throws -> ConfResult {
|
||||||
|
guard atomicConf.wrappedValue != nil else { throw SessionUtilError.nilConfigObject }
|
||||||
|
|
||||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
||||||
// blocking access in it's `mutate` closure
|
// blocking access in it's `mutate` closure
|
||||||
return atomicConf.mutate { conf in
|
return atomicConf.mutate { conf in
|
||||||
convoInfoVolatileChanges.forEach { threadInfo in
|
convoInfoVolatileChanges.forEach { threadInfo in
|
||||||
var cThreadId: [CChar] = threadInfo.cThreadId
|
var cThreadId: [CChar] = threadInfo.threadId.cArray
|
||||||
|
|
||||||
switch threadInfo.variant {
|
switch threadInfo.variant {
|
||||||
case .contact:
|
case .contact:
|
||||||
|
@ -209,10 +211,10 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
convo_info_volatile_set_1to1(conf, &oneToOne)
|
convo_info_volatile_set_1to1(conf, &oneToOne)
|
||||||
|
|
||||||
case .legacyClosedGroup:
|
case .legacyGroup:
|
||||||
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
|
|
||||||
guard convo_info_volatile_get_or_construct_legacy_closed(conf, &legacyClosedGroup, &cThreadId) else {
|
guard convo_info_volatile_get_or_construct_legacy_group(conf, &legacyGroup, &cThreadId) else {
|
||||||
SNLog("Unable to create legacy group conversation when updating last read timestamp")
|
SNLog("Unable to create legacy group conversation when updating last read timestamp")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -220,27 +222,27 @@ internal extension SessionUtil {
|
||||||
threadInfo.changes.forEach { change in
|
threadInfo.changes.forEach { change in
|
||||||
switch change {
|
switch change {
|
||||||
case .lastReadTimestampMs(let lastReadMs):
|
case .lastReadTimestampMs(let lastReadMs):
|
||||||
legacyClosedGroup.last_read = lastReadMs
|
legacyGroup.last_read = lastReadMs
|
||||||
|
|
||||||
case .markedAsUnread(let unread):
|
case .markedAsUnread(let unread):
|
||||||
legacyClosedGroup.unread = unread
|
legacyGroup.unread = unread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
convo_info_volatile_set_legacy_closed(conf, &legacyClosedGroup)
|
convo_info_volatile_set_legacy_group(conf, &legacyGroup)
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
guard
|
guard
|
||||||
var cBaseUrl: [CChar] = threadInfo.cBaseUrl,
|
var cBaseUrl: [CChar] = threadInfo.openGroupUrlInfo?.server.cArray,
|
||||||
var cRoomToken: [CChar] = threadInfo.cRoomToken,
|
var cRoomToken: [CChar] = threadInfo.openGroupUrlInfo?.roomToken.cArray,
|
||||||
var cPubkey: [UInt8] = threadInfo.cPubkey
|
var cPubkey: [UInt8] = threadInfo.openGroupUrlInfo?.publicKey.bytes
|
||||||
else {
|
else {
|
||||||
SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info")
|
SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var openGroup: convo_info_volatile_open = convo_info_volatile_open()
|
var community: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
|
|
||||||
guard convo_info_volatile_get_or_construct_open(conf, &openGroup, &cBaseUrl, &cRoomToken, &cPubkey) else {
|
guard convo_info_volatile_get_or_construct_community(conf, &community, &cBaseUrl, &cRoomToken, &cPubkey) else {
|
||||||
SNLog("Unable to create legacy group conversation when updating last read timestamp")
|
SNLog("Unable to create legacy group conversation when updating last read timestamp")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -248,15 +250,15 @@ internal extension SessionUtil {
|
||||||
threadInfo.changes.forEach { change in
|
threadInfo.changes.forEach { change in
|
||||||
switch change {
|
switch change {
|
||||||
case .lastReadTimestampMs(let lastReadMs):
|
case .lastReadTimestampMs(let lastReadMs):
|
||||||
openGroup.last_read = lastReadMs
|
community.last_read = lastReadMs
|
||||||
|
|
||||||
case .markedAsUnread(let unread):
|
case .markedAsUnread(let unread):
|
||||||
openGroup.unread = unread
|
community.unread = unread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
convo_info_volatile_set_open(conf, &openGroup)
|
convo_info_volatile_set_community(conf, &community)
|
||||||
|
|
||||||
case .closedGroup: return // TODO: Need to add when the type is added to the lib
|
case .group: return // TODO: Need to add when the type is added to the lib
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +286,7 @@ internal extension SessionUtil {
|
||||||
VolatileThreadInfo(
|
VolatileThreadInfo(
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
variant: thread.variant,
|
variant: thread.variant,
|
||||||
openGroupUrlInfo: (thread.variant != .openGroup ? nil :
|
openGroupUrlInfo: (thread.variant != .community ? nil :
|
||||||
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: thread.id)
|
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: thread.id)
|
||||||
),
|
),
|
||||||
changes: [.markedAsUnread(thread.markedAsUnread ?? false)]
|
changes: [.markedAsUnread(thread.markedAsUnread ?? false)]
|
||||||
|
@ -340,7 +342,7 @@ internal extension SessionUtil {
|
||||||
let change: VolatileThreadInfo = VolatileThreadInfo(
|
let change: VolatileThreadInfo = VolatileThreadInfo(
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
variant: threadVariant,
|
variant: threadVariant,
|
||||||
openGroupUrlInfo: (threadVariant != .openGroup ? nil :
|
openGroupUrlInfo: (threadVariant != .community ? nil :
|
||||||
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: threadId)
|
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: threadId)
|
||||||
),
|
),
|
||||||
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
|
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
|
||||||
|
@ -394,47 +396,36 @@ internal extension SessionUtil {
|
||||||
return atomicConf.mutate { conf in
|
return atomicConf.mutate { conf in
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact:
|
case .contact:
|
||||||
var cThreadId: [CChar] = threadId
|
var cThreadId: [CChar] = threadId.cArray
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||||
guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else { return false }
|
guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else { return false }
|
||||||
|
|
||||||
return (oneToOne.last_read > timestampMs)
|
return (oneToOne.last_read > timestampMs)
|
||||||
|
|
||||||
case .legacyClosedGroup:
|
case .legacyGroup:
|
||||||
var cThreadId: [CChar] = threadId
|
var cThreadId: [CChar] = threadId.cArray
|
||||||
.bytes
|
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
|
||||||
|
|
||||||
guard convo_info_volatile_get_legacy_closed(conf, &legacyClosedGroup, &cThreadId) else {
|
guard convo_info_volatile_get_legacy_group(conf, &legacyGroup, &cThreadId) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return (legacyClosedGroup.last_read > timestampMs)
|
return (legacyGroup.last_read > timestampMs)
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
guard let openGroup: OpenGroup = openGroup else { return false }
|
guard let openGroup: OpenGroup = openGroup else { return false }
|
||||||
|
|
||||||
var cBaseUrl: [CChar] = openGroup.server
|
var cBaseUrl: [CChar] = openGroup.server.cArray
|
||||||
.bytes
|
var cRoomToken: [CChar] = openGroup.roomToken.cArray
|
||||||
.map { CChar(bitPattern: $0) }
|
var convoCommunity: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
var cRoomToken: [CChar] = openGroup.roomToken
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
var cPubKey: [CChar] = openGroup.publicKey
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
var convoOpenGroup: convo_info_volatile_open = convo_info_volatile_open()
|
|
||||||
|
|
||||||
guard convo_info_volatile_get_open(conf, &convoOpenGroup, &cBaseUrl, &cRoomToken, &cPubKey) else {
|
guard convo_info_volatile_get_community(conf, &convoCommunity, &cBaseUrl, &cRoomToken) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return (convoOpenGroup.last_read > timestampMs)
|
return (convoCommunity.last_read > timestampMs)
|
||||||
|
|
||||||
case .closedGroup: return false // TODO: Need to add when the type is added to the lib
|
case .group: return false // TODO: Need to add when the type is added to the lib
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,28 +457,9 @@ public extension SessionUtil {
|
||||||
|
|
||||||
let threadId: String
|
let threadId: String
|
||||||
let variant: SessionThread.Variant
|
let variant: SessionThread.Variant
|
||||||
private let openGroupUrlInfo: OpenGroupUrlInfo?
|
fileprivate let openGroupUrlInfo: OpenGroupUrlInfo?
|
||||||
let changes: [Change]
|
let changes: [Change]
|
||||||
|
|
||||||
var cThreadId: [CChar] {
|
|
||||||
threadId.bytes.map { CChar(bitPattern: $0) }
|
|
||||||
}
|
|
||||||
var cBaseUrl: [CChar]? {
|
|
||||||
(openGroupUrlInfo?.server).map {
|
|
||||||
$0.bytes.map { CChar(bitPattern: $0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var cRoomToken: [CChar]? {
|
|
||||||
(openGroupUrlInfo?.roomToken).map {
|
|
||||||
$0.bytes.map { CChar(bitPattern: $0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var cPubkey: [UInt8]? {
|
|
||||||
(openGroupUrlInfo?.publicKey).map {
|
|
||||||
Data(hex: $0).bytes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
threadId: String,
|
threadId: String,
|
||||||
variant: SessionThread.Variant,
|
variant: SessionThread.Variant,
|
||||||
|
|
|
@ -31,23 +31,18 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
let profileName: String = String(cString: profileNamePtr)
|
let profileName: String = String(cString: profileNamePtr)
|
||||||
let profilePic: user_profile_pic = user_profile_get_pic(conf)
|
let profilePic: user_profile_pic = user_profile_get_pic(conf)
|
||||||
var profilePictureUrl: String? = nil
|
let profilePictureUrl: String? = String(libSessionVal: profilePic.url, nullIfEmpty: true)
|
||||||
var profilePictureKey: Data? = nil
|
|
||||||
|
|
||||||
// Make sure the url and key exist before reading the memory
|
|
||||||
if
|
|
||||||
profilePic.keylen > 0,
|
|
||||||
let profilePictureUrlPtr: UnsafePointer<CChar> = profilePic.url,
|
|
||||||
let profilePictureKeyPtr: UnsafePointer<UInt8> = profilePic.key
|
|
||||||
{
|
|
||||||
profilePictureUrl = String(cString: profilePictureUrlPtr)
|
|
||||||
profilePictureKey = Data(bytes: profilePictureKeyPtr, count: profilePic.keylen)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Make sure the url and key exists before reading the memory
|
||||||
return (
|
return (
|
||||||
profileName: profileName,
|
profileName: profileName,
|
||||||
profilePictureUrl: profilePictureUrl,
|
profilePictureUrl: profilePictureUrl,
|
||||||
profilePictureKey: profilePictureKey
|
profilePictureKey: (profilePictureUrl == nil ? nil :
|
||||||
|
Data(
|
||||||
|
libSessionVal: profilePic.url,
|
||||||
|
count: ProfileManager.avatarAES256KeyByteLength
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,35 +101,14 @@ internal extension SessionUtil {
|
||||||
// blocking access in it's `mutate` closure
|
// blocking access in it's `mutate` closure
|
||||||
return atomicConf.mutate { conf in
|
return atomicConf.mutate { conf in
|
||||||
// Update the name
|
// Update the name
|
||||||
var updatedName: [CChar] = profile.name
|
var updatedName: [CChar] = profile.name.cArray
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
user_profile_set_name(conf, &updatedName)
|
user_profile_set_name(conf, &updatedName)
|
||||||
|
|
||||||
// Either assign the updated profile pic, or sent a blank profile pic (to remove the current one)
|
// Either assign the updated profile pic, or sent a blank profile pic (to remove the current one)
|
||||||
let profilePic: user_profile_pic? = {
|
var profilePic: user_profile_pic = user_profile_pic()
|
||||||
guard
|
profilePic.url = profile.profilePictureUrl.toLibSession()
|
||||||
let profilePictureUrl: String = profile.profilePictureUrl,
|
profilePic.key = profile.profileEncryptionKey.toLibSession()
|
||||||
let profileEncryptionKey: Data = profile.profileEncryptionKey
|
user_profile_set_pic(conf, profilePic)
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
let updatedUrl: [CChar] = profilePictureUrl
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let updatedKey: [UInt8] = profileEncryptionKey
|
|
||||||
.bytes
|
|
||||||
|
|
||||||
return updatedUrl.withUnsafeBufferPointer { urlPtr in
|
|
||||||
updatedKey.withUnsafeBufferPointer { keyPtr in
|
|
||||||
user_profile_pic(
|
|
||||||
url: urlPtr.baseAddress,
|
|
||||||
key: keyPtr.baseAddress,
|
|
||||||
keylen: updatedKey.count
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
user_profile_set_pic(conf, (profilePic ?? user_profile_pic()))
|
|
||||||
|
|
||||||
return ConfResult(
|
return ConfResult(
|
||||||
needsPush: config_needs_push(conf),
|
needsPush: config_needs_push(conf),
|
||||||
|
|
|
@ -55,6 +55,8 @@ public enum SessionUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var libSessionVersion: String { String(cString: LIBSESSION_UTIL_VERSION_STR) }
|
||||||
|
|
||||||
// MARK: - Loading
|
// MARK: - Loading
|
||||||
|
|
||||||
public static func loadState(
|
public static func loadState(
|
||||||
|
@ -133,6 +135,9 @@ public enum SessionUtil {
|
||||||
|
|
||||||
case .convoInfoVolatile:
|
case .convoInfoVolatile:
|
||||||
return convo_info_volatile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
|
return convo_info_volatile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
|
||||||
|
|
||||||
|
case .userGroups:
|
||||||
|
return user_groups_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -208,11 +213,10 @@ public enum SessionUtil {
|
||||||
|
|
||||||
// MARK: - Pushes
|
// MARK: - Pushes
|
||||||
|
|
||||||
public static func pendingChanges(
|
public static func pendingChanges(_ db: Database) throws -> [OutgoingConfResult] {
|
||||||
_ db: Database,
|
guard Identity.userExists(db) else { throw SessionUtilError.userDoesNotExist }
|
||||||
userPublicKey: String,
|
|
||||||
ed25519SecretKey: [UInt8]
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
) throws -> [OutgoingConfResult] {
|
|
||||||
let existingDumpInfo: Set<DumpInfo> = try ConfigDump
|
let existingDumpInfo: Set<DumpInfo> = try ConfigDump
|
||||||
.select(.variant, .publicKey, .combinedMessageHashes)
|
.select(.variant, .publicKey, .combinedMessageHashes)
|
||||||
.asRequest(of: DumpInfo.self)
|
.asRequest(of: DumpInfo.self)
|
||||||
|
@ -287,6 +291,20 @@ public enum SessionUtil {
|
||||||
return config_needs_dump(atomicConf.wrappedValue)
|
return config_needs_dump(atomicConf.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func configHashes(for publicKey: String) -> [String] {
|
||||||
|
return Storage.shared
|
||||||
|
.read { db in
|
||||||
|
try ConfigDump
|
||||||
|
.filter(ConfigDump.Columns.publicKey == publicKey)
|
||||||
|
.select(.combinedMessageHashes)
|
||||||
|
.asRequest(of: String.self)
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
.defaulting(to: [])
|
||||||
|
.compactMap { ConfigDump.messageHashes(from: $0) }
|
||||||
|
.flatMap { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Receiving
|
// MARK: - Receiving
|
||||||
|
|
||||||
public static func handleConfigMessages(
|
public static func handleConfigMessages(
|
||||||
|
@ -373,7 +391,7 @@ public enum SessionUtil {
|
||||||
mergeResult: mergeResult.result
|
mergeResult: mergeResult.result
|
||||||
)
|
)
|
||||||
|
|
||||||
case .groups:
|
case .userGroups:
|
||||||
return try SessionUtil.handleGroupsUpdate(
|
return try SessionUtil.handleGroupsUpdate(
|
||||||
db,
|
db,
|
||||||
in: atomicConf,
|
in: atomicConf,
|
||||||
|
|
|
@ -5,4 +5,5 @@ import Foundation
|
||||||
public enum SessionUtilError: Error {
|
public enum SessionUtilError: Error {
|
||||||
case unableToCreateConfigObject
|
case unableToCreateConfigObject
|
||||||
case nilConfigObject
|
case nilConfigObject
|
||||||
|
case userDoesNotExist
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - String
|
||||||
|
|
||||||
|
public extension String {
|
||||||
|
var cArray: [CChar] { [UInt8](self.utf8).map { CChar(bitPattern: $0) } }
|
||||||
|
|
||||||
|
/// Initialize with an optional pointer and a specific length
|
||||||
|
init?(pointer: UnsafeRawPointer?, length: Int, encoding: String.Encoding = .utf8) {
|
||||||
|
guard
|
||||||
|
let pointer: UnsafeRawPointer = pointer,
|
||||||
|
let result: String = String(data: Data(bytes: pointer, count: length), encoding: encoding)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
self = result
|
||||||
|
}
|
||||||
|
|
||||||
|
init?<T>(
|
||||||
|
libSessionVal: T,
|
||||||
|
nullTerminated: Bool = true,
|
||||||
|
nullIfEmpty: Bool = false
|
||||||
|
) {
|
||||||
|
let result: String = {
|
||||||
|
guard !nullTerminated else {
|
||||||
|
return String(cString: withUnsafeBytes(of: libSessionVal) { [UInt8]($0) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(
|
||||||
|
data: Data(libSessionVal: libSessionVal, count: MemoryLayout<T>.size),
|
||||||
|
encoding: .utf8
|
||||||
|
)
|
||||||
|
.defaulting(to: "")
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard !nullIfEmpty || !result.isEmpty else { return nil }
|
||||||
|
|
||||||
|
self = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLibSession<T>() -> T {
|
||||||
|
let targetSize: Int = MemoryLayout<T>.stride
|
||||||
|
let result: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(
|
||||||
|
byteCount: targetSize,
|
||||||
|
alignment: MemoryLayout<T>.alignment
|
||||||
|
)
|
||||||
|
self.utf8CString.withUnsafeBytes { result.copyMemory(from: $0.baseAddress!, byteCount: $0.count) }
|
||||||
|
|
||||||
|
return result.withMemoryRebound(to: T.self, capacity: targetSize) { $0.pointee }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Optional<String> {
|
||||||
|
func toLibSession<T>() -> T {
|
||||||
|
switch self {
|
||||||
|
case .some(let value): return value.toLibSession()
|
||||||
|
case .none: return "".toLibSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
|
public extension Data {
|
||||||
|
var cArray: [UInt8] { [UInt8](self) }
|
||||||
|
|
||||||
|
init<T>(libSessionVal: T, count: Int) {
|
||||||
|
self = Data(
|
||||||
|
bytes: Swift.withUnsafeBytes(of: libSessionVal) { [UInt8]($0) },
|
||||||
|
count: count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLibSession<T>() -> T {
|
||||||
|
let targetSize: Int = MemoryLayout<T>.stride
|
||||||
|
let result: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(
|
||||||
|
byteCount: targetSize,
|
||||||
|
alignment: MemoryLayout<T>.alignment
|
||||||
|
)
|
||||||
|
self.withUnsafeBytes { result.copyMemory(from: $0.baseAddress!, byteCount: $0.count) }
|
||||||
|
|
||||||
|
return result.withMemoryRebound(to: T.self, capacity: targetSize) { $0.pointee }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Optional<Data> {
|
||||||
|
func toLibSession<T>() -> T {
|
||||||
|
switch self {
|
||||||
|
case .some(let value): return value.toLibSession()
|
||||||
|
case .none: return Data().toLibSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Array
|
||||||
|
|
||||||
|
public extension Array where Element == CChar {
|
||||||
|
func nullTerminated() -> [Element] {
|
||||||
|
guard self.last != CChar(0) else { return self }
|
||||||
|
|
||||||
|
return self.appending(CChar(0))
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,18 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>AvailableLibraries</key>
|
<key>AvailableLibraries</key>
|
||||||
<array>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libsession-util.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>LibraryIdentifier</key>
|
<key>LibraryIdentifier</key>
|
||||||
<string>ios-arm64_x86_64-simulator</string>
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
|
@ -19,18 +31,6 @@
|
||||||
<key>SupportedPlatformVariant</key>
|
<key>SupportedPlatformVariant</key>
|
||||||
<string>simulator</string>
|
<string>simulator</string>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>LibraryIdentifier</key>
|
|
||||||
<string>ios-arm64</string>
|
|
||||||
<key>LibraryPath</key>
|
|
||||||
<string>libsession-util.a</string>
|
|
||||||
<key>SupportedArchitectures</key>
|
|
||||||
<array>
|
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>SupportedPlatform</key>
|
|
||||||
<string>ios</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XFWK</string>
|
<string>XFWK</string>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,8 +1,11 @@
|
||||||
module SessionUtil {
|
module SessionUtil {
|
||||||
module capi {
|
module capi {
|
||||||
|
header "session/version.h"
|
||||||
header "session/export.h"
|
header "session/export.h"
|
||||||
header "session/config.h"
|
header "session/config.h"
|
||||||
header "session/config/error.h"
|
header "session/config/error.h"
|
||||||
|
header "session/config/expiring.h"
|
||||||
|
header "session/config/user_groups.h"
|
||||||
header "session/config/convo_info_volatile.h"
|
header "session/config/convo_info_volatile.h"
|
||||||
header "session/config/user_profile.h"
|
header "session/config/user_profile.h"
|
||||||
header "session/config/util.h"
|
header "session/config/util.h"
|
||||||
|
|
|
@ -225,7 +225,9 @@ class ConfigMessage {
|
||||||
|
|
||||||
// Constructor tag
|
// Constructor tag
|
||||||
struct increment_seqno_t {};
|
struct increment_seqno_t {};
|
||||||
|
struct retain_seqno_t {};
|
||||||
inline constexpr increment_seqno_t increment_seqno{};
|
inline constexpr increment_seqno_t increment_seqno{};
|
||||||
|
inline constexpr retain_seqno_t retain_seqno{};
|
||||||
|
|
||||||
class MutableConfigMessage : public ConfigMessage {
|
class MutableConfigMessage : public ConfigMessage {
|
||||||
protected:
|
protected:
|
||||||
|
@ -292,7 +294,14 @@ class MutableConfigMessage : public ConfigMessage {
|
||||||
|
|
||||||
/// Constructor that does the same thing as the `m.increment()` factory method. The second
|
/// Constructor that does the same thing as the `m.increment()` factory method. The second
|
||||||
/// value should be the literal `increment_seqno` value (to select this constructor).
|
/// value should be the literal `increment_seqno` value (to select this constructor).
|
||||||
explicit MutableConfigMessage(const ConfigMessage& m, increment_seqno_t);
|
explicit MutableConfigMessage(const ConfigMessage& m, const increment_seqno_t&);
|
||||||
|
|
||||||
|
/// Constructor that moves a immutable message into a mutable one, retaining the current seqno.
|
||||||
|
/// This is typically used in situations where the ConfigMessage has had some implicit seqno
|
||||||
|
/// increment already (e.g. from merging) and we want it to become mutable without incrementing
|
||||||
|
/// the seqno again. The second value should be the literal `retain_seqno` value (to select
|
||||||
|
/// this constructor).
|
||||||
|
explicit MutableConfigMessage(ConfigMessage&& m, const retain_seqno_t&);
|
||||||
|
|
||||||
using ConfigMessage::data;
|
using ConfigMessage::data;
|
||||||
/// Returns a mutable reference to the underlying config data.
|
/// Returns a mutable reference to the underlying config data.
|
||||||
|
|
|
@ -90,6 +90,7 @@ class ConfigBase {
|
||||||
// already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter.
|
// already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter.
|
||||||
MutableConfigMessage& dirty();
|
MutableConfigMessage& dirty();
|
||||||
|
|
||||||
|
public:
|
||||||
// class for proxying subfield access; this class should never be stored but only used
|
// class for proxying subfield access; this class should never be stored but only used
|
||||||
// ephemerally (most of its methods are rvalue-qualified). This lets constructs such as
|
// ephemerally (most of its methods are rvalue-qualified). This lets constructs such as
|
||||||
// foo["abc"]["def"]["ghi"] = 12;
|
// foo["abc"]["def"]["ghi"] = 12;
|
||||||
|
@ -271,7 +272,7 @@ class ConfigBase {
|
||||||
std::string string_or(std::string fallback) const {
|
std::string string_or(std::string fallback) const {
|
||||||
if (auto* s = string())
|
if (auto* s = string())
|
||||||
return *s;
|
return *s;
|
||||||
return std::move(fallback);
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a const pointer to the integer if one exists at the given location, nullptr
|
/// Returns a const pointer to the integer if one exists at the given location, nullptr
|
||||||
|
@ -297,7 +298,7 @@ class ConfigBase {
|
||||||
/// Replaces the current value with the given string. This also auto-vivifies any
|
/// Replaces the current value with the given string. This also auto-vivifies any
|
||||||
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
|
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
|
||||||
/// they currently exist along the path.
|
/// they currently exist along the path.
|
||||||
void operator=(std::string value) { assign_if_changed(std::move(value)); }
|
void operator=(std::string&& value) { assign_if_changed(std::move(value)); }
|
||||||
/// Same as above, but takes a string_view for convenience (this makes a copy).
|
/// Same as above, but takes a string_view for convenience (this makes a copy).
|
||||||
void operator=(std::string_view value) { *this = std::string{value}; }
|
void operator=(std::string_view value) { *this = std::string{value}; }
|
||||||
/// Same as above, but takes a ustring_view
|
/// Same as above, but takes a ustring_view
|
||||||
|
@ -391,6 +392,7 @@ class ConfigBase {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected:
|
||||||
// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
|
// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
|
||||||
// the object. The base implementation does nothing. The counterpart to this,
|
// the object. The base implementation does nothing. The counterpart to this,
|
||||||
// `load_extra_data()`, is called when loading from a dump that has extra data; a subclass
|
// `load_extra_data()`, is called when loading from a dump that has extra data; a subclass
|
||||||
|
@ -429,6 +431,11 @@ class ConfigBase {
|
||||||
/// to use. This is rarely needed externally; it is public merely for testing purposes.
|
/// to use. This is rarely needed externally; it is public merely for testing purposes.
|
||||||
virtual const char* encryption_domain() const = 0;
|
virtual const char* encryption_domain() const = 0;
|
||||||
|
|
||||||
|
/// The zstd compression level to use for this type. Subclasses can override this if they have
|
||||||
|
/// some particular special compression level, or to disable compression entirely (by returning
|
||||||
|
/// std::nullopt). The default is zstd level 1.
|
||||||
|
virtual std::optional<int> compression_level() const { return 1; }
|
||||||
|
|
||||||
// How many config lags should be used for this object; default to 5. Implementing subclasses
|
// How many config lags should be used for this object; default to 5. Implementing subclasses
|
||||||
// can override to return a different constant if desired. More lags require more "diff"
|
// can override to return a different constant if desired. More lags require more "diff"
|
||||||
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
|
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
|
||||||
|
@ -463,13 +470,16 @@ class ConfigBase {
|
||||||
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
|
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
|
||||||
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
|
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
|
||||||
// storage of the most recent serialized push data.
|
// storage of the most recent serialized push data.
|
||||||
bool needs_push() const;
|
virtual bool needs_push() const;
|
||||||
|
|
||||||
// Returns the data messages to push to the server along with the seqno value of the data. If
|
// Returns the data messages to push to the server along with the seqno value of the data. If
|
||||||
// the config is currently dirty (i.e. has previously unsent modifications) then this marks it
|
// the config is currently dirty (i.e. has previously unsent modifications) then this marks it
|
||||||
// as awaiting-confirmation instead of dirty so that any future change immediately increments
|
// as awaiting-confirmation instead of dirty so that any future change immediately increments
|
||||||
// the seqno.
|
// the seqno.
|
||||||
std::pair<ustring, seqno_t> push();
|
//
|
||||||
|
// Subclasses that need to perform pre-push tasks (such as pruning stale data) can override this
|
||||||
|
// to prune and then call the base method to perform the actual push generation.
|
||||||
|
virtual std::pair<ustring, seqno_t> push();
|
||||||
|
|
||||||
// Should be called after the push is confirmed stored on the storage server swarm to let the
|
// Should be called after the push is confirmed stored on the storage server swarm to let the
|
||||||
// object know the data is stored. (Once this is called `needs_push` will start returning false
|
// object know the data is stored. (Once this is called `needs_push` will start returning false
|
||||||
|
|
|
@ -0,0 +1,239 @@
|
||||||
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <session/config.hpp>
|
||||||
|
#include <session/types.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace session::config {
|
||||||
|
|
||||||
|
/// Base class for types representing a community; this base type handles the url/room/pubkey that
|
||||||
|
/// such a type need. Generally a class inherits from this to extend with the local
|
||||||
|
/// community-related values.
|
||||||
|
struct community {
|
||||||
|
|
||||||
|
// 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX')
|
||||||
|
static constexpr size_t URL_MAX_LENGTH = 267;
|
||||||
|
static constexpr size_t ROOM_MAX_LENGTH = 64;
|
||||||
|
|
||||||
|
community() = default;
|
||||||
|
|
||||||
|
// Constructs an empty community struct from url, room, and pubkey. `base_url` will be
|
||||||
|
// normalized if not already. pubkey is 32 bytes.
|
||||||
|
community(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||||
|
|
||||||
|
// Same as above, but takes pubkey as an encoded (hex or base32z or base64) string.
|
||||||
|
community(std::string_view base_url, std::string_view room, std::string_view pubkey_encoded);
|
||||||
|
|
||||||
|
// Takes a combined room URL (e.g. https://whatever.com/r/Room?public_key=01234....), either
|
||||||
|
// new style (with /r/) or old style (without /r/). Note that the URL gets canonicalized so
|
||||||
|
// the resulting `base_url()` and `room()` values may not be exactly equal to what is given.
|
||||||
|
//
|
||||||
|
// See also `parse_full_url` which does the same thing but returns it in pieces rather than
|
||||||
|
// constructing a new `community` object.
|
||||||
|
explicit community(std::string_view full_url);
|
||||||
|
|
||||||
|
// Replaces the baseurl/room/pubkey of this object from a URL. This parses the URL, then stores
|
||||||
|
// the values as if passed to set_base_url/set_room/set_pubkey.
|
||||||
|
//
|
||||||
|
// The base URL will be normalized; the room name will be case-preserving (but see `set_room`
|
||||||
|
// for info on limitations on "case-preserving", particularly for volatile configs); and the
|
||||||
|
// embedded pubkey must be encoded in one of hex, base32z, or base64.
|
||||||
|
void set_full_url(std::string_view full_url);
|
||||||
|
|
||||||
|
// Replaces the base_url of this object. Note that changing the URL and then giving it to `set`
|
||||||
|
// will end up inserting a *new* record but not removing the *old* one (you need to erase first
|
||||||
|
// to do that).
|
||||||
|
void set_base_url(std::string_view new_url);
|
||||||
|
|
||||||
|
// Changes the room token. This stores (or updates) the name as given as the localized room,
|
||||||
|
// and separately stores the normalized (lower-case) token. Note that the localized name does
|
||||||
|
// not persist across a push or dump in some config contexts (such as volatile room info). If
|
||||||
|
// the new room given here changes more than just case (i.e. if the normalized room token
|
||||||
|
// changes) then a call to `set` will end up inserting a *new* record but not removing the *old*
|
||||||
|
// one (you need to erase first to do that).
|
||||||
|
void set_room(std::string_view room);
|
||||||
|
|
||||||
|
// Updates the pubkey of this community (typically this is not called directly but rather
|
||||||
|
// via `set_server` or during construction). Throws std::invalid_argument if the given
|
||||||
|
// pubkey does not look like a valid pubkey. The std::string_view version takes the pubkey
|
||||||
|
// as any of hex/base64/base32z.
|
||||||
|
//
|
||||||
|
// NOTE: the pubkey of all communities with the same URLs are stored in common, so changing
|
||||||
|
// one community pubkey (and storing) will affect all communities using the same community
|
||||||
|
// base URL.
|
||||||
|
void set_pubkey(ustring_view pubkey);
|
||||||
|
void set_pubkey(std::string_view pubkey);
|
||||||
|
|
||||||
|
// Accesses the base url (i.e. not including room or pubkey). Always lower-case/normalized.
|
||||||
|
const std::string& base_url() const { return base_url_; }
|
||||||
|
|
||||||
|
// Accesses the room token; this is case-preserving, where possible. In some contexts, however,
|
||||||
|
// such as volatile info, the case is not preserved and this will always return the normalized
|
||||||
|
// (lower-case) form rather than the preferred form.
|
||||||
|
const std::string& room() const { return localized_room_ ? *localized_room_ : room_; }
|
||||||
|
|
||||||
|
// Accesses the normalized room token, i.e. always lower-case.
|
||||||
|
const std::string& room_norm() const { return room_; }
|
||||||
|
|
||||||
|
const ustring& pubkey() const { return pubkey_; } // Accesses the server pubkey (32 bytes).
|
||||||
|
std::string pubkey_hex() const; // Accesses the server pubkey as hex (64 hex digits).
|
||||||
|
std::string pubkey_b32z() const; // Accesses the server pubkey as base32z (52 alphanumeric
|
||||||
|
// digits)
|
||||||
|
std::string pubkey_b64() const; // Accesses the server pubkey as unpadded base64 (43 from
|
||||||
|
// alphanumeric, '+', and '/').
|
||||||
|
|
||||||
|
// Takes a base URL as input and returns it in canonical form. This involves doing things
|
||||||
|
// like lower casing it and removing redundant ports (e.g. :80 when using http://). Throws
|
||||||
|
// std::invalid_argument if given an invalid base URL.
|
||||||
|
static std::string canonical_url(std::string_view url);
|
||||||
|
|
||||||
|
// Takes a room token and returns it in canonical form (i.e. lower-cased). Throws
|
||||||
|
// std::invalid_argument if given an invalid room token (e.g. too long, or containing token
|
||||||
|
// other than a-z, 0-9, -, _).
|
||||||
|
static std::string canonical_room(std::string_view room);
|
||||||
|
|
||||||
|
// Same as above, but modifies the argument in-place instead of returning a modified
|
||||||
|
// copy.
|
||||||
|
static void canonicalize_url(std::string& url);
|
||||||
|
static void canonicalize_room(std::string& room);
|
||||||
|
|
||||||
|
// Takes a full room URL, splits it up into canonical url (see above), room, and server
|
||||||
|
// pubkey. We take both the deprecated form (e.g.
|
||||||
|
// https://example.com/SomeRoom?public_key=...) and new form
|
||||||
|
// (https://example.com/r/SomeRoom?public_key=...). The public_key is typically specified
|
||||||
|
// in hex (64 digits), but we also accept base64 (43 chars or 44 with padding) and base32z
|
||||||
|
// (52 chars) encodings (for slightly shorter URLs).
|
||||||
|
//
|
||||||
|
// The returned URL is normalized (lower-cased, and cleaned up).
|
||||||
|
//
|
||||||
|
// The returned room name is *not* normalized, that is, it preserve case.
|
||||||
|
//
|
||||||
|
// Throw std::invalid_argument if anything in the URL is unparseable or invalid.
|
||||||
|
static std::tuple<std::string, std::string, ustring> parse_full_url(std::string_view full_url);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// The canonical base url and room (i.e. lower-cased, URL cleaned up):
|
||||||
|
std::string base_url_, room_;
|
||||||
|
// The localized token of this room, that is, with case preserved (so `room_` could be
|
||||||
|
// `someroom` and this could `SomeRoom`). Omitted if not available.
|
||||||
|
std::optional<std::string> localized_room_;
|
||||||
|
// server pubkey
|
||||||
|
ustring pubkey_;
|
||||||
|
|
||||||
|
// Construction without a pubkey for when pubkey isn't known yet but will be set shortly
|
||||||
|
// after constructing (or when isn't needed, such as when deleting).
|
||||||
|
community(std::string_view base_url, std::string_view room);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct comm_iterator_helper {
|
||||||
|
|
||||||
|
comm_iterator_helper(dict::const_iterator it_server, dict::const_iterator end_server) :
|
||||||
|
it_server{std::move(it_server)}, end_server{std::move(end_server)} {}
|
||||||
|
|
||||||
|
std::optional<dict::const_iterator> it_server, end_server, it_room, end_room;
|
||||||
|
|
||||||
|
bool operator==(const comm_iterator_helper& other) const {
|
||||||
|
return it_server == other.it_server && it_room == other.it_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void next_server() {
|
||||||
|
++*it_server;
|
||||||
|
it_room.reset();
|
||||||
|
end_room.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool done() const { return !it_server || *it_server == *end_server; }
|
||||||
|
|
||||||
|
template <typename Comm, typename Any>
|
||||||
|
bool load(std::shared_ptr<Any>& val) {
|
||||||
|
while (it_server) {
|
||||||
|
if (*it_server == *end_server) {
|
||||||
|
it_server.reset();
|
||||||
|
end_server.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& [base_url, server_info] = **it_server;
|
||||||
|
auto* server_info_dict = std::get_if<dict>(&server_info);
|
||||||
|
if (!server_info_dict) {
|
||||||
|
next_server();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string* pubkey_raw = nullptr;
|
||||||
|
if (auto pubkey_it = server_info_dict->find("#"); pubkey_it != server_info_dict->end())
|
||||||
|
if (auto* pk_sc = std::get_if<scalar>(&pubkey_it->second))
|
||||||
|
pubkey_raw = std::get_if<std::string>(pk_sc);
|
||||||
|
|
||||||
|
if (!pubkey_raw) {
|
||||||
|
next_server();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ustring_view pubkey{
|
||||||
|
reinterpret_cast<const unsigned char*>(pubkey_raw->data()), pubkey_raw->size()};
|
||||||
|
|
||||||
|
if (!it_room) {
|
||||||
|
if (auto rit = server_info_dict->find("R");
|
||||||
|
rit != server_info_dict->end() && std::holds_alternative<dict>(rit->second)) {
|
||||||
|
auto& rooms_dict = std::get<dict>(rit->second);
|
||||||
|
it_room = rooms_dict.begin();
|
||||||
|
end_room = rooms_dict.end();
|
||||||
|
} else {
|
||||||
|
next_server();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (it_room) {
|
||||||
|
if (*it_room == *end_room) {
|
||||||
|
it_room.reset();
|
||||||
|
end_room.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& [room, data] = **it_room;
|
||||||
|
auto* data_dict = std::get_if<dict>(&data);
|
||||||
|
if (!data_dict) {
|
||||||
|
++*it_room;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
val = std::make_shared<Any>(Comm{});
|
||||||
|
auto& og = std::get<Comm>(*val);
|
||||||
|
try {
|
||||||
|
og.set_base_url(base_url);
|
||||||
|
og.set_room(room); // Will be replaced with "n" in the `.load` below
|
||||||
|
og.set_pubkey(pubkey);
|
||||||
|
og.load(*data_dict);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
++*it_room;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
++*it_server;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool advance() {
|
||||||
|
if (it_room) {
|
||||||
|
++*it_room;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (it_server) {
|
||||||
|
++*it_server;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace session::config
|
|
@ -5,20 +5,27 @@ extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "base.h"
|
#include "base.h"
|
||||||
|
#include "expiring.h"
|
||||||
#include "profile_pic.h"
|
#include "profile_pic.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
typedef struct contacts_contact {
|
typedef struct contacts_contact {
|
||||||
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||||
|
|
||||||
// These can be NULL. When setting, either NULL or empty string will clear the setting.
|
// These two will be 0-length strings when unset:
|
||||||
const char* name;
|
char name[101];
|
||||||
const char* nickname;
|
char nickname[101];
|
||||||
user_profile_pic profile_pic;
|
user_profile_pic profile_pic;
|
||||||
|
|
||||||
bool approved;
|
bool approved;
|
||||||
bool approved_me;
|
bool approved_me;
|
||||||
bool blocked;
|
bool blocked;
|
||||||
|
bool hidden;
|
||||||
|
|
||||||
|
int priority;
|
||||||
|
|
||||||
|
CONVO_EXPIRATION_MODE exp_mode;
|
||||||
|
int exp_minutes;
|
||||||
|
|
||||||
} contacts_contact;
|
} contacts_contact;
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <session/config.hpp>
|
#include <session/config.hpp>
|
||||||
|
|
||||||
#include "base.hpp"
|
#include "base.hpp"
|
||||||
|
#include "expiring.hpp"
|
||||||
#include "namespaces.hpp"
|
#include "namespaces.hpp"
|
||||||
#include "profile_pic.hpp"
|
#include "profile_pic.hpp"
|
||||||
|
|
||||||
extern "C" struct contacts_contact;
|
extern "C" struct contacts_contact;
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
|
||||||
namespace session::config {
|
namespace session::config {
|
||||||
|
|
||||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||||
|
@ -18,30 +22,41 @@ namespace session::config {
|
||||||
/// c - dict of contacts; within this dict each key is the session pubkey (binary, 33 bytes) and
|
/// c - dict of contacts; within this dict each key is the session pubkey (binary, 33 bytes) and
|
||||||
/// value is a dict containing keys:
|
/// value is a dict containing keys:
|
||||||
///
|
///
|
||||||
/// ! - dummy value that is always set to an empty string. This ensures that we always have at
|
/// n - contact name (string). This is always serialized, even if empty (but empty indicates
|
||||||
/// least one key set, which is required to keep the dict value alive (empty dicts get
|
/// no name) so that we always have at least one key set (required to keep the dict value
|
||||||
/// pruned when serialied).
|
/// alive as empty dicts get pruned).
|
||||||
/// n - contact name (string)
|
|
||||||
/// N - contact nickname (string)
|
/// N - contact nickname (string)
|
||||||
/// p - profile url (string)
|
/// p - profile url (string)
|
||||||
/// q - profile decryption key (binary)
|
/// q - profile decryption key (binary)
|
||||||
/// a - 1 if approved, omitted otherwise (int)
|
/// a - 1 if approved, omitted otherwise (int)
|
||||||
/// A - 1 if remote has approved me, omitted otherwise (int)
|
/// A - 1 if remote has approved me, omitted otherwise (int)
|
||||||
/// b - 1 if contact is blocked, omitted otherwise
|
/// b - 1 if contact is blocked, omitted otherwise
|
||||||
|
/// h - 1 if the conversation with this contact is hidden, omitted if visible.
|
||||||
|
/// + - the conversation priority, for pinned messages. Omitted means not pinned; otherwise an
|
||||||
|
/// integer value >0, where a higher priority means the conversation is meant to appear
|
||||||
|
/// earlier in the pinned conversation list.
|
||||||
|
/// e - Disappearing messages expiration type. Omitted if disappearing messages are not enabled
|
||||||
|
/// for the conversation with this contact; 1 for delete-after-send, and 2 for
|
||||||
|
/// delete-after-read.
|
||||||
|
/// E - Disappearing message timer, in minutes. Omitted when `e` is omitted.
|
||||||
|
|
||||||
/// Struct containing contact info. Note that data must be copied/used immediately as the data will
|
/// Struct containing contact info.
|
||||||
/// not remain valid beyond other calls into the library. When settings things in this externally
|
|
||||||
/// (e.g. to pass into `set()`), take note that the `name` and `nickname` are string_views: that is,
|
|
||||||
/// they must reference existing string data that remains valid for the duration of the contact_info
|
|
||||||
/// instance.
|
|
||||||
struct contact_info {
|
struct contact_info {
|
||||||
|
static constexpr size_t MAX_NAME_LENGTH = 100;
|
||||||
|
|
||||||
std::string session_id; // in hex
|
std::string session_id; // in hex
|
||||||
std::optional<std::string_view> name;
|
std::string name;
|
||||||
std::optional<std::string_view> nickname;
|
std::string nickname;
|
||||||
std::optional<profile_pic> profile_picture;
|
profile_pic profile_picture;
|
||||||
bool approved = false;
|
bool approved = false;
|
||||||
bool approved_me = false;
|
bool approved_me = false;
|
||||||
bool blocked = false;
|
bool blocked = false;
|
||||||
|
bool hidden = false; // True if the conversation with this contact is not visible in the convo
|
||||||
|
// list (typically because it has been deleted).
|
||||||
|
int priority = 0; // If >0 then this message is pinned; higher values mean higher priority
|
||||||
|
// (i.e. pinned earlier in the pinned list).
|
||||||
|
expiration_mode exp_mode = expiration_mode::none; // The expiry time; none if not expiring.
|
||||||
|
std::chrono::minutes exp_timer{0}; // The expiration timer (in minutes)
|
||||||
|
|
||||||
explicit contact_info(std::string sid);
|
explicit contact_info(std::string sid);
|
||||||
|
|
||||||
|
@ -49,20 +64,13 @@ struct contact_info {
|
||||||
contact_info(const struct contacts_contact& c); // From c struct
|
contact_info(const struct contacts_contact& c); // From c struct
|
||||||
void into(contacts_contact& c) const; // Into c struct
|
void into(contacts_contact& c) const; // Into c struct
|
||||||
|
|
||||||
// Sets a name, storing the name internally in the object. This is intended for use where the
|
// Sets a name or nickname; this is exactly the same as assigning to .name/.nickname directly,
|
||||||
// source string is a temporary may not outlive the `contact_info` object: the name is first
|
// except that we throw an exception if the given name is longer than MAX_NAME_LENGTH.
|
||||||
// copied into an internal std::string, and then the name string_view references that.
|
|
||||||
void set_name(std::string name);
|
void set_name(std::string name);
|
||||||
|
|
||||||
// Same as above, but for nickname.
|
|
||||||
void set_nickname(std::string nickname);
|
void set_nickname(std::string nickname);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Contacts;
|
friend class Contacts;
|
||||||
|
|
||||||
std::string name_;
|
|
||||||
std::string nickname_;
|
|
||||||
|
|
||||||
void load(const dict& info_dict);
|
void load(const dict& info_dict);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,13 +119,20 @@ class Contacts : public ConfigBase {
|
||||||
/// contacts.set(c);
|
/// contacts.set(c);
|
||||||
void set(const contact_info& contact);
|
void set(const contact_info& contact);
|
||||||
|
|
||||||
/// Alternative to `set()` for setting individual fields.
|
/// Alternative to `set()` for setting a single field. (If setting multiple fields at once you
|
||||||
|
/// should use `set()` instead).
|
||||||
void set_name(std::string_view session_id, std::string name);
|
void set_name(std::string_view session_id, std::string name);
|
||||||
void set_nickname(std::string_view session_id, std::string nickname);
|
void set_nickname(std::string_view session_id, std::string nickname);
|
||||||
void set_profile_pic(std::string_view session_id, profile_pic pic);
|
void set_profile_pic(std::string_view session_id, profile_pic pic);
|
||||||
void set_approved(std::string_view session_id, bool approved);
|
void set_approved(std::string_view session_id, bool approved);
|
||||||
void set_approved_me(std::string_view session_id, bool approved_me);
|
void set_approved_me(std::string_view session_id, bool approved_me);
|
||||||
void set_blocked(std::string_view session_id, bool blocked);
|
void set_blocked(std::string_view session_id, bool blocked);
|
||||||
|
void set_hidden(std::string_view session_id, bool hidden);
|
||||||
|
void set_priority(std::string_view session_id, int priority);
|
||||||
|
void set_expiry(
|
||||||
|
std::string_view session_id,
|
||||||
|
expiration_mode exp_mode,
|
||||||
|
std::chrono::minutes expiration_timer = 0min);
|
||||||
|
|
||||||
/// Removes a contact, if present. Returns true if it was found and removed, false otherwise.
|
/// Removes a contact, if present. Returns true if it was found and removed, false otherwise.
|
||||||
/// Note that this removes all fields related to a contact, even fields we do not know about.
|
/// Note that this removes all fields related to a contact, even fields we do not know about.
|
||||||
|
|
|
@ -14,7 +14,7 @@ typedef struct convo_info_volatile_1to1 {
|
||||||
bool unread; // true if the conversation is explicitly marked unread
|
bool unread; // true if the conversation is explicitly marked unread
|
||||||
} convo_info_volatile_1to1;
|
} convo_info_volatile_1to1;
|
||||||
|
|
||||||
typedef struct convo_info_volatile_open {
|
typedef struct convo_info_volatile_community {
|
||||||
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
||||||
// only has port if non-default, has trailing / removed)
|
// only has port if non-default, has trailing / removed)
|
||||||
char room[65]; // null-terminated (max length 64), normalized (always lower-case)
|
char room[65]; // null-terminated (max length 64), normalized (always lower-case)
|
||||||
|
@ -22,15 +22,15 @@ typedef struct convo_info_volatile_open {
|
||||||
|
|
||||||
int64_t last_read; // ms since unix epoch
|
int64_t last_read; // ms since unix epoch
|
||||||
bool unread; // true if marked unread
|
bool unread; // true if marked unread
|
||||||
} convo_info_volatile_open;
|
} convo_info_volatile_community;
|
||||||
|
|
||||||
typedef struct convo_info_volatile_legacy_closed {
|
typedef struct convo_info_volatile_legacy_group {
|
||||||
char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID,
|
char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID,
|
||||||
// though isn't really one.
|
// though isn't really one.
|
||||||
|
|
||||||
int64_t last_read; // ms since unix epoch
|
int64_t last_read; // ms since unix epoch
|
||||||
bool unread; // true if marked unread
|
bool unread; // true if marked unread
|
||||||
} convo_info_volatile_legacy_closed;
|
} convo_info_volatile_legacy_group;
|
||||||
|
|
||||||
/// Constructs a conversations config object and sets a pointer to it in `conf`.
|
/// Constructs a conversations config object and sets a pointer to it in `conf`.
|
||||||
///
|
///
|
||||||
|
@ -78,76 +78,75 @@ bool convo_info_volatile_get_or_construct_1to1(
|
||||||
const config_object* conf, convo_info_volatile_1to1* convo, const char* session_id)
|
const config_object* conf, convo_info_volatile_1to1* convo, const char* session_id)
|
||||||
__attribute__((warn_unused_result));
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
/// open-group versions of the 1-to-1 functions:
|
/// community versions of the 1-to-1 functions:
|
||||||
///
|
///
|
||||||
/// Gets an open group convo info. `base_url` and `room` are null-terminated c strings; pubkey is
|
/// Gets a community convo info. `base_url` and `room` are null-terminated c strings; pubkey is
|
||||||
/// 32 bytes. base_url and room will always be lower-cased (if not already).
|
/// 32 bytes. base_url and room will always be lower-cased (if not already).
|
||||||
bool convo_info_volatile_get_open(
|
bool convo_info_volatile_get_community(
|
||||||
const config_object* conf,
|
const config_object* conf,
|
||||||
convo_info_volatile_open* og,
|
convo_info_volatile_community* comm,
|
||||||
const char* base_url,
|
const char* base_url,
|
||||||
const char* room,
|
const char* room) __attribute__((warn_unused_result));
|
||||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
bool convo_info_volatile_get_or_construct_community(
|
||||||
bool convo_info_volatile_get_or_construct_open(
|
|
||||||
const config_object* conf,
|
const config_object* conf,
|
||||||
convo_info_volatile_open* convo,
|
convo_info_volatile_community* convo,
|
||||||
const char* base_url,
|
const char* base_url,
|
||||||
const char* room,
|
const char* room,
|
||||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||||
|
|
||||||
/// Fills `convo` with the conversation info given a legacy closed group ID (specified as a
|
/// Fills `convo` with the conversation info given a legacy group ID (specified as a null-terminated
|
||||||
/// null-terminated hex string), if the conversation exists, and returns true. If the conversation
|
/// hex string), if the conversation exists, and returns true. If the conversation does not exist
|
||||||
/// does not exist then `convo` is left unchanged and false is returned.
|
/// then `convo` is left unchanged and false is returned.
|
||||||
bool convo_info_volatile_get_legacy_closed(
|
bool convo_info_volatile_get_legacy_group(
|
||||||
const config_object* conf, convo_info_volatile_legacy_closed* convo, const char* id)
|
const config_object* conf, convo_info_volatile_legacy_group* convo, const char* id)
|
||||||
__attribute__((warn_unused_result));
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
/// Same as the above except that when the conversation does not exist, this sets all the convo
|
/// Same as the above except that when the conversation does not exist, this sets all the convo
|
||||||
/// fields to defaults and loads it with the given id.
|
/// fields to defaults and loads it with the given id.
|
||||||
///
|
///
|
||||||
/// Returns true as long as it is given a valid legacy closed group id (i.e. same format as a
|
/// Returns true as long as it is given a valid legacy group id (i.e. same format as a session id).
|
||||||
/// session id). A false return is considered an error, and means the id was not a valid session
|
/// A false return is considered an error, and means the id was not a valid session id.
|
||||||
/// id.
|
|
||||||
///
|
///
|
||||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||||
/// setting fields in the convo, and then giving it to convo_info_volatile_set().
|
/// setting fields in the convo, and then giving it to convo_info_volatile_set().
|
||||||
bool convo_info_volatile_get_or_construct_legacy_closed(
|
bool convo_info_volatile_get_or_construct_legacy_group(
|
||||||
const config_object* conf, convo_info_volatile_legacy_closed* convo, const char* id)
|
const config_object* conf, convo_info_volatile_legacy_group* convo, const char* id)
|
||||||
__attribute__((warn_unused_result));
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
/// Adds or updates a conversation from the given convo info
|
/// Adds or updates a conversation from the given convo info
|
||||||
void convo_info_volatile_set_1to1(config_object* conf, const convo_info_volatile_1to1* convo);
|
void convo_info_volatile_set_1to1(config_object* conf, const convo_info_volatile_1to1* convo);
|
||||||
void convo_info_volatile_set_open(config_object* conf, const convo_info_volatile_open* convo);
|
void convo_info_volatile_set_community(
|
||||||
void convo_info_volatile_set_legacy_closed(
|
config_object* conf, const convo_info_volatile_community* convo);
|
||||||
config_object* conf, const convo_info_volatile_legacy_closed* convo);
|
void convo_info_volatile_set_legacy_group(
|
||||||
|
config_object* conf, const convo_info_volatile_legacy_group* convo);
|
||||||
|
|
||||||
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
||||||
/// and removed, false if the conversation was not present. You must not call this during
|
/// and removed, false if the conversation was not present. You must not call this during
|
||||||
/// iteration; see details below.
|
/// iteration; see details below.
|
||||||
bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id);
|
bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id);
|
||||||
bool convo_info_volatile_erase_open(
|
bool convo_info_volatile_erase_community(
|
||||||
config_object* conf, const char* base_url, const char* room, unsigned const char* pubkey);
|
config_object* conf, const char* base_url, const char* room);
|
||||||
bool convo_info_volatile_erase_legacy_closed(config_object* conf, const char* group_id);
|
bool convo_info_volatile_erase_legacy_group(config_object* conf, const char* group_id);
|
||||||
|
|
||||||
/// Returns the number of conversations.
|
/// Returns the number of conversations.
|
||||||
size_t convo_info_volatile_size(const config_object* conf);
|
size_t convo_info_volatile_size(const config_object* conf);
|
||||||
/// Returns the number of conversations of the specific type.
|
/// Returns the number of conversations of the specific type.
|
||||||
size_t convo_info_volatile_size_1to1(const config_object* conf);
|
size_t convo_info_volatile_size_1to1(const config_object* conf);
|
||||||
size_t convo_info_volatile_size_open(const config_object* conf);
|
size_t convo_info_volatile_size_communities(const config_object* conf);
|
||||||
size_t convo_info_volatile_size_legacy_closed(const config_object* conf);
|
size_t convo_info_volatile_size_legacy_groups(const config_object* conf);
|
||||||
|
|
||||||
/// Functions for iterating through the entire conversation list. Intended use is:
|
/// Functions for iterating through the entire conversation list. Intended use is:
|
||||||
///
|
///
|
||||||
/// convo_info_volatile_1to1 c1;
|
/// convo_info_volatile_1to1 c1;
|
||||||
/// convo_info_volatile_open c2;
|
/// convo_info_volatile_community c2;
|
||||||
/// convo_info_volatile_legacy_closed c3;
|
/// convo_info_volatile_legacy_group c3;
|
||||||
/// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos);
|
/// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos);
|
||||||
/// for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) {
|
/// for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) {
|
||||||
/// if (convo_info_volatile_it_is_1to1(it, &c1)) {
|
/// if (convo_info_volatile_it_is_1to1(it, &c1)) {
|
||||||
/// // use c1.whatever
|
/// // use c1.whatever
|
||||||
/// } else if (convo_info_volatile_it_is_open(it, &c2)) {
|
/// } else if (convo_info_volatile_it_is_community(it, &c2)) {
|
||||||
/// // use c2.whatever
|
/// // use c2.whatever
|
||||||
/// } else if (convo_info_volatile_it_is_legacy_closed(it, &c3)) {
|
/// } else if (convo_info_volatile_it_is_legacy_group(it, &c3)) {
|
||||||
/// // use c3.whatever
|
/// // use c3.whatever
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
@ -169,6 +168,8 @@ size_t convo_info_volatile_size_legacy_closed(const config_object* conf);
|
||||||
/// convo_info_volatile_iterator_erase(it);
|
/// convo_info_volatile_iterator_erase(it);
|
||||||
/// else
|
/// else
|
||||||
/// convo_info_volatile_iterator_advance(it);
|
/// convo_info_volatile_iterator_advance(it);
|
||||||
|
/// } else {
|
||||||
|
/// convo_info_volatile_iterator_advance(it);
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// convo_info_volatile_iterator_free(it);
|
/// convo_info_volatile_iterator_free(it);
|
||||||
|
@ -185,8 +186,9 @@ convo_info_volatile_iterator* convo_info_volatile_iterator_new(const config_obje
|
||||||
// of the `it_is_whatever` function: it will always be true for the particular type being iterated
|
// of the `it_is_whatever` function: it will always be true for the particular type being iterated
|
||||||
// over).
|
// over).
|
||||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1(const config_object* conf);
|
convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1(const config_object* conf);
|
||||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_open(const config_object* conf);
|
convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities(
|
||||||
convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_closed(
|
const config_object* conf);
|
||||||
|
convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups(
|
||||||
const config_object* conf);
|
const config_object* conf);
|
||||||
|
|
||||||
// Frees an iterator once no longer needed.
|
// Frees an iterator once no longer needed.
|
||||||
|
@ -202,14 +204,15 @@ void convo_info_volatile_iterator_advance(convo_info_volatile_iterator* it);
|
||||||
// returns true. Otherwise it returns false.
|
// returns true. Otherwise it returns false.
|
||||||
bool convo_info_volatile_it_is_1to1(convo_info_volatile_iterator* it, convo_info_volatile_1to1* c);
|
bool convo_info_volatile_it_is_1to1(convo_info_volatile_iterator* it, convo_info_volatile_1to1* c);
|
||||||
|
|
||||||
// If the current iterator record is an open group conversation this sets the details into `c` and
|
// If the current iterator record is a community conversation this sets the details into `c` and
|
||||||
// returns true. Otherwise it returns false.
|
// returns true. Otherwise it returns false.
|
||||||
bool convo_info_volatile_it_is_open(convo_info_volatile_iterator* it, convo_info_volatile_open* c);
|
bool convo_info_volatile_it_is_community(
|
||||||
|
convo_info_volatile_iterator* it, convo_info_volatile_community* c);
|
||||||
|
|
||||||
// If the current iterator record is a legacy closed group conversation this sets the details into
|
// If the current iterator record is a legacy group conversation this sets the details into `c` and
|
||||||
// `c` and returns true. Otherwise it returns false.
|
// returns true. Otherwise it returns false.
|
||||||
bool convo_info_volatile_it_is_legacy_closed(
|
bool convo_info_volatile_it_is_legacy_group(
|
||||||
convo_info_volatile_iterator* it, convo_info_volatile_legacy_closed* c);
|
convo_info_volatile_iterator* it, convo_info_volatile_legacy_group* c);
|
||||||
|
|
||||||
// Erases the current convo while advancing the iterator to the next convo in the iteration.
|
// Erases the current convo while advancing the iterator to the next convo in the iteration.
|
||||||
void convo_info_volatile_iterator_erase(config_object* conf, convo_info_volatile_iterator* it);
|
void convo_info_volatile_iterator_erase(config_object* conf, convo_info_volatile_iterator* it);
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
#include <session/config.hpp>
|
#include <session/config.hpp>
|
||||||
|
|
||||||
#include "base.hpp"
|
#include "base.hpp"
|
||||||
|
#include "community.hpp"
|
||||||
|
|
||||||
|
using namespace std::literals;
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
struct convo_info_volatile_1to1;
|
struct convo_info_volatile_1to1;
|
||||||
struct convo_info_volatile_open;
|
struct convo_info_volatile_community;
|
||||||
struct convo_info_volatile_legacy_closed;
|
struct convo_info_volatile_legacy_group;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace session::config {
|
namespace session::config {
|
||||||
|
@ -29,22 +32,23 @@ class ConvoInfoVolatile;
|
||||||
/// included, but will be 0 if no messages are read.
|
/// included, but will be 0 if no messages are read.
|
||||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||||
///
|
///
|
||||||
/// o - open group conversations. Each key is: BASE_URL + '\0' + LC_ROOM_NAME + '\0' +
|
/// o - community conversations. This is a nested dict where the outer keys are the BASE_URL of the
|
||||||
/// SERVER_PUBKEY (in bytes). Note that room name is *always* lower-cased here (so that clients
|
/// community and the outer value is a dict containing:
|
||||||
/// with the same room but with different cases will always set the same key). Values are dicts
|
/// - `#` -- the 32-byte server pubkey
|
||||||
/// with keys:
|
/// - `R` -- dict of rooms on the server; each key is the lower-case room name, value is a dict
|
||||||
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always included,
|
/// containing keys:
|
||||||
/// but will be 0 if no messages are read.
|
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
|
||||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
/// included, but will be 0 if no messages are read.
|
||||||
|
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||||
///
|
///
|
||||||
/// C - legacy closed group conversations. The key is the closed group identifier (which looks
|
/// C - legacy group conversations (aka closed groups). The key is the group identifier (which
|
||||||
/// indistinguishable from a Session ID, but isn't really a proper Session ID). Values are
|
/// looks indistinguishable from a Session ID, but isn't really a proper Session ID). Values
|
||||||
/// dicts with keys:
|
/// are dicts with keys:
|
||||||
/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included,
|
/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included,
|
||||||
/// but will be 0 if no messages are read.
|
/// but will be 0 if no messages are read.
|
||||||
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
/// u - will be present and set to 1 if this conversation is specifically marked unread.
|
||||||
///
|
///
|
||||||
/// c - reserved for future tracking of new closed group conversations.
|
/// c - reserved for future tracking of new group conversations.
|
||||||
|
|
||||||
namespace convo {
|
namespace convo {
|
||||||
|
|
||||||
|
@ -71,96 +75,34 @@ namespace convo {
|
||||||
friend class session::config::ConvoInfoVolatile;
|
friend class session::config::ConvoInfoVolatile;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct open_group : base {
|
struct community : config::community, base {
|
||||||
// 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX')
|
|
||||||
static constexpr size_t MAX_URL = 267, MAX_ROOM = 64;
|
|
||||||
|
|
||||||
std::string_view base_url() const; // Accesses the base url (i.e. not including room or
|
using config::community::community;
|
||||||
// pubkey). Always lower-case.
|
|
||||||
std::string_view room()
|
|
||||||
const; // Accesses the room name, always in lower-case. (Note that the
|
|
||||||
// actual open group info might not be lower-case; it is just in
|
|
||||||
// the open group convo where we force it lower-case).
|
|
||||||
ustring_view pubkey() const; // Accesses the server pubkey (32 bytes).
|
|
||||||
std::string pubkey_hex() const; // Accesses the server pubkey as hex (64 hex digits).
|
|
||||||
|
|
||||||
open_group() = default;
|
|
||||||
|
|
||||||
// Constructs an empty open_group convo struct from url, room, and pubkey. `base_url` and
|
|
||||||
// `room` will be lower-cased if not already (they do not have to be passed lower-case).
|
|
||||||
// pubkey is 32 bytes.
|
|
||||||
open_group(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
|
||||||
|
|
||||||
// Same as above, but takes pubkey as a hex string.
|
|
||||||
open_group(std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
|
||||||
|
|
||||||
// Takes a combined room URL (e.g. https://whatever.com/r/Room?public_key=01234....), either
|
|
||||||
// new style (with /r/) or old style (without /r/). Note that the URL gets canonicalized so
|
|
||||||
// the resulting `base_url()` and `room()` values may not be exactly equal to what is given.
|
|
||||||
//
|
|
||||||
// See also `parse_full_url` which does the same thing but returns it in pieces rather than
|
|
||||||
// constructing a new `open_group` object.
|
|
||||||
explicit open_group(std::string_view full_url);
|
|
||||||
|
|
||||||
// Internal ctor/method for C API implementations:
|
// Internal ctor/method for C API implementations:
|
||||||
open_group(const struct convo_info_volatile_open& c); // From c struct
|
community(const convo_info_volatile_community& c); // From c struct
|
||||||
void into(convo_info_volatile_open& c) const; // Into c struct
|
void into(convo_info_volatile_community& c) const; // Into c struct
|
||||||
|
|
||||||
// Replaces the baseurl/room/pubkey of this object. Note that changing this and then giving
|
|
||||||
// it to `set` will end up inserting a *new* record but not removing the *old* one (you need
|
|
||||||
// to erase first to do that).
|
|
||||||
void set_server(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
|
||||||
void set_server(
|
|
||||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
|
||||||
void set_server(std::string_view full_url);
|
|
||||||
|
|
||||||
// Loads the baseurl/room/pubkey of this object from an encoded key. Throws
|
|
||||||
// std::invalid_argument if the encoded key does not look right.
|
|
||||||
void load_encoded_key(std::string key);
|
|
||||||
|
|
||||||
// Takes a base URL as input and returns it in canonical form. This involves doing things
|
|
||||||
// like lower casing it and removing redundant ports (e.g. :80 when using http://).
|
|
||||||
static std::string canonical_url(std::string_view url);
|
|
||||||
|
|
||||||
// Takes a full room URL, splits it up into canonical url (see above), lower-case room
|
|
||||||
// token, and server pubkey. We take both the deprecated form (e.g.
|
|
||||||
// https://example.com/SomeRoom?public_key=...) and new form
|
|
||||||
// (https://example.com/r/SomeRoom?public_key=...). The public_key is typically specified
|
|
||||||
// in hex (64 digits), but we also accept unpadded base64 (43 chars) and base32z (52 chars)
|
|
||||||
// encodings (for slightly shorter URLs).
|
|
||||||
static std::tuple<std::string, std::string, ustring> parse_full_url(
|
|
||||||
std::string_view full_url);
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::string key;
|
|
||||||
size_t url_size = 0;
|
|
||||||
|
|
||||||
friend class session::config::ConvoInfoVolatile;
|
friend class session::config::ConvoInfoVolatile;
|
||||||
|
friend struct session::config::comm_iterator_helper;
|
||||||
// Returns the key value we use in the stored dict for this open group, i.e.
|
|
||||||
// lc(URL) + lc(NAME) + PUBKEY_BYTES.
|
|
||||||
static std::string make_key(
|
|
||||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
|
||||||
static std::string make_key(
|
|
||||||
std::string_view base_url, std::string_view room, ustring_view pubkey);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct legacy_closed_group : base {
|
struct legacy_group : base {
|
||||||
std::string id; // in hex, indistinguishable from a Session ID
|
std::string id; // in hex, indistinguishable from a Session ID
|
||||||
|
|
||||||
// Constructs an empty legacy_closed_group from a quasi-session_id
|
// Constructs an empty legacy_group from a quasi-session_id
|
||||||
explicit legacy_closed_group(std::string&& group_id);
|
explicit legacy_group(std::string&& group_id);
|
||||||
explicit legacy_closed_group(std::string_view group_id);
|
explicit legacy_group(std::string_view group_id);
|
||||||
|
|
||||||
// Internal ctor/method for C API implementations:
|
// Internal ctor/method for C API implementations:
|
||||||
legacy_closed_group(const struct convo_info_volatile_legacy_closed& c); // From c struct
|
legacy_group(const struct convo_info_volatile_legacy_group& c); // From c struct
|
||||||
void into(convo_info_volatile_legacy_closed& c) const; // Into c struct
|
void into(convo_info_volatile_legacy_group& c) const; // Into c struct
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class session::config::ConvoInfoVolatile;
|
friend class session::config::ConvoInfoVolatile;
|
||||||
};
|
};
|
||||||
|
|
||||||
using any = std::variant<one_to_one, open_group, legacy_closed_group>;
|
using any = std::variant<one_to_one, community, legacy_group>;
|
||||||
} // namespace convo
|
} // namespace convo
|
||||||
|
|
||||||
class ConvoInfoVolatile : public ConfigBase {
|
class ConvoInfoVolatile : public ConfigBase {
|
||||||
|
@ -186,33 +128,49 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
|
|
||||||
const char* encryption_domain() const override { return "ConvoInfoVolatile"; }
|
const char* encryption_domain() const override { return "ConvoInfoVolatile"; }
|
||||||
|
|
||||||
|
/// Our pruning ages. We ignore added conversations that are more than PRUNE_LOW before now,
|
||||||
|
/// and we active remove (when doing a new push) any conversations that are more than PRUNE_HIGH
|
||||||
|
/// before now. Clients can mostly ignore these and just add all conversations; the class just
|
||||||
|
/// transparently ignores (or removes) pruned values.
|
||||||
|
static constexpr auto PRUNE_LOW = 30 * 24h;
|
||||||
|
static constexpr auto PRUNE_HIGH = 45 * 24h;
|
||||||
|
|
||||||
|
/// Overrides push() to prune stale last-read values before we do the push.
|
||||||
|
std::pair<ustring, seqno_t> push() override;
|
||||||
|
|
||||||
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
|
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
|
||||||
/// not found, otherwise returns a filled out `convo::one_to_one`.
|
/// not found, otherwise returns a filled out `convo::one_to_one`.
|
||||||
std::optional<convo::one_to_one> get_1to1(std::string_view session_id) const;
|
std::optional<convo::one_to_one> get_1to1(std::string_view session_id) const;
|
||||||
|
|
||||||
/// Looks up and returns an open group conversation. Takes the base URL, room name (case
|
/// Looks up and returns a community conversation. Takes the base URL and room name (case
|
||||||
/// insensitive), and pubkey (in hex). Retuns nullopt if the open group was not found,
|
/// insensitive). Retuns nullopt if the community was not found, otherwise a filled out
|
||||||
/// otherwise a filled out `convo::open_group`.
|
/// `convo::community`.
|
||||||
std::optional<convo::open_group> get_open(
|
std::optional<convo::community> get_community(
|
||||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
|
std::string_view base_url, std::string_view room) const;
|
||||||
|
|
||||||
/// Same as above, but takes the pubkey as bytes instead of hex
|
/// Looks up and returns a legacy group conversation by ID. The ID looks like a hex Session ID,
|
||||||
std::optional<convo::open_group> get_open(
|
/// but isn't really a Session ID. Returns nullopt if there is no record of the group
|
||||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
/// conversation.
|
||||||
|
std::optional<convo::legacy_group> get_legacy_group(std::string_view pubkey_hex) const;
|
||||||
/// Looks up and returns a legacy closed group conversation by ID. The ID looks like a hex
|
|
||||||
/// Session ID, but isn't really a Session ID. Returns nullopt if there is no record of the
|
|
||||||
/// closed group conversation.
|
|
||||||
std::optional<convo::legacy_closed_group> get_legacy_closed(std::string_view pubkey_hex) const;
|
|
||||||
|
|
||||||
/// These are the same as the above methods (without "_or_construct" in the name), except that
|
/// These are the same as the above methods (without "_or_construct" in the name), except that
|
||||||
/// when the conversation doesn't exist a new one is created, prefilled with the pubkey/url/etc.
|
/// when the conversation doesn't exist a new one is created, prefilled with the pubkey/url/etc.
|
||||||
convo::one_to_one get_or_construct_1to1(std::string_view session_id) const;
|
convo::one_to_one get_or_construct_1to1(std::string_view session_id) const;
|
||||||
convo::open_group get_or_construct_open(
|
convo::legacy_group get_or_construct_legacy_group(std::string_view pubkey_hex) const;
|
||||||
|
|
||||||
|
/// This is similar to get_community, except that it also takes the pubkey; the community is
|
||||||
|
/// looked up by the url & room; if not found, it is constructed using room, url, and pubkey; if
|
||||||
|
/// it *is* found, then it will always have the *input* pubkey, not the stored pubkey
|
||||||
|
/// (effectively the provided pubkey replaces the stored one in the returned object; this is not
|
||||||
|
/// applied to storage, however, unless/until the instance is given to `set()`).
|
||||||
|
///
|
||||||
|
/// Note, however, that when modifying an object like this the update is *only* applied to the
|
||||||
|
/// returned object; like other fields, it is not updated in the internal state unless/until
|
||||||
|
/// that community instance is passed to `set()`.
|
||||||
|
convo::community get_or_construct_community(
|
||||||
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
|
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
|
||||||
convo::open_group get_or_construct_open(
|
convo::community get_or_construct_community(
|
||||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||||
convo::legacy_closed_group get_or_construct_legacy_closed(std::string_view pubkey_hex) const;
|
|
||||||
|
|
||||||
/// Inserts or replaces existing conversation info. For example, to update a 1-to-1
|
/// Inserts or replaces existing conversation info. For example, to update a 1-to-1
|
||||||
/// conversation last read time you would do:
|
/// conversation last read time you would do:
|
||||||
|
@ -222,31 +180,35 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
/// conversations.set(info);
|
/// conversations.set(info);
|
||||||
///
|
///
|
||||||
void set(const convo::one_to_one& c);
|
void set(const convo::one_to_one& c);
|
||||||
void set(const convo::legacy_closed_group& c);
|
void set(const convo::legacy_group& c);
|
||||||
void set(const convo::open_group& c);
|
void set(const convo::community& c);
|
||||||
|
|
||||||
void set(const convo::any& c); // Variant which can be any of the above
|
void set(const convo::any& c); // Variant which can be any of the above
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void set_base(const convo::base& c, DictFieldProxy& info);
|
void set_base(const convo::base& c, DictFieldProxy& info);
|
||||||
|
|
||||||
|
// Drills into the nested dicts to access community details; if the second argument is
|
||||||
|
// non-nullptr then it will be set to the community's pubkey, if it exists.
|
||||||
|
DictFieldProxy community_field(
|
||||||
|
const convo::community& og, ustring_view* get_pubkey = nullptr) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Removes a one-to-one conversation. Returns true if found and removed, false if not present.
|
/// Removes a one-to-one conversation. Returns true if found and removed, false if not present.
|
||||||
bool erase_1to1(std::string_view pubkey);
|
bool erase_1to1(std::string_view pubkey);
|
||||||
|
|
||||||
/// Removes an open group conversation record. Returns true if found and removed, false if not
|
/// Removes a community conversation record. Returns true if found and removed, false if not
|
||||||
/// present. Arguments are the same as `get_open`.
|
/// present. Arguments are the same as `get_community`.
|
||||||
bool erase_open(std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
|
bool erase_community(std::string_view base_url, std::string_view room);
|
||||||
bool erase_open(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
|
||||||
|
|
||||||
/// Removes a legacy closed group conversation. Returns true if found and removed, false if not
|
/// Removes a legacy group conversation. Returns true if found and removed, false if not
|
||||||
/// present.
|
/// present.
|
||||||
bool erase_legacy_closed(std::string_view pubkey_hex);
|
bool erase_legacy_group(std::string_view pubkey_hex);
|
||||||
|
|
||||||
/// Removes a conversation taking the convo::whatever record (rather than the pubkey/url).
|
/// Removes a conversation taking the convo::whatever record (rather than the pubkey/url).
|
||||||
bool erase(const convo::one_to_one& c);
|
bool erase(const convo::one_to_one& c);
|
||||||
bool erase(const convo::open_group& c);
|
bool erase(const convo::community& c);
|
||||||
bool erase(const convo::legacy_closed_group& c);
|
bool erase(const convo::legacy_group& c);
|
||||||
|
|
||||||
bool erase(const convo::any& c); // Variant of any of them
|
bool erase(const convo::any& c); // Variant of any of them
|
||||||
|
|
||||||
|
@ -261,11 +223,10 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
/// Returns the number of conversations (of any type).
|
/// Returns the number of conversations (of any type).
|
||||||
size_t size() const;
|
size_t size() const;
|
||||||
|
|
||||||
/// Returns the number of 1-to-1, open group, and legacy closed group conversations,
|
/// Returns the number of 1-to-1, community, and legacy group conversations, respectively.
|
||||||
/// respectively.
|
|
||||||
size_t size_1to1() const;
|
size_t size_1to1() const;
|
||||||
size_t size_open() const;
|
size_t size_communities() const;
|
||||||
size_t size_legacy_closed() const;
|
size_t size_legacy_groups() const;
|
||||||
|
|
||||||
/// Returns true if the conversation list is empty.
|
/// Returns true if the conversation list is empty.
|
||||||
bool empty() const { return size() == 0; }
|
bool empty() const { return size() == 0; }
|
||||||
|
@ -276,9 +237,9 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
/// for (auto& convo : conversations) {
|
/// for (auto& convo : conversations) {
|
||||||
/// if (auto* dm = std::get_if<convo::one_to_one>(&convo)) {
|
/// if (auto* dm = std::get_if<convo::one_to_one>(&convo)) {
|
||||||
/// // use dm->session_id, dm->last_read, etc.
|
/// // use dm->session_id, dm->last_read, etc.
|
||||||
/// } else if (auto* og = std::get_if<convo::open_group>(&convo)) {
|
/// } else if (auto* og = std::get_if<convo::community>(&convo)) {
|
||||||
/// // use og->base_url, og->room, om->last_read, etc.
|
/// // use og->base_url, og->room, om->last_read, etc.
|
||||||
/// } else if (auto* lcg = std::get_if<convo::legacy_closed_group>(&convo)) {
|
/// } else if (auto* lcg = std::get_if<convo::legacy_group>(&convo)) {
|
||||||
/// // use lcg->id, lcg->last_read
|
/// // use lcg->id, lcg->last_read
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
@ -302,8 +263,8 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
///
|
///
|
||||||
/// Alternatively, you can use the first version with two loops: the first loop through all
|
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||||
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||||
/// through that vector calling `erase_1to1()`/`erase_open()`/`erase_legacy_closed()` for each
|
/// through that vector calling `erase_1to1()`/`erase_community()`/`erase_legacy_group()` for
|
||||||
/// one.
|
/// each one.
|
||||||
///
|
///
|
||||||
iterator begin() const { return iterator{data}; }
|
iterator begin() const { return iterator{data}; }
|
||||||
iterator end() const { return iterator{}; }
|
iterator end() const { return iterator{}; }
|
||||||
|
@ -313,12 +274,11 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
|
|
||||||
/// Returns an iterator that iterates only through one type of conversations
|
/// Returns an iterator that iterates only through one type of conversations
|
||||||
subtype_iterator<convo::one_to_one> begin_1to1() const { return {data}; }
|
subtype_iterator<convo::one_to_one> begin_1to1() const { return {data}; }
|
||||||
subtype_iterator<convo::open_group> begin_open() const { return {data}; }
|
subtype_iterator<convo::community> begin_communities() const { return {data}; }
|
||||||
subtype_iterator<convo::legacy_closed_group> begin_legacy_closed() const { return {data}; }
|
subtype_iterator<convo::legacy_group> begin_legacy_groups() const { return {data}; }
|
||||||
|
|
||||||
using iterator_category = std::input_iterator_tag;
|
using iterator_category = std::input_iterator_tag;
|
||||||
using value_type =
|
using value_type = std::variant<convo::one_to_one, convo::community, convo::legacy_group>;
|
||||||
std::variant<convo::one_to_one, convo::open_group, convo::legacy_closed_group>;
|
|
||||||
using reference = value_type&;
|
using reference = value_type&;
|
||||||
using pointer = value_type*;
|
using pointer = value_type*;
|
||||||
using difference_type = std::ptrdiff_t;
|
using difference_type = std::ptrdiff_t;
|
||||||
|
@ -326,15 +286,15 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
struct iterator {
|
struct iterator {
|
||||||
protected:
|
protected:
|
||||||
std::shared_ptr<convo::any> _val;
|
std::shared_ptr<convo::any> _val;
|
||||||
std::optional<dict::const_iterator> _it_11, _end_11, _it_open, _end_open, _it_lclosed,
|
std::optional<dict::const_iterator> _it_11, _end_11, _it_lgroup, _end_lgroup;
|
||||||
_end_lclosed;
|
std::optional<comm_iterator_helper> _it_comm;
|
||||||
void _load_val();
|
void _load_val();
|
||||||
iterator() = default; // Constructs an end tombstone
|
iterator() = default; // Constructs an end tombstone
|
||||||
explicit iterator(
|
explicit iterator(
|
||||||
const DictFieldRoot& data,
|
const DictFieldRoot& data,
|
||||||
bool oneto1 = true,
|
bool oneto1 = true,
|
||||||
bool open = true,
|
bool communities = true,
|
||||||
bool closed = true);
|
bool legacy_groups = true);
|
||||||
friend class ConvoInfoVolatile;
|
friend class ConvoInfoVolatile;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -358,8 +318,8 @@ class ConvoInfoVolatile : public ConfigBase {
|
||||||
iterator(
|
iterator(
|
||||||
data,
|
data,
|
||||||
std::is_same_v<convo::one_to_one, ConvoType>,
|
std::is_same_v<convo::one_to_one, ConvoType>,
|
||||||
std::is_same_v<convo::open_group, ConvoType>,
|
std::is_same_v<convo::community, ConvoType>,
|
||||||
std::is_same_v<convo::legacy_closed_group, ConvoType>) {}
|
std::is_same_v<convo::legacy_group, ConvoType>) {}
|
||||||
friend class ConvoInfoVolatile;
|
friend class ConvoInfoVolatile;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
typedef enum CONVO_EXPIRATION_MODE {
|
||||||
|
CONVO_EXPIRATION_NONE = 0,
|
||||||
|
CONVO_EXPIRATION_AFTER_SEND = 1,
|
||||||
|
CONVO_EXPIRATION_AFTER_READ = 2,
|
||||||
|
} CONVO_EXPIRATION_MODE;
|
|
@ -0,0 +1,8 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace session::config {
|
||||||
|
|
||||||
|
enum class expiration_mode : int8_t { none = 0, after_send = 1, after_read = 2 };
|
||||||
|
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ enum class Namespace : std::int16_t {
|
||||||
UserProfile = 2,
|
UserProfile = 2,
|
||||||
Contacts = 3,
|
Contacts = 3,
|
||||||
ConvoInfoVolatile = 4,
|
ConvoInfoVolatile = 4,
|
||||||
ClosedGroupInfo = 11,
|
UserGroups = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace session::config
|
} // namespace session::config
|
||||||
|
|
|
@ -7,13 +7,12 @@ extern "C" {
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
typedef struct user_profile_pic {
|
typedef struct user_profile_pic {
|
||||||
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
|
// Null-terminated C string containing the uploaded URL of the pic. Will be length 0 if there
|
||||||
// profile pic.
|
// is no profile pic.
|
||||||
const char* url;
|
char url[224];
|
||||||
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
|
// The profile pic decryption key, in bytes. This is a byte buffer of length 32, *not* a
|
||||||
// null-terminated C string. Will be NULL if there is no profile pic.
|
// null-terminated C string. This is only valid when there is a url (i.e. url has strlen > 0).
|
||||||
const unsigned char* key;
|
unsigned char key[32];
|
||||||
size_t keylen;
|
|
||||||
} user_profile_pic;
|
} user_profile_pic;
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
@ -1,39 +1,57 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#include "session/types.hpp"
|
#include "session/types.hpp"
|
||||||
|
|
||||||
namespace session::config {
|
namespace session::config {
|
||||||
|
|
||||||
// Profile pic info. Note that `url` is null terminated (though the null lies just beyond the end
|
// Profile pic info.
|
||||||
// of the string view: that is, it views into a full std::string).
|
|
||||||
struct profile_pic {
|
struct profile_pic {
|
||||||
private:
|
static constexpr size_t MAX_URL_LENGTH = 223;
|
||||||
std::string url_;
|
|
||||||
ustring key_;
|
|
||||||
|
|
||||||
public:
|
std::string url;
|
||||||
std::string_view url;
|
ustring key;
|
||||||
ustring_view key;
|
|
||||||
|
static void check_key(ustring_view key) {
|
||||||
|
if (!(key.empty() || key.size() == 32))
|
||||||
|
throw std::invalid_argument{"Invalid profile pic key: 32 bytes required"};
|
||||||
|
}
|
||||||
|
|
||||||
// Default constructor, makes an empty profile pic
|
// Default constructor, makes an empty profile pic
|
||||||
profile_pic() = default;
|
profile_pic() = default;
|
||||||
|
|
||||||
// Constructs from string views: the values must stay alive for the duration of the profile_pic
|
// Constructs from a URL and key. Key must be empty or 32 bytes.
|
||||||
// instance. (If not, use `set_url`/`set_key` or the rvalue-argument constructor instead).
|
profile_pic(std::string_view url, ustring_view key) : url{url}, key{key} {
|
||||||
profile_pic(std::string_view url, ustring_view key) : url{url}, key{key} {}
|
check_key(this->key);
|
||||||
|
}
|
||||||
|
|
||||||
// Constructs from temporary strings; the strings are stored/managed internally
|
// Constructs from a string/ustring pair moved into the constructor
|
||||||
profile_pic(std::string&& url, ustring&& key) :
|
profile_pic(std::string&& url, ustring&& key) : url{std::move(url)}, key{std::move(key)} {
|
||||||
url_{std::move(url)}, key_{std::move(key)}, url{url_}, key{key_} {}
|
check_key(this->key);
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if either url or key are empty
|
// Returns true if either url or key are empty (or invalid)
|
||||||
bool empty() const { return url.empty() || key.empty(); }
|
bool empty() const { return url.empty() || key.size() != 32; }
|
||||||
|
|
||||||
// Sets the url or key to a temporary value that needs to be copied and owned by this
|
// Clears the current url/key, if set. This is just a shortcut for calling `.clear()` on each
|
||||||
// profile_pic object. (This is only needed when the source string may not outlive the
|
// of them.
|
||||||
// profile_pic object; if it does, the `url` or `key` can be assigned to directly).
|
void clear() {
|
||||||
void set_url(std::string url);
|
url.clear();
|
||||||
void set_key(ustring key);
|
key.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The object in boolean context is true if url and key are both set, i.e. the opposite of
|
||||||
|
// `empty()`.
|
||||||
|
explicit operator bool() const { return !empty(); }
|
||||||
|
|
||||||
|
// Sets and validates the key. The key can be empty, or 32 bytes. This is almost the same as
|
||||||
|
// just setting `.key` directly, except that it will throw if the provided key is invalid (i.e.
|
||||||
|
// neither empty nor 32 bytes).
|
||||||
|
void set_key(ustring new_key) {
|
||||||
|
check_key(new_key);
|
||||||
|
key = std::move(new_key);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace session::config
|
} // namespace session::config
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "base.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
typedef struct ugroups_legacy_group_info {
|
||||||
|
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||||
|
|
||||||
|
char name[101]; // Null-terminated C string (human-readable). Max length is 511. Will always
|
||||||
|
// be set (even if an empty string).
|
||||||
|
|
||||||
|
bool have_enc_keys; // Will be true if we have an encryption keypair, false if not.
|
||||||
|
unsigned char enc_pubkey[32]; // If `have_enc_keys`, this is the 32-byte pubkey
|
||||||
|
unsigned char enc_seckey[32]; // If `have_enc_keys`, this is the 32-byte secret key
|
||||||
|
|
||||||
|
int64_t disappearing_timer; // Minutes. 0 == disabled.
|
||||||
|
bool hidden; // true if hidden from the convo list
|
||||||
|
int priority; // pinned message priority; 0 = unpinned, larger means pinned higher (i.e. higher
|
||||||
|
// priority conversations come first).
|
||||||
|
} ugroups_legacy_group_info;
|
||||||
|
|
||||||
|
typedef struct ugroups_community_info {
|
||||||
|
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
||||||
|
// only has port if non-default, has trailing / removed)
|
||||||
|
char room[65]; // null-terminated (max length 64); this is case-preserving (i.e. can be
|
||||||
|
// "SomeRoom" instead of "someroom". Note this is different from volatile
|
||||||
|
// info (that one is always forced lower-cased).
|
||||||
|
unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls)
|
||||||
|
|
||||||
|
int priority; // pinned message priority; 0 = unpinned, larger means pinned higher (i.e. higher
|
||||||
|
// priority conversations come first).
|
||||||
|
} ugroups_community_info;
|
||||||
|
|
||||||
|
int user_groups_init(
|
||||||
|
config_object** conf,
|
||||||
|
const unsigned char* ed25519_secretkey,
|
||||||
|
const unsigned char* dump,
|
||||||
|
size_t dumplen,
|
||||||
|
char* error) __attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Gets community conversation info into `comm`, if the community info was found. `base_url` and
|
||||||
|
/// `room` are null-terminated c strings; pubkey is 32 bytes. base_url will be
|
||||||
|
/// normalized/lower-cased; room is case-insensitive for the lookup: note that this may well return
|
||||||
|
/// a community info with a different room capitalization than the one provided to the call.
|
||||||
|
///
|
||||||
|
/// Returns true if the community was found and `comm` populated; false otherwise.
|
||||||
|
bool user_groups_get_community(
|
||||||
|
const config_object* conf,
|
||||||
|
ugroups_community_info* comm,
|
||||||
|
const char* base_url,
|
||||||
|
const char* room) __attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Like the above, but if the community was not found, this constructs one that can be inserted.
|
||||||
|
/// `base_url` will be normalized in the returned object. `room` is a case-insensitive lookup key
|
||||||
|
/// for the room token. Note that it has subtle handling w.r.t its case: if an existing room is
|
||||||
|
/// found, you get back a record with the found case (which could differ in case from what you
|
||||||
|
/// provided). If you want to override to what you provided regardless of what is there you should
|
||||||
|
/// immediately set the name of the returned object to the case you prefer. If a *new* record is
|
||||||
|
/// constructed, however, it will match the room token case as given here.
|
||||||
|
///
|
||||||
|
/// Note that this is all different from convo_info_volatile, which always forces the room token to
|
||||||
|
/// lower-case (because it does not preserve the case).
|
||||||
|
bool user_groups_get_or_construct_community(
|
||||||
|
const config_object* conf,
|
||||||
|
ugroups_community_info* comm,
|
||||||
|
const char* base_url,
|
||||||
|
const char* room,
|
||||||
|
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Fills `group` with the conversation info given a legacy group ID (specified as a null-terminated
|
||||||
|
/// hex string), if the conversation exists, and returns true. If the conversation does not exist
|
||||||
|
/// then `group` is left unchanged and false is returned.
|
||||||
|
bool user_groups_get_legacy_group(
|
||||||
|
const config_object* conf, ugroups_legacy_group_info* group, const char* id)
|
||||||
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Same as the above except that when the conversation does not exist, this sets all the group
|
||||||
|
/// fields to defaults and loads it with the given id.
|
||||||
|
///
|
||||||
|
/// Returns true as long as it is given a valid legacy group group id (i.e. same format as a session
|
||||||
|
/// id). A false return is considered an error, and means the id was not a valid session id.
|
||||||
|
///
|
||||||
|
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||||
|
/// setting fields in the group, and then giving it to user_groups_set().
|
||||||
|
bool user_groups_get_or_construct_legacy_group(
|
||||||
|
const config_object* conf, ugroups_legacy_group_info* group, const char* id)
|
||||||
|
__attribute__((warn_unused_result));
|
||||||
|
|
||||||
|
/// Adds or updates a conversation from the given group info
|
||||||
|
void user_groups_set_community(config_object* conf, const ugroups_community_info* group);
|
||||||
|
void user_groups_set_legacy_group(config_object* conf, const ugroups_legacy_group_info* group);
|
||||||
|
|
||||||
|
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
||||||
|
/// and removed, false if the conversation was not present. You must not call this during
|
||||||
|
/// iteration; see details below.
|
||||||
|
bool user_groups_erase_community(config_object* conf, const char* base_url, const char* room);
|
||||||
|
bool user_groups_erase_legacy_group(config_object* conf, const char* group_id);
|
||||||
|
|
||||||
|
/// Returns the number of conversations.
|
||||||
|
size_t user_groups_size(const config_object* conf);
|
||||||
|
/// Returns the number of conversations of the specific type.
|
||||||
|
size_t user_groups_size_communities(const config_object* conf);
|
||||||
|
size_t user_groups_size_legacy_groups(const config_object* conf);
|
||||||
|
|
||||||
|
/// Functions for iterating through the entire conversation list. Intended use is:
|
||||||
|
///
|
||||||
|
/// ugroups_community_info c2;
|
||||||
|
/// ugroups_legacy_group_info c3;
|
||||||
|
/// user_groups_iterator *it = user_groups_iterator_new(my_groups);
|
||||||
|
/// for (; !user_groups_iterator_done(it); user_groups_iterator_advance(it)) {
|
||||||
|
/// if (user_groups_it_is_community(it, &c2)) {
|
||||||
|
/// // use c2.whatever
|
||||||
|
/// } else if (user_groups_it_is_legacy_group(it, &c3)) {
|
||||||
|
/// // use c3.whatever
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// user_groups_iterator_free(it);
|
||||||
|
///
|
||||||
|
/// It is permitted to modify records (e.g. with a call to one of the `user_groups_set_*`
|
||||||
|
/// functions) and add records while iterating.
|
||||||
|
///
|
||||||
|
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||||
|
/// iteration by calling either user_groups_iterator_advance if not deleting, or
|
||||||
|
/// user_groups_iterator_erase to erase and advance. Usage looks like this:
|
||||||
|
///
|
||||||
|
/// ugroups_community_info comm;
|
||||||
|
/// ugroups_iterator *it = ugroups_iterator_new(my_groups);
|
||||||
|
/// while (!user_groups_iterator_done(it)) {
|
||||||
|
/// if (user_groups_it_is_community(it, &comm)) {
|
||||||
|
/// bool should_delete = /* ... */;
|
||||||
|
/// if (should_delete)
|
||||||
|
/// user_groups_iterator_erase(it);
|
||||||
|
/// else
|
||||||
|
/// user_groups_iterator_advance(it);
|
||||||
|
/// } else {
|
||||||
|
/// user_groups_iterator_advance(it);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// user_groups_iterator_free(it);
|
||||||
|
///
|
||||||
|
|
||||||
|
typedef struct user_groups_iterator user_groups_iterator;
|
||||||
|
|
||||||
|
// Starts a new iterator that iterates over all conversations.
|
||||||
|
user_groups_iterator* user_groups_iterator_new(const config_object* conf);
|
||||||
|
|
||||||
|
// The same as `user_groups_iterator_new` except that this iterates *only* over one type of
|
||||||
|
// conversation. You still need to use `user_groups_it_is_community` (or the alternatives)
|
||||||
|
// to load the data in each pass of the loop. (You can, however, safely ignore the bool return
|
||||||
|
// value of the `it_is_whatever` function: it will always be true for the particular type being
|
||||||
|
// iterated over).
|
||||||
|
user_groups_iterator* user_groups_iterator_new_communities(const config_object* conf);
|
||||||
|
user_groups_iterator* user_groups_iterator_new_legacy_groups(const config_object* conf);
|
||||||
|
|
||||||
|
// Frees an iterator once no longer needed.
|
||||||
|
void user_groups_iterator_free(user_groups_iterator* it);
|
||||||
|
|
||||||
|
// Returns true if iteration has reached the end.
|
||||||
|
bool user_groups_iterator_done(user_groups_iterator* it);
|
||||||
|
|
||||||
|
// Advances the iterator.
|
||||||
|
void user_groups_iterator_advance(user_groups_iterator* it);
|
||||||
|
|
||||||
|
// If the current iterator record is a community conversation this sets the details into `c` and
|
||||||
|
// returns true. Otherwise it returns false.
|
||||||
|
bool user_groups_it_is_community(user_groups_iterator* it, ugroups_community_info* c);
|
||||||
|
|
||||||
|
// If the current iterator record is a legacy group conversation this sets the details into
|
||||||
|
// `c` and returns true. Otherwise it returns false.
|
||||||
|
bool user_groups_it_is_legacy_group(user_groups_iterator* it, ugroups_legacy_group_info* c);
|
||||||
|
|
||||||
|
// Erases the current group while advancing the iterator to the next group in the iteration.
|
||||||
|
void user_groups_iterator_erase(config_object* conf, user_groups_iterator* it);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
|
@ -0,0 +1,335 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <session/config.hpp>
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "community.hpp"
|
||||||
|
#include "namespaces.hpp"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
struct ugroups_legacy_group_info;
|
||||||
|
struct ugroups_community_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace session::config {
|
||||||
|
|
||||||
|
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||||
|
///
|
||||||
|
/// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and
|
||||||
|
/// value is a dict containing keys:
|
||||||
|
///
|
||||||
|
/// n - name (string). Always set, even if empty.
|
||||||
|
/// k - encryption public key (32 bytes). Optional.
|
||||||
|
/// K - encryption secret key (32 bytes). Optional.
|
||||||
|
/// m - set of member session ids (each 33 bytes).
|
||||||
|
/// a - set of admin session ids (each 33 bytes).
|
||||||
|
/// E - disappearing messages duration, in minutes, > 0. Omitted if disappearing messages is
|
||||||
|
/// disabled. (Note that legacy groups only support expire after-read)
|
||||||
|
/// h - hidden: 1 if the conversation has been removed from the conversation list, omitted if
|
||||||
|
/// visible.
|
||||||
|
/// + - the conversation priority, for pinned messages. Omitted means not pinned; otherwise an
|
||||||
|
/// integer value >0, where a higher priority means the conversation is meant to appear
|
||||||
|
/// earlier in the pinned conversation list.
|
||||||
|
///
|
||||||
|
/// o - dict of communities (AKA open groups); within this dict (which deliberately has the same
|
||||||
|
/// layout as convo_info_volatile) each key is the SOGS base URL (in canonical form), and value
|
||||||
|
/// is a dict of:
|
||||||
|
///
|
||||||
|
/// # - server pubkey
|
||||||
|
/// R - dict of rooms on the server. Each key is the *lower-case* room name; each value is:
|
||||||
|
/// n - the room name as is commonly used, i.e. with possible capitalization (if
|
||||||
|
/// appropriate). For instance, a room name SudokuSolvers would be "sudokusolvers" in
|
||||||
|
/// the outer key, with the capitalization variation in use ("SudokuSolvers") in this
|
||||||
|
/// key. This key is *always* present (to keep the room dict non-empty).
|
||||||
|
/// + - the conversation priority, for pinned messages. Omitted means not pinned; otherwise
|
||||||
|
/// an integer value >0, where a higher priority means the conversation is meant to
|
||||||
|
/// appear earlier in the pinned conversation list.
|
||||||
|
///
|
||||||
|
/// c - reserved for future storage of new-style group info.
|
||||||
|
|
||||||
|
/// Struct containing legacy group info (aka "closed groups").
|
||||||
|
struct legacy_group_info {
|
||||||
|
static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded
|
||||||
|
|
||||||
|
std::string session_id; // The legacy group "session id" (33 bytes).
|
||||||
|
std::string name; // human-readable; this should normally always be set, but in theory could be
|
||||||
|
// set to an empty string.
|
||||||
|
ustring enc_pubkey; // bytes (32 or empty)
|
||||||
|
ustring enc_seckey; // bytes (32 or empty)
|
||||||
|
std::chrono::minutes disappearing_timer{0}; // 0 == disabled.
|
||||||
|
bool hidden = false; // true if the conversation is hidden from the convo list
|
||||||
|
int priority = 0; // The priority; 0 means unpinned, larger means pinned higher (i.e.
|
||||||
|
// higher priority conversations come first).
|
||||||
|
|
||||||
|
/// Constructs a new legacy group info from an id (which must look like a session_id). Throws
|
||||||
|
/// if id is invalid.
|
||||||
|
explicit legacy_group_info(std::string sid);
|
||||||
|
|
||||||
|
// Accesses the session ids (in hex) of members of this group. The key is the hex session_id;
|
||||||
|
// the value indicates whether the member is an admin (true) or not (false).
|
||||||
|
const std::map<std::string, bool>& members() const { return members_; }
|
||||||
|
|
||||||
|
// Returns a pair of the number of admins, and regular members of this group. (If all you want
|
||||||
|
// is the overall number just use `.members().size()` instead).
|
||||||
|
std::pair<size_t, size_t> counts() const;
|
||||||
|
|
||||||
|
// Adds a member (by session id and admin status) to this group. Returns true if the member was
|
||||||
|
// inserted or changed admin status, false if the member already existed. Throws
|
||||||
|
// std::invalid_argument if the given session id is invalid.
|
||||||
|
bool insert(std::string session_id, bool admin);
|
||||||
|
|
||||||
|
// Removes a member (by session id) from this group. Returns true if the member was
|
||||||
|
// removed, false if the member was not present.
|
||||||
|
bool erase(const std::string& session_id);
|
||||||
|
|
||||||
|
// Internal ctor/method for C API implementations:
|
||||||
|
legacy_group_info(const struct ugroups_legacy_group_info& c); // From c struct
|
||||||
|
void into(struct ugroups_legacy_group_info& c) const; // Into c struct
|
||||||
|
|
||||||
|
private:
|
||||||
|
// session_id => (is admin)
|
||||||
|
std::map<std::string, bool> members_;
|
||||||
|
|
||||||
|
friend class UserGroups;
|
||||||
|
|
||||||
|
void load(const dict& info_dict);
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Community (aka open group) info
|
||||||
|
struct community_info : community {
|
||||||
|
// Note that *changing* url/room/pubkey and then doing a set inserts a new room under the given
|
||||||
|
// url/room/pubkey, it does *not* update an existing room.
|
||||||
|
|
||||||
|
// See community_base (comm_base.hpp) for common constructors
|
||||||
|
using community::community;
|
||||||
|
|
||||||
|
// Internal ctor/method for C API implementations:
|
||||||
|
community_info(const struct ugroups_community_info& c); // From c struct
|
||||||
|
void into(ugroups_community_info& c) const; // Into c struct
|
||||||
|
|
||||||
|
int priority = 0; // The priority; 0 means unpinned, larger means pinned higher (i.e.
|
||||||
|
// higher priority conversations come first).
|
||||||
|
|
||||||
|
private:
|
||||||
|
void load(const dict& info_dict);
|
||||||
|
|
||||||
|
friend class UserGroups;
|
||||||
|
friend class comm_iterator_helper;
|
||||||
|
};
|
||||||
|
|
||||||
|
using any_group_info = std::variant<community_info, legacy_group_info>;
|
||||||
|
|
||||||
|
class UserGroups : public ConfigBase {
|
||||||
|
|
||||||
|
public:
|
||||||
|
// No default constructor
|
||||||
|
UserGroups() = delete;
|
||||||
|
|
||||||
|
/// Constructs a user group list from existing data (stored from `dump()`) and the user's
|
||||||
|
/// secret key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||||
|
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||||
|
///
|
||||||
|
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||||
|
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||||
|
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||||
|
/// the secret key.
|
||||||
|
///
|
||||||
|
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||||
|
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||||
|
UserGroups(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||||
|
|
||||||
|
Namespace storage_namespace() const override { return Namespace::UserGroups; }
|
||||||
|
|
||||||
|
const char* encryption_domain() const override { return "UserGroups"; }
|
||||||
|
|
||||||
|
/// Looks up and returns a community (aka open group) conversation. Takes the base URL and room
|
||||||
|
/// token (case insensitive). Retuns nullopt if the open group was not found, otherwise a
|
||||||
|
/// filled out `community_info`. Note that the `room` argument here is case-insensitive, but
|
||||||
|
/// the returned value will be the room as stored in the object (i.e. it may have a different
|
||||||
|
/// case from the requested `room` value).
|
||||||
|
std::optional<community_info> get_community(
|
||||||
|
std::string_view base_url, std::string_view room) const;
|
||||||
|
|
||||||
|
/// Looks up and returns a legacy group by group ID (hex, looks like a Session ID). Returns
|
||||||
|
/// nullopt if the group was not found, otherwise returns a filled out `legacy_group_info`.
|
||||||
|
std::optional<legacy_group_info> get_legacy_group(std::string_view pubkey_hex) const;
|
||||||
|
|
||||||
|
/// Same as `get_community`, except if the community isn't found a new blank one is created for
|
||||||
|
/// you, prefilled with the url/room/pubkey.
|
||||||
|
///
|
||||||
|
/// Note that `room` and `pubkey` have special handling:
|
||||||
|
/// - `room` is case-insensitive for the lookup: if a matching room is found then the returned
|
||||||
|
/// value reflects the room case of the existing record, which is not necessarily the same as
|
||||||
|
/// the `room` argument given here (to force a case change, set it within the returned
|
||||||
|
/// object).
|
||||||
|
/// - `pubkey` is not used to find an existing community, but if the community found has a
|
||||||
|
/// *different* pubkey from the one given then the returned record has its pubkey updated in
|
||||||
|
/// the return instance (note that this changed value is not committed to storage, however,
|
||||||
|
/// until the instance is passed to `set()`). For the string_view version the pubkey is
|
||||||
|
/// accepted as hex, base32z, or base64.
|
||||||
|
community_info get_or_construct_community(
|
||||||
|
std::string_view base_url,
|
||||||
|
std::string_view room,
|
||||||
|
std::string_view pubkey_encoded) const;
|
||||||
|
community_info get_or_construct_community(
|
||||||
|
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||||
|
|
||||||
|
/// Gets or constructs a blank legacy_group_info for the given group id.
|
||||||
|
legacy_group_info get_or_construct_legacy_group(std::string_view pubkey_hex) const;
|
||||||
|
|
||||||
|
/// Inserts or replaces existing group info. For example, to update the info for a community
|
||||||
|
/// you would do:
|
||||||
|
///
|
||||||
|
/// auto info = conversations.get_or_construct_community(some_session_id);
|
||||||
|
/// info.last_read = new_unix_timestamp;
|
||||||
|
/// conversations.set(info);
|
||||||
|
///
|
||||||
|
void set(const community_info& info);
|
||||||
|
void set(const legacy_group_info& info);
|
||||||
|
/// Takes a variant of either group type to set:
|
||||||
|
void set(const any_group_info& info);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Drills into the nested dicts to access open group details
|
||||||
|
DictFieldProxy community_field(
|
||||||
|
const community_info& og, ustring_view* get_pubkey = nullptr) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// Removes a community group. Returns true if found and removed, false if not present.
|
||||||
|
/// Arguments are the same as `get_community`.
|
||||||
|
bool erase_community(std::string_view base_url, std::string_view room);
|
||||||
|
|
||||||
|
/// Removes a legacy group conversation. Returns true if found and removed, false if not
|
||||||
|
/// present.
|
||||||
|
bool erase_legacy_group(std::string_view pubkey_hex);
|
||||||
|
|
||||||
|
/// Removes a conversation taking the community_info or legacy_group_info instance (rather than
|
||||||
|
/// the pubkey/url) for convenience.
|
||||||
|
bool erase(const community_info& g);
|
||||||
|
bool erase(const legacy_group_info& c);
|
||||||
|
bool erase(const any_group_info& info);
|
||||||
|
|
||||||
|
struct iterator;
|
||||||
|
|
||||||
|
/// This works like erase, but takes an iterator to the group to remove. The element is removed
|
||||||
|
/// and the iterator to the next element after the removed one is returned. This is intended
|
||||||
|
/// for use where elements are to be removed during iteration: see below for an example.
|
||||||
|
iterator erase(iterator it);
|
||||||
|
|
||||||
|
/// Returns the number of groups (of any type).
|
||||||
|
size_t size() const;
|
||||||
|
|
||||||
|
/// Returns the number of communities
|
||||||
|
size_t size_communities() const;
|
||||||
|
|
||||||
|
/// Returns the number of legacy groups
|
||||||
|
size_t size_legacy_groups() const;
|
||||||
|
|
||||||
|
/// Returns true if the group list is empty.
|
||||||
|
bool empty() const { return size() == 0; }
|
||||||
|
|
||||||
|
/// Iterators for iterating through all groups. Typically you access this implicit via a
|
||||||
|
/// for loop over the `UserGroups` object:
|
||||||
|
///
|
||||||
|
/// for (auto& group : usergroups) {
|
||||||
|
/// if (auto* comm = std::get_if<community_info>(&group)) {
|
||||||
|
/// // use comm->name, comm->priority, etc.
|
||||||
|
/// } else if (auto* lg = std::get_if<legacy_group_info>(&convo)) {
|
||||||
|
/// // use lg->session_id, lg->hidden, etc.
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// This iterates through all groups in sorted order (sorted first by convo type, then by
|
||||||
|
/// id within the type).
|
||||||
|
///
|
||||||
|
/// It is permitted to modify and add records while iterating (e.g. by modifying one of the
|
||||||
|
/// `comm`/`lg` objects and then calling set()).
|
||||||
|
///
|
||||||
|
/// If you need to erase the current conversation during iteration then care is required: you
|
||||||
|
/// need to advance the iterator via the iterator version of erase when erasing an element
|
||||||
|
/// rather than incrementing it regularly. For example:
|
||||||
|
///
|
||||||
|
/// for (auto it = conversations.begin(); it != conversations.end(); ) {
|
||||||
|
/// if (should_remove(*it))
|
||||||
|
/// it = converations.erase(it);
|
||||||
|
/// else
|
||||||
|
/// ++it;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||||
|
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||||
|
/// through that vector calling `erase_1to1()`/`erase_open()`/`erase_legacy_group()` for each
|
||||||
|
/// one.
|
||||||
|
///
|
||||||
|
iterator begin() const { return iterator{data}; }
|
||||||
|
iterator end() const { return iterator{}; }
|
||||||
|
|
||||||
|
template <typename GroupType>
|
||||||
|
struct subtype_iterator;
|
||||||
|
|
||||||
|
/// Returns an iterator that iterates only through one type of conversations. (The regular
|
||||||
|
/// `.end()` iterator is valid for testing the end of these iterations).
|
||||||
|
subtype_iterator<community_info> begin_communities() const { return {data}; }
|
||||||
|
subtype_iterator<legacy_group_info> begin_legacy_groups() const { return {data}; }
|
||||||
|
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using value_type = std::variant<community_info, legacy_group_info>;
|
||||||
|
using reference = value_type&;
|
||||||
|
using pointer = value_type*;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
|
||||||
|
struct iterator {
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<any_group_info> _val;
|
||||||
|
std::optional<comm_iterator_helper> _it_comm;
|
||||||
|
std::optional<dict::const_iterator> _it_legacy, _end_legacy;
|
||||||
|
void _load_val();
|
||||||
|
iterator() = default; // Constructs an end tombstone
|
||||||
|
explicit iterator(
|
||||||
|
const DictFieldRoot& data, bool communities = true, bool legacy_closed = true);
|
||||||
|
friend class UserGroups;
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool operator==(const iterator& other) const;
|
||||||
|
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||||
|
bool done() const; // Equivalent to comparing against the end iterator
|
||||||
|
any_group_info& operator*() const { return *_val; }
|
||||||
|
any_group_info* operator->() const { return _val.get(); }
|
||||||
|
iterator& operator++();
|
||||||
|
iterator operator++(int) {
|
||||||
|
auto copy{*this};
|
||||||
|
++*this;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename GroupType>
|
||||||
|
struct subtype_iterator : iterator {
|
||||||
|
protected:
|
||||||
|
subtype_iterator(const DictFieldRoot& data) :
|
||||||
|
iterator(
|
||||||
|
data,
|
||||||
|
std::is_same_v<community_info, GroupType>,
|
||||||
|
std::is_same_v<legacy_group_info, GroupType>) {}
|
||||||
|
friend class UserGroups;
|
||||||
|
|
||||||
|
public:
|
||||||
|
GroupType& operator*() const { return std::get<GroupType>(*_val); }
|
||||||
|
GroupType* operator->() const { return &std::get<GroupType>(*_val); }
|
||||||
|
subtype_iterator& operator++() {
|
||||||
|
iterator::operator++();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
subtype_iterator operator++(int) {
|
||||||
|
auto copy{*this};
|
||||||
|
++*this;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace session::config
|
|
@ -44,9 +44,9 @@ class UserProfile final : public ConfigBase {
|
||||||
/// Sets the user profile name; if given an empty string then the name is removed.
|
/// Sets the user profile name; if given an empty string then the name is removed.
|
||||||
void set_name(std::string_view new_name);
|
void set_name(std::string_view new_name);
|
||||||
|
|
||||||
/// Gets the user's current profile pic URL and decryption key. Returns nullptr for *both*
|
/// Gets the user's current profile pic URL and decryption key. The returned object will
|
||||||
/// values if *either* value is unset or empty in the config data.
|
/// evaluate as false if the URL and/or key are not set.
|
||||||
std::optional<profile_pic> get_profile_pic() const;
|
profile_pic get_profile_pic() const;
|
||||||
|
|
||||||
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
|
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
|
||||||
/// one is empty.
|
/// one is empty.
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/// libsession-util version triplet (major, minor, patch)
|
||||||
|
extern const uint16_t LIBSESSION_UTIL_VERSION[3];
|
||||||
|
|
||||||
|
/// Printable full libsession-util name and version string, such as `libsession-util v0.1.2-release`
|
||||||
|
/// for a tagged release or `libsession-util v0.1.2-7f144eb5` for an untagged build.
|
||||||
|
extern const char* LIBSESSION_UTIL_VERSION_FULL;
|
||||||
|
|
||||||
|
/// Just the version component as a string, e.g. `v0.1.2-release`.
|
||||||
|
extern const char* LIBSESSION_UTIL_VERSION_STR;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
|
@ -373,7 +373,7 @@ public extension ClosedGroupControlMessage.Kind {
|
||||||
let addedMemberNames: [String] = memberIds
|
let addedMemberNames: [String] = memberIds
|
||||||
.map {
|
.map {
|
||||||
knownMemberNameMap[$0] ??
|
knownMemberNameMap[$0] ??
|
||||||
Profile.truncated(id: $0, threadVariant: .legacyClosedGroup)
|
Profile.truncated(id: $0, threadVariant: .legacyGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(
|
return String(
|
||||||
|
@ -396,7 +396,7 @@ public extension ClosedGroupControlMessage.Kind {
|
||||||
let removedMemberNames: [String] = memberIds.removing(userPublicKey)
|
let removedMemberNames: [String] = memberIds.removing(userPublicKey)
|
||||||
.map {
|
.map {
|
||||||
knownMemberNameMap[$0] ??
|
knownMemberNameMap[$0] ??
|
||||||
Profile.truncated(id: $0, threadVariant: .legacyClosedGroup)
|
Profile.truncated(id: $0, threadVariant: .legacyGroup)
|
||||||
}
|
}
|
||||||
let format: String = (removedMemberNames.count > 1 ?
|
let format: String = (removedMemberNames.count > 1 ?
|
||||||
"GROUP_MEMBERS_REMOVED".localized() :
|
"GROUP_MEMBERS_REMOVED".localized() :
|
||||||
|
|
|
@ -15,6 +15,8 @@ public final class SharedConfigMessage: ControlMessage {
|
||||||
public var seqNo: Int64
|
public var seqNo: Int64
|
||||||
public var data: Data
|
public var data: Data
|
||||||
|
|
||||||
|
/// SharedConfigMessages should last for 30 days rather than the standard 14
|
||||||
|
public override var ttl: UInt64 { 30 * 24 * 60 * 60 * 1000 }
|
||||||
public override var isSelfSendValid: Bool { true }
|
public override var isSelfSendValid: Bool { true }
|
||||||
|
|
||||||
// MARK: - Kind
|
// MARK: - Kind
|
||||||
|
@ -23,14 +25,14 @@ public final class SharedConfigMessage: ControlMessage {
|
||||||
case userProfile
|
case userProfile
|
||||||
case contacts
|
case contacts
|
||||||
case convoInfoVolatile
|
case convoInfoVolatile
|
||||||
case groups
|
case userGroups
|
||||||
|
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .userProfile: return "userProfile"
|
case .userProfile: return "userProfile"
|
||||||
case .contacts: return "contacts"
|
case .contacts: return "contacts"
|
||||||
case .convoInfoVolatile: return "convoInfoVolatile"
|
case .convoInfoVolatile: return "convoInfoVolatile"
|
||||||
case .groups: return "groups"
|
case .userGroups: return "userGroups"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +84,7 @@ public final class SharedConfigMessage: ControlMessage {
|
||||||
case .userProfile: return .userProfile
|
case .userProfile: return .userProfile
|
||||||
case .contacts: return .contacts
|
case .contacts: return .contacts
|
||||||
case .convoInfoVolatile: return .convoInfoVolatile
|
case .convoInfoVolatile: return .convoInfoVolatile
|
||||||
case .groups: return .groups
|
case .userGroups: return .userGroups
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
seqNo: sharedConfigMessage.seqno,
|
seqNo: sharedConfigMessage.seqno,
|
||||||
|
@ -98,7 +100,7 @@ public final class SharedConfigMessage: ControlMessage {
|
||||||
case .userProfile: return .userProfile
|
case .userProfile: return .userProfile
|
||||||
case .contacts: return .contacts
|
case .contacts: return .contacts
|
||||||
case .convoInfoVolatile: return .convoInfoVolatile
|
case .convoInfoVolatile: return .convoInfoVolatile
|
||||||
case .groups: return .groups
|
case .userGroups: return .userGroups
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
seqno: self.seqNo,
|
seqno: self.seqNo,
|
||||||
|
@ -135,7 +137,7 @@ public extension SharedConfigMessage.Kind {
|
||||||
case .userProfile: return .userProfile
|
case .userProfile: return .userProfile
|
||||||
case .contacts: return .contacts
|
case .contacts: return .contacts
|
||||||
case .convoInfoVolatile: return .convoInfoVolatile
|
case .convoInfoVolatile: return .convoInfoVolatile
|
||||||
case .groups: return .groups
|
case .userGroups: return .userGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,10 @@ public extension Message {
|
||||||
|
|
||||||
return .contact(publicKey: thread.id)
|
return .contact(publicKey: thread.id)
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
return .closedGroup(groupPublicKey: thread.id)
|
return .closedGroup(groupPublicKey: thread.id)
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
guard let openGroup: OpenGroup = try thread.openGroup.fetchOne(db) else {
|
guard let openGroup: OpenGroup = try thread.openGroup.fetchOne(db) else {
|
||||||
throw StorageError.objectNotFound
|
throw StorageError.objectNotFound
|
||||||
}
|
}
|
||||||
|
|
|
@ -361,7 +361,7 @@ public extension Message {
|
||||||
let blindedUserPublicKey: String? = SessionThread
|
let blindedUserPublicKey: String? = SessionThread
|
||||||
.getUserHexEncodedBlindedKey(
|
.getUserHexEncodedBlindedKey(
|
||||||
threadId: openGroupId,
|
threadId: openGroupId,
|
||||||
threadVariant: .openGroup
|
threadVariant: .community
|
||||||
)
|
)
|
||||||
for (encodedEmoji, rawReaction) in reactions {
|
for (encodedEmoji, rawReaction) in reactions {
|
||||||
if let decodedEmoji = encodedEmoji.removingPercentEncoding,
|
if let decodedEmoji = encodedEmoji.removingPercentEncoding,
|
||||||
|
|
|
@ -218,8 +218,8 @@ public extension VisibleMessage {
|
||||||
recipient: (try? interaction.recipientStates.fetchOne(db))?.recipientId,
|
recipient: (try? interaction.recipientStates.fetchOne(db))?.recipientId,
|
||||||
groupPublicKey: try? interaction.thread
|
groupPublicKey: try? interaction.thread
|
||||||
.filter(
|
.filter(
|
||||||
SessionThread.Columns.variant == SessionThread.Variant.legacyClosedGroup ||
|
SessionThread.Columns.variant == SessionThread.Variant.legacyGroup ||
|
||||||
SessionThread.Columns.variant == SessionThread.Variant.closedGroup
|
SessionThread.Columns.variant == SessionThread.Variant.group
|
||||||
)
|
)
|
||||||
.select(.id)
|
.select(.id)
|
||||||
.asRequest(of: String.self)
|
.asRequest(of: String.self)
|
||||||
|
|
|
@ -363,7 +363,7 @@ public enum OpenGroupAPI {
|
||||||
requests: requestResponseType,
|
requests: requestResponseType,
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
.flatMap { (info: ResponseInfoType, data: [Endpoint: Codable]) -> AnyPublisher<CapabilitiesAndRoomResponse, Error> in
|
.tryMap { (info: ResponseInfoType, data: [Endpoint: Codable]) -> CapabilitiesAndRoomResponse in
|
||||||
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (data[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
|
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (data[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
|
||||||
let maybeRoomResponse: Codable? = data
|
let maybeRoomResponse: Codable? = data
|
||||||
.first(where: { key, _ in
|
.first(where: { key, _ in
|
||||||
|
@ -380,20 +380,15 @@ public enum OpenGroupAPI {
|
||||||
let capabilities: Capabilities = maybeCapabilities?.body,
|
let capabilities: Capabilities = maybeCapabilities?.body,
|
||||||
let roomInfo: ResponseInfoType = maybeRoom?.responseInfo,
|
let roomInfo: ResponseInfoType = maybeRoom?.responseInfo,
|
||||||
let room: Room = maybeRoom?.body
|
let room: Room = maybeRoom?.body
|
||||||
else {
|
else { throw HTTPError.parsingFailed }
|
||||||
return Fail(error: HTTPError.parsingFailed)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just((
|
return (
|
||||||
info: info,
|
info: info,
|
||||||
data: (
|
data: (
|
||||||
capabilities: (info: capabilitiesInfo, data: capabilities),
|
capabilities: (info: capabilitiesInfo, data: capabilities),
|
||||||
room: (info: roomInfo, data: room)
|
room: (info: roomInfo, data: room)
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -434,7 +429,7 @@ public enum OpenGroupAPI {
|
||||||
requests: requestResponseType,
|
requests: requestResponseType,
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
.flatMap { (info: ResponseInfoType, data: [Endpoint: Codable]) -> AnyPublisher<(capabilities: (info: ResponseInfoType, data: Capabilities), rooms: (info: ResponseInfoType, data: [Room])), Error> in
|
.tryMap { (info: ResponseInfoType, data: [Endpoint: Codable]) -> (capabilities: (info: ResponseInfoType, data: Capabilities), rooms: (info: ResponseInfoType, data: [Room])) in
|
||||||
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (data[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
|
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (data[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
|
||||||
let maybeRooms: HTTP.BatchSubResponse<[Room]>? = data
|
let maybeRooms: HTTP.BatchSubResponse<[Room]>? = data
|
||||||
.first(where: { key, _ in
|
.first(where: { key, _ in
|
||||||
|
@ -450,17 +445,12 @@ public enum OpenGroupAPI {
|
||||||
let capabilities: Capabilities = maybeCapabilities?.body,
|
let capabilities: Capabilities = maybeCapabilities?.body,
|
||||||
let roomsInfo: ResponseInfoType = maybeRooms?.responseInfo,
|
let roomsInfo: ResponseInfoType = maybeRooms?.responseInfo,
|
||||||
let rooms: [Room] = maybeRooms?.body
|
let rooms: [Room] = maybeRooms?.body
|
||||||
else {
|
else { throw HTTPError.parsingFailed }
|
||||||
return Fail(error: HTTPError.parsingFailed)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just((
|
return (
|
||||||
capabilities: (info: capabilitiesInfo, data: capabilities),
|
capabilities: (info: capabilitiesInfo, data: capabilities),
|
||||||
rooms: (info: roomsInfo, data: rooms)
|
rooms: (info: roomsInfo, data: rooms)
|
||||||
))
|
)
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -957,15 +947,10 @@ public enum OpenGroupAPI {
|
||||||
timeout: FileServerAPI.fileTimeout,
|
timeout: FileServerAPI.fileTimeout,
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
.flatMap { responseInfo, maybeData -> AnyPublisher<(ResponseInfoType, Data), Error> in
|
.tryMap { responseInfo, maybeData -> (ResponseInfoType, Data) in
|
||||||
guard let data: Data = maybeData else {
|
guard let data: Data = maybeData else { throw HTTPError.parsingFailed }
|
||||||
return Fail(error: HTTPError.parsingFailed)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Just((responseInfo, data))
|
return (responseInfo, data)
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,7 @@ public final class OpenGroupManager {
|
||||||
|
|
||||||
// Optionally try to insert a new version of the OpenGroup (it will fail if there is already an
|
// Optionally try to insert a new version of the OpenGroup (it will fail if there is already an
|
||||||
// inactive one but that won't matter as we then activate it
|
// inactive one but that won't matter as we then activate it
|
||||||
_ = try? SessionThread.fetchOrCreate(db, id: threadId, variant: .openGroup)
|
_ = try? SessionThread.fetchOrCreate(db, id: threadId, variant: .community)
|
||||||
_ = try? SessionThread.filter(id: threadId).updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
_ = try? SessionThread.filter(id: threadId).updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||||
|
|
||||||
if (try? OpenGroup.exists(db, id: threadId)) == false {
|
if (try? OpenGroup.exists(db, id: threadId)) == false {
|
||||||
|
@ -249,67 +249,64 @@ public final class OpenGroupManager {
|
||||||
OpenGroup.Columns.sequenceNumber.set(to: 0)
|
OpenGroup.Columns.sequenceNumber.set(to: 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
// We was to avoid blocking the db write thread so we dispatch the API call to a different thread
|
// We want to avoid blocking the db write thread so we dispatch the API call to a different thread
|
||||||
//
|
//
|
||||||
// Note: We don't do this after the db commit as it can fail (resulting in endless loading)
|
// Note: We don't do this after the db commit as it can fail (resulting in endless loading)
|
||||||
return Just(())
|
return Deferred {
|
||||||
.setFailureType(to: Error.self)
|
dependencies.storage
|
||||||
.subscribe(on: OpenGroupAPI.workQueue)
|
.readPublisherFlatMap(receiveOn: OpenGroupAPI.workQueue) { db in
|
||||||
.receive(on: OpenGroupAPI.workQueue)
|
// Note: The initial request for room info and it's capabilities should NOT be
|
||||||
.flatMap { _ in
|
// authenticated (this is because if the server requires blinding and the auth
|
||||||
dependencies.storage
|
// headers aren't blinded it will error - these endpoints do support unauthenticated
|
||||||
.readPublisherFlatMap { db in
|
// retrieval so doing so prevents the error)
|
||||||
// Note: The initial request for room info and it's capabilities should NOT be
|
OpenGroupAPI
|
||||||
// authenticated (this is because if the server requires blinding and the auth
|
.capabilitiesAndRoom(
|
||||||
// headers aren't blinded it will error - these endpoints do support unauthenticated
|
|
||||||
// retrieval so doing so prevents the error)
|
|
||||||
OpenGroupAPI
|
|
||||||
.capabilitiesAndRoom(
|
|
||||||
db,
|
|
||||||
for: roomToken,
|
|
||||||
on: targetServer,
|
|
||||||
using: dependencies
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.flatMap { response -> Future<Void, Error> in
|
|
||||||
Future<Void, Error> { resolver in
|
|
||||||
dependencies.storage.write { db in
|
|
||||||
// Enqueue a config sync job (have a newly added open group to sync)
|
|
||||||
if !calledFromConfigHandling {
|
|
||||||
ConfigurationSyncJob.enqueue(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the capabilities first
|
|
||||||
OpenGroupManager.handleCapabilities(
|
|
||||||
db,
|
db,
|
||||||
capabilities: response.data.capabilities.data,
|
|
||||||
on: targetServer
|
|
||||||
)
|
|
||||||
|
|
||||||
// Then the room
|
|
||||||
try OpenGroupManager.handlePollInfo(
|
|
||||||
db,
|
|
||||||
pollInfo: OpenGroupAPI.RoomPollInfo(room: response.data.room.data),
|
|
||||||
publicKey: publicKey,
|
|
||||||
for: roomToken,
|
for: roomToken,
|
||||||
on: targetServer,
|
on: targetServer,
|
||||||
dependencies: dependencies
|
using: dependencies
|
||||||
) {
|
)
|
||||||
resolver(Result.success(()))
|
}
|
||||||
}
|
}
|
||||||
|
.receive(on: OpenGroupAPI.workQueue)
|
||||||
|
.flatMap { response -> Future<Void, Error> in
|
||||||
|
Future<Void, Error> { resolver in
|
||||||
|
dependencies.storage.write { db in
|
||||||
|
// Enqueue a config sync job (have a newly added open group to sync)
|
||||||
|
if !calledFromConfigHandling {
|
||||||
|
ConfigurationSyncJob.enqueue(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the capabilities first
|
||||||
|
OpenGroupManager.handleCapabilities(
|
||||||
|
db,
|
||||||
|
capabilities: response.data.capabilities.data,
|
||||||
|
on: targetServer
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then the room
|
||||||
|
try OpenGroupManager.handlePollInfo(
|
||||||
|
db,
|
||||||
|
pollInfo: OpenGroupAPI.RoomPollInfo(room: response.data.room.data),
|
||||||
|
publicKey: publicKey,
|
||||||
|
for: roomToken,
|
||||||
|
on: targetServer,
|
||||||
|
dependencies: dependencies
|
||||||
|
) {
|
||||||
|
resolver(Result.success(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.handleEvents(
|
}
|
||||||
receiveCompletion: { result in
|
.handleEvents(
|
||||||
switch result {
|
receiveCompletion: { result in
|
||||||
case .finished: break
|
switch result {
|
||||||
case .failure: SNLog("Failed to join open group.")
|
case .finished: break
|
||||||
}
|
case .failure: SNLog("Failed to join open group.")
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
.eraseToAnyPublisher()
|
)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(_ db: Database, openGroupId: String, dependencies: OGMDependencies = OGMDependencies()) {
|
public func delete(_ db: Database, openGroupId: String, dependencies: OGMDependencies = OGMDependencies()) {
|
||||||
|
@ -961,14 +958,14 @@ public final class OpenGroupManager {
|
||||||
|
|
||||||
// Try to retrieve the default rooms 8 times
|
// Try to retrieve the default rooms 8 times
|
||||||
let publisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.storage
|
let publisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.storage
|
||||||
.readPublisherFlatMap { db in
|
.readPublisherFlatMap(receiveOn: OpenGroupAPI.workQueue) { db in
|
||||||
OpenGroupAPI.capabilitiesAndRooms(
|
OpenGroupAPI.capabilitiesAndRooms(
|
||||||
db,
|
db,
|
||||||
on: OpenGroupAPI.defaultServer,
|
on: OpenGroupAPI.defaultServer,
|
||||||
using: dependencies
|
using: dependencies
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.subscribe(on: OpenGroupAPI.workQueue)
|
.receive(on: OpenGroupAPI.workQueue)
|
||||||
.retry(8)
|
.retry(8)
|
||||||
.map { response in
|
.map { response in
|
||||||
dependencies.storage.writeAsync { db in
|
dependencies.storage.writeAsync { db in
|
||||||
|
|
|
@ -3716,7 +3716,7 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
|
||||||
case userProfile = 1
|
case userProfile = 1
|
||||||
case contacts = 2
|
case contacts = 2
|
||||||
case convoInfoVolatile = 3
|
case convoInfoVolatile = 3
|
||||||
case groups = 4
|
case userGroups = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
private class func SNProtoSharedConfigMessageKindWrap(_ value: SessionProtos_SharedConfigMessage.Kind) -> SNProtoSharedConfigMessageKind {
|
private class func SNProtoSharedConfigMessageKindWrap(_ value: SessionProtos_SharedConfigMessage.Kind) -> SNProtoSharedConfigMessageKind {
|
||||||
|
@ -3724,7 +3724,7 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
|
||||||
case .userProfile: return .userProfile
|
case .userProfile: return .userProfile
|
||||||
case .contacts: return .contacts
|
case .contacts: return .contacts
|
||||||
case .convoInfoVolatile: return .convoInfoVolatile
|
case .convoInfoVolatile: return .convoInfoVolatile
|
||||||
case .groups: return .groups
|
case .userGroups: return .userGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3733,7 +3733,7 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
|
||||||
case .userProfile: return .userProfile
|
case .userProfile: return .userProfile
|
||||||
case .contacts: return .contacts
|
case .contacts: return .contacts
|
||||||
case .convoInfoVolatile: return .convoInfoVolatile
|
case .convoInfoVolatile: return .convoInfoVolatile
|
||||||
case .groups: return .groups
|
case .userGroups: return .userGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1623,7 +1623,7 @@ struct SessionProtos_SharedConfigMessage {
|
||||||
case userProfile // = 1
|
case userProfile // = 1
|
||||||
case contacts // = 2
|
case contacts // = 2
|
||||||
case convoInfoVolatile // = 3
|
case convoInfoVolatile // = 3
|
||||||
case groups // = 4
|
case userGroups // = 4
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self = .userProfile
|
self = .userProfile
|
||||||
|
@ -1634,7 +1634,7 @@ struct SessionProtos_SharedConfigMessage {
|
||||||
case 1: self = .userProfile
|
case 1: self = .userProfile
|
||||||
case 2: self = .contacts
|
case 2: self = .contacts
|
||||||
case 3: self = .convoInfoVolatile
|
case 3: self = .convoInfoVolatile
|
||||||
case 4: self = .groups
|
case 4: self = .userGroups
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1644,7 +1644,7 @@ struct SessionProtos_SharedConfigMessage {
|
||||||
case .userProfile: return 1
|
case .userProfile: return 1
|
||||||
case .contacts: return 2
|
case .contacts: return 2
|
||||||
case .convoInfoVolatile: return 3
|
case .convoInfoVolatile: return 3
|
||||||
case .groups: return 4
|
case .userGroups: return 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3343,6 +3343,6 @@ extension SessionProtos_SharedConfigMessage.Kind: SwiftProtobuf._ProtoNameProvid
|
||||||
1: .same(proto: "USER_PROFILE"),
|
1: .same(proto: "USER_PROFILE"),
|
||||||
2: .same(proto: "CONTACTS"),
|
2: .same(proto: "CONTACTS"),
|
||||||
3: .same(proto: "CONVO_INFO_VOLATILE"),
|
3: .same(proto: "CONVO_INFO_VOLATILE"),
|
||||||
4: .same(proto: "GROUPS"),
|
4: .same(proto: "USER_GROUPS"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,7 +277,7 @@ message SharedConfigMessage {
|
||||||
USER_PROFILE = 1;
|
USER_PROFILE = 1;
|
||||||
CONTACTS = 2;
|
CONTACTS = 2;
|
||||||
CONVO_INFO_VOLATILE = 3;
|
CONVO_INFO_VOLATILE = 3;
|
||||||
GROUPS = 4;
|
USER_GROUPS = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @required
|
// @required
|
||||||
|
|
|
@ -918,23 +918,25 @@ public class SignalAttachment: Equatable, Hashable {
|
||||||
let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
|
let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
|
||||||
exportSession.outputURL = exportURL
|
exportSession.outputURL = exportURL
|
||||||
|
|
||||||
let publisher = Future<SignalAttachment, Error> { resolver in
|
let publisher = Deferred {
|
||||||
exportSession.exportAsynchronously {
|
Future<SignalAttachment, Error> { resolver in
|
||||||
let baseFilename = dataSource.sourceFilename
|
exportSession.exportAsynchronously {
|
||||||
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
|
let baseFilename = dataSource.sourceFilename
|
||||||
|
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
|
||||||
guard let dataSource = DataSourcePath.dataSource(with: exportURL,
|
|
||||||
shouldDeleteOnDeallocation: true) else {
|
guard let dataSource = DataSourcePath.dataSource(with: exportURL,
|
||||||
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
|
shouldDeleteOnDeallocation: true) else {
|
||||||
attachment.error = .couldNotConvertToMpeg4
|
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
|
||||||
|
attachment.error = .couldNotConvertToMpeg4
|
||||||
|
resolver(Result.success(attachment))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.sourceFilename = mp4Filename
|
||||||
|
|
||||||
|
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
|
||||||
resolver(Result.success(attachment))
|
resolver(Result.success(attachment))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.sourceFilename = mp4Filename
|
|
||||||
|
|
||||||
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
|
|
||||||
resolver(Result.success(attachment))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
|
@ -70,7 +70,7 @@ extension MessageReceiver {
|
||||||
// Create the group
|
// Create the group
|
||||||
let groupAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: groupPublicKey)) ?? false)
|
let groupAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: groupPublicKey)) ?? false)
|
||||||
let thread: SessionThread = try SessionThread
|
let thread: SessionThread = try SessionThread
|
||||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyClosedGroup)
|
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
|
||||||
.with(shouldBeVisible: true)
|
.with(shouldBeVisible: true)
|
||||||
.saved(db)
|
.saved(db)
|
||||||
let closedGroup: ClosedGroup = try ClosedGroup(
|
let closedGroup: ClosedGroup = try ClosedGroup(
|
||||||
|
|
|
@ -168,7 +168,7 @@ extension MessageReceiver {
|
||||||
// past two weeks)
|
// past two weeks)
|
||||||
if isInitialSync {
|
if isInitialSync {
|
||||||
let existingClosedGroupsIds: [String] = (try? SessionThread
|
let existingClosedGroupsIds: [String] = (try? SessionThread
|
||||||
.filter(SessionThread.Columns.variant == SessionThread.Variant.legacyClosedGroup)
|
.filter(SessionThread.Columns.variant == SessionThread.Variant.legacyGroup)
|
||||||
.fetchAll(db))
|
.fetchAll(db))
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
.map { $0.id }
|
.map { $0.id }
|
||||||
|
|
|
@ -393,7 +393,7 @@ extension MessageReceiver {
|
||||||
).save(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
try GroupMember
|
try GroupMember
|
||||||
.filter(GroupMember.Columns.groupId == thread.id)
|
.filter(GroupMember.Columns.groupId == thread.id)
|
||||||
.fetchAll(db)
|
.fetchAll(db)
|
||||||
|
@ -405,7 +405,7 @@ extension MessageReceiver {
|
||||||
).save(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .openGroup:
|
case .community:
|
||||||
try RecipientState(
|
try RecipientState(
|
||||||
interactionId: interactionId,
|
interactionId: interactionId,
|
||||||
recipientId: thread.id, // For open groups this will always be the thread id
|
recipientId: thread.id, // For open groups this will always be the thread id
|
||||||
|
|
|
@ -40,7 +40,7 @@ extension MessageSender {
|
||||||
do {
|
do {
|
||||||
// Create the relevant objects in the database
|
// Create the relevant objects in the database
|
||||||
thread = try SessionThread
|
thread = try SessionThread
|
||||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyClosedGroup)
|
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
|
||||||
try ClosedGroup(
|
try ClosedGroup(
|
||||||
threadId: groupPublicKey,
|
threadId: groupPublicKey,
|
||||||
name: name,
|
name: name,
|
||||||
|
|
|
@ -284,14 +284,14 @@ public enum MessageReceiver {
|
||||||
// Note: We don't want to create a thread for an open group if it doesn't exist
|
// Note: We don't want to create a thread for an open group if it doesn't exist
|
||||||
if (try? SessionThread.exists(db, id: openGroupId)) != true { return nil }
|
if (try? SessionThread.exists(db, id: openGroupId)) != true { return nil }
|
||||||
|
|
||||||
return (openGroupId, .openGroup)
|
return (openGroupId, .community)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let groupPublicKey: String = message.groupPublicKey {
|
if let groupPublicKey: String = message.groupPublicKey {
|
||||||
// Note: We don't want to create a thread for a closed group if it doesn't exist
|
// Note: We don't want to create a thread for a closed group if it doesn't exist
|
||||||
if (try? SessionThread.exists(db, id: groupPublicKey)) != true { return nil }
|
if (try? SessionThread.exists(db, id: groupPublicKey)) != true { return nil }
|
||||||
|
|
||||||
return (groupPublicKey, .legacyClosedGroup)
|
return (groupPublicKey, .legacyGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the 'syncTarget' value if there is one
|
// Extract the 'syncTarget' value if there is one
|
||||||
|
|
|
@ -68,6 +68,7 @@ extension MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func performUploadsIfNeeded(
|
public static func performUploadsIfNeeded(
|
||||||
|
queue: DispatchQueue,
|
||||||
preparedSendData: PreparedSendData
|
preparedSendData: PreparedSendData
|
||||||
) -> AnyPublisher<PreparedSendData, Error> {
|
) -> AnyPublisher<PreparedSendData, Error> {
|
||||||
// We need an interactionId in order for a message to have uploads
|
// We need an interactionId in order for a message to have uploads
|
||||||
|
@ -95,7 +96,7 @@ extension MessageSender {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.readPublisherFlatMap { db -> AnyPublisher<(attachments: [Attachment], openGroup: OpenGroup?), Error> in
|
.readPublisherFlatMap(receiveOn: queue) { db -> AnyPublisher<(attachments: [Attachment], openGroup: OpenGroup?), Error> in
|
||||||
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
|
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
|
||||||
.stateInfo(interactionId: interactionId, state: .uploading)
|
.stateInfo(interactionId: interactionId, state: .uploading)
|
||||||
.fetchAll(db))
|
.fetchAll(db))
|
||||||
|
|
|
@ -236,8 +236,9 @@ public final class MessageSender {
|
||||||
return PreparedSendData()
|
return PreparedSendData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach the user's profile if needed
|
// Attach the user's profile if needed (no need to do so for 'Note to Self' or sync messages as they
|
||||||
if var messageWithProfile: MessageWithProfile = message as? MessageWithProfile {
|
// will be managed by the user config handling
|
||||||
|
if !isSelfSend, !isSyncMessage, var messageWithProfile: MessageWithProfile = message as? MessageWithProfile {
|
||||||
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||||
|
|
||||||
if let profileKey: Data = profile.profileEncryptionKey, let profilePictureUrl: String = profile.profilePictureUrl {
|
if let profileKey: Data = profile.profileEncryptionKey, let profilePictureUrl: String = profile.profilePictureUrl {
|
||||||
|
@ -597,8 +598,8 @@ public final class MessageSender {
|
||||||
// uploading first, this is here to ensure we don't send a message which should have uploaded
|
// uploading first, this is here to ensure we don't send a message which should have uploaded
|
||||||
// files
|
// files
|
||||||
//
|
//
|
||||||
// If you see this error then you need to call `MessageSender.performUploadsIfNeeded(preparedSendData:)`
|
// If you see this error then you need to call
|
||||||
// before calling this function
|
// `MessageSender.performUploadsIfNeeded(queue:preparedSendData:)` before calling this function
|
||||||
switch preparedSendData.message {
|
switch preparedSendData.message {
|
||||||
case let visibleMessage as VisibleMessage:
|
case let visibleMessage as VisibleMessage:
|
||||||
guard visibleMessage.attachmentIds.count == preparedSendData.totalAttachmentsUploaded else {
|
guard visibleMessage.attachmentIds.count == preparedSendData.totalAttachmentsUploaded else {
|
||||||
|
@ -674,7 +675,7 @@ public final class MessageSender {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return dependencies.storage
|
return dependencies.storage
|
||||||
.writePublisher { db -> Void in
|
.writePublisher(receiveOn: DispatchQueue.global(qos: .default)) { db -> Void in
|
||||||
try MessageSender.handleSuccessfulMessageSend(
|
try MessageSender.handleSuccessfulMessageSend(
|
||||||
db,
|
db,
|
||||||
message: updatedMessage,
|
message: updatedMessage,
|
||||||
|
@ -701,20 +702,22 @@ public final class MessageSender {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future<Bool, Error> { resolver in
|
return Deferred {
|
||||||
NotifyPushServerJob.run(
|
Future<Bool, Error> { resolver in
|
||||||
job,
|
NotifyPushServerJob.run(
|
||||||
queue: DispatchQueue.global(qos: .default),
|
job,
|
||||||
success: { _, _ in resolver(Result.success(true)) },
|
queue: DispatchQueue.global(qos: .default),
|
||||||
failure: { _, _, _ in
|
success: { _, _ in resolver(Result.success(true)) },
|
||||||
// Always fulfill because the notify PN server job isn't critical.
|
failure: { _, _, _ in
|
||||||
resolver(Result.success(true))
|
// Always fulfill because the notify PN server job isn't critical.
|
||||||
},
|
resolver(Result.success(true))
|
||||||
deferred: { _ in
|
},
|
||||||
// Always fulfill because the notify PN server job isn't critical.
|
deferred: { _ in
|
||||||
resolver(Result.success(true))
|
// Always fulfill because the notify PN server job isn't critical.
|
||||||
}
|
resolver(Result.success(true))
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -762,7 +765,7 @@ public final class MessageSender {
|
||||||
|
|
||||||
// Send the result
|
// Send the result
|
||||||
return dependencies.storage
|
return dependencies.storage
|
||||||
.readPublisherFlatMap { db in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .default)) { db in
|
||||||
OpenGroupAPI
|
OpenGroupAPI
|
||||||
.send(
|
.send(
|
||||||
db,
|
db,
|
||||||
|
@ -781,7 +784,7 @@ public final class MessageSender {
|
||||||
let updatedMessage: Message = message
|
let updatedMessage: Message = message
|
||||||
updatedMessage.openGroupServerMessageId = UInt64(responseData.id)
|
updatedMessage.openGroupServerMessageId = UInt64(responseData.id)
|
||||||
|
|
||||||
return dependencies.storage.writePublisher { db in
|
return dependencies.storage.writePublisher(receiveOn: DispatchQueue.global(qos: .default)) { db in
|
||||||
// The `posted` value is in seconds but we sent it in ms so need that for de-duping
|
// The `posted` value is in seconds but we sent it in ms so need that for de-duping
|
||||||
try MessageSender.handleSuccessfulMessageSend(
|
try MessageSender.handleSuccessfulMessageSend(
|
||||||
db,
|
db,
|
||||||
|
@ -831,7 +834,7 @@ public final class MessageSender {
|
||||||
|
|
||||||
// Send the result
|
// Send the result
|
||||||
return dependencies.storage
|
return dependencies.storage
|
||||||
.readPublisherFlatMap { db in
|
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .default)) { db in
|
||||||
return OpenGroupAPI
|
return OpenGroupAPI
|
||||||
.send(
|
.send(
|
||||||
db,
|
db,
|
||||||
|
@ -846,7 +849,7 @@ public final class MessageSender {
|
||||||
let updatedMessage: Message = message
|
let updatedMessage: Message = message
|
||||||
updatedMessage.openGroupServerMessageId = UInt64(responseData.id)
|
updatedMessage.openGroupServerMessageId = UInt64(responseData.id)
|
||||||
|
|
||||||
return dependencies.storage.writePublisher { db in
|
return dependencies.storage.writePublisher(receiveOn: DispatchQueue.global(qos: .default)) { db in
|
||||||
// The `posted` value is in seconds but we sent it in ms so need that for de-duping
|
// The `posted` value is in seconds but we sent it in ms so need that for de-duping
|
||||||
try MessageSender.handleSuccessfulMessageSend(
|
try MessageSender.handleSuccessfulMessageSend(
|
||||||
db,
|
db,
|
||||||
|
|
|
@ -59,7 +59,7 @@ public enum PushNotificationAPI {
|
||||||
// Unsubscribe from all closed groups (including ones the user is no longer a member of,
|
// Unsubscribe from all closed groups (including ones the user is no longer a member of,
|
||||||
// just in case)
|
// just in case)
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.readPublisher { db -> (String, Set<String>) in
|
.readPublisher(receiveOn: DispatchQueue.global(qos: .background)) { db -> (String, Set<String>) in
|
||||||
(
|
(
|
||||||
getUserHexEncodedPublicKey(db),
|
getUserHexEncodedPublicKey(db),
|
||||||
try ClosedGroup
|
try ClosedGroup
|
||||||
|
|
|
@ -82,15 +82,12 @@ public final class ClosedGroupPoller: Poller {
|
||||||
for publicKey: String
|
for publicKey: String
|
||||||
) -> AnyPublisher<Snode, Error> {
|
) -> AnyPublisher<Snode, Error> {
|
||||||
return SnodeAPI.getSwarm(for: publicKey)
|
return SnodeAPI.getSwarm(for: publicKey)
|
||||||
.flatMap { swarm -> AnyPublisher<Snode, Error> in
|
.tryMap { swarm -> Snode in
|
||||||
guard let snode: Snode = swarm.randomElement() else {
|
guard let snode: Snode = swarm.randomElement() else {
|
||||||
return Fail(error: OnionRequestAPIError.insufficientSnodes)
|
throw OnionRequestAPIError.insufficientSnodes
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Just(snode)
|
return snode
|
||||||
.setFailureType(to: Error.self)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class CurrentUserPoller: Poller {
|
public final class CurrentUserPoller: Poller {
|
||||||
public static var namespaces: [SnodeAPI.Namespace] = [
|
public static var namespaces: [SnodeAPI.Namespace] = [
|
||||||
.default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configGroups
|
.default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups
|
||||||
]
|
]
|
||||||
|
|
||||||
private var targetSnode: Atomic<Snode?> = Atomic(nil)
|
private var targetSnode: Atomic<Snode?> = Atomic(nil)
|
||||||
|
@ -89,11 +89,8 @@ public final class CurrentUserPoller: Poller {
|
||||||
// If we haven't retrieved a target snode at this point then either the cache
|
// If we haven't retrieved a target snode at this point then either the cache
|
||||||
// is empty or we have used all of the snodes and need to start from scratch
|
// is empty or we have used all of the snodes and need to start from scratch
|
||||||
return SnodeAPI.getSwarm(for: publicKey)
|
return SnodeAPI.getSwarm(for: publicKey)
|
||||||
.flatMap { [weak self] _ -> AnyPublisher<Snode, Error> in
|
.tryFlatMap { [weak self] _ -> AnyPublisher<Snode, Error> in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else { throw SnodeAPIError.generic }
|
||||||
return Fail(error: SnodeAPIError.generic)
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.targetSnode.mutate { $0 = nil }
|
self?.targetSnode.mutate { $0 = nil }
|
||||||
self?.usedSnodes.mutate { $0.removeAll() }
|
self?.usedSnodes.mutate { $0.removeAll() }
|
||||||
|
|
|
@ -91,7 +91,7 @@ extension OpenGroupAPI {
|
||||||
let server: String = self.server
|
let server: String = self.server
|
||||||
|
|
||||||
return dependencies.storage
|
return dependencies.storage
|
||||||
.readPublisherFlatMap { db -> AnyPublisher<(Int64, PollResponse), Error> in
|
.readPublisherFlatMap(receiveOn: Threading.pollerQueue) { db -> AnyPublisher<(Int64, PollResponse), Error> in
|
||||||
let failureCount: Int64 = (try? OpenGroup
|
let failureCount: Int64 = (try? OpenGroup
|
||||||
.select(max(OpenGroup.Columns.pollFailureCount))
|
.select(max(OpenGroup.Columns.pollFailureCount))
|
||||||
.asRequest(of: Int64.self)
|
.asRequest(of: Int64.self)
|
||||||
|
@ -225,7 +225,7 @@ extension OpenGroupAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
return dependencies.storage
|
return dependencies.storage
|
||||||
.readPublisherFlatMap { db in
|
.readPublisherFlatMap(receiveOn: OpenGroupAPI.workQueue) { db in
|
||||||
OpenGroupAPI.capabilities(
|
OpenGroupAPI.capabilities(
|
||||||
db,
|
db,
|
||||||
server: server,
|
server: server,
|
||||||
|
|
|
@ -200,9 +200,16 @@ public class Poller {
|
||||||
poller?.pollerName(for: publicKey) ??
|
poller?.pollerName(for: publicKey) ??
|
||||||
"poller with public key \(publicKey)"
|
"poller with public key \(publicKey)"
|
||||||
)
|
)
|
||||||
|
let configHashes: [String] = SessionUtil.configHashes(for: publicKey)
|
||||||
|
|
||||||
// Fetch the messages
|
// Fetch the messages
|
||||||
return SnodeAPI.getMessages(in: namespaces, from: snode, associatedWith: publicKey)
|
return SnodeAPI
|
||||||
|
.poll(
|
||||||
|
namespaces: namespaces,
|
||||||
|
refreshingConfigHashes: configHashes,
|
||||||
|
from: snode,
|
||||||
|
associatedWith: publicKey
|
||||||
|
)
|
||||||
.flatMap { namespacedResults -> AnyPublisher<Void, Error> in
|
.flatMap { namespacedResults -> AnyPublisher<Void, Error> in
|
||||||
guard
|
guard
|
||||||
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
|
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
|
||||||
|
@ -322,15 +329,17 @@ public class Poller {
|
||||||
return Publishers
|
return Publishers
|
||||||
.MergeMany(
|
.MergeMany(
|
||||||
jobsToRun.map { job -> AnyPublisher<Void, Error> in
|
jobsToRun.map { job -> AnyPublisher<Void, Error> in
|
||||||
Future<Void, Error> { resolver in
|
Deferred {
|
||||||
// Note: In the background we just want jobs to fail silently
|
Future<Void, Error> { resolver in
|
||||||
MessageReceiveJob.run(
|
// Note: In the background we just want jobs to fail silently
|
||||||
job,
|
MessageReceiveJob.run(
|
||||||
queue: queue,
|
job,
|
||||||
success: { _, _ in resolver(Result.success(())) },
|
queue: queue,
|
||||||
failure: { _, _, _ in resolver(Result.success(())) },
|
success: { _, _ in resolver(Result.success(())) },
|
||||||
deferred: { _ in resolver(Result.success(())) }
|
failure: { _, _, _ in resolver(Result.success(())) },
|
||||||
)
|
deferred: { _ in resolver(Result.success(())) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,9 @@ public class TypingIndicators {
|
||||||
|
|
||||||
// Don't send typing indicators in group threads
|
// Don't send typing indicators in group threads
|
||||||
guard
|
guard
|
||||||
threadVariant != .legacyClosedGroup &&
|
threadVariant != .legacyGroup &&
|
||||||
threadVariant != .closedGroup &&
|
threadVariant != .group &&
|
||||||
threadVariant != .openGroup
|
threadVariant != .community
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
self.threadId = threadId
|
self.threadId = threadId
|
||||||
|
|
|
@ -34,7 +34,7 @@ public extension MentionInfo {
|
||||||
let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName)
|
let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName)
|
||||||
|
|
||||||
/// **Note:** The `\(MentionInfo.profileKey).*` value **MUST** be first
|
/// **Note:** The `\(MentionInfo.profileKey).*` value **MUST** be first
|
||||||
let limitSQL: SQL? = (threadVariant == .openGroup ? SQL("LIMIT 20") : nil)
|
let limitSQL: SQL? = (threadVariant == .community ? SQL("LIMIT 20") : nil)
|
||||||
|
|
||||||
let request: SQLRequest<MentionInfo> = {
|
let request: SQLRequest<MentionInfo> = {
|
||||||
guard let pattern: FTS5Pattern = pattern else {
|
guard let pattern: FTS5Pattern = pattern else {
|
||||||
|
@ -57,7 +57,7 @@ public extension MentionInfo {
|
||||||
|
|
||||||
WHERE (
|
WHERE (
|
||||||
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
|
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
|
||||||
\(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR
|
\(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR
|
||||||
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
|
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -83,7 +83,7 @@ public extension MentionInfo {
|
||||||
JOIN \(Profile.self) ON (
|
JOIN \(Profile.self) ON (
|
||||||
\(Profile.self).rowid = \(profileFullTextSearch).rowid AND
|
\(Profile.self).rowid = \(profileFullTextSearch).rowid AND
|
||||||
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
|
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
|
||||||
\(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR
|
\(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR
|
||||||
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
|
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -304,9 +304,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
let isGroupThread: Bool = (
|
let isGroupThread: Bool = (
|
||||||
self.threadVariant == .openGroup ||
|
self.threadVariant == .community ||
|
||||||
self.threadVariant == .legacyClosedGroup ||
|
self.threadVariant == .legacyGroup ||
|
||||||
self.threadVariant == .closedGroup
|
self.threadVariant == .group
|
||||||
)
|
)
|
||||||
|
|
||||||
return ViewModel(
|
return ViewModel(
|
||||||
|
@ -741,13 +741,13 @@ public extension MessageViewModel {
|
||||||
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
|
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
|
||||||
)
|
)
|
||||||
LEFT JOIN \(GroupMember.self) AS \(groupMemberModeratorTableLiteral) ON (
|
LEFT JOIN \(GroupMember.self) AS \(groupMemberModeratorTableLiteral) ON (
|
||||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
|
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
|
||||||
\(groupMemberModeratorTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
|
\(groupMemberModeratorTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
|
||||||
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
|
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
|
||||||
\(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)"))
|
\(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)"))
|
||||||
)
|
)
|
||||||
LEFT JOIN \(GroupMember.self) AS \(groupMemberAdminTableLiteral) ON (
|
LEFT JOIN \(GroupMember.self) AS \(groupMemberAdminTableLiteral) ON (
|
||||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
|
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
|
||||||
\(groupMemberAdminTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
|
\(groupMemberAdminTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
|
||||||
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
|
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
|
||||||
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))
|
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))
|
||||||
|
|
|
@ -103,8 +103,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
||||||
public var canWrite: Bool {
|
public var canWrite: Bool {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact: return true
|
case .contact: return true
|
||||||
case .legacyClosedGroup, .closedGroup: return currentUserIsClosedGroupMember == true
|
case .legacyGroup, .group: return currentUserIsClosedGroupMember == true
|
||||||
case .openGroup: return openGroupPermissions?.contains(.write) ?? false
|
case .community: return openGroupPermissions?.contains(.write) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,15 +161,15 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
||||||
public var profile: Profile? {
|
public var profile: Profile? {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact: return contactProfile
|
case .contact: return contactProfile
|
||||||
case .legacyClosedGroup, .closedGroup:
|
case .legacyGroup, .group:
|
||||||
return (closedGroupProfileBack ?? closedGroupProfileBackFallback)
|
return (closedGroupProfileBack ?? closedGroupProfileBackFallback)
|
||||||
case .openGroup: return nil
|
case .community: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var additionalProfile: Profile? {
|
public var additionalProfile: Profile? {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .legacyClosedGroup, .closedGroup: return closedGroupProfileFront
|
case .legacyGroup, .group: return closedGroupProfileFront
|
||||||
default: return nil
|
default: return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,8 +194,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
||||||
public var userCount: Int? {
|
public var userCount: Int? {
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact: return nil
|
case .contact: return nil
|
||||||
case .legacyClosedGroup, .closedGroup: return closedGroupUserCount
|
case .legacyGroup, .group: return closedGroupUserCount
|
||||||
case .openGroup: return openGroupUserCount
|
case .community: return openGroupUserCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1355,8 +1355,8 @@ public extension SessionThreadViewModel {
|
||||||
LEFT JOIN \(OpenGroup.self) ON false
|
LEFT JOIN \(OpenGroup.self) ON false
|
||||||
|
|
||||||
WHERE (
|
WHERE (
|
||||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyClosedGroup)")) OR
|
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyGroup)")) OR
|
||||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.closedGroup)"))
|
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.group)"))
|
||||||
)
|
)
|
||||||
GROUP BY \(thread[.id])
|
GROUP BY \(thread[.id])
|
||||||
"""
|
"""
|
||||||
|
@ -1435,7 +1435,7 @@ public extension SessionThreadViewModel {
|
||||||
) AS \(groupMemberInfoLiteral) ON false
|
) AS \(groupMemberInfoLiteral) ON false
|
||||||
|
|
||||||
WHERE
|
WHERE
|
||||||
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
|
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
|
||||||
\(SQL("\(thread[.id]) != \(userPublicKey)"))
|
\(SQL("\(thread[.id]) != \(userPublicKey)"))
|
||||||
GROUP BY \(thread[.id])
|
GROUP BY \(thread[.id])
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -34,7 +34,7 @@ public struct ProfileManager {
|
||||||
// Before encrypting and submitting we NULL pad the name data to this length.
|
// Before encrypting and submitting we NULL pad the name data to this length.
|
||||||
private static let nameDataLength: UInt = 64
|
private static let nameDataLength: UInt = 64
|
||||||
public static let maxAvatarDiameter: CGFloat = 640
|
public static let maxAvatarDiameter: CGFloat = 640
|
||||||
internal static let avatarAES256KeyByteLength: Int = 32
|
public static let avatarAES256KeyByteLength: Int = 32
|
||||||
private static let avatarNonceLength: Int = 12
|
private static let avatarNonceLength: Int = 12
|
||||||
private static let avatarTagLength: Int = 16
|
private static let avatarTagLength: Int = 16
|
||||||
|
|
||||||
|
|
|
@ -32,25 +32,22 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
error?.deallocate()
|
error?.deallocate()
|
||||||
|
|
||||||
// Empty contacts shouldn't have an existing contact
|
// Empty contacts shouldn't have an existing contact
|
||||||
var definitelyRealId: [CChar] = "050000000000000000000000000000000000000000000000000000000000000000"
|
var definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||||
.bytes
|
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
|
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
|
||||||
expect(contacts_get(conf, contactPtr, &definitelyRealId)).to(beFalse())
|
expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse())
|
||||||
|
|
||||||
expect(contacts_size(conf)).to(equal(0))
|
expect(contacts_size(conf)).to(equal(0))
|
||||||
|
|
||||||
var contact2: contacts_contact = contacts_contact()
|
var contact2: contacts_contact = contacts_contact()
|
||||||
expect(contacts_get_or_construct(conf, &contact2, &definitelyRealId)).to(beTrue())
|
expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue())
|
||||||
expect(contact2.name).to(beNil())
|
expect(String(libSessionVal: contact2.name)).to(beEmpty())
|
||||||
expect(contact2.nickname).to(beNil())
|
expect(String(libSessionVal: contact2.nickname)).to(beEmpty())
|
||||||
expect(contact2.approved).to(beFalse())
|
expect(contact2.approved).to(beFalse())
|
||||||
expect(contact2.approved_me).to(beFalse())
|
expect(contact2.approved_me).to(beFalse())
|
||||||
expect(contact2.blocked).to(beFalse())
|
expect(contact2.blocked).to(beFalse())
|
||||||
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
expect(contact2.profile_pic.url).to(beNil())
|
expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty())
|
||||||
expect(contact2.profile_pic.key).to(beNil())
|
|
||||||
expect(contact2.profile_pic.keylen).to(equal(0))
|
|
||||||
|
|
||||||
// We don't need to push anything, since this is a default contact
|
// We don't need to push anything, since this is a default contact
|
||||||
expect(config_needs_push(conf)).to(beFalse())
|
expect(config_needs_push(conf)).to(beFalse())
|
||||||
|
@ -68,14 +65,8 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
toPush?.deallocate()
|
toPush?.deallocate()
|
||||||
|
|
||||||
// Update the contact data
|
// Update the contact data
|
||||||
let contact2Name: [CChar] = "Joe"
|
contact2.name = "Joe".toLibSession()
|
||||||
.bytes
|
contact2.nickname = "Joey".toLibSession()
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let contact2Nickname: [CChar] = "Joey"
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
contact2Name.withUnsafeBufferPointer { contact2.name = $0.baseAddress }
|
|
||||||
contact2Nickname.withUnsafeBufferPointer { contact2.nickname = $0.baseAddress }
|
|
||||||
contact2.approved = true
|
contact2.approved = true
|
||||||
contact2.approved_me = true
|
contact2.approved_me = true
|
||||||
|
|
||||||
|
@ -85,19 +76,14 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
// Ensure the contact details were updated
|
// Ensure the contact details were updated
|
||||||
var contact3: contacts_contact = contacts_contact()
|
var contact3: contacts_contact = contacts_contact()
|
||||||
expect(contacts_get(conf, &contact3, &definitelyRealId)).to(beTrue())
|
expect(contacts_get(conf, &contact3, &definitelyRealId)).to(beTrue())
|
||||||
expect(String(cString: contact3.name)).to(equal("Joe"))
|
expect(String(libSessionVal: contact3.name)).to(equal("Joe"))
|
||||||
expect(String(cString: contact3.nickname)).to(equal("Joey"))
|
expect(String(libSessionVal: contact3.nickname)).to(equal("Joey"))
|
||||||
expect(contact3.approved).to(beTrue())
|
expect(contact3.approved).to(beTrue())
|
||||||
expect(contact3.approved_me).to(beTrue())
|
expect(contact3.approved_me).to(beTrue())
|
||||||
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
expect(contact3.profile_pic.url).to(beNil())
|
expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty())
|
||||||
expect(contact3.profile_pic.key).to(beNil())
|
|
||||||
expect(contact3.profile_pic.keylen).to(equal(0))
|
|
||||||
expect(contact3.blocked).to(beFalse())
|
expect(contact3.blocked).to(beFalse())
|
||||||
|
expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId))
|
||||||
let contact3SessionId: [CChar] = withUnsafeBytes(of: contact3.session_id) { [UInt8]($0) }
|
|
||||||
.map { CChar($0) }
|
|
||||||
expect(contact3SessionId).to(equal(definitelyRealId.nullTerminated()))
|
|
||||||
|
|
||||||
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
||||||
// to dump the updated state:
|
// to dump the updated state:
|
||||||
|
@ -144,29 +130,24 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
// Ensure the contact details were updated
|
// Ensure the contact details were updated
|
||||||
var contact4: contacts_contact = contacts_contact()
|
var contact4: contacts_contact = contacts_contact()
|
||||||
expect(contacts_get(conf2, &contact4, &definitelyRealId)).to(beTrue())
|
expect(contacts_get(conf2, &contact4, &definitelyRealId)).to(beTrue())
|
||||||
expect(String(cString: contact4.name)).to(equal("Joe"))
|
expect(String(libSessionVal: contact4.name)).to(equal("Joe"))
|
||||||
expect(String(cString: contact4.nickname)).to(equal("Joey"))
|
expect(String(libSessionVal: contact4.nickname)).to(equal("Joey"))
|
||||||
expect(contact4.approved).to(beTrue())
|
expect(contact4.approved).to(beTrue())
|
||||||
expect(contact4.approved_me).to(beTrue())
|
expect(contact4.approved_me).to(beTrue())
|
||||||
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
expect(contact4.profile_pic.url).to(beNil())
|
expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty())
|
||||||
expect(contact4.profile_pic.key).to(beNil())
|
|
||||||
expect(contact4.profile_pic.keylen).to(equal(0))
|
|
||||||
expect(contact4.blocked).to(beFalse())
|
expect(contact4.blocked).to(beFalse())
|
||||||
|
|
||||||
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
|
var anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||||
.bytes
|
var cAnotherId: [CChar] = anotherId.cArray
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
var contact5: contacts_contact = contacts_contact()
|
var contact5: contacts_contact = contacts_contact()
|
||||||
expect(contacts_get_or_construct(conf2, &contact5, &anotherId)).to(beTrue())
|
expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
|
||||||
expect(contact5.name).to(beNil())
|
expect(String(libSessionVal: contact5.name)).to(beEmpty())
|
||||||
expect(contact5.nickname).to(beNil())
|
expect(String(libSessionVal: contact5.nickname)).to(beEmpty())
|
||||||
expect(contact5.approved).to(beFalse())
|
expect(contact5.approved).to(beFalse())
|
||||||
expect(contact5.approved_me).to(beFalse())
|
expect(contact5.approved_me).to(beFalse())
|
||||||
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
|
||||||
expect(contact5.profile_pic.url).to(beNil())
|
expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty())
|
||||||
expect(contact5.profile_pic.key).to(beNil())
|
|
||||||
expect(contact5.profile_pic.keylen).to(equal(0))
|
|
||||||
expect(contact5.blocked).to(beFalse())
|
expect(contact5.blocked).to(beFalse())
|
||||||
|
|
||||||
// We're not setting any fields, but we should still keep a record of the session id
|
// We're not setting any fields, but we should still keep a record of the session id
|
||||||
|
@ -201,24 +182,16 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
var contact6: contacts_contact = contacts_contact()
|
var contact6: contacts_contact = contacts_contact()
|
||||||
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
|
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
|
||||||
while !contacts_iterator_done(contactIterator, &contact6) {
|
while !contacts_iterator_done(contactIterator, &contact6) {
|
||||||
sessionIds.append(
|
sessionIds.append(String(libSessionVal: contact6.session_id) ?? "(N/A)")
|
||||||
String(cString: withUnsafeBytes(of: contact6.session_id) { [UInt8]($0) }
|
nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)")
|
||||||
.map { CChar($0) }
|
|
||||||
.nullTerminated()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
nicknames.append(
|
|
||||||
contact6.nickname.map { String(cString: $0) } ??
|
|
||||||
"(N/A)"
|
|
||||||
)
|
|
||||||
contacts_iterator_advance(contactIterator)
|
contacts_iterator_advance(contactIterator)
|
||||||
}
|
}
|
||||||
contacts_iterator_free(contactIterator) // Need to free the iterator
|
contacts_iterator_free(contactIterator) // Need to free the iterator
|
||||||
|
|
||||||
expect(sessionIds.count).to(equal(2))
|
expect(sessionIds.count).to(equal(2))
|
||||||
expect(sessionIds.count).to(equal(contacts_size(conf)))
|
expect(sessionIds.count).to(equal(contacts_size(conf)))
|
||||||
expect(sessionIds.first).to(equal(String(cString: definitelyRealId.nullTerminated())))
|
expect(sessionIds.first).to(equal(definitelyRealId))
|
||||||
expect(sessionIds.last).to(equal(String(cString: anotherId.nullTerminated())))
|
expect(sessionIds.last).to(equal(anotherId))
|
||||||
expect(nicknames.first).to(equal("Joey"))
|
expect(nicknames.first).to(equal("Joey"))
|
||||||
expect(nicknames.last).to(equal("(N/A)"))
|
expect(nicknames.last).to(equal("(N/A)"))
|
||||||
|
|
||||||
|
@ -228,24 +201,15 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
contacts_erase(conf, definitelyRealId)
|
contacts_erase(conf, definitelyRealId)
|
||||||
|
|
||||||
// Client 2 adds a new friend:
|
// Client 2 adds a new friend:
|
||||||
var thirdId: [CChar] = "052222222222222222222222222222222222222222222222222222222222222222"
|
var thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
|
||||||
.bytes
|
var cThirdId: [CChar] = thirdId.cArray
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let nickname7: [CChar] = "Nickname 3"
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let profileUrl7: [CChar] = "http://example.com/huge.bmp"
|
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let profileKey7: [UInt8] = "qwerty".bytes
|
|
||||||
var contact7: contacts_contact = contacts_contact()
|
var contact7: contacts_contact = contacts_contact()
|
||||||
expect(contacts_get_or_construct(conf2, &contact7, &thirdId)).to(beTrue())
|
expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
|
||||||
nickname7.withUnsafeBufferPointer { contact7.nickname = $0.baseAddress }
|
contact7.nickname = "Nickname 3".toLibSession()
|
||||||
contact7.approved = true
|
contact7.approved = true
|
||||||
contact7.approved_me = true
|
contact7.approved_me = true
|
||||||
profileUrl7.withUnsafeBufferPointer { contact7.profile_pic.url = $0.baseAddress }
|
contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession()
|
||||||
profileKey7.withUnsafeBufferPointer { contact7.profile_pic.key = $0.baseAddress }
|
contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
|
||||||
contact7.profile_pic.keylen = 6
|
|
||||||
contacts_set(conf2, &contact7)
|
contacts_set(conf2, &contact7)
|
||||||
|
|
||||||
expect(config_needs_push(conf)).to(beTrue())
|
expect(config_needs_push(conf)).to(beTrue())
|
||||||
|
@ -308,23 +272,15 @@ class ConfigContactsSpec: QuickSpec {
|
||||||
var contact8: contacts_contact = contacts_contact()
|
var contact8: contacts_contact = contacts_contact()
|
||||||
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
|
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
|
||||||
while !contacts_iterator_done(contactIterator2, &contact8) {
|
while !contacts_iterator_done(contactIterator2, &contact8) {
|
||||||
sessionIds2.append(
|
sessionIds2.append(String(libSessionVal: contact8.session_id) ?? "(N/A)")
|
||||||
String(cString: withUnsafeBytes(of: contact8.session_id) { [UInt8]($0) }
|
nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)")
|
||||||
.map { CChar($0) }
|
|
||||||
.nullTerminated()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
nicknames2.append(
|
|
||||||
contact8.nickname.map { String(cString: $0) } ??
|
|
||||||
"(N/A)"
|
|
||||||
)
|
|
||||||
contacts_iterator_advance(contactIterator2)
|
contacts_iterator_advance(contactIterator2)
|
||||||
}
|
}
|
||||||
contacts_iterator_free(contactIterator2) // Need to free the iterator
|
contacts_iterator_free(contactIterator2) // Need to free the iterator
|
||||||
|
|
||||||
expect(sessionIds2.count).to(equal(2))
|
expect(sessionIds2.count).to(equal(2))
|
||||||
expect(sessionIds2.first).to(equal(String(cString: anotherId.nullTerminated())))
|
expect(sessionIds2.first).to(equal(anotherId))
|
||||||
expect(sessionIds2.last).to(equal(String(cString: thirdId.nullTerminated())))
|
expect(sessionIds2.last).to(equal(thirdId))
|
||||||
expect(nicknames2.first).to(equal("(N/A)"))
|
expect(nicknames2.first).to(equal("(N/A)"))
|
||||||
expect(nicknames2.last).to(equal("Nickname 3"))
|
expect(nicknames2.last).to(equal("Nickname 3"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,9 +60,9 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
// The new data doesn't get stored until we call this:
|
// The new data doesn't get stored until we call this:
|
||||||
convo_info_volatile_set_1to1(conf, &oneToOne2)
|
convo_info_volatile_set_1to1(conf, &oneToOne2)
|
||||||
|
|
||||||
var legacyClosed1: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||||
expect(convo_info_volatile_get_legacy_closed(conf, &legacyClosed1, &definitelyRealId))
|
expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &definitelyRealId))
|
||||||
.to(beFalse())
|
.to(beFalse())
|
||||||
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &definitelyRealId)).to(beTrue())
|
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &definitelyRealId)).to(beTrue())
|
||||||
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
|
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
|
||||||
|
@ -70,40 +70,34 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
expect(config_needs_push(conf)).to(beTrue())
|
expect(config_needs_push(conf)).to(beTrue())
|
||||||
expect(config_needs_dump(conf)).to(beTrue())
|
expect(config_needs_dump(conf)).to(beTrue())
|
||||||
|
|
||||||
var openGroupBaseUrl: [CChar] = "http://Example.ORG:5678"
|
var openGroupBaseUrl: [CChar] = "http://Example.ORG:5678".cArray
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let openGroupBaseUrlResult: [CChar] = ("http://Example.ORG:5678"
|
let openGroupBaseUrlResult: [CChar] = ("http://Example.ORG:5678"
|
||||||
.lowercased()
|
.lowercased()
|
||||||
.bytes
|
.cArray +
|
||||||
.map { CChar(bitPattern: $0) } +
|
|
||||||
[CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
|
[CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
|
||||||
)
|
)
|
||||||
var openGroupRoom: [CChar] = "SudokuRoom"
|
var openGroupRoom: [CChar] = "SudokuRoom".cArray
|
||||||
.bytes
|
|
||||||
.map { CChar(bitPattern: $0) }
|
|
||||||
let openGroupRoomResult: [CChar] = ("SudokuRoom"
|
let openGroupRoomResult: [CChar] = ("SudokuRoom"
|
||||||
.lowercased()
|
.lowercased()
|
||||||
.bytes
|
.cArray +
|
||||||
.map { CChar(bitPattern: $0) } +
|
|
||||||
[CChar](repeating: 0, count: (65 - openGroupRoom.count))
|
[CChar](repeating: 0, count: (65 - openGroupRoom.count))
|
||||||
)
|
)
|
||||||
var openGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
var openGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||||
.bytes
|
.bytes
|
||||||
var openGroup1: convo_info_volatile_open = convo_info_volatile_open()
|
var community1: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
expect(convo_info_volatile_get_or_construct_open(conf, &openGroup1, &openGroupBaseUrl, &openGroupRoom, &openGroupPubkey)).to(beTrue())
|
expect(convo_info_volatile_get_or_construct_community(conf, &community1, &openGroupBaseUrl, &openGroupRoom, &openGroupPubkey)).to(beTrue())
|
||||||
expect(withUnsafeBytes(of: openGroup1.base_url) { [UInt8]($0) }
|
expect(withUnsafeBytes(of: community1.base_url) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
).to(equal(openGroupBaseUrlResult))
|
).to(equal(openGroupBaseUrlResult))
|
||||||
expect(withUnsafeBytes(of: openGroup1.room) { [UInt8]($0) }
|
expect(withUnsafeBytes(of: community1.room) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
).to(equal(openGroupRoomResult))
|
).to(equal(openGroupRoomResult))
|
||||||
expect(withUnsafePointer(to: openGroup1.pubkey) { Data(bytes: $0, count: 32).toHexString() })
|
expect(withUnsafePointer(to: community1.pubkey) { Data(bytes: $0, count: 32).toHexString() })
|
||||||
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||||
openGroup1.unread = true
|
community1.unread = true
|
||||||
|
|
||||||
// The new data doesn't get stored until we call this:
|
// The new data doesn't get stored until we call this:
|
||||||
convo_info_volatile_set_open(conf, &openGroup1);
|
convo_info_volatile_set_community(conf, &community1);
|
||||||
|
|
||||||
var toPush: UnsafeMutablePointer<UInt8>? = nil
|
var toPush: UnsafeMutablePointer<UInt8>? = nil
|
||||||
var toPushLen: Int = 0
|
var toPushLen: Int = 0
|
||||||
|
@ -143,17 +137,17 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
).to(equal(definitelyRealId.nullTerminated()))
|
).to(equal(definitelyRealId.nullTerminated()))
|
||||||
expect(oneToOne4.unread).to(beFalse())
|
expect(oneToOne4.unread).to(beFalse())
|
||||||
|
|
||||||
var openGroup2: convo_info_volatile_open = convo_info_volatile_open()
|
var community2: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
expect(convo_info_volatile_get_open(conf2, &openGroup2, &openGroupBaseUrl, &openGroupRoom, &openGroupPubkey)).to(beTrue())
|
expect(convo_info_volatile_get_community(conf2, &community2, &openGroupBaseUrl, &openGroupRoom)).to(beTrue())
|
||||||
expect(withUnsafeBytes(of: openGroup2.base_url) { [UInt8]($0) }
|
expect(withUnsafeBytes(of: community2.base_url) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
).to(equal(openGroupBaseUrlResult))
|
).to(equal(openGroupBaseUrlResult))
|
||||||
expect(withUnsafeBytes(of: openGroup2.room) { [UInt8]($0) }
|
expect(withUnsafeBytes(of: community2.room) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
).to(equal(openGroupRoomResult))
|
).to(equal(openGroupRoomResult))
|
||||||
expect(withUnsafePointer(to: openGroup2.pubkey) { Data(bytes: $0, count: 32).toHexString() })
|
expect(withUnsafePointer(to: community2.pubkey) { Data(bytes: $0, count: 32).toHexString() })
|
||||||
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||||
openGroup2.unread = true
|
community2.unread = true
|
||||||
|
|
||||||
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
|
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||||
.bytes
|
.bytes
|
||||||
|
@ -165,10 +159,10 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
var thirdId: [CChar] = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
var thirdId: [CChar] = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
.bytes
|
.bytes
|
||||||
.map { CChar(bitPattern: $0) }
|
.map { CChar(bitPattern: $0) }
|
||||||
var legacyClosed2: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
expect(convo_info_volatile_get_or_construct_legacy_closed(conf2, &legacyClosed2, &thirdId)).to(beTrue())
|
expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &thirdId)).to(beTrue())
|
||||||
legacyClosed2.last_read = (nowTimestampMs - 50)
|
legacyGroup2.last_read = (nowTimestampMs - 50)
|
||||||
convo_info_volatile_set_legacy_closed(conf2, &legacyClosed2)
|
convo_info_volatile_set_legacy_group(conf2, &legacyGroup2)
|
||||||
expect(config_needs_push(conf2)).to(beTrue())
|
expect(config_needs_push(conf2)).to(beTrue())
|
||||||
|
|
||||||
var toPush2: UnsafeMutablePointer<UInt8>? = nil
|
var toPush2: UnsafeMutablePointer<UInt8>? = nil
|
||||||
|
@ -190,12 +184,12 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
var seen: [String] = []
|
var seen: [String] = []
|
||||||
expect(convo_info_volatile_size(conf)).to(equal(4))
|
expect(convo_info_volatile_size(conf)).to(equal(4))
|
||||||
expect(convo_info_volatile_size_1to1(conf)).to(equal(2))
|
expect(convo_info_volatile_size_1to1(conf)).to(equal(2))
|
||||||
expect(convo_info_volatile_size_open(conf)).to(equal(1))
|
expect(convo_info_volatile_size_communities(conf)).to(equal(1))
|
||||||
expect(convo_info_volatile_size_legacy_closed(conf)).to(equal(1))
|
expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1))
|
||||||
|
|
||||||
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
|
||||||
var c2: convo_info_volatile_open = convo_info_volatile_open()
|
var c2: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
var c3: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
|
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
|
||||||
|
|
||||||
while !convo_info_volatile_iterator_done(it) {
|
while !convo_info_volatile_iterator_done(it) {
|
||||||
|
@ -206,7 +200,7 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
)
|
)
|
||||||
seen.append("1-to-1: \(sessionId)")
|
seen.append("1-to-1: \(sessionId)")
|
||||||
}
|
}
|
||||||
else if convo_info_volatile_it_is_open(it, &c2) {
|
else if convo_info_volatile_it_is_community(it, &c2) {
|
||||||
let baseUrl: String = String(cString: withUnsafeBytes(of: c2.base_url) { [UInt8]($0) }
|
let baseUrl: String = String(cString: withUnsafeBytes(of: c2.base_url) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
|
@ -218,7 +212,7 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
|
|
||||||
seen.append("og: \(baseUrl)/r/\(room)")
|
seen.append("og: \(baseUrl)/r/\(room)")
|
||||||
}
|
}
|
||||||
else if convo_info_volatile_it_is_legacy_closed(it, &c3) {
|
else if convo_info_volatile_it_is_legacy_group(it, &c3) {
|
||||||
let groupId: String = String(cString: withUnsafeBytes(of: c3.group_id) { [UInt8]($0) }
|
let groupId: String = String(cString: withUnsafeBytes(of: c3.group_id) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
|
@ -271,11 +265,11 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
]))
|
]))
|
||||||
|
|
||||||
var seen2: [String] = []
|
var seen2: [String] = []
|
||||||
var c2: convo_info_volatile_open = convo_info_volatile_open()
|
var c2: convo_info_volatile_community = convo_info_volatile_community()
|
||||||
let it2: OpaquePointer = convo_info_volatile_iterator_new_open(conf)
|
let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf)
|
||||||
|
|
||||||
while !convo_info_volatile_iterator_done(it2) {
|
while !convo_info_volatile_iterator_done(it2) {
|
||||||
expect(convo_info_volatile_it_is_open(it2, &c2)).to(beTrue())
|
expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue())
|
||||||
let baseUrl: String = String(cString: withUnsafeBytes(of: c2.base_url) { [UInt8]($0) }
|
let baseUrl: String = String(cString: withUnsafeBytes(of: c2.base_url) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
|
@ -291,11 +285,11 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
|
||||||
]))
|
]))
|
||||||
|
|
||||||
var seen3: [String] = []
|
var seen3: [String] = []
|
||||||
var c3: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
|
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
|
||||||
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_closed(conf)
|
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf)
|
||||||
|
|
||||||
while !convo_info_volatile_iterator_done(it3) {
|
while !convo_info_volatile_iterator_done(it3) {
|
||||||
expect(convo_info_volatile_it_is_legacy_closed(it3, &c3)).to(beTrue())
|
expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue())
|
||||||
let groupId: String = String(cString: withUnsafeBytes(of: c3.group_id) { [UInt8]($0) }
|
let groupId: String = String(cString: withUnsafeBytes(of: c3.group_id) { [UInt8]($0) }
|
||||||
.map { CChar($0) }
|
.map { CChar($0) }
|
||||||
.nullTerminated()
|
.nullTerminated()
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
||||||
import Sodium
|
import Sodium
|
||||||
import SessionUtil
|
import SessionUtil
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
import SessionMessagingKit
|
||||||
|
|
||||||
import Quick
|
import Quick
|
||||||
import Nimble
|
import Nimble
|
||||||
|
@ -68,25 +69,14 @@ class ConfigUserProfileSpec: QuickSpec {
|
||||||
|
|
||||||
// This should also be unset:
|
// This should also be unset:
|
||||||
let pic: user_profile_pic = user_profile_get_pic(conf)
|
let pic: user_profile_pic = user_profile_get_pic(conf)
|
||||||
expect(pic.url).to(beNil())
|
expect(String(libSessionVal: pic.url)).to(beEmpty())
|
||||||
expect(pic.key).to(beNil())
|
|
||||||
expect(pic.keylen).to(equal(0))
|
|
||||||
|
|
||||||
// Now let's go set a profile name and picture:
|
// Now let's go set a profile name and picture:
|
||||||
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
|
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
|
||||||
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp"
|
let p: user_profile_pic = user_profile_pic(
|
||||||
.bytes
|
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
|
||||||
.map { CChar(bitPattern: $0) }
|
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
|
||||||
let profileKey: [UInt8] = "secretNOTSECRET".bytes
|
)
|
||||||
let p: user_profile_pic = profileUrl.withUnsafeBufferPointer { profileUrlPtr in
|
|
||||||
profileKey.withUnsafeBufferPointer { profileKeyPtr in
|
|
||||||
user_profile_pic(
|
|
||||||
url: profileUrlPtr.baseAddress,
|
|
||||||
key: profileKeyPtr.baseAddress,
|
|
||||||
keylen: 6
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(user_profile_set_pic(conf, p)).to(equal(0))
|
expect(user_profile_set_pic(conf, p)).to(equal(0))
|
||||||
|
|
||||||
// Retrieve them just to make sure they set properly:
|
// Retrieve them just to make sure they set properly:
|
||||||
|
@ -95,11 +85,9 @@ class ConfigUserProfileSpec: QuickSpec {
|
||||||
expect(String(cString: namePtr2!)).to(equal("Kallie"))
|
expect(String(cString: namePtr2!)).to(equal("Kallie"))
|
||||||
|
|
||||||
let pic2: user_profile_pic = user_profile_get_pic(conf);
|
let pic2: user_profile_pic = user_profile_get_pic(conf);
|
||||||
expect(pic2.url).toNot(beNil())
|
expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp"))
|
||||||
expect(pic2.key).toNot(beNil())
|
expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength))
|
||||||
expect(pic2.keylen).to(equal(6))
|
.to(equal("secret78901234567890123456789012".data(using: .utf8)))
|
||||||
expect(String(cString: pic2.url!)).to(equal("http://example.org/omg-pic-123.bmp"))
|
|
||||||
expect(String(pointer: pic2.key, length: pic2.keylen)).to(equal("secret"))
|
|
||||||
|
|
||||||
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
||||||
// to dump the updated state:
|
// to dump the updated state:
|
||||||
|
@ -125,7 +113,7 @@ class ConfigUserProfileSpec: QuickSpec {
|
||||||
1:& d
|
1:& d
|
||||||
1:n 6:Kallie
|
1:n 6:Kallie
|
||||||
1:p 34:http://example.org/omg-pic-123.bmp
|
1:p 34:http://example.org/omg-pic-123.bmp
|
||||||
1:q 6:secret
|
1:q 32:secret78901234567890123456789012
|
||||||
e
|
e
|
||||||
1:< l
|
1:< l
|
||||||
l i0e 32:
|
l i0e 32:
|
||||||
|
@ -146,12 +134,13 @@ class ConfigUserProfileSpec: QuickSpec {
|
||||||
]
|
]
|
||||||
.flatMap { $0 }
|
.flatMap { $0 }
|
||||||
let expPush1Encrypted: [UInt8] = Data(hex: [
|
let expPush1Encrypted: [UInt8] = Data(hex: [
|
||||||
"a2952190dcb9797bc48e48f6dc7b3254d004bde9091cfc9ec3433cbc5939a3726deb04f58a546d7d79e6f8",
|
"877c8e0f5d33f5fffa5a4e162785a9a89918e95de1c4b925201f1f5c29d9ee4f8c36e2b278fce1e6",
|
||||||
"0ea185d43bf93278398556304998ae882304075c77f15c67f9914c4d10005a661f29ff7a79e0a9de7f2172",
|
"b9d999689dd86ff8e79e0a04004fa54d24da89bc2604cb1df8c1356da8f14710543ecec44f2d57fc",
|
||||||
"5ba3b5a6c19eaa3797671b8fa4008d62e9af2744629cbb46664c4d8048e2867f66ed9254120371bdb24e95",
|
"56ea8b7e73d119c69d755f4d513d5d069f02396b8ec0cbed894169836f57ca4b782ce705895c593b",
|
||||||
"b2d92341fa3b1f695046113a768ceb7522269f937ead5591bfa8a5eeee3010474002f2db9de043f0f0d1cf",
|
"4230d50c175d44a08045388d3f4160bacb617b9ae8de3ebc8d9024245cd09ce102627cab2acf1b91",
|
||||||
"b1066a03e7b5d6cfb70a8f84a20cd2df5a510cd3d175708015a52dd4a105886d916db0005dbea5706e5a5d",
|
"26159211359606611ca5814de320d1a7099a65c99b0eebbefb92a115f5efa6b9132809300ac010c6",
|
||||||
"c37ffd0a0ca2824b524da2e2ad181a48bb38e21ed9abe136014a4ee1e472cb2f53102db2a46afa9d68"
|
"857cfbd62af71b0fa97eccec75cb95e67edf40b35fdb9cad125a6976693ab085c6bba96a2e51826e",
|
||||||
|
"81e16b9ec1232af5680f2ced55310486"
|
||||||
].joined()).bytes
|
].joined()).bytes
|
||||||
|
|
||||||
expect(String(pointer: toPush2, length: toPush2Len, encoding: .ascii))
|
expect(String(pointer: toPush2, length: toPush2Len, encoding: .ascii))
|
||||||
|
@ -259,19 +248,10 @@ class ConfigUserProfileSpec: QuickSpec {
|
||||||
user_profile_set_name(conf2, "Raz")
|
user_profile_set_name(conf2, "Raz")
|
||||||
|
|
||||||
// And, on conf2, we're also going to change the profile pic:
|
// And, on conf2, we're also going to change the profile pic:
|
||||||
let profile2Url: [CChar] = "http://new.example.com/pic"
|
let p2: user_profile_pic = user_profile_pic(
|
||||||
.bytes
|
url: "http://new.example.com/pic".toLibSession(),
|
||||||
.map { CChar(bitPattern: $0) }
|
key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession()
|
||||||
let profile2Key: [UInt8] = "qwert\0yuio".bytes
|
)
|
||||||
let p2: user_profile_pic = profile2Url.withUnsafeBufferPointer { profile2UrlPtr in
|
|
||||||
profile2Key.withUnsafeBufferPointer { profile2KeyPtr in
|
|
||||||
user_profile_pic(
|
|
||||||
url: profile2UrlPtr.baseAddress,
|
|
||||||
key: profile2KeyPtr.baseAddress,
|
|
||||||
keylen: 10
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user_profile_set_pic(conf2, p2)
|
user_profile_set_pic(conf2, p2)
|
||||||
|
|
||||||
// Both have changes, so push need a push
|
// Both have changes, so push need a push
|
||||||
|
@ -336,14 +316,16 @@ class ConfigUserProfileSpec: QuickSpec {
|
||||||
// Since only one of them set a profile pic there should be no conflict there:
|
// Since only one of them set a profile pic there should be no conflict there:
|
||||||
let pic3: user_profile_pic = user_profile_get_pic(conf)
|
let pic3: user_profile_pic = user_profile_get_pic(conf)
|
||||||
expect(pic3.url).toNot(beNil())
|
expect(pic3.url).toNot(beNil())
|
||||||
expect(String(cString: pic3.url!)).to(equal("http://new.example.com/pic"))
|
expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic"))
|
||||||
expect(pic3.key).toNot(beNil())
|
expect(pic3.key).toNot(beNil())
|
||||||
expect(String(pointer: pic3.key, length: pic3.keylen)).to(equal("qwert\0yuio"))
|
expect(Data(libSessionVal: pic3.key, count: 32).toHexString())
|
||||||
|
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
|
||||||
let pic4: user_profile_pic = user_profile_get_pic(conf2)
|
let pic4: user_profile_pic = user_profile_get_pic(conf2)
|
||||||
expect(pic4.url).toNot(beNil())
|
expect(pic4.url).toNot(beNil())
|
||||||
expect(String(cString: pic4.url!)).to(equal("http://new.example.com/pic"))
|
expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic"))
|
||||||
expect(pic4.key).toNot(beNil())
|
expect(pic4.key).toNot(beNil())
|
||||||
expect(String(pointer: pic4.key, length: pic4.keylen)).to(equal("qwert\0yuio"))
|
expect(Data(libSessionVal: pic4.key, count: 32).toHexString())
|
||||||
|
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
|
||||||
|
|
||||||
config_confirm_pushed(conf, seqno5)
|
config_confirm_pushed(conf, seqno5)
|
||||||
config_confirm_pushed(conf2, seqno6)
|
config_confirm_pushed(conf2, seqno6)
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Sodium
|
||||||
|
|
||||||
|
import Quick
|
||||||
|
import Nimble
|
||||||
|
|
||||||
|
@testable import SessionMessagingKit
|
||||||
|
|
||||||
|
class TypeConversionUtilitiesSpec: QuickSpec {
|
||||||
|
// MARK: - Spec
|
||||||
|
|
||||||
|
override func spec() {
|
||||||
|
// MARK: - String
|
||||||
|
|
||||||
|
describe("a String") {
|
||||||
|
it("can convert to a cArray") {
|
||||||
|
expect("Test123".cArray).to(equal([84, 101, 115, 116, 49, 50, 51]))
|
||||||
|
}
|
||||||
|
|
||||||
|
context("when initialised with a pointer and length") {
|
||||||
|
it("returns null when given a null pointer") {
|
||||||
|
let test: [CChar] = [84, 101, 115, 116]
|
||||||
|
let result = test.withUnsafeBufferPointer { ptr in
|
||||||
|
String(pointer: nil, length: 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result).to(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns a truncated string when given an incorrect length") {
|
||||||
|
let test: [CChar] = [84, 101, 115, 116]
|
||||||
|
let result = test.withUnsafeBufferPointer { ptr in
|
||||||
|
String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result).to(equal("Te"))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns a string when valid") {
|
||||||
|
let test: [CChar] = [84, 101, 115, 116]
|
||||||
|
let result = test.withUnsafeBufferPointer { ptr in
|
||||||
|
String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result).to(equal("Test\0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("when initialised with a libSession value") {
|
||||||
|
it("returns a string when valid and null terminated") {
|
||||||
|
let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 115, 116, 0)
|
||||||
|
let result = String(libSessionVal: value, nullTerminated: true)
|
||||||
|
|
||||||
|
expect(result).to(equal("Test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns a string when valid and not null terminated") {
|
||||||
|
let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 115, 116)
|
||||||
|
let result = String(libSessionVal: value, nullTerminated: false)
|
||||||
|
|
||||||
|
expect(result).to(equal("Te\0st"))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns an empty string when null and not set to return null") {
|
||||||
|
let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0)
|
||||||
|
let result = String(libSessionVal: value, nullIfEmpty: false)
|
||||||
|
|
||||||
|
expect(result).to(equal(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns null when specified and empty") {
|
||||||
|
let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0)
|
||||||
|
let result = String(libSessionVal: value, nullIfEmpty: true)
|
||||||
|
|
||||||
|
expect(result).to(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
it("defaults the null terminated flag to true") {
|
||||||
|
let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 0, 0)
|
||||||
|
let result = String(libSessionVal: value)
|
||||||
|
|
||||||
|
expect(result).to(equal("Te"))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("defaults the null if empty flag to false") {
|
||||||
|
let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0)
|
||||||
|
let result = String(libSessionVal: value)
|
||||||
|
|
||||||
|
expect(result).to(equal(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can convert to a libSession value") {
|
||||||
|
let result: (CChar, CChar, CChar, CChar, CChar) = "Test".toLibSession()
|
||||||
|
expect(result.0).to(equal(84))
|
||||||
|
expect(result.1).to(equal(101))
|
||||||
|
expect(result.2).to(equal(115))
|
||||||
|
expect(result.3).to(equal(116))
|
||||||
|
expect(result.4).to(equal(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
context("when optional") {
|
||||||
|
context("returns null when null") {
|
||||||
|
let value: String? = nil
|
||||||
|
let result: (CChar, CChar, CChar, CChar, CChar)? = value?.toLibSession()
|
||||||
|
|
||||||
|
expect(result).to(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
context("returns a libSession value when not null") {
|
||||||
|
let value: String? = "Test"
|
||||||
|
let result: (CChar, CChar, CChar, CChar, CChar)? = value?.toLibSession()
|
||||||
|
|
||||||
|
expect(result?.0).to(equal(84))
|
||||||
|
expect(result?.1).to(equal(101))
|
||||||
|
expect(result?.2).to(equal(115))
|
||||||
|
expect(result?.3).to(equal(116))
|
||||||
|
expect(result?.4).to(equal(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
|
describe("Data") {
|
||||||
|
it("can convert to a cArray") {
|
||||||
|
expect(Data([1, 2, 3]).cArray).to(equal([1, 2, 3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
context("when initialised with a libSession value") {
|
||||||
|
it("returns truncated data when given the wrong length") {
|
||||||
|
let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5)
|
||||||
|
let result = Data(libSessionVal: value, count: 2)
|
||||||
|
|
||||||
|
expect(result).to(equal(Data([1, 2])))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns data when valid") {
|
||||||
|
let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5)
|
||||||
|
let result = Data(libSessionVal: value, count: 5)
|
||||||
|
|
||||||
|
expect(result).to(equal(Data([1, 2, 3, 4, 5])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can convert to a libSession value") {
|
||||||
|
let result: (Int8, Int8, Int8, Int8, Int8) = Data([1, 2, 3, 4, 5]).toLibSession()
|
||||||
|
expect(result.0).to(equal(1))
|
||||||
|
expect(result.1).to(equal(2))
|
||||||
|
expect(result.2).to(equal(3))
|
||||||
|
expect(result.3).to(equal(4))
|
||||||
|
expect(result.4).to(equal(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
context("when optional") {
|
||||||
|
context("returns null when null") {
|
||||||
|
let value: Data? = nil
|
||||||
|
let result: (Int8, Int8, Int8, Int8, Int8)? = value?.toLibSession()
|
||||||
|
|
||||||
|
expect(result).to(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
context("returns a libSession value when not null") {
|
||||||
|
let value: Data? = Data([1, 2, 3, 4, 5])
|
||||||
|
let result: (Int8, Int8, Int8, Int8, Int8)? = value?.toLibSession()
|
||||||
|
|
||||||
|
expect(result?.0).to(equal(1))
|
||||||
|
expect(result?.1).to(equal(2))
|
||||||
|
expect(result?.2).to(equal(3))
|
||||||
|
expect(result?.3).to(equal(4))
|
||||||
|
expect(result?.4).to(equal(5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Array
|
||||||
|
|
||||||
|
describe("an Array") {
|
||||||
|
context("when adding a null terminated character") {
|
||||||
|
it("adds a null termination character when not present") {
|
||||||
|
let value: [CChar] = [1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 5, 0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("adds nothing when already present") {
|
||||||
|
let value: [CChar] = [1, 2, 3, 4, 0]
|
||||||
|
|
||||||
|
expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue