Updated to the latest version of libSession-util
Updated the SharedConfigMessage type to have a TTL of 30 days Updated the SnodeAPI to have a 'poll' method to be more consistent with the OpenGroupAPI (it also does multiple things now so is cleaner) Added logic to limit the number of config messages to be retrieved per poll Added the 'ValidatableResponse' protocol to standardise SnodeAPI response validation Added the libSession version to the logs Fixed an issue where the user profile pic wouldn't get synced correctly due to memory going out of scope Fixed some threading issues Refactored the thread variants to follow the updated terminology (will think about refactoring other code areas later) Cleaned up the Combine error handling Started fixing broken unit tests
This commit is contained in:
parent
345b693225
commit
f30b383bb8
|
@ -631,6 +631,8 @@
|
|||
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; };
|
||||
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 */,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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")"
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)))"
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -5,4 +5,5 @@ import Foundation
|
|||
public enum SessionUtilError: Error {
|
||||
case unableToCreateConfigObject
|
||||
case nilConfigObject
|
||||
case userDoesNotExist
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - String
|
||||
|
||||
public extension String {
|
||||
var cArray: [CChar] { [UInt8](self.utf8).map { CChar(bitPattern: $0) } }
|
||||
|
||||
/// Initialize with an optional pointer and a specific length
|
||||
init?(pointer: UnsafeRawPointer?, length: Int, encoding: String.Encoding = .utf8) {
|
||||
guard
|
||||
let pointer: UnsafeRawPointer = pointer,
|
||||
let result: String = String(data: Data(bytes: pointer, count: length), encoding: encoding)
|
||||
else { return nil }
|
||||
|
||||
self = result
|
||||
}
|
||||
|
||||
init?<T>(
|
||||
libSessionVal: T,
|
||||
nullTerminated: Bool = true,
|
||||
nullIfEmpty: Bool = false
|
||||
) {
|
||||
let result: String = {
|
||||
guard !nullTerminated else {
|
||||
return String(cString: withUnsafeBytes(of: libSessionVal) { [UInt8]($0) })
|
||||
}
|
||||
|
||||
return String(
|
||||
data: Data(libSessionVal: libSessionVal, count: MemoryLayout<T>.size),
|
||||
encoding: .utf8
|
||||
)
|
||||
.defaulting(to: "")
|
||||
}()
|
||||
|
||||
guard !nullIfEmpty || !result.isEmpty else { return nil }
|
||||
|
||||
self = result
|
||||
}
|
||||
|
||||
func toLibSession<T>() -> T {
|
||||
let targetSize: Int = MemoryLayout<T>.stride
|
||||
let result: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(
|
||||
byteCount: targetSize,
|
||||
alignment: MemoryLayout<T>.alignment
|
||||
)
|
||||
self.utf8CString.withUnsafeBytes { result.copyMemory(from: $0.baseAddress!, byteCount: $0.count) }
|
||||
|
||||
return result.withMemoryRebound(to: T.self, capacity: targetSize) { $0.pointee }
|
||||
}
|
||||
}
|
||||
|
||||
public extension Optional<String> {
|
||||
func toLibSession<T>() -> T {
|
||||
switch self {
|
||||
case .some(let value): return value.toLibSession()
|
||||
case .none: return "".toLibSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
public extension Data {
|
||||
var cArray: [UInt8] { [UInt8](self) }
|
||||
|
||||
init<T>(libSessionVal: T, count: Int) {
|
||||
self = Data(
|
||||
bytes: Swift.withUnsafeBytes(of: libSessionVal) { [UInt8]($0) },
|
||||
count: count
|
||||
)
|
||||
}
|
||||
|
||||
func toLibSession<T>() -> T {
|
||||
let targetSize: Int = MemoryLayout<T>.stride
|
||||
let result: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(
|
||||
byteCount: targetSize,
|
||||
alignment: MemoryLayout<T>.alignment
|
||||
)
|
||||
self.withUnsafeBytes { result.copyMemory(from: $0.baseAddress!, byteCount: $0.count) }
|
||||
|
||||
return result.withMemoryRebound(to: T.self, capacity: targetSize) { $0.pointee }
|
||||
}
|
||||
}
|
||||
|
||||
public extension Optional<Data> {
|
||||
func toLibSession<T>() -> T {
|
||||
switch self {
|
||||
case .some(let value): return value.toLibSession()
|
||||
case .none: return Data().toLibSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array
|
||||
|
||||
public extension Array where Element == CChar {
|
||||
func nullTerminated() -> [Element] {
|
||||
guard self.last != CChar(0) else { return self }
|
||||
|
||||
return self.appending(CChar(0))
|
||||
}
|
||||
}
|
|
@ -4,6 +4,18 @@
|
|||
<dict>
|
||||
<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>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
#pragma once
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <session/config.hpp>
|
||||
#include <session/types.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// Base class for types representing a community; this base type handles the url/room/pubkey that
|
||||
/// such a type need. Generally a class inherits from this to extend with the local
|
||||
/// community-related values.
|
||||
struct community {
|
||||
|
||||
// 267 = len('https://') + 253 (max valid DNS name length) + len(':XXXXX')
|
||||
static constexpr size_t URL_MAX_LENGTH = 267;
|
||||
static constexpr size_t ROOM_MAX_LENGTH = 64;
|
||||
|
||||
community() = default;
|
||||
|
||||
// Constructs an empty community struct from url, room, and pubkey. `base_url` will be
|
||||
// normalized if not already. pubkey is 32 bytes.
|
||||
community(std::string_view base_url, std::string_view room, ustring_view pubkey);
|
||||
|
||||
// Same as above, but takes pubkey as an encoded (hex or base32z or base64) string.
|
||||
community(std::string_view base_url, std::string_view room, std::string_view pubkey_encoded);
|
||||
|
||||
// Takes a combined room URL (e.g. https://whatever.com/r/Room?public_key=01234....), either
|
||||
// new style (with /r/) or old style (without /r/). Note that the URL gets canonicalized so
|
||||
// the resulting `base_url()` and `room()` values may not be exactly equal to what is given.
|
||||
//
|
||||
// See also `parse_full_url` which does the same thing but returns it in pieces rather than
|
||||
// constructing a new `community` object.
|
||||
explicit community(std::string_view full_url);
|
||||
|
||||
// Replaces the baseurl/room/pubkey of this object from a URL. This parses the URL, then stores
|
||||
// the values as if passed to set_base_url/set_room/set_pubkey.
|
||||
//
|
||||
// The base URL will be normalized; the room name will be case-preserving (but see `set_room`
|
||||
// for info on limitations on "case-preserving", particularly for volatile configs); and the
|
||||
// embedded pubkey must be encoded in one of hex, base32z, or base64.
|
||||
void set_full_url(std::string_view full_url);
|
||||
|
||||
// Replaces the base_url of this object. Note that changing the URL and then giving it to `set`
|
||||
// will end up inserting a *new* record but not removing the *old* one (you need to erase first
|
||||
// to do that).
|
||||
void set_base_url(std::string_view new_url);
|
||||
|
||||
// Changes the room token. This stores (or updates) the name as given as the localized room,
|
||||
// and separately stores the normalized (lower-case) token. Note that the localized name does
|
||||
// not persist across a push or dump in some config contexts (such as volatile room info). If
|
||||
// the new room given here changes more than just case (i.e. if the normalized room token
|
||||
// changes) then a call to `set` will end up inserting a *new* record but not removing the *old*
|
||||
// one (you need to erase first to do that).
|
||||
void set_room(std::string_view room);
|
||||
|
||||
// Updates the pubkey of this community (typically this is not called directly but rather
|
||||
// via `set_server` or during construction). Throws std::invalid_argument if the given
|
||||
// pubkey does not look like a valid pubkey. The std::string_view version takes the pubkey
|
||||
// as any of hex/base64/base32z.
|
||||
//
|
||||
// NOTE: the pubkey of all communities with the same URLs are stored in common, so changing
|
||||
// one community pubkey (and storing) will affect all communities using the same community
|
||||
// base URL.
|
||||
void set_pubkey(ustring_view pubkey);
|
||||
void set_pubkey(std::string_view pubkey);
|
||||
|
||||
// Accesses the base url (i.e. not including room or pubkey). Always lower-case/normalized.
|
||||
const std::string& base_url() const { return base_url_; }
|
||||
|
||||
// Accesses the room token; this is case-preserving, where possible. In some contexts, however,
|
||||
// such as volatile info, the case is not preserved and this will always return the normalized
|
||||
// (lower-case) form rather than the preferred form.
|
||||
const std::string& room() const { return localized_room_ ? *localized_room_ : room_; }
|
||||
|
||||
// Accesses the normalized room token, i.e. always lower-case.
|
||||
const std::string& room_norm() const { return room_; }
|
||||
|
||||
const ustring& pubkey() const { return pubkey_; } // Accesses the server pubkey (32 bytes).
|
||||
std::string pubkey_hex() const; // Accesses the server pubkey as hex (64 hex digits).
|
||||
std::string pubkey_b32z() const; // Accesses the server pubkey as base32z (52 alphanumeric
|
||||
// digits)
|
||||
std::string pubkey_b64() const; // Accesses the server pubkey as unpadded base64 (43 from
|
||||
// alphanumeric, '+', and '/').
|
||||
|
||||
// Takes a base URL as input and returns it in canonical form. This involves doing things
|
||||
// like lower casing it and removing redundant ports (e.g. :80 when using http://). Throws
|
||||
// std::invalid_argument if given an invalid base URL.
|
||||
static std::string canonical_url(std::string_view url);
|
||||
|
||||
// Takes a room token and returns it in canonical form (i.e. lower-cased). Throws
|
||||
// std::invalid_argument if given an invalid room token (e.g. too long, or containing token
|
||||
// other than a-z, 0-9, -, _).
|
||||
static std::string canonical_room(std::string_view room);
|
||||
|
||||
// Same as above, but modifies the argument in-place instead of returning a modified
|
||||
// copy.
|
||||
static void canonicalize_url(std::string& url);
|
||||
static void canonicalize_room(std::string& room);
|
||||
|
||||
// Takes a full room URL, splits it up into canonical url (see above), room, and server
|
||||
// pubkey. We take both the deprecated form (e.g.
|
||||
// https://example.com/SomeRoom?public_key=...) and new form
|
||||
// (https://example.com/r/SomeRoom?public_key=...). The public_key is typically specified
|
||||
// in hex (64 digits), but we also accept base64 (43 chars or 44 with padding) and base32z
|
||||
// (52 chars) encodings (for slightly shorter URLs).
|
||||
//
|
||||
// The returned URL is normalized (lower-cased, and cleaned up).
|
||||
//
|
||||
// The returned room name is *not* normalized, that is, it preserve case.
|
||||
//
|
||||
// Throw std::invalid_argument if anything in the URL is unparseable or invalid.
|
||||
static std::tuple<std::string, std::string, ustring> parse_full_url(std::string_view full_url);
|
||||
|
||||
protected:
|
||||
// The canonical base url and room (i.e. lower-cased, URL cleaned up):
|
||||
std::string base_url_, room_;
|
||||
// The localized token of this room, that is, with case preserved (so `room_` could be
|
||||
// `someroom` and this could `SomeRoom`). Omitted if not available.
|
||||
std::optional<std::string> localized_room_;
|
||||
// server pubkey
|
||||
ustring pubkey_;
|
||||
|
||||
// Construction without a pubkey for when pubkey isn't known yet but will be set shortly
|
||||
// after constructing (or when isn't needed, such as when deleting).
|
||||
community(std::string_view base_url, std::string_view room);
|
||||
};
|
||||
|
||||
struct comm_iterator_helper {
|
||||
|
||||
comm_iterator_helper(dict::const_iterator it_server, dict::const_iterator end_server) :
|
||||
it_server{std::move(it_server)}, end_server{std::move(end_server)} {}
|
||||
|
||||
std::optional<dict::const_iterator> it_server, end_server, it_room, end_room;
|
||||
|
||||
bool operator==(const comm_iterator_helper& other) const {
|
||||
return it_server == other.it_server && it_room == other.it_room;
|
||||
}
|
||||
|
||||
void next_server() {
|
||||
++*it_server;
|
||||
it_room.reset();
|
||||
end_room.reset();
|
||||
}
|
||||
|
||||
bool done() const { return !it_server || *it_server == *end_server; }
|
||||
|
||||
template <typename Comm, typename Any>
|
||||
bool load(std::shared_ptr<Any>& val) {
|
||||
while (it_server) {
|
||||
if (*it_server == *end_server) {
|
||||
it_server.reset();
|
||||
end_server.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& [base_url, server_info] = **it_server;
|
||||
auto* server_info_dict = std::get_if<dict>(&server_info);
|
||||
if (!server_info_dict) {
|
||||
next_server();
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string* pubkey_raw = nullptr;
|
||||
if (auto pubkey_it = server_info_dict->find("#"); pubkey_it != server_info_dict->end())
|
||||
if (auto* pk_sc = std::get_if<scalar>(&pubkey_it->second))
|
||||
pubkey_raw = std::get_if<std::string>(pk_sc);
|
||||
|
||||
if (!pubkey_raw) {
|
||||
next_server();
|
||||
continue;
|
||||
}
|
||||
|
||||
ustring_view pubkey{
|
||||
reinterpret_cast<const unsigned char*>(pubkey_raw->data()), pubkey_raw->size()};
|
||||
|
||||
if (!it_room) {
|
||||
if (auto rit = server_info_dict->find("R");
|
||||
rit != server_info_dict->end() && std::holds_alternative<dict>(rit->second)) {
|
||||
auto& rooms_dict = std::get<dict>(rit->second);
|
||||
it_room = rooms_dict.begin();
|
||||
end_room = rooms_dict.end();
|
||||
} else {
|
||||
next_server();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
while (it_room) {
|
||||
if (*it_room == *end_room) {
|
||||
it_room.reset();
|
||||
end_room.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
auto& [room, data] = **it_room;
|
||||
auto* data_dict = std::get_if<dict>(&data);
|
||||
if (!data_dict) {
|
||||
++*it_room;
|
||||
continue;
|
||||
}
|
||||
|
||||
val = std::make_shared<Any>(Comm{});
|
||||
auto& og = std::get<Comm>(*val);
|
||||
try {
|
||||
og.set_base_url(base_url);
|
||||
og.set_room(room); // Will be replaced with "n" in the `.load` below
|
||||
og.set_pubkey(pubkey);
|
||||
og.load(*data_dict);
|
||||
} catch (const std::exception& e) {
|
||||
++*it_room;
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
++*it_server;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool advance() {
|
||||
if (it_room) {
|
||||
++*it_room;
|
||||
return true;
|
||||
}
|
||||
if (it_server) {
|
||||
++*it_server;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -5,20 +5,27 @@ extern "C" {
|
|||
#endif
|
||||
|
||||
#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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
typedef enum CONVO_EXPIRATION_MODE {
|
||||
CONVO_EXPIRATION_NONE = 0,
|
||||
CONVO_EXPIRATION_AFTER_SEND = 1,
|
||||
CONVO_EXPIRATION_AFTER_READ = 2,
|
||||
} CONVO_EXPIRATION_MODE;
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace session::config {
|
||||
|
||||
enum class expiration_mode : int8_t { none = 0, after_send = 1, after_read = 2 };
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ enum class Namespace : std::int16_t {
|
|||
UserProfile = 2,
|
||||
Contacts = 3,
|
||||
ConvoInfoVolatile = 4,
|
||||
ClosedGroupInfo = 11,
|
||||
UserGroups = 5,
|
||||
};
|
||||
|
||||
} // namespace session::config
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "base.h"
|
||||
#include "util.h"
|
||||
|
||||
typedef struct ugroups_legacy_group_info {
|
||||
char session_id[67]; // in hex; 66 hex chars + null terminator.
|
||||
|
||||
char name[101]; // Null-terminated C string (human-readable). Max length is 511. Will always
|
||||
// be set (even if an empty string).
|
||||
|
||||
bool have_enc_keys; // Will be true if we have an encryption keypair, false if not.
|
||||
unsigned char enc_pubkey[32]; // If `have_enc_keys`, this is the 32-byte pubkey
|
||||
unsigned char enc_seckey[32]; // If `have_enc_keys`, this is the 32-byte secret key
|
||||
|
||||
int64_t disappearing_timer; // Minutes. 0 == disabled.
|
||||
bool hidden; // true if hidden from the convo list
|
||||
int priority; // pinned message priority; 0 = unpinned, larger means pinned higher (i.e. higher
|
||||
// priority conversations come first).
|
||||
} ugroups_legacy_group_info;
|
||||
|
||||
typedef struct ugroups_community_info {
|
||||
char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case,
|
||||
// only has port if non-default, has trailing / removed)
|
||||
char room[65]; // null-terminated (max length 64); this is case-preserving (i.e. can be
|
||||
// "SomeRoom" instead of "someroom". Note this is different from volatile
|
||||
// info (that one is always forced lower-cased).
|
||||
unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls)
|
||||
|
||||
int priority; // pinned message priority; 0 = unpinned, larger means pinned higher (i.e. higher
|
||||
// priority conversations come first).
|
||||
} ugroups_community_info;
|
||||
|
||||
int user_groups_init(
|
||||
config_object** conf,
|
||||
const unsigned char* ed25519_secretkey,
|
||||
const unsigned char* dump,
|
||||
size_t dumplen,
|
||||
char* error) __attribute__((warn_unused_result));
|
||||
|
||||
/// Gets community conversation info into `comm`, if the community info was found. `base_url` and
|
||||
/// `room` are null-terminated c strings; pubkey is 32 bytes. base_url will be
|
||||
/// normalized/lower-cased; room is case-insensitive for the lookup: note that this may well return
|
||||
/// a community info with a different room capitalization than the one provided to the call.
|
||||
///
|
||||
/// Returns true if the community was found and `comm` populated; false otherwise.
|
||||
bool user_groups_get_community(
|
||||
const config_object* conf,
|
||||
ugroups_community_info* comm,
|
||||
const char* base_url,
|
||||
const char* room) __attribute__((warn_unused_result));
|
||||
|
||||
/// Like the above, but if the community was not found, this constructs one that can be inserted.
|
||||
/// `base_url` will be normalized in the returned object. `room` is a case-insensitive lookup key
|
||||
/// for the room token. Note that it has subtle handling w.r.t its case: if an existing room is
|
||||
/// found, you get back a record with the found case (which could differ in case from what you
|
||||
/// provided). If you want to override to what you provided regardless of what is there you should
|
||||
/// immediately set the name of the returned object to the case you prefer. If a *new* record is
|
||||
/// constructed, however, it will match the room token case as given here.
|
||||
///
|
||||
/// Note that this is all different from convo_info_volatile, which always forces the room token to
|
||||
/// lower-case (because it does not preserve the case).
|
||||
bool user_groups_get_or_construct_community(
|
||||
const config_object* conf,
|
||||
ugroups_community_info* comm,
|
||||
const char* base_url,
|
||||
const char* room,
|
||||
unsigned const char* pubkey) __attribute__((warn_unused_result));
|
||||
|
||||
/// Fills `group` with the conversation info given a legacy group ID (specified as a null-terminated
|
||||
/// hex string), if the conversation exists, and returns true. If the conversation does not exist
|
||||
/// then `group` is left unchanged and false is returned.
|
||||
bool user_groups_get_legacy_group(
|
||||
const config_object* conf, ugroups_legacy_group_info* group, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Same as the above except that when the conversation does not exist, this sets all the group
|
||||
/// fields to defaults and loads it with the given id.
|
||||
///
|
||||
/// Returns true as long as it is given a valid legacy group group id (i.e. same format as a session
|
||||
/// id). A false return is considered an error, and means the id was not a valid session id.
|
||||
///
|
||||
/// This is the method that should usually be used to create or update a conversation, followed by
|
||||
/// setting fields in the group, and then giving it to user_groups_set().
|
||||
bool user_groups_get_or_construct_legacy_group(
|
||||
const config_object* conf, ugroups_legacy_group_info* group, const char* id)
|
||||
__attribute__((warn_unused_result));
|
||||
|
||||
/// Adds or updates a conversation from the given group info
|
||||
void user_groups_set_community(config_object* conf, const ugroups_community_info* group);
|
||||
void user_groups_set_legacy_group(config_object* conf, const ugroups_legacy_group_info* group);
|
||||
|
||||
/// Erases a conversation from the conversation list. Returns true if the conversation was found
|
||||
/// and removed, false if the conversation was not present. You must not call this during
|
||||
/// iteration; see details below.
|
||||
bool user_groups_erase_community(config_object* conf, const char* base_url, const char* room);
|
||||
bool user_groups_erase_legacy_group(config_object* conf, const char* group_id);
|
||||
|
||||
/// Returns the number of conversations.
|
||||
size_t user_groups_size(const config_object* conf);
|
||||
/// Returns the number of conversations of the specific type.
|
||||
size_t user_groups_size_communities(const config_object* conf);
|
||||
size_t user_groups_size_legacy_groups(const config_object* conf);
|
||||
|
||||
/// Functions for iterating through the entire conversation list. Intended use is:
|
||||
///
|
||||
/// ugroups_community_info c2;
|
||||
/// ugroups_legacy_group_info c3;
|
||||
/// user_groups_iterator *it = user_groups_iterator_new(my_groups);
|
||||
/// for (; !user_groups_iterator_done(it); user_groups_iterator_advance(it)) {
|
||||
/// if (user_groups_it_is_community(it, &c2)) {
|
||||
/// // use c2.whatever
|
||||
/// } else if (user_groups_it_is_legacy_group(it, &c3)) {
|
||||
/// // use c3.whatever
|
||||
/// }
|
||||
/// }
|
||||
/// user_groups_iterator_free(it);
|
||||
///
|
||||
/// It is permitted to modify records (e.g. with a call to one of the `user_groups_set_*`
|
||||
/// functions) and add records while iterating.
|
||||
///
|
||||
/// If you need to remove while iterating then usage is slightly different: you must advance the
|
||||
/// iteration by calling either user_groups_iterator_advance if not deleting, or
|
||||
/// user_groups_iterator_erase to erase and advance. Usage looks like this:
|
||||
///
|
||||
/// ugroups_community_info comm;
|
||||
/// ugroups_iterator *it = ugroups_iterator_new(my_groups);
|
||||
/// while (!user_groups_iterator_done(it)) {
|
||||
/// if (user_groups_it_is_community(it, &comm)) {
|
||||
/// bool should_delete = /* ... */;
|
||||
/// if (should_delete)
|
||||
/// user_groups_iterator_erase(it);
|
||||
/// else
|
||||
/// user_groups_iterator_advance(it);
|
||||
/// } else {
|
||||
/// user_groups_iterator_advance(it);
|
||||
/// }
|
||||
/// }
|
||||
/// user_groups_iterator_free(it);
|
||||
///
|
||||
|
||||
typedef struct user_groups_iterator user_groups_iterator;
|
||||
|
||||
// Starts a new iterator that iterates over all conversations.
|
||||
user_groups_iterator* user_groups_iterator_new(const config_object* conf);
|
||||
|
||||
// The same as `user_groups_iterator_new` except that this iterates *only* over one type of
|
||||
// conversation. You still need to use `user_groups_it_is_community` (or the alternatives)
|
||||
// to load the data in each pass of the loop. (You can, however, safely ignore the bool return
|
||||
// value of the `it_is_whatever` function: it will always be true for the particular type being
|
||||
// iterated over).
|
||||
user_groups_iterator* user_groups_iterator_new_communities(const config_object* conf);
|
||||
user_groups_iterator* user_groups_iterator_new_legacy_groups(const config_object* conf);
|
||||
|
||||
// Frees an iterator once no longer needed.
|
||||
void user_groups_iterator_free(user_groups_iterator* it);
|
||||
|
||||
// Returns true if iteration has reached the end.
|
||||
bool user_groups_iterator_done(user_groups_iterator* it);
|
||||
|
||||
// Advances the iterator.
|
||||
void user_groups_iterator_advance(user_groups_iterator* it);
|
||||
|
||||
// If the current iterator record is a community conversation this sets the details into `c` and
|
||||
// returns true. Otherwise it returns false.
|
||||
bool user_groups_it_is_community(user_groups_iterator* it, ugroups_community_info* c);
|
||||
|
||||
// If the current iterator record is a legacy group conversation this sets the details into
|
||||
// `c` and returns true. Otherwise it returns false.
|
||||
bool user_groups_it_is_legacy_group(user_groups_iterator* it, ugroups_legacy_group_info* c);
|
||||
|
||||
// Erases the current group while advancing the iterator to the next group in the iteration.
|
||||
void user_groups_iterator_erase(config_object* conf, user_groups_iterator* it);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -0,0 +1,335 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <session/config.hpp>
|
||||
|
||||
#include "base.hpp"
|
||||
#include "community.hpp"
|
||||
#include "namespaces.hpp"
|
||||
|
||||
extern "C" {
|
||||
struct ugroups_legacy_group_info;
|
||||
struct ugroups_community_info;
|
||||
}
|
||||
|
||||
namespace session::config {
|
||||
|
||||
/// keys used in this config, either currently or in the past (so that we don't reuse):
|
||||
///
|
||||
/// C - dict of legacy groups; within this dict each key is the group pubkey (binary, 33 bytes) and
|
||||
/// value is a dict containing keys:
|
||||
///
|
||||
/// n - name (string). Always set, even if empty.
|
||||
/// k - encryption public key (32 bytes). Optional.
|
||||
/// K - encryption secret key (32 bytes). Optional.
|
||||
/// m - set of member session ids (each 33 bytes).
|
||||
/// a - set of admin session ids (each 33 bytes).
|
||||
/// E - disappearing messages duration, in minutes, > 0. Omitted if disappearing messages is
|
||||
/// disabled. (Note that legacy groups only support expire after-read)
|
||||
/// h - hidden: 1 if the conversation has been removed from the conversation list, omitted if
|
||||
/// visible.
|
||||
/// + - the conversation priority, for pinned messages. Omitted means not pinned; otherwise an
|
||||
/// integer value >0, where a higher priority means the conversation is meant to appear
|
||||
/// earlier in the pinned conversation list.
|
||||
///
|
||||
/// o - dict of communities (AKA open groups); within this dict (which deliberately has the same
|
||||
/// layout as convo_info_volatile) each key is the SOGS base URL (in canonical form), and value
|
||||
/// is a dict of:
|
||||
///
|
||||
/// # - server pubkey
|
||||
/// R - dict of rooms on the server. Each key is the *lower-case* room name; each value is:
|
||||
/// n - the room name as is commonly used, i.e. with possible capitalization (if
|
||||
/// appropriate). For instance, a room name SudokuSolvers would be "sudokusolvers" in
|
||||
/// the outer key, with the capitalization variation in use ("SudokuSolvers") in this
|
||||
/// key. This key is *always* present (to keep the room dict non-empty).
|
||||
/// + - the conversation priority, for pinned messages. Omitted means not pinned; otherwise
|
||||
/// an integer value >0, where a higher priority means the conversation is meant to
|
||||
/// appear earlier in the pinned conversation list.
|
||||
///
|
||||
/// c - reserved for future storage of new-style group info.
|
||||
|
||||
/// Struct containing legacy group info (aka "closed groups").
|
||||
struct legacy_group_info {
|
||||
static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded
|
||||
|
||||
std::string session_id; // The legacy group "session id" (33 bytes).
|
||||
std::string name; // human-readable; this should normally always be set, but in theory could be
|
||||
// set to an empty string.
|
||||
ustring enc_pubkey; // bytes (32 or empty)
|
||||
ustring enc_seckey; // bytes (32 or empty)
|
||||
std::chrono::minutes disappearing_timer{0}; // 0 == disabled.
|
||||
bool hidden = false; // true if the conversation is hidden from the convo list
|
||||
int priority = 0; // The priority; 0 means unpinned, larger means pinned higher (i.e.
|
||||
// higher priority conversations come first).
|
||||
|
||||
/// Constructs a new legacy group info from an id (which must look like a session_id). Throws
|
||||
/// if id is invalid.
|
||||
explicit legacy_group_info(std::string sid);
|
||||
|
||||
// Accesses the session ids (in hex) of members of this group. The key is the hex session_id;
|
||||
// the value indicates whether the member is an admin (true) or not (false).
|
||||
const std::map<std::string, bool>& members() const { return members_; }
|
||||
|
||||
// Returns a pair of the number of admins, and regular members of this group. (If all you want
|
||||
// is the overall number just use `.members().size()` instead).
|
||||
std::pair<size_t, size_t> counts() const;
|
||||
|
||||
// Adds a member (by session id and admin status) to this group. Returns true if the member was
|
||||
// inserted or changed admin status, false if the member already existed. Throws
|
||||
// std::invalid_argument if the given session id is invalid.
|
||||
bool insert(std::string session_id, bool admin);
|
||||
|
||||
// Removes a member (by session id) from this group. Returns true if the member was
|
||||
// removed, false if the member was not present.
|
||||
bool erase(const std::string& session_id);
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
legacy_group_info(const struct ugroups_legacy_group_info& c); // From c struct
|
||||
void into(struct ugroups_legacy_group_info& c) const; // Into c struct
|
||||
|
||||
private:
|
||||
// session_id => (is admin)
|
||||
std::map<std::string, bool> members_;
|
||||
|
||||
friend class UserGroups;
|
||||
|
||||
void load(const dict& info_dict);
|
||||
};
|
||||
|
||||
/// Community (aka open group) info
|
||||
struct community_info : community {
|
||||
// Note that *changing* url/room/pubkey and then doing a set inserts a new room under the given
|
||||
// url/room/pubkey, it does *not* update an existing room.
|
||||
|
||||
// See community_base (comm_base.hpp) for common constructors
|
||||
using community::community;
|
||||
|
||||
// Internal ctor/method for C API implementations:
|
||||
community_info(const struct ugroups_community_info& c); // From c struct
|
||||
void into(ugroups_community_info& c) const; // Into c struct
|
||||
|
||||
int priority = 0; // The priority; 0 means unpinned, larger means pinned higher (i.e.
|
||||
// higher priority conversations come first).
|
||||
|
||||
private:
|
||||
void load(const dict& info_dict);
|
||||
|
||||
friend class UserGroups;
|
||||
friend class comm_iterator_helper;
|
||||
};
|
||||
|
||||
using any_group_info = std::variant<community_info, legacy_group_info>;
|
||||
|
||||
class UserGroups : public ConfigBase {
|
||||
|
||||
public:
|
||||
// No default constructor
|
||||
UserGroups() = delete;
|
||||
|
||||
/// Constructs a user group list from existing data (stored from `dump()`) and the user's
|
||||
/// secret key for generating the data encryption key. To construct a blank list (i.e. with no
|
||||
/// pre-existing dumped data to load) pass `std::nullopt` as the second argument.
|
||||
///
|
||||
/// \param ed25519_secretkey - contains the libsodium secret key used to encrypt/decrypt the
|
||||
/// data when pushing/pulling from the swarm. This can either be the full 64-byte value (which
|
||||
/// is technically the 32-byte seed followed by the 32-byte pubkey), or just the 32-byte seed of
|
||||
/// the secret key.
|
||||
///
|
||||
/// \param dumped - either `std::nullopt` to construct a new, empty object; or binary state data
|
||||
/// that was previously dumped from an instance of this class by calling `dump()`.
|
||||
UserGroups(ustring_view ed25519_secretkey, std::optional<ustring_view> dumped);
|
||||
|
||||
Namespace storage_namespace() const override { return Namespace::UserGroups; }
|
||||
|
||||
const char* encryption_domain() const override { return "UserGroups"; }
|
||||
|
||||
/// Looks up and returns a community (aka open group) conversation. Takes the base URL and room
|
||||
/// token (case insensitive). Retuns nullopt if the open group was not found, otherwise a
|
||||
/// filled out `community_info`. Note that the `room` argument here is case-insensitive, but
|
||||
/// the returned value will be the room as stored in the object (i.e. it may have a different
|
||||
/// case from the requested `room` value).
|
||||
std::optional<community_info> get_community(
|
||||
std::string_view base_url, std::string_view room) const;
|
||||
|
||||
/// Looks up and returns a legacy group by group ID (hex, looks like a Session ID). Returns
|
||||
/// nullopt if the group was not found, otherwise returns a filled out `legacy_group_info`.
|
||||
std::optional<legacy_group_info> get_legacy_group(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Same as `get_community`, except if the community isn't found a new blank one is created for
|
||||
/// you, prefilled with the url/room/pubkey.
|
||||
///
|
||||
/// Note that `room` and `pubkey` have special handling:
|
||||
/// - `room` is case-insensitive for the lookup: if a matching room is found then the returned
|
||||
/// value reflects the room case of the existing record, which is not necessarily the same as
|
||||
/// the `room` argument given here (to force a case change, set it within the returned
|
||||
/// object).
|
||||
/// - `pubkey` is not used to find an existing community, but if the community found has a
|
||||
/// *different* pubkey from the one given then the returned record has its pubkey updated in
|
||||
/// the return instance (note that this changed value is not committed to storage, however,
|
||||
/// until the instance is passed to `set()`). For the string_view version the pubkey is
|
||||
/// accepted as hex, base32z, or base64.
|
||||
community_info get_or_construct_community(
|
||||
std::string_view base_url,
|
||||
std::string_view room,
|
||||
std::string_view pubkey_encoded) const;
|
||||
community_info get_or_construct_community(
|
||||
std::string_view base_url, std::string_view room, ustring_view pubkey) const;
|
||||
|
||||
/// Gets or constructs a blank legacy_group_info for the given group id.
|
||||
legacy_group_info get_or_construct_legacy_group(std::string_view pubkey_hex) const;
|
||||
|
||||
/// Inserts or replaces existing group info. For example, to update the info for a community
|
||||
/// you would do:
|
||||
///
|
||||
/// auto info = conversations.get_or_construct_community(some_session_id);
|
||||
/// info.last_read = new_unix_timestamp;
|
||||
/// conversations.set(info);
|
||||
///
|
||||
void set(const community_info& info);
|
||||
void set(const legacy_group_info& info);
|
||||
/// Takes a variant of either group type to set:
|
||||
void set(const any_group_info& info);
|
||||
|
||||
protected:
|
||||
// Drills into the nested dicts to access open group details
|
||||
DictFieldProxy community_field(
|
||||
const community_info& og, ustring_view* get_pubkey = nullptr) const;
|
||||
|
||||
public:
|
||||
/// Removes a community group. Returns true if found and removed, false if not present.
|
||||
/// Arguments are the same as `get_community`.
|
||||
bool erase_community(std::string_view base_url, std::string_view room);
|
||||
|
||||
/// Removes a legacy group conversation. Returns true if found and removed, false if not
|
||||
/// present.
|
||||
bool erase_legacy_group(std::string_view pubkey_hex);
|
||||
|
||||
/// Removes a conversation taking the community_info or legacy_group_info instance (rather than
|
||||
/// the pubkey/url) for convenience.
|
||||
bool erase(const community_info& g);
|
||||
bool erase(const legacy_group_info& c);
|
||||
bool erase(const any_group_info& info);
|
||||
|
||||
struct iterator;
|
||||
|
||||
/// This works like erase, but takes an iterator to the group to remove. The element is removed
|
||||
/// and the iterator to the next element after the removed one is returned. This is intended
|
||||
/// for use where elements are to be removed during iteration: see below for an example.
|
||||
iterator erase(iterator it);
|
||||
|
||||
/// Returns the number of groups (of any type).
|
||||
size_t size() const;
|
||||
|
||||
/// Returns the number of communities
|
||||
size_t size_communities() const;
|
||||
|
||||
/// Returns the number of legacy groups
|
||||
size_t size_legacy_groups() const;
|
||||
|
||||
/// Returns true if the group list is empty.
|
||||
bool empty() const { return size() == 0; }
|
||||
|
||||
/// Iterators for iterating through all groups. Typically you access this implicit via a
|
||||
/// for loop over the `UserGroups` object:
|
||||
///
|
||||
/// for (auto& group : usergroups) {
|
||||
/// if (auto* comm = std::get_if<community_info>(&group)) {
|
||||
/// // use comm->name, comm->priority, etc.
|
||||
/// } else if (auto* lg = std::get_if<legacy_group_info>(&convo)) {
|
||||
/// // use lg->session_id, lg->hidden, etc.
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// This iterates through all groups in sorted order (sorted first by convo type, then by
|
||||
/// id within the type).
|
||||
///
|
||||
/// It is permitted to modify and add records while iterating (e.g. by modifying one of the
|
||||
/// `comm`/`lg` objects and then calling set()).
|
||||
///
|
||||
/// If you need to erase the current conversation during iteration then care is required: you
|
||||
/// need to advance the iterator via the iterator version of erase when erasing an element
|
||||
/// rather than incrementing it regularly. For example:
|
||||
///
|
||||
/// for (auto it = conversations.begin(); it != conversations.end(); ) {
|
||||
/// if (should_remove(*it))
|
||||
/// it = converations.erase(it);
|
||||
/// else
|
||||
/// ++it;
|
||||
/// }
|
||||
///
|
||||
/// Alternatively, you can use the first version with two loops: the first loop through all
|
||||
/// converations doesn't erase but just builds a vector of IDs to erase, then the second loops
|
||||
/// through that vector calling `erase_1to1()`/`erase_open()`/`erase_legacy_group()` for each
|
||||
/// one.
|
||||
///
|
||||
iterator begin() const { return iterator{data}; }
|
||||
iterator end() const { return iterator{}; }
|
||||
|
||||
template <typename GroupType>
|
||||
struct subtype_iterator;
|
||||
|
||||
/// Returns an iterator that iterates only through one type of conversations. (The regular
|
||||
/// `.end()` iterator is valid for testing the end of these iterations).
|
||||
subtype_iterator<community_info> begin_communities() const { return {data}; }
|
||||
subtype_iterator<legacy_group_info> begin_legacy_groups() const { return {data}; }
|
||||
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
using value_type = std::variant<community_info, legacy_group_info>;
|
||||
using reference = value_type&;
|
||||
using pointer = value_type*;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
struct iterator {
|
||||
protected:
|
||||
std::shared_ptr<any_group_info> _val;
|
||||
std::optional<comm_iterator_helper> _it_comm;
|
||||
std::optional<dict::const_iterator> _it_legacy, _end_legacy;
|
||||
void _load_val();
|
||||
iterator() = default; // Constructs an end tombstone
|
||||
explicit iterator(
|
||||
const DictFieldRoot& data, bool communities = true, bool legacy_closed = true);
|
||||
friend class UserGroups;
|
||||
|
||||
public:
|
||||
bool operator==(const iterator& other) const;
|
||||
bool operator!=(const iterator& other) const { return !(*this == other); }
|
||||
bool done() const; // Equivalent to comparing against the end iterator
|
||||
any_group_info& operator*() const { return *_val; }
|
||||
any_group_info* operator->() const { return _val.get(); }
|
||||
iterator& operator++();
|
||||
iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename GroupType>
|
||||
struct subtype_iterator : iterator {
|
||||
protected:
|
||||
subtype_iterator(const DictFieldRoot& data) :
|
||||
iterator(
|
||||
data,
|
||||
std::is_same_v<community_info, GroupType>,
|
||||
std::is_same_v<legacy_group_info, GroupType>) {}
|
||||
friend class UserGroups;
|
||||
|
||||
public:
|
||||
GroupType& operator*() const { return std::get<GroupType>(*_val); }
|
||||
GroupType* operator->() const { return &std::get<GroupType>(*_val); }
|
||||
subtype_iterator& operator++() {
|
||||
iterator::operator++();
|
||||
return *this;
|
||||
}
|
||||
subtype_iterator operator++(int) {
|
||||
auto copy{*this};
|
||||
++*this;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace session::config
|
|
@ -44,9 +44,9 @@ class UserProfile final : public ConfigBase {
|
|||
/// Sets the user profile name; if given an empty string then the name is removed.
|
||||
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.
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/// libsession-util version triplet (major, minor, patch)
|
||||
extern const uint16_t LIBSESSION_UTIL_VERSION[3];
|
||||
|
||||
/// Printable full libsession-util name and version string, such as `libsession-util v0.1.2-release`
|
||||
/// for a tagged release or `libsession-util v0.1.2-7f144eb5` for an untagged build.
|
||||
extern const char* LIBSESSION_UTIL_VERSION_FULL;
|
||||
|
||||
/// Just the version component as a string, e.g. `v0.1.2-release`.
|
||||
extern const char* LIBSESSION_UTIL_VERSION_STR;
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
|
@ -373,7 +373,7 @@ public extension ClosedGroupControlMessage.Kind {
|
|||
let addedMemberNames: [String] = memberIds
|
||||
.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() :
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -277,7 +277,7 @@ message SharedConfigMessage {
|
|||
USER_PROFILE = 1;
|
||||
CONTACTS = 2;
|
||||
CONVO_INFO_VOLATILE = 3;
|
||||
GROUPS = 4;
|
||||
USER_GROUPS = 4;
|
||||
}
|
||||
|
||||
// @required
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)'"))
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)"))
|
||||
|
|
|
@ -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])
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
@testable import SessionMessagingKit
|
||||
|
||||
class TypeConversionUtilitiesSpec: QuickSpec {
|
||||
// MARK: - Spec
|
||||
|
||||
override func spec() {
|
||||
// MARK: - String
|
||||
|
||||
describe("a String") {
|
||||
it("can convert to a cArray") {
|
||||
expect("Test123".cArray).to(equal([84, 101, 115, 116, 49, 50, 51]))
|
||||
}
|
||||
|
||||
context("when initialised with a pointer and length") {
|
||||
it("returns null when given a null pointer") {
|
||||
let test: [CChar] = [84, 101, 115, 116]
|
||||
let result = test.withUnsafeBufferPointer { ptr in
|
||||
String(pointer: nil, length: 5)
|
||||
}
|
||||
|
||||
expect(result).to(beNil())
|
||||
}
|
||||
|
||||
it("returns a truncated string when given an incorrect length") {
|
||||
let test: [CChar] = [84, 101, 115, 116]
|
||||
let result = test.withUnsafeBufferPointer { ptr in
|
||||
String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 2)
|
||||
}
|
||||
|
||||
expect(result).to(equal("Te"))
|
||||
}
|
||||
|
||||
it("returns a string when valid") {
|
||||
let test: [CChar] = [84, 101, 115, 116]
|
||||
let result = test.withUnsafeBufferPointer { ptr in
|
||||
String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 5)
|
||||
}
|
||||
|
||||
expect(result).to(equal("Test\0"))
|
||||
}
|
||||
}
|
||||
|
||||
context("when initialised with a libSession value") {
|
||||
it("returns a string when valid and null terminated") {
|
||||
let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 115, 116, 0)
|
||||
let result = String(libSessionVal: value, nullTerminated: true)
|
||||
|
||||
expect(result).to(equal("Test"))
|
||||
}
|
||||
|
||||
it("returns a string when valid and not null terminated") {
|
||||
let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 115, 116)
|
||||
let result = String(libSessionVal: value, nullTerminated: false)
|
||||
|
||||
expect(result).to(equal("Te\0st"))
|
||||
}
|
||||
|
||||
it("returns an empty string when null and not set to return null") {
|
||||
let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0)
|
||||
let result = String(libSessionVal: value, nullIfEmpty: false)
|
||||
|
||||
expect(result).to(equal(""))
|
||||
}
|
||||
|
||||
it("returns null when specified and empty") {
|
||||
let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0)
|
||||
let result = String(libSessionVal: value, nullIfEmpty: true)
|
||||
|
||||
expect(result).to(beNil())
|
||||
}
|
||||
|
||||
it("defaults the null terminated flag to true") {
|
||||
let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 0, 0)
|
||||
let result = String(libSessionVal: value)
|
||||
|
||||
expect(result).to(equal("Te"))
|
||||
}
|
||||
|
||||
it("defaults the null if empty flag to false") {
|
||||
let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0)
|
||||
let result = String(libSessionVal: value)
|
||||
|
||||
expect(result).to(equal(""))
|
||||
}
|
||||
}
|
||||
|
||||
it("can convert to a libSession value") {
|
||||
let result: (CChar, CChar, CChar, CChar, CChar) = "Test".toLibSession()
|
||||
expect(result.0).to(equal(84))
|
||||
expect(result.1).to(equal(101))
|
||||
expect(result.2).to(equal(115))
|
||||
expect(result.3).to(equal(116))
|
||||
expect(result.4).to(equal(0))
|
||||
}
|
||||
|
||||
context("when optional") {
|
||||
context("returns null when null") {
|
||||
let value: String? = nil
|
||||
let result: (CChar, CChar, CChar, CChar, CChar)? = value?.toLibSession()
|
||||
|
||||
expect(result).to(beNil())
|
||||
}
|
||||
|
||||
context("returns a libSession value when not null") {
|
||||
let value: String? = "Test"
|
||||
let result: (CChar, CChar, CChar, CChar, CChar)? = value?.toLibSession()
|
||||
|
||||
expect(result?.0).to(equal(84))
|
||||
expect(result?.1).to(equal(101))
|
||||
expect(result?.2).to(equal(115))
|
||||
expect(result?.3).to(equal(116))
|
||||
expect(result?.4).to(equal(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
describe("Data") {
|
||||
it("can convert to a cArray") {
|
||||
expect(Data([1, 2, 3]).cArray).to(equal([1, 2, 3]))
|
||||
}
|
||||
|
||||
context("when initialised with a libSession value") {
|
||||
it("returns truncated data when given the wrong length") {
|
||||
let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5)
|
||||
let result = Data(libSessionVal: value, count: 2)
|
||||
|
||||
expect(result).to(equal(Data([1, 2])))
|
||||
}
|
||||
|
||||
it("returns data when valid") {
|
||||
let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5)
|
||||
let result = Data(libSessionVal: value, count: 5)
|
||||
|
||||
expect(result).to(equal(Data([1, 2, 3, 4, 5])))
|
||||
}
|
||||
}
|
||||
|
||||
it("can convert to a libSession value") {
|
||||
let result: (Int8, Int8, Int8, Int8, Int8) = Data([1, 2, 3, 4, 5]).toLibSession()
|
||||
expect(result.0).to(equal(1))
|
||||
expect(result.1).to(equal(2))
|
||||
expect(result.2).to(equal(3))
|
||||
expect(result.3).to(equal(4))
|
||||
expect(result.4).to(equal(5))
|
||||
}
|
||||
|
||||
context("when optional") {
|
||||
context("returns null when null") {
|
||||
let value: Data? = nil
|
||||
let result: (Int8, Int8, Int8, Int8, Int8)? = value?.toLibSession()
|
||||
|
||||
expect(result).to(beNil())
|
||||
}
|
||||
|
||||
context("returns a libSession value when not null") {
|
||||
let value: Data? = Data([1, 2, 3, 4, 5])
|
||||
let result: (Int8, Int8, Int8, Int8, Int8)? = value?.toLibSession()
|
||||
|
||||
expect(result?.0).to(equal(1))
|
||||
expect(result?.1).to(equal(2))
|
||||
expect(result?.2).to(equal(3))
|
||||
expect(result?.3).to(equal(4))
|
||||
expect(result?.4).to(equal(5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array
|
||||
|
||||
describe("an Array") {
|
||||
context("when adding a null terminated character") {
|
||||
it("adds a null termination character when not present") {
|
||||
let value: [CChar] = [1, 2, 3, 4, 5]
|
||||
|
||||
expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 5, 0]))
|
||||
}
|
||||
|
||||
it("adds nothing when already present") {
|
||||
let value: [CChar] = [1, 2, 3, 4, 0]
|
||||
|
||||
expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue