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 */; };
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.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 */; };
FD43EE9F297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9E297E2EE0009C87C5 /* SessionUtil+ConvoInfoVolatile.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 */; };
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.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 */; };
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.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>"; };
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>"; };
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>"; };
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>"; };
@ -1922,6 +1927,7 @@
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>"; };
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>"; };
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>"; };
@ -3853,6 +3859,14 @@
path = "Shared Models";
sourceTree = "<group>";
};
FD432435299DEA1C008A0213 /* Utilities */ = {
isa = PBXGroup;
children = (
FD432436299DEA38008A0213 /* TypeConversion+Utilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FD7115F528C8150600B47552 /* Combine */ = {
isa = PBXGroup;
children = (
@ -4030,6 +4044,7 @@
children = (
FD2B4B022949886900AB4848 /* Database */,
FD8ECF8E29381FB200C0D1BB /* Config Handling */,
FD432435299DEA1C008A0213 /* Utilities */,
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */,
FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */,
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */,
@ -4040,6 +4055,7 @@
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
isa = PBXGroup;
children = (
FDDC08F029A300D500BF9681 /* Utilities */,
FD2B4AFA29429D1000AB4848 /* ConfigContactsSpec.swift */,
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */,
FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */,
@ -4203,6 +4219,14 @@
path = _TestUtilities;
sourceTree = "<group>";
};
FDDC08F029A300D500BF9681 /* Utilities */ = {
isa = PBXGroup;
children = (
FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FDE7214E287E50D50093DF33 /* Scripts */ = {
isa = PBXGroup;
children = (
@ -4259,6 +4283,7 @@
FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */,
FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */,
FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */,
FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */,
);
path = Types;
sourceTree = "<group>";
@ -5430,6 +5455,7 @@
FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */,
FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */,
FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */,
FD4324302999F0BC008A0213 /* ValidatableResponse.swift in Sources */,
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */,
FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */,
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */,
@ -5762,6 +5788,7 @@
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */,
FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */,
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */,
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
FD245C56285065EA00B966DD /* SNProto.swift in Sources */,
@ -6023,6 +6050,7 @@
buildActionMask = 2147483647;
files = (
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */,
FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */,
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,

View File

@ -326,7 +326,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
guard
shouldMarkAsRead,
let threadVariant: SessionThread.Variant = try SessionThread
let threadVariant: SessionThread.Variant = try? SessionThread
.filter(id: interaction.threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
@ -421,7 +421,9 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
let webRTCSession: WebRTCSession = self.webRTCSession
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()
}

View File

@ -462,18 +462,19 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in
Storage.shared
.writePublisherFlatMap { db -> AnyPublisher<Void, Error> in
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db -> AnyPublisher<Void, Error> in
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,
groupPublicKey: threadId,
with: updatedMemberIds,
name: updatedName
)
}
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
receiveCompletion: { [weak self] result in
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)
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in
Storage.shared
.writePublisherFlatMap { db in
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
MessageSender.createClosedGroup(db, name: name, members: selectedContacts)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
receiveCompletion: { result in

View File

@ -158,20 +158,20 @@ extension ContextMenuVC {
)
let canCopySessionId: Bool = (
cellViewModel.variant == .standardIncoming &&
cellViewModel.threadVariant != .openGroup
cellViewModel.threadVariant != .community
)
let canDelete: Bool = (
cellViewModel.threadVariant != .openGroup ||
cellViewModel.threadVariant != .community ||
currentUserIsOpenGroupModerator ||
cellViewModel.state == .failed
)
let canBan: Bool = (
cellViewModel.threadVariant == .openGroup &&
cellViewModel.threadVariant == .community &&
currentUserIsOpenGroupModerator
)
let shouldShowEmojiActions: Bool = {
if cellViewModel.threadVariant == .openGroup {
if cellViewModel.threadVariant == .community {
return OpenGroupManager.isOpenGroupSupport(.reactions, on: cellViewModel.threadOpenGroupServer)
}
return !currentThreadIsMessageRequest

View File

@ -421,7 +421,7 @@ extension ConversationVC:
// Send the message
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 {
return
}
@ -545,7 +545,7 @@ extension ConversationVC:
// Send the message
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 {
return
}
@ -1110,9 +1110,9 @@ extension ConversationVC:
guard
cellViewModel.reactionInfo?.isEmpty == false &&
(
self.viewModel.threadData.threadVariant == .legacyClosedGroup ||
self.viewModel.threadData.threadVariant == .closedGroup ||
self.viewModel.threadData.threadVariant == .openGroup
self.viewModel.threadData.threadVariant == .legacyGroup ||
self.viewModel.threadData.threadVariant == .group ||
self.viewModel.threadData.threadVariant == .community
),
let allMessages: [MessageViewModel] = self.viewModel.interactionData
.first(where: { $0.model == .messages })?
@ -1173,10 +1173,10 @@ extension ConversationVC:
}
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) {
guard cellViewModel.threadVariant == .openGroup else { return }
guard cellViewModel.threadVariant == .community else { return }
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
let openGroup: OpenGroup = try? OpenGroup
.fetchOne(db, id: cellViewModel.threadId),
@ -1185,10 +1185,7 @@ extension ConversationVC:
.filter(id: cellViewModel.id)
.asRequest(of: Int64.self)
.fetchOne(db)
else {
return Fail(error: StorageError.objectNotFound)
.eraseToAnyPublisher()
}
else { throw StorageError.objectNotFound }
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
.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
// Perform the sending logic
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 {
return Just(nil)
.setFailureType(to: Error.self)
@ -1360,10 +1357,7 @@ extension ConversationVC:
.filter(id: cellViewModel.id)
.asRequest(of: Int64.self)
.fetchOne(db)
else {
return Fail(error: MessageSenderError.invalidMessage)
.eraseToAnyPublisher()
}
else { throw MessageSenderError.invalidMessage }
let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager
.addPendingReaction(
@ -1552,7 +1546,7 @@ extension ConversationVC:
}
Storage.shared
.writePublisherFlatMap { db in
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db in
OpenGroupManager.shared.add(
db,
roomToken: room,
@ -1674,9 +1668,11 @@ extension ConversationVC:
// Remote deletion logic
func deleteRemotely(from viewController: UIViewController?, request: AnyPublisher<Void, Error>, onComplete: (() -> ())?) {
// Show a loading indicator
Future<Void, Error> { resolver in
ModalActivityIndicatorViewController.present(fromViewController: viewController, canCancel: false) { _ in
resolver(Result.success(()))
Deferred {
Future<Void, Error> { resolver in
ModalActivityIndicatorViewController.present(fromViewController: viewController, canCancel: false) { _ in
resolver(Result.success(()))
}
}
}
.flatMap { _ in request }
@ -1707,7 +1703,7 @@ extension ConversationVC:
// How we delete the message differs depending on the type of thread
switch cellViewModel.threadVariant {
// Handle open group messages the old way
case .openGroup:
case .community:
// 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
(
@ -1786,7 +1782,7 @@ extension ConversationVC:
// Delete the message from the open group
deleteRemotely(
from: self,
request: Storage.shared.readPublisherFlatMap { db in
request: Storage.shared.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
OpenGroupAPI.messageDelete(
db,
id: openGroupServerMessageId,
@ -1800,7 +1796,7 @@ extension ConversationVC:
self?.showInputAccessoryView()
}
case .contact, .legacyClosedGroup, .closedGroup:
case .contact, .legacyGroup, .group:
let serverHash: String? = Storage.shared.read { db -> String? in
try Interaction
.select(.serverHash)
@ -1859,7 +1855,7 @@ extension ConversationVC:
})
actionSheet.addAction(UIAlertAction(
title: (cellViewModel.threadVariant == .legacyClosedGroup || cellViewModel.threadVariant == .closedGroup ?
title: (cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group ?
"delete_message_for_everyone".localized() :
String(format: "delete_message_for_me_and_recipient".localized(), threadName)
),
@ -1963,7 +1959,7 @@ extension ConversationVC:
}
func ban(_ cellViewModel: MessageViewModel) {
guard cellViewModel.threadVariant == .openGroup else { return }
guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId
let modal: ConfirmationModal = ConfirmationModal(
@ -1975,10 +1971,9 @@ extension ConversationVC:
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in
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 {
return Fail(error: StorageError.objectNotFound)
.eraseToAnyPublisher()
throw StorageError.objectNotFound
}
return OpenGroupAPI
@ -2020,7 +2015,7 @@ extension ConversationVC:
}
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) {
guard cellViewModel.threadVariant == .openGroup else { return }
guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId
let modal: ConfirmationModal = ConfirmationModal(
@ -2032,10 +2027,9 @@ extension ConversationVC:
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in
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 {
return Fail(error: StorageError.objectNotFound)
.eraseToAnyPublisher()
throw StorageError.objectNotFound
}
return OpenGroupAPI
@ -2300,7 +2294,7 @@ extension ConversationVC {
}
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
// 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)

View File

@ -107,7 +107,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
threadId: self.threadId,
threadVariant: self.initialThreadVariant,
threadIsNoteToSelf: (self.threadId == getUserHexEncodedPublicKey()),
currentUserIsClosedGroupMember: ((self.initialThreadVariant != .legacyClosedGroup && self.initialThreadVariant != .closedGroup) ?
currentUserIsClosedGroupMember: ((self.initialThreadVariant != .legacyGroup && self.initialThreadVariant != .group) ?
nil :
Storage.shared.read { db in
GroupMember
@ -342,7 +342,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
.read { db -> [MentionInfo] in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
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 :
try? Capability
.select(.variant)

View File

@ -306,18 +306,18 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
)
)
let isGroupThread: Bool = (
cellViewModel.threadVariant == .openGroup ||
cellViewModel.threadVariant == .legacyClosedGroup ||
cellViewModel.threadVariant == .closedGroup
cellViewModel.threadVariant == .community ||
cellViewModel.threadVariant == .legacyGroup ||
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)
profilePictureViewWidthConstraint.constant = (isGroupThread ? VisibleMessageCell.profilePictureSize : 0)
profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil)
profilePictureView.update(
publicKey: cellViewModel.authorId,
threadVariant: cellViewModel.threadVariant,
threadVariant: .contact, // Should always be '.contact'
customImageData: nil,
profile: cellViewModel.profile,
additionalProfile: nil
@ -710,9 +710,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
maxWidth: maxWidth,
showingAllReactions: showExpandedReactions,
showNumbers: (
cellViewModel.threadVariant == .legacyClosedGroup ||
cellViewModel.threadVariant == .closedGroup ||
cellViewModel.threadVariant == .openGroup
cellViewModel.threadVariant == .legacyGroup ||
cellViewModel.threadVariant == .group ||
cellViewModel.threadVariant == .community
)
)
}
@ -860,7 +860,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
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
guard cellViewModel.threadVariant != .openGroup else {
guard cellViewModel.threadVariant != .community else {
guard SessionId.Prefix(from: cellViewModel.authorId) == .blinded else { return }
delegate?.startThread(
@ -1070,9 +1070,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
case .standardIncoming, .standardIncomingDeleted:
let isGroupThread = (
cellViewModel.threadVariant == .openGroup ||
cellViewModel.threadVariant == .legacyClosedGroup ||
cellViewModel.threadVariant == .closedGroup
cellViewModel.threadVariant == .community ||
cellViewModel.threadVariant == .legacyGroup ||
cellViewModel.threadVariant == .group
)
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)

View File

@ -180,7 +180,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
override var title: String {
switch threadVariant {
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))
let currentUserIsClosedGroupMember: Bool = (
(
threadVariant == .legacyClosedGroup ||
threadVariant == .closedGroup
threadVariant == .legacyGroup ||
threadVariant == .group
) &&
threadViewModel.currentUserIsClosedGroupMember == true
)
@ -307,14 +307,14 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
SectionModel(
model: .content,
elements: [
(threadVariant == .legacyClosedGroup || threadVariant == .closedGroup ? nil :
(threadVariant == .legacyGroup || threadVariant == .group ? nil :
SessionCell.Info(
id: .copyThreadId,
leftAccessory: .icon(
UIImage(named: "ic_copy")?
.withRenderingMode(.alwaysTemplate)
),
title: (threadVariant == .openGroup ?
title: (threadVariant == .community ?
"COPY_GROUP_URL".localized() :
"vc_conversation_settings_copy_session_id_button_title".localized()
),
@ -324,10 +324,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
onTap: {
switch threadVariant {
case .contact, .legacyClosedGroup, .closedGroup:
case .contact, .legacyGroup, .group:
UIPasteboard.general.string = threadId
case .openGroup:
case .community:
guard
let server: String = threadViewModel.openGroupServer,
let roomToken: String = threadViewModel.openGroupRoomToken,
@ -387,7 +387,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
}
),
(threadVariant != .openGroup ? nil :
(threadVariant != .community ? nil :
SessionCell.Info(
id: .addToOpenGroup,
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(
id: .disappearingMessages,
leftAccessory: .icon(
@ -495,7 +495,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
onTap: { [weak self] in
dependencies.storage
.writePublisherFlatMap { db in
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
MessageSender.leave(db, groupPublicKey: threadId)
}
.sinkUntilComplete()
@ -538,8 +538,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
isEnabled: (
(
threadViewModel.threadVariant != .legacyClosedGroup &&
threadViewModel.threadVariant != .closedGroup
threadViewModel.threadVariant != .legacyGroup &&
threadViewModel.threadVariant != .group
) ||
currentUserIsClosedGroupMember
),
@ -576,8 +576,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
),
isEnabled: (
(
threadViewModel.threadVariant != .legacyClosedGroup &&
threadViewModel.threadVariant != .closedGroup
threadViewModel.threadVariant != .legacyGroup &&
threadViewModel.threadVariant != .group
) ||
currentUserIsClosedGroupMember
),

View File

@ -168,12 +168,12 @@ final class ConversationTitleView: UIView {
switch threadVariant {
case .contact: break
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
subtitleLabel?.attributedText = NSAttributedString(
string: "\(userCount) member\(userCount == 1 ? "" : "s")"
)
case .openGroup:
case .community:
subtitleLabel?.attributedText = NSAttributedString(
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
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
try Contact
.filter(id: threadViewModel.threadId)
.updateAllAndConfig(

View File

@ -95,7 +95,7 @@ public class HomeViewModel {
joinToPagedType: {
let profile: TypedTableAlias<Profile> = 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
return SQL("""
@ -367,12 +367,12 @@ public class HomeViewModel {
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
Storage.shared.writeAsync { db in
switch threadVariant {
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
MessageSender
.leave(db, groupPublicKey: threadId)
.sinkUntilComplete()
case .openGroup:
case .community:
OpenGroupManager.shared.delete(db, openGroupId: threadId)
default: break

View File

@ -445,7 +445,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
return UISwipeActionsConfiguration(actions: [ delete, block ])
case .legacyClosedGroup, .closedGroup, .openGroup:
case .legacyGroup, .group, .community:
return UISwipeActionsConfiguration(actions: [ delete ])
}
@ -469,7 +469,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
let closedGroupThreadIds: [String] = (viewModel.threadData
.first { $0.model == .threads }?
.elements
.filter { $0.threadVariant == .legacyClosedGroup || $0.threadVariant == .closedGroup }
.filter { $0.threadVariant == .legacyGroup || $0.threadVariant == .group }
.map { $0.threadId })
.defaulting(to: [])
let alertVC: UIAlertController = UIAlertController(

View File

@ -186,12 +186,12 @@ public class MessageRequestsViewModel {
) { _ in
Storage.shared.write { db in
switch threadVariant {
case .contact, .openGroup:
case .contact, .community:
_ = try SessionThread
.filter(id: threadId)
.deleteAll(db)
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
try ClosedGroup.removeKeysAndUnsubscribe(
db,
threadId: threadId,

View File

@ -346,17 +346,14 @@ enum GiphyAPI {
// URLError codes are negative values
return HTTPError.generic
}
.flatMap { data, _ -> AnyPublisher<[GiphyImageInfo], Error> in
.tryMap { data, _ -> [GiphyImageInfo] in
Logger.error("search request succeeded")
guard let imageInfos = self.parseGiphyImages(responseData: data) else {
return Fail(error: HTTPError.invalidResponse)
.eraseToAnyPublisher()
throw HTTPError.invalidResponse
}
return Just(imageInfos)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return imageInfos
}
.eraseToAnyPublisher()
}

View File

@ -86,26 +86,17 @@ class PhotoCapture: NSObject {
return Just(())
.subscribe(on: sessionQueue)
.setFailureType(to: Error.self)
.flatMap { [weak self] _ -> AnyPublisher<Void, Error> in
.tryMap { [weak self] _ -> Void in
self?.session.beginConfiguration()
defer { self?.session.commitConfiguration() }
do {
try self?.updateCurrentInput(position: .back)
}
catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
try self?.updateCurrentInput(position: .back)
guard let photoOutput = self?.captureOutput.photoOutput else {
return Fail(error: PhotoCaptureError.initializationFailed)
.eraseToAnyPublisher()
}
guard self?.session.canAddOutput(photoOutput) == true else {
return Fail(error: PhotoCaptureError.initializationFailed)
.eraseToAnyPublisher()
guard
let photoOutput = self?.captureOutput.photoOutput,
self?.session.canAddOutput(photoOutput) == true
else {
throw PhotoCaptureError.initializationFailed
}
if let connection = photoOutput.connection(with: .video) {
@ -130,9 +121,7 @@ class PhotoCapture: NSObject {
}
}
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return ()
}
.handleEvents(
receiveCompletion: { [weak self] result in
@ -172,21 +161,12 @@ class PhotoCapture: NSObject {
return Just(())
.setFailureType(to: Error.self)
.subscribe(on: sessionQueue)
.flatMap { [weak self, newPosition = self.desiredPosition] _ -> AnyPublisher<Void, Error> in
.tryMap { [weak self, newPosition = self.desiredPosition] _ -> Void in
self?.session.beginConfiguration()
defer { self?.session.commitConfiguration() }
do {
try self?.updateCurrentInput(position: newPosition)
}
catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
try self?.updateCurrentInput(position: newPosition)
return ()
}
.eraseToAnyPublisher()
}

View File

@ -137,64 +137,68 @@ class PhotoCollectionContents {
}
private func requestImageDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: DataSource, dataUTI: String), Error> {
return Future { [weak self] resolver in
let options: PHImageRequestOptions = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
_ = 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")))
return
return Deferred {
Future { [weak self] resolver in
let options: PHImageRequestOptions = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
_ = 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")))
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()
}
private func requestVideoDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: DataSource, dataUTI: String), Error> {
return Future { [weak self] resolver in
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
_ = 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")))
return
}
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 Deferred {
Future { [weak self] resolver in
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
_ = 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")))
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 {
func registerNotificationSettings() -> Future<Void, Never>
func registerNotificationSettings() -> AnyPublisher<Void, Never>
func notify(
category: AppNotificationCategory,
@ -150,7 +150,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
func registerNotificationSettings() -> AnyPublisher<Void, Never> {
return adaptee.registerNotificationSettings()
.eraseToAnyPublisher()
}
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
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.
@ -203,7 +202,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
case .contact:
notificationTitle = (isMessageRequest ? "Session" : senderName)
case .legacyClosedGroup, .closedGroup, .openGroup:
case .legacyGroup, .group, .community:
notificationTitle = String(
format: NotificationStrings.incomingGroupMessageTitleFormat,
senderName,
@ -275,9 +274,9 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
// No call notifications for muted or group threads
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
guard
thread.variant != .legacyClosedGroup &&
thread.variant != .closedGroup &&
thread.variant != .openGroup
thread.variant != .legacyGroup &&
thread.variant != .group &&
thread.variant != .community
else { return }
guard
interaction.variant == .infoCall,
@ -347,9 +346,9 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
// No reaction notifications for muted, group threads or message requests
guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return }
guard
thread.variant != .legacyClosedGroup &&
thread.variant != .closedGroup &&
thread.variant != .openGroup
thread.variant != .legacyGroup &&
thread.variant != .group &&
thread.variant != .community
else { return }
guard !isMessageRequest else { return }
@ -539,7 +538,7 @@ class NotificationActionHandler {
}
return Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: DispatchQueue.main) { db in
let interaction: Interaction = try Interaction(
threadId: thread.id,
authorId: getUserHexEncodedPublicKey(db),
@ -607,7 +606,7 @@ class NotificationActionHandler {
private func markAsRead(thread: SessionThread) -> AnyPublisher<Void, Error> {
return Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
try Interaction.markAsRead(
db,
interactionId: try thread.interactions

View File

@ -54,10 +54,9 @@ public enum PushRegistrationError: Error {
return registerUserNotificationSettings()
.setFailureType(to: Error.self)
.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)
return Fail(error: PushRegistrationError.pushNotSupported(description: "Push not supported on simulators"))
.eraseToAnyPublisher()
throw PushRegistrationError.pushNotSupported(description: "Push not supported on simulators")
#endif
return self.registerForVanillaPushToken()
@ -101,7 +100,6 @@ public enum PushRegistrationError: Error {
public func registerUserNotificationSettings() -> AnyPublisher<Void, Never> {
AssertIsOnMainThread()
return notificationPresenter.registerNotificationSettings()
.eraseToAnyPublisher()
}
/**
@ -142,8 +140,10 @@ public enum PushRegistrationError: Error {
UIApplication.shared.registerForRemoteNotifications()
// No pending vanilla token yet; create a new publisher
let publisher: AnyPublisher<Data, Error> = Future<Data, Error> { self.vanillaTokenResolver = $0 }
.eraseToAnyPublisher()
let publisher: AnyPublisher<Data, Error> = Deferred {
Future<Data, Error> { self.vanillaTokenResolver = $0 }
}
.eraseToAnyPublisher()
self.vanillaTokenPublisher = publisher
return publisher
@ -238,8 +238,10 @@ public enum PushRegistrationError: Error {
}
// No pending voip token yet. Create a new publisher
let publisher: AnyPublisher<Data?, Error> = Future<Data?, Error> { self.voipTokenResolver = $0 }
.eraseToAnyPublisher()
let publisher: AnyPublisher<Data?, Error> = Deferred {
Future<Data?, Error> { self.voipTokenResolver = $0 }
}
.eraseToAnyPublisher()
self.voipTokenPublisher = publisher
return publisher

View File

@ -73,14 +73,16 @@ public enum SyncPushTokensJob: JobExecutor {
.eraseToAnyPublisher()
}
return Future<Void, Error> { resolver in
SyncPushTokensJob.registerForPushNotifications(
pushToken: pushToken,
voipToken: voipToken,
isForcedUpdate: shouldUploadTokens,
success: { resolver(Result.success(())) },
failure: { resolver(Result.failure($0)) }
)
return Deferred {
Future<Void, Error> { resolver in
SyncPushTokensJob.registerForPushNotifications(
pushToken: pushToken,
voipToken: voipToken,
isForcedUpdate: shouldUploadTokens,
success: { resolver(Result.success(())) },
failure: { resolver(Result.failure($0)) }
)
}
}
.handleEvents(
receiveCompletion: { result in

View File

@ -73,25 +73,27 @@ class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelega
}
extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
func registerNotificationSettings() -> Future<Void, Never> {
return Future { [weak self] resolver in
self?.notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
self?.notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories)
if granted {}
else if let error: Error = error {
Logger.error("failed with error: \(error)")
func registerNotificationSettings() -> AnyPublisher<Void, Never> {
return Deferred {
Future { [weak self] resolver in
self?.notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
self?.notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories)
if granted {}
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(
@ -114,7 +116,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier)
let shouldGroupNotification: Bool = (
threadVariant == .openGroup &&
threadVariant == .community &&
replacingIdentifier == threadIdentifier
)
let isAppActive = UIApplication.shared.applicationState == .active

View File

@ -23,11 +23,8 @@ enum Onboarding {
return Atomic(
SnodeAPI.getSwarm(for: userPublicKey)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { swarm -> AnyPublisher<Void, Error> in
guard let snode = swarm.randomElement() else {
return Fail(error: SnodeAPIError.generic)
.eraseToAnyPublisher()
}
.tryFlatMap { swarm -> AnyPublisher<Void, Error> in
guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic }
return CurrentUserPoller.poll(
namespaces: [.configUserProfile],
@ -41,7 +38,7 @@ enum Onboarding {
)
}
.flatMap { _ -> AnyPublisher<String?, Error> in
Storage.shared.readPublisher { db in
Storage.shared.readPublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
try Profile
.filter(id: userPublicKey)
.select(.name)

View File

@ -169,7 +169,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in
Storage.shared
.writePublisherFlatMap { db in
.writePublisherFlatMap(receiveOn: DispatchQueue.main) { db in
OpenGroupManager.shared.add(
db,
roomToken: roomToken,
@ -194,7 +194,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
if shouldOpenCommunity {
SessionApp.presentConversation(
for: OpenGroup.idFor(roomToken: roomToken, server: server),
threadVariant: .openGroup,
threadVariant: .community,
isMessageRequest: false,
action: .compose,
focusInteractionInfo: nil,

View File

@ -322,7 +322,7 @@ extension OpenGroupSuggestionGrid {
Publishers
.MergeMany(
Storage.shared
.readPublisherFlatMap { db in
.readPublisherFlatMap(receiveOn: DispatchQueue.main) { db in
OpenGroupManager
.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)
.defaulting(to: "")
OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)")
OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion), App: \(version), libSession: \(SessionUtil.libSessionVersion)")
DDLog.flushLog()
let logFilePaths: [String] = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths

View File

@ -352,14 +352,14 @@ public final class FullConversationCell: UITableViewCell {
}
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
ThemeManager.onThemeChange(observer: displayNameLabel) { [weak self, weak snippetLabel] theme, _ in
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(
content: (cellViewModel.threadMemberNames ?? ""),
currentUserPublicKey: cellViewModel.currentUserPublicKey,
@ -409,9 +409,9 @@ public final class FullConversationCell: UITableViewCell {
)
hasMentionView.isHidden = !(
((cellViewModel.threadUnreadMentionCount ?? 0) > 0) && (
cellViewModel.threadVariant == .legacyClosedGroup ||
cellViewModel.threadVariant == .closedGroup ||
cellViewModel.threadVariant == .openGroup
cellViewModel.threadVariant == .legacyGroup ||
cellViewModel.threadVariant == .group ||
cellViewModel.threadVariant == .community
)
)
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)
result.append(NSAttributedString(

View File

@ -67,11 +67,8 @@ public final class BackgroundPoller {
return SnodeAPI.getSwarm(for: userPublicKey)
.subscribeOnMain(immediately: true)
.receiveOnMain(immediately: true)
.flatMap { swarm -> AnyPublisher<Void, Error> in
guard let snode = swarm.randomElement() else {
return Fail(error: SnodeAPIError.generic)
.eraseToAnyPublisher()
}
.tryFlatMap { swarm -> AnyPublisher<Void, Error> in
guard let snode = swarm.randomElement() else { throw SnodeAPIError.generic }
return CurrentUserPoller.poll(
namespaces: CurrentUserPoller.namespaces,
@ -104,10 +101,9 @@ public final class BackgroundPoller {
SnodeAPI.getSwarm(for: groupPublicKey)
.subscribeOnMain(immediately: true)
.receiveOnMain(immediately: true)
.flatMap { swarm -> AnyPublisher<Void, Error> in
.tryFlatMap { swarm -> AnyPublisher<Void, Error> in
guard let snode: Snode = swarm.randomElement() else {
return Fail(error: OnionRequestAPIError.insufficientSnodes)
.eraseToAnyPublisher()
throw OnionRequestAPIError.insufficientSnodes
}
return ClosedGroupPoller.poll(

View File

@ -241,7 +241,7 @@ enum MockDataGenerator {
}
let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .legacyClosedGroup)
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .legacyGroup)
.with(shouldBeVisible: true)
.saved(db)
_ = try! ClosedGroup(
@ -367,7 +367,7 @@ enum MockDataGenerator {
// Create the open group model and the thread
let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .openGroup)
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .community)
.with(shouldBeVisible: true)
.saved(db)
_ = try! OpenGroup(

View File

@ -163,48 +163,50 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
.eraseToAnyPublisher()
}
return Future<Void, Error> { [weak self] resolver in
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
if let error = error {
return
}
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
preconditionFailure()
}
self?.peerConnection?.setLocalDescription(sdp) { error in
return Deferred {
Future<Void, Error> { [weak self] resolver in
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
if let error = error {
print("Couldn't initiate call due to error: \(error).")
resolver(Result.failure(error))
return
}
}
Storage.shared
.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
)
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
preconditionFailure()
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
}
self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't initiate call due to error: \(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()
@ -216,10 +218,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
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 {
return Fail(error: WebRTCSessionError.noThread)
.eraseToAnyPublisher()
throw WebRTCSessionError.noThread
}
return Just(thread)
@ -246,7 +247,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
}
Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
try MessageSender
.preparedSendData(
db,
@ -293,10 +294,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
self.queuedICECandidates.removeAll()
Storage.shared
.writePublisherFlatMap { db in
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else {
return Fail(error: WebRTCSessionError.noThread)
.eraseToAnyPublisher()
throw WebRTCSessionError.noThread
}
SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")

View File

@ -565,7 +565,7 @@ enum _003_YDBToGRDBMigration: Migration {
switch legacyThread {
case let groupThread as SMKLegacy._GroupThread:
threadVariant = (groupThread.isOpenGroup ? .openGroup : .legacyClosedGroup)
threadVariant = (groupThread.isOpenGroup ? .community : .legacyGroup)
onlyNotifyForMentions = groupThread.isOnlyNotifyingForMentions
default:

View File

@ -1036,7 +1036,7 @@ extension Attachment {
let attachmentId: String = self.id
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
// 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
@ -1068,8 +1068,7 @@ extension Attachment {
if destination.shouldEncrypt {
guard let ciphertext = Cryptography.encryptAttachmentData(data, shouldPad: true, outKey: &encryptionKey, outDigest: &digest) else {
SNLog("Couldn't encrypt attachment.")
return Fail(error: AttachmentError.encryptionFailed)
.eraseToAnyPublisher()
throw AttachmentError.encryptionFailed
}
data = ciphertext
@ -1077,10 +1076,7 @@ extension Attachment {
// Check the file size
SNLog("File size: \(data.count) bytes.")
if data.count > FileServerAPI.maxFileSize {
return Fail(error: HTTPError.maxFileSizeExceeded)
.eraseToAnyPublisher()
}
if data.count > FileServerAPI.maxFileSize { throw HTTPError.maxFileSizeExceeded }
// Update the attachment to the 'uploading' state
_ = try? Attachment
@ -1131,13 +1127,14 @@ extension Attachment {
.eraseToAnyPublisher()
}
}
.receive(on: queue)
.flatMap { fileId, encryptionKey, digest -> AnyPublisher<String?, Error> in
/// Save the final upload info
///
/// **Note:** We **MUST** use the `.with` function here to ensure the `isValid` flag is
/// updated correctly
Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: queue) { db in
try self
.with(
serverId: fileId,

View File

@ -353,7 +353,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
state: .sending
).insert(db)
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
let closedGroupMemberIds: Set<String> = (try? GroupMember
.select(.profileId)
.filter(GroupMember.Columns.groupId == threadId)
@ -379,7 +379,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
).insert(db)
}
case .openGroup:
case .community:
// 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 just use the open group id as the 'recipientId' value

View File

@ -318,17 +318,12 @@ public extension LinkPreview {
.flatMap { data, response in
parseLinkDataAndBuildDraft(linkData: data, response: response, linkUrlString: previewUrl)
}
.flatMap { linkPreviewDraft -> AnyPublisher<LinkPreviewDraft, Error> in
guard linkPreviewDraft.isValid() else {
return Fail(error: LinkPreviewError.noPreview)
.eraseToAnyPublisher()
}
.tryMap { linkPreviewDraft -> LinkPreviewDraft in
guard linkPreviewDraft.isValid() else { throw LinkPreviewError.noPreview }
setCachedLinkPreview(linkPreviewDraft, forPreviewUrl: previewUrl)
return Just(linkPreviewDraft)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return linkPreviewDraft
}
.eraseToAnyPublisher()
}
@ -362,25 +357,18 @@ public extension LinkPreview {
.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.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 {
return Fail(error: LinkPreviewError.assertionFailure)
.eraseToAnyPublisher()
throw LinkPreviewError.assertionFailure
}
if let contentType: String = urlResponse.allHeaderFields["Content-Type"] as? String {
guard contentType.lowercased().hasPrefix("text/") else {
return Fail(error: LinkPreviewError.invalidContent)
.eraseToAnyPublisher()
throw LinkPreviewError.invalidContent
}
}
guard data.count > 0 else {
return Fail(error: LinkPreviewError.invalidContent)
.eraseToAnyPublisher()
}
guard data.count > 0 else { throw LinkPreviewError.invalidContent }
return Just((data, response))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return (data, response)
}
.catch { error -> AnyPublisher<(Data, URLResponse), Error> in
guard isRetryable(error: error), remainingRetries > 0 else {
@ -496,63 +484,44 @@ public extension LinkPreview {
priority: .high,
shouldIgnoreSignalProxy: true
)
.flatMap { asset, _ -> AnyPublisher<Data, Error> in
do {
let imageSize = NSData.imageSize(forFilePath: asset.filePath, mimeType: imageMimeType)
guard imageSize.width > 0, imageSize.height > 0 else {
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()
.tryMap { asset, _ -> Data in
let imageSize = NSData.imageSize(forFilePath: asset.filePath, mimeType: imageMimeType)
guard imageSize.width > 0, imageSize.height > 0 else {
throw LinkPreviewError.invalidContent
}
catch {
return Fail(error: LinkPreviewError.assertionFailure)
.eraseToAnyPublisher()
guard let data: Data = try? Data(contentsOf: URL(fileURLWithPath: asset.filePath)) else {
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 }
.eraseToAnyPublisher()

View File

@ -324,9 +324,9 @@ public extension Profile {
}
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,
// 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)))"

View File

@ -37,9 +37,9 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible {
case contact
case legacyClosedGroup
case openGroup
case closedGroup
case legacyGroup
case community
case group
}
/// Unique identifier for a thread (formerly known as uniqueId)
@ -312,8 +312,8 @@ public extension SessionThread {
profile: Profile? = nil
) -> String {
switch variant {
case .legacyClosedGroup, .closedGroup: return (closedGroupName ?? "Unknown Group")
case .openGroup: return (openGroupName ?? "Unknown Group")
case .legacyGroup, .group: return (closedGroupName ?? "Unknown Group")
case .community: return (openGroupName ?? "Unknown Community")
case .contact:
guard !isNoteToSelf else { return "NOTE_TO_SELF".localized() }
guard let profile: Profile = profile else {
@ -329,7 +329,7 @@ public extension SessionThread {
threadVariant: Variant
) -> String? {
guard
threadVariant == .openGroup,
threadVariant == .community,
let blindingInfo: (edkeyPair: Box.KeyPair?, publicKey: String?) = Storage.shared.read({ db in
return (
Identity.fetchUserEd25519KeyPair(db),

View File

@ -20,7 +20,7 @@ public struct ConfigDump: Codable, Equatable, Hashable, FetchableRecord, Persist
case userProfile
case contacts
case convoInfoVolatile
case groups
case userGroups
}
/// The type of config this dump is for
@ -66,14 +66,14 @@ public extension ConfigDump {
}
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 {
switch self {
case .userProfile: return .userProfile
case .contacts: return .contacts
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 .contacts: return SnodeAPI.Namespace.configContacts
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)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { _, response -> AnyPublisher<Data, Error> in
guard let response: Data = response else {
return Fail(error: HTTPError.parsingFailed)
.eraseToAnyPublisher()
}
.tryMap { _, response -> Data in
guard let response: Data = response else { throw HTTPError.parsingFailed }
return Just(response)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return response
}
.eraseToAnyPublisher()
}

View File

@ -87,17 +87,14 @@ public enum AttachmentDownloadJob: JobExecutor {
Just(attachment.downloadUrl)
.setFailureType(to: Error.self)
.flatMap { maybeDownloadUrl -> AnyPublisher<Data, Error> in
.tryFlatMap { maybeDownloadUrl -> AnyPublisher<Data, Error> in
guard
let downloadUrl: String = maybeDownloadUrl,
let fileId: String = Attachment.fileId(for: downloadUrl)
else {
return Fail(error: AttachmentDownloadError.invalidUrl)
.eraseToAnyPublisher()
}
else { throw AttachmentDownloadError.invalidUrl }
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
guard let openGroup: OpenGroup = maybeOpenGroup else {
return FileServerAPI
@ -109,7 +106,7 @@ public enum AttachmentDownloadJob: JobExecutor {
}
return Storage.shared
.readPublisherFlatMap { db in
.readPublisherFlatMap(receiveOn: queue) { db in
OpenGroupAPI
.downloadFile(
db,
@ -123,41 +120,34 @@ public enum AttachmentDownloadJob: JobExecutor {
}
.eraseToAnyPublisher()
}
.flatMap { data -> AnyPublisher<Void, Error> in
do {
// Store the encrypted data temporarily
try data.write(to: temporaryFileUrl, options: .atomic)
// Decrypt the data
let plaintext: Data = try {
guard
let key: Data = attachment.encryptionKey,
let digest: Data = attachment.digest,
key.count > 0,
digest.count > 0
else { return data } // Open group attachments are unencrypted
return try Cryptography.decryptAttachment(
data,
withKey: key,
digest: digest,
unpaddedSize: UInt32(attachment.byteCount)
)
}()
// Write the data to disk
guard try attachment.write(data: plaintext) else {
throw AttachmentDownloadError.failedToSaveFile
}
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
catch {
return Fail(error: error)
.eraseToAnyPublisher()
.receive(on: queue)
.tryMap { data -> Void in
// Store the encrypted data temporarily
try data.write(to: temporaryFileUrl, options: .atomic)
// Decrypt the data
let plaintext: Data = try {
guard
let key: Data = attachment.encryptionKey,
let digest: Data = attachment.digest,
key.count > 0,
digest.count > 0
else { return data } // Open group attachments are unencrypted
return try Cryptography.decryptAttachment(
data,
withKey: key,
digest: digest,
unpaddedSize: UInt32(attachment.byteCount)
)
}()
// Write the data to disk
guard try attachment.write(data: plaintext) else {
throw AttachmentDownloadError.failedToSaveFile
}
return ()
}
.sinkUntilComplete(
receiveCompletion: { result in

View File

@ -39,18 +39,7 @@ public enum ConfigurationSyncJob: JobExecutor {
// fresh install due to the migrations getting run)
guard
let pendingSwarmConfigChanges: [SingleDestinationChanges] = Storage.shared
.read({ db -> [SessionUtil.OutgoingConfResult]? in
guard
Identity.userExists(db),
let ed25519SecretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey
else { return nil }
return try SessionUtil.pendingChanges(
db,
userPublicKey: getUserHexEncodedPublicKey(db),
ed25519SecretKey: ed25519SecretKey
)
})?
.read({ db in try SessionUtil.pendingChanges(db) })?
.grouped(by: { $0.destination })
.map({ (destination: Message.Destination, value: [SessionUtil.OutgoingConfResult]) -> SingleDestinationChanges in
SingleDestinationChanges(
@ -75,7 +64,7 @@ public enum ConfigurationSyncJob: JobExecutor {
}
Storage.shared
.readPublisher { db in
.readPublisher(receiveOn: queue) { db in
try pendingSwarmConfigChanges
.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
Publishers
.MergeMany(
@ -119,17 +106,17 @@ public enum ConfigurationSyncJob: JobExecutor {
.collect()
.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
// expected so if that happens fail and re-run later
guard responses.count == pendingSwarmConfigChanges.count else {
return Fail(error: HTTPError.invalidResponse)
.eraseToAnyPublisher()
throw HTTPError.invalidResponse
}
// Process the response data into an easy to understand for (this isn't strictly
// 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
let maybePublicKey: String? = {
switch pendingSwarmChange.destination {
@ -179,10 +166,6 @@ public enum ConfigurationSyncJob: JobExecutor {
}
}
.flatMap { $0 }
return Just(successfulChanges)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.map { (successfulChanges: [SuccessfulChange]) -> [ConfigDump] in
// 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
switch result {
case .finished: break
case .failure(let error):
failure(job, error, false)
case .failure(let error): failure(job, error, false)
}
},
receiveValue: { (configDumps: [ConfigDump]) in
@ -354,7 +336,7 @@ public extension ConfigurationSyncJob {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard Features.useSharedUtilForUserConfig else {
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
// 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)
@ -369,21 +351,21 @@ public extension ConfigurationSyncJob {
interactionId: nil
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.eraseToAnyPublisher()
}
// Trigger the job emitting the result when completed
return Future { resolver in
ConfigurationSyncJob.run(
Job(variant: .configurationSync),
queue: DispatchQueue.global(qos: .userInitiated),
success: { _, _ in resolver(Result.success(())) },
failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
deferred: { _ in }
)
return Deferred {
Future { resolver in
ConfigurationSyncJob.run(
Job(variant: .configurationSync),
queue: DispatchQueue.global(qos: .userInitiated),
success: { _, _ in resolver(Result.success(())) },
failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
deferred: { _ in }
)
}
}
.eraseToAnyPublisher()
}

View File

@ -84,7 +84,7 @@ public enum GarbageCollectionJob: JobExecutor {
SELECT \(interaction.alias[Column.rowID])
FROM \(Interaction.self)
JOIN \(SessionThread.self) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
\(thread[.id]) = \(interaction[.threadId])
)
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
/// so we shouldn't get here until attachments have already been uploaded
Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: queue) { db in
try MessageSender.preparedSendData(
db,
message: details.message,
@ -173,9 +173,9 @@ public enum MessageSendJob: JobExecutor {
interactionId: job.interactionId
)
}
.subscribe(on: queue)
.map { sendData in sendData.with(fileIds: messageFileIds) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.receive(on: queue)
.sinkUntilComplete(
receiveCompletion: { result in
switch result {

View File

@ -36,7 +36,7 @@ public enum SendReadReceiptsJob: JobExecutor {
}
Storage.shared
.writePublisher { db in
.writePublisher(receiveOn: queue) { db in
try MessageSender.preparedSendData(
db,
message: ReadReceipt(
@ -46,7 +46,6 @@ public enum SendReadReceiptsJob: JobExecutor {
interactionId: nil
)
}
.subscribe(on: queue)
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.receive(on: queue)
.sinkUntilComplete(
@ -120,9 +119,9 @@ public extension SendReadReceiptsJob {
.joining(
// Don't send read receipts in group threads
required: Interaction.thread
.filter(SessionThread.Columns.variant != SessionThread.Variant.legacyClosedGroup)
.filter(SessionThread.Columns.variant != SessionThread.Variant.closedGroup)
.filter(SessionThread.Columns.variant != SessionThread.Variant.openGroup)
.filter(SessionThread.Columns.variant != SessionThread.Variant.legacyGroup)
.filter(SessionThread.Columns.variant != SessionThread.Variant.group)
.filter(SessionThread.Columns.variant != SessionThread.Variant.community)
)
.distinct()
)

View File

@ -42,14 +42,17 @@ internal extension SessionUtil {
isBlocked: contact.blocked,
didApproveMe: contact.approved_me
)
let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true)
let profileResult: Profile = Profile(
id: contactId,
name: (contact.name.map { String(cString: $0) } ?? ""),
nickname: contact.nickname.map { String(cString: $0) },
profilePictureUrl: contact.profile_pic.url.map { String(cString: $0) },
profileEncryptionKey: (contact.profile_pic.key != nil && contact.profile_pic.keylen > 0 ?
Data(bytes: contact.profile_pic.key, count: contact.profile_pic.keylen) :
nil
name: (String(libSessionVal: contact.name) ?? ""),
nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true),
profilePictureUrl: profilePictureUrl,
profileEncryptionKey: (profilePictureUrl == nil ? nil :
Data(
libSessionVal: contact.profile_pic.key,
count: ProfileManager.avatarAES256KeyByteLength
)
)
)
@ -165,9 +168,7 @@ internal extension SessionUtil {
// Update the name
targetContacts
.forEach { (id, maybeContact, maybeProfile) in
var sessionId: [CChar] = id
.bytes
.map { CChar(bitPattern: $0) }
var sessionId: [CChar] = id.cArray
var contact: contacts_contact = contacts_contact()
guard contacts_get_or_construct(conf, &contact, &sessionId) else {
SNLog("Unable to upsert contact from Config Message")
@ -179,60 +180,33 @@ internal extension SessionUtil {
contact.approved = updatedContact.isApproved
contact.approved_me = updatedContact.didApproveMe
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 {
/// Users we have sent a message request to may not have profile info in certain situations
///
/// Note: We **MUST** store these in local variables rather than access them directly or they won't
/// exist in memory long enough to actually be assigned in the C type
let updatedName: [CChar]? = (updatedProfile.name.isEmpty ?
nil :
updatedProfile.name
.bytes
.map { CChar(bitPattern: $0) }
let oldAvatarUrl: String? = String(libSessionVal: contact.profile_pic.url)
let oldAvatarKey: Data? = Data(
libSessionVal: contact.profile_pic.key,
count: ProfileManager.avatarAES256KeyByteLength
)
let updatedNickname: [CChar]? = updatedProfile.nickname?
.bytes
.map { CChar(bitPattern: $0) }
let updatedAvatarUrl: [CChar]? = updatedProfile.profilePictureUrl?
.bytes
.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)
contact.name = updatedProfile.name.toLibSession()
contact.nickname = updatedProfile.nickname.toLibSession()
contact.profile_pic.url = updatedProfile.profilePictureUrl.toLibSession()
contact.profile_pic.key = updatedProfile.profileEncryptionKey.toLibSession()
// Download the profile picture if needed
if oldAvatarUrl != updatedProfile.profilePictureUrl || oldAvatarKey != updatedProfile.profileEncryptionKey {
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(

View File

@ -21,8 +21,8 @@ internal extension SessionUtil {
let volatileThreadInfo: [VolatileThreadInfo] = atomicConf.mutate { conf -> [VolatileThreadInfo] in
var volatileThreadInfo: [VolatileThreadInfo] = []
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var openGroup: convo_info_volatile_open = convo_info_volatile_open()
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
var community: convo_info_volatile_community = convo_info_volatile_community()
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
while !convo_info_volatile_iterator_done(convoIterator) {
@ -43,23 +43,23 @@ internal extension SessionUtil {
)
)
}
else if convo_info_volatile_it_is_open(convoIterator, &openGroup) {
let server: String = String(cString: withUnsafeBytes(of: openGroup.base_url) { [UInt8]($0) }
else if convo_info_volatile_it_is_community(convoIterator, &community) {
let server: String = String(cString: withUnsafeBytes(of: community.base_url) { [UInt8]($0) }
.map { CChar($0) }
.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) }
.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()
})
volatileThreadInfo.append(
VolatileThreadInfo(
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
variant: .openGroup,
variant: .community,
openGroupUrlInfo: VolatileThreadInfo.OpenGroupUrlInfo(
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
server: server,
@ -67,14 +67,14 @@ internal extension SessionUtil {
publicKey: publicKey
),
changes: [
.markedAsUnread(openGroup.unread),
.lastReadTimestampMs(openGroup.last_read)
.markedAsUnread(community.unread),
.lastReadTimestampMs(community.last_read)
]
)
)
}
else if convo_info_volatile_it_is_legacy_closed(convoIterator, &legacyClosedGroup) {
let groupId: String = String(cString: withUnsafeBytes(of: legacyClosedGroup.group_id) { [UInt8]($0) }
else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) {
let groupId: String = String(cString: withUnsafeBytes(of: legacyGroup.group_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
@ -82,10 +82,10 @@ internal extension SessionUtil {
volatileThreadInfo.append(
VolatileThreadInfo(
threadId: groupId,
variant: .legacyClosedGroup,
variant: .legacyGroup,
changes: [
.markedAsUnread(legacyClosedGroup.unread),
.lastReadTimestampMs(legacyClosedGroup.last_read)
.markedAsUnread(legacyGroup.unread),
.lastReadTimestampMs(legacyGroup.last_read)
]
)
)
@ -183,11 +183,13 @@ internal extension SessionUtil {
convoInfoVolatileChanges: [VolatileThreadInfo],
in atomicConf: Atomic<UnsafeMutablePointer<config_object>?>
) 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
// blocking access in it's `mutate` closure
return atomicConf.mutate { conf in
convoInfoVolatileChanges.forEach { threadInfo in
var cThreadId: [CChar] = threadInfo.cThreadId
var cThreadId: [CChar] = threadInfo.threadId.cArray
switch threadInfo.variant {
case .contact:
@ -209,10 +211,10 @@ internal extension SessionUtil {
}
convo_info_volatile_set_1to1(conf, &oneToOne)
case .legacyClosedGroup:
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
case .legacyGroup:
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")
return
}
@ -220,27 +222,27 @@ internal extension SessionUtil {
threadInfo.changes.forEach { change in
switch change {
case .lastReadTimestampMs(let lastReadMs):
legacyClosedGroup.last_read = lastReadMs
legacyGroup.last_read = lastReadMs
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
var cBaseUrl: [CChar] = threadInfo.cBaseUrl,
var cRoomToken: [CChar] = threadInfo.cRoomToken,
var cPubkey: [UInt8] = threadInfo.cPubkey
var cBaseUrl: [CChar] = threadInfo.openGroupUrlInfo?.server.cArray,
var cRoomToken: [CChar] = threadInfo.openGroupUrlInfo?.roomToken.cArray,
var cPubkey: [UInt8] = threadInfo.openGroupUrlInfo?.publicKey.bytes
else {
SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info")
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")
return
}
@ -248,15 +250,15 @@ internal extension SessionUtil {
threadInfo.changes.forEach { change in
switch change {
case .lastReadTimestampMs(let lastReadMs):
openGroup.last_read = lastReadMs
community.last_read = lastReadMs
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(
threadId: thread.id,
variant: thread.variant,
openGroupUrlInfo: (thread.variant != .openGroup ? nil :
openGroupUrlInfo: (thread.variant != .community ? nil :
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: thread.id)
),
changes: [.markedAsUnread(thread.markedAsUnread ?? false)]
@ -340,7 +342,7 @@ internal extension SessionUtil {
let change: VolatileThreadInfo = VolatileThreadInfo(
threadId: threadId,
variant: threadVariant,
openGroupUrlInfo: (threadVariant != .openGroup ? nil :
openGroupUrlInfo: (threadVariant != .community ? nil :
try VolatileThreadInfo.OpenGroupUrlInfo.fetchOne(db, id: threadId)
),
changes: [.lastReadTimestampMs(lastReadTimestampMs)]
@ -394,47 +396,36 @@ internal extension SessionUtil {
return atomicConf.mutate { conf in
switch threadVariant {
case .contact:
var cThreadId: [CChar] = threadId
.bytes
.map { CChar(bitPattern: $0) }
var cThreadId: [CChar] = threadId.cArray
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else { return false }
return (oneToOne.last_read > timestampMs)
case .legacyClosedGroup:
var cThreadId: [CChar] = threadId
.bytes
.map { CChar(bitPattern: $0) }
var legacyClosedGroup: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
case .legacyGroup:
var cThreadId: [CChar] = threadId.cArray
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
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 (legacyClosedGroup.last_read > timestampMs)
return (legacyGroup.last_read > timestampMs)
case .openGroup:
case .community:
guard let openGroup: OpenGroup = openGroup else { return false }
var cBaseUrl: [CChar] = openGroup.server
.bytes
.map { CChar(bitPattern: $0) }
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()
var cBaseUrl: [CChar] = openGroup.server.cArray
var cRoomToken: [CChar] = openGroup.roomToken.cArray
var convoCommunity: convo_info_volatile_community = convo_info_volatile_community()
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 (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 variant: SessionThread.Variant
private let openGroupUrlInfo: OpenGroupUrlInfo?
fileprivate let openGroupUrlInfo: OpenGroupUrlInfo?
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(
threadId: String,
variant: SessionThread.Variant,

View File

@ -31,23 +31,18 @@ internal extension SessionUtil {
let profileName: String = String(cString: profileNamePtr)
let profilePic: user_profile_pic = user_profile_get_pic(conf)
var profilePictureUrl: String? = nil
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)
}
let profilePictureUrl: String? = String(libSessionVal: profilePic.url, nullIfEmpty: true)
// Make sure the url and key exists before reading the memory
return (
profileName: profileName,
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
return atomicConf.mutate { conf in
// Update the name
var updatedName: [CChar] = profile.name
.bytes
.map { CChar(bitPattern: $0) }
var updatedName: [CChar] = profile.name.cArray
user_profile_set_name(conf, &updatedName)
// Either assign the updated profile pic, or sent a blank profile pic (to remove the current one)
let profilePic: user_profile_pic? = {
guard
let profilePictureUrl: String = profile.profilePictureUrl,
let profileEncryptionKey: Data = profile.profileEncryptionKey
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()))
var profilePic: user_profile_pic = user_profile_pic()
profilePic.url = profile.profilePictureUrl.toLibSession()
profilePic.key = profile.profileEncryptionKey.toLibSession()
user_profile_set_pic(conf, profilePic)
return ConfResult(
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
public static func loadState(
@ -133,6 +135,9 @@ public enum SessionUtil {
case .convoInfoVolatile:
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
public static func pendingChanges(
_ db: Database,
userPublicKey: String,
ed25519SecretKey: [UInt8]
) throws -> [OutgoingConfResult] {
public static func pendingChanges(_ db: Database) throws -> [OutgoingConfResult] {
guard Identity.userExists(db) else { throw SessionUtilError.userDoesNotExist }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let existingDumpInfo: Set<DumpInfo> = try ConfigDump
.select(.variant, .publicKey, .combinedMessageHashes)
.asRequest(of: DumpInfo.self)
@ -287,6 +291,20 @@ public enum SessionUtil {
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
public static func handleConfigMessages(
@ -373,7 +391,7 @@ public enum SessionUtil {
mergeResult: mergeResult.result
)
case .groups:
case .userGroups:
return try SessionUtil.handleGroupsUpdate(
db,
in: atomicConf,

View File

@ -5,4 +5,5 @@ import Foundation
public enum SessionUtilError: Error {
case unableToCreateConfigObject
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>
<key>AvailableLibraries</key>
<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>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
@ -19,18 +31,6 @@
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</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>
<key>CFBundlePackageType</key>
<string>XFWK</string>

View File

@ -1,8 +1,11 @@
module SessionUtil {
module capi {
header "session/version.h"
header "session/export.h"
header "session/config.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/user_profile.h"
header "session/config/util.h"

View File

@ -225,7 +225,9 @@ class ConfigMessage {
// Constructor tag
struct increment_seqno_t {};
struct retain_seqno_t {};
inline constexpr increment_seqno_t increment_seqno{};
inline constexpr retain_seqno_t retain_seqno{};
class MutableConfigMessage : public ConfigMessage {
protected:
@ -292,7 +294,14 @@ class MutableConfigMessage : public ConfigMessage {
/// 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).
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;
/// 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.
MutableConfigMessage& dirty();
public:
// 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
// foo["abc"]["def"]["ghi"] = 12;
@ -271,7 +272,7 @@ class ConfigBase {
std::string string_or(std::string fallback) const {
if (auto* s = string())
return *s;
return std::move(fallback);
return fallback;
}
/// 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
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
/// 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).
void operator=(std::string_view value) { *this = std::string{value}; }
/// 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
// 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
@ -429,6 +431,11 @@ class ConfigBase {
/// to use. This is rarely needed externally; it is public merely for testing purposes.
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
// 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
@ -463,13 +470,16 @@ class ConfigBase {
// 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
// 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
// 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
// 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
// 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
#include "base.h"
#include "expiring.h"
#include "profile_pic.h"
#include "util.h"
typedef struct contacts_contact {
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.
const char* name;
const char* nickname;
// These two will be 0-length strings when unset:
char name[101];
char nickname[101];
user_profile_pic profile_pic;
bool approved;
bool approved_me;
bool blocked;
bool hidden;
int priority;
CONVO_EXPIRATION_MODE exp_mode;
int exp_minutes;
} contacts_contact;

View File

@ -1,16 +1,20 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <iterator>
#include <memory>
#include <session/config.hpp>
#include "base.hpp"
#include "expiring.hpp"
#include "namespaces.hpp"
#include "profile_pic.hpp"
extern "C" struct contacts_contact;
using namespace std::literals;
namespace session::config {
/// 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
/// value is a dict containing keys:
///
/// ! - dummy value that is always set to an empty string. This ensures that we always have at
/// least one key set, which is required to keep the dict value alive (empty dicts get
/// pruned when serialied).
/// n - contact name (string)
/// n - contact name (string). This is always serialized, even if empty (but empty indicates
/// no name) so that we always have at least one key set (required to keep the dict value
/// alive as empty dicts get pruned).
/// N - contact nickname (string)
/// p - profile url (string)
/// q - profile decryption key (binary)
/// a - 1 if approved, omitted otherwise (int)
/// A - 1 if remote has approved me, omitted otherwise (int)
/// 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
/// 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 containing contact info.
struct contact_info {
static constexpr size_t MAX_NAME_LENGTH = 100;
std::string session_id; // in hex
std::optional<std::string_view> name;
std::optional<std::string_view> nickname;
std::optional<profile_pic> profile_picture;
std::string name;
std::string nickname;
profile_pic profile_picture;
bool approved = false;
bool approved_me = 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);
@ -49,20 +64,13 @@ struct contact_info {
contact_info(const struct contacts_contact& c); // From 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
// source string is a temporary may not outlive the `contact_info` object: the name is first
// copied into an internal std::string, and then the name string_view references that.
// Sets a name or nickname; this is exactly the same as assigning to .name/.nickname directly,
// except that we throw an exception if the given name is longer than MAX_NAME_LENGTH.
void set_name(std::string name);
// Same as above, but for nickname.
void set_nickname(std::string nickname);
private:
friend class Contacts;
std::string name_;
std::string nickname_;
void load(const dict& info_dict);
};
@ -111,13 +119,20 @@ class Contacts : public ConfigBase {
/// contacts.set(c);
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_nickname(std::string_view session_id, std::string nickname);
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_me(std::string_view session_id, bool approved_me);
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.
/// 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
} 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,
// only has port if non-default, has trailing / removed)
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
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,
// though isn't really one.
int64_t last_read; // ms since unix epoch
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`.
///
@ -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)
__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).
bool convo_info_volatile_get_open(
bool convo_info_volatile_get_community(
const config_object* conf,
convo_info_volatile_open* og,
convo_info_volatile_community* comm,
const char* base_url,
const char* room,
unsigned const char* pubkey) __attribute__((warn_unused_result));
bool convo_info_volatile_get_or_construct_open(
const char* room) __attribute__((warn_unused_result));
bool convo_info_volatile_get_or_construct_community(
const config_object* conf,
convo_info_volatile_open* convo,
convo_info_volatile_community* convo,
const char* base_url,
const char* room,
unsigned const char* pubkey) __attribute__((warn_unused_result));
/// Fills `convo` with the conversation info given a legacy closed group ID (specified as a
/// null-terminated hex string), if the conversation exists, and returns true. If the conversation
/// does not exist then `convo` is left unchanged and false is returned.
bool convo_info_volatile_get_legacy_closed(
const config_object* conf, convo_info_volatile_legacy_closed* convo, const char* id)
/// Fills `convo` 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 `convo` is left unchanged and false is returned.
bool convo_info_volatile_get_legacy_group(
const config_object* conf, convo_info_volatile_legacy_group* convo, const char* id)
__attribute__((warn_unused_result));
/// 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.
///
/// Returns true as long as it is given a valid legacy closed 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.
/// Returns true as long as it is given a valid legacy 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 convo, and then giving it to convo_info_volatile_set().
bool convo_info_volatile_get_or_construct_legacy_closed(
const config_object* conf, convo_info_volatile_legacy_closed* convo, const char* id)
bool convo_info_volatile_get_or_construct_legacy_group(
const config_object* conf, convo_info_volatile_legacy_group* convo, const char* id)
__attribute__((warn_unused_result));
/// 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_open(config_object* conf, const convo_info_volatile_open* convo);
void convo_info_volatile_set_legacy_closed(
config_object* conf, const convo_info_volatile_legacy_closed* convo);
void convo_info_volatile_set_community(
config_object* conf, const convo_info_volatile_community* 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
/// and removed, false if the conversation was not present. You must not call this during
/// iteration; see details below.
bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id);
bool convo_info_volatile_erase_open(
config_object* conf, const char* base_url, const char* room, unsigned const char* pubkey);
bool convo_info_volatile_erase_legacy_closed(config_object* conf, const char* group_id);
bool convo_info_volatile_erase_community(
config_object* conf, const char* base_url, const char* room);
bool convo_info_volatile_erase_legacy_group(config_object* conf, const char* group_id);
/// Returns the number of conversations.
size_t convo_info_volatile_size(const config_object* conf);
/// 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_open(const config_object* conf);
size_t convo_info_volatile_size_legacy_closed(const config_object* conf);
size_t convo_info_volatile_size_communities(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:
///
/// convo_info_volatile_1to1 c1;
/// convo_info_volatile_open c2;
/// convo_info_volatile_legacy_closed c3;
/// convo_info_volatile_community c2;
/// convo_info_volatile_legacy_group c3;
/// 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)) {
/// if (convo_info_volatile_it_is_1to1(it, &c1)) {
/// // 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
/// } 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
/// }
/// }
@ -169,6 +168,8 @@ size_t convo_info_volatile_size_legacy_closed(const config_object* conf);
/// convo_info_volatile_iterator_erase(it);
/// else
/// convo_info_volatile_iterator_advance(it);
/// } else {
/// convo_info_volatile_iterator_advance(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
// over).
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_legacy_closed(
convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities(
const config_object* conf);
convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups(
const config_object* conf);
// 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.
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.
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
// `c` and returns true. Otherwise it returns false.
bool convo_info_volatile_it_is_legacy_closed(
convo_info_volatile_iterator* it, convo_info_volatile_legacy_closed* 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 convo_info_volatile_it_is_legacy_group(
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.
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 "base.hpp"
#include "community.hpp"
using namespace std::literals;
extern "C" {
struct convo_info_volatile_1to1;
struct convo_info_volatile_open;
struct convo_info_volatile_legacy_closed;
struct convo_info_volatile_community;
struct convo_info_volatile_legacy_group;
}
namespace session::config {
@ -29,22 +32,23 @@ class ConvoInfoVolatile;
/// 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.
///
/// o - open group conversations. Each key is: BASE_URL + '\0' + LC_ROOM_NAME + '\0' +
/// SERVER_PUBKEY (in bytes). Note that room name is *always* lower-cased here (so that clients
/// with the same room but with different cases will always set the same key). Values are dicts
/// with keys:
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always 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.
/// o - community conversations. This is a nested dict where the outer keys are the BASE_URL of the
/// community and the outer value is a dict containing:
/// - `#` -- the 32-byte server pubkey
/// - `R` -- dict of rooms on the server; each key is the lower-case room name, value is a dict
/// containing keys:
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
/// 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
/// indistinguishable from a Session ID, but isn't really a proper Session ID). Values are
/// dicts with keys:
/// C - legacy group conversations (aka closed groups). The key is the group identifier (which
/// looks indistinguishable from a Session ID, but isn't really a proper Session ID). Values
/// are dicts with keys:
/// r - the unix timestamp (integer milliseconds) of the last-read message. Always 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 - reserved for future tracking of new closed group conversations.
/// c - reserved for future tracking of new group conversations.
namespace convo {
@ -71,96 +75,34 @@ namespace convo {
friend class session::config::ConvoInfoVolatile;
};
struct open_group : base {
// 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX')
static constexpr size_t MAX_URL = 267, MAX_ROOM = 64;
struct community : config::community, base {
std::string_view base_url() const; // Accesses the base url (i.e. not including room or
// 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);
using config::community::community;
// Internal ctor/method for C API implementations:
open_group(const struct convo_info_volatile_open& c); // From c struct
void into(convo_info_volatile_open& 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;
community(const convo_info_volatile_community& c); // From c struct
void into(convo_info_volatile_community& c) const; // Into c struct
friend class session::config::ConvoInfoVolatile;
// 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);
friend struct session::config::comm_iterator_helper;
};
struct legacy_closed_group : base {
struct legacy_group : base {
std::string id; // in hex, indistinguishable from a Session ID
// Constructs an empty legacy_closed_group from a quasi-session_id
explicit legacy_closed_group(std::string&& group_id);
explicit legacy_closed_group(std::string_view group_id);
// Constructs an empty legacy_group from a quasi-session_id
explicit legacy_group(std::string&& group_id);
explicit legacy_group(std::string_view group_id);
// Internal ctor/method for C API implementations:
legacy_closed_group(const struct convo_info_volatile_legacy_closed& c); // From c struct
void into(convo_info_volatile_legacy_closed& c) const; // Into c struct
legacy_group(const struct convo_info_volatile_legacy_group& c); // From c struct
void into(convo_info_volatile_legacy_group& c) const; // Into c struct
private:
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
class ConvoInfoVolatile : public ConfigBase {
@ -186,33 +128,49 @@ class ConvoInfoVolatile : public ConfigBase {
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
/// 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;
/// Looks up and returns an open group conversation. Takes the base URL, room name (case
/// insensitive), and pubkey (in hex). Retuns nullopt if the open group was not found,
/// otherwise a filled out `convo::open_group`.
std::optional<convo::open_group> get_open(
std::string_view base_url, std::string_view room, std::string_view pubkey_hex) const;
/// Looks up and returns a community conversation. Takes the base URL and room name (case
/// insensitive). Retuns nullopt if the community was not found, otherwise a filled out
/// `convo::community`.
std::optional<convo::community> get_community(
std::string_view base_url, std::string_view room) const;
/// Same as above, but takes the pubkey as bytes instead of hex
std::optional<convo::open_group> get_open(
std::string_view base_url, std::string_view room, ustring_view pubkey) 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;
/// Looks up and returns a legacy 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 group
/// conversation.
std::optional<convo::legacy_group> get_legacy_group(std::string_view pubkey_hex) const;
/// 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.
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;
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;
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
/// conversation last read time you would do:
@ -222,31 +180,35 @@ class ConvoInfoVolatile : public ConfigBase {
/// conversations.set(info);
///
void set(const convo::one_to_one& c);
void set(const convo::legacy_closed_group& c);
void set(const convo::open_group& c);
void set(const convo::legacy_group& c);
void set(const convo::community& c);
void set(const convo::any& c); // Variant which can be any of the above
protected:
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:
/// Removes a one-to-one conversation. Returns true if found and removed, false if not present.
bool erase_1to1(std::string_view pubkey);
/// Removes an open group conversation record. Returns true if found and removed, false if not
/// present. Arguments are the same as `get_open`.
bool erase_open(std::string_view base_url, std::string_view room, std::string_view pubkey_hex);
bool erase_open(std::string_view base_url, std::string_view room, ustring_view pubkey);
/// Removes a community conversation record. 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 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.
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).
bool erase(const convo::one_to_one& c);
bool erase(const convo::open_group& c);
bool erase(const convo::legacy_closed_group& c);
bool erase(const convo::community& c);
bool erase(const convo::legacy_group& c);
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).
size_t size() const;
/// Returns the number of 1-to-1, open group, and legacy closed group conversations,
/// respectively.
/// Returns the number of 1-to-1, community, and legacy group conversations, respectively.
size_t size_1to1() const;
size_t size_open() const;
size_t size_legacy_closed() const;
size_t size_communities() const;
size_t size_legacy_groups() const;
/// Returns true if the conversation list is empty.
bool empty() const { return size() == 0; }
@ -276,9 +237,9 @@ class ConvoInfoVolatile : public ConfigBase {
/// for (auto& convo : conversations) {
/// if (auto* dm = std::get_if<convo::one_to_one>(&convo)) {
/// // 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.
/// } 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
/// }
/// }
@ -302,8 +263,8 @@ class ConvoInfoVolatile : public ConfigBase {
///
/// 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_closed()` for each
/// one.
/// through that vector calling `erase_1to1()`/`erase_community()`/`erase_legacy_group()` for
/// each one.
///
iterator begin() const { return iterator{data}; }
iterator end() const { return iterator{}; }
@ -313,12 +274,11 @@ class ConvoInfoVolatile : public ConfigBase {
/// 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::open_group> begin_open() const { return {data}; }
subtype_iterator<convo::legacy_closed_group> begin_legacy_closed() const { return {data}; }
subtype_iterator<convo::community> begin_communities() const { return {data}; }
subtype_iterator<convo::legacy_group> begin_legacy_groups() const { return {data}; }
using iterator_category = std::input_iterator_tag;
using value_type =
std::variant<convo::one_to_one, convo::open_group, convo::legacy_closed_group>;
using value_type = std::variant<convo::one_to_one, convo::community, convo::legacy_group>;
using reference = value_type&;
using pointer = value_type*;
using difference_type = std::ptrdiff_t;
@ -326,15 +286,15 @@ class ConvoInfoVolatile : public ConfigBase {
struct iterator {
protected:
std::shared_ptr<convo::any> _val;
std::optional<dict::const_iterator> _it_11, _end_11, _it_open, _end_open, _it_lclosed,
_end_lclosed;
std::optional<dict::const_iterator> _it_11, _end_11, _it_lgroup, _end_lgroup;
std::optional<comm_iterator_helper> _it_comm;
void _load_val();
iterator() = default; // Constructs an end tombstone
explicit iterator(
const DictFieldRoot& data,
bool oneto1 = true,
bool open = true,
bool closed = true);
bool communities = true,
bool legacy_groups = true);
friend class ConvoInfoVolatile;
public:
@ -358,8 +318,8 @@ class ConvoInfoVolatile : public ConfigBase {
iterator(
data,
std::is_same_v<convo::one_to_one, ConvoType>,
std::is_same_v<convo::open_group, ConvoType>,
std::is_same_v<convo::legacy_closed_group, ConvoType>) {}
std::is_same_v<convo::community, ConvoType>,
std::is_same_v<convo::legacy_group, ConvoType>) {}
friend class ConvoInfoVolatile;
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,
Contacts = 3,
ConvoInfoVolatile = 4,
ClosedGroupInfo = 11,
UserGroups = 5,
};
} // namespace session::config

View File

@ -7,13 +7,12 @@ extern "C" {
#include <stddef.h>
typedef struct user_profile_pic {
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
// profile pic.
const char* url;
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
// null-terminated C string. Will be NULL if there is no profile pic.
const unsigned char* key;
size_t keylen;
// Null-terminated C string containing the uploaded URL of the pic. Will be length 0 if there
// is no profile pic.
char url[224];
// The profile pic decryption key, in bytes. This is a byte buffer of length 32, *not* a
// null-terminated C string. This is only valid when there is a url (i.e. url has strlen > 0).
unsigned char key[32];
} user_profile_pic;
#ifdef __cplusplus

View File

@ -1,39 +1,57 @@
#pragma once
#include <stdexcept>
#include "session/types.hpp"
namespace session::config {
// Profile pic info. Note that `url` is null terminated (though the null lies just beyond the end
// of the string view: that is, it views into a full std::string).
// Profile pic info.
struct profile_pic {
private:
std::string url_;
ustring key_;
static constexpr size_t MAX_URL_LENGTH = 223;
public:
std::string_view url;
ustring_view key;
std::string url;
ustring 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
profile_pic() = default;
// Constructs from string views: the values must stay alive for the duration of the profile_pic
// 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} {}
// Constructs from a URL and key. Key must be empty or 32 bytes.
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
profile_pic(std::string&& url, ustring&& key) :
url_{std::move(url)}, key_{std::move(key)}, url{url_}, key{key_} {}
// Constructs from a string/ustring pair moved into the constructor
profile_pic(std::string&& url, ustring&& key) : url{std::move(url)}, key{std::move(key)} {
check_key(this->key);
}
// Returns true if either url or key are empty
bool empty() const { return url.empty() || key.empty(); }
// Returns true if either url or key are empty (or invalid)
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
// profile_pic object. (This is only needed when the source string may not outlive the
// profile_pic object; if it does, the `url` or `key` can be assigned to directly).
void set_url(std::string url);
void set_key(ustring key);
// Clears the current url/key, if set. This is just a shortcut for calling `.clear()` on each
// of them.
void clear() {
url.clear();
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

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.
void set_name(std::string_view new_name);
/// Gets the user's current profile pic URL and decryption key. Returns nullptr for *both*
/// values if *either* value is unset or empty in the config data.
std::optional<profile_pic> get_profile_pic() const;
/// Gets the user's current profile pic URL and decryption key. The returned object will
/// evaluate as false if the URL and/or key are not set.
profile_pic get_profile_pic() const;
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
/// 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
.map {
knownMemberNameMap[$0] ??
Profile.truncated(id: $0, threadVariant: .legacyClosedGroup)
Profile.truncated(id: $0, threadVariant: .legacyGroup)
}
return String(
@ -396,7 +396,7 @@ public extension ClosedGroupControlMessage.Kind {
let removedMemberNames: [String] = memberIds.removing(userPublicKey)
.map {
knownMemberNameMap[$0] ??
Profile.truncated(id: $0, threadVariant: .legacyClosedGroup)
Profile.truncated(id: $0, threadVariant: .legacyGroup)
}
let format: String = (removedMemberNames.count > 1 ?
"GROUP_MEMBERS_REMOVED".localized() :

View File

@ -15,6 +15,8 @@ public final class SharedConfigMessage: ControlMessage {
public var seqNo: Int64
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 }
// MARK: - Kind
@ -23,14 +25,14 @@ public final class SharedConfigMessage: ControlMessage {
case userProfile
case contacts
case convoInfoVolatile
case groups
case userGroups
public var description: String {
switch self {
case .userProfile: return "userProfile"
case .contacts: return "contacts"
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 .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .groups: return .groups
case .userGroups: return .userGroups
}
}(),
seqNo: sharedConfigMessage.seqno,
@ -98,7 +100,7 @@ public final class SharedConfigMessage: ControlMessage {
case .userProfile: return .userProfile
case .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .groups: return .groups
case .userGroups: return .userGroups
}
}(),
seqno: self.seqNo,
@ -135,7 +137,7 @@ public extension SharedConfigMessage.Kind {
case .userProfile: return .userProfile
case .contacts: return .contacts
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)
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
return .closedGroup(groupPublicKey: thread.id)
case .openGroup:
case .community:
guard let openGroup: OpenGroup = try thread.openGroup.fetchOne(db) else {
throw StorageError.objectNotFound
}

View File

@ -361,7 +361,7 @@ public extension Message {
let blindedUserPublicKey: String? = SessionThread
.getUserHexEncodedBlindedKey(
threadId: openGroupId,
threadVariant: .openGroup
threadVariant: .community
)
for (encodedEmoji, rawReaction) in reactions {
if let decodedEmoji = encodedEmoji.removingPercentEncoding,

View File

@ -218,8 +218,8 @@ public extension VisibleMessage {
recipient: (try? interaction.recipientStates.fetchOne(db))?.recipientId,
groupPublicKey: try? interaction.thread
.filter(
SessionThread.Columns.variant == SessionThread.Variant.legacyClosedGroup ||
SessionThread.Columns.variant == SessionThread.Variant.closedGroup
SessionThread.Columns.variant == SessionThread.Variant.legacyGroup ||
SessionThread.Columns.variant == SessionThread.Variant.group
)
.select(.id)
.asRequest(of: String.self)

View File

@ -363,7 +363,7 @@ public enum OpenGroupAPI {
requests: requestResponseType,
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 maybeRoomResponse: Codable? = data
.first(where: { key, _ in
@ -380,20 +380,15 @@ public enum OpenGroupAPI {
let capabilities: Capabilities = maybeCapabilities?.body,
let roomInfo: ResponseInfoType = maybeRoom?.responseInfo,
let room: Room = maybeRoom?.body
else {
return Fail(error: HTTPError.parsingFailed)
.eraseToAnyPublisher()
}
else { throw HTTPError.parsingFailed }
return Just((
return (
info: info,
data: (
capabilities: (info: capabilitiesInfo, data: capabilities),
room: (info: roomInfo, data: room)
)
))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
)
}
.eraseToAnyPublisher()
}
@ -434,7 +429,7 @@ public enum OpenGroupAPI {
requests: requestResponseType,
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 maybeRooms: HTTP.BatchSubResponse<[Room]>? = data
.first(where: { key, _ in
@ -450,17 +445,12 @@ public enum OpenGroupAPI {
let capabilities: Capabilities = maybeCapabilities?.body,
let roomsInfo: ResponseInfoType = maybeRooms?.responseInfo,
let rooms: [Room] = maybeRooms?.body
else {
return Fail(error: HTTPError.parsingFailed)
.eraseToAnyPublisher()
}
else { throw HTTPError.parsingFailed }
return Just((
return (
capabilities: (info: capabilitiesInfo, data: capabilities),
rooms: (info: roomsInfo, data: rooms)
))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
)
}
.eraseToAnyPublisher()
}
@ -957,15 +947,10 @@ public enum OpenGroupAPI {
timeout: FileServerAPI.fileTimeout,
using: dependencies
)
.flatMap { responseInfo, maybeData -> AnyPublisher<(ResponseInfoType, Data), Error> in
guard let data: Data = maybeData else {
return Fail(error: HTTPError.parsingFailed)
.eraseToAnyPublisher()
}
.tryMap { responseInfo, maybeData -> (ResponseInfoType, Data) in
guard let data: Data = maybeData else { throw HTTPError.parsingFailed }
return Just((responseInfo, data))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return (responseInfo, data)
}
.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
// 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))
if (try? OpenGroup.exists(db, id: threadId)) == false {
@ -249,67 +249,64 @@ public final class OpenGroupManager {
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)
return Just(())
.setFailureType(to: Error.self)
.subscribe(on: OpenGroupAPI.workQueue)
.receive(on: OpenGroupAPI.workQueue)
.flatMap { _ in
dependencies.storage
.readPublisherFlatMap { db in
// Note: The initial request for room info and it's capabilities should NOT be
// authenticated (this is because if the server requires blinding and the auth
// 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(
return Deferred {
dependencies.storage
.readPublisherFlatMap(receiveOn: OpenGroupAPI.workQueue) { db in
// Note: The initial request for room info and it's capabilities should NOT be
// authenticated (this is because if the server requires blinding and the auth
// headers aren't blinded it will error - these endpoints do support unauthenticated
// retrieval so doing so prevents the error)
OpenGroupAPI
.capabilitiesAndRoom(
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(()))
}
using: dependencies
)
}
}
.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
switch result {
case .finished: break
case .failure: SNLog("Failed to join open group.")
}
}
.handleEvents(
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Failed to join open group.")
}
)
.eraseToAnyPublisher()
}
)
.eraseToAnyPublisher()
}
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
let publisher: AnyPublisher<[OpenGroupAPI.Room], Error> = dependencies.storage
.readPublisherFlatMap { db in
.readPublisherFlatMap(receiveOn: OpenGroupAPI.workQueue) { db in
OpenGroupAPI.capabilitiesAndRooms(
db,
on: OpenGroupAPI.defaultServer,
using: dependencies
)
}
.subscribe(on: OpenGroupAPI.workQueue)
.receive(on: OpenGroupAPI.workQueue)
.retry(8)
.map { response in
dependencies.storage.writeAsync { db in

View File

@ -3716,7 +3716,7 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
case userProfile = 1
case contacts = 2
case convoInfoVolatile = 3
case groups = 4
case userGroups = 4
}
private class func SNProtoSharedConfigMessageKindWrap(_ value: SessionProtos_SharedConfigMessage.Kind) -> SNProtoSharedConfigMessageKind {
@ -3724,7 +3724,7 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
case .userProfile: return .userProfile
case .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .groups: return .groups
case .userGroups: return .userGroups
}
}
@ -3733,7 +3733,7 @@ extension SNProtoAttachmentPointer.SNProtoAttachmentPointerBuilder {
case .userProfile: return .userProfile
case .contacts: return .contacts
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 contacts // = 2
case convoInfoVolatile // = 3
case groups // = 4
case userGroups // = 4
init() {
self = .userProfile
@ -1634,7 +1634,7 @@ struct SessionProtos_SharedConfigMessage {
case 1: self = .userProfile
case 2: self = .contacts
case 3: self = .convoInfoVolatile
case 4: self = .groups
case 4: self = .userGroups
default: return nil
}
}
@ -1644,7 +1644,7 @@ struct SessionProtos_SharedConfigMessage {
case .userProfile: return 1
case .contacts: return 2
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"),
2: .same(proto: "CONTACTS"),
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;
CONTACTS = 2;
CONVO_INFO_VOLATILE = 3;
GROUPS = 4;
USER_GROUPS = 4;
}
// @required

View File

@ -918,23 +918,25 @@ public class SignalAttachment: Equatable, Hashable {
let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
exportSession.outputURL = exportURL
let publisher = Future<SignalAttachment, Error> { resolver in
exportSession.exportAsynchronously {
let baseFilename = dataSource.sourceFilename
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
guard let dataSource = DataSourcePath.dataSource(with: exportURL,
shouldDeleteOnDeallocation: true) else {
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .couldNotConvertToMpeg4
let publisher = Deferred {
Future<SignalAttachment, Error> { resolver in
exportSession.exportAsynchronously {
let baseFilename = dataSource.sourceFilename
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
guard let dataSource = DataSourcePath.dataSource(with: exportURL,
shouldDeleteOnDeallocation: true) else {
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))
return
}
dataSource.sourceFilename = mp4Filename
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
resolver(Result.success(attachment))
}
}
.eraseToAnyPublisher()

View File

@ -70,7 +70,7 @@ extension MessageReceiver {
// Create the group
let groupAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: groupPublicKey)) ?? false)
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyClosedGroup)
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
.with(shouldBeVisible: true)
.saved(db)
let closedGroup: ClosedGroup = try ClosedGroup(

View File

@ -168,7 +168,7 @@ extension MessageReceiver {
// past two weeks)
if isInitialSync {
let existingClosedGroupsIds: [String] = (try? SessionThread
.filter(SessionThread.Columns.variant == SessionThread.Variant.legacyClosedGroup)
.filter(SessionThread.Columns.variant == SessionThread.Variant.legacyGroup)
.fetchAll(db))
.defaulting(to: [])
.map { $0.id }

View File

@ -393,7 +393,7 @@ extension MessageReceiver {
).save(db)
}
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
try GroupMember
.filter(GroupMember.Columns.groupId == thread.id)
.fetchAll(db)
@ -405,7 +405,7 @@ extension MessageReceiver {
).save(db)
}
case .openGroup:
case .community:
try RecipientState(
interactionId: interactionId,
recipientId: thread.id, // For open groups this will always be the thread id

View File

@ -40,7 +40,7 @@ extension MessageSender {
do {
// Create the relevant objects in the database
thread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyClosedGroup)
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
try ClosedGroup(
threadId: groupPublicKey,
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
if (try? SessionThread.exists(db, id: openGroupId)) != true { return nil }
return (openGroupId, .openGroup)
return (openGroupId, .community)
}
if let groupPublicKey: String = message.groupPublicKey {
// 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 }
return (groupPublicKey, .legacyClosedGroup)
return (groupPublicKey, .legacyGroup)
}
// Extract the 'syncTarget' value if there is one

View File

@ -68,6 +68,7 @@ extension MessageSender {
}
public static func performUploadsIfNeeded(
queue: DispatchQueue,
preparedSendData: PreparedSendData
) -> AnyPublisher<PreparedSendData, Error> {
// We need an interactionId in order for a message to have uploads
@ -95,7 +96,7 @@ extension MessageSender {
}()
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
.stateInfo(interactionId: interactionId, state: .uploading)
.fetchAll(db))

View File

@ -236,8 +236,9 @@ public final class MessageSender {
return PreparedSendData()
}
// Attach the user's profile if needed
if var messageWithProfile: MessageWithProfile = message as? MessageWithProfile {
// Attach the user's profile if needed (no need to do so for 'Note to Self' or sync messages as they
// will be managed by the user config handling
if !isSelfSend, !isSyncMessage, var messageWithProfile: MessageWithProfile = message as? MessageWithProfile {
let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
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
// files
//
// If you see this error then you need to call `MessageSender.performUploadsIfNeeded(preparedSendData:)`
// before calling this function
// If you see this error then you need to call
// `MessageSender.performUploadsIfNeeded(queue:preparedSendData:)` before calling this function
switch preparedSendData.message {
case let visibleMessage as VisibleMessage:
guard visibleMessage.attachmentIds.count == preparedSendData.totalAttachmentsUploaded else {
@ -674,7 +675,7 @@ public final class MessageSender {
}()
return dependencies.storage
.writePublisher { db -> Void in
.writePublisher(receiveOn: DispatchQueue.global(qos: .default)) { db -> Void in
try MessageSender.handleSuccessfulMessageSend(
db,
message: updatedMessage,
@ -701,20 +702,22 @@ public final class MessageSender {
.eraseToAnyPublisher()
}
return Future<Bool, Error> { resolver in
NotifyPushServerJob.run(
job,
queue: DispatchQueue.global(qos: .default),
success: { _, _ in resolver(Result.success(true)) },
failure: { _, _, _ in
// 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.
resolver(Result.success(true))
}
)
return Deferred {
Future<Bool, Error> { resolver in
NotifyPushServerJob.run(
job,
queue: DispatchQueue.global(qos: .default),
success: { _, _ in resolver(Result.success(true)) },
failure: { _, _, _ in
// 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.
resolver(Result.success(true))
}
)
}
}
.eraseToAnyPublisher()
}
@ -762,7 +765,7 @@ public final class MessageSender {
// Send the result
return dependencies.storage
.readPublisherFlatMap { db in
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .default)) { db in
OpenGroupAPI
.send(
db,
@ -781,7 +784,7 @@ public final class MessageSender {
let updatedMessage: Message = message
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
try MessageSender.handleSuccessfulMessageSend(
db,
@ -831,7 +834,7 @@ public final class MessageSender {
// Send the result
return dependencies.storage
.readPublisherFlatMap { db in
.readPublisherFlatMap(receiveOn: DispatchQueue.global(qos: .default)) { db in
return OpenGroupAPI
.send(
db,
@ -846,7 +849,7 @@ public final class MessageSender {
let updatedMessage: Message = message
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
try MessageSender.handleSuccessfulMessageSend(
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,
// just in case)
Storage.shared
.readPublisher { db -> (String, Set<String>) in
.readPublisher(receiveOn: DispatchQueue.global(qos: .background)) { db -> (String, Set<String>) in
(
getUserHexEncodedPublicKey(db),
try ClosedGroup

View File

@ -82,15 +82,12 @@ public final class ClosedGroupPoller: Poller {
for publicKey: String
) -> AnyPublisher<Snode, Error> {
return SnodeAPI.getSwarm(for: publicKey)
.flatMap { swarm -> AnyPublisher<Snode, Error> in
.tryMap { swarm -> Snode in
guard let snode: Snode = swarm.randomElement() else {
return Fail(error: OnionRequestAPIError.insufficientSnodes)
.eraseToAnyPublisher()
throw OnionRequestAPIError.insufficientSnodes
}
return Just(snode)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
return snode
}
.eraseToAnyPublisher()
}

View File

@ -9,7 +9,7 @@ import SessionUtilitiesKit
public final class CurrentUserPoller: Poller {
public static var namespaces: [SnodeAPI.Namespace] = [
.default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configGroups
.default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups
]
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
// is empty or we have used all of the snodes and need to start from scratch
return SnodeAPI.getSwarm(for: publicKey)
.flatMap { [weak self] _ -> AnyPublisher<Snode, Error> in
guard let strongSelf = self else {
return Fail(error: SnodeAPIError.generic)
.eraseToAnyPublisher()
}
.tryFlatMap { [weak self] _ -> AnyPublisher<Snode, Error> in
guard let strongSelf = self else { throw SnodeAPIError.generic }
self?.targetSnode.mutate { $0 = nil }
self?.usedSnodes.mutate { $0.removeAll() }

View File

@ -91,7 +91,7 @@ extension OpenGroupAPI {
let server: String = self.server
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
.select(max(OpenGroup.Columns.pollFailureCount))
.asRequest(of: Int64.self)
@ -225,7 +225,7 @@ extension OpenGroupAPI {
}
return dependencies.storage
.readPublisherFlatMap { db in
.readPublisherFlatMap(receiveOn: OpenGroupAPI.workQueue) { db in
OpenGroupAPI.capabilities(
db,
server: server,

View File

@ -200,9 +200,16 @@ public class Poller {
poller?.pollerName(for: publicKey) ??
"poller with public key \(publicKey)"
)
let configHashes: [String] = SessionUtil.configHashes(for: publicKey)
// 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
guard
(calledFromBackgroundPoller && isBackgroundPollValid()) ||
@ -322,15 +329,17 @@ public class Poller {
return Publishers
.MergeMany(
jobsToRun.map { job -> AnyPublisher<Void, Error> in
Future<Void, Error> { resolver in
// Note: In the background we just want jobs to fail silently
MessageReceiveJob.run(
job,
queue: queue,
success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in resolver(Result.success(())) },
deferred: { _ in resolver(Result.success(())) }
)
Deferred {
Future<Void, Error> { resolver in
// Note: In the background we just want jobs to fail silently
MessageReceiveJob.run(
job,
queue: queue,
success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in resolver(Result.success(())) },
deferred: { _ in resolver(Result.success(())) }
)
}
}
.eraseToAnyPublisher()
}

View File

@ -39,9 +39,9 @@ public class TypingIndicators {
// Don't send typing indicators in group threads
guard
threadVariant != .legacyClosedGroup &&
threadVariant != .closedGroup &&
threadVariant != .openGroup
threadVariant != .legacyGroup &&
threadVariant != .group &&
threadVariant != .community
else { return nil }
self.threadId = threadId

View File

@ -34,7 +34,7 @@ public extension MentionInfo {
let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName)
/// **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> = {
guard let pattern: FTS5Pattern = pattern else {
@ -57,7 +57,7 @@ public extension MentionInfo {
WHERE (
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
\(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR
\(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
)
)
@ -83,7 +83,7 @@ public extension MentionInfo {
JOIN \(Profile.self) ON (
\(Profile.self).rowid = \(profileFullTextSearch).rowid AND
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
\(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR
\(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
)
)

View File

@ -304,9 +304,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
}
}()
let isGroupThread: Bool = (
self.threadVariant == .openGroup ||
self.threadVariant == .legacyClosedGroup ||
self.threadVariant == .closedGroup
self.threadVariant == .community ||
self.threadVariant == .legacyGroup ||
self.threadVariant == .group
)
return ViewModel(
@ -741,13 +741,13 @@ public extension MessageViewModel {
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
)
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).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
\(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)"))
)
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).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))

View File

@ -103,8 +103,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var canWrite: Bool {
switch threadVariant {
case .contact: return true
case .legacyClosedGroup, .closedGroup: return currentUserIsClosedGroupMember == true
case .openGroup: return openGroupPermissions?.contains(.write) ?? false
case .legacyGroup, .group: return currentUserIsClosedGroupMember == true
case .community: return openGroupPermissions?.contains(.write) ?? false
}
}
@ -161,15 +161,15 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var profile: Profile? {
switch threadVariant {
case .contact: return contactProfile
case .legacyClosedGroup, .closedGroup:
case .legacyGroup, .group:
return (closedGroupProfileBack ?? closedGroupProfileBackFallback)
case .openGroup: return nil
case .community: return nil
}
}
public var additionalProfile: Profile? {
switch threadVariant {
case .legacyClosedGroup, .closedGroup: return closedGroupProfileFront
case .legacyGroup, .group: return closedGroupProfileFront
default: return nil
}
}
@ -194,8 +194,8 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
public var userCount: Int? {
switch threadVariant {
case .contact: return nil
case .legacyClosedGroup, .closedGroup: return closedGroupUserCount
case .openGroup: return openGroupUserCount
case .legacyGroup, .group: return closedGroupUserCount
case .community: return openGroupUserCount
}
}
@ -1355,8 +1355,8 @@ public extension SessionThreadViewModel {
LEFT JOIN \(OpenGroup.self) ON false
WHERE (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyClosedGroup)")) OR
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.closedGroup)"))
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyGroup)")) OR
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.group)"))
)
GROUP BY \(thread[.id])
"""
@ -1435,7 +1435,7 @@ public extension SessionThreadViewModel {
) AS \(groupMemberInfoLiteral) ON false
WHERE
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
\(SQL("\(thread[.id]) != \(userPublicKey)"))
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.
private static let nameDataLength: UInt = 64
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 avatarTagLength: Int = 16

View File

@ -32,25 +32,22 @@ class ConfigContactsSpec: QuickSpec {
error?.deallocate()
// Empty contacts shouldn't have an existing contact
var definitelyRealId: [CChar] = "050000000000000000000000000000000000000000000000000000000000000000"
.bytes
.map { CChar(bitPattern: $0) }
var definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray
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))
var contact2: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf, &contact2, &definitelyRealId)).to(beTrue())
expect(contact2.name).to(beNil())
expect(contact2.nickname).to(beNil())
expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact2.name)).to(beEmpty())
expect(String(libSessionVal: contact2.nickname)).to(beEmpty())
expect(contact2.approved).to(beFalse())
expect(contact2.approved_me).to(beFalse())
expect(contact2.blocked).to(beFalse())
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact2.profile_pic.url).to(beNil())
expect(contact2.profile_pic.key).to(beNil())
expect(contact2.profile_pic.keylen).to(equal(0))
expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty())
// We don't need to push anything, since this is a default contact
expect(config_needs_push(conf)).to(beFalse())
@ -68,14 +65,8 @@ class ConfigContactsSpec: QuickSpec {
toPush?.deallocate()
// Update the contact data
let contact2Name: [CChar] = "Joe"
.bytes
.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.name = "Joe".toLibSession()
contact2.nickname = "Joey".toLibSession()
contact2.approved = true
contact2.approved_me = true
@ -85,19 +76,14 @@ class ConfigContactsSpec: QuickSpec {
// Ensure the contact details were updated
var contact3: contacts_contact = contacts_contact()
expect(contacts_get(conf, &contact3, &definitelyRealId)).to(beTrue())
expect(String(cString: contact3.name)).to(equal("Joe"))
expect(String(cString: contact3.nickname)).to(equal("Joey"))
expect(String(libSessionVal: contact3.name)).to(equal("Joe"))
expect(String(libSessionVal: contact3.nickname)).to(equal("Joey"))
expect(contact3.approved).to(beTrue())
expect(contact3.approved_me).to(beTrue())
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact3.profile_pic.url).to(beNil())
expect(contact3.profile_pic.key).to(beNil())
expect(contact3.profile_pic.keylen).to(equal(0))
expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty())
expect(contact3.blocked).to(beFalse())
let contact3SessionId: [CChar] = withUnsafeBytes(of: contact3.session_id) { [UInt8]($0) }
.map { CChar($0) }
expect(contact3SessionId).to(equal(definitelyRealId.nullTerminated()))
expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
@ -144,29 +130,24 @@ class ConfigContactsSpec: QuickSpec {
// Ensure the contact details were updated
var contact4: contacts_contact = contacts_contact()
expect(contacts_get(conf2, &contact4, &definitelyRealId)).to(beTrue())
expect(String(cString: contact4.name)).to(equal("Joe"))
expect(String(cString: contact4.nickname)).to(equal("Joey"))
expect(String(libSessionVal: contact4.name)).to(equal("Joe"))
expect(String(libSessionVal: contact4.nickname)).to(equal("Joey"))
expect(contact4.approved).to(beTrue())
expect(contact4.approved_me).to(beTrue())
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact4.profile_pic.url).to(beNil())
expect(contact4.profile_pic.key).to(beNil())
expect(contact4.profile_pic.keylen).to(equal(0))
expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty())
expect(contact4.blocked).to(beFalse())
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
.bytes
.map { CChar(bitPattern: $0) }
var anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray
var contact5: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact5, &anotherId)).to(beTrue())
expect(contact5.name).to(beNil())
expect(contact5.nickname).to(beNil())
expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
expect(String(libSessionVal: contact5.name)).to(beEmpty())
expect(String(libSessionVal: contact5.nickname)).to(beEmpty())
expect(contact5.approved).to(beFalse())
expect(contact5.approved_me).to(beFalse())
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(contact5.profile_pic.url).to(beNil())
expect(contact5.profile_pic.key).to(beNil())
expect(contact5.profile_pic.keylen).to(equal(0))
expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty())
expect(contact5.blocked).to(beFalse())
// 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()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact6) {
sessionIds.append(
String(cString: withUnsafeBytes(of: contact6.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
)
nicknames.append(
contact6.nickname.map { String(cString: $0) } ??
"(N/A)"
)
sessionIds.append(String(libSessionVal: contact6.session_id) ?? "(N/A)")
nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
expect(sessionIds.count).to(equal(2))
expect(sessionIds.count).to(equal(contacts_size(conf)))
expect(sessionIds.first).to(equal(String(cString: definitelyRealId.nullTerminated())))
expect(sessionIds.last).to(equal(String(cString: anotherId.nullTerminated())))
expect(sessionIds.first).to(equal(definitelyRealId))
expect(sessionIds.last).to(equal(anotherId))
expect(nicknames.first).to(equal("Joey"))
expect(nicknames.last).to(equal("(N/A)"))
@ -228,24 +201,15 @@ class ConfigContactsSpec: QuickSpec {
contacts_erase(conf, definitelyRealId)
// Client 2 adds a new friend:
var thirdId: [CChar] = "052222222222222222222222222222222222222222222222222222222222222222"
.bytes
.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 thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
var cThirdId: [CChar] = thirdId.cArray
var contact7: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact7, &thirdId)).to(beTrue())
nickname7.withUnsafeBufferPointer { contact7.nickname = $0.baseAddress }
expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
contact7.nickname = "Nickname 3".toLibSession()
contact7.approved = true
contact7.approved_me = true
profileUrl7.withUnsafeBufferPointer { contact7.profile_pic.url = $0.baseAddress }
profileKey7.withUnsafeBufferPointer { contact7.profile_pic.key = $0.baseAddress }
contact7.profile_pic.keylen = 6
contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession()
contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
contacts_set(conf2, &contact7)
expect(config_needs_push(conf)).to(beTrue())
@ -308,23 +272,15 @@ class ConfigContactsSpec: QuickSpec {
var contact8: contacts_contact = contacts_contact()
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator2, &contact8) {
sessionIds2.append(
String(cString: withUnsafeBytes(of: contact8.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
)
nicknames2.append(
contact8.nickname.map { String(cString: $0) } ??
"(N/A)"
)
sessionIds2.append(String(libSessionVal: contact8.session_id) ?? "(N/A)")
nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator2)
}
contacts_iterator_free(contactIterator2) // Need to free the iterator
expect(sessionIds2.count).to(equal(2))
expect(sessionIds2.first).to(equal(String(cString: anotherId.nullTerminated())))
expect(sessionIds2.last).to(equal(String(cString: thirdId.nullTerminated())))
expect(sessionIds2.first).to(equal(anotherId))
expect(sessionIds2.last).to(equal(thirdId))
expect(nicknames2.first).to(equal("(N/A)"))
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:
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()
expect(convo_info_volatile_get_legacy_closed(conf, &legacyClosed1, &definitelyRealId))
expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &definitelyRealId))
.to(beFalse())
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &definitelyRealId)).to(beTrue())
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
@ -70,40 +70,34 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
var openGroupBaseUrl: [CChar] = "http://Example.ORG:5678"
.bytes
.map { CChar(bitPattern: $0) }
var openGroupBaseUrl: [CChar] = "http://Example.ORG:5678".cArray
let openGroupBaseUrlResult: [CChar] = ("http://Example.ORG:5678"
.lowercased()
.bytes
.map { CChar(bitPattern: $0) } +
.cArray +
[CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
)
var openGroupRoom: [CChar] = "SudokuRoom"
.bytes
.map { CChar(bitPattern: $0) }
var openGroupRoom: [CChar] = "SudokuRoom".cArray
let openGroupRoomResult: [CChar] = ("SudokuRoom"
.lowercased()
.bytes
.map { CChar(bitPattern: $0) } +
.cArray +
[CChar](repeating: 0, count: (65 - openGroupRoom.count))
)
var openGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.bytes
var openGroup1: convo_info_volatile_open = convo_info_volatile_open()
expect(convo_info_volatile_get_or_construct_open(conf, &openGroup1, &openGroupBaseUrl, &openGroupRoom, &openGroupPubkey)).to(beTrue())
expect(withUnsafeBytes(of: openGroup1.base_url) { [UInt8]($0) }
var community1: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_or_construct_community(conf, &community1, &openGroupBaseUrl, &openGroupRoom, &openGroupPubkey)).to(beTrue())
expect(withUnsafeBytes(of: community1.base_url) { [UInt8]($0) }
.map { CChar($0) }
).to(equal(openGroupBaseUrlResult))
expect(withUnsafeBytes(of: openGroup1.room) { [UInt8]($0) }
expect(withUnsafeBytes(of: community1.room) { [UInt8]($0) }
.map { CChar($0) }
).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"))
openGroup1.unread = true
community1.unread = true
// 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 toPushLen: Int = 0
@ -143,17 +137,17 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
).to(equal(definitelyRealId.nullTerminated()))
expect(oneToOne4.unread).to(beFalse())
var openGroup2: convo_info_volatile_open = convo_info_volatile_open()
expect(convo_info_volatile_get_open(conf2, &openGroup2, &openGroupBaseUrl, &openGroupRoom, &openGroupPubkey)).to(beTrue())
expect(withUnsafeBytes(of: openGroup2.base_url) { [UInt8]($0) }
var community2: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_community(conf2, &community2, &openGroupBaseUrl, &openGroupRoom)).to(beTrue())
expect(withUnsafeBytes(of: community2.base_url) { [UInt8]($0) }
.map { CChar($0) }
).to(equal(openGroupBaseUrlResult))
expect(withUnsafeBytes(of: openGroup2.room) { [UInt8]($0) }
expect(withUnsafeBytes(of: community2.room) { [UInt8]($0) }
.map { CChar($0) }
).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"))
openGroup2.unread = true
community2.unread = true
var anotherId: [CChar] = "051111111111111111111111111111111111111111111111111111111111111111"
.bytes
@ -165,10 +159,10 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
var thirdId: [CChar] = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
.bytes
.map { CChar(bitPattern: $0) }
var legacyClosed2: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
expect(convo_info_volatile_get_or_construct_legacy_closed(conf2, &legacyClosed2, &thirdId)).to(beTrue())
legacyClosed2.last_read = (nowTimestampMs - 50)
convo_info_volatile_set_legacy_closed(conf2, &legacyClosed2)
var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &thirdId)).to(beTrue())
legacyGroup2.last_read = (nowTimestampMs - 50)
convo_info_volatile_set_legacy_group(conf2, &legacyGroup2)
expect(config_needs_push(conf2)).to(beTrue())
var toPush2: UnsafeMutablePointer<UInt8>? = nil
@ -190,12 +184,12 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
var seen: [String] = []
expect(convo_info_volatile_size(conf)).to(equal(4))
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_legacy_closed(conf)).to(equal(1))
expect(convo_info_volatile_size_communities(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 c2: convo_info_volatile_open = convo_info_volatile_open()
var c3: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
var c2: convo_info_volatile_community = convo_info_volatile_community()
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
while !convo_info_volatile_iterator_done(it) {
@ -206,7 +200,7 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
)
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) }
.map { CChar($0) }
.nullTerminated()
@ -218,7 +212,7 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
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) }
.map { CChar($0) }
.nullTerminated()
@ -271,11 +265,11 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
]))
var seen2: [String] = []
var c2: convo_info_volatile_open = convo_info_volatile_open()
let it2: OpaquePointer = convo_info_volatile_iterator_new_open(conf)
var c2: convo_info_volatile_community = convo_info_volatile_community()
let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf)
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) }
.map { CChar($0) }
.nullTerminated()
@ -291,11 +285,11 @@ class ConfigConvoInfoVolatileSpec: QuickSpec {
]))
var seen3: [String] = []
var c3: convo_info_volatile_legacy_closed = convo_info_volatile_legacy_closed()
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_closed(conf)
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf)
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) }
.map { CChar($0) }
.nullTerminated()

View File

@ -4,6 +4,7 @@ import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import SessionMessagingKit
import Quick
import Nimble
@ -68,25 +69,14 @@ class ConfigUserProfileSpec: QuickSpec {
// This should also be unset:
let pic: user_profile_pic = user_profile_get_pic(conf)
expect(pic.url).to(beNil())
expect(pic.key).to(beNil())
expect(pic.keylen).to(equal(0))
expect(String(libSessionVal: pic.url)).to(beEmpty())
// Now let's go set a profile name and picture:
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp"
.bytes
.map { CChar(bitPattern: $0) }
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
)
}
}
let p: user_profile_pic = user_profile_pic(
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
)
expect(user_profile_set_pic(conf, p)).to(equal(0))
// Retrieve them just to make sure they set properly:
@ -95,11 +85,9 @@ class ConfigUserProfileSpec: QuickSpec {
expect(String(cString: namePtr2!)).to(equal("Kallie"))
let pic2: user_profile_pic = user_profile_get_pic(conf);
expect(pic2.url).toNot(beNil())
expect(pic2.key).toNot(beNil())
expect(pic2.keylen).to(equal(6))
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"))
expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp"))
expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength))
.to(equal("secret78901234567890123456789012".data(using: .utf8)))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
@ -125,7 +113,7 @@ class ConfigUserProfileSpec: QuickSpec {
1:& d
1:n 6:Kallie
1:p 34:http://example.org/omg-pic-123.bmp
1:q 6:secret
1:q 32:secret78901234567890123456789012
e
1:< l
l i0e 32:
@ -146,12 +134,13 @@ class ConfigUserProfileSpec: QuickSpec {
]
.flatMap { $0 }
let expPush1Encrypted: [UInt8] = Data(hex: [
"a2952190dcb9797bc48e48f6dc7b3254d004bde9091cfc9ec3433cbc5939a3726deb04f58a546d7d79e6f8",
"0ea185d43bf93278398556304998ae882304075c77f15c67f9914c4d10005a661f29ff7a79e0a9de7f2172",
"5ba3b5a6c19eaa3797671b8fa4008d62e9af2744629cbb46664c4d8048e2867f66ed9254120371bdb24e95",
"b2d92341fa3b1f695046113a768ceb7522269f937ead5591bfa8a5eeee3010474002f2db9de043f0f0d1cf",
"b1066a03e7b5d6cfb70a8f84a20cd2df5a510cd3d175708015a52dd4a105886d916db0005dbea5706e5a5d",
"c37ffd0a0ca2824b524da2e2ad181a48bb38e21ed9abe136014a4ee1e472cb2f53102db2a46afa9d68"
"877c8e0f5d33f5fffa5a4e162785a9a89918e95de1c4b925201f1f5c29d9ee4f8c36e2b278fce1e6",
"b9d999689dd86ff8e79e0a04004fa54d24da89bc2604cb1df8c1356da8f14710543ecec44f2d57fc",
"56ea8b7e73d119c69d755f4d513d5d069f02396b8ec0cbed894169836f57ca4b782ce705895c593b",
"4230d50c175d44a08045388d3f4160bacb617b9ae8de3ebc8d9024245cd09ce102627cab2acf1b91",
"26159211359606611ca5814de320d1a7099a65c99b0eebbefb92a115f5efa6b9132809300ac010c6",
"857cfbd62af71b0fa97eccec75cb95e67edf40b35fdb9cad125a6976693ab085c6bba96a2e51826e",
"81e16b9ec1232af5680f2ced55310486"
].joined()).bytes
expect(String(pointer: toPush2, length: toPush2Len, encoding: .ascii))
@ -259,19 +248,10 @@ class ConfigUserProfileSpec: QuickSpec {
user_profile_set_name(conf2, "Raz")
// And, on conf2, we're also going to change the profile pic:
let profile2Url: [CChar] = "http://new.example.com/pic"
.bytes
.map { CChar(bitPattern: $0) }
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
)
}
}
let p2: user_profile_pic = user_profile_pic(
url: "http://new.example.com/pic".toLibSession(),
key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession()
)
user_profile_set_pic(conf2, p2)
// 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:
let pic3: user_profile_pic = user_profile_get_pic(conf)
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(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)
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(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(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