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:
Morgan Pretty 2023-02-20 12:56:48 +11:00
parent 345b693225
commit f30b383bb8
135 changed files with 3315 additions and 2056 deletions

View File

@ -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 */,

View File

@ -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()
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
), ),

View File

@ -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")"
) )

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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,

View File

@ -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()
} }

View File

@ -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()
} }

View File

@ -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)))
}
} }
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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)
} }

View File

@ -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

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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.")

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

@ -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)))"

View File

@ -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),

View File

@ -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
} }
} }
} }

View File

@ -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()
} }

View File

@ -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

View File

@ -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()
} }

View File

@ -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 (

View File

@ -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 {

View File

@ -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()
) )

View File

@ -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(

View File

@ -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,

View File

@ -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),

View File

@ -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,

View File

@ -5,4 +5,5 @@ import Foundation
public enum SessionUtilError: Error { public enum SessionUtilError: Error {
case unableToCreateConfigObject case unableToCreateConfigObject
case nilConfigObject case nilConfigObject
case userDoesNotExist
} }

View File

@ -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))
}
}

View File

@ -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>

View File

@ -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"

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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.

View File

@ -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);

View File

@ -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:

View File

@ -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;

View File

@ -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 };
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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() :

View File

@ -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
} }
} }
} }

View File

@ -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
} }

View File

@ -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,

View File

@ -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)

View File

@ -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()
} }

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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"),
] ]
} }

View File

@ -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

View File

@ -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()

View File

@ -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(

View File

@ -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 }

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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))

View File

@ -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,

View File

@ -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

View File

@ -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()
} }

View File

@ -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() }

View File

@ -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,

View File

@ -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()
} }

View File

@ -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

View File

@ -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)'"))
) )
) )

View File

@ -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)"))

View File

@ -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])
""" """

View File

@ -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

View File

@ -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"))
} }

View File

@ -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()

View File

@ -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)

View File

@ -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