From 83ccd7eaef924b93d2486e9566dab738b215713c Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 2 Nov 2017 14:20:19 +0100 Subject: [PATCH 1/2] Universal, extensible client message storage implementation This commit represents squashed 'wip/universal-message-storage-modules' branch --- Core/XMPPFramework.h | 24 +- Core/XMPPInternal.h | 1 + Core/XMPPStream.h | 115 +- Core/XMPPStream.m | 399 ++-- .../NSManagedObject+XMPPCoreDataStorage.h | 29 + .../NSManagedObject+XMPPCoreDataStorage.m | 65 + .../CoreDataStorage/XMPPCoreDataStorage.m | 2 +- .../XMPPMessage.xcdatamodel/elements | Bin 0 -> 134828 bytes .../XMPPMessage.xcdatamodel/layout | Bin 0 -> 17560 bytes ...geContextCoreDataStorageObject+Protected.h | 50 + .../XMPPMessageContextCoreDataStorageObject.h | 15 + .../XMPPMessageContextCoreDataStorageObject.m | 18 + ...ntextItemCoreDataStorageObject+Protected.h | 91 + ...PMessageContextItemCoreDataStorageObject.h | 151 ++ ...PMessageContextItemCoreDataStorageObject.m | 194 ++ .../XMPPMessageCoreDataStorage.h | 65 + .../XMPPMessageCoreDataStorage.m | 182 ++ ...sageCoreDataStorageObject+ContextHelpers.h | 94 + ...sageCoreDataStorageObject+ContextHelpers.m | 221 +++ ...PPMessageCoreDataStorageObject+Protected.h | 83 + .../XMPPMessageCoreDataStorageObject.h | 86 + .../XMPPMessageCoreDataStorageObject.m | 536 +++++ ...PMessageCoreDataStorage+XMPPOneToOneChat.h | 17 + ...PMessageCoreDataStorage+XMPPOneToOneChat.m | 22 + Extensions/OneToOneChat/XMPPOneToOneChat.h | 27 + Extensions/OneToOneChat/XMPPOneToOneChat.m | 40 + .../XMPPMessageCoreDataStorage+XEP_0066.h | 49 + .../XMPPMessageCoreDataStorage+XEP_0066.m | 80 + .../XEP-0066/XMPPOutOfBandResourceMessaging.h | 28 + .../XEP-0066/XMPPOutOfBandResourceMessaging.m | 76 + .../XMPPMessageCoreDataStorage+XEP_0184.h | 29 + .../XMPPMessageCoreDataStorage+XEP_0184.m | 65 + .../XEP-0184/XMPPMessageDeliveryReceipts.h | 16 + .../XEP-0184/XMPPMessageDeliveryReceipts.m | 5 + .../Managed Messaging/XMPPManagedMessaging.h | 34 + .../Managed Messaging/XMPPManagedMessaging.m | 112 ++ .../XMPPMessageCoreDataStorage+XEP_0198.h | 51 + .../XMPPMessageCoreDataStorage+XEP_0198.m | 121 ++ Extensions/XEP-0203/NSXMLElement+XEP_0203.h | 3 + Extensions/XEP-0203/NSXMLElement+XEP_0203.m | 53 +- Extensions/XEP-0203/XMPPDelayedDelivery.h | 25 + Extensions/XEP-0203/XMPPDelayedDelivery.m | 57 + .../XMPPMessageCoreDataStorage+XEP_0203.h | 49 + .../XMPPMessageCoreDataStorage+XEP_0203.m | 89 + .../XMPPMessageCoreDataStorage+XEP_0245.h | 22 + .../XMPPMessageCoreDataStorage+XEP_0245.m | 30 + Extensions/XEP-0297/NSXMLElement+XEP_0297.m | 24 +- .../XEP-0308/XMPPCapabilities+XEP_0308.h | 14 + .../XEP-0308/XMPPCapabilities+XEP_0308.m | 24 + .../XEP-0308/XMPPLastMessageCorrection.h | 29 + .../XEP-0308/XMPPLastMessageCorrection.m | 131 ++ .../XMPPMessageCoreDataStorage+XEP_0308.h | 42 + .../XMPPMessageCoreDataStorage+XEP_0308.m | 79 + .../XEP-0313/XMPPMessageArchiveManagement.h | 8 + .../XEP-0313/XMPPMessageArchiveManagement.m | 129 +- .../XMPPMessageCoreDataStorage+XEP_0313.h | 103 + .../XMPPMessageCoreDataStorage+XEP_0313.m | 167 ++ Extensions/XMPPMUCLight/XMPPMUCLight.h | 2 +- Extensions/XMPPMUCLight/XMPPMUCLight.m | 11 +- .../XMPPMessageCoreDataStorage+XMPPMUCLight.h | 28 + .../XMPPMessageCoreDataStorage+XMPPMUCLight.m | 53 + Extensions/XMPPMUCLight/XMPPRoomLight.h | 1 + Extensions/XMPPMUCLight/XMPPRoomLight.m | 34 +- Utilities/GCDMulticastDelegate.h | 41 +- Utilities/GCDMulticastDelegate.m | 76 +- XMPPFramework.xcodeproj/project.pbxproj | 418 ++++ .../project.pbxproj | 56 + .../Testing-Shared/XMPPDelayedDeliveryTests.m | 131 ++ .../XMPPLastMessageCorrectionTests.m | 184 ++ Xcode/Testing-Shared/XMPPMUCLightTests.m | 2 +- .../XMPPManagedMessagingTests.m | 214 ++ .../XMPPMessageArchiveManagementTests.m | 87 +- .../XMPPMessageCoreDataStorageTests.m | 1754 +++++++++++++++++ .../XMPPMessageDeliveryReceiptsTests.m | 47 + Xcode/Testing-Shared/XMPPMockStream.h | 2 + Xcode/Testing-Shared/XMPPMockStream.m | 22 + Xcode/Testing-Shared/XMPPOneToOneChatTests.m | 76 + .../XMPPOutOfBandResourceMessagingTests.m | 65 + .../XMPPRoomLightCoreDataStorageTests.m | 71 +- .../project.pbxproj | 28 + .../project.pbxproj | 28 + 81 files changed, 7477 insertions(+), 225 deletions(-) create mode 100644 Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h create mode 100644 Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m create mode 100644 Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements create mode 100644 Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/layout create mode 100644 Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m create mode 100644 Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h create mode 100644 Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorage.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorage.m create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h create mode 100644 Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m create mode 100644 Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.h create mode 100644 Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.m create mode 100644 Extensions/OneToOneChat/XMPPOneToOneChat.h create mode 100644 Extensions/OneToOneChat/XMPPOneToOneChat.m create mode 100644 Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.h create mode 100644 Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.m create mode 100644 Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.h create mode 100644 Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.m create mode 100644 Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.h create mode 100644 Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.m create mode 100644 Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.h create mode 100644 Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.m create mode 100644 Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.h create mode 100644 Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.m create mode 100644 Extensions/XEP-0203/XMPPDelayedDelivery.h create mode 100644 Extensions/XEP-0203/XMPPDelayedDelivery.m create mode 100644 Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.h create mode 100644 Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.m create mode 100644 Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.h create mode 100644 Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.m create mode 100644 Extensions/XEP-0308/XMPPCapabilities+XEP_0308.h create mode 100644 Extensions/XEP-0308/XMPPCapabilities+XEP_0308.m create mode 100644 Extensions/XEP-0308/XMPPLastMessageCorrection.h create mode 100644 Extensions/XEP-0308/XMPPLastMessageCorrection.m create mode 100644 Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.h create mode 100644 Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.m create mode 100644 Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.h create mode 100644 Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.m create mode 100644 Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.h create mode 100644 Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.m create mode 100644 Xcode/Testing-Shared/XMPPDelayedDeliveryTests.m create mode 100644 Xcode/Testing-Shared/XMPPLastMessageCorrectionTests.m create mode 100644 Xcode/Testing-Shared/XMPPManagedMessagingTests.m create mode 100644 Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m create mode 100644 Xcode/Testing-Shared/XMPPMessageDeliveryReceiptsTests.m create mode 100644 Xcode/Testing-Shared/XMPPOneToOneChatTests.m create mode 100644 Xcode/Testing-Shared/XMPPOutOfBandResourceMessagingTests.m diff --git a/Core/XMPPFramework.h b/Core/XMPPFramework.h index cb5326d31b..c57ddcf266 100644 --- a/Core/XMPPFramework.h +++ b/Core/XMPPFramework.h @@ -34,6 +34,7 @@ #import "XMPPTimer.h" #import "XMPPCoreDataStorage.h" #import "XMPPCoreDataStorageProtected.h" +#import "XMPPDelayedDelivery.h" #import "NSXMLElement+XEP_0203.h" #import "XMPPFileTransfer.h" #import "XMPPIncomingFileTransfer.h" @@ -105,6 +106,7 @@ #import "TURNSocket.h" #import "XMPPIQ+XEP_0066.h" #import "XMPPMessage+XEP_0066.h" +#import "XMPPOutOfBandResourceMessaging.h" #import "XMPPRegistration.h" #import "NSDate+XMPPDateTimeProfiles.h" #import "XMPPDateTimeProfiles.h" @@ -116,6 +118,7 @@ #import "XMPPCapsCoreDataStorageObject.h" #import "XMPPCapsResourceCoreDataStorageObject.h" #import "XMPPCapabilities.h" +#import "XMPPCapabilities+XEP_0308.h" #import "XMPPMessageArchivingCoreDataStorage.h" #import "XMPPMessageArchiving_Contact_CoreDataObject.h" #import "XMPPMessageArchiving_Message_CoreDataObject.h" @@ -131,6 +134,7 @@ #import "XMPPStreamManagementMemoryStorage.h" #import "XMPPStreamManagementStanzas.h" #import "XMPPStreamManagement.h" +#import "XMPPManagedMessaging.h" #import "XMPPAutoPing.h" #import "XMPPPing.h" #import "XMPPAutoTime.h" @@ -144,6 +148,7 @@ #import "NSXMLElement+XEP_0297.h" #import "NSXMLElement+XEP_0203.h" #import "XMPPMessage+XEP_0308.h" +#import "XMPPLastMessageCorrection.h" #import "XMPPMessageArchiveManagement.h" #import "XMPPRoomLightCoreDataStorage+XEP_0313.h" #import "XMPPMessage+XEP_0333.h" @@ -159,7 +164,24 @@ #import "XMPPRoomLightCoreDataStorage.h" #import "XMPPRoomLightCoreDataStorageProtected.h" #import "XMPPRoomLightMessageCoreDataStorageObject.h" - +#import "XMPPOneToOneChat.h" +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorage+XMPPOneToOneChat.h" +#import "XMPPMessageCoreDataStorage+XMPPMUCLight.h" +#import "XMPPMessageCoreDataStorage+XEP_0066.h" +#import "XMPPMessageCoreDataStorage+XEP_0184.h" +#import "XMPPMessageCoreDataStorage+XEP_0198.h" +#import "XMPPMessageCoreDataStorage+XEP_0203.h" +#import "XMPPMessageCoreDataStorage+XEP_0245.h" +#import "XMPPMessageCoreDataStorage+XEP_0308.h" +#import "XMPPMessageCoreDataStorage+XEP_0313.h" +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessageContextCoreDataStorageObject.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "XMPPMessageContextItemCoreDataStorageObject+Protected.h" FOUNDATION_EXPORT double XMPPFrameworkVersionNumber; FOUNDATION_EXPORT const unsigned char XMPPFrameworkVersionString[]; diff --git a/Core/XMPPInternal.h b/Core/XMPPInternal.h index 1a1bc50c14..f4dba9754d 100644 --- a/Core/XMPPInternal.h +++ b/Core/XMPPInternal.h @@ -99,6 +99,7 @@ extern NSString *const XMPPStreamDidChangeMyJIDNotification; * This is an advanced technique, but makes for some interesting possibilities. **/ - (void)injectElement:(NSXMLElement *)element; +- (void)injectElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID; /** * The XMPP standard only supports , and stanzas (excluding session setup stuff). diff --git a/Core/XMPPStream.h b/Core/XMPPStream.h index e73e068f16..d601a873f4 100644 --- a/Core/XMPPStream.h +++ b/Core/XMPPStream.h @@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @class XMPPModule; @class XMPPElement; @class XMPPElementReceipt; +@class XMPPElementEvent; @protocol XMPPStreamDelegate; #if TARGET_OS_IPHONE @@ -615,8 +616,9 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; - (void)sendElement:(NSXMLElement *)element; /** - * Just like the sendElement: method above, - * but allows you to receive a receipt that can later be used to verify the element has been sent. + * Just like the sendElement: method above, but allows you to: + * - Receive a receipt that can later be used to verify the element has been sent. + * - Provide an event ID that can later be used to trace the element in delegate callbacks. * * If you later want to check to see if the element has been sent: * @@ -644,6 +646,7 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; * Even if you close the xmpp stream after this point, the OS will still do everything it can to send the data. **/ - (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt * _Nullable * _Nullable)receiptPtr; +- (void)sendElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID andGetReceipt:(XMPPElementReceipt **)receiptPtr; /** * Fetches and resends the myPresence element (if available) in a single atomic operation. @@ -733,6 +736,19 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; **/ - (void)enumerateModulesOfClass:(Class)aClass withBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block; +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Element Event Context +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the stream metadata corresponding to the currently processed XMPP stanza. + * + * Event information is only available in the context of @c didSendXXX/didFailToSendXXX/didReceiveXXX delegate callbacks. + * This method returns nil if called outside of those callbacks. + * For more details, please refer to @c XMPPElementEvent documentation. + */ +- (XMPPElementEvent *)currentElementEvent; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -785,6 +801,79 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; @end +/** + * A handle that allows identifying elements sent or received in the stream across different delegates + * and tracking their processing progress. + * + * While the core XMPP specification does not require stanzas to be uniquely identifiable, you may still want to + * identify them internally across different modules or trace the sent ones to the respective send result delegate callbacks. + * + * An instance of this class is provided in the context of execution of any of the @c didSendXXX/didFailToSendXXX/didReceiveXXX + * stream delegate methods. It is retrieved by calling the @c currentElementEvent method on the calling stream. + * The delegates can then use it to: + * - identify the corresponding XMPP stanzas. + * - be notified of asynchronous processing completion for a given XMPP stanza. + * + * Using @c XMPPElementEvent handles is a more robust approach than relying on pointer equality of @c XMPPElement instances. + */ +@interface XMPPElementEvent : NSObject + +/// The universally unique identifier of the event that provides the internal identity of the corresponding XMPP stanza. +@property (nonatomic, copy, readonly) NSString *uniqueID; + +/// The value of the stream's @c myJID property at the time when the event occured. +@property (nonatomic, strong, readonly) XMPPJID *myJID; + +/// The local device time when the event occured. +@property (nonatomic, strong, readonly) NSDate *timestamp; + +/** + * A flag indicating whether all delegates are done processing the given event. + * + * Supports Key-Value Observing. Change notifications are emitted on the stream queue. + * + * @see beginDelayedProcessing + * @see endDelayedProcessingWithToken + */ +@property (nonatomic, assign, readonly, getter=isProcessingCompleted) BOOL processingCompleted; + +// Instances are created by the stream only. +- (instancetype)init NS_UNAVAILABLE; + +/** + * Marks the event as being asynchronously processed by a delegate and returns a completion token. + * + * Event processing is completed after every @c beginDelayedProcessing call has been followed + * by @c endDelayedProcessingWithToken: with a matching completion token. + * + * Unpaired invocations may lead to undefined behavior or stalled events. + * + * Events that are not marked for asynchronous processing by any of the delegates complete immediately + * after control returns from all callbacks. + * + * @see endDelayedProcessingWithToken: + * @see processingCompleted + */ +- (id)beginDelayedProcessing; + +/** + * Marks an end of the previously initiated asynchronous delegate processing. + * + * Event processing is completed after every @c beginDelayedProcessing call has been followed + * by @c endDelayedProcessingWithToken: with a matching completion token. + * + * Unpaired invocations may lead to undefined behavior or stalled events. + * + * Events that are not marked for asynchronous processing by any of the delegates complete immediately + * after control returns from all callbacks. + * + * @see beginDelayedProcessing + * @see processingCompleted + */ +- (void)endDelayedProcessingWithToken:(id)delayedProcessingToken; + +@end + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -987,6 +1076,9 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; * As documented in NSXML / KissXML, elements are read-access thread-safe, but write-access thread-unsafe. * If you have need to modify an element for any reason, * you should copy the element first, and then modify and use the copy. + * + * Delegates can obtain event metadata associated with the respective element by calling @c currentElementEvent on @c sender + * from within these callbacks. For more details, please refer to @c XMPPElementEvent documentation. **/ - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq; - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message; @@ -1032,6 +1124,9 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; * These methods are called after their respective XML elements are sent over the stream. * These methods may be used to listen for certain events (such as an unavailable presence having been sent), * or for general logging purposes. (E.g. a central history logging mechanism). + * + * Delegates can obtain event metadata associated with the respective element by calling @c currentElementEvent on @c sender + * from within these callbacks. For more details, please refer to @c XMPPElementEvent documentation. **/ - (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq; - (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message; @@ -1040,11 +1135,24 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; /** * These methods are called after failing to send the respective XML elements over the stream. * This occurs when the stream gets disconnected before the element can get sent out. + * + * Delegates can obtain event metadata associated with the respective element by calling @c currentElementEvent on @c sender + * from within these callbacks. + * Note that if these methods are called, the event context is incomplete, e.g. the stream might have not been connected + * and the actual myJID value is not determined. For more details, please refer to @c XMPPElementEvent documentation. **/ - (void)xmppStream:(XMPPStream *)sender didFailToSendIQ:(XMPPIQ *)iq error:(NSError *)error; - (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error; - (void)xmppStream:(XMPPStream *)sender didFailToSendPresence:(XMPPPresence *)presence error:(NSError *)error; +/** + * This method is called after all delegates are done processing the given event. + * + * For more details, please refer to @c XMPPElementEvent documentation. + */ +- (void)xmppStream:(XMPPStream *)sender didFinishProcessingElementEvent:(XMPPElementEvent *)event +NS_SWIFT_NAME(xmppStream(_:didFinishProcessing:)); + /** * This method is called if the XMPP Stream's jid changes. **/ @@ -1136,6 +1244,9 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; * If you're using custom elements, you must register the custom element name(s). * Otherwise the xmppStream will treat non-XMPP elements as errors (xmppStream:didReceiveError:). * + * Delegates can obtain event metadata associated with the respective element by calling @c currentElementEvent on @c sender + * from within these callbacks. For more details, please refer to @c XMPPElementEvent documentation. + * * @see registerCustomElementNames (in XMPPInternal.h) **/ - (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element; diff --git a/Core/XMPPStream.m b/Core/XMPPStream.m index 8a738719ae..e47987c931 100644 --- a/Core/XMPPStream.m +++ b/Core/XMPPStream.m @@ -153,6 +153,19 @@ - (void)signalFailure; @end +@interface XMPPElementEvent () + +@property (nonatomic, unsafe_unretained, readonly) XMPPStream *xmppStream; +@property (nonatomic, assign, readwrite, getter=isProcessingCompleted) BOOL processingCompleted; + +@end + +@interface XMPPElementEvent (PrivateAPI) + +- (instancetype)initWithStream:(XMPPStream *)xmppStream uniqueID:(NSString *)uniqueID myJID:(XMPPJID *)myJID timestamp:(NSDate *)timestamp; + +@end + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2253,7 +2266,7 @@ - (float)serverXmppStreamVersionNumber } } -- (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag +- (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag registeringEventWithID:(NSString *)eventID { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2268,7 +2281,7 @@ - (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag // None of the delegates implement the method. // Use a shortcut. - [self continueSendIQ:iq withTag:tag]; + [self continueSendIQ:iq withTag:tag registeredEvent:[self generateElementEventWithID:eventID]]; } else { @@ -2314,10 +2327,12 @@ - (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag { dispatch_async(xmppQueue, ^{ @autoreleasepool { + XMPPElementEvent *event = [self generateElementEventWithID:eventID]; + if (state == STATE_XMPP_CONNECTED) { - [self continueSendIQ:modifiedIQ withTag:tag]; + [self continueSendIQ:modifiedIQ withTag:tag registeredEvent:event]; } else { - [self failToSendIQ:modifiedIQ]; + [self failToSendIQ:modifiedIQ withRegisteredEvent:event]; } }}); } @@ -2325,7 +2340,7 @@ - (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag } } -- (void)sendMessage:(XMPPMessage *)message withTag:(long)tag +- (void)sendMessage:(XMPPMessage *)message withTag:(long)tag registeringEventWithID:(NSString *)eventID { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2340,7 +2355,7 @@ - (void)sendMessage:(XMPPMessage *)message withTag:(long)tag // None of the delegates implement the method. // Use a shortcut. - [self continueSendMessage:message withTag:tag]; + [self continueSendMessage:message withTag:tag registeredEvent:[self generateElementEventWithID:eventID]]; } else { @@ -2386,11 +2401,13 @@ - (void)sendMessage:(XMPPMessage *)message withTag:(long)tag { dispatch_async(xmppQueue, ^{ @autoreleasepool { + XMPPElementEvent *event = [self generateElementEventWithID:eventID]; + if (state == STATE_XMPP_CONNECTED) { - [self continueSendMessage:modifiedMessage withTag:tag]; + [self continueSendMessage:modifiedMessage withTag:tag registeredEvent:event]; } else { - [self failToSendMessage:modifiedMessage]; + [self failToSendMessage:modifiedMessage withRegisteredEvent:event]; } }}); } @@ -2398,7 +2415,7 @@ - (void)sendMessage:(XMPPMessage *)message withTag:(long)tag } } -- (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag +- (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag registeringEventWithID:(NSString *)eventID { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2413,7 +2430,7 @@ - (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag // None of the delegates implement the method. // Use a shortcut. - [self continueSendPresence:presence withTag:tag]; + [self continueSendPresence:presence withTag:tag registeredEvent:[self generateElementEventWithID:eventID]]; } else { @@ -2459,10 +2476,12 @@ - (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag { dispatch_async(xmppQueue, ^{ @autoreleasepool { + XMPPElementEvent *event = [self generateElementEventWithID:eventID]; + if (state == STATE_XMPP_CONNECTED) { - [self continueSendPresence:modifiedPresence withTag:tag]; + [self continueSendPresence:modifiedPresence withTag:tag registeredEvent:event]; } else { - [self failToSendPresence:modifiedPresence]; + [self failToSendPresence:modifiedPresence withRegisteredEvent:event]; } }}); } @@ -2470,7 +2489,7 @@ - (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag } } -- (void)continueSendIQ:(XMPPIQ *)iq withTag:(long)tag +- (void)continueSendIQ:(XMPPIQ *)iq withTag:(long)tag registeredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2485,10 +2504,12 @@ - (void)continueSendIQ:(XMPPIQ *)iq withTag:(long)tag withTimeout:TIMEOUT_XMPP_WRITE tag:tag]; - [multicastDelegate xmppStream:self didSendIQ:iq]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didSendIQ:iq]; + }]; } -- (void)continueSendMessage:(XMPPMessage *)message withTag:(long)tag +- (void)continueSendMessage:(XMPPMessage *)message withTag:(long)tag registeredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2503,10 +2524,12 @@ - (void)continueSendMessage:(XMPPMessage *)message withTag:(long)tag withTimeout:TIMEOUT_XMPP_WRITE tag:tag]; - [multicastDelegate xmppStream:self didSendMessage:message]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didSendMessage:message]; + }]; } -- (void)continueSendPresence:(XMPPPresence *)presence withTag:(long)tag +- (void)continueSendPresence:(XMPPPresence *)presence withTag:(long)tag registeredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2536,10 +2559,12 @@ - (void)continueSendPresence:(XMPPPresence *)presence withTag:(long)tag } } - [multicastDelegate xmppStream:self didSendPresence:presence]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didSendPresence:presence]; + }]; } -- (void)continueSendElement:(NSXMLElement *)element withTag:(long)tag +- (void)continueSendElement:(NSXMLElement *)element withTag:(long)tag registeringEventWithID:(NSString *)eventID { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2556,7 +2581,11 @@ - (void)continueSendElement:(NSXMLElement *)element withTag:(long)tag if ([customElementNames countForObject:[element name]]) { - [multicastDelegate xmppStream:self didSendCustomElement:element]; + XMPPElementEvent *event = [self generateElementEventWithID:eventID]; + + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didSendCustomElement:element]; + }]; } } @@ -2564,22 +2593,22 @@ - (void)continueSendElement:(NSXMLElement *)element withTag:(long)tag * Private method. * Presencts a common method for the various public sendElement methods. **/ -- (void)sendElement:(NSXMLElement *)element withTag:(long)tag +- (void)sendElement:(NSXMLElement *)element withTag:(long)tag registeringEventWithID:(NSString *)eventID { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); if ([element isKindOfClass:[XMPPIQ class]]) { - [self sendIQ:(XMPPIQ *)element withTag:tag]; + [self sendIQ:(XMPPIQ *)element withTag:tag registeringEventWithID:eventID]; } else if ([element isKindOfClass:[XMPPMessage class]]) { - [self sendMessage:(XMPPMessage *)element withTag:tag]; + [self sendMessage:(XMPPMessage *)element withTag:tag registeringEventWithID:eventID]; } else if ([element isKindOfClass:[XMPPPresence class]]) { - [self sendPresence:(XMPPPresence *)element withTag:tag]; + [self sendPresence:(XMPPPresence *)element withTag:tag registeringEventWithID:eventID]; } else { @@ -2587,19 +2616,19 @@ - (void)sendElement:(NSXMLElement *)element withTag:(long)tag if ([elementName isEqualToString:@"iq"]) { - [self sendIQ:[XMPPIQ iqFromElement:element] withTag:tag]; + [self sendIQ:[XMPPIQ iqFromElement:element] withTag:tag registeringEventWithID:eventID]; } else if ([elementName isEqualToString:@"message"]) { - [self sendMessage:[XMPPMessage messageFromElement:element] withTag:tag]; + [self sendMessage:[XMPPMessage messageFromElement:element] withTag:tag registeringEventWithID:eventID]; } else if ([elementName isEqualToString:@"presence"]) { - [self sendPresence:[XMPPPresence presenceFromElement:element] withTag:tag]; + [self sendPresence:[XMPPPresence presenceFromElement:element] withTag:tag registeringEventWithID:eventID]; } else { - [self continueSendElement:element withTag:tag]; + [self continueSendElement:element withTag:tag registeringEventWithID:eventID]; } } } @@ -2610,24 +2639,12 @@ - (void)sendElement:(NSXMLElement *)element withTag:(long)tag **/ - (void)sendElement:(NSXMLElement *)element { - if (element == nil) return; - - dispatch_block_t block = ^{ @autoreleasepool { - - if (state == STATE_XMPP_CONNECTED) - { - [self sendElement:element withTag:TAG_XMPP_WRITE_STREAM]; - } - else - { - [self failToSendElement:element]; - } - }}; - - if (dispatch_get_specific(xmppQueueTag)) - block(); - else - dispatch_async(xmppQueue, block); + [self sendElement:element registeringEventWithID:[self generateUUID] andGetReceipt:nil]; +} + +- (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt **)receiptPtr +{ + [self sendElement:element registeringEventWithID:[self generateUUID] andGetReceipt:receiptPtr]; } /** @@ -2637,57 +2654,61 @@ - (void)sendElement:(NSXMLElement *)element * After the element has been successfully sent, * the xmppStream:didSendElementWithTag: delegate method is called. **/ -- (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt **)receiptPtr +- (void)sendElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID andGetReceipt:(XMPPElementReceipt **)receiptPtr { if (element == nil) return; - if (receiptPtr == nil) - { - [self sendElement:element]; - } - else - { - __block XMPPElementReceipt *receipt = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - if (state == STATE_XMPP_CONNECTED) - { - receipt = [[XMPPElementReceipt alloc] init]; - [receipts addObject:receipt]; - - [self sendElement:element withTag:TAG_XMPP_WRITE_RECEIPT]; - } + dispatch_block_t block = ^{ @autoreleasepool { + XMPPElementReceipt *receipt; + + if (state == STATE_XMPP_CONNECTED) + { + if (receiptPtr) + { + receipt = [[XMPPElementReceipt alloc] init]; + [receipts addObject:receipt]; + + [self sendElement:element withTag:TAG_XMPP_WRITE_RECEIPT registeringEventWithID:eventID]; + } else { - [self failToSendElement:element]; + [self sendElement:element withTag:TAG_XMPP_WRITE_STREAM registeringEventWithID:eventID]; } - }}; - - if (dispatch_get_specific(xmppQueueTag)) - block(); - else - dispatch_sync(xmppQueue, block); - - *receiptPtr = receipt; - } + } + else + { + [self failToSendElement:element registeringEventWithID:eventID]; + } + + if (receiptPtr) + *receiptPtr = receipt; + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else if (receiptPtr) + dispatch_sync(xmppQueue, block); + else + dispatch_async(xmppQueue, block); } -- (void)failToSendElement:(NSXMLElement *)element +- (void)failToSendElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + XMPPElementEvent *event = [self generateElementEventWithID:eventID]; + if ([element isKindOfClass:[XMPPIQ class]]) { - [self failToSendIQ:(XMPPIQ *)element]; + [self failToSendIQ:(XMPPIQ *)element withRegisteredEvent:event]; } else if ([element isKindOfClass:[XMPPMessage class]]) { - [self failToSendMessage:(XMPPMessage *)element]; + [self failToSendMessage:(XMPPMessage *)element withRegisteredEvent:event]; } else if ([element isKindOfClass:[XMPPPresence class]]) { - [self failToSendPresence:(XMPPPresence *)element]; + [self failToSendPresence:(XMPPPresence *)element withRegisteredEvent:event]; } else { @@ -2695,20 +2716,20 @@ - (void)failToSendElement:(NSXMLElement *)element if ([elementName isEqualToString:@"iq"]) { - [self failToSendIQ:[XMPPIQ iqFromElement:element]]; + [self failToSendIQ:[XMPPIQ iqFromElement:element] withRegisteredEvent:event]; } else if ([elementName isEqualToString:@"message"]) { - [self failToSendMessage:[XMPPMessage messageFromElement:element]]; + [self failToSendMessage:[XMPPMessage messageFromElement:element] withRegisteredEvent:event]; } else if ([elementName isEqualToString:@"presence"]) { - [self failToSendPresence:[XMPPPresence presenceFromElement:element]]; + [self failToSendPresence:[XMPPPresence presenceFromElement:element] withRegisteredEvent:event]; } } } -- (void)failToSendIQ:(XMPPIQ *)iq +- (void)failToSendIQ:(XMPPIQ *)iq withRegisteredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); @@ -2716,10 +2737,12 @@ - (void)failToSendIQ:(XMPPIQ *)iq code:XMPPStreamInvalidState userInfo:nil]; - [multicastDelegate xmppStream:self didFailToSendIQ:iq error:error]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didFailToSendIQ:iq error:error]; + }]; } -- (void)failToSendMessage:(XMPPMessage *)message +- (void)failToSendMessage:(XMPPMessage *)message withRegisteredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); @@ -2727,10 +2750,12 @@ - (void)failToSendMessage:(XMPPMessage *)message code:XMPPStreamInvalidState userInfo:nil]; - [multicastDelegate xmppStream:self didFailToSendMessage:message error:error]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didFailToSendMessage:message error:error]; + }]; } -- (void)failToSendPresence:(XMPPPresence *)presence +- (void)failToSendPresence:(XMPPPresence *)presence withRegisteredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); @@ -2738,7 +2763,9 @@ - (void)failToSendPresence:(XMPPPresence *)presence code:XMPPStreamInvalidState userInfo:nil]; - [multicastDelegate xmppStream:self didFailToSendPresence:presence error:error]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didFailToSendPresence:presence error:error]; + }]; } /** @@ -2831,7 +2858,7 @@ - (void)sendBindElement:(NSXMLElement *)element dispatch_async(xmppQueue, block); } -- (void)receiveIQ:(XMPPIQ *)iq +- (void)receiveIQ:(XMPPIQ *)iq withRegisteredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2853,14 +2880,14 @@ - (void)receiveIQ:(XMPPIQ *)iq dispatch_async(willReceiveStanzaQueue, ^{ dispatch_async(xmppQueue, ^{ @autoreleasepool { if (state == STATE_XMPP_CONNECTED) { - [self continueReceiveIQ:iq]; + [self continueReceiveIQ:iq withRegisteredEvent:event]; } }}); }); } else { - [self continueReceiveIQ:iq]; + [self continueReceiveIQ:iq withRegisteredEvent:event]; } } else @@ -2896,7 +2923,7 @@ - (void)receiveIQ:(XMPPIQ *)iq if (state == STATE_XMPP_CONNECTED) { if (modifiedIQ) - [self continueReceiveIQ:modifiedIQ]; + [self continueReceiveIQ:modifiedIQ withRegisteredEvent:event]; else [multicastDelegate xmppStreamDidFilterStanza:self]; } @@ -2905,7 +2932,7 @@ - (void)receiveIQ:(XMPPIQ *)iq } } -- (void)receiveMessage:(XMPPMessage *)message +- (void)receiveMessage:(XMPPMessage *)message withRegisteredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -2928,14 +2955,14 @@ - (void)receiveMessage:(XMPPMessage *)message dispatch_async(xmppQueue, ^{ @autoreleasepool { if (state == STATE_XMPP_CONNECTED) { - [self continueReceiveMessage:message]; + [self continueReceiveMessage:message withRegisteredEvent:event]; } }}); }); } else { - [self continueReceiveMessage:message]; + [self continueReceiveMessage:message withRegisteredEvent:event]; } } else @@ -2971,7 +2998,7 @@ - (void)receiveMessage:(XMPPMessage *)message if (state == STATE_XMPP_CONNECTED) { if (modifiedMessage) - [self continueReceiveMessage:modifiedMessage]; + [self continueReceiveMessage:modifiedMessage withRegisteredEvent:event]; else [multicastDelegate xmppStreamDidFilterStanza:self]; } @@ -2980,7 +3007,7 @@ - (void)receiveMessage:(XMPPMessage *)message } } -- (void)receivePresence:(XMPPPresence *)presence +- (void)receivePresence:(XMPPPresence *)presence withRegisteredEvent:(XMPPElementEvent *)event { NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); @@ -3003,14 +3030,14 @@ - (void)receivePresence:(XMPPPresence *)presence dispatch_async(xmppQueue, ^{ @autoreleasepool { if (state == STATE_XMPP_CONNECTED) { - [self continueReceivePresence:presence]; + [self continueReceivePresence:presence withRegisteredEvent:event]; } }}); }); } else { - [self continueReceivePresence:presence]; + [self continueReceivePresence:presence withRegisteredEvent:event]; } } else @@ -3046,7 +3073,7 @@ - (void)receivePresence:(XMPPPresence *)presence if (state == STATE_XMPP_CONNECTED) { if (modifiedPresence) - [self continueReceivePresence:presence]; + [self continueReceivePresence:presence withRegisteredEvent:event]; else [multicastDelegate xmppStreamDidFilterStanza:self]; } @@ -3055,7 +3082,7 @@ - (void)receivePresence:(XMPPPresence *)presence } } -- (void)continueReceiveIQ:(XMPPIQ *)iq +- (void)continueReceiveIQ:(XMPPIQ *)iq withRegisteredEvent:(XMPPElementEvent *)event { if ([iq requiresResponse]) { @@ -3066,27 +3093,32 @@ - (void)continueReceiveIQ:(XMPPIQ *)iq // So we notifiy all interested delegates and modules about the received IQ, // keeping track of whether or not any of them have handled it. - GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; - - id del; - dispatch_queue_t dq; - - SEL selector = @selector(xmppStream:didReceiveIQ:); - dispatch_semaphore_t delSemaphore = dispatch_semaphore_create(0); dispatch_group_t delGroup = dispatch_group_create(); - while ([delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) - { - dispatch_group_async(delGroup, dq, ^{ @autoreleasepool { - - if ([del xmppStream:self didReceiveIQ:iq]) - { - dispatch_semaphore_signal(delSemaphore); - } - - }}); - } + dispatch_group_enter(delGroup); + id didReceiveIqProcessingToken = [event beginDelayedProcessing]; + + [self performDelegateActionWithElementEvent:event block:^{ + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + id delegate; + dispatch_queue_t delegateQueue; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue forSelector:@selector(xmppStream:didReceiveIQ:)]) + { + dispatch_group_async(delGroup, delegateQueue, ^{ @autoreleasepool { + + if ([delegate xmppStream:self didReceiveIQ:iq]) + { + dispatch_semaphore_signal(delSemaphore); + } + + }}); + } + + dispatch_group_leave(delGroup); + }]; dispatch_async(didReceiveIqQueue, ^{ @autoreleasepool { @@ -3144,6 +3176,7 @@ - (void)continueReceiveIQ:(XMPPIQ *)iq dispatch_release(delGroup); #endif + [event endDelayedProcessingWithToken:didReceiveIqProcessingToken]; }}); } else @@ -3151,18 +3184,24 @@ - (void)continueReceiveIQ:(XMPPIQ *)iq // The IQ doesn't require a response. // So we can just fire the delegate method and ignore the responses. - [multicastDelegate xmppStream:self didReceiveIQ:iq]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didReceiveIQ:iq]; + }]; } } -- (void)continueReceiveMessage:(XMPPMessage *)message +- (void)continueReceiveMessage:(XMPPMessage *)message withRegisteredEvent:(XMPPElementEvent *)event { - [multicastDelegate xmppStream:self didReceiveMessage:message]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didReceiveMessage:message]; + }]; } -- (void)continueReceivePresence:(XMPPPresence *)presence +- (void)continueReceivePresence:(XMPPPresence *)presence withRegisteredEvent:(XMPPElementEvent *)event { - [multicastDelegate xmppStream:self didReceivePresence:presence]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didReceivePresence:presence]; + }]; } /** @@ -3170,6 +3209,11 @@ - (void)continueReceivePresence:(XMPPPresence *)presence * This is an advanced technique, but makes for some interesting possibilities. **/ - (void)injectElement:(NSXMLElement *)element +{ + [self injectElement:element registeringEventWithID:[self generateUUID]]; +} + +- (void)injectElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID { if (element == nil) return; @@ -3179,18 +3223,20 @@ - (void)injectElement:(NSXMLElement *)element { return_from_block; } + + XMPPElementEvent *event = [self generateElementEventWithID:eventID]; if ([element isKindOfClass:[XMPPIQ class]]) { - [self receiveIQ:(XMPPIQ *)element]; + [self receiveIQ:(XMPPIQ *)element withRegisteredEvent:event]; } else if ([element isKindOfClass:[XMPPMessage class]]) { - [self receiveMessage:(XMPPMessage *)element]; + [self receiveMessage:(XMPPMessage *)element withRegisteredEvent:event]; } else if ([element isKindOfClass:[XMPPPresence class]]) { - [self receivePresence:(XMPPPresence *)element]; + [self receivePresence:(XMPPPresence *)element withRegisteredEvent:event]; } else { @@ -3198,19 +3244,21 @@ - (void)injectElement:(NSXMLElement *)element if ([elementName isEqualToString:@"iq"]) { - [self receiveIQ:[XMPPIQ iqFromElement:element]]; + [self receiveIQ:[XMPPIQ iqFromElement:element] withRegisteredEvent:event]; } else if ([elementName isEqualToString:@"message"]) { - [self receiveMessage:[XMPPMessage messageFromElement:element]]; + [self receiveMessage:[XMPPMessage messageFromElement:element] withRegisteredEvent:event]; } else if ([elementName isEqualToString:@"presence"]) { - [self receivePresence:[XMPPPresence presenceFromElement:element]]; + [self receivePresence:[XMPPPresence presenceFromElement:element] withRegisteredEvent:event]; } else if ([customElementNames countForObject:elementName]) { - [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + }]; } else { @@ -4630,17 +4678,19 @@ - (void)xmppParser:(XMPPParser *)sender didReadElement:(NSXMLElement *)element } else { + XMPPElementEvent *event = [self generateElementEventWithID:[self generateUUID]]; + if ([elementName isEqualToString:@"iq"]) { - [self receiveIQ:[XMPPIQ iqFromElement:element]]; + [self receiveIQ:[XMPPIQ iqFromElement:element] withRegisteredEvent:event]; } else if ([elementName isEqualToString:@"message"]) { - [self receiveMessage:[XMPPMessage messageFromElement:element]]; + [self receiveMessage:[XMPPMessage messageFromElement:element] withRegisteredEvent:event]; } else if ([elementName isEqualToString:@"presence"]) { - [self receivePresence:[XMPPPresence presenceFromElement:element]]; + [self receivePresence:[XMPPPresence presenceFromElement:element] withRegisteredEvent:event]; } else if ([self isP2P] && ([elementName isEqualToString:@"stream:features"] || [elementName isEqualToString:@"features"])) @@ -4649,7 +4699,9 @@ - (void)xmppParser:(XMPPParser *)sender didReadElement:(NSXMLElement *)element } else if ([customElementNames countForObject:elementName]) { - [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + [self performDelegateActionWithElementEvent:event block:^{ + [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + }]; } else { @@ -5059,6 +5111,16 @@ - (void)enumerateModulesOfClass:(Class)aClass withBlock:(void (^)(XMPPModule *mo }]; } +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Element Event Context +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPElementEvent *)currentElementEvent +{ + XMPPElementEvent *event = [GCDMulticastDelegateInvocationContext currentContext].value; + return event.xmppStream == self ? event : nil; +} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -5080,6 +5142,23 @@ - (GCDAsyncSocket*) newSocket { return socket; } +- (XMPPElementEvent *)generateElementEventWithID:(NSString *)eventID +{ + return [[XMPPElementEvent alloc] initWithStream:self uniqueID:eventID myJID:self.myJID timestamp:[NSDate date]]; +} + +- (void)performDelegateActionWithElementEvent:(XMPPElementEvent *)event block:(dispatch_block_t)block +{ + GCDMulticastDelegateInvocationContext *eventProcessingDelegateInvocationContext = [[GCDMulticastDelegateInvocationContext alloc] initWithValue:event]; + + [eventProcessingDelegateInvocationContext becomeCurrentOnQueue:self.xmppQueue forActionWithBlock:block]; + + dispatch_group_notify(eventProcessingDelegateInvocationContext.continuityGroup, self.xmppQueue, ^{ + event.processingCompleted = YES; + [multicastDelegate xmppStream:self didFinishProcessingElementEvent:event]; + }); +} + @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -5165,3 +5244,55 @@ - (void)dealloc } @end + +@implementation XMPPElementEvent + +- (instancetype)initWithStream:(XMPPStream *)xmppStream uniqueID:(NSString *)uniqueID myJID:(XMPPJID *)myJID timestamp:(NSDate *)timestamp +{ + self = [super init]; + if (self) { + _xmppStream = xmppStream; + _uniqueID = [uniqueID copy]; + _myJID = myJID; + _timestamp = timestamp; + } + return self; +} + +- (BOOL)isProcessingCompleted +{ + __block BOOL result; + + dispatch_block_t block = ^{ + result = _processingCompleted; + }; + + if (dispatch_get_specific(self.xmppStream.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppStream.xmppQueue, block); + + return result; +} + +- (id)beginDelayedProcessing +{ + GCDMulticastDelegateInvocationContext *currentContext = [GCDMulticastDelegateInvocationContext currentContext]; + NSAssert(currentContext.value == self, @"Delayed processing can only be initiated in the context matching the current event"); + + dispatch_group_enter(currentContext.continuityGroup); + + return currentContext; +} + +- (void)endDelayedProcessingWithToken:(id)delayedProcessingToken +{ + NSAssert([delayedProcessingToken isKindOfClass:[GCDMulticastDelegateInvocationContext class]], @"Invalid delayed processing token"); + + GCDMulticastDelegateInvocationContext *originalContext = delayedProcessingToken; + NSAssert(originalContext.value == self, @"Delayed processing token mismatch"); + + dispatch_group_leave(originalContext.continuityGroup); +} + +@end diff --git a/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h new file mode 100644 index 0000000000..add2dd60fa --- /dev/null +++ b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h @@ -0,0 +1,29 @@ +#import +#import "XMPPJID.h" + +@interface NSManagedObject (XMPPCoreDataStorage) + +/// @brief Inserts a managed object with an entity whose name matches the class name. +/// @discussion An assertion will be triggered if no matching entity is found in the model. ++ (instancetype)xmpp_insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Returns a fetch request for an entity whose name matches the class name. +/// @discussion An assertion will be triggered if no matching entity is found in the model. ++ (NSFetchRequest *)xmpp_fetchRequestInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Returns a predicate for filtering managed objects on JID component attributes. +/// @discussion The provided keypaths are relative to the fetched entity and the filtering logic follows @c [XMPPJID @c isEqualToJID:options:] implementation. ++ (NSPredicate *)xmpp_jidPredicateWithDomainKeyPath:(NSString *)domainKeyPath + resourceKeyPath:(NSString *)resourceKeyPath + userKeyPath:(NSString *)userKeyPath + value:(XMPPJID *)value + compareOptions:(XMPPJIDCompareOptions)compareOptions; + +@end + +@interface NSManagedObjectContext (XMPPCoreDataStorage) + +/// Executes the provided fetch request raising an assertion upon failure. +- (NSArray *)xmpp_executeForcedSuccessFetchRequest:(NSFetchRequest *)fetchRequest; + +@end diff --git a/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m new file mode 100644 index 0000000000..697917b9d5 --- /dev/null +++ b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.m @@ -0,0 +1,65 @@ +#import "NSManagedObject+XMPPCoreDataStorage.h" + +@implementation NSManagedObject (XMPPCoreDataStorage) + ++ (instancetype)xmpp_insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + return [[self alloc] initWithEntity:[self xmpp_entityInManagedObjectContext:managedObjectContext] + insertIntoManagedObjectContext:managedObjectContext]; +} + ++ (NSFetchRequest *)xmpp_fetchRequestInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = [self xmpp_entityInManagedObjectContext:managedObjectContext]; + return fetchRequest; +} + ++ (NSPredicate *)xmpp_jidPredicateWithDomainKeyPath:(NSString *)domainKeyPath resourceKeyPath:(NSString *)resourceKeyPath userKeyPath:(NSString *)userKeyPath value:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + NSMutableArray *subpredicates = [[NSMutableArray alloc] init]; + + if (compareOptions & XMPPJIDCompareDomain) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K = %@", domainKeyPath, value.domain]]; + } + + if (compareOptions & XMPPJIDCompareResource) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K = %@", resourceKeyPath, value.resource]]; + } + + if (compareOptions & XMPPJIDCompareUser) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K = %@", userKeyPath, value.user]]; + } + + return [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]; +} + ++ (NSEntityDescription *)xmpp_entityInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSUInteger selfEntityIndex = [managedObjectContext.persistentStoreCoordinator.managedObjectModel.entities indexOfObjectPassingTest:^BOOL(NSEntityDescription * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + BOOL matchesSelf = [obj.managedObjectClassName isEqualToString:NSStringFromClass(self)]; + if (matchesSelf) { + *stop = YES; + } + return matchesSelf; + }]; + NSAssert(selfEntityIndex != NSNotFound, @"Entity for %@ not found", self); + + return managedObjectContext.persistentStoreCoordinator.managedObjectModel.entities[selfEntityIndex]; +} + +@end + +@implementation NSManagedObjectContext (XMPPCoreDataStorage) + +- (NSArray *)xmpp_executeForcedSuccessFetchRequest:(NSFetchRequest *)fetchRequest +{ + NSError *error; + NSArray *fetchResult = [self executeFetchRequest:fetchRequest error:&error]; + if (!fetchResult) { + NSAssert(NO, @"Fetch request %@ failed with error %@", fetchRequest, error); + } + return fetchResult; +} + +@end diff --git a/Extensions/CoreDataStorage/XMPPCoreDataStorage.m b/Extensions/CoreDataStorage/XMPPCoreDataStorage.m index 39218e30f0..813331c956 100644 --- a/Extensions/CoreDataStorage/XMPPCoreDataStorage.m +++ b/Extensions/CoreDataStorage/XMPPCoreDataStorage.m @@ -560,7 +560,7 @@ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { return; } - + XMPPLogVerbose(@"%@: Creating persistentStoreCoordinator", [self class]); persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; diff --git a/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements b/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements new file mode 100644 index 0000000000000000000000000000000000000000..5d2171caf1d4a2bd3c25bbe13b18301d7f726adb GIT binary patch literal 134828 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 literal 0 HcmV?d00001 diff --git a/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h new file mode 100644 index 0000000000..7e5be17584 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject+Protected.h @@ -0,0 +1,50 @@ +#import "XMPPMessageContextCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageCoreDataStorageObject, XMPPMessageContextJIDItemCoreDataStorageObject, XMPPMessageContextMarkerItemCoreDataStorageObject, XMPPMessageContextStringItemCoreDataStorageObject, XMPPMessageContextTimestampItemCoreDataStorageObject; + +@interface XMPPMessageContextCoreDataStorageObject (Protected) + +/// The message the context object is assigned to. +@property (nonatomic, strong, nullable) XMPPMessageCoreDataStorageObject *message; + +/// The JID values aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *jidItems; + +/// The markers aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *markerItems; + +/// The string values aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *stringItems; + +/// The timestamp values aggregated by the context object. +@property (nonatomic, copy, nullable) NSSet *timestampItems; + +@end + +@interface XMPPMessageContextCoreDataStorageObject (CoreDataGeneratedRelationshipAccesssors) + +- (void)addJidItemsObject:(XMPPMessageContextJIDItemCoreDataStorageObject *)value; +- (void)removeJidItemsObject:(XMPPMessageContextJIDItemCoreDataStorageObject *)value; +- (void)addJidItems:(NSSet *)value; +- (void)removeJidItems:(NSSet *)value; + +- (void)addMarkerItemsObject:(XMPPMessageContextMarkerItemCoreDataStorageObject *)value; +- (void)removeMarkerItemsObject:(XMPPMessageContextMarkerItemCoreDataStorageObject *)value; +- (void)addMarkerItems:(NSSet *)value; +- (void)removeMarkerItems:(NSSet *)value; + +- (void)addStringItemsObject:(XMPPMessageContextStringItemCoreDataStorageObject *)value; +- (void)removeStringItemsObject:(XMPPMessageContextStringItemCoreDataStorageObject *)value; +- (void)addStringItems:(NSSet *)value; +- (void)removeStringItems:(NSSet *)value; + +- (void)addTimestampItemsObject:(XMPPMessageContextTimestampItemCoreDataStorageObject *)value; +- (void)removeTimestampItemsObject:(XMPPMessageContextTimestampItemCoreDataStorageObject *)value; +- (void)addTimestampItems:(NSSet *)value; +- (void)removeTimestampItems:(NSSet *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h new file mode 100644 index 0000000000..1ead01a7fa --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.h @@ -0,0 +1,15 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An auxiliary context storage object aggregating module-provided values assigned to a stored message. + + @see XMPPMessageCoreDataStorageObject + @see XMPPMessageContextItemCoreDataStorageObject + */ +@interface XMPPMessageContextCoreDataStorageObject : NSManagedObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m new file mode 100644 index 0000000000..8c14ef9faa --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextCoreDataStorageObject.m @@ -0,0 +1,18 @@ +#import "XMPPMessageContextCoreDataStorageObject.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" + +@interface XMPPMessageContextCoreDataStorageObject () + +@property (nonatomic, strong, nullable) XMPPMessageCoreDataStorageObject *message; +@property (nonatomic, copy, nullable) NSSet *jidItems; +@property (nonatomic, copy, nullable) NSSet *markerItems; +@property (nonatomic, copy, nullable) NSSet *stringItems; +@property (nonatomic, copy, nullable) NSSet *timestampItems; + +@end + +@implementation XMPPMessageContextCoreDataStorageObject + +@dynamic message, jidItems, markerItems, stringItems, timestampItems; + +@end diff --git a/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h new file mode 100644 index 0000000000..d39994b029 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject+Protected.h @@ -0,0 +1,91 @@ +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "XMPPJID.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageContextCoreDataStorageObject; + +typedef NS_ENUM(int16_t, XMPPMessageDirection); + +/// A tag assigned to a JID auxiliary value. +typedef NSString * XMPPMessageContextJIDItemTag NS_EXTENSIBLE_STRING_ENUM; + +/// A tag assigned to an auxiliary marker. +typedef NSString * XMPPMessageContextMarkerItemTag NS_EXTENSIBLE_STRING_ENUM; + +/// A tag assigned to a string auxiliary value. +typedef NSString * XMPPMessageContextStringItemTag NS_EXTENSIBLE_STRING_ENUM; + +/// A tag assigned to a timestamp auxiliary value. +typedef NSString * XMPPMessageContextTimestampItemTag NS_EXTENSIBLE_STRING_ENUM; + +@interface XMPPMessageContextItemCoreDataStorageObject (Protected) + +/// The context element aggregating the value. +@property (nonatomic, strong, nullable) XMPPMessageContextCoreDataStorageObject *contextElement; + +@end + +/// A storage object representing a module-provided JID value assigned to a stored message. +@interface XMPPMessageContextJIDItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the value. +@property (nonatomic, copy, nullable) XMPPMessageContextJIDItemTag tag; + +/// The stored JID value. +@property (nonatomic, strong, nullable) XMPPJID *value; + +/// Returns a predicate to fetch values with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextJIDItemTag)value; + +/// Returns a predicate to fetch items with the specified value. ++ (NSPredicate *)jidPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +@end + +/// A storage object representing a module-provided marker assigned to a stored message. +@interface XMPPMessageContextMarkerItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the marker. +@property (nonatomic, copy, nullable) XMPPMessageContextMarkerItemTag tag; + +/// Returns a predicate to fetch markers with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextMarkerItemTag)value; + +@end + +/// A storage object representing a module-provided string value assigned to a stored message. +@interface XMPPMessageContextStringItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the value. +@property (nonatomic, copy, nullable) XMPPMessageContextStringItemTag tag; + +/// The stored string value. +@property (nonatomic, copy, nullable) NSString *value; + +/// Returns a predicate to fetch values with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextStringItemTag)tag; + +/// Returns a predicate to fetch items with the specified value. ++ (NSPredicate *)stringPredicateWithValue:(NSString *)value; + +@end + +/// A storage object representing a module-provided timestamp value assigned to a stored message. +@interface XMPPMessageContextTimestampItemCoreDataStorageObject : XMPPMessageContextItemCoreDataStorageObject + +/// The tag assigned to the value. +@property (nonatomic, copy, nullable) XMPPMessageContextTimestampItemTag tag; + +/// The stored timestamp value. +@property (nonatomic, strong, nullable) NSDate *value; + +/// Returns a predicate to fetch values with the specified tag. ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextTimestampItemTag)value; + +/// Returns a predicate to fetch items with values in the specified range. ++ (NSPredicate *)timestampRangePredicateWithStartValue:(nullable NSDate *)startValue endValue:(nullable NSDate *)endValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h new file mode 100644 index 0000000000..a8bdd3567d --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.h @@ -0,0 +1,151 @@ +#import +#import "XMPPJID.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageCoreDataStorageObject; + +typedef NS_ENUM(int16_t, XMPPMessageDirection); +typedef NS_ENUM(int16_t, XMPPMessageType); + +typedef NS_ENUM(NSInteger, XMPPMessageContentCompareOperator) { + /// Content is equal the search string. + XMPPMessageContentCompareOperatorEquals, + /// Content begins with the search string. + XMPPMessageContentCompareOperatorBeginsWith, + /// Content contains the search string. + XMPPMessageContentCompareOperatorContains, + /// Content ends with the search string. + XMPPMessageContentCompareOperatorEndsWith, + /// Content is equal to the search string and the search string can contain wildcard characters. + XMPPMessageContentCompareOperatorLike, + /// Content matches the the search string interpreted as a regular expression. + XMPPMessageContentCompareOperatorMatches +}; + +typedef NS_OPTIONS(NSInteger, XMPPMessageContentCompareOptions) { + /// Content comparison is case-insensitive. + XMPPMessageContentCompareCaseInsensitive = 1 << 0, + /// Content comparison is diacritic-insensitive. + XMPPMessageContentCompareDiacriticInsensitive = 1 << 1 +}; + +/** + A storage object representing a module-provided value assigned to a stored message. + + @see XMPPMessageCoreDataStorageObject + @see XMPPMessageContextCoreDataStorageObject + */ +@interface XMPPMessageContextItemCoreDataStorageObject : NSManagedObject + +@end + +@interface XMPPMessageContextItemCoreDataStorageObject (XMPPMessageCoreDataStorageFetch) + +/** + Returns a fetch request for timestamp context values with associated messages. + + A common application use case involves fetching temporally ordered messages. In terms of the message storage Core Data model, + this translates to fetching timestamp context values with specific predicates and then looking up the message objects they are attached to. + + The modules that assign custom timestamp context values will also provide appropriate predicates to be used with this method. + It is application's responsibility to avoid fetches with duplicate messages when composing predicates coming from multiple modules. + */ ++ (NSFetchRequest *)requestByTimestampsWithPredicate:(NSPredicate *)predicate + inAscendingOrder:(BOOL)isInAscendingOrder + fromManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include the single most relevant stream context timestamp per message. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)streamTimestampKindPredicate; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values from the given range. + + In order to request an open range, provide a nil value for the respective boundary. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)timestampRangePredicateWithStartValue:(nullable NSDate *)startValue endValue:(nullable NSDate *)endValue; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with the given @c fromJID value. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageFromJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with the given @c toJID value. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageToJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages exchanged with an entity with the given JID value. + + The relevant messages in this case are the outgoing ones with a matching @c toJID value and incoming ones with a matching @c fromJID value. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageRemotePartyJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with specific body content. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageBodyPredicateWithValue:(NSString *)value + compareOperator:(XMPPMessageContentCompareOperator)compareOperator + options:(XMPPMessageContentCompareOptions)options; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with specific subject content. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageSubjectPredicateWithValue:(NSString *)value + compareOperator:(XMPPMessageContentCompareOperator)compareOperator + options:(XMPPMessageContentCompareOptions)options; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages from the given thread. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageThreadPredicateWithValue:(NSString *)value; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages with the specified direction. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageDirectionPredicateWithValue:(XMPPMessageDirection)value; + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include timestamp values for messages of the specified type. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + */ ++ (NSPredicate *)messageTypePredicateWithValue:(XMPPMessageType)value; + +/// Returns the message the context item is associated with. +- (XMPPMessageCoreDataStorageObject *)message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m new file mode 100644 index 0000000000..8d92e14b37 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageContextItemCoreDataStorageObject.m @@ -0,0 +1,194 @@ +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "XMPPMessageContextItemCoreDataStorageObject+Protected.h" +#import "XMPPJID.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" + +@interface XMPPMessageContextItemCoreDataStorageObject () + +@property (nonatomic, strong, nullable) XMPPMessageContextCoreDataStorageObject *contextElement; + +@end + +@implementation XMPPMessageContextItemCoreDataStorageObject + +@dynamic contextElement; + ++ (NSPredicate *)tagPredicateWithValue:(NSString *)value +{ + return [NSPredicate predicateWithFormat:@"%K = %@", NSStringFromSelector(@selector(tag)), value]; +} + +@end + +@interface XMPPMessageContextJIDItemCoreDataStorageObject () + +@property (nonatomic, copy, nullable) NSString *valueDomain; +@property (nonatomic, copy, nullable) NSString *valueResource; +@property (nonatomic, copy, nullable) NSString *valueUser; + +@end + +@interface XMPPMessageContextJIDItemCoreDataStorageObject (CoreDataGeneratedPrimitiveAccessors) + +- (XMPPJID *)primitiveValue; +- (void)setPrimitiveValue:(XMPPJID *)value; +- (void)setPrimitiveValueDomain:(NSString *)value; +- (void)setPrimitiveValueResource:(NSString *)value; +- (void)setPrimitiveValueUser:(NSString *)value; + +@end + +@implementation XMPPMessageContextJIDItemCoreDataStorageObject + +@dynamic tag, valueDomain, valueResource, valueUser; + +#pragma mark - value transient property + +- (XMPPJID *)value +{ + [self willAccessValueForKey:NSStringFromSelector(@selector(value))]; + XMPPJID *value = [self primitiveValue]; + [self didAccessValueForKey:NSStringFromSelector(@selector(value))]; + + if (value) { + return value; + } + + XMPPJID *newValue = [XMPPJID jidWithUser:self.valueUser domain:self.valueDomain resource:self.valueResource]; + [self setPrimitiveValue:newValue]; + + return newValue; +} + +- (void)setValue:(XMPPJID *)value +{ + if ([self.value isEqualToJID:value]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; + [self setPrimitiveValue:value]; + [self setPrimitiveValueDomain:value.domain]; + [self setPrimitiveValueResource:value.resource]; + [self setPrimitiveValueUser:value.user]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; +} + +- (void)setValueDomain:(NSString *)valueDomain +{ + if ([self.valueDomain isEqualToString:valueDomain]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self setPrimitiveValueDomain:valueDomain]; + [self setPrimitiveValue:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueDomain))]; +} + +- (void)setValueResource:(NSString *)valueResource +{ + if ([self.valueResource isEqualToString:valueResource]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self setPrimitiveValueResource:valueResource]; + [self setPrimitiveValue:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueResource))]; +} + +- (void)setValueUser:(NSString *)valueUser +{ + if ([self.valueUser isEqualToString:valueUser]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self setPrimitiveValueUser:valueUser]; + [self setPrimitiveValue:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(value))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(valueUser))]; +} + +#pragma mark - Public + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextJIDItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + ++ (NSPredicate *)jidPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + return [self xmpp_jidPredicateWithDomainKeyPath:NSStringFromSelector(@selector(valueDomain)) + resourceKeyPath:NSStringFromSelector(@selector(valueResource)) + userKeyPath:NSStringFromSelector(@selector(valueUser)) + value:value + compareOptions:compareOptions]; +} + +@end + +@implementation XMPPMessageContextMarkerItemCoreDataStorageObject + +@dynamic tag; + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextMarkerItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + +@end + +@implementation XMPPMessageContextStringItemCoreDataStorageObject + +@dynamic tag, value; + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextStringItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + ++ (NSPredicate *)stringPredicateWithValue:(NSString *)value +{ + return [NSPredicate predicateWithFormat:@"%K = %@", NSStringFromSelector(@selector(value)), value]; +} + +@end + +@implementation XMPPMessageContextTimestampItemCoreDataStorageObject + +@dynamic tag, value; + ++ (NSPredicate *)tagPredicateWithValue:(XMPPMessageContextTimestampItemTag)value +{ + return [super tagPredicateWithValue:value]; +} + ++ (NSPredicate *)timestampRangePredicateWithStartValue:(NSDate *)startValue endValue:(NSDate *)endValue +{ + NSMutableArray *subpredicates = [[NSMutableArray alloc] init]; + + if (startValue) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K >= %@", NSStringFromSelector(@selector(value)), startValue]]; + } + + if (endValue) { + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K <= %@", NSStringFromSelector(@selector(value)), endValue]]; + } + + return subpredicates.count == 1 ? subpredicates.firstObject : [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]; +} + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorage.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.h new file mode 100644 index 0000000000..a244cfc882 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.h @@ -0,0 +1,65 @@ +#import "XMPPCoreDataStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageCoreDataStorageObject, XMPPElementEvent, XMPPMessageCoreDataStorageTransaction; + +/** + A client message storage implementation that supports per-module extensibility while maintaining a fixed underlying Core Data model. + + The design is based on assigning auxiliary context objects to each stored XMPP message. Those context objects aggregate arbitrary sets of tagged primitive values. + By defining their own context aggregations and value tags, modules can extend storage capabilities and expose them via a simple API using categories on the core classes. + + The application-facing API consists of the main interface and any categories provided by module authors. The protected interface provides module helper methods. + + @see XMPPMessageCoreDataStorageObject + */ +@interface XMPPMessageCoreDataStorage : XMPPCoreDataStorage + +/// Inserts a core message storage object into the application-facing (main thread) managed object context. +- (XMPPMessageCoreDataStorageObject *)insertOutgoingMessageStorageObject; + +/** + Provides a storage transaction for processing an incoming message stream event on a background thread managed object context. + + A new incoming direction storage object will be inserted and stream event properties registered on it before it is provided to update blocks. + The transaction will trigger an assertion if a message storage object registered for the provided stream event already exists. + + Callers should perform all actions on the provided transaction immediately in the handler block. + Attempts to store and access it later will trigger an assertion. + */ +- (void)provideTransactionForIncomingMessageEvent:(XMPPElementEvent *)event withHandler:(void (^)(XMPPMessageCoreDataStorageTransaction *transaction))handler; + +/** + Provides a storage transaction for processing an outgoing message stream event on a background thread managed object context. + + A previously inserted outgoing direction storage object will be looked up and stream event properties registered on it + before it is provided to update blocks. + It is assumed that the application had registered the respective stream event ID using @c registerOutgoingMessageStreamEventID: before sending the message, + otherwise an assertion will be triggered. + + Callers should perform all actions on the provided transaction immediately in the handler block. + Attempts to store and access it later will trigger an assertion. + */ +- (void)provideTransactionForOutgoingMessageEvent:(XMPPElementEvent *)event withHandler:(void (^)(XMPPMessageCoreDataStorageTransaction *transaction))handler; + +@end + +/** + An object that manages storage updates associated with a single message stream event. + + The actions are enqueued to be performed as a single batch once the given event has been processed by all modules. + This way, the main thread context never sees a message that has only been partially processed. It is particularly important when handling XMPP extensions + that affect temporal ordering or replace messages (e.g. delayed delivery, last message correction). + */ +@interface XMPPMessageCoreDataStorageTransaction : NSObject + +// Transaction lifetime is managed by the storage. +- (instancetype)init NS_UNAVAILABLE; + +/// Enqueues actions to be performed on a corresponding storage object in response to a message stream event. +- (void)scheduleStorageUpdateWithBlock:(void (^)(XMPPMessageCoreDataStorageObject *messageObject))block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorage.m b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.m new file mode 100644 index 0000000000..5c797ecf3d --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorage.m @@ -0,0 +1,182 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" +#import "XMPPStream.h" + +typedef NSMutableArray XMPPMessageCoreDataStorageUpdateBatch; + +static void * const XMPPMessageCoreDataStorageIncomingEventObservationContext = (void *)&XMPPMessageCoreDataStorageIncomingEventObservationContext; +static void * const XMPPMessageCoreDataStorageOutgoingEventObservationContext = (void *)&XMPPMessageCoreDataStorageOutgoingEventObservationContext; + +static void * const XMPPMessageCoreDataStorageTransactionIndexQueueTag = (void *)&XMPPMessageCoreDataStorageTransactionIndexQueueTag; + +static NSString * const XMPPElementEventCompletionKeyPath = @"processingCompleted"; + +@interface XMPPMessageCoreDataStorage () + +@property (nonatomic, copy, readonly) NSMutableDictionary *transactionIndex; +@property (nonatomic, strong, readonly) dispatch_queue_t transactionIndexQueue; + +@end + +@interface XMPPMessageCoreDataStorageTransaction () + +@property (nonatomic, unsafe_unretained, readonly) XMPPMessageCoreDataStorage *storage; +@property (nonatomic, strong, readonly) XMPPMessageCoreDataStorageUpdateBatch *updateBatch; + +- (instancetype)initWithStorage:(XMPPMessageCoreDataStorage *)storage; + +@end + +@implementation XMPPMessageCoreDataStorage + +- (id)initWithDatabaseFilename:(NSString *)aDatabaseFileName storeOptions:(NSDictionary *)theStoreOptions +{ + self = [super initWithDatabaseFilename:aDatabaseFileName storeOptions:theStoreOptions]; + if (self) { + [self commonMessageStorageInit]; + } + return self; +} + +- (id)initWithInMemoryStore +{ + self = [super initWithInMemoryStore]; + if (self) { + [self commonMessageStorageInit]; + } + return self; +} + +- (void)commonMessageStorageInit +{ + _transactionIndex = [[NSMutableDictionary alloc] init]; + _transactionIndexQueue = dispatch_queue_create("XMPPMessageCoreDataStorage.transactionIndexQueue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(_transactionIndexQueue, + XMPPMessageCoreDataStorageTransactionIndexQueueTag, + XMPPMessageCoreDataStorageTransactionIndexQueueTag, + NULL); +} + +- (XMPPMessageCoreDataStorageObject *)insertOutgoingMessageStorageObject +{ + XMPPMessageCoreDataStorageObject *messageObject = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.mainThreadManagedObjectContext]; + messageObject.direction = XMPPMessageDirectionOutgoing; + return messageObject; +} + +- (void)provideTransactionForIncomingMessageEvent:(XMPPElementEvent *)event withHandler:(void (^)(XMPPMessageCoreDataStorageTransaction * _Nonnull))handler +{ + [self provideTransactionForMessageEvent:event withObservationContext:XMPPMessageCoreDataStorageIncomingEventObservationContext handler:handler]; +} + +- (void)provideTransactionForOutgoingMessageEvent:(XMPPElementEvent *)event withHandler:(void (^)(XMPPMessageCoreDataStorageTransaction * _Nonnull))handler +{ + [self provideTransactionForMessageEvent:event withObservationContext:XMPPMessageCoreDataStorageOutgoingEventObservationContext handler:handler]; +} + +- (void)provideTransactionForMessageEvent:(XMPPElementEvent *)event withObservationContext:(void *)observationContext handler:(void (^)(XMPPMessageCoreDataStorageTransaction *transaction))handler +{ + id transactionLookupToken = [event beginDelayedProcessing]; + + dispatch_async(self.transactionIndexQueue, ^{ + XMPPMessageCoreDataStorageTransaction *transaction = self.transactionIndex[event.uniqueID]; + if (!transaction) { + transaction = [[XMPPMessageCoreDataStorageTransaction alloc] initWithStorage:self]; + self.transactionIndex[event.uniqueID] = transaction; + + [event addObserver:transaction + forKeyPath:XMPPElementEventCompletionKeyPath + options:NSKeyValueObservingOptionNew + context:observationContext]; + } + + handler(transaction); + + [event endDelayedProcessingWithToken:transactionLookupToken]; + }); +} + +- (void)unregisterTransactionForMessageEvent:(XMPPElementEvent *)event withObservationContext:(void *)observationContext +{ + dispatch_async(self.transactionIndexQueue, ^{ + XMPPMessageCoreDataStorageTransaction *transaction = self.transactionIndex[event.uniqueID]; + NSAssert(transaction, @"No transaction registered for the given event"); + [event removeObserver:transaction forKeyPath:XMPPElementEventCompletionKeyPath context:observationContext]; + [self.transactionIndex removeObjectForKey:event.uniqueID]; + }); +} + +@end + +@implementation XMPPMessageCoreDataStorageTransaction + +- (instancetype)initWithStorage:(XMPPMessageCoreDataStorage *)storage +{ + self = [super init]; + if (self) { + _storage = storage; + _updateBatch = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)scheduleStorageUpdateWithBlock:(void (^)(XMPPMessageCoreDataStorageObject * _Nonnull))block +{ + NSAssert(dispatch_get_specific(XMPPMessageCoreDataStorageTransactionIndexQueueTag), @"This has to be invoked from a transaction handler block"); + [self.updateBatch addObject:block]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == XMPPMessageCoreDataStorageIncomingEventObservationContext) { + if ([keyPath isEqualToString:XMPPElementEventCompletionKeyPath] && [change[NSKeyValueChangeNewKey] isEqualToNumber:@YES]) { + [self observeProcessingCompletionForIncomingMessageEvent:object]; + [self.storage unregisterTransactionForMessageEvent:object withObservationContext:context]; + } + } else if (context == XMPPMessageCoreDataStorageOutgoingEventObservationContext) { + if ([keyPath isEqualToString:XMPPElementEventCompletionKeyPath] && [change[NSKeyValueChangeNewKey] isEqualToNumber:@YES]) { + [self observeProcessingCompletionForOutgoingMessageEvent:object]; + [self.storage unregisterTransactionForMessageEvent:object withObservationContext:context]; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)observeProcessingCompletionForIncomingMessageEvent:(XMPPElementEvent *)event +{ + [self.storage scheduleBlock:^{ + NSAssert(![XMPPMessageCoreDataStorageObject findWithStreamEventID:event.uniqueID inManagedObjectContext:self.storage.managedObjectContext], + @"Unexpected existing storage object found"); + + XMPPMessageCoreDataStorageObject *insertedMessageObject = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.managedObjectContext]; + insertedMessageObject.direction = XMPPMessageDirectionIncoming; + [insertedMessageObject registerIncomingMessageStreamEventID:event.uniqueID streamJID:event.myJID streamEventTimestamp:event.timestamp]; + + for (void (^updateBlock)(XMPPMessageCoreDataStorageObject *) in self.updateBatch) { + updateBlock(insertedMessageObject); + } + }]; +} + +- (void)observeProcessingCompletionForOutgoingMessageEvent:(XMPPElementEvent *)event +{ + [self.storage scheduleBlock:^{ + XMPPMessageCoreDataStorageObject *existingMessageObject = [XMPPMessageCoreDataStorageObject findWithStreamEventID:event.uniqueID + inManagedObjectContext:self.storage.managedObjectContext]; + NSAssert(existingMessageObject, @"Expected existing storage object not found"); + NSAssert(existingMessageObject.direction == XMPPMessageDirectionOutgoing, @"Unexpected existing storage object direction"); + + [existingMessageObject registerOutgoingMessageStreamJID:event.myJID streamEventTimestamp:event.timestamp]; + + for (void (^updateBlock)(XMPPMessageCoreDataStorageObject *) in self.updateBatch) { + updateBlock(existingMessageObject); + } + }]; +} + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h new file mode 100644 index 0000000000..20c2be75fe --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.h @@ -0,0 +1,94 @@ +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" +#import "XMPPMessageContextItemCoreDataStorageObject+Protected.h" + +NS_ASSUME_NONNULL_BEGIN + +/// An API to be used by modules to manipulate auxiliary context objects assigned to a stored message. +@interface XMPPMessageCoreDataStorageObject (ContextHelpers) + +/// Inserts a new context element associated with the message. +- (XMPPMessageContextCoreDataStorageObject *)appendContextElement; + +/** + @brief Enumerates the message's context elements until the lookup block returns a non-nil value and returns that value. + @discussion This method expects the lookup block to only return a non-nil value for a single element and will trigger an assertion otherwise. + */ +- (nullable id)lookupInContextWithBlock:(id __nullable (^)(XMPPMessageContextCoreDataStorageObject *contextElement))lookupBlock; + +@end + +/// An API to be used by modules to manipulate auxiliary context object values assigned to a stored message. +@interface XMPPMessageContextCoreDataStorageObject (ContextHelpers) + +/// Inserts a new JID value associated with the context element. +- (XMPPMessageContextJIDItemCoreDataStorageObject *)appendJIDItemWithTag:(XMPPMessageContextJIDItemTag)tag value:(XMPPJID *)value +NS_SWIFT_NAME(appendJIDItem(with:value:)); + +/// Inserts a new marker associated with the context element. +- (XMPPMessageContextMarkerItemCoreDataStorageObject *)appendMarkerItemWithTag:(XMPPMessageContextMarkerItemTag)tag +NS_SWIFT_NAME(appendMarkerItem(with:)); + +/// Inserts a new string value associated with the context element. +- (XMPPMessageContextStringItemCoreDataStorageObject *)appendStringItemWithTag:(XMPPMessageContextStringItemTag)tag value:(NSString *)value +NS_SWIFT_NAME(appendStringItem(with:value:)); + +/// Inserts a new timestamp value associated with the context element. +- (XMPPMessageContextTimestampItemCoreDataStorageObject *)appendTimestampItemWithTag:(XMPPMessageContextTimestampItemTag)tag value:(NSDate *)value +NS_SWIFT_NAME(appendTimestampItem(with:value:)); + +/// Removes all JID values with the given tag associated with the context element. +- (void)removeJIDItemsWithTag:(XMPPMessageContextJIDItemTag)tag +NS_SWIFT_NAME(removeJIDItems(with:)); + +/// Removes all markers with the given tag associated with the context element. +- (void)removeMarkerItemsWithTag:(XMPPMessageContextMarkerItemTag)tag +NS_SWIFT_NAME(removeMarkerItems(with:)); + +/// Removes all string values with the given tag associated with the context element. +- (void)removeStringItemsWithTag:(XMPPMessageContextStringItemTag)tag +NS_SWIFT_NAME(removeStringItems(with:)); + +/// Removes all timestamp values with the given tag associated with the context element. +- (void)removeTimestampItemsWithTag:(XMPPMessageContextTimestampItemTag)tag +NS_SWIFT_NAME(removeTimestampItems(with:)); + +/// Returns all JID values with the given tag associated with the context element. +- (NSSet *)jidItemValuesForTag:(XMPPMessageContextJIDItemTag)tag +NS_SWIFT_NAME(jidItemValues(for:)); + +/// @brief Returns the unique JID value with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching value. +- (nullable XMPPJID *)jidItemValueForTag:(XMPPMessageContextJIDItemTag)tag +NS_SWIFT_NAME(jidItemValue(for:)); + +/// Returns the number of markers with the given tag associated with the context element. +- (NSInteger)markerItemCountForTag:(XMPPMessageContextMarkerItemTag)tag +NS_SWIFT_NAME(markerItemCount(for:)); + +/// @brief Tests whether there is a marker with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching marker. +- (BOOL)hasMarkerItemForTag:(XMPPMessageContextMarkerItemTag)tag +NS_SWIFT_NAME(hasMarkerItem(for:)); + +/// Returns all string values with the given tag associated with the context element. +- (NSSet *)stringItemValuesForTag:(XMPPMessageContextStringItemTag)tag +NS_SWIFT_NAME(stringItemValues(for:)); + +/// @brief Returns the unique string value with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching value. +- (nullable NSString *)stringItemValueForTag:(XMPPMessageContextStringItemTag)tag +NS_SWIFT_NAME(stringItemValue(for:)); + +/// Returns all timestamp values with the given tag associated with the context element. +- (NSSet *)timestampItemValuesForTag:(XMPPMessageContextTimestampItemTag)tag +NS_SWIFT_NAME(timestampItemValues(for:)); + +/// @brief Returns the unique timestamp value with the given tag associated with the context element. +/// @discussion Will trigger an assertion if there is more than one matching value. +- (nullable NSDate *)timestampItemValueForTag:(XMPPMessageContextTimestampItemTag)tag +NS_SWIFT_NAME(timestampItemValue(for:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m new file mode 100644 index 0000000000..c2c325ec54 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+ContextHelpers.m @@ -0,0 +1,221 @@ +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageContextCoreDataStorageObject+Protected.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" + +@implementation XMPPMessageCoreDataStorageObject (ContextHelpers) + +- (XMPPMessageContextCoreDataStorageObject *)appendContextElement +{ + NSAssert(self.managedObjectContext, @"Attempted to append a context element to a message not associated with any managed object context"); + + XMPPMessageContextCoreDataStorageObject *insertedElement = [XMPPMessageContextCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedElement.message = self; + return insertedElement; +} + +- (id)lookupInContextWithBlock:(id (^)(XMPPMessageContextCoreDataStorageObject * _Nonnull))lookupBlock +{ + id lookupResult; + for (XMPPMessageContextCoreDataStorageObject *contextElement in self.contextElements) { + id elementResult = lookupBlock(contextElement); + if (!elementResult) { + continue; + } + NSAssert(!lookupResult, @"A unique lookup result is expected"); + lookupResult = elementResult; +#ifdef NS_BLOCK_ASSERTIONS + break; +#endif + } + return lookupResult; +} + +@end + +@implementation XMPPMessageContextCoreDataStorageObject (ContextHelpers) + +- (XMPPMessageContextJIDItemCoreDataStorageObject *)appendJIDItemWithTag:(XMPPMessageContextJIDItemTag)tag value:(XMPPJID *)value +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextJIDItemCoreDataStorageObject *insertedItem = [XMPPMessageContextJIDItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.value = value; + insertedItem.contextElement = self; + return insertedItem; +} + +- (XMPPMessageContextMarkerItemCoreDataStorageObject *)appendMarkerItemWithTag:(XMPPMessageContextMarkerItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextMarkerItemCoreDataStorageObject *insertedItem = [XMPPMessageContextMarkerItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.contextElement = self; + return insertedItem; +} + +- (XMPPMessageContextStringItemCoreDataStorageObject *)appendStringItemWithTag:(XMPPMessageContextStringItemTag)tag value:(NSString *)value +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextStringItemCoreDataStorageObject *insertedItem = [XMPPMessageContextStringItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.value = value; + insertedItem.contextElement = self; + return insertedItem; +} + +- (XMPPMessageContextTimestampItemCoreDataStorageObject *)appendTimestampItemWithTag:(XMPPMessageContextTimestampItemTag)tag value:(NSDate *)value +{ + NSAssert(self.managedObjectContext, @"Attempted to append an item to a context element not associated with any managed object context"); + + XMPPMessageContextTimestampItemCoreDataStorageObject *insertedItem = [XMPPMessageContextTimestampItemCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.managedObjectContext]; + insertedItem.tag = tag; + insertedItem.value = value; + insertedItem.contextElement = self; + return insertedItem; +} + +- (void)removeJIDItemsWithTag:(XMPPMessageContextJIDItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextJIDItemCoreDataStorageObject *jidItem in [self jidItemsForTag:tag expectingSingleElement:NO]) { + [self removeJidItemsObject:jidItem]; + [self.managedObjectContext deleteObject:jidItem]; + } +} + +- (void)removeMarkerItemsWithTag:(XMPPMessageContextMarkerItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextMarkerItemCoreDataStorageObject *markerItem in [self markerItemsForTag:tag expectingSingleElement:NO]) { + [self removeMarkerItemsObject:markerItem]; + [self.managedObjectContext deleteObject:markerItem]; + } +} + +- (void)removeStringItemsWithTag:(XMPPMessageContextStringItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextStringItemCoreDataStorageObject *stringItem in [self stringItemsForTag:tag expectingSingleElement:NO]) { + [self removeStringItemsObject:stringItem]; + [self.managedObjectContext deleteObject:stringItem]; + } +} + +- (void)removeTimestampItemsWithTag:(XMPPMessageContextTimestampItemTag)tag +{ + NSAssert(self.managedObjectContext, @"Attempted to remove an item from a context element not associated with any managed object context"); + + for (XMPPMessageContextTimestampItemCoreDataStorageObject *timestampItem in [self timestampItemsForTag:tag expectingSingleElement:NO]) { + [self removeTimestampItemsObject:timestampItem]; + [self.managedObjectContext deleteObject:timestampItem]; + } +} + +- (NSSet *)jidItemValuesForTag:(XMPPMessageContextJIDItemTag)tag +{ + return [[self jidItemsForTag:tag expectingSingleElement:NO] valueForKey:NSStringFromSelector(@selector(value))]; +} + +- (XMPPJID *)jidItemValueForTag:(XMPPMessageContextJIDItemTag)tag +{ + return [[self jidItemsForTag:tag expectingSingleElement:YES] anyObject].value; +} + +- (NSInteger)markerItemCountForTag:(XMPPMessageContextMarkerItemTag)tag +{ + return [self markerItemsForTag:tag expectingSingleElement:NO].count; +} + +- (BOOL)hasMarkerItemForTag:(XMPPMessageContextMarkerItemTag)tag +{ + return [[self markerItemsForTag:tag expectingSingleElement:YES] anyObject] != nil; +} + +- (NSSet *)stringItemValuesForTag:(XMPPMessageContextStringItemTag)tag +{ + return [[self stringItemsForTag:tag expectingSingleElement:NO] valueForKey:NSStringFromSelector(@selector(value))]; +} + +- (NSString *)stringItemValueForTag:(XMPPMessageContextStringItemTag)tag +{ + return [[self stringItemsForTag:tag expectingSingleElement:YES] anyObject].value; +} + +- (NSSet *)timestampItemValuesForTag:(XMPPMessageContextTimestampItemTag)tag +{ + return [[self timestampItemsForTag:tag expectingSingleElement:NO] valueForKey:NSStringFromSelector(@selector(value))]; +} + +- (NSDate *)timestampItemValueForTag:(XMPPMessageContextTimestampItemTag)tag +{ + return [[self timestampItemsForTag:tag expectingSingleElement:YES] anyObject].value; +} + +- (NSSet *)jidItemsForTag:(XMPPMessageContextJIDItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.jidItems objectsPassingTest:^BOOL(XMPPMessageContextJIDItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +- (NSSet *)markerItemsForTag:(XMPPMessageContextMarkerItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.markerItems objectsPassingTest:^BOOL(XMPPMessageContextMarkerItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +- (NSSet *)stringItemsForTag:(XMPPMessageContextStringItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.stringItems objectsPassingTest:^BOOL(XMPPMessageContextStringItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +- (NSSet *)timestampItemsForTag:(XMPPMessageContextTimestampItemTag)tag expectingSingleElement:(BOOL)isSingleElementExpected +{ + NSSet *filteredSet = [self.timestampItems objectsPassingTest:^BOOL(XMPPMessageContextTimestampItemCoreDataStorageObject * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL matchesTag = [obj.tag isEqualToString:tag]; +#ifdef NS_BLOCK_ASSERTIONS + if (matchesTag && isSingleElementExpected) { + *stop = YES; + } +#endif + return matchesTag; + }]; + NSAssert(!(isSingleElementExpected && filteredSet.count > 1) , @"Only one item expected"); + return filteredSet; +} + +@end diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h new file mode 100644 index 0000000000..e57b32b5b1 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject+Protected.h @@ -0,0 +1,83 @@ +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessageContextCoreDataStorageObject; + +/// An API to be used by modules to manipulate core message objects. +@interface XMPPMessageCoreDataStorageObject (Protected) + +/// The persistent attribute storing the domain component of @c fromJID property. +@property (nonatomic, copy, nullable) NSString *fromDomain; + +/// The persistent attribute storing the resource component of @c fromJID property. +@property (nonatomic, copy, nullable) NSString *fromResource; + +/// The persistent attribute storing the user component of @c fromJID property. +@property (nonatomic, copy, nullable) NSString *fromUser; + +/// The persistent attribute storing the domain component of @c toJID property. +@property (nonatomic, copy, nullable) NSString *toDomain; + +/// The persistent attribute storing the resource component of @c toJID property. +@property (nonatomic, copy, nullable) NSString *toResource; + +/// The persistent attribute storing the user component of @c toJID property. +@property (nonatomic, copy, nullable) NSString *toUser; + +/// The auxiliary context objects assigned to the message. +@property (nonatomic, copy, nullable) NSSet *contextElements; + +/// @brief Returns the message object from the given context that has a stream element event with the given ID recorded. +/// @discussion As the stream element event IDs are expected to be unique, this method will trigger an assertion if more than one matching object is found. ++ (nullable XMPPMessageCoreDataStorageObject *)findWithStreamEventID:(NSString *)streamEventID + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Returns the message object from the given context with the given stanza ID, if only one is found. +/// @discussion The XMPP standard does not enforce uniqueness of stanza IDs and this method will return nil if more than one matching object is found. ++ (nullable XMPPMessageCoreDataStorageObject *)findWithUniqueStanzaID:(NSString *)stanzaID + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext; + +/// @brief Records stream element event properties for the incoming message. +/// @discussion This method will trigger an assertion unless invoked on an incoming message. It will also trigger an assertion when invoked more than once. +- (void)registerIncomingMessageStreamEventID:(NSString *)streamEventID + streamJID:(XMPPJID *)streamJID + streamEventTimestamp:(NSDate *)streamEventTimestamp; + +/// @brief Records the core RFC 3921/6121 properties of an incoming message from the given XML representation. +/// @discussion This method will trigger an assertion unless invoked on an incoming message. Subsequent invocations will overwrite previous values. +- (void)registerIncomingMessageCore:(XMPPMessage *)message; + +/** + Records stream element event properties for the sent message that has a pending outgoing event registration. + + This method will trigger an assertion unless invoked on an outgoing message. + It will also trigger an assertion if not matched with a prior @c registerOutgoingMessageStreamEventID: invocation. + */ +- (void)registerOutgoingMessageStreamJID:(XMPPJID *)streamJID streamEventTimestamp:(NSDate *)streamEventTimestamp; + +/** + Retires the current stream element event timestamp or marks the initial timestamp for retirement if no timestamp is currently registered. + + A single message object can be associated with multiple timestamps, e.g. there can be several transmission attempts for an outgoing message + or a message can have both stream timestamp and delayed delivery timestamp assigned. + + At the same time, a common application use case involves fetching temporally ordered messages. In terms of the message storage Core Data model, + this translates to fetching timestamp context values with specific tags and then looking up the message objects they are attached to. + + For this approach to work, there needs to be at most one timestamp per message that meets the fetch criteria; retiring stream timestamps allows to exclude duplicates. + */ +- (void)retireStreamTimestamp; + +@end + +@interface XMPPMessageCoreDataStorageObject (CoreDataGeneratedRelationshipAccesssors) + +- (void)addContextElementsObject:(XMPPMessageContextCoreDataStorageObject *)value; +- (void)removeContextElementsObject:(XMPPMessageContextCoreDataStorageObject *)value; +- (void)addContextElements:(NSSet *)value; +- (void)removeContextElements:(NSSet *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h new file mode 100644 index 0000000000..87f5c8a606 --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.h @@ -0,0 +1,86 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPJID, XMPPMessage; + +typedef NS_ENUM(int16_t, XMPPMessageDirection) { + /// A value indicating that the message's origin is not defined. + XMPPMessageDirectionUnspecified, + /// A value indicating that the message has been received from the stream. + XMPPMessageDirectionIncoming, + /// A value indicating that the message is originating from the device. + XMPPMessageDirectionOutgoing +}; + +typedef NS_ENUM(int16_t, XMPPMessageType) { + /// A value indicating normal message type as per RFC 3921/6121 + XMPPMessageTypeNormal, + /// A value indicating chat message type as per RFC 3921/6121 + XMPPMessageTypeChat, + /// A value indicating error message type as per RFC 3921/6121 + XMPPMessageTypeError, + /// A value indicating groupchat message type as per RFC 3921/6121 + XMPPMessageTypeGroupchat, + /// A value indicating headline message type as per RFC 3921/6121 + XMPPMessageTypeHeadline +}; + +/** + An object storing the core XMPP message properties defined in RFC 3921/6121. + + @see XMPPMessageCoreDataStorage + @see XMPPMessageContextCoreDataStorageObject + @see XMPPMessageContextItemCoreDataStorageObject + */ +@interface XMPPMessageCoreDataStorageObject : NSManagedObject + +/// The value of "from" attribute (transient). +@property (nonatomic, strong, nullable) XMPPJID *fromJID; + +/// The value of "to" attribute (transient). +@property (nonatomic, strong, nullable) XMPPJID *toJID; + +/// The contents of "body" child element. +@property (nonatomic, copy, nullable) NSString *body; + +/// The value of "id" attribute. +@property (nonatomic, copy, nullable) NSString *stanzaID; + +/// The contents of "subject" child element. +@property (nonatomic, copy, nullable) NSString *subject; + +/// The contents of "thread" child element. +@property (nonatomic, copy, nullable) NSString *thread; + +/// The transmission direction from client's point of view. +@property (nonatomic, assign) XMPPMessageDirection direction; + +/// The value of "type" attribute. +@property (nonatomic, assign) XMPPMessageType type; + +/// @brief Returns the XML representation of the message including only the core RFC 3921/6121 properties. +/// @discussion Applications employing store-then-send approach to messaging can use this method to obtain the seed of an outgoing message stanza they later decorate with extension-derived values. +- (XMPPMessage *)coreMessage; + +/** + Records a unique outgoing XMPP stream element event ID for the message. + + After recording the ID, the application should use the @c sendElement:registeringEventWithID:andGetReceipt: method to send the message, providing the recorded value. + This way, modules will be able to track the message in their stream callbacks and update the storage accordingly. + + This method will trigger an assertion unless invoked on an outgoing message. It will also trigger an assertion if called more than once per actual transmission attempt. + */ +- (void)registerOutgoingMessageStreamEventID:(NSString *)outgoingMessageStreamEventID; + +/// @brief Returns the local stream JID for the most recent stream element event associated with the message. +/// @discussion Incoming messages always have a single stream element event associated with them. Outgoing messages can have 0 or more, one per each transmission attempt. +- (nullable XMPPJID *)streamJID; + +/// @brief Returns the timestamp for the most recent stream element event associated with the message. +/// @discussion Incoming messages always have a single stream element event associated with them. Outgoing messages can have 0 or more, one per each transmission attempt. +- (nullable NSDate *)streamTimestamp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m new file mode 100644 index 0000000000..0735d35d8f --- /dev/null +++ b/Extensions/MessageStorage/XMPPMessageCoreDataStorageObject.m @@ -0,0 +1,536 @@ +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessageContextItemCoreDataStorageObject.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" +#import "XMPPJID.h" +#import "XMPPMessage.h" + +static XMPPMessageContextJIDItemTag const XMPPMessageContextStreamJIDTag = @"XMPPMessageContextStreamJID"; +static XMPPMessageContextMarkerItemTag const XMPPMessageContextPendingStreamContextAssignmentTag = @"XMPPMessageContextPendingStreamContextAssignment"; +static XMPPMessageContextMarkerItemTag const XMPPMessageContextLatestStreamTimestampRetirementTag = @"XMPPMessageContextLatestStreamTimestampRetirement"; +static XMPPMessageContextStringItemTag const XMPPMessageContextStreamEventIDTag = @"XMPPMessageContextStreamEventID"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextActiveStreamTimestampTag = @"XMPPMessageContextActiveStreamTimestamp"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextRetiredStreamTimestampTag = @"XMPPMessageContextRetiredStreamTimestamp"; + +@interface XMPPMessageCoreDataStorageObject () + +@property (nonatomic, copy, nullable) NSString *fromDomain; +@property (nonatomic, copy, nullable) NSString *fromResource; +@property (nonatomic, copy, nullable) NSString *fromUser; +@property (nonatomic, copy, nullable) NSString *toDomain; +@property (nonatomic, copy, nullable) NSString *toResource; +@property (nonatomic, copy, nullable) NSString *toUser; + +@property (nonatomic, copy, nullable) NSSet *contextElements; + +@end + +@interface XMPPMessageCoreDataStorageObject (CoreDataGeneratedPrimitiveAccessors) + +- (XMPPJID *)primitiveFromJID; +- (void)setPrimitiveFromJID:(XMPPJID *)value; +- (void)setPrimitiveFromDomain:(NSString *)value; +- (void)setPrimitiveFromResource:(NSString *)value; +- (void)setPrimitiveFromUser:(NSString *)value; + +- (XMPPJID *)primitiveToJID; +- (void)setPrimitiveToJID:(XMPPJID *)value; +- (void)setPrimitiveToDomain:(NSString *)value; +- (void)setPrimitiveToResource:(NSString *)value; +- (void)setPrimitiveToUser:(NSString *)value; + +@end + +@implementation XMPPMessageCoreDataStorageObject + +@dynamic fromDomain, fromResource, fromUser, toDomain, toResource, toUser, body, stanzaID, subject, thread, direction, type, contextElements; + +#pragma mark - fromJID transient property + +- (XMPPJID *)fromJID +{ + [self willAccessValueForKey:NSStringFromSelector(@selector(fromJID))]; + XMPPJID *fromJID = [self primitiveFromJID]; + [self didAccessValueForKey:NSStringFromSelector(@selector(fromJID))]; + + if (fromJID) { + return fromJID; + } + + XMPPJID *newFromJID = [XMPPJID jidWithUser:self.fromUser domain:self.fromDomain resource:self.fromResource]; + [self setPrimitiveFromJID:newFromJID]; + + return newFromJID; +} + +- (void)setFromJID:(XMPPJID *)fromJID +{ + if ([self.fromJID isEqualToJID:fromJID options:XMPPJIDCompareFull]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; + [self setPrimitiveFromJID:fromJID]; + [self setPrimitiveFromDomain:fromJID.domain]; + [self setPrimitiveFromResource:fromJID.resource]; + [self setPrimitiveFromUser:fromJID.user]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; +} + +- (void)setFromDomain:(NSString *)fromDomain +{ + if ([self.fromDomain isEqualToString:fromDomain]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self setPrimitiveFromDomain:fromDomain]; + [self setPrimitiveFromJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromDomain))]; +} + +- (void)setFromResource:(NSString *)fromResource +{ + if ([self.fromResource isEqualToString:fromResource]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self setPrimitiveFromResource:fromResource]; + [self setPrimitiveFromJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromResource))]; +} + +- (void)setFromUser:(NSString *)fromUser +{ + if ([self.fromUser isEqualToString:fromUser]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self setPrimitiveFromUser:fromUser]; + [self setPrimitiveFromJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(fromUser))]; +} + +#pragma mark - toJID transient property + +- (XMPPJID *)toJID +{ + [self willAccessValueForKey:NSStringFromSelector(@selector(toJID))]; + XMPPJID *toJID = [self primitiveToJID]; + [self didAccessValueForKey:NSStringFromSelector(@selector(toJID))]; + + if (toJID) { + return toJID; + } + + XMPPJID *newToJID = [XMPPJID jidWithUser:self.toUser domain:self.toDomain resource:self.toResource]; + [self setPrimitiveToJID:newToJID]; + + return newToJID; +} + +- (void)setToJID:(XMPPJID *)toJID +{ + if ([self.toJID isEqualToJID:toJID options:XMPPJIDCompareFull]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toUser))]; + [self setPrimitiveToJID:toJID]; + [self setPrimitiveToDomain:toJID.domain]; + [self setPrimitiveToResource:toJID.resource]; + [self setPrimitiveToUser:toJID.user]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toResource))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toUser))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; +} + +- (void)setToDomain:(NSString *)toDomain +{ + if ([self.toDomain isEqualToString:toDomain]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self setPrimitiveToDomain:toDomain]; + [self setPrimitiveToJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toDomain))]; +} + +- (void)setToResource:(NSString *)toResource +{ + if ([self.toResource isEqualToString:toResource]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toResource))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self setPrimitiveToResource:toResource]; + [self setPrimitiveToJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toResource))]; +} + +- (void)setToUser:(NSString *)toUser +{ + if ([self.toUser isEqualToString:toUser]) { + return; + } + + [self willChangeValueForKey:NSStringFromSelector(@selector(toUser))]; + [self willChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self setPrimitiveToUser:toUser]; + [self setPrimitiveToJID:nil]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toJID))]; + [self didChangeValueForKey:NSStringFromSelector(@selector(toUser))]; +} + +#pragma mark - Public + ++ (XMPPMessageCoreDataStorageObject *)findWithStreamEventID:(NSString *)streamEventID inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageContextStringItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + NSArray *predicates = @[[XMPPMessageContextStringItemCoreDataStorageObject stringPredicateWithValue:streamEventID], + [XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextStreamEventIDTag]]; + fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; + + NSArray *fetchResult = [managedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + NSAssert(fetchResult.count <= 1, @"Expected a single context item for any given stream event ID"); + + return fetchResult.firstObject.contextElement.message; +} + ++ (XMPPMessageCoreDataStorageObject *)findWithUniqueStanzaID:(NSString *)stanzaID inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"%K = %@", NSStringFromSelector(@selector(stanzaID)), stanzaID]; + + NSArray *fetchResult = [managedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + return fetchResult.count == 1 ? fetchResult.firstObject : nil; +} + +- (XMPPMessage *)coreMessage +{ + NSString *typeString; + switch (self.type) { + case XMPPMessageTypeChat: + typeString = @"chat"; + break; + + case XMPPMessageTypeError: + typeString = @"error"; + break; + + case XMPPMessageTypeGroupchat: + typeString = @"groupchat"; + break; + + case XMPPMessageTypeHeadline: + typeString = @"headline"; + break; + + case XMPPMessageTypeNormal: + typeString = @"normal"; + break; + } + + XMPPMessage *message = [[XMPPMessage alloc] initWithType:typeString to:self.toJID elementID:self.stanzaID]; + + if (self.body) { + [message addBody:self.body]; + } + if (self.subject) { + [message addSubject:self.subject]; + } + if (self.thread) { + [message addThread:self.thread]; + } + + return message; +} + +- (void)registerIncomingMessageCore:(XMPPMessage *)message +{ + NSAssert(self.direction == XMPPMessageDirectionIncoming, @"Only applicable to incoming message objects"); + + self.fromJID = [message from]; + self.toJID = [message to]; + self.body = [message body]; + self.stanzaID = [message elementID]; + self.subject = [message subject]; + self.thread = [message thread]; + + if ([[message type] isEqualToString:@"chat"]) { + self.type = XMPPMessageTypeChat; + } else if ([[message type] isEqualToString:@"error"]) { + self.type = XMPPMessageTypeError; + } else if ([[message type] isEqualToString:@"groupchat"]) { + self.type = XMPPMessageTypeGroupchat; + } else if ([[message type] isEqualToString:@"headline"]) { + self.type = XMPPMessageTypeHeadline; + } else { + self.type = XMPPMessageTypeNormal; + } +} + +- (void)registerIncomingMessageStreamEventID:(NSString *)streamEventID streamJID:(XMPPJID *)streamJID streamEventTimestamp:(NSDate *)streamEventTimestamp +{ + NSAssert(self.direction == XMPPMessageDirectionIncoming, @"Only applicable to incoming message objects"); + NSAssert(![self lookupCurrentStreamContext], @"Another stream context element already exists"); + + XMPPMessageContextCoreDataStorageObject *streamContext = [self appendContextElement]; + [streamContext appendStringItemWithTag:XMPPMessageContextStreamEventIDTag value:streamEventID]; + [streamContext appendJIDItemWithTag:XMPPMessageContextStreamJIDTag value:streamJID]; + [streamContext appendTimestampItemWithTag:XMPPMessageContextActiveStreamTimestampTag value:streamEventTimestamp]; +} + +- (void)registerOutgoingMessageStreamEventID:(NSString *)outgoingMessageStreamEventID +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"Only applicable to outgoing message objects"); + NSAssert(![self lookupPendingStreamContext], @"Pending stream context element already exists"); + + XMPPMessageContextCoreDataStorageObject *streamContext = [self appendContextElement]; + [streamContext appendStringItemWithTag:XMPPMessageContextStreamEventIDTag value:outgoingMessageStreamEventID]; + [streamContext appendMarkerItemWithTag:XMPPMessageContextPendingStreamContextAssignmentTag]; +} + +- (void)registerOutgoingMessageStreamJID:(XMPPJID *)streamJID streamEventTimestamp:(NSDate *)streamEventTimestamp +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"Only applicable to outgoing message objects"); + + XMPPMessageContextCoreDataStorageObject *streamContext = [self lookupPendingStreamContext]; + NSAssert(streamContext, @"No pending stream context element found"); + + XMPPMessageContextTimestampItemTag timestampTag; + if ([self lookupActiveStreamContext]) { + [self retireStreamTimestamp]; + timestampTag = XMPPMessageContextActiveStreamTimestampTag; + } else if (![self lookupLatestRetiredStreamContext]) { + timestampTag = XMPPMessageContextActiveStreamTimestampTag; + } else { + timestampTag = XMPPMessageContextRetiredStreamTimestampTag; + } + + [streamContext removeMarkerItemsWithTag:XMPPMessageContextPendingStreamContextAssignmentTag]; + [streamContext appendJIDItemWithTag:XMPPMessageContextStreamJIDTag value:streamJID]; + [streamContext appendTimestampItemWithTag:timestampTag value:streamEventTimestamp]; +} + +- (XMPPJID *)streamJID +{ + return [[self lookupCurrentStreamContext] jidItemValueForTag:XMPPMessageContextStreamJIDTag]; +} + +- (NSDate *)streamTimestamp +{ + XMPPMessageContextCoreDataStorageObject *latestStreamContext = [self lookupCurrentStreamContext]; + return [latestStreamContext timestampItemValueForTag:XMPPMessageContextActiveStreamTimestampTag] ?: [latestStreamContext timestampItemValueForTag:XMPPMessageContextRetiredStreamTimestampTag]; +} + +- (void)retireStreamTimestamp +{ + XMPPMessageContextCoreDataStorageObject *previousRetiredStreamContext = [self lookupLatestRetiredStreamContext]; + XMPPMessageContextCoreDataStorageObject *activeStreamContext = [self lookupActiveStreamContext]; + + if (activeStreamContext) { + [previousRetiredStreamContext removeMarkerItemsWithTag:XMPPMessageContextLatestStreamTimestampRetirementTag]; + + NSDate *retiredStreamTimestamp = [activeStreamContext timestampItemValueForTag:XMPPMessageContextActiveStreamTimestampTag]; + [activeStreamContext removeTimestampItemsWithTag:XMPPMessageContextActiveStreamTimestampTag]; + [activeStreamContext appendTimestampItemWithTag:XMPPMessageContextRetiredStreamTimestampTag value:retiredStreamTimestamp]; + [activeStreamContext appendMarkerItemWithTag:XMPPMessageContextLatestStreamTimestampRetirementTag]; + } else if (!previousRetiredStreamContext) { + XMPPMessageContextCoreDataStorageObject *initialPendingStreamContext = [self lookupPendingStreamContext]; + [initialPendingStreamContext appendMarkerItemWithTag:XMPPMessageContextLatestStreamTimestampRetirementTag]; + } else { + NSAssert(NO, @"No stream context element found for retiring"); + } +} + +#pragma mark - Overridden + +- (void)awakeFromSnapshotEvents:(NSSnapshotEventType)flags +{ + [super awakeFromSnapshotEvents:flags]; + + [self setPrimitiveFromJID:nil]; + [self setPrimitiveToJID:nil]; +} + +#pragma mark - Private + +- (XMPPMessageContextCoreDataStorageObject *)lookupPendingStreamContext +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextPendingStreamContextAssignmentTag] ? contextElement : nil; + }]; +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupCurrentStreamContext +{ + return [self lookupActiveStreamContext] ?: [self lookupLatestRetiredStreamContext]; +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupActiveStreamContext +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement timestampItemValueForTag:XMPPMessageContextActiveStreamTimestampTag] ? contextElement : nil; + }]; +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupLatestRetiredStreamContext +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextLatestStreamTimestampRetirementTag] ? contextElement : nil; + }]; +} + +@end + +@implementation XMPPMessageContextItemCoreDataStorageObject (XMPPMessageCoreDataStorageFetch) + ++ (NSFetchRequest *)requestByTimestampsWithPredicate:(NSPredicate *)predicate inAscendingOrder:(BOOL)isInAscendingOrder fromManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageContextTimestampItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + fetchRequest.predicate = predicate; + fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(value)) ascending:isInAscendingOrder]]; + return fetchRequest; +} + ++ (NSPredicate *)streamTimestampKindPredicate +{ + return [XMPPMessageContextTimestampItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextActiveStreamTimestampTag]; +} + ++ (NSPredicate *)timestampRangePredicateWithStartValue:(nullable NSDate *)startValue endValue:(nullable NSDate *)endValue +{ + return [XMPPMessageContextTimestampItemCoreDataStorageObject timestampRangePredicateWithStartValue:startValue endValue:endValue]; +} + ++ (NSPredicate *)messageFromJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + return [self xmpp_jidPredicateWithDomainKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(fromDomain))] + resourceKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(fromResource))] + userKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(fromUser))] + value:value + compareOptions:compareOptions]; +} + ++ (NSPredicate *)messageToJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + return [self xmpp_jidPredicateWithDomainKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(toDomain))] + resourceKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(toResource))] + userKeyPath:[[self messageKeyPath] stringByAppendingFormat:@".%@", NSStringFromSelector(@selector(toUser))] + value:value + compareOptions:compareOptions]; +} + ++ (NSPredicate *)messageRemotePartyJIDPredicateWithValue:(XMPPJID *)value compareOptions:(XMPPJIDCompareOptions)compareOptions +{ + NSArray *outgoingMessagePredicates = @[[self messageToJIDPredicateWithValue:value compareOptions:compareOptions], + [XMPPMessageContextItemCoreDataStorageObject messageDirectionPredicateWithValue:XMPPMessageDirectionOutgoing]]; + NSArray *incomingMessagePredicates = @[[self messageFromJIDPredicateWithValue:value compareOptions:compareOptions], + [XMPPMessageContextItemCoreDataStorageObject messageDirectionPredicateWithValue:XMPPMessageDirectionIncoming]]; + + return [NSCompoundPredicate orPredicateWithSubpredicates:@[[NSCompoundPredicate andPredicateWithSubpredicates:outgoingMessagePredicates], + [NSCompoundPredicate andPredicateWithSubpredicates:incomingMessagePredicates]]]; +} + ++ (NSPredicate *)messageBodyPredicateWithValue:(NSString *)value compareOperator:(XMPPMessageContentCompareOperator)compareOperator options:(XMPPMessageContentCompareOptions)options +{ + return [self messageContentPredicateWithKey:NSStringFromSelector(@selector(body)) value:value compareOperator:compareOperator options:options]; +} + ++ (NSPredicate *)messageSubjectPredicateWithValue:(NSString *)value compareOperator:(XMPPMessageContentCompareOperator)compareOperator options:(XMPPMessageContentCompareOptions)options +{ + return [self messageContentPredicateWithKey:NSStringFromSelector(@selector(subject)) value:value compareOperator:compareOperator options:options]; +} + ++ (NSPredicate *)messageThreadPredicateWithValue:(NSString *)value +{ + return [NSPredicate predicateWithFormat:@"%K.%K = %@", [self messageKeyPath], NSStringFromSelector(@selector(thread)), value]; +} + ++ (NSPredicate *)messageDirectionPredicateWithValue:(XMPPMessageDirection)value +{ + return [NSPredicate predicateWithFormat:@"%K.%K = %d", [self messageKeyPath], NSStringFromSelector(@selector(direction)), value]; +} + ++ (NSPredicate *)messageTypePredicateWithValue:(XMPPMessageType)value +{ + return [NSPredicate predicateWithFormat:@"%K.%K = %d", [self messageKeyPath], NSStringFromSelector(@selector(type)), value]; +} + ++ (NSPredicate *)messageContentPredicateWithKey:(NSString *)contentKey value:(NSString *)value compareOperator:(XMPPMessageContentCompareOperator)compareOperator options:(XMPPMessageContentCompareOptions)options +{ + NSMutableString *predicateFormat = [[NSMutableString alloc] initWithFormat:@"%@.%@ ", [self messageKeyPath], contentKey]; + + switch (compareOperator) { + case XMPPMessageContentCompareOperatorEquals: + [predicateFormat appendString:@"= "]; + break; + case XMPPMessageContentCompareOperatorBeginsWith: + [predicateFormat appendString:@"BEGINSWITH "]; + break; + case XMPPMessageContentCompareOperatorContains: + [predicateFormat appendString:@"CONTAINS "]; + break; + case XMPPMessageContentCompareOperatorEndsWith: + [predicateFormat appendString:@"ENDSWITH "]; + break; + case XMPPMessageContentCompareOperatorLike: + [predicateFormat appendString:@"LIKE "]; + break; + case XMPPMessageContentCompareOperatorMatches: + [predicateFormat appendString:@"MATCHES "]; + break; + } + + NSMutableString *optionString = [[NSMutableString alloc] init]; + if (options & XMPPMessageContentCompareCaseInsensitive) { + [optionString appendString:@"c"]; + } + if (options & XMPPMessageContentCompareDiacriticInsensitive) { + [optionString appendString:@"d"]; + } + if (optionString.length > 0) { + [predicateFormat appendFormat:@"[%@] ", optionString]; + } + + [predicateFormat appendString:@"%@"]; + + return [NSPredicate predicateWithFormat:predicateFormat, value]; +} + ++ (NSString *)messageKeyPath +{ + return [NSString stringWithFormat:@"%@.%@", NSStringFromSelector(@selector(contextElement)), NSStringFromSelector(@selector(message))]; +} + +- (XMPPMessageCoreDataStorageObject *)message +{ + return self.contextElement.message; +} + +@end diff --git a/Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.h b/Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.h new file mode 100644 index 0000000000..ebabe52e6e --- /dev/null +++ b/Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.h @@ -0,0 +1,17 @@ +#import "XMPPMessageCoreDataStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessage; + +@interface XMPPMessageCoreDataStorageTransaction (XMPPOneToOneChat) + +/// Stores core XMPP properties for the received chat message. +- (void)storeReceivedChatMessage:(XMPPMessage *)message; + +/// Registers outgoing stream event information for the chat message processed in the transaction. +- (void)registerSentChatMessage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.m b/Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.m new file mode 100644 index 0000000000..042c2a5b64 --- /dev/null +++ b/Extensions/OneToOneChat/XMPPMessageCoreDataStorage+XMPPOneToOneChat.m @@ -0,0 +1,22 @@ +#import "XMPPMessageCoreDataStorage+XMPPOneToOneChat.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" + +@implementation XMPPMessageCoreDataStorageTransaction (XMPPOneToOneChat) + +- (void)storeReceivedChatMessage:(XMPPMessage *)message +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + [messageObject registerIncomingMessageCore:message]; + }]; +} + +- (void)registerSentChatMessage +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionOutgoing, @"This action is only allowed for outgoing message objects"); + // No additional processing required + }]; +} + +@end diff --git a/Extensions/OneToOneChat/XMPPOneToOneChat.h b/Extensions/OneToOneChat/XMPPOneToOneChat.h new file mode 100644 index 0000000000..a635aefc4a --- /dev/null +++ b/Extensions/OneToOneChat/XMPPOneToOneChat.h @@ -0,0 +1,27 @@ +#import "XMPPModule.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessage; + +/// @brief A module that handles one-to-one chat messaging. +/// @discussion This module triggers delegate callbacks for all sent or received messages of type 'chat'. +@interface XMPPOneToOneChat : XMPPModule + +@end + +/// A protocol defining @c XMPPOneToOneChat module delegate API. +@protocol XMPPOneToOneChatDelegate + +@optional +/// Notifies the delegate that a chat message has been received in the stream. +- (void)xmppOneToOneChat:(XMPPOneToOneChat *)xmppOneToOneChat didReceiveChatMessage:(XMPPMessage *)message +NS_SWIFT_NAME(xmppOneToOneChat(_:didReceiveChatMessage:)); + +/// Notifies the delegate that a chat message has been sent in the stream. +- (void)xmppOneToOneChat:(XMPPOneToOneChat *)xmppOneToOneChat didSendChatMessage:(XMPPMessage *)message +NS_SWIFT_NAME(xmppOneToOneChat(_:didSendChatMessage:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/OneToOneChat/XMPPOneToOneChat.m b/Extensions/OneToOneChat/XMPPOneToOneChat.m new file mode 100644 index 0000000000..e884bc45e6 --- /dev/null +++ b/Extensions/OneToOneChat/XMPPOneToOneChat.m @@ -0,0 +1,40 @@ +#import "XMPPOneToOneChat.h" +#import "XMPPMessage.h" +#import "XMPPStream.h" +#import "XMPPLogging.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@implementation XMPPOneToOneChat + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (![message isChatMessage]) { + return; + } + + XMPPLogInfo(@"Received chat message from %@", [message from]); + [multicastDelegate xmppOneToOneChat:self didReceiveChatMessage:message]; +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (![message isChatMessage]) { + return; + } + + XMPPLogInfo(@"Sent chat message to %@", [message to]); + [multicastDelegate xmppOneToOneChat:self didSendChatMessage:message]; +} + +@end diff --git a/Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.h b/Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.h new file mode 100644 index 0000000000..2bb9574778 --- /dev/null +++ b/Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.h @@ -0,0 +1,49 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XMPPMessageCoreDataStorageTransaction (XEP_0066) + +/// @brief Registers XEP-0066 out of band resource information for the received message. +/// @discussion It is assumed that the provided @c XMPPMessage contains the relevant information. This method does not store core XMPP message properties. +- (void)registerOutOfBandResourceForReceivedMessage:(XMPPMessage *)message; + +@end + +@interface XMPPMessageCoreDataStorageObject (XEP_0066) + +/// @brief Returns the internal ID of the XEP-0066 resource associated with the message. +/// @discussion The internal resource ID can be used as a reference to some auxiliary local storage (e.g. transferred files repository) +- (nullable NSString *)outOfBandResourceInternalID; + +/// Returns the URI string identifying the XEP-0066 resource associated with the message. +- (nullable NSString *)outOfBandResourceURIString; + +/// Returns the human-readable description of the XEP-0066 resource associated with the message. +- (nullable NSString *)outOfBandResourceDescription; + +/** + Associates the message storage object with a XEP-0066 resource with the given internal ID and an optional description. + + Each message storage object can only have a resource assigned once, subsequent attempts will trigger an assertion. + + @see setAssignedOutOfBandResourceURIString: + */ +- (void)assignOutOfBandResourceWithInternalID:(NSString *)internalID description:(nullable NSString *)resourceDescription; + +/** + Provides a resource URI to the message storage object with an associated XEP-0066 resource. + + An assertion will be triggered if no resource is associated with the message yet. + + The reason why assigning the resource and setting the URI use separate methods is that preparing an outgoing XEP-0066 message + is often an asynchronous 2-step process, i.e. the URI may not be immediately available. + + @see assignOutOfBandResourceWithInternalID:description: + */ +- (void)setAssignedOutOfBandResourceURIString:(NSString *)resourceURIString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.m b/Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.m new file mode 100644 index 0000000000..2cf46657c9 --- /dev/null +++ b/Extensions/XEP-0066/XMPPMessageCoreDataStorage+XEP_0066.m @@ -0,0 +1,80 @@ +#import "XMPPMessageCoreDataStorage+XEP_0066.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessage+XEP_0066.h" + +static XMPPMessageContextStringItemTag const XMPPMessageContextOutOfBandResourceIDTag = @"XMPPMessageContextOutOfBandResourceID"; +static XMPPMessageContextStringItemTag const XMPPMessageContextOutOfBandResourceURIStringTag = @"XMPPMessageContextOutOfBandResourceURIString"; +static XMPPMessageContextStringItemTag const XMPPMessageContextOutOfBandResourceDescriptionTag = @"XMPPMessageContextOutOfBandResourceDescription"; + +@interface XMPPMessageCoreDataStorageObject (XEP_0066_Private) + +- (void)appendOutOfBandResourceContextWithInternalID:(NSString *)internalID description:(NSString *)resourceDescription; + +@end + +@implementation XMPPMessageCoreDataStorageTransaction (XEP_0066) + +- (void)registerOutOfBandResourceForReceivedMessage:(XMPPMessage *)message +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + [messageObject appendOutOfBandResourceContextWithInternalID:[NSUUID UUID].UUIDString description:[message outOfBandDesc]]; + [messageObject setAssignedOutOfBandResourceURIString:[message outOfBandURI]]; + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XEP_0066) + +- (nullable NSString *)outOfBandResourceInternalID +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextOutOfBandResourceIDTag]; + }]; +} + +- (nullable NSString *)outOfBandResourceURIString +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextOutOfBandResourceURIStringTag]; + }]; +} + +- (nullable NSString *)outOfBandResourceDescription +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextOutOfBandResourceDescriptionTag]; + }]; +} + +- (void)assignOutOfBandResourceWithInternalID:(NSString *)internalID description:(NSString *)resourceDescription +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"This action is only allowed for outgoing message objects"); + [self appendOutOfBandResourceContextWithInternalID:internalID description:resourceDescription]; +} + +- (void)setAssignedOutOfBandResourceURIString:(NSString *)resourceURIString +{ + XMPPMessageContextCoreDataStorageObject *outOfBandResourceContext = + [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextOutOfBandResourceIDTag] ? contextElement : nil; + }]; + NSAssert(outOfBandResourceContext, @"No out of band resource is assigned yet"); + NSAssert(![outOfBandResourceContext stringItemValueForTag:XMPPMessageContextOutOfBandResourceURIStringTag], @"Out of band resource URI is already set"); + + [outOfBandResourceContext appendStringItemWithTag:XMPPMessageContextOutOfBandResourceURIStringTag value:resourceURIString]; +} + +- (void)appendOutOfBandResourceContextWithInternalID:(NSString *)internalID description:(NSString *)resourceDescription +{ + NSAssert(![self outOfBandResourceInternalID], @"Out of band resource is already assigned"); + + XMPPMessageContextCoreDataStorageObject *outOfBandResourceContext = [self appendContextElement]; + [outOfBandResourceContext appendStringItemWithTag:XMPPMessageContextOutOfBandResourceIDTag value:internalID]; + if (resourceDescription) { + [outOfBandResourceContext appendStringItemWithTag:XMPPMessageContextOutOfBandResourceDescriptionTag value:resourceDescription]; + } +} + +@end diff --git a/Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.h b/Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.h new file mode 100644 index 0000000000..11a58cba3a --- /dev/null +++ b/Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.h @@ -0,0 +1,28 @@ +#import "XMPPModule.h" + +@class XMPPMessage; + +NS_ASSUME_NONNULL_BEGIN + +/// A module that handles incoming XEP-0066 Out of Band Data URI messages. +@interface XMPPOutOfBandResourceMessaging : XMPPModule + +/// @brief The set of URL schemes handled by the module. +/// @discussion If set to @c nil (the default), URL filtering is disabled. +@property (copy, nullable) NSSet *relevantURLSchemes; + +@end + +/// A protocol defining @c XMPPOutOfBandResourceMessagingDelegate module delegate API. +@protocol XMPPOutOfBandResourceMessagingDelegate + +@optional + +/// Notifies the delegate that a message containing a relevant XEP-0066 Out of Band Data URI has been received in the stream. +- (void)xmppOutOfBandResourceMessaging:(XMPPOutOfBandResourceMessaging *)xmppOutOfBandResourceMessaging + didReceiveOutOfBandResourceMessage:(XMPPMessage *)message +NS_SWIFT_NAME(xmppOutOfBandResourceMessaging(_:didReceiveOutOfBandResourceMessage:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.m b/Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.m new file mode 100644 index 0000000000..91dcc30773 --- /dev/null +++ b/Extensions/XEP-0066/XMPPOutOfBandResourceMessaging.m @@ -0,0 +1,76 @@ +#import "XMPPOutOfBandResourceMessaging.h" +#import "XMPPMessage+XEP_0066.h" +#import "XMPPStream.h" +#import "XMPPLogging.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@implementation XMPPOutOfBandResourceMessaging + +@synthesize relevantURLSchemes = _relevantURLSchemes; + +- (NSSet *)relevantURLSchemes +{ + __block NSSet *result; + dispatch_block_t block = ^{ + result = _relevantURLSchemes; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setRelevantURLSchemes:(NSSet *)relevantURLSchemes +{ + NSSet *newValue = [relevantURLSchemes copy]; + dispatch_block_t block = ^{ + _relevantURLSchemes = newValue; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)didActivate +{ + XMPPLogTrace(); +} + +- (void)willDeactivate +{ + XMPPLogTrace(); +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (![message hasOutOfBandData]) { + return; + } + + NSString *resourceURIString = [message outOfBandURI]; + if (self.relevantURLSchemes) { + NSURL *resourceURL = [NSURL URLWithString:resourceURIString]; + if (!resourceURL.scheme || ![self.relevantURLSchemes containsObject:resourceURL.scheme]) { + return; + } + } + + XMPPLogInfo(@"Received out of band resource message"); + [multicastDelegate xmppOutOfBandResourceMessaging:self didReceiveOutOfBandResourceMessage:message]; +} + +@end diff --git a/Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.h b/Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.h new file mode 100644 index 0000000000..9def2b695d --- /dev/null +++ b/Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.h @@ -0,0 +1,29 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XMPPMessageCoreDataStorageTransaction (XEP_0184) + +/// @brief Stores core XMPP properties of a received XEP-0184 delivery receipt response message and associates it with the delivered message. +/// @discussion Although the XEP does not call for a mandatory delivered content message ID in the response message, this method assumes it is present. +- (void)storeReceivedDeliveryReceiptResponseMessage:(XMPPMessage *)message; + +@end + +@interface XMPPMessageCoreDataStorageObject (XEP_0184) + +/// Returns the message object that contains a XEP-0184 delivery receipt response for the provided delivered message ID. ++ (nullable XMPPMessageCoreDataStorageObject *)findDeliveryReceiptResponseForMessageWithID:(NSString *)messageID + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +NS_SWIFT_NAME(findDeliveryReceiptResponse(forMessageWithID:in:)); + +/// Returns @c YES if the storage contains a XEP-0184 delivery receipt response message for the given message object. +- (BOOL)hasAssociatedDeliveryReceiptResponseMessage; + +/// Returns the delivered message ID if the given object contains a XEP-0184 delivery receipt response message. +- (nullable NSString *)messageDeliveryReceiptResponseID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.m b/Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.m new file mode 100644 index 0000000000..bb27cb855b --- /dev/null +++ b/Extensions/XEP-0184/XMPPMessageCoreDataStorage+XEP_0184.m @@ -0,0 +1,65 @@ +#import "XMPPMessageCoreDataStorage+XEP_0184.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" +#import "XMPPMessage+XEP_0184.h" + +static XMPPMessageContextMarkerItemTag const XMPPMessageContextAssociatedDeliveryReceiptResponseTag = @"XMPPMessageContextAssociatedDeliveryReceiptResponse"; +static XMPPMessageContextStringItemTag const XMPPMessageContextDeliveryReceiptResponseIDTag = @"XMPPMessageContextDeliveryReceiptResponseID"; + +@implementation XMPPMessageCoreDataStorageTransaction (XEP_0184) + +- (void)storeReceivedDeliveryReceiptResponseMessage:(XMPPMessage *)message +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + + [messageObject registerIncomingMessageCore:message]; + + NSString *deliveredMessageID = [message receiptResponseID]; + + XMPPMessageContextCoreDataStorageObject *deliveryReceiptContext = [messageObject appendContextElement]; + [deliveryReceiptContext appendStringItemWithTag:XMPPMessageContextDeliveryReceiptResponseIDTag value:deliveredMessageID]; + + XMPPMessageCoreDataStorageObject *sentMessageObject = [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:deliveredMessageID + inManagedObjectContext:messageObject.managedObjectContext]; + if (!sentMessageObject) { + return; + } + + XMPPMessageContextCoreDataStorageObject *deliveryConfirmationContext = [sentMessageObject appendContextElement]; + [deliveryConfirmationContext appendMarkerItemWithTag:XMPPMessageContextAssociatedDeliveryReceiptResponseTag]; + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XEP_0184) + ++ (XMPPMessageCoreDataStorageObject *)findDeliveryReceiptResponseForMessageWithID:(NSString *)messageID inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageContextStringItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + NSArray *predicates = @[[XMPPMessageContextStringItemCoreDataStorageObject stringPredicateWithValue:messageID], + [XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextDeliveryReceiptResponseIDTag]]; + fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; + + NSArray *result = [managedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + NSAssert(result.count <= 1, @"Multiple delivery receipt context items for the given response ID"); + return result.firstObject.message; +} + +- (BOOL)hasAssociatedDeliveryReceiptResponseMessage +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextAssociatedDeliveryReceiptResponseTag] ? contextElement : nil; + }] != nil; +} + +- (NSString *)messageDeliveryReceiptResponseID +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextDeliveryReceiptResponseIDTag]; + }]; +} + +@end diff --git a/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h index 9d053d40e2..2dd149fa1e 100644 --- a/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h +++ b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h @@ -2,6 +2,8 @@ #define _XMPP_MESSAGE_DELIVERY_RECEIPTS_H +@class XMPPMessage; + /** * XMPPMessageDeliveryReceipts can be configured to automatically send delivery receipts and requests in accordance to XEP-0184 **/ @@ -30,5 +32,19 @@ NS_ASSUME_NONNULL_BEGIN @property (assign) BOOL autoSendMessageDeliveryReceipts; +@end + +/** + * A protocol defining @c XMPPManagedMessaging module delegate API. +**/ +@protocol XMPPMessageDeliveryReceiptsDelegate + +@optional + +/** + * Notifies the delegate of a receipt response message received in the stream. +**/ +- (void)xmppMessageDeliveryReceipts:(XMPPMessageDeliveryReceipts *)xmppMessageDeliveryReceipts didReceiveReceiptResponseMessage:(XMPPMessage *)message; + @end NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m index 564bdd9773..6731d9eeb0 100644 --- a/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m +++ b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m @@ -129,6 +129,11 @@ - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message [sender sendElement:generatedReceiptResponse]; } } + + if ([message hasReceiptResponse]) + { + [multicastDelegate xmppMessageDeliveryReceipts:self didReceiveReceiptResponseMessage:message]; + } } - (XMPPMessage *)xmppStream:(XMPPStream *)sender willSendMessage:(XMPPMessage *)message diff --git a/Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.h b/Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.h new file mode 100644 index 0000000000..ff64d648e9 --- /dev/null +++ b/Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.h @@ -0,0 +1,34 @@ +#import "XMPPModule.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPMessage; + +/** + A module working in tandem with @c XMPPStreamManagement to trace outgoing message stream acknowledgements. + + This module only monitors messages with @c elementID assigned. The rationale behind this is that any potential retransmissions + of messages without IDs will cause deduplication issues on the receiving end. + */ +@interface XMPPManagedMessaging : XMPPModule + +@end + +/// A protocol defining @c XMPPManagedMessaging module delegate API. +@protocol XMPPManagedMessagingDelegate + +@optional + +/// Notifies the delegate that a message subject to monitoring has been sent in the stream. +- (void)xmppManagedMessaging:(XMPPManagedMessaging *)sender didBeginMonitoringOutgoingMessage:(XMPPMessage *)message; + +/// Notifies the delegate that @c XMPPStreamManagement module has received server acknowledgement for sent messages with given IDs. +- (void)xmppManagedMessaging:(XMPPManagedMessaging *)sender didConfirmSentMessagesWithIDs:(NSArray *)messageIDs; + +/// @brief Notifies the delegate that post-reauthentication message acknowledgement processing is finished. +/// At this point, no more acknowledgements for currently monitored messages are to be expected. +- (void)xmppManagedMessagingDidFinishProcessingPreviousStreamConfirmations:(XMPPManagedMessaging *)sender; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.m b/Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.m new file mode 100644 index 0000000000..c9437662dc --- /dev/null +++ b/Extensions/XEP-0198/Managed Messaging/XMPPManagedMessaging.m @@ -0,0 +1,112 @@ +#import "XMPPManagedMessaging.h" +#import "XMPPStreamManagement.h" +#import "XMPPLogging.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +static NSString * const XMPPManagedMessagingURLScheme = @"xmppmanagedmessage"; + +@implementation XMPPManagedMessaging + +- (void)didActivate +{ + XMPPLogTrace(); + [self.xmppStream autoAddDelegate:self delegateQueue:self.moduleQueue toModulesOfClass:[XMPPStreamManagement class]]; +} + +- (void)willDeactivate +{ + XMPPLogTrace(); + [self.xmppStream removeAutoDelegate:self delegateQueue:self.moduleQueue fromModulesOfClass:[XMPPStreamManagement class]]; +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (![message elementID]) { + XMPPLogWarn(@"Sent message without an ID excluded from managed messaging"); + return; + } + + XMPPLogInfo(@"Registering message with ID=%@ for managed messaging", [message elementID]); + [multicastDelegate xmppManagedMessaging:self didBeginMonitoringOutgoingMessage:message]; +} + +- (id)xmppStreamManagement:(XMPPStreamManagement *)sender stanzaIdForSentElement:(XMPPElement *)element +{ + if (![element isKindOfClass:[XMPPMessage class]] || ![element elementID]) { + return nil; + } + + NSURLComponents *managedMessageURLComponents = [[NSURLComponents alloc] init]; + managedMessageURLComponents.scheme = XMPPManagedMessagingURLScheme; + managedMessageURLComponents.path = [element elementID]; + + return managedMessageURLComponents.URL; +} + +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender didReceiveAckForStanzaIds:(NSArray *)stanzaIds +{ + XMPPLogTrace(); + + NSArray *resumeStanzaIDs; + [sender didResumeWithAckedStanzaIds:&resumeStanzaIDs serverResponse:nil]; + if ([resumeStanzaIDs isEqualToArray:stanzaIds]) { + // Handled in -xmppStreamDidAuthenticate: + return; + } + + [self processStreamManagementAcknowledgementForStanzaIDs:stanzaIds]; +} + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + XMPPLogTrace(); + + dispatch_group_t stanzaAcknowledgementGroup = dispatch_group_create(); + + [sender enumerateModulesOfClass:[XMPPStreamManagement class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + NSArray *acknowledgedStanzaIDs; + [(XMPPStreamManagement *)module didResumeWithAckedStanzaIds:&acknowledgedStanzaIDs serverResponse:nil]; + if (acknowledgedStanzaIDs.count == 0) { + return; + } + + dispatch_group_async(stanzaAcknowledgementGroup, self.moduleQueue, ^{ + [self processStreamManagementAcknowledgementForStanzaIDs:acknowledgedStanzaIDs]; + }); + }]; + + dispatch_group_notify(stanzaAcknowledgementGroup, self.moduleQueue, ^{ + [multicastDelegate xmppManagedMessagingDidFinishProcessingPreviousStreamConfirmations:self]; + }); +} + +- (void)processStreamManagementAcknowledgementForStanzaIDs:(NSArray *)stanzaIDs +{ + NSMutableArray *managedMessageIDs = [NSMutableArray array]; + for (id stanzaID in stanzaIDs) { + if (![stanzaID isKindOfClass:[NSURL class]] || ![((NSURL *)stanzaID).scheme isEqualToString:XMPPManagedMessagingURLScheme]) { + continue; + } + // Extracting path directly from NSURL does not work if it doesn't start with "/" + NSURLComponents *managedMessageURLComponents = [[NSURLComponents alloc] initWithURL:stanzaID resolvingAgainstBaseURL:NO]; + [managedMessageIDs addObject:managedMessageURLComponents.path]; + } + + if (managedMessageIDs.count == 0) { + return; + } + + XMPPLogInfo(@"Confirming managed messages with IDs={%@}", [managedMessageIDs componentsJoinedByString:@","]); + [multicastDelegate xmppManagedMessaging:self didConfirmSentMessagesWithIDs:managedMessageIDs]; +} + +@end diff --git a/Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.h b/Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.h new file mode 100644 index 0000000000..3ff353d12e --- /dev/null +++ b/Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.h @@ -0,0 +1,51 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, XMPPManagedMessagingStatus) { + /// The status of an untracked message. + XMPPManagedMessagingStatusUnspecified, + /// The status of a tracked outgoing message for which an acknowledgement has not been received yet. + XMPPManagedMessagingStatusPendingAcknowledgement, + /// The status of an outgoing message for which an acknowledgement has been received. + XMPPManagedMessagingStatusAcknowledged, + /// The status of a tracked outgoing message for which an acknowledgement has never been received. + XMPPManagedMessagingStatusUnacknowledged +}; + +@interface XMPPMessageCoreDataStorage (XEP_0198) + +/** + Marks sent message objects with given element IDs as acknowledged by the stream's remote end. + + This method is intended to be invoked in response to @c XMPPManagedMessagingDelegate + @c xmppManagedMessaging:didConfirmSentMessagesWithIDs: delegate callback. + */ +- (void)registerAcknowledgedManagedMessageIDs:(NSArray *)messageIDs; + +/** + Marks sent message objects that are still pending stream acknowledgement as never acknowledged. + + This method is intended to be invoked in response to @c XMPPManagedMessagingDelegate + @c xmppManagedMessagingDidFinishProcessingPreviousStreamConfirmations: delegate callback. + */ +- (void)registerFailureForUnacknowledgedManagedMessages; + +@end + +@interface XMPPMessageCoreDataStorageTransaction (XEP_0198) + +/// Marks the outgoing message associated with the given transaction as pending stream acknowledgement. +- (void)registerSentManagedMessage; + +@end + +@interface XMPPMessageCoreDataStorageObject (XEP_0198) + +/// Returns the message object's stream acknowledgement status. +- (XMPPManagedMessagingStatus)managedMessagingStatus; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.m b/Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.m new file mode 100644 index 0000000000..a9af8619e3 --- /dev/null +++ b/Extensions/XEP-0198/Managed Messaging/XMPPMessageCoreDataStorage+XEP_0198.m @@ -0,0 +1,121 @@ +#import "XMPPMessageCoreDataStorage+XEP_0198.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" + +static XMPPMessageContextTimestampItemTag const XMPPMessageContextManagedMessagingAttemptTimestampTag = @"XMPPMessageContextManagedMessagingAttemptTimestamp"; +static XMPPMessageContextMarkerItemTag const XMPPMessageContextManagedMessagingPendingStatusTag = @"XMPPMessageContextManagedMessagingPendingStatus"; +static XMPPMessageContextMarkerItemTag const XMPPMessageContextManagedMessagingAcknowledgedStatusTag = @"XMPPMessageContextManagedMessagingAcknowledgedStatus"; + +@interface XMPPMessageCoreDataStorageObject (XEP_0198_Private) + +- (XMPPMessageContextCoreDataStorageObject *)lookupManagedMessagingContextWithBlock:(BOOL (^)(XMPPMessageContextCoreDataStorageObject *contextElement))block; +- (id)lookupInManagedMessagingContextWithBlock:(id (^)(XMPPMessageContextCoreDataStorageObject *contextElement))block; + +@end + +@implementation XMPPMessageCoreDataStorage (XEP_0198) + +- (void)registerAcknowledgedManagedMessageIDs:(NSArray *)messageIDs +{ + [self scheduleBlock:^{ + // TODO: a single fetch + for (NSString *messageID in messageIDs) { + XMPPMessageCoreDataStorageObject *message = [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:messageID + inManagedObjectContext:self.managedObjectContext]; + XMPPMessageContextCoreDataStorageObject *managedMessagingContext = + [message lookupManagedMessagingContextWithBlock:^BOOL(XMPPMessageContextCoreDataStorageObject *contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextManagedMessagingPendingStatusTag]; + }]; + NSAssert(managedMessagingContext, @"No managed messaging context awaiting confirmation found"); + + [managedMessagingContext removeMarkerItemsWithTag:XMPPMessageContextManagedMessagingPendingStatusTag]; + [managedMessagingContext appendMarkerItemWithTag:XMPPMessageContextManagedMessagingAcknowledgedStatusTag]; + } + }]; +} + +- (void)registerFailureForUnacknowledgedManagedMessages +{ + [self scheduleBlock:^{ + NSFetchRequest *fetchRequest = [XMPPMessageContextMarkerItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.managedObjectContext]; + fetchRequest.predicate = [XMPPMessageContextMarkerItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextManagedMessagingPendingStatusTag]; + + for (XMPPMessageContextMarkerItemCoreDataStorageObject *markerItem in [self.managedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]) { + XMPPMessageContextCoreDataStorageObject *managedMessagingContext = markerItem.contextElement; + [managedMessagingContext removeMarkerItemsWithTag:XMPPMessageContextManagedMessagingPendingStatusTag]; + } + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTransaction (XEP_0198) + +- (void)registerSentManagedMessage +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionOutgoing, @"No outgoing message found"); + NSAssert(![messageObject lookupManagedMessagingContextWithBlock:^BOOL(XMPPMessageContextCoreDataStorageObject *contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextManagedMessagingAcknowledgedStatusTag]; + }], @"Managed message already acknowledged"); + + XMPPMessageContextCoreDataStorageObject *managedMessagingContext = + [messageObject lookupManagedMessagingContextWithBlock:^BOOL(XMPPMessageContextCoreDataStorageObject *contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextManagedMessagingPendingStatusTag]; + }]; + if (!managedMessagingContext) { + managedMessagingContext = [messageObject appendContextElement]; + [managedMessagingContext appendMarkerItemWithTag:XMPPMessageContextManagedMessagingPendingStatusTag]; + } + + [managedMessagingContext appendTimestampItemWithTag:XMPPMessageContextManagedMessagingAttemptTimestampTag value:[messageObject streamTimestamp]]; + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XEP_0198) + +- (XMPPManagedMessagingStatus)managedMessagingStatus +{ + __block BOOL hasManagedMessagingContext = NO; + NSNumber *managedMessagingStatusNumber = [self lookupInManagedMessagingContextWithBlock:^id(XMPPMessageContextCoreDataStorageObject *contextElement) { + hasManagedMessagingContext = YES; + + if ([contextElement hasMarkerItemForTag:XMPPMessageContextManagedMessagingAcknowledgedStatusTag]) { + return @(XMPPManagedMessagingStatusAcknowledged); + } + + if ([contextElement hasMarkerItemForTag:XMPPMessageContextManagedMessagingPendingStatusTag]) { + return @(XMPPManagedMessagingStatusPendingAcknowledgement); + } + + return nil; + }]; + + if (managedMessagingStatusNumber) { + return managedMessagingStatusNumber.integerValue; + } else if (hasManagedMessagingContext) { + return XMPPManagedMessagingStatusUnacknowledged; + } else { + return XMPPManagedMessagingStatusUnspecified; + } +} + +- (XMPPMessageContextCoreDataStorageObject *)lookupManagedMessagingContextWithBlock:(BOOL (^)(XMPPMessageContextCoreDataStorageObject *))block +{ + return [self lookupInManagedMessagingContextWithBlock:^id(XMPPMessageContextCoreDataStorageObject *contextElement) { + return block(contextElement) ? contextElement : nil; + }]; +} + +- (id)lookupInManagedMessagingContextWithBlock:(id (^)(XMPPMessageContextCoreDataStorageObject *))block +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement timestampItemValuesForTag:XMPPMessageContextManagedMessagingAttemptTimestampTag].count > 0 ? block(contextElement) : nil; + }]; +} + +@end diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.h b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h index 662fda8303..528997c318 100644 --- a/Extensions/XEP-0203/NSXMLElement+XEP_0203.h +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h @@ -1,10 +1,13 @@ #import @import KissXML; +@class XMPPJID; @interface NSXMLElement (XEP_0203) @property (nonatomic, readonly) BOOL wasDelayed; @property (nonatomic, readonly, nullable) NSDate *delayedDeliveryDate; +- (XMPPJID *)delayedDeliveryFrom; +- (NSString *)delayedDeliveryReasonDescription; @end diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.m b/Extensions/XEP-0203/NSXMLElement+XEP_0203.m index ee404d3326..82b0b371e6 100644 --- a/Extensions/XEP-0203/NSXMLElement+XEP_0203.m +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.m @@ -1,6 +1,7 @@ #import "NSXMLElement+XEP_0203.h" #import "XMPPDateTimeProfiles.h" #import "NSXMLElement+XMPP.h" +#import "XMPPJID.h" #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). @@ -10,27 +11,11 @@ @implementation NSXMLElement (XEP_0203) - (BOOL)wasDelayed { - NSXMLElement *delay; - - delay = [self elementForName:@"delay" xmlns:@"urn:xmpp:delay"]; - if (delay) - { - return YES; - } - - delay = [self elementForName:@"x" xmlns:@"jabber:x:delay"]; - if (delay) - { - return YES; - } - - return NO; + return [self anyDelayedDeliveryChildElement] != nil; } - (NSDate *)delayedDeliveryDate { - NSXMLElement *delay; - // From XEP-0203 (Delayed Delivery) // // - delay = [self elementForName:@"x" xmlns:@"jabber:x:delay"]; - if (delay) + NSXMLElement *legacyDelay = [self legacyDelayedDeliveryChildElement]; + if (legacyDelay) { NSDate *stamp; - NSString *stampValue = [delay attributeStringValueForName:@"stamp"]; + NSString *stampValue = [legacyDelay attributeStringValueForName:@"stamp"]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; @@ -81,4 +66,30 @@ - (NSDate *)delayedDeliveryDate return nil; } +- (XMPPJID *)delayedDeliveryFrom +{ + NSString *delayedDeliveryFromString = [[self anyDelayedDeliveryChildElement] attributeStringValueForName:@"from"]; + return delayedDeliveryFromString ? [XMPPJID jidWithString:delayedDeliveryFromString] : nil; +} + +- (NSString *)delayedDeliveryReasonDescription +{ + return [self anyDelayedDeliveryChildElement].stringValue; +} + +- (NSXMLElement *)delayedDeliveryChildElement +{ + return [self elementForName:@"delay" xmlns:@"urn:xmpp:delay"]; +} + +- (NSXMLElement *)legacyDelayedDeliveryChildElement +{ + return [self elementForName:@"x" xmlns:@"jabber:x:delay"]; +} + +- (NSXMLElement *)anyDelayedDeliveryChildElement +{ + return [self delayedDeliveryChildElement] ?: [self legacyDelayedDeliveryChildElement]; +} + @end diff --git a/Extensions/XEP-0203/XMPPDelayedDelivery.h b/Extensions/XEP-0203/XMPPDelayedDelivery.h new file mode 100644 index 0000000000..29d5240dd0 --- /dev/null +++ b/Extensions/XEP-0203/XMPPDelayedDelivery.h @@ -0,0 +1,25 @@ +#import "XMPP.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A module for processing XEP-0203 Delayed Delivery information in incoming XMPP stanzas. +@interface XMPPDelayedDelivery : XMPPModule + +@end + +/// A protocol defining @c XMPPDelayedDelivery module delegate API. +@protocol XMPPDelayedDeliveryDelegate + +@optional + +/// Notifies the delegate that a delayed delivery message has been received in the stream. +- (void)xmppDelayedDelivery:(XMPPDelayedDelivery *)xmppDelayedDelivery + didReceiveDelayedMessage:(XMPPMessage *)delayedMessage; + +/// Notifies the delegate that a delayed delivery presence has been received in the stream. +- (void)xmppDelayedDelivery:(XMPPDelayedDelivery *)xmppDelayedDelivery + didReceiveDelayedPresence:(XMPPPresence *)delayedPresence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0203/XMPPDelayedDelivery.m b/Extensions/XEP-0203/XMPPDelayedDelivery.m new file mode 100644 index 0000000000..7831754150 --- /dev/null +++ b/Extensions/XEP-0203/XMPPDelayedDelivery.m @@ -0,0 +1,57 @@ +#import "XMPPDelayedDelivery.h" +#import "XMPPLogging.h" +#import "NSXMLElement+XEP_0203.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@implementation XMPPDelayedDelivery + +- (void)didActivate +{ + XMPPLogTrace(); +} + +- (void)willDeactivate +{ + XMPPLogTrace(); +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (![message wasDelayed]) { + return; + } + + XMPPLogInfo(@"Received delayed delivery message with date: %@, origin: %@, reason description: %@", + [message delayedDeliveryDate], + [message delayedDeliveryFrom] ?: @"unspecified", + [message delayedDeliveryReasonDescription] ?: @"unspecified"); + + [multicastDelegate xmppDelayedDelivery:self didReceiveDelayedMessage:message]; +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + XMPPLogTrace(); + + if (![presence wasDelayed]) { + return; + } + + XMPPLogInfo(@"Received delayed delivery presence with date: %@, origin: %@, reason description: %@", + [presence delayedDeliveryDate], + [presence delayedDeliveryFrom] ?: @"unspecified", + [presence delayedDeliveryReasonDescription] ?: @"unspecified"); + + [multicastDelegate xmppDelayedDelivery:self didReceiveDelayedPresence:presence]; +} + +@end diff --git a/Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.h b/Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.h new file mode 100644 index 0000000000..d61c6789d8 --- /dev/null +++ b/Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.h @@ -0,0 +1,49 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageContextItemCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XMPPMessageCoreDataStorageTransaction (XEP_0203) + +/// @brief Registers XEP-0203 delayed delivery information for the received message. +/// @discussion It is assumed that the provided @c XMPPMessage contains at least a delayed delivery timestamp. +- (void)registerDelayedDeliveryForReceivedMessage:(XMPPMessage *)message; + +@end + +@interface XMPPMessageCoreDataStorageObject (XEP_0203) + +/// Returns the timestamp when the message was originally sent. +- (nullable NSDate *)delayedDeliveryDate; + +/// Returns the JID of the entity that originally sent/delayed the delivery of the message. +- (nullable XMPPJID *)delayedDeliveryFrom; + +/// Returns the natural language description of the reason for the delay. +- (nullable NSString *)delayedDeliveryReasonDescription; + +/// Associates delayed delivery information with the message. +- (void)setDelayedDeliveryDate:(NSDate *)delayedDeliveryDate + from:(nullable XMPPJID *)delayedDeliveryFrom + reasonDescription:(nullable NSString *)delayedDeliveryReasonDescription; + +@end + +@interface XMPPMessageContextItemCoreDataStorageObject (XEP_0203) + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include the delayed delivery context timestamp for each message. + + It is possible to OR-combine this predicate with @c streamTimestampKindPredicate without getting duplicates + as the result set of the latter will not include any messages with delayed delivery timestamps assigned. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + @see streamTimestampKindPredicate + */ ++ (NSPredicate *)delayedDeliveryTimestampKindPredicate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.m b/Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.m new file mode 100644 index 0000000000..cbe87ebb9f --- /dev/null +++ b/Extensions/XEP-0203/XMPPMessageCoreDataStorage+XEP_0203.m @@ -0,0 +1,89 @@ +#import "XMPPMessageCoreDataStorage+XEP_0203.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "XMPPMessage.h" +#import "NSXMLElement+XEP_0203.h" + +static XMPPMessageContextTimestampItemTag const XMPPMessageContextDelayedDeliveryTimestampTag = @"XMPPMessageContextDelayedDeliveryTimestamp"; +static XMPPMessageContextJIDItemTag const XMPPMessageContextDelayedDeliveryFromTag = @"XMPPMessageContextDelayedDeliveryFrom"; +static XMPPMessageContextStringItemTag const XMPPMessageContextDelayedDeliveryReasonDescriptionTag = @"XMPPMessageContextDelayedDeliveryReasonDescription"; + +@interface XMPPMessageCoreDataStorageObject (XEP_0203_Private) + +- (void)appendDelayedDeliveryContextWithDate:(NSDate *)delayedDeliveryDate + from:(XMPPJID *)delayedDeliveryFrom + reasonDescription:(NSString *)delayedDeliveryReasonDescription; + +@end + +@implementation XMPPMessageCoreDataStorageTransaction (XEP_0203) + +- (void)registerDelayedDeliveryForReceivedMessage:(XMPPMessage *)message +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + + [messageObject appendDelayedDeliveryContextWithDate:[message delayedDeliveryDate] + from:[message delayedDeliveryFrom] + reasonDescription:[message delayedDeliveryReasonDescription]]; + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XEP_0203) + +- (NSDate *)delayedDeliveryDate +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement timestampItemValueForTag:XMPPMessageContextDelayedDeliveryTimestampTag]; + }]; +} + +- (XMPPJID *)delayedDeliveryFrom +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement jidItemValueForTag:XMPPMessageContextDelayedDeliveryFromTag]; + }]; +} + +- (NSString *)delayedDeliveryReasonDescription +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextDelayedDeliveryReasonDescriptionTag]; + }]; +} + +- (void)setDelayedDeliveryDate:(NSDate *)delayedDeliveryDate from:(XMPPJID *)delayedDeliveryFrom reasonDescription:(NSString *)delayedDeliveryReasonDescription +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"This action is only allowed for outgoing message objects"); + [self appendDelayedDeliveryContextWithDate:delayedDeliveryDate from:delayedDeliveryFrom reasonDescription:delayedDeliveryReasonDescription]; +} + +- (void)appendDelayedDeliveryContextWithDate:(NSDate *)delayedDeliveryDate from:(XMPPJID *)delayedDeliveryFrom reasonDescription:(NSString *)delayedDeliveryReasonDescription +{ + NSAssert(![self delayedDeliveryDate], @"Delayed delivery information is already present"); + + [self retireStreamTimestamp]; + + XMPPMessageContextCoreDataStorageObject *delayedDeliveryContextElement = [self appendContextElement]; + + [delayedDeliveryContextElement appendTimestampItemWithTag:XMPPMessageContextDelayedDeliveryTimestampTag value:delayedDeliveryDate]; + if (delayedDeliveryFrom) { + [delayedDeliveryContextElement appendJIDItemWithTag:XMPPMessageContextDelayedDeliveryFromTag value:delayedDeliveryFrom]; + } + if (delayedDeliveryReasonDescription) { + [delayedDeliveryContextElement appendStringItemWithTag:XMPPMessageContextDelayedDeliveryReasonDescriptionTag value:delayedDeliveryReasonDescription]; + } +} + +@end + +@implementation XMPPMessageContextItemCoreDataStorageObject (XEP_0203) + ++ (NSPredicate *)delayedDeliveryTimestampKindPredicate +{ + return [XMPPMessageContextTimestampItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextDelayedDeliveryTimestampTag]; +} + +@end diff --git a/Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.h b/Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.h new file mode 100644 index 0000000000..f8340fec5a --- /dev/null +++ b/Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.h @@ -0,0 +1,22 @@ +#import "XMPPMessageCoreDataStorageObject.h" + +@interface XMPPMessageCoreDataStorageObject (XEP_0245) + +/** + Returns the actual /me command action phrase. + + The action phrase is the result of stripping the "/me " body prefix. + This method returns nil if the body cannot be interpreted as a /me command. + */ +- (nullable NSString *)meCommandText; + +/** + Returns the JID of the action subject. + + The relevant JID is either the value of the "from" attribute or, for outgoing messages, + the stream JID associated with the message. + This method returns nil if the body cannot be interpreted as a /me command. + */ +- (nullable XMPPJID *)meCommandSubjectJID; + +@end diff --git a/Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.m b/Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.m new file mode 100644 index 0000000000..b82fb38956 --- /dev/null +++ b/Extensions/XEP-0245/XMPPMessageCoreDataStorage+XEP_0245.m @@ -0,0 +1,30 @@ +#import "XMPPMessageCoreDataStorage+XEP_0245.h" + +@implementation XMPPMessageCoreDataStorageObject (XEP_0245) + +- (NSString *)meCommandText +{ + NSRange meCommandPrefixRange = [self meCommandPrefixRange]; + return meCommandPrefixRange.location != NSNotFound ? [self.body stringByReplacingCharactersInRange:meCommandPrefixRange withString:@""] : nil; +} + +- (XMPPJID *)meCommandSubjectJID +{ + if ([self meCommandPrefixRange].location == NSNotFound) { + return nil; + } + + if (self.fromJID) { + return self.fromJID; + } else { + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"Only outgoing messages without from JID are supported here"); + return [self streamJID]; + } +} + +- (NSRange)meCommandPrefixRange +{ + return [self.body rangeOfString:@"/me " options:NSAnchoredSearch]; +} + +@end diff --git a/Extensions/XEP-0297/NSXMLElement+XEP_0297.m b/Extensions/XEP-0297/NSXMLElement+XEP_0297.m index bda7682cc4..ddb790ee29 100644 --- a/Extensions/XEP-0297/NSXMLElement+XEP_0297.m +++ b/Extensions/XEP-0297/NSXMLElement+XEP_0297.m @@ -66,11 +66,11 @@ - (XMPPIQ *)forwardedIQ { if([self isForwardedStanza]) { - return [XMPPIQ iqFromElement:[self elementForName:@"iq"]]; + return [XMPPIQ iqFromElement:[self elementForName_fixed:@"iq"]]; } else { - return [XMPPIQ iqFromElement:[[self forwardedStanza] elementForName:@"iq"]]; + return [XMPPIQ iqFromElement:[[self forwardedStanza] elementForName_fixed:@"iq"]]; } } @@ -90,11 +90,11 @@ - (XMPPMessage *)forwardedMessage { if([self isForwardedStanza]) { - return [XMPPMessage messageFromElement:[self elementForName:@"message"]]; + return [XMPPMessage messageFromElement:[self elementForName_fixed:@"message"]]; } else { - return [XMPPMessage messageFromElement:[[self forwardedStanza] elementForName:@"message"]]; + return [XMPPMessage messageFromElement:[[self forwardedStanza] elementForName_fixed:@"message"]]; } } @@ -115,11 +115,11 @@ - (XMPPPresence *)forwardedPresence { if([self isForwardedStanza]) { - return [XMPPPresence presenceFromElement:[self elementForName:@"presence"]]; + return [XMPPPresence presenceFromElement:[self elementForName_fixed:@"presence"]]; } else { - return [XMPPPresence presenceFromElement:[[self forwardedStanza] elementForName:@"presence"]]; + return [XMPPPresence presenceFromElement:[[self forwardedStanza] elementForName_fixed:@"presence"]]; } } @@ -135,4 +135,16 @@ - (BOOL)hasForwardedPresence } } +#pragma mark -elementsForName: bug workaround + +- (NSXMLElement *)elementForName_fixed:(NSString *)name +{ + for (__kindof NSXMLNode *child in self.children) { + if ([child isKindOfClass:[NSXMLElement class]] && [child.name isEqualToString:name]) { + return child; + } + } + return nil; +} + @end diff --git a/Extensions/XEP-0308/XMPPCapabilities+XEP_0308.h b/Extensions/XEP-0308/XMPPCapabilities+XEP_0308.h new file mode 100644 index 0000000000..bb64057786 --- /dev/null +++ b/Extensions/XEP-0308/XMPPCapabilities+XEP_0308.h @@ -0,0 +1,14 @@ +#import "XMPPCapabilities.h" + +NS_ASSUME_NONNULL_BEGIN + +@class XMPPJID; + +@interface XMPPCapabilities (XEP_0308) + +/// Returns YES if it has been determined that the entity with the given JID is capable of receiving XEP-0308 correction messages. +- (BOOL)isLastMessageCorrectionCapabilityConfirmedForJID:(XMPPJID *)jid; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0308/XMPPCapabilities+XEP_0308.m b/Extensions/XEP-0308/XMPPCapabilities+XEP_0308.m new file mode 100644 index 0000000000..e641338740 --- /dev/null +++ b/Extensions/XEP-0308/XMPPCapabilities+XEP_0308.m @@ -0,0 +1,24 @@ +#import "XMPPCapabilities+XEP_0308.h" + +static NSString * const XMPPLastMessageCorrectionCapabilitiesFeature = @"urn:xmpp:message-correct:0"; + +@implementation XMPPCapabilities (XEP_0308) + +- (BOOL)isLastMessageCorrectionCapabilityConfirmedForJID:(XMPPJID *)jid +{ + if (![self.xmppCapabilitiesStorage areCapabilitiesKnownForJID:jid xmppStream:self.xmppStream]) { + return NO; + } + + NSXMLElement *capabilities = [self.xmppCapabilitiesStorage capabilitiesForJID:jid xmppStream:self.xmppStream]; + for (NSXMLElement *feature in [capabilities children]) { + if ([[feature name] isEqualToString:@"feature"] + && [[feature attributeStringValueForName:@"var"] isEqualToString:XMPPLastMessageCorrectionCapabilitiesFeature]) { + return YES; + } + } + + return NO; +} + +@end diff --git a/Extensions/XEP-0308/XMPPLastMessageCorrection.h b/Extensions/XEP-0308/XMPPLastMessageCorrection.h new file mode 100644 index 0000000000..92479db1d5 --- /dev/null +++ b/Extensions/XEP-0308/XMPPLastMessageCorrection.h @@ -0,0 +1,29 @@ +#import "XMPPModule.h" + +NS_ASSUME_NONNULL_BEGIN; + +@class XMPPMessage; + +/** + A module that handles XEP-0308 message corrections. + + This module has the following interactions with other modules: + - Reports XEP-0308 capability to @c XMPPCapabilities. + - Observes MUC/MUC Light affiliation change callbacks to indicate that the last message can no longer be corrected after rejoining the room. + */ +@interface XMPPLastMessageCorrection : XMPPModule + +/// Returns YES if a sent message with the given element ID can still be corrected, as per the respective XEP rules. +- (BOOL)canCorrectSentMessageWithID:(NSString *)messageID; + +@end + +/// A protocol defining @c XMPPLastMessageCorrection module delegate API. +@protocol XMPPLastMessageCorrectionDelegate + +/// Notifies the delegate that a message correction has been received in the stream. +- (void)xmppLastMessageCorrection:(XMPPLastMessageCorrection *)xmppLastMessageCorrection didReceiveCorrectedMessage:(XMPPMessage *)correctedMessage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0308/XMPPLastMessageCorrection.m b/Extensions/XEP-0308/XMPPLastMessageCorrection.m new file mode 100644 index 0000000000..0fd759b689 --- /dev/null +++ b/Extensions/XEP-0308/XMPPLastMessageCorrection.m @@ -0,0 +1,131 @@ +#import "XMPPLastMessageCorrection.h" +#import "XMPPCapabilities.h" +#import "XMPPRoom.h" +#import "XMPPMUCLight.h" +#import "XMPPMessage+XEP_0308.h" +//#import "XMPPJID.h" +#import "XMPPLogging.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +static NSString * const XMPPLastMessageCorrectionNamespace = @"urn:xmpp:message-correct:0"; + +@interface XMPPLastMessageCorrection () + +@property (nonatomic, strong, readonly) NSMutableDictionary *sentMessageIDIndex; + +@end + +@implementation XMPPLastMessageCorrection + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + self = [super initWithDispatchQueue:queue]; + if (self) { + _sentMessageIDIndex = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)canCorrectSentMessageWithID:(NSString *)messageID +{ + __block BOOL result; + dispatch_block_t block = ^{ + result = [self.sentMessageIDIndex.allValues containsObject:messageID]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)didActivate +{ + XMPPLogTrace(); + + [self.xmppStream autoAddDelegate:self delegateQueue:self.moduleQueue toModulesOfClass:[XMPPCapabilities class]]; + [self.xmppStream autoAddDelegate:self delegateQueue:self.moduleQueue toModulesOfClass:[XMPPRoom class]]; + [self.xmppStream autoAddDelegate:self delegateQueue:self.moduleQueue toModulesOfClass:[XMPPMUCLight class]]; +} + +- (void)willDeactivate +{ + XMPPLogTrace(); + + [self.sentMessageIDIndex removeAllObjects]; + [self.xmppStream removeAutoDelegate:self delegateQueue:self.moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; + [self.xmppStream removeAutoDelegate:self delegateQueue:self.moduleQueue fromModulesOfClass:[XMPPRoom class]]; + [self.xmppStream removeAutoDelegate:self delegateQueue:self.moduleQueue fromModulesOfClass:[XMPPMUCLight class]]; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (![message isMessageCorrection]) { + return; + } + + XMPPLogInfo(@"Received correction for message with ID: %@", [message correctedMessageID]); + [multicastDelegate xmppLastMessageCorrection:self didReceiveCorrectedMessage:message]; +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + self.sentMessageIDIndex[[[message to] bare]] = [message elementID]; + XMPPLogInfo(@"Updated last sent message ID for %@", [[message to] bare]); +} + +- (void)xmppStreamDidChangeMyJID:(XMPPStream *)xmppStream +{ + XMPPLogTrace(); + + [self.sentMessageIDIndex removeAllObjects]; + XMPPLogInfo(@"My JID changed, resetting sent message ID index"); +} + +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + XMPPLogTrace(); + + NSXMLElement *lastMessageCorrectionFeatureElement = [NSXMLElement elementWithName:@"feature"]; + [lastMessageCorrectionFeatureElement addAttributeWithName:@"var" stringValue:XMPPLastMessageCorrectionNamespace]; + [query addChild:lastMessageCorrectionFeatureElement]; +} + +- (void)xmppRoomDidJoin:(XMPPRoom *)sender +{ + XMPPLogTrace(); + [self.sentMessageIDIndex removeObjectForKey:[sender.roomJID bare]]; + XMPPLogInfo(@"Reset last sent message ID for MUC room %@", [sender.roomJID bare]); +} + +- (void)xmppMUCLight:(XMPPMUCLight *)sender changedAffiliation:(NSString *)affiliation userJID:(XMPPJID *)userJID roomJID:(XMPPJID *)roomJID +{ + XMPPLogTrace(); + + if ([affiliation isEqualToString:@"none"]) { + return; + } + + // TODO: member->owner and owner->member transitions should not break message correction continuity + if (![userJID isEqualToJID:sender.xmppStream.myJID options:XMPPJIDCompareBare]) { + return; + } + + [self.sentMessageIDIndex removeObjectForKey:[roomJID bare]]; + XMPPLogInfo(@"Reset last sent message ID for MUC Light room %@", [roomJID bare]); +} + +@end diff --git a/Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.h b/Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.h new file mode 100644 index 0000000000..e88e518f91 --- /dev/null +++ b/Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.h @@ -0,0 +1,42 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XMPPMessageCoreDataStorageTransaction (XEP_0308) + +/// @brief Registers a reference to the original message that is being corrected by the provided XEP-0308 message. +/// @discussion It is assumed that the provided message contains a XEP-0308 element referring the original message. +- (void)registerOriginalMessageIDForReceivedCorrectedMessage:(XMPPMessage *)message; + +@end + +@interface XMPPMessageCoreDataStorageObject (XEP_0308) + +/// @brief Returns the message object that contains a XEP-0308 correction of a message with the provided element ID. +/// @see findCorrectionForMessageWithID:inManagedObjectContext: ++ (nullable XMPPMessageCoreDataStorageObject *)findCorrectionForMessageWithID:(NSString *)originalMessageID + inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +NS_SWIFT_NAME(findCorrection(forMessageWithID:in:)); + +/** + Returns YES if the storage contains a XEP-0308 correction of the given message object. + + A message object representing the corrected message will not be included in @c XMPPMessageContextItemCoreDataStorageObject + timestamp context fetch results. Instead, an application is expected to check for a potential correction message presence + using this method and, if needed, look up the correction using @c findCorrectionForMessageWithID:inManagedObjectContext: . + + @see findCorrectionForMessageWithID:inManagedObjectContext: + */ +- (BOOL)hasAssociatedCorrectionMessage; + +/// Returns the ID of the corrected message if the given object represents a XEP-0308 message correction. +- (nullable NSString *)messageCorrectionID; + +/// @brief Marks the represented message as a XEP-0308 correction of the message with the provided element ID. +/// @discussion This method can only be invoked on an object representing an outgoing message. +- (void)assignMessageCorrectionID:(NSString *)messageCorrectionID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.m b/Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.m new file mode 100644 index 0000000000..ff7da0b3ec --- /dev/null +++ b/Extensions/XEP-0308/XMPPMessageCoreDataStorage+XEP_0308.m @@ -0,0 +1,79 @@ +#import "XMPPMessageCoreDataStorage+XEP_0308.h" +#import "XMPPMessageCOreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" +#import "XMPPMessage+XEP_0308.h" + +static XMPPMessageContextMarkerItemTag const XMPPMessageContextAssociatedCorrectionTag = @"XMPPMessageContextAssociatedCorrection"; +static XMPPMessageContextStringItemTag const XMPPMessageContextCorrectionIDTag = @"XMPPMessageContextCorrectionID"; + +@interface XMPPMessageCoreDataStorageObject (XEP_0308_Private) + +- (void)appendMessageCorrectionContextWithID:(NSString *)originalMessageID; + +@end + +@implementation XMPPMessageCoreDataStorageTransaction (XEP_0308) + +- (void)registerOriginalMessageIDForReceivedCorrectedMessage:(XMPPMessage *)message +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + [messageObject appendMessageCorrectionContextWithID:[message correctedMessageID]]; + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XEP_0308) + ++ (XMPPMessageCoreDataStorageObject *)findCorrectionForMessageWithID:(NSString *)originalMessageID inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *fetchRequest = [XMPPMessageContextStringItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + NSArray *predicates = @[[XMPPMessageContextStringItemCoreDataStorageObject stringPredicateWithValue:originalMessageID], + [XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextCorrectionIDTag]]; + fetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; + + NSArray *result = [managedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + NSAssert(result.count <= 1, @"Multiple correction context items for the given original ID"); + return result.firstObject.message; +} + +- (BOOL)hasAssociatedCorrectionMessage +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement hasMarkerItemForTag:XMPPMessageContextAssociatedCorrectionTag] ? contextElement : nil; + }] != nil; +} + +- (NSString *)messageCorrectionID +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextCorrectionIDTag]; + }]; +} + +- (void)assignMessageCorrectionID:(NSString *)originalMessageID +{ + NSAssert(self.direction == XMPPMessageDirectionOutgoing, @"Only allowed for outgoing message objects"); + [self appendMessageCorrectionContextWithID:originalMessageID]; +} + +- (void)appendMessageCorrectionContextWithID:(NSString *)originalMessageID +{ + NSAssert(self.managedObjectContext, @"Attempted to assign a correction ID with no managed object context available"); + NSAssert(![self messageCorrectionID], @"Message correction ID is already assigned"); + + [self retireStreamTimestamp]; + + XMPPMessageContextCoreDataStorageObject *correctionContext = [self appendContextElement]; + [correctionContext appendStringItemWithTag:XMPPMessageContextCorrectionIDTag value:originalMessageID]; + + XMPPMessageCoreDataStorageObject *originalMessage = [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:originalMessageID + inManagedObjectContext:self.managedObjectContext]; + NSAssert(originalMessage, @"Original message object not found"); + XMPPMessageContextCoreDataStorageObject *correctionOriginContext = [originalMessage appendContextElement]; + [correctionOriginContext appendMarkerItemWithTag:XMPPMessageContextAssociatedCorrectionTag]; +} + +@end diff --git a/Extensions/XEP-0313/XMPPMessageArchiveManagement.h b/Extensions/XEP-0313/XMPPMessageArchiveManagement.h index 1a0e803ff6..9b1414908a 100644 --- a/Extensions/XEP-0313/XMPPMessageArchiveManagement.h +++ b/Extensions/XEP-0313/XMPPMessageArchiveManagement.h @@ -24,6 +24,12 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readwrite, assign) NSInteger resultAutomaticPagingPageSize; +/** + When enabled, original messages unwrapped from query results will be injected back into the stream, exposing them to other modules. + Disabled by default. + */ +@property (readwrite, assign) BOOL submitsPayloadMessagesForStreamProcessing; + - (void)retrieveMessageArchiveWithFields:(nullable NSArray *)fields withResultSet:(nullable XMPPResultSet *)resultSet; @@ -41,9 +47,11 @@ NS_ASSUME_NONNULL_BEGIN @protocol XMPPMessageArchiveManagementDelegate @optional +- (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFinishReceivingMessagesWithArchiveIDs:(NSArray *)archiveIDs; - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFinishReceivingMessagesWithSet:(XMPPResultSet *)resultSet; - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didReceiveMAMMessage:(XMPPMessage *)message; - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFailToReceiveMessages:(XMPPIQ *)error; +- (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didSubmitPayloadMessageFromQueryResult:(NSXMLElement *)result; - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didReceiveFormFields:(XMPPIQ *)iq; - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFailToReceiveFormFields:(XMPPIQ *)iq; diff --git a/Extensions/XEP-0313/XMPPMessageArchiveManagement.m b/Extensions/XEP-0313/XMPPMessageArchiveManagement.m index ae8436c0e3..5c209b042d 100644 --- a/Extensions/XEP-0313/XMPPMessageArchiveManagement.m +++ b/Extensions/XEP-0313/XMPPMessageArchiveManagement.m @@ -14,14 +14,19 @@ #define XMLNS_XMPP_MAM @"urn:xmpp:mam:1" @interface XMPPMessageArchiveManagement() + @property (strong, nonatomic) NSString *queryID; @property (strong, nonatomic) XMPPIDTracker *xmppIDTracker; +@property (strong, nonatomic) NSMutableDictionary *resultSetPageElementsIndex; +@property (strong, nonatomic) dispatch_group_t resultSetPageProcessingGroup; + @end @implementation XMPPMessageArchiveManagement @synthesize resultAutomaticPagingPageSize=_resultAutomaticPagingPageSize; @synthesize xmppIDTracker; +@synthesize submitsPayloadMessagesForStreamProcessing=_submitsPayloadMessagesForStreamProcessing; - (NSInteger)resultAutomaticPagingPageSize { @@ -51,6 +56,34 @@ - (void)setResultAutomaticPagingPageSize:(NSInteger)resultAutomaticPagingPageSiz dispatch_async(moduleQueue, block); } +- (BOOL)submitsPayloadMessagesForStreamProcessing +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = _submitsPayloadMessagesForStreamProcessing; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setSubmitsPayloadMessagesForStreamProcessing:(BOOL)submitsPayloadMessagesForStreamProcessing +{ + dispatch_block_t block = ^{ + _submitsPayloadMessagesForStreamProcessing = submitsPayloadMessagesForStreamProcessing; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + - (void)retrieveMessageArchiveWithFields:(NSArray *)fields withResultSet:(XMPPResultSet *)resultSet { [self retrieveMessageArchiveAt:nil withFields:fields withResultSet:resultSet]; } @@ -78,6 +111,8 @@ - (void)retrieveMessageArchiveAt:(XMPPJID *)archiveJID withFormElement:(NSXMLEle } self.queryID = [XMPPStream generateUUID]; + self.resultSetPageElementsIndex = [[NSMutableDictionary alloc] init]; + self.resultSetPageProcessingGroup = dispatch_group_create(); NSXMLElement *queryElement = [NSXMLElement elementWithName:@"query" xmlns:XMLNS_XMPP_MAM]; [queryElement addAttributeWithName:@"queryid" stringValue:self.queryID]; @@ -107,28 +142,49 @@ - (void)retrieveMessageArchiveAt:(XMPPJID *)archiveJID withFormElement:(NSXMLEle - (void)handleMessageArchiveIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)trackerInfo { - if ([[iq type] isEqualToString:@"result"]) { - - NSXMLElement *finElement = [iq elementForName:@"fin" xmlns:XMLNS_XMPP_MAM]; - NSXMLElement *setElement = [finElement elementForName:@"set" xmlns:@"http://jabber.org/protocol/rsm"]; - - XMPPResultSet *resultSet = [XMPPResultSet resultSetFromElement:setElement]; - NSString *lastId = [resultSet elementForName:@"last"].stringValue; + NSString *finalizedQueryID = self.queryID; + + dispatch_group_notify(self.resultSetPageProcessingGroup, self.moduleQueue, ^{ + NSMutableArray *pageArchiveIDs; + if ([finalizedQueryID isEqualToString:self.queryID]) { + pageArchiveIDs = [[NSMutableArray alloc] init]; + for (NSXMLElement *result in self.resultSetPageElementsIndex.allValues) { + [pageArchiveIDs addObject:[result attributeStringValueForName:@"id"]]; + } + + self.queryID = nil; + self.resultSetPageElementsIndex = nil; + self.resultSetPageProcessingGroup = nil; + } - if (self.resultAutomaticPagingPageSize == 0 || [finElement attributeBoolValueForName:@"complete"] || !lastId) { + if ([[iq type] isEqualToString:@"result"]) { + NSXMLElement *finElement = [iq elementForName:@"fin" xmlns:XMLNS_XMPP_MAM]; + NSXMLElement *setElement = [finElement elementForName:@"set" xmlns:@"http://jabber.org/protocol/rsm"]; + + XMPPResultSet *resultSet = [XMPPResultSet resultSetFromElement:setElement]; [multicastDelegate xmppMessageArchiveManagement:self didFinishReceivingMessagesWithSet:resultSet]; - return; + + if (pageArchiveIDs.count > 0) { + [multicastDelegate xmppMessageArchiveManagement:self didFinishReceivingMessagesWithArchiveIDs:pageArchiveIDs]; + } + + NSString *lastId = [resultSet elementForName:@"last"].stringValue; + if (self.resultAutomaticPagingPageSize != 0 && ![finElement attributeBoolValueForName:@"complete"] && lastId) { + [self continueAutomaticPagingWithOriginalIQ:[XMPPIQ iqFromElement:[trackerInfo element]] lastResultID:lastId]; + } + } else { + [multicastDelegate xmppMessageArchiveManagement:self didFailToReceiveMessages:iq]; } - - XMPPIQ *originalIq = [XMPPIQ iqFromElement:[trackerInfo element]]; - XMPPJID *originalArchiveJID = [originalIq to]; - NSXMLElement *originalFormElement = [[[originalIq elementForName:@"query"] elementForName:@"x"] copy]; - XMPPResultSet *pagingResultSet = [[XMPPResultSet alloc] initWithMax:self.resultAutomaticPagingPageSize after:lastId]; - - [self retrieveMessageArchiveAt:originalArchiveJID withFormElement:originalFormElement resultSet:pagingResultSet]; - } else { - [multicastDelegate xmppMessageArchiveManagement:self didFailToReceiveMessages:iq]; - } + }); +} + +- (void)continueAutomaticPagingWithOriginalIQ:(XMPPIQ *)originalIQ lastResultID:(NSString *)lastResultID +{ + XMPPJID *originalArchiveJID = [originalIQ to]; + NSXMLElement *originalFormElement = [[[originalIQ elementForName:@"query"] elementForName:@"x"] copy]; + XMPPResultSet *pagingResultSet = [[XMPPResultSet alloc] initWithMax:self.resultAutomaticPagingPageSize after:lastResultID]; + + [self retrieveMessageArchiveAt:originalArchiveJID withFormElement:originalFormElement resultSet:pagingResultSet]; } + (NSXMLElement *)fieldWithVar:(NSString *)var type:(NSString *)type andValue:(NSString *)value { @@ -221,15 +277,36 @@ - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq return NO; } -- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message { +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPElementEvent *event = [sender currentElementEvent]; + NSXMLElement *result = [message elementForName:@"result" xmlns:XMLNS_XMPP_MAM]; - BOOL forwarded = [result hasForwardedStanza]; - NSString *queryID = [result attributeForName:@"queryid"].stringValue; - - if (forwarded && [queryID isEqualToString:self.queryID]) { - [multicastDelegate xmppMessageArchiveManagement:self didReceiveMAMMessage:message]; - } + + if ([queryID isEqualToString:self.queryID]) { + NSString *processingID = [sender generateUUID]; + self.resultSetPageElementsIndex[processingID] = result; + + [multicastDelegate xmppMessageArchiveManagement:self didReceiveMAMMessage:message]; + + if (self.submitsPayloadMessagesForStreamProcessing && [result forwardedMessage]) { + dispatch_group_enter(self.resultSetPageProcessingGroup); + [self.xmppStream injectElement:[result forwardedMessage] registeringEventWithID:processingID]; + } + } + + NSXMLElement *injectedPayloadMessageResult = self.resultSetPageElementsIndex[event.uniqueID]; + if (injectedPayloadMessageResult) { + [multicastDelegate xmppMessageArchiveManagement:self didSubmitPayloadMessageFromQueryResult:injectedPayloadMessageResult]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didFinishProcessingElementEvent:(XMPPElementEvent *)event +{ + if (self.resultSetPageElementsIndex[event.uniqueID]) { + dispatch_group_leave(self.resultSetPageProcessingGroup); + } } @end diff --git a/Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.h b/Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.h new file mode 100644 index 0000000000..9d5ab06f1b --- /dev/null +++ b/Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.h @@ -0,0 +1,103 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" +#import "XMPPMessageContextItemCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class NSXMLElement; + +typedef NS_ENUM(NSInteger, XMPPMessageArchiveQueryResultStorageMode) { + /// A mode where only MAM metadata (archive ID and timestamp) are stored. + XMPPMessageArchiveQueryResultStorageModeMetadataOnly, + /// A mode where both MAM metadata (archive ID/timestamp) and the embedded payload is stored. + XMPPMessageArchiveQueryResultStorageModeComplete +}; + +typedef NS_OPTIONS(NSInteger, XMPPMessageArchiveTimestampContextOptions) { + /// A flag indicating that a MAM timestamp context fetch should include items belonging to incomplete query result set pages. + XMPPMessageArchiveTimestampContextIncludingPartialResultPages = 1 << 0, + /// A flag indicating that a MAM timestamp context fetch should include placeholder items for messages removed from the middle of an archive. + XMPPMessageArchiveTimestampContextIncludingDeletedResultItems = 1 << 1, +}; + +@interface XMPPMessageCoreDataStorage (XEP_0313) + +/** + Marks message objects with given archive IDs as belonging to a complete result set page. + + MAM archive content is streamed to the client and then processed in the framework message by message. + As in-order processing of the individual messages within a single page cannot be guaranteed, staging updates + is the only way to prevent gaps in the local history in certain situations, e.g. when critical errors occur or an app crashes. + Avoiding such gaps is important as they make incremental archive synchronization impossible. + + This method is intented to be invoked in response to MAM module's @c xmppMessageArchiveManagement:didFinishReceivingMessagesWithArchiveIDs: + delegate callback. At that point all messages from the given page have already been processed locally. + */ +- (void)finalizeResultSetPageWithMessageArchiveIDs:(NSArray *)archiveIDs; + +@end + +@interface XMPPMessageCoreDataStorageTransaction (XEP_0313) + +/** + Stores MAM metadata along with an optional payload from a @c result element contained in a received query result message. + + This method is intended to be invoked in one of the two possible scenarios: + + 1. The MAM module is not configured to submit payloads for further stream processing (@c submitsPayloadMessagesForStreamProcessing set to @c NO). + + In this case the module's delegate is expected to extract the query result item in the @c xmppMessageArchiveManagement:didReceiveMAMMessage: callback + and invoke this method in @c XMPPMessageArchiveQueryResultStorageModeComplete mode, storing both the metadata and the actual payload. + However, since the storage is not expected to be aware of any other XMPP extensions, only the basic RFC 3921/6121 properties can be stored + for the provided payload. + + 2. The module is configured to submit payloads for further stream processing (@c submitsPayloadMessagesForStreamProcessing set to @c YES). + + In this scenario it is assumed that other modules will handle payload storage the same way they do for "live" messages. The module's delegate + is still responsible for registering MAM metadata though. To do so, it should invoke this method in @c XMPPMessageArchiveQueryResultStorageModeMetadataOnly + mode on a transaction in the context of the @c xmppMessageArchiveManagement:didSubmitPayloadMessageFromQueryResult: callback, allowing MAM metadata + to be linked to the message storage object processed by other modules. + + This method will abort the whole transaction if it is determined that the corresponding message is already stored locally. + */ +- (void)storeMessageArchiveQueryResultItem:(NSXMLElement *)resultItem inMode:(XMPPMessageArchiveQueryResultStorageMode)storageMode; + +@end + +@interface XMPPMessageCoreDataStorageObject (XEP_0313) + +/// Returns the unique archive ID assigned by the server for message objects received via MAM. +- (nullable NSString *)messageArchiveID; + +/// Returns the timestamp of when the message was originally sent (for an outgoing message) or received (for an incoming message) +/// for message objects received via MAM. +- (nullable NSDate *)messageArchiveDate; + +/** + Returns YES for own chat message objects received via MAM. + + Note that such messages will not appear in fetch results from @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + on @c XMPPMessageContextItemCoreDataStorageObject when using a predicate obtained from @c messageRemotePartyJIDPredicateWithValue:compareOptions:. + This is because that predicate only expects outgoing direction messages to have the relevant @c toJID value. + */ +- (BOOL)isMyArchivedChatMessage; + +@end + +@interface XMPPMessageContextItemCoreDataStorageObject (XEP_0313) + +/** + Returns a predicate to be provided to @c requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + that limits the fetch results to only include the message archive context timestamp for each message. + + It is possible to OR-combine this predicate with @c streamTimestampKindPredicate without getting duplicates + as the result set of the latter will not include any messages with message archive timestamps assigned. + + @see requestByTimestampsWithPredicate:inAscendingOrder:fromManagedObjectContext: + @see streamTimestampKindPredicate + */ ++ (NSPredicate *)messageArchiveTimestampKindPredicateWithOptions:(XMPPMessageArchiveTimestampContextOptions)options; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.m b/Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.m new file mode 100644 index 0000000000..e74d78e1e4 --- /dev/null +++ b/Extensions/XEP-0313/XMPPMessageCoreDataStorage+XEP_0313.m @@ -0,0 +1,167 @@ +#import "XMPPMessageCoreDataStorage+XEP_0313.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessageCoreDataStorageObject+ContextHelpers.h" +#import "NSManagedObject+XMPPCoreDataStorage.h" +#import "XMPPMessage.h" +#import "NSXMLElement+XMPP.h" +#import "NSXMLElement+XEP_0297.h" + +static XMPPMessageContextStringItemTag const XMPPMessageContextMAMArchiveIDTag = @"XMPPMessageContextMAMArchiveID"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextMAMPartialResultPageTimestampTag = @"XMPPMessageContextMAMPartialResultPageTimestamp"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextMAMCompleteResultPageTimestampTag = @"XMPPMessageContextMAMCompleteResultPageTimestamp"; +static XMPPMessageContextTimestampItemTag const XMPPMessageContextMAMDeletedResultItemTimestampTag = @"XMPPMessageContextMAMDeletedResultItemTimestamp"; + +@implementation XMPPMessageCoreDataStorage (XEP_0313) + +- (void)finalizeResultSetPageWithMessageArchiveIDs:(NSArray *)archiveIDs +{ + if (archiveIDs.count == 0) { + return; + } + + [self scheduleBlock:^{ + NSFetchRequest *finalizedArchiveIDsFetchRequest = + [XMPPMessageContextStringItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.managedObjectContext]; + + NSPredicate *archiveIDTagPredicate = [XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextMAMArchiveIDTag]; + NSMutableArray *archiveIDSubpredicates = [[NSMutableArray alloc] init]; + for (NSString *archiveID in archiveIDs) { + [archiveIDSubpredicates addObject:[XMPPMessageContextStringItemCoreDataStorageObject stringPredicateWithValue:archiveID]]; + } + finalizedArchiveIDsFetchRequest.predicate = + [NSCompoundPredicate andPredicateWithSubpredicates:@[[NSCompoundPredicate orPredicateWithSubpredicates:archiveIDSubpredicates], + archiveIDTagPredicate]]; + + NSArray *finalizedArchiveIDContextItems = [self.managedObjectContext xmpp_executeForcedSuccessFetchRequest:finalizedArchiveIDsFetchRequest]; + for (XMPPMessageContextStringItemCoreDataStorageObject *archiveIDContextItem in finalizedArchiveIDContextItems) { + XMPPMessageContextCoreDataStorageObject *partialResultContext = + [archiveIDContextItem.message lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement timestampItemValueForTag:XMPPMessageContextMAMPartialResultPageTimestampTag] ? contextElement : nil; + }]; + + if (!partialResultContext) { + continue; + } + + NSDate *partialResultTimestamp = [partialResultContext timestampItemValueForTag:XMPPMessageContextMAMPartialResultPageTimestampTag]; + [partialResultContext removeTimestampItemsWithTag:XMPPMessageContextMAMPartialResultPageTimestampTag]; + [partialResultContext appendTimestampItemWithTag:XMPPMessageContextMAMCompleteResultPageTimestampTag value:partialResultTimestamp]; + } + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTransaction (XEP_0313) + +- (void)storeMessageArchiveQueryResultItem:(NSXMLElement *)resultItem inMode:(XMPPMessageArchiveQueryResultStorageMode)storageMode +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + + if ([[self class] isMessageArchiveQueryResultItem:resultItem alreadyStoredInManagedObjectContext:messageObject.managedObjectContext]) { + [messageObject.managedObjectContext deleteObject:messageObject]; + return; + } + + NSString *resultItemMessageID = [[resultItem forwardedMessage] elementID]; + if (resultItemMessageID + && [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:resultItemMessageID inManagedObjectContext:messageObject.managedObjectContext]) + { + [messageObject.managedObjectContext deleteObject:messageObject]; + return; + } + + [messageObject retireStreamTimestamp]; + + XMPPMessageContextCoreDataStorageObject *messageArchiveContext = [messageObject appendContextElement]; + [messageArchiveContext appendStringItemWithTag:XMPPMessageContextMAMArchiveIDTag value:[resultItem attributeStringValueForName:@"id"]]; + + XMPPMessageContextTimestampItemTag timestampTag = + [resultItem forwardedMessage] ? XMPPMessageContextMAMPartialResultPageTimestampTag : XMPPMessageContextMAMDeletedResultItemTimestampTag; + [messageArchiveContext appendTimestampItemWithTag:timestampTag value:[resultItem forwardedStanzaDelayedDeliveryDate]]; + + if (storageMode == XMPPMessageArchiveQueryResultStorageModeComplete && [resultItem forwardedMessage]) { + [messageObject registerIncomingMessageCore:[resultItem forwardedMessage]]; + } + }]; +} + ++ (BOOL)isMessageArchiveQueryResultItem:(NSXMLElement *)resultItem alreadyStoredInManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + NSFetchRequest *existingArchiveIDFetchRequest = + [XMPPMessageContextStringItemCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:managedObjectContext]; + + NSArray *predicates = @[[XMPPMessageContextStringItemCoreDataStorageObject stringPredicateWithValue:[resultItem attributeStringValueForName:@"id"]], + [XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextMAMArchiveIDTag]]; + existingArchiveIDFetchRequest.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; + + NSArray *existingArchiveIDResult = + [managedObjectContext xmpp_executeForcedSuccessFetchRequest:existingArchiveIDFetchRequest]; + if (existingArchiveIDResult.count != 0) { + NSAssert(existingArchiveIDResult.count == 1, @"Expected a single message matching the given archive ID"); + NSAssert([[existingArchiveIDResult.firstObject.message messageArchiveDate] isEqualToDate:[resultItem forwardedStanzaDelayedDeliveryDate]], + @"The timestamp on an existing message does not match"); + return YES; + } else { + return NO; + } +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XEP_0313) + +- (NSString *)messageArchiveID +{ + return [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement stringItemValueForTag:XMPPMessageContextMAMArchiveIDTag]; + }]; +} + +- (NSDate *)messageArchiveDate +{ + NSArray *archiveTimestampTags = @[XMPPMessageContextMAMPartialResultPageTimestampTag, + XMPPMessageContextMAMCompleteResultPageTimestampTag, + XMPPMessageContextMAMDeletedResultItemTimestampTag]; + + for (XMPPMessageContextTimestampItemTag archiveTimestampTag in archiveTimestampTags) { + NSDate *archiveTimestamp = [self lookupInContextWithBlock:^id _Nullable(XMPPMessageContextCoreDataStorageObject * _Nonnull contextElement) { + return [contextElement timestampItemValueForTag:archiveTimestampTag]; + }]; + + if (archiveTimestamp) { + return archiveTimestamp; + } + } + + return nil; +} + +- (BOOL)isMyArchivedChatMessage +{ + return self.type == XMPPMessageTypeChat && [self messageArchiveID] && [self.fromJID isEqualToJID:[self streamJID] options:XMPPJIDCompareBare]; +} + +@end + +@implementation XMPPMessageContextItemCoreDataStorageObject (XEP_0313) + ++ (NSPredicate *)messageArchiveTimestampKindPredicateWithOptions:(XMPPMessageArchiveTimestampContextOptions)options +{ + NSMutableArray *subpredicates = [[NSMutableArray alloc] init]; + [subpredicates addObject:[XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextMAMCompleteResultPageTimestampTag]]; + + if (options & XMPPMessageArchiveTimestampContextIncludingPartialResultPages) { + [subpredicates addObject:[XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextMAMPartialResultPageTimestampTag]]; + } + + if (options & XMPPMessageArchiveTimestampContextIncludingDeletedResultItems) { + [subpredicates addObject:[XMPPMessageContextStringItemCoreDataStorageObject tagPredicateWithValue:XMPPMessageContextMAMDeletedResultItemTimestampTag]]; + } + + return [NSCompoundPredicate orPredicateWithSubpredicates:subpredicates]; +} + +@end diff --git a/Extensions/XMPPMUCLight/XMPPMUCLight.h b/Extensions/XMPPMUCLight/XMPPMUCLight.h index 66d4ef89b2..b2206d6850 100644 --- a/Extensions/XMPPMUCLight/XMPPMUCLight.h +++ b/Extensions/XMPPMUCLight/XMPPMUCLight.h @@ -53,7 +53,7 @@ - (void)xmppMUCLight:(nonnull XMPPMUCLight *)sender didDiscoverRooms:(nonnull NSArray<__kindof NSXMLElement*>*)rooms forServiceNamed:(nonnull NSString *)serviceName; - (void)xmppMUCLight:(nonnull XMPPMUCLight *)sender failedToDiscoverRoomsForServiceNamed:(nonnull NSString *)serviceName withError:(nonnull NSError *)error; -- (void)xmppMUCLight:(nonnull XMPPMUCLight *)sender changedAffiliation:(nonnull NSString *)affiliation roomJID:(nonnull XMPPJID *)roomJID; +- (void)xmppMUCLight:(nonnull XMPPMUCLight *)sender changedAffiliation:(nonnull NSString *)affiliation userJID:(nonnull XMPPJID *)userJID roomJID:(nonnull XMPPJID *)roomJID; - (void)xmppMUCLight:(nonnull XMPPMUCLight *)sender didRequestBlockingList:(nonnull NSArray*)items forServiceNamed:(nonnull NSString *)serviceName; - (void)xmppMUCLight:(nonnull XMPPMUCLight *)sender failedToRequestBlockingList:(nonnull NSString *)serviceName withError:(nonnull XMPPIQ *)iq; diff --git a/Extensions/XMPPMUCLight/XMPPMUCLight.m b/Extensions/XMPPMUCLight/XMPPMUCLight.m index 57b214121e..4e8a05df87 100644 --- a/Extensions/XMPPMUCLight/XMPPMUCLight.m +++ b/Extensions/XMPPMUCLight/XMPPMUCLight.m @@ -241,12 +241,11 @@ - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message XMPPJID *from = message.from; NSXMLElement *x = [message elementForName:@"x" xmlns:XMPPRoomLightAffiliations]; - NSXMLElement *user = [x elementForName:@"user"]; - NSString *affiliation = [user attributeForName:@"affiliation"].stringValue; - - if (affiliation) { - [multicastDelegate xmppMUCLight:self changedAffiliation:affiliation roomJID:from]; - } + for (NSXMLElement *user in [x elementsForName:@"user"]) { + NSString *affiliation = [user attributeForName:@"affiliation"].stringValue; + XMPPJID *userJID = [XMPPJID jidWithString:user.stringValue]; + [multicastDelegate xmppMUCLight:self changedAffiliation:affiliation userJID:userJID roomJID:from]; + } } - (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module { diff --git a/Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.h b/Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.h new file mode 100644 index 0000000000..2620995b42 --- /dev/null +++ b/Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.h @@ -0,0 +1,28 @@ +#import "XMPPMessageCoreDataStorage.h" +#import "XMPPMessageCoreDataStorageObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XMPPMessageCoreDataStorageTransaction (XMPPMUCLight) + +/// Stores core XMPP properties for the received MUC Light message. +- (void)storeReceivedRoomLightMessage:(XMPPMessage *)message; + +/// Registers outgoing stream event information for the chat message processed in the transaction. +- (void)registerSentRoomLightMessage; + +@end + +@interface XMPPMessageCoreDataStorageObject (XMPPMUCLight) + +/** + Returns YES for incoming messages where the MUC Light occupant associated with the message matches the stream JID. + + A single user may have several clients in the same MUC Light room. The messages broadcasted from "sibling" resources will appear as incoming; + an application may use this method to detect such messages and treat them as if they were outgoing. + */ +- (BOOL)isMyIncomingRoomLightMessage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.m b/Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.m new file mode 100644 index 0000000000..9b8d3c1f36 --- /dev/null +++ b/Extensions/XMPPMUCLight/XMPPMessageCoreDataStorage/XMPPMessageCoreDataStorage+XMPPMUCLight.m @@ -0,0 +1,53 @@ +#import "XMPPMessageCoreDataStorage+XMPPMUCLight.h" +#import "XMPPMessageCoreDataStorageObject+Protected.h" +#import "XMPPMessage.h" + +@implementation XMPPMessageCoreDataStorageTransaction (XMPPMUCLight) + +- (void)storeReceivedRoomLightMessage:(XMPPMessage *)message +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionIncoming, @"This action is only allowed for incoming message objects"); + + if (![[self class] isEchoedRoomLightMessage:message inManagedObjectContext:messageObject.managedObjectContext]) { + [messageObject registerIncomingMessageCore:message]; + } else { + [messageObject.managedObjectContext deleteObject:messageObject]; + } + }]; +} + +- (void)registerSentRoomLightMessage +{ + [self scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + NSAssert(messageObject.direction == XMPPMessageDirectionOutgoing, @"This action is only allowed for outgoing message objects"); + // No additional processing required + }]; +} + ++ (BOOL)isEchoedRoomLightMessage:(XMPPMessage *)message inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext +{ + if (![message elementID]) { + return NO; + } + + XMPPMessageCoreDataStorageObject *sentMessageStorageObject = [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:[message elementID] + inManagedObjectContext:managedObjectContext]; + return sentMessageStorageObject && sentMessageStorageObject.direction == XMPPMessageDirectionOutgoing; +} + +@end + +@implementation XMPPMessageCoreDataStorageObject (XMPPMUCLight) + +- (BOOL)isMyIncomingRoomLightMessage +{ + if (self.type != XMPPMessageTypeGroupchat || self.direction != XMPPMessageDirectionIncoming) { + return NO; + } + + NSString *roomLightUserString = self.fromJID.resource; + return roomLightUserString && [[XMPPJID jidWithString:roomLightUserString] isEqualToJID:[self streamJID] options:XMPPJIDCompareBare]; +} + +@end diff --git a/Extensions/XMPPMUCLight/XMPPRoomLight.h b/Extensions/XMPPMUCLight/XMPPRoomLight.h index 95229d6685..bc763b9150 100644 --- a/Extensions/XMPPMUCLight/XMPPRoomLight.h +++ b/Extensions/XMPPMUCLight/XMPPRoomLight.h @@ -22,6 +22,7 @@ @property (readonly, nonatomic, strong, nonnull) XMPPJID *roomJID; @property (readonly, nonatomic, strong, nonnull) NSString *domain; @property (nonatomic, assign) BOOL shouldStoreAffiliationChangeMessages; +@property (assign) BOOL shouldHandleMemberMessagesWithoutBody; - (nonnull NSString *)roomname; - (nonnull NSString *)subject; diff --git a/Extensions/XMPPMUCLight/XMPPRoomLight.m b/Extensions/XMPPMUCLight/XMPPRoomLight.m index c12f527380..ab9183bf4d 100644 --- a/Extensions/XMPPMUCLight/XMPPRoomLight.m +++ b/Extensions/XMPPMUCLight/XMPPRoomLight.m @@ -15,6 +15,7 @@ @interface XMPPRoomLight() { BOOL shouldStoreAffiliationChangeMessages; + BOOL shouldHandleMemberMessagesWithoutBody; NSString *roomname; NSString *subject; NSArray *knownMembersList; @@ -105,6 +106,33 @@ - (void)setShouldStoreAffiliationChangeMessages:(BOOL)newValue dispatch_async(moduleQueue, block); } +- (BOOL)shouldHandleMemberMessagesWithoutBody +{ + __block BOOL result; + dispatch_block_t block = ^{ @autoreleasepool { + result = shouldHandleMemberMessagesWithoutBody; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setShouldHandleMemberMessagesWithoutBody:(BOOL)newValue +{ + dispatch_block_t block = ^{ @autoreleasepool { + shouldHandleMemberMessagesWithoutBody = newValue; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + - (nonnull NSString *)roomname { @synchronized(roomname) { return [roomname copy]; @@ -668,9 +696,9 @@ - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message // Is this a message we need to store (a chat message)? // // We store messages that from is full room-id@domain/user-who-sends-message - // and that have something in the body + // and that have something in the body (unless empty messages are allowed) - if ([from isFull] && [message isGroupChatMessageWithBody]) { + if ([from isFull] && [message isGroupChatMessage] && (self.shouldHandleMemberMessagesWithoutBody || [message isMessageWithBody])) { [xmppRoomLightStorage handleIncomingMessage:message room:self]; [multicastDelegate xmppRoomLight:self didReceiveMessage:message]; }else if(destroyRoom){ @@ -700,7 +728,7 @@ - (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message // A message to all recipients MUST be of type groupchat. // A message to an individual recipient would have a . - if ([message isGroupChatMessageWithBody]){ + if ([message isGroupChatMessage] && (self.shouldHandleMemberMessagesWithoutBody || [message isMessageWithBody])) { [xmppRoomLightStorage handleOutgoingMessage:message room:self]; } } diff --git a/Utilities/GCDMulticastDelegate.h b/Utilities/GCDMulticastDelegate.h index 3c48a7ee72..18ae32677d 100644 --- a/Utilities/GCDMulticastDelegate.h +++ b/Utilities/GCDMulticastDelegate.h @@ -1,6 +1,6 @@ #import -@class GCDMulticastDelegateEnumerator; +@class GCDMulticastDelegateEnumerator, GCDMulticastDelegateInvocationContext; /** * This class provides multicast delegate functionality. That is: @@ -54,5 +54,44 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr ofClass:(Class)aClass; - (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr forSelector:(SEL)aSelector; +@end + +/** + * A helper class for propagating custom context across multicast delegate invocations. + * + * This class serves 2 main purposes: + * - provides an auxiliary path of custom data delivery to the invoked delegate methods + * - makes it possible to track the delegate method invocation completion + * + * The context propagates along the cascade of invocations, i.e. when a delegate method calls another multicast delegate, + * that subsequent invocation belongs to the same context. This is particularly relevant w.r.t. the xmpp framework + * architecture where a scenario with two layers of delegation is common: stream -> module and module -> application. + * The propagating context is what enables the framework to deliver stream event-related information across modules + * to the application callbacks. + * + * The default context propagation junctions are invocation forwarding and delegate enumerator creation. As long as + * they are executed under an existing context, the propagation is automatic. + * + * A manual propagation scenario (e.g. asynchronous message processing within a module) would consist of the following steps: + * 1. Capturing the context object while still in the delegate callback context with @c currentContext. + * 2. Entering the captured context's @c continuityGroup. + * 3. Restoring the context on an arbitrary queue with @c becomeCurrentOnQueue:forActionWithBlock: + * 4. Leaving the @c continuityGroup within the action block submitted to @c becomeCurrentOnQueue:forActionWithBlock: + * + * Steps 2. and 4. are only required if @c becomeCurrentOnQueue:forActionWithBlock: itself is invoked asynchronously + * (e.g. in a network or disk IO completion callback). + */ +@interface GCDMulticastDelegateInvocationContext : NSObject + +@property (nonatomic, strong, readonly) id value; +@property (nonatomic, strong, readonly) dispatch_group_t continuityGroup; + ++ (instancetype)currentContext; + +- (instancetype)initWithValue:(id)value; +- (instancetype)init NS_UNAVAILABLE; + +- (void)becomeCurrentOnQueue:(dispatch_queue_t)queue forActionWithBlock:(dispatch_block_t)block; + @end NS_ASSUME_NONNULL_END diff --git a/Utilities/GCDMulticastDelegate.m b/Utilities/GCDMulticastDelegate.m index 1785c274d4..4d9f5f2ae0 100644 --- a/Utilities/GCDMulticastDelegate.m +++ b/Utilities/GCDMulticastDelegate.m @@ -9,6 +9,10 @@ #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif +static void * const GCDMulticastDelegateInvocationContextKey = (void *)&GCDMulticastDelegateInvocationContextKey; + +static void GCDMulticastDelegateInvocationContextLeave(void *contextPtr); + /** * How does this class work? * @@ -76,6 +80,12 @@ - (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes; @end +@interface GCDMulticastDelegateInvocationContext () + +- (dispatch_queue_t)transferContextToTargetQueue:(dispatch_queue_t)targetQueue; + +@end + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -227,7 +237,22 @@ - (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector - (GCDMulticastDelegateEnumerator *)delegateEnumerator { - return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes]; + NSMutableArray *actualDelegateNodes; + + GCDMulticastDelegateInvocationContext *currentInvocationContext = [GCDMulticastDelegateInvocationContext currentContext]; + if (currentInvocationContext) { + actualDelegateNodes = [[NSMutableArray alloc] init]; + for (GCDMulticastDelegateNode *node in delegateNodes) { + dispatch_queue_t contextTransferQueue = [currentInvocationContext transferContextToTargetQueue:node.delegateQueue]; + GCDMulticastDelegateNode *contextTransferNode = [[GCDMulticastDelegateNode alloc] initWithDelegate:node.delegate + delegateQueue:contextTransferQueue]; + [actualDelegateNodes addObject:contextTransferNode]; + } + } else { + actualDelegateNodes = delegateNodes; + } + + return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:actualDelegateNodes]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector @@ -276,7 +301,10 @@ - (void)forwardInvocation:(NSInvocation *)origInvocation NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; - dispatch_async(node.delegateQueue, ^{ @autoreleasepool { + GCDMulticastDelegateInvocationContext *currentContext = [GCDMulticastDelegateInvocationContext currentContext]; + dispatch_queue_t invocationQueue = [currentContext transferContextToTargetQueue:node.delegateQueue] ?: node.delegateQueue; + + dispatch_async(invocationQueue, ^{ @autoreleasepool { [dupInvocation invokeWithTarget:nodeDelegate]; @@ -652,3 +680,47 @@ - (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr for } @end + +@implementation GCDMulticastDelegateInvocationContext + ++ (instancetype)currentContext +{ + void *contextPtr = dispatch_get_specific(GCDMulticastDelegateInvocationContextKey); + return contextPtr ? CFBridgingRelease(CFRetain(contextPtr)) : nil; +} + +- (instancetype)initWithValue:(id)value +{ + self = [super init]; + if (self) { + _continuityGroup = dispatch_group_create(); + _value = value; + } + return self; +} + +- (void)becomeCurrentOnQueue:(dispatch_queue_t)queue forActionWithBlock:(dispatch_block_t)block +{ + dispatch_async([self transferContextToTargetQueue:queue], block); +} + +- (dispatch_queue_t)transferContextToTargetQueue:(dispatch_queue_t)targetQueue +{ + dispatch_group_enter(self.continuityGroup); + + dispatch_queue_t contextTransferQueue = dispatch_queue_create_with_target("GCDMulticastDelegateInvocationContext.contextTransferQueue", nil, targetQueue); + dispatch_queue_set_specific(contextTransferQueue, + GCDMulticastDelegateInvocationContextKey, + (void *)CFBridgingRetain(self), + GCDMulticastDelegateInvocationContextLeave); + + return contextTransferQueue; +} + +@end + +static void GCDMulticastDelegateInvocationContextLeave(void *contextPtr) +{ + GCDMulticastDelegateInvocationContext *context = CFBridgingRelease(contextPtr); + dispatch_group_leave(context.continuityGroup); +} diff --git a/XMPPFramework.xcodeproj/project.pbxproj b/XMPPFramework.xcodeproj/project.pbxproj index a0b10483fd..2bd77778ce 100644 --- a/XMPPFramework.xcodeproj/project.pbxproj +++ b/XMPPFramework.xcodeproj/project.pbxproj @@ -914,6 +914,66 @@ D9DCD6A21E6259970010D1C7 /* KissXML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DCD3841E6250CE0010D1C7 /* KissXML.framework */; }; D9DCD6A31E6259970010D1C7 /* libidn.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DCD3951E6250D20010D1C7 /* libidn.framework */; }; D9DCD6C01E625B4D0010D1C7 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DCD6BF1E625B4D0010D1C7 /* AppKit.framework */; }; + DD1C59831F4429FD003D73DB /* XMPPDelayedDelivery.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1C59811F4429FD003D73DB /* XMPPDelayedDelivery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59841F4429FD003D73DB /* XMPPDelayedDelivery.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1C59811F4429FD003D73DB /* XMPPDelayedDelivery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59851F4429FD003D73DB /* XMPPDelayedDelivery.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1C59811F4429FD003D73DB /* XMPPDelayedDelivery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59861F4429FD003D73DB /* XMPPDelayedDelivery.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1C59821F4429FD003D73DB /* XMPPDelayedDelivery.m */; }; + DD1C59871F4429FD003D73DB /* XMPPDelayedDelivery.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1C59821F4429FD003D73DB /* XMPPDelayedDelivery.m */; }; + DD1C59881F4429FD003D73DB /* XMPPDelayedDelivery.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1C59821F4429FD003D73DB /* XMPPDelayedDelivery.m */; }; + DD06EA4A1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h in Headers */ = {isa = PBXBuildFile; fileRef = DD06EA481F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD06EA4B1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h in Headers */ = {isa = PBXBuildFile; fileRef = DD06EA481F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD06EA4C1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h in Headers */ = {isa = PBXBuildFile; fileRef = DD06EA481F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD06EA4D1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA491F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m */; }; + DD06EA4E1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA491F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m */; }; + DD06EA4F1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA491F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m */; }; + DD1784121F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */; }; + DD1784131F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */; }; + DD1784141F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */; }; + DD1784151F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784161F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784171F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784181F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */; }; + DD1784191F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */; }; + DD17841A1F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */; }; + DD17841B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD17841C1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD17841D1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD17841E1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */; }; + DD17841F1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */; }; + DD1784201F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */; }; + DD1784211F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784221F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784231F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1784241F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */; }; + DD1784251F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */; }; + DD1784261F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */; }; + DD19E4011F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4021F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4031F8CA02100CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4041F8CA06D00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4051F8CA06E00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4061F8CA06F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E40D1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E40E1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E40F1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD19E4101F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */; }; + DD19E4111F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */; }; + DD19E4121F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */; }; + DD1E12301F5EE6100012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E12311F5EE6110012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E12321F5EE6120012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59A71F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1C59A51F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59A81F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1C59A51F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59A91F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1C59A51F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1C59AA1F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1C59A61F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m */; }; + DD1C59AB1F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1C59A61F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m */; }; + DD1C59AC1F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1C59A61F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m */; }; + DD203B941F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h in Headers */ = {isa = PBXBuildFile; fileRef = DD203B921F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD203B951F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h in Headers */ = {isa = PBXBuildFile; fileRef = DD203B921F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD203B961F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h in Headers */ = {isa = PBXBuildFile; fileRef = DD203B921F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD203B971F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B931F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m */; }; + DD203B981F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B931F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m */; }; + DD203B991F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B931F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m */; }; DD1E73331ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E73341ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E73351ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -923,6 +983,84 @@ DD1E733A1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E733B1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD1E733C1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F5701F7CD9B500F54F18 /* XMPPOneToOneChat.h in Headers */ = {isa = PBXBuildFile; fileRef = DD26F56E1F7CD9B500F54F18 /* XMPPOneToOneChat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F5711F7CD9B500F54F18 /* XMPPOneToOneChat.h in Headers */ = {isa = PBXBuildFile; fileRef = DD26F56E1F7CD9B500F54F18 /* XMPPOneToOneChat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F5721F7CD9B500F54F18 /* XMPPOneToOneChat.h in Headers */ = {isa = PBXBuildFile; fileRef = DD26F56E1F7CD9B500F54F18 /* XMPPOneToOneChat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F5731F7CD9B500F54F18 /* XMPPOneToOneChat.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F56F1F7CD9B500F54F18 /* XMPPOneToOneChat.m */; }; + DD26F5741F7CD9B500F54F18 /* XMPPOneToOneChat.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F56F1F7CD9B500F54F18 /* XMPPOneToOneChat.m */; }; + DD26F5751F7CD9B500F54F18 /* XMPPOneToOneChat.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F56F1F7CD9B500F54F18 /* XMPPOneToOneChat.m */; }; + DD2AD6E81F84B49200E0FED2 /* XMPPManagedMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2AD6DD1F84B49200E0FED2 /* XMPPManagedMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2AD6E91F84B49200E0FED2 /* XMPPManagedMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2AD6DD1F84B49200E0FED2 /* XMPPManagedMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2AD6EA1F84B49200E0FED2 /* XMPPManagedMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2AD6DD1F84B49200E0FED2 /* XMPPManagedMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2AD6EB1F84B49200E0FED2 /* XMPPManagedMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD6DE1F84B49200E0FED2 /* XMPPManagedMessaging.m */; }; + DD2AD6EC1F84B49200E0FED2 /* XMPPManagedMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD6DE1F84B49200E0FED2 /* XMPPManagedMessaging.m */; }; + DD2AD6ED1F84B49200E0FED2 /* XMPPManagedMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD6DE1F84B49200E0FED2 /* XMPPManagedMessaging.m */; }; + DD855F911F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = DD855F8F1F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD855F921F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = DD855F8F1F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD855F931F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h in Headers */ = {isa = PBXBuildFile; fileRef = DD855F8F1F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD855F941F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = DD855F901F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m */; }; + DD855F951F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = DD855F901F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m */; }; + DD855F961F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m in Sources */ = {isa = PBXBuildFile; fileRef = DD855F901F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m */; }; + DDA938801F790FAC00979230 /* XMPPLastMessageCorrection.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA9387E1F790FAC00979230 /* XMPPLastMessageCorrection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA938811F790FAC00979230 /* XMPPLastMessageCorrection.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA9387E1F790FAC00979230 /* XMPPLastMessageCorrection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA938821F790FAC00979230 /* XMPPLastMessageCorrection.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA9387E1F790FAC00979230 /* XMPPLastMessageCorrection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA938831F790FAC00979230 /* XMPPLastMessageCorrection.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA9387F1F790FAC00979230 /* XMPPLastMessageCorrection.m */; }; + DDA938841F790FAC00979230 /* XMPPLastMessageCorrection.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA9387F1F790FAC00979230 /* XMPPLastMessageCorrection.m */; }; + DDA938851F790FAC00979230 /* XMPPLastMessageCorrection.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA9387F1F790FAC00979230 /* XMPPLastMessageCorrection.m */; }; + DDA938881F7913D100979230 /* XMPPCapabilities+XEP_0308.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA938861F7913D100979230 /* XMPPCapabilities+XEP_0308.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA938891F7913D100979230 /* XMPPCapabilities+XEP_0308.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA938861F7913D100979230 /* XMPPCapabilities+XEP_0308.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA9388A1F7913D100979230 /* XMPPCapabilities+XEP_0308.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA938861F7913D100979230 /* XMPPCapabilities+XEP_0308.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA9388B1F7913D100979230 /* XMPPCapabilities+XEP_0308.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA938871F7913D100979230 /* XMPPCapabilities+XEP_0308.m */; }; + DDA9388C1F7913D100979230 /* XMPPCapabilities+XEP_0308.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA938871F7913D100979230 /* XMPPCapabilities+XEP_0308.m */; }; + DDA9388D1F7913D100979230 /* XMPPCapabilities+XEP_0308.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA938871F7913D100979230 /* XMPPCapabilities+XEP_0308.m */; }; + DD1E80671F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E80681F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E80691F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */ = {isa = PBXBuildFile; fileRef = DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD1E806A1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */; }; + DD1E806B1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */; }; + DD1E806C1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */; }; + DD26F58D1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h in Headers */ = {isa = PBXBuildFile; fileRef = DD26F58B1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F58E1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h in Headers */ = {isa = PBXBuildFile; fileRef = DD26F58B1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F58F1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h in Headers */ = {isa = PBXBuildFile; fileRef = DD26F58B1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD26F5901F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F58C1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m */; }; + DD26F5911F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F58C1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m */; }; + DD26F5921F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F58C1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m */; }; + DD8924E51F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8924E31F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD8924E61F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8924E31F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD8924E71F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8924E31F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD8924E81F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8924E41F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m */; }; + DD8924E91F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8924E41F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m */; }; + DD8924EA1F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8924E41F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m */; }; + DD40042B1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h in Headers */ = {isa = PBXBuildFile; fileRef = DD4004291F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD40042C1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h in Headers */ = {isa = PBXBuildFile; fileRef = DD4004291F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD40042D1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h in Headers */ = {isa = PBXBuildFile; fileRef = DD4004291F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD40042E1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m in Sources */ = {isa = PBXBuildFile; fileRef = DD40042A1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m */; }; + DD40042F1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m in Sources */ = {isa = PBXBuildFile; fileRef = DD40042A1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m */; }; + DD4004301F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m in Sources */ = {isa = PBXBuildFile; fileRef = DD40042A1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m */; }; + DDA11A5D1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA11A5B1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA11A5E1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA11A5B1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA11A5F1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h in Headers */ = {isa = PBXBuildFile; fileRef = DDA11A5B1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DDA11A601F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A5C1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m */; }; + DDA11A611F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A5C1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m */; }; + DDA11A621F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A5C1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m */; }; + DD8924D81F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8924D61F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD8924D91F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8924D61F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD8924DA1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h in Headers */ = {isa = PBXBuildFile; fileRef = DD8924D61F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD8924DB1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8924D71F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m */; }; + DD8924DC1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8924D71F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m */; }; + DD8924DD1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8924D71F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m */; }; + DD2147151F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2147131F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2147161F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2147131F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2147171F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h in Headers */ = {isa = PBXBuildFile; fileRef = DD2147131F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD2147181F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2147141F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m */; }; + DD2147191F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2147141F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m */; }; + DD21471A1F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2147141F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m */; }; + DDFFF40A1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */; }; + DDFFF40B1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */; }; + DDFFF40C1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */; }; + DDFFF40D1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */; }; + DDFFF40E1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */; }; + DDFFF40F1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1511,9 +1649,55 @@ D9DCD5331E6256D90010D1C7 /* XMPPFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = XMPPFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D9DCD6961E6258CF0010D1C7 /* XMPPFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = XMPPFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D9DCD6BF1E625B4D0010D1C7 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/System/Library/Frameworks/AppKit.framework; sourceTree = DEVELOPER_DIR; }; + DD1C59811F4429FD003D73DB /* XMPPDelayedDelivery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPDelayedDelivery.h; sourceTree = ""; }; + DD1C59821F4429FD003D73DB /* XMPPDelayedDelivery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPDelayedDelivery.m; sourceTree = ""; }; + DD06EA481F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0184.h"; sourceTree = ""; }; + DD06EA491F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0184.m"; sourceTree = ""; }; + DD1784071F3C9FA800D662A6 /* XMPPMessage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = XMPPMessage.xcdatamodel; sourceTree = ""; }; + DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageCoreDataStorageObject.h; sourceTree = ""; }; + DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageCoreDataStorageObject.m; sourceTree = ""; }; + DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageContextCoreDataStorageObject.h; sourceTree = ""; }; + DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageContextCoreDataStorageObject.m; sourceTree = ""; }; + DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageCoreDataStorage.h; sourceTree = ""; }; + DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageCoreDataStorage.m; sourceTree = ""; }; + DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageContextCoreDataStorageObject+Protected.h"; sourceTree = ""; }; + DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageContextItemCoreDataStorageObject+Protected.h"; sourceTree = ""; }; + DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorageObject+ContextHelpers.h"; sourceTree = ""; }; + DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorageObject+ContextHelpers.m"; sourceTree = ""; }; + DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorageObject+Protected.h"; sourceTree = ""; }; + DD1C59A51F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0203.h"; sourceTree = ""; }; + DD1C59A61F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0203.m"; sourceTree = ""; }; + DD203B921F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0308.h"; sourceTree = ""; }; + DD203B931F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0308.m"; sourceTree = ""; }; DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XMPPRoomLightCoreDataStorage+XEP_0313.h"; sourceTree = ""; }; DD1E73321ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XMPPRoomLightCoreDataStorage+XEP_0313.m"; sourceTree = ""; }; DD1E73391ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPRoomLightCoreDataStorageProtected.h; sourceTree = ""; }; + DD26F56E1F7CD9B500F54F18 /* XMPPOneToOneChat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XMPPOneToOneChat.h; sourceTree = ""; }; + DD26F56F1F7CD9B500F54F18 /* XMPPOneToOneChat.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XMPPOneToOneChat.m; sourceTree = ""; }; + DD2AD6DD1F84B49200E0FED2 /* XMPPManagedMessaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPManagedMessaging.h; sourceTree = ""; }; + DD2AD6DE1F84B49200E0FED2 /* XMPPManagedMessaging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPManagedMessaging.m; sourceTree = ""; }; + DD855F8F1F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XMPPOutOfBandResourceMessaging.h; sourceTree = ""; }; + DD855F901F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XMPPOutOfBandResourceMessaging.m; sourceTree = ""; }; + DDA9387E1F790FAC00979230 /* XMPPLastMessageCorrection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XMPPLastMessageCorrection.h; sourceTree = ""; }; + DDA9387F1F790FAC00979230 /* XMPPLastMessageCorrection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XMPPLastMessageCorrection.m; sourceTree = ""; }; + DDA938861F7913D100979230 /* XMPPCapabilities+XEP_0308.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPCapabilities+XEP_0308.h"; sourceTree = ""; }; + DDA938871F7913D100979230 /* XMPPCapabilities+XEP_0308.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPCapabilities+XEP_0308.m"; sourceTree = ""; }; + DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XMPPMessageContextItemCoreDataStorageObject.h; sourceTree = ""; }; + DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageContextItemCoreDataStorageObject.m; sourceTree = ""; }; + DD26F58B1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XMPPOneToOneChat.h"; sourceTree = ""; }; + DD26F58C1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XMPPOneToOneChat.m"; sourceTree = ""; }; + DD8924E31F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XMPPMUCLight.h"; sourceTree = ""; }; + DD8924E41F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XMPPMUCLight.m"; sourceTree = ""; }; + DD4004291F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0066.h"; sourceTree = ""; }; + DD40042A1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0066.m"; sourceTree = ""; }; + DDA11A5B1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0198.h"; sourceTree = ""; }; + DDA11A5C1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0198.m"; sourceTree = ""; }; + DD8924D61F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0245.h"; sourceTree = ""; }; + DD8924D71F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0245.m"; sourceTree = ""; }; + DD2147131F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XMPPMessageCoreDataStorage+XEP_0313.h"; sourceTree = ""; }; + DD2147141F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XMPPMessageCoreDataStorage+XEP_0313.m"; sourceTree = ""; }; + DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+XMPPCoreDataStorage.h"; sourceTree = ""; }; + DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+XMPPCoreDataStorage.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1757,6 +1941,8 @@ D9DCD1431E6250930010D1C7 /* Reconnect */, D9DCD1461E6250930010D1C7 /* Roster */, D9DCD15E1E6250930010D1C7 /* SystemInputActivityMonitor */, + DD26F56D1F7CD99100F54F18 /* OneToOneChat */, + DD1784051F3C9FA800D662A6 /* MessageStorage */, D9DCD1611E6250930010D1C7 /* XEP-0009 */, D9DCD1681E6250930010D1C7 /* XEP-0012 */, D9DCD16D1E6250930010D1C7 /* XEP-0016 */, @@ -1785,6 +1971,7 @@ D9DCD2131E6250930010D1C7 /* XEP-0203 */, D9DCD2161E6250930010D1C7 /* XEP-0223 */, D9DCD2191E6250930010D1C7 /* XEP-0224 */, + DD8924D51F7AA29900E7D917 /* XEP-0245 */, D9DCD21E1E6250930010D1C7 /* XEP-0280 */, D9DCD2231E6250930010D1C7 /* XEP-0297 */, D9DCD2261E6250930010D1C7 /* XEP-0308 */, @@ -1815,6 +2002,8 @@ D9DCD1221E6250920010D1C7 /* XMPPCoreDataStorage.h */, D9DCD1231E6250920010D1C7 /* XMPPCoreDataStorage.m */, D9DCD1241E6250920010D1C7 /* XMPPCoreDataStorageProtected.h */, + DDFFF4081F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h */, + DDFFF4091F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m */, ); path = CoreDataStorage; sourceTree = ""; @@ -2107,6 +2296,10 @@ D9DCD1BF1E6250930010D1C7 /* XMPPIQ+XEP_0066.m */, D9DCD1C01E6250930010D1C7 /* XMPPMessage+XEP_0066.h */, D9DCD1C11E6250930010D1C7 /* XMPPMessage+XEP_0066.m */, + DD855F8F1F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h */, + DD855F901F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m */, + DD4004291F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h */, + DD40042A1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m */, ); path = "XEP-0066"; sourceTree = ""; @@ -2251,6 +2444,8 @@ D9DCD1FA1E6250930010D1C7 /* XMPPMessage+XEP_0184.m */, D9DCD1FB1E6250930010D1C7 /* XMPPMessageDeliveryReceipts.h */, D9DCD1FC1E6250930010D1C7 /* XMPPMessageDeliveryReceipts.m */, + DD06EA481F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h */, + DD06EA491F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m */, ); path = "XEP-0184"; sourceTree = ""; @@ -2267,6 +2462,7 @@ D9DCD2001E6250930010D1C7 /* XEP-0198 */ = { isa = PBXGroup; children = ( + DD2AD6D91F84B49200E0FED2 /* Managed Messaging */, D9DCD2011E6250930010D1C7 /* Memory Storage */, D9DCD2041E6250930010D1C7 /* Private */, D9DCD2071E6250930010D1C7 /* XMPPStreamManagement.h */, @@ -2318,8 +2514,12 @@ D9DCD2131E6250930010D1C7 /* XEP-0203 */ = { isa = PBXGroup; children = ( + DD1C59811F4429FD003D73DB /* XMPPDelayedDelivery.h */, + DD1C59821F4429FD003D73DB /* XMPPDelayedDelivery.m */, D9DCD2141E6250930010D1C7 /* NSXMLElement+XEP_0203.h */, D9DCD2151E6250930010D1C7 /* NSXMLElement+XEP_0203.m */, + DD1C59A51F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h */, + DD1C59A61F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m */, ); path = "XEP-0203"; sourceTree = ""; @@ -2369,6 +2569,12 @@ children = ( D9DCD2271E6250930010D1C7 /* XMPPMessage+XEP_0308.h */, D9DCD2281E6250930010D1C7 /* XMPPMessage+XEP_0308.m */, + DDA9387E1F790FAC00979230 /* XMPPLastMessageCorrection.h */, + DDA9387F1F790FAC00979230 /* XMPPLastMessageCorrection.m */, + DDA938861F7913D100979230 /* XMPPCapabilities+XEP_0308.h */, + DDA938871F7913D100979230 /* XMPPCapabilities+XEP_0308.m */, + DD203B921F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h */, + DD203B931F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m */, ); path = "XEP-0308"; sourceTree = ""; @@ -2380,6 +2586,8 @@ D9DCD22B1E6250930010D1C7 /* XMPPMessageArchiveManagement.m */, DD1E73311ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.h */, DD1E73321ED885FD009B529B /* XMPPRoomLightCoreDataStorage+XEP_0313.m */, + DD2147131F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h */, + DD2147141F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m */, ); path = "XEP-0313"; sourceTree = ""; @@ -2446,6 +2654,7 @@ isa = PBXGroup; children = ( D9DCD2411E6250930010D1C7 /* CoreDataStorage */, + DD8924E21F7B789C00E7D917 /* XMPPMessageCoreDataStorage */, D9DCD2471E6250930010D1C7 /* XMPPMUCLight.h */, D9DCD2481E6250930010D1C7 /* XMPPMUCLight.m */, D9DCD2491E6250930010D1C7 /* XMPPRoomLight.h */, @@ -2533,6 +2742,67 @@ name = Frameworks; sourceTree = ""; }; + DD26F56D1F7CD99100F54F18 /* OneToOneChat */ = { + isa = PBXGroup; + children = ( + DD26F56E1F7CD9B500F54F18 /* XMPPOneToOneChat.h */, + DD26F56F1F7CD9B500F54F18 /* XMPPOneToOneChat.m */, + DD26F58B1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h */, + DD26F58C1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m */, + ); + path = OneToOneChat; + sourceTree = ""; + }; + DD2AD6D91F84B49200E0FED2 /* Managed Messaging */ = { + isa = PBXGroup; + children = ( + DD2AD6DD1F84B49200E0FED2 /* XMPPManagedMessaging.h */, + DD2AD6DE1F84B49200E0FED2 /* XMPPManagedMessaging.m */, + ); + path = "Managed Messaging"; + sourceTree = ""; + }; + DD1784051F3C9FA800D662A6 /* MessageStorage */ = { + isa = PBXGroup; + children = ( + DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */, + DD17840C1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h */, + DD17840D1F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m */, + DD1784081F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h */, + DD1E122F1F5EE4DA0012A506 /* XMPPMessageCoreDataStorageObject+Protected.h */, + DD1784091F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m */, + DD19E40B1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h */, + DD19E40C1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m */, + DD17840A1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h */, + DD19E3FF1F8C9F0F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h */, + DD17840B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m */, + DD1E80651F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h */, + DD19E4001F8C9FCB00CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h */, + DD1E80661F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m */, + ); + path = MessageStorage; + sourceTree = ""; + }; + DD8924E21F7B789C00E7D917 /* XMPPMessageCoreDataStorage */ = { + isa = PBXGroup; + children = ( + DD8924E31F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h */, + DD8924E41F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m */, + DDA11A5B1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h */, + DDA11A5C1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m */, + ); + path = XMPPMessageCoreDataStorage; + sourceTree = ""; + }; + DD8924D51F7AA29900E7D917 /* XEP-0245 */ = { + isa = PBXGroup; + children = ( + DD8924D61F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h */, + DD8924D71F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m */, + ); + path = "XEP-0245"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2540,7 +2810,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + DD40042B1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h in Headers */, 0D44BB2E1E5370FC000930E0 /* NSXMLElement+XMPP.h in Headers */, + DD8924D81F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h in Headers */, 0D44BB561E537105000930E0 /* XMPPCustomBinding.h in Headers */, 0D44BB571E537105000930E0 /* XMPPSASLAuthentication.h in Headers */, DD1E733A1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */, @@ -2552,6 +2824,7 @@ 0D44BB4A1E537105000930E0 /* XMPPDeprecatedDigestAuthentication.h in Headers */, D9DCD3261E6250930010D1C7 /* NSXMLElement+XEP_0335.h in Headers */, D9DCD2A41E6250930010D1C7 /* XMPPMessage+XEP0045.h in Headers */, + DD855F911F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h in Headers */, D9DCD2EE1E6250930010D1C7 /* XMPPMessageArchiving_Message_CoreDataObject.h in Headers */, D9DCD2791E6250930010D1C7 /* XMPPRosterMemoryStoragePrivate.h in Headers */, D9DCD3241E6250930010D1C7 /* XMPPMessage+XEP_0334.h in Headers */, @@ -2581,21 +2854,29 @@ D9DCD3181E6250930010D1C7 /* XMPPMessage+XEP_0280.h in Headers */, D9DCD2DA1E6250930010D1C7 /* XMPPMessage+XEP_0085.h in Headers */, D9DCD2681E6250930010D1C7 /* XMPPProcessOne.h in Headers */, + DD203B941F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h in Headers */, D9DCD2E51E6250930010D1C7 /* XMPPCapsCoreDataStorageObject.h in Headers */, D9DCD27D1E6250930010D1C7 /* XMPPRoster.h in Headers */, + DDFFF40A1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */, + DD1E80671F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */, D9DCD25E1E6250930010D1C7 /* OMEMOModule.h in Headers */, D9DCD2581E6250930010D1C7 /* NSXMLElement+OMEMO.h in Headers */, D9DCD31C1E6250930010D1C7 /* NSXMLElement+XEP_0297.h in Headers */, D9DCD32A1E6250930010D1C7 /* XMPPIQ+XEP_0357.h in Headers */, D9DCD24D1E6250930010D1C7 /* XMPPCoreDataStorage.h in Headers */, D9DCD24B1E6250930010D1C7 /* XMPPBandwidthMonitor.h in Headers */, + DD2AD6E81F84B49200E0FED2 /* XMPPManagedMessaging.h in Headers */, D9DCD2901E6250930010D1C7 /* XMPPRoomCoreDataStorage.h in Headers */, D9DCD3081E6250930010D1C7 /* XMPPAutoPing.h in Headers */, + DD1784151F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */, D9DCD2F61E6250930010D1C7 /* XMPPvCardAvatarModule.h in Headers */, D9DCD28B1E6250930010D1C7 /* XMPPLastActivity.h in Headers */, + DDA938801F790FAC00979230 /* XMPPLastMessageCorrection.h in Headers */, D9DCD2601E6250930010D1C7 /* OMEMOPreKey.h in Headers */, + DDA938881F7913D100979230 /* XMPPCapabilities+XEP_0308.h in Headers */, D9DCD25A1E6250930010D1C7 /* OMEMOBundle.h in Headers */, D9DCD2A61E6250930010D1C7 /* XMPPMUC.h in Headers */, + DD19E4041F8CA06D00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */, D9DCD27F1E6250930010D1C7 /* XMPPRosterPrivate.h in Headers */, D9DCD2661E6250930010D1C7 /* XMPPMessage+OMEMO.h in Headers */, D9DCD2851E6250930010D1C7 /* XMPPIQ+JabberRPCResonse.h in Headers */, @@ -2614,9 +2895,12 @@ D9DCD2FE1E6250930010D1C7 /* XMPPMessageDeliveryReceipts.h in Headers */, D9DCD2B81E6250930010D1C7 /* XMPPvCardTempAdr.h in Headers */, D9DCD2FC1E6250930010D1C7 /* XMPPMessage+XEP_0184.h in Headers */, + DD8924E51F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h in Headers */, + DD06EA4A1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h in Headers */, D9DCD2C21E6250930010D1C7 /* XMPPvCardTempModule.h in Headers */, D9DCD32E1E6250930010D1C7 /* XMPPSlot.h in Headers */, D9DCD31E1E6250930010D1C7 /* XMPPMessage+XEP_0308.h in Headers */, + DD1C59A71F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h in Headers */, D9DCD2871E6250930010D1C7 /* XMPPJabberRPCModule.h in Headers */, D9DCD3351E6250930010D1C7 /* XMPPMUCLight.h in Headers */, D9DCD2771E6250930010D1C7 /* XMPPRosterMemoryStorage.h in Headers */, @@ -2640,20 +2924,26 @@ D9DCD3021E6250930010D1C7 /* XMPPStreamManagementMemoryStorage.h in Headers */, D9DCD2CC1E6250930010D1C7 /* XMPPPubSub.h in Headers */, D9DCD25C1E6250930010D1C7 /* OMEMOKeyData.h in Headers */, + DD1784211F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */, D9DCD2941E6250930010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.h in Headers */, D9DCD29C1E6250930010D1C7 /* XMPPRoomOccupantHybridMemoryStorageObject.h in Headers */, + DD1E12301F5EE6100012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */, D9DCD26E1E6250930010D1C7 /* XMPPResourceCoreDataStorageObject.h in Headers */, D9DCD2831E6250930010D1C7 /* XMPPIQ+JabberRPC.h in Headers */, D9DCD3201E6250930010D1C7 /* XMPPMessageArchiveManagement.h in Headers */, + DD1C59831F4429FD003D73DB /* XMPPDelayedDelivery.h in Headers */, D9DCD2D61E6250930010D1C7 /* NSDate+XMPPDateTimeProfiles.h in Headers */, + DD19E40D1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */, D9DCD2BA1E6250930010D1C7 /* XMPPvCardTempAdrTypes.h in Headers */, D9DCD2C81E6250930010D1C7 /* XMPPResultSet.h in Headers */, D9DCD2B41E6250930010D1C7 /* XMPPvCardTempCoreDataStorageObject.h in Headers */, D9DCD2AA1E6250930010D1C7 /* XMPPRoomMessage.h in Headers */, D9DCD3371E6250930010D1C7 /* XMPPRoomLight.h in Headers */, + DDA11A5D1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h in Headers */, D9DCD2C01E6250930010D1C7 /* XMPPvCardTempLabel.h in Headers */, D9DCD2D81E6250930010D1C7 /* XMPPDateTimeProfiles.h in Headers */, D9DCD30E1E6250930010D1C7 /* XMPPTime.h in Headers */, + DD26F58D1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h in Headers */, D9DCD2751E6250930010D1C7 /* XMPPResourceMemoryStorageObject.h in Headers */, D9DCD2C61E6250930010D1C7 /* NSXMLElement+XEP_0059.h in Headers */, D9DCD2CA1E6250930010D1C7 /* XMPPIQ+XEP_0060.h in Headers */, @@ -2670,14 +2960,18 @@ D9DCD2A01E6250930010D1C7 /* XMPPRoomMessageMemoryStorageObject.h in Headers */, D9DCD28D1E6250930010D1C7 /* XMPPPrivacy.h in Headers */, D9DCD2AE1E6250930010D1C7 /* XMPPvCardAvatarCoreDataStorageObject.h in Headers */, + DD2147151F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h in Headers */, D9DCD2DE1E6250930010D1C7 /* XMPPTransports.h in Headers */, 0D44BB121E5370ED000930E0 /* XMPPFramework.h in Headers */, 0D44BB4E1E537105000930E0 /* XMPPDigestMD5Authentication.h in Headers */, 0D44BB691E537110000930E0 /* GCDMulticastDelegate.h in Headers */, 0D44BB501E537105000930E0 /* XMPPPlainAuthentication.h in Headers */, + DD26F5701F7CD9B500F54F18 /* XMPPOneToOneChat.h in Headers */, 0D44BB211E5370ED000930E0 /* XMPPStream.h in Headers */, 0D44BB1F1E5370ED000930E0 /* XMPPPresence.h in Headers */, 0D44BB191E5370ED000930E0 /* XMPPMessage.h in Headers */, + DD17841B1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */, + DD19E4011F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */, 0D44BB161E5370ED000930E0 /* XMPPJID.h in Headers */, 0D44BB4C1E537105000930E0 /* XMPPDeprecatedPlainAuthentication.h in Headers */, 0D44BB141E5370ED000930E0 /* XMPPIQ.h in Headers */, @@ -2701,7 +2995,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + DD40042C1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h in Headers */, D9DCD4981E6256D90010D1C7 /* NSXMLElement+XMPP.h in Headers */, + DD8924D91F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h in Headers */, D9DCD4991E6256D90010D1C7 /* XMPPCustomBinding.h in Headers */, D9DCD49A1E6256D90010D1C7 /* XMPPSASLAuthentication.h in Headers */, DD1E733B1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */, @@ -2713,6 +3009,7 @@ D9DCD4A01E6256D90010D1C7 /* XMPPDeprecatedDigestAuthentication.h in Headers */, D9DCD4A11E6256D90010D1C7 /* NSXMLElement+XEP_0335.h in Headers */, D9DCD4A21E6256D90010D1C7 /* XMPPMessage+XEP0045.h in Headers */, + DD855F921F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h in Headers */, D9DCD4A31E6256D90010D1C7 /* XMPPMessageArchiving_Message_CoreDataObject.h in Headers */, D9DCD4A41E6256D90010D1C7 /* XMPPRosterMemoryStoragePrivate.h in Headers */, D9DCD4A51E6256D90010D1C7 /* XMPPMessage+XEP_0334.h in Headers */, @@ -2742,21 +3039,29 @@ D9DCD4BC1E6256D90010D1C7 /* XMPPMessage+XEP_0280.h in Headers */, D9DCD4BD1E6256D90010D1C7 /* XMPPMessage+XEP_0085.h in Headers */, D9DCD4BE1E6256D90010D1C7 /* XMPPProcessOne.h in Headers */, + DD203B951F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h in Headers */, D9DCD4BF1E6256D90010D1C7 /* XMPPCapsCoreDataStorageObject.h in Headers */, D9DCD4C01E6256D90010D1C7 /* XMPPRoster.h in Headers */, + DDFFF40B1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */, + DD1E80681F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */, D9DCD4C11E6256D90010D1C7 /* OMEMOModule.h in Headers */, D9DCD4C21E6256D90010D1C7 /* NSXMLElement+OMEMO.h in Headers */, D9DCD4C31E6256D90010D1C7 /* NSXMLElement+XEP_0297.h in Headers */, D9DCD4C41E6256D90010D1C7 /* XMPPIQ+XEP_0357.h in Headers */, D9DCD4C51E6256D90010D1C7 /* XMPPCoreDataStorage.h in Headers */, D9DCD4C61E6256D90010D1C7 /* XMPPBandwidthMonitor.h in Headers */, + DD2AD6E91F84B49200E0FED2 /* XMPPManagedMessaging.h in Headers */, D9DCD4C71E6256D90010D1C7 /* XMPPRoomCoreDataStorage.h in Headers */, D9DCD4C81E6256D90010D1C7 /* XMPPAutoPing.h in Headers */, + DD1784161F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */, D9DCD4C91E6256D90010D1C7 /* XMPPvCardAvatarModule.h in Headers */, D9DCD4CA1E6256D90010D1C7 /* XMPPLastActivity.h in Headers */, + DDA938811F790FAC00979230 /* XMPPLastMessageCorrection.h in Headers */, D9DCD4CB1E6256D90010D1C7 /* OMEMOPreKey.h in Headers */, + DDA938891F7913D100979230 /* XMPPCapabilities+XEP_0308.h in Headers */, D9DCD4CC1E6256D90010D1C7 /* OMEMOBundle.h in Headers */, D9DCD4CD1E6256D90010D1C7 /* XMPPMUC.h in Headers */, + DD19E4051F8CA06E00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */, D9DCD4CE1E6256D90010D1C7 /* XMPPRosterPrivate.h in Headers */, D9DCD4CF1E6256D90010D1C7 /* XMPPMessage+OMEMO.h in Headers */, D9DCD4D01E6256D90010D1C7 /* XMPPIQ+JabberRPCResonse.h in Headers */, @@ -2775,9 +3080,12 @@ D9DCD4DD1E6256D90010D1C7 /* XMPPMessageDeliveryReceipts.h in Headers */, D9DCD4DE1E6256D90010D1C7 /* XMPPvCardTempAdr.h in Headers */, D9DCD4DF1E6256D90010D1C7 /* XMPPMessage+XEP_0184.h in Headers */, + DD8924E61F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h in Headers */, + DD06EA4B1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h in Headers */, D9DCD4E01E6256D90010D1C7 /* XMPPvCardTempModule.h in Headers */, D9DCD4E11E6256D90010D1C7 /* XMPPSlot.h in Headers */, D9DCD4E21E6256D90010D1C7 /* XMPPMessage+XEP_0308.h in Headers */, + DD1C59A81F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h in Headers */, D9DCD4E31E6256D90010D1C7 /* XMPPJabberRPCModule.h in Headers */, D9DCD4E41E6256D90010D1C7 /* XMPPMUCLight.h in Headers */, D9DCD4E51E6256D90010D1C7 /* XMPPRosterMemoryStorage.h in Headers */, @@ -2801,20 +3109,26 @@ D9DCD4F61E6256D90010D1C7 /* XMPPStreamManagementMemoryStorage.h in Headers */, D9DCD4F71E6256D90010D1C7 /* XMPPPubSub.h in Headers */, D9DCD4F81E6256D90010D1C7 /* OMEMOKeyData.h in Headers */, + DD1784221F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */, D9DCD4F91E6256D90010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.h in Headers */, D9DCD4FA1E6256D90010D1C7 /* XMPPRoomOccupantHybridMemoryStorageObject.h in Headers */, + DD1E12311F5EE6110012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */, D9DCD4FB1E6256D90010D1C7 /* XMPPResourceCoreDataStorageObject.h in Headers */, D9DCD4FC1E6256D90010D1C7 /* XMPPIQ+JabberRPC.h in Headers */, D9DCD4FD1E6256D90010D1C7 /* XMPPMessageArchiveManagement.h in Headers */, + DD1C59841F4429FD003D73DB /* XMPPDelayedDelivery.h in Headers */, D9DCD4FE1E6256D90010D1C7 /* NSDate+XMPPDateTimeProfiles.h in Headers */, + DD19E40E1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */, D9DCD4FF1E6256D90010D1C7 /* XMPPvCardTempAdrTypes.h in Headers */, D9DCD5001E6256D90010D1C7 /* XMPPResultSet.h in Headers */, D9DCD5011E6256D90010D1C7 /* XMPPvCardTempCoreDataStorageObject.h in Headers */, D9DCD5021E6256D90010D1C7 /* XMPPRoomMessage.h in Headers */, D9DCD5031E6256D90010D1C7 /* XMPPRoomLight.h in Headers */, + DDA11A5E1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h in Headers */, D9DCD5041E6256D90010D1C7 /* XMPPvCardTempLabel.h in Headers */, D9DCD5051E6256D90010D1C7 /* XMPPDateTimeProfiles.h in Headers */, D9DCD5061E6256D90010D1C7 /* XMPPTime.h in Headers */, + DD26F58E1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h in Headers */, D9DCD5071E6256D90010D1C7 /* XMPPResourceMemoryStorageObject.h in Headers */, D9DCD5081E6256D90010D1C7 /* NSXMLElement+XEP_0059.h in Headers */, D9DCD5091E6256D90010D1C7 /* XMPPIQ+XEP_0060.h in Headers */, @@ -2831,14 +3145,18 @@ D9DCD5141E6256D90010D1C7 /* XMPPRoomMessageMemoryStorageObject.h in Headers */, D9DCD5151E6256D90010D1C7 /* XMPPPrivacy.h in Headers */, D9DCD5161E6256D90010D1C7 /* XMPPvCardAvatarCoreDataStorageObject.h in Headers */, + DD2147161F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h in Headers */, D9DCD5171E6256D90010D1C7 /* XMPPTransports.h in Headers */, D9DCD5181E6256D90010D1C7 /* XMPPFramework.h in Headers */, D9DCD5191E6256D90010D1C7 /* XMPPDigestMD5Authentication.h in Headers */, D9DCD51A1E6256D90010D1C7 /* GCDMulticastDelegate.h in Headers */, D9DCD51B1E6256D90010D1C7 /* XMPPPlainAuthentication.h in Headers */, + DD26F5711F7CD9B500F54F18 /* XMPPOneToOneChat.h in Headers */, D9DCD51C1E6256D90010D1C7 /* XMPPStream.h in Headers */, D9DCD51D1E6256D90010D1C7 /* XMPPPresence.h in Headers */, D9DCD51E1E6256D90010D1C7 /* XMPPMessage.h in Headers */, + DD17841C1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */, + DD19E4021F8CA02000CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */, D9DCD51F1E6256D90010D1C7 /* XMPPJID.h in Headers */, D9DCD5201E6256D90010D1C7 /* XMPPDeprecatedPlainAuthentication.h in Headers */, D9DCD5211E6256D90010D1C7 /* XMPPIQ.h in Headers */, @@ -2862,7 +3180,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + DD40042D1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.h in Headers */, D9DCD5FB1E6258CF0010D1C7 /* NSXMLElement+XMPP.h in Headers */, + DD8924DA1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.h in Headers */, D9DCD5FC1E6258CF0010D1C7 /* XMPPCustomBinding.h in Headers */, D9DCD5FD1E6258CF0010D1C7 /* XMPPSASLAuthentication.h in Headers */, DD1E733C1ED88622009B529B /* XMPPRoomLightCoreDataStorageProtected.h in Headers */, @@ -2874,6 +3194,7 @@ D9DCD6031E6258CF0010D1C7 /* XMPPDeprecatedDigestAuthentication.h in Headers */, D9DCD6041E6258CF0010D1C7 /* NSXMLElement+XEP_0335.h in Headers */, D9DCD6051E6258CF0010D1C7 /* XMPPMessage+XEP0045.h in Headers */, + DD855F931F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.h in Headers */, D9DCD6061E6258CF0010D1C7 /* XMPPMessageArchiving_Message_CoreDataObject.h in Headers */, D9DCD6071E6258CF0010D1C7 /* XMPPRosterMemoryStoragePrivate.h in Headers */, D9DCD6081E6258CF0010D1C7 /* XMPPMessage+XEP_0334.h in Headers */, @@ -2903,21 +3224,29 @@ D9DCD61F1E6258CF0010D1C7 /* XMPPMessage+XEP_0280.h in Headers */, D9DCD6201E6258CF0010D1C7 /* XMPPMessage+XEP_0085.h in Headers */, D9DCD6211E6258CF0010D1C7 /* XMPPProcessOne.h in Headers */, + DD203B961F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.h in Headers */, D9DCD6221E6258CF0010D1C7 /* XMPPCapsCoreDataStorageObject.h in Headers */, D9DCD6231E6258CF0010D1C7 /* XMPPRoster.h in Headers */, + DDFFF40C1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.h in Headers */, + DD1E80691F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.h in Headers */, D9DCD6241E6258CF0010D1C7 /* OMEMOModule.h in Headers */, D9DCD6251E6258CF0010D1C7 /* NSXMLElement+OMEMO.h in Headers */, D9DCD6261E6258CF0010D1C7 /* NSXMLElement+XEP_0297.h in Headers */, D9DCD6271E6258CF0010D1C7 /* XMPPIQ+XEP_0357.h in Headers */, D9DCD6281E6258CF0010D1C7 /* XMPPCoreDataStorage.h in Headers */, D9DCD6291E6258CF0010D1C7 /* XMPPBandwidthMonitor.h in Headers */, + DD2AD6EA1F84B49200E0FED2 /* XMPPManagedMessaging.h in Headers */, D9DCD62A1E6258CF0010D1C7 /* XMPPRoomCoreDataStorage.h in Headers */, D9DCD62B1E6258CF0010D1C7 /* XMPPAutoPing.h in Headers */, + DD1784171F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.h in Headers */, D9DCD62C1E6258CF0010D1C7 /* XMPPvCardAvatarModule.h in Headers */, D9DCD62D1E6258CF0010D1C7 /* XMPPLastActivity.h in Headers */, + DDA938821F790FAC00979230 /* XMPPLastMessageCorrection.h in Headers */, D9DCD62E1E6258CF0010D1C7 /* OMEMOPreKey.h in Headers */, + DDA9388A1F7913D100979230 /* XMPPCapabilities+XEP_0308.h in Headers */, D9DCD62F1E6258CF0010D1C7 /* OMEMOBundle.h in Headers */, D9DCD6301E6258CF0010D1C7 /* XMPPMUC.h in Headers */, + DD19E4061F8CA06F00CED8EF /* XMPPMessageContextCoreDataStorageObject+Protected.h in Headers */, D9DCD6311E6258CF0010D1C7 /* XMPPRosterPrivate.h in Headers */, D9DCD6321E6258CF0010D1C7 /* XMPPMessage+OMEMO.h in Headers */, D9DCD6331E6258CF0010D1C7 /* XMPPIQ+JabberRPCResonse.h in Headers */, @@ -2936,9 +3265,12 @@ D9DCD6401E6258CF0010D1C7 /* XMPPMessageDeliveryReceipts.h in Headers */, D9DCD6411E6258CF0010D1C7 /* XMPPvCardTempAdr.h in Headers */, D9DCD6421E6258CF0010D1C7 /* XMPPMessage+XEP_0184.h in Headers */, + DD8924E71F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.h in Headers */, + DD06EA4C1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.h in Headers */, D9DCD6431E6258CF0010D1C7 /* XMPPvCardTempModule.h in Headers */, D9DCD6441E6258CF0010D1C7 /* XMPPSlot.h in Headers */, D9DCD6451E6258CF0010D1C7 /* XMPPMessage+XEP_0308.h in Headers */, + DD1C59A91F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.h in Headers */, D9DCD6461E6258CF0010D1C7 /* XMPPJabberRPCModule.h in Headers */, D9DCD6471E6258CF0010D1C7 /* XMPPMUCLight.h in Headers */, D9DCD6481E6258CF0010D1C7 /* XMPPRosterMemoryStorage.h in Headers */, @@ -2962,20 +3294,26 @@ D9DCD6591E6258CF0010D1C7 /* XMPPStreamManagementMemoryStorage.h in Headers */, D9DCD65A1E6258CF0010D1C7 /* XMPPPubSub.h in Headers */, D9DCD65B1E6258CF0010D1C7 /* OMEMOKeyData.h in Headers */, + DD1784231F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.h in Headers */, D9DCD65C1E6258CF0010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.h in Headers */, D9DCD65D1E6258CF0010D1C7 /* XMPPRoomOccupantHybridMemoryStorageObject.h in Headers */, + DD1E12321F5EE6120012A506 /* XMPPMessageCoreDataStorageObject+Protected.h in Headers */, D9DCD65E1E6258CF0010D1C7 /* XMPPResourceCoreDataStorageObject.h in Headers */, D9DCD65F1E6258CF0010D1C7 /* XMPPIQ+JabberRPC.h in Headers */, D9DCD6601E6258CF0010D1C7 /* XMPPMessageArchiveManagement.h in Headers */, + DD1C59851F4429FD003D73DB /* XMPPDelayedDelivery.h in Headers */, D9DCD6611E6258CF0010D1C7 /* NSDate+XMPPDateTimeProfiles.h in Headers */, + DD19E40F1F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.h in Headers */, D9DCD6621E6258CF0010D1C7 /* XMPPvCardTempAdrTypes.h in Headers */, D9DCD6631E6258CF0010D1C7 /* XMPPResultSet.h in Headers */, D9DCD6641E6258CF0010D1C7 /* XMPPvCardTempCoreDataStorageObject.h in Headers */, D9DCD6651E6258CF0010D1C7 /* XMPPRoomMessage.h in Headers */, D9DCD6661E6258CF0010D1C7 /* XMPPRoomLight.h in Headers */, + DDA11A5F1F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.h in Headers */, D9DCD6671E6258CF0010D1C7 /* XMPPvCardTempLabel.h in Headers */, D9DCD6681E6258CF0010D1C7 /* XMPPDateTimeProfiles.h in Headers */, D9DCD6691E6258CF0010D1C7 /* XMPPTime.h in Headers */, + DD26F58F1F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.h in Headers */, D9DCD66A1E6258CF0010D1C7 /* XMPPResourceMemoryStorageObject.h in Headers */, D9DCD66B1E6258CF0010D1C7 /* NSXMLElement+XEP_0059.h in Headers */, D9DCD66C1E6258CF0010D1C7 /* XMPPIQ+XEP_0060.h in Headers */, @@ -2992,14 +3330,18 @@ D9DCD6771E6258CF0010D1C7 /* XMPPRoomMessageMemoryStorageObject.h in Headers */, D9DCD6781E6258CF0010D1C7 /* XMPPPrivacy.h in Headers */, D9DCD6791E6258CF0010D1C7 /* XMPPvCardAvatarCoreDataStorageObject.h in Headers */, + DD2147171F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.h in Headers */, D9DCD67A1E6258CF0010D1C7 /* XMPPTransports.h in Headers */, D9DCD67B1E6258CF0010D1C7 /* XMPPFramework.h in Headers */, D9DCD67C1E6258CF0010D1C7 /* XMPPDigestMD5Authentication.h in Headers */, D9DCD67D1E6258CF0010D1C7 /* GCDMulticastDelegate.h in Headers */, D9DCD67E1E6258CF0010D1C7 /* XMPPPlainAuthentication.h in Headers */, + DD26F5721F7CD9B500F54F18 /* XMPPOneToOneChat.h in Headers */, D9DCD67F1E6258CF0010D1C7 /* XMPPStream.h in Headers */, D9DCD6801E6258CF0010D1C7 /* XMPPPresence.h in Headers */, D9DCD6811E6258CF0010D1C7 /* XMPPMessage.h in Headers */, + DD17841D1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.h in Headers */, + DD19E4031F8CA02100CED8EF /* XMPPMessageContextItemCoreDataStorageObject+Protected.h in Headers */, D9DCD6821E6258CF0010D1C7 /* XMPPJID.h in Headers */, D9DCD6831E6258CF0010D1C7 /* XMPPDeprecatedPlainAuthentication.h in Headers */, D9DCD6841E6258CF0010D1C7 /* XMPPIQ.h in Headers */, @@ -3376,6 +3718,7 @@ D9DCD3131E6250930010D1C7 /* XEP_0223.m in Sources */, D9DCD2651E6250930010D1C7 /* XMPPIQ+OMEMO.m in Sources */, D9DCD3151E6250930010D1C7 /* XMPPAttentionModule.m in Sources */, + DD1E806A1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */, D9DCD2551E6250930010D1C7 /* XMPPOutgoingFileTransfer.m in Sources */, D9DCD2951E6250930010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.m in Sources */, D9DCD2931E6250930010D1C7 /* XMPPRoomMessageCoreDataStorageObject.m in Sources */, @@ -3387,22 +3730,26 @@ D9DCD2CF1E6250930010D1C7 /* TURNSocket.m in Sources */, D9DCD2A31E6250930010D1C7 /* XMPPRoomOccupantMemoryStorageObject.m in Sources */, D9DCD2741E6250930010D1C7 /* XMPPUserCoreDataStorageObject.m in Sources */, + DD8924E81F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m in Sources */, D9DCD2FF1E6250930010D1C7 /* XMPPMessageDeliveryReceipts.m in Sources */, D9DCD2C31E6250930010D1C7 /* XMPPvCardTempModule.m in Sources */, D9DCD24E1E6250930010D1C7 /* XMPPCoreDataStorage.m in Sources */, D9DCD2671E6250930010D1C7 /* XMPPMessage+OMEMO.m in Sources */, 0D44BB2B1E5370FC000930E0 /* NSData+XMPP.m in Sources */, + DD26F5901F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m in Sources */, D9DCD29F1E6250930010D1C7 /* XMPPRoomMemoryStorage.m in Sources */, 0D44BB491E537105000930E0 /* XMPPAnonymousAuthentication.m in Sources */, D9DCD3361E6250930010D1C7 /* XMPPMUCLight.m in Sources */, D9DCD30B1E6250930010D1C7 /* XMPPPing.m in Sources */, D9DCD2691E6250930010D1C7 /* XMPPProcessOne.m in Sources */, D9DCD3211E6250930010D1C7 /* XMPPMessageArchiveManagement.m in Sources */, + DD8924DB1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m in Sources */, D9DCD3031E6250930010D1C7 /* XMPPStreamManagementMemoryStorage.m in Sources */, D9DCD3341E6250930010D1C7 /* XMPPRoomLightMessageCoreDataStorageObject.m in Sources */, 0D44BB221E5370ED000930E0 /* XMPPStream.m in Sources */, D9DCD2BD1E6250930010D1C7 /* XMPPvCardTempBase.m in Sources */, 0D44BB4D1E537105000930E0 /* XMPPDeprecatedPlainAuthentication.m in Sources */, + DD26F5731F7CD9B500F54F18 /* XMPPOneToOneChat.m in Sources */, D9DCD3171E6250930010D1C7 /* XMPPMessage+XEP_0224.m in Sources */, D9DCD2E41E6250930010D1C7 /* XMPPCapabilitiesCoreDataStorage.m in Sources */, 0D44BB511E537105000930E0 /* XMPPPlainAuthentication.m in Sources */, @@ -3414,6 +3761,7 @@ D9DCD24C1E6250930010D1C7 /* XMPPBandwidthMonitor.m in Sources */, D9DCD3111E6250930010D1C7 /* NSXMLElement+XEP_0203.m in Sources */, 0D44BB551E537105000930E0 /* XMPPXOAuth2Google.m in Sources */, + DD1784241F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */, D9DCD2EB1E6250930010D1C7 /* XMPPMessageArchiving.xcdatamodeld in Sources */, D9DCD2A51E6250930010D1C7 /* XMPPMessage+XEP0045.m in Sources */, D9DCD2DF1E6250930010D1C7 /* XMPPTransports.m in Sources */, @@ -3422,6 +3770,7 @@ D9DCD2881E6250930010D1C7 /* XMPPJabberRPCModule.m in Sources */, D9DCD2571E6250930010D1C7 /* XMPPGoogleSharedStatus.m in Sources */, D9DCD2C91E6250930010D1C7 /* XMPPResultSet.m in Sources */, + DD203B971F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m in Sources */, 0D44BB171E5370ED000930E0 /* XMPPJID.m in Sources */, 0D44BB201E5370ED000930E0 /* XMPPPresence.m in Sources */, D9DCD29B1E6250930010D1C7 /* XMPPRoomMessageHybridCoreDataStorageObject.m in Sources */, @@ -3430,12 +3779,15 @@ D9DCD2611E6250930010D1C7 /* OMEMOPreKey.m in Sources */, D9DCD3071E6250930010D1C7 /* XMPPStreamManagement.m in Sources */, D9DCD26F1E6250930010D1C7 /* XMPPResourceCoreDataStorageObject.m in Sources */, + DD40042E1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m in Sources */, + DDA11A601F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m in Sources */, D9DCD28E1E6250930010D1C7 /* XMPPPrivacy.m in Sources */, D9DCD2911E6250930010D1C7 /* XMPPRoomCoreDataStorage.m in Sources */, D9DCD2591E6250930010D1C7 /* NSXMLElement+OMEMO.m in Sources */, D9DCD2CD1E6250930010D1C7 /* XMPPPubSub.m in Sources */, D9DCD2E21E6250930010D1C7 /* XMPPCapabilities.xcdatamodel in Sources */, D9DCD32F1E6250930010D1C7 /* XMPPSlot.m in Sources */, + DDA9388B1F7913D100979230 /* XMPPCapabilities+XEP_0308.m in Sources */, D9DCD2F91E6250930010D1C7 /* XMPPMessage+XEP_0172.m in Sources */, D9DCD2B11E6250930010D1C7 /* XMPPvCardCoreDataStorage.m in Sources */, D9DCD2F51E6250930010D1C7 /* XMPPURI.m in Sources */, @@ -3455,6 +3807,8 @@ D9DCD2531E6250930010D1C7 /* XMPPIncomingFileTransfer.m in Sources */, 0D44BB531E537105000930E0 /* XMPPSCRAMSHA1Authentication.m in Sources */, D9DCD32B1E6250930010D1C7 /* XMPPIQ+XEP_0357.m in Sources */, + DD1C59861F4429FD003D73DB /* XMPPDelayedDelivery.m in Sources */, + DD855F941F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m in Sources */, D9DCD2F71E6250930010D1C7 /* XMPPvCardAvatarModule.m in Sources */, D9DCD26B1E6250930010D1C7 /* XMPPReconnect.m in Sources */, D9DCD2B91E6250930010D1C7 /* XMPPvCardTempAdr.m in Sources */, @@ -3472,17 +3826,22 @@ D9DCD2A91E6250930010D1C7 /* XMPPRoom.m in Sources */, D9DCD25B1E6250930010D1C7 /* OMEMOBundle.m in Sources */, D9DCD2E61E6250930010D1C7 /* XMPPCapsCoreDataStorageObject.m in Sources */, + DD1784181F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */, D9DCD2D51E6250930010D1C7 /* XMPPRegistration.m in Sources */, D9DCD27E1E6250930010D1C7 /* XMPPRoster.m in Sources */, D9DCD3051E6250930010D1C7 /* XMPPStreamManagementStanzas.m in Sources */, D9DCD2AD1E6250930010D1C7 /* XMPPvCard.xcdatamodeld in Sources */, D9DCD2D71E6250930010D1C7 /* NSDate+XMPPDateTimeProfiles.m in Sources */, + DD2AD6EB1F84B49200E0FED2 /* XMPPManagedMessaging.m in Sources */, D9DCD3381E6250930010D1C7 /* XMPPRoomLight.m in Sources */, + DDA938831F790FAC00979230 /* XMPPLastMessageCorrection.m in Sources */, 0D44BB111E5370ED000930E0 /* XMPPElement.m in Sources */, + DD2147181F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m in Sources */, 0D44BB6A1E537110000930E0 /* GCDMulticastDelegate.m in Sources */, D96D6E7E1F8D9701006DEC58 /* XMPPPushModule.m in Sources */, D9DCD2861E6250930010D1C7 /* XMPPIQ+JabberRPCResonse.m in Sources */, D9DCD2EF1E6250930010D1C7 /* XMPPMessageArchiving_Message_CoreDataObject.m in Sources */, + DD1C59AA1F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m in Sources */, D9DCD25F1E6250930010D1C7 /* OMEMOModule.m in Sources */, D9DCD2D11E6250930010D1C7 /* XMPPIQ+XEP_0066.m in Sources */, D9DCD2A71E6250930010D1C7 /* XMPPMUC.m in Sources */, @@ -3495,12 +3854,17 @@ D9DCD2DD1E6250930010D1C7 /* XMPPSoftwareVersion.m in Sources */, D9DCD28A1E6250930010D1C7 /* XMPPIQ+LastActivity.m in Sources */, D9DCD2B31E6250930010D1C7 /* XMPPvCardCoreDataStorageObject.m in Sources */, + DDFFF40D1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */, + DD06EA4D1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m in Sources */, D9DCD2B51E6250930010D1C7 /* XMPPvCardTempCoreDataStorageObject.m in Sources */, D9DCD3231E6250930010D1C7 /* XMPPMessage+XEP_0333.m in Sources */, D9DCD2A11E6250930010D1C7 /* XMPPRoomMessageMemoryStorageObject.m in Sources */, D9DCD2721E6250930010D1C7 /* XMPPRosterCoreDataStorage.m in Sources */, 0D44BB681E537110000930E0 /* DDList.m in Sources */, + DD17841E1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */, 0D44BB701E537110000930E0 /* XMPPSRVResolver.m in Sources */, + DD19E4101F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */, + DD1784121F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */, D9DCD3191E6250930010D1C7 /* XMPPMessage+XEP_0280.m in Sources */, 0D44BB6E1E537110000930E0 /* XMPPIDTracker.m in Sources */, D9DCD2DB1E6250930010D1C7 /* XMPPMessage+XEP_0085.m in Sources */, @@ -3528,6 +3892,7 @@ D9DCD40D1E6256D90010D1C7 /* XEP_0223.m in Sources */, D9DCD40E1E6256D90010D1C7 /* XMPPIQ+OMEMO.m in Sources */, D9DCD40F1E6256D90010D1C7 /* XMPPAttentionModule.m in Sources */, + DD1E806B1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */, D9DCD4101E6256D90010D1C7 /* XMPPOutgoingFileTransfer.m in Sources */, D9DCD4111E6256D90010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.m in Sources */, D9DCD4121E6256D90010D1C7 /* XMPPRoomMessageCoreDataStorageObject.m in Sources */, @@ -3539,22 +3904,26 @@ D9DCD4171E6256D90010D1C7 /* TURNSocket.m in Sources */, D9DCD4181E6256D90010D1C7 /* XMPPRoomOccupantMemoryStorageObject.m in Sources */, D9DCD4191E6256D90010D1C7 /* XMPPUserCoreDataStorageObject.m in Sources */, + DD8924E91F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m in Sources */, D9DCD41A1E6256D90010D1C7 /* XMPPMessageDeliveryReceipts.m in Sources */, D9DCD41B1E6256D90010D1C7 /* XMPPvCardTempModule.m in Sources */, D9DCD41C1E6256D90010D1C7 /* XMPPCoreDataStorage.m in Sources */, D9DCD41D1E6256D90010D1C7 /* XMPPMessage+OMEMO.m in Sources */, D9DCD41E1E6256D90010D1C7 /* NSData+XMPP.m in Sources */, + DD26F5911F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m in Sources */, D9DCD41F1E6256D90010D1C7 /* XMPPRoomMemoryStorage.m in Sources */, D9DCD4201E6256D90010D1C7 /* XMPPAnonymousAuthentication.m in Sources */, D9DCD4211E6256D90010D1C7 /* XMPPMUCLight.m in Sources */, D9DCD4221E6256D90010D1C7 /* XMPPPing.m in Sources */, D9DCD4231E6256D90010D1C7 /* XMPPProcessOne.m in Sources */, D9DCD4241E6256D90010D1C7 /* XMPPMessageArchiveManagement.m in Sources */, + DD8924DC1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m in Sources */, D9DCD4251E6256D90010D1C7 /* XMPPStreamManagementMemoryStorage.m in Sources */, D9DCD4261E6256D90010D1C7 /* XMPPRoomLightMessageCoreDataStorageObject.m in Sources */, D9DCD4271E6256D90010D1C7 /* XMPPStream.m in Sources */, D9DCD4281E6256D90010D1C7 /* XMPPvCardTempBase.m in Sources */, D9DCD4291E6256D90010D1C7 /* XMPPDeprecatedPlainAuthentication.m in Sources */, + DD26F5741F7CD9B500F54F18 /* XMPPOneToOneChat.m in Sources */, D9DCD42A1E6256D90010D1C7 /* XMPPMessage+XEP_0224.m in Sources */, D9DCD42B1E6256D90010D1C7 /* XMPPCapabilitiesCoreDataStorage.m in Sources */, D9DCD42C1E6256D90010D1C7 /* XMPPPlainAuthentication.m in Sources */, @@ -3566,6 +3935,7 @@ D9DCD4321E6256D90010D1C7 /* XMPPBandwidthMonitor.m in Sources */, D9DCD4331E6256D90010D1C7 /* NSXMLElement+XEP_0203.m in Sources */, D9DCD4341E6256D90010D1C7 /* XMPPXOAuth2Google.m in Sources */, + DD1784251F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */, D9DCD4351E6256D90010D1C7 /* XMPPMessageArchiving.xcdatamodeld in Sources */, D9DCD4361E6256D90010D1C7 /* XMPPMessage+XEP0045.m in Sources */, D9DCD4371E6256D90010D1C7 /* XMPPTransports.m in Sources */, @@ -3574,6 +3944,7 @@ D9DCD43A1E6256D90010D1C7 /* XMPPJabberRPCModule.m in Sources */, D9DCD43B1E6256D90010D1C7 /* XMPPGoogleSharedStatus.m in Sources */, D9DCD43C1E6256D90010D1C7 /* XMPPResultSet.m in Sources */, + DD203B981F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m in Sources */, D9DCD43D1E6256D90010D1C7 /* XMPPJID.m in Sources */, D9DCD43E1E6256D90010D1C7 /* XMPPPresence.m in Sources */, D9DCD43F1E6256D90010D1C7 /* XMPPRoomMessageHybridCoreDataStorageObject.m in Sources */, @@ -3582,12 +3953,15 @@ D9DCD4421E6256D90010D1C7 /* OMEMOPreKey.m in Sources */, D9DCD4431E6256D90010D1C7 /* XMPPStreamManagement.m in Sources */, D9DCD4441E6256D90010D1C7 /* XMPPResourceCoreDataStorageObject.m in Sources */, + DD40042F1F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m in Sources */, + DDA11A611F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m in Sources */, D9DCD4451E6256D90010D1C7 /* XMPPPrivacy.m in Sources */, D9DCD4461E6256D90010D1C7 /* XMPPRoomCoreDataStorage.m in Sources */, D9DCD4471E6256D90010D1C7 /* NSXMLElement+OMEMO.m in Sources */, D9DCD4481E6256D90010D1C7 /* XMPPPubSub.m in Sources */, D9DCD4491E6256D90010D1C7 /* XMPPCapabilities.xcdatamodel in Sources */, D9DCD44A1E6256D90010D1C7 /* XMPPSlot.m in Sources */, + DDA9388C1F7913D100979230 /* XMPPCapabilities+XEP_0308.m in Sources */, D9DCD44B1E6256D90010D1C7 /* XMPPMessage+XEP_0172.m in Sources */, D9DCD44C1E6256D90010D1C7 /* XMPPvCardCoreDataStorage.m in Sources */, D9DCD44D1E6256D90010D1C7 /* XMPPURI.m in Sources */, @@ -3607,6 +3981,8 @@ D9DCD45B1E6256D90010D1C7 /* XMPPIncomingFileTransfer.m in Sources */, D9DCD45C1E6256D90010D1C7 /* XMPPSCRAMSHA1Authentication.m in Sources */, D9DCD45D1E6256D90010D1C7 /* XMPPIQ+XEP_0357.m in Sources */, + DD1C59871F4429FD003D73DB /* XMPPDelayedDelivery.m in Sources */, + DD855F951F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m in Sources */, D9DCD45E1E6256D90010D1C7 /* XMPPvCardAvatarModule.m in Sources */, D9DCD45F1E6256D90010D1C7 /* XMPPReconnect.m in Sources */, D9DCD4601E6256D90010D1C7 /* XMPPvCardTempAdr.m in Sources */, @@ -3624,17 +4000,22 @@ D9DCD46C1E6256D90010D1C7 /* XMPPRoom.m in Sources */, D9DCD46D1E6256D90010D1C7 /* OMEMOBundle.m in Sources */, D9DCD46E1E6256D90010D1C7 /* XMPPCapsCoreDataStorageObject.m in Sources */, + DD1784191F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */, D9DCD46F1E6256D90010D1C7 /* XMPPRegistration.m in Sources */, D9DCD4701E6256D90010D1C7 /* XMPPRoster.m in Sources */, D9DCD4711E6256D90010D1C7 /* XMPPStreamManagementStanzas.m in Sources */, D9DCD4721E6256D90010D1C7 /* XMPPvCard.xcdatamodeld in Sources */, D9DCD4731E6256D90010D1C7 /* NSDate+XMPPDateTimeProfiles.m in Sources */, + DD2AD6EC1F84B49200E0FED2 /* XMPPManagedMessaging.m in Sources */, D9DCD4741E6256D90010D1C7 /* XMPPRoomLight.m in Sources */, + DDA938841F790FAC00979230 /* XMPPLastMessageCorrection.m in Sources */, D9DCD4751E6256D90010D1C7 /* XMPPElement.m in Sources */, + DD2147191F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m in Sources */, D9DCD4761E6256D90010D1C7 /* GCDMulticastDelegate.m in Sources */, D96D6E7F1F8D9701006DEC58 /* XMPPPushModule.m in Sources */, D9DCD4771E6256D90010D1C7 /* XMPPIQ+JabberRPCResonse.m in Sources */, D9DCD4781E6256D90010D1C7 /* XMPPMessageArchiving_Message_CoreDataObject.m in Sources */, + DD1C59AB1F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m in Sources */, D9DCD4791E6256D90010D1C7 /* OMEMOModule.m in Sources */, D9DCD47A1E6256D90010D1C7 /* XMPPIQ+XEP_0066.m in Sources */, D9DCD47B1E6256D90010D1C7 /* XMPPMUC.m in Sources */, @@ -3647,12 +4028,17 @@ D9DCD4821E6256D90010D1C7 /* XMPPSoftwareVersion.m in Sources */, D9DCD4831E6256D90010D1C7 /* XMPPIQ+LastActivity.m in Sources */, D9DCD4841E6256D90010D1C7 /* XMPPvCardCoreDataStorageObject.m in Sources */, + DDFFF40E1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */, + DD06EA4E1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m in Sources */, D9DCD4851E6256D90010D1C7 /* XMPPvCardTempCoreDataStorageObject.m in Sources */, D9DCD4861E6256D90010D1C7 /* XMPPMessage+XEP_0333.m in Sources */, D9DCD4871E6256D90010D1C7 /* XMPPRoomMessageMemoryStorageObject.m in Sources */, D9DCD4881E6256D90010D1C7 /* XMPPRosterCoreDataStorage.m in Sources */, D9DCD4891E6256D90010D1C7 /* DDList.m in Sources */, + DD17841F1F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */, D9DCD48A1E6256D90010D1C7 /* XMPPSRVResolver.m in Sources */, + DD19E4111F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */, + DD1784131F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */, D9DCD48B1E6256D90010D1C7 /* XMPPMessage+XEP_0280.m in Sources */, D9DCD48C1E6256D90010D1C7 /* XMPPIDTracker.m in Sources */, D9DCD48D1E6256D90010D1C7 /* XMPPMessage+XEP_0085.m in Sources */, @@ -3680,6 +4066,7 @@ D9DCD5701E6258CF0010D1C7 /* XEP_0223.m in Sources */, D9DCD5711E6258CF0010D1C7 /* XMPPIQ+OMEMO.m in Sources */, D9DCD5721E6258CF0010D1C7 /* XMPPAttentionModule.m in Sources */, + DD1E806C1F56AC4D003C21A0 /* XMPPMessageContextItemCoreDataStorageObject.m in Sources */, D9DCD5731E6258CF0010D1C7 /* XMPPOutgoingFileTransfer.m in Sources */, D9DCD5741E6258CF0010D1C7 /* XMPPRoomOccupantCoreDataStorageObject.m in Sources */, D9DCD5751E6258CF0010D1C7 /* XMPPRoomMessageCoreDataStorageObject.m in Sources */, @@ -3691,22 +4078,26 @@ D9DCD57A1E6258CF0010D1C7 /* TURNSocket.m in Sources */, D9DCD57B1E6258CF0010D1C7 /* XMPPRoomOccupantMemoryStorageObject.m in Sources */, D9DCD57C1E6258CF0010D1C7 /* XMPPUserCoreDataStorageObject.m in Sources */, + DD8924EA1F7B78BD00E7D917 /* XMPPMessageCoreDataStorage+XMPPMUCLight.m in Sources */, D9DCD57D1E6258CF0010D1C7 /* XMPPMessageDeliveryReceipts.m in Sources */, D9DCD57E1E6258CF0010D1C7 /* XMPPvCardTempModule.m in Sources */, D9DCD57F1E6258CF0010D1C7 /* XMPPCoreDataStorage.m in Sources */, D9DCD5801E6258CF0010D1C7 /* XMPPMessage+OMEMO.m in Sources */, D9DCD5811E6258CF0010D1C7 /* NSData+XMPP.m in Sources */, + DD26F5921F7CF25300F54F18 /* XMPPMessageCoreDataStorage+XMPPOneToOneChat.m in Sources */, D9DCD5821E6258CF0010D1C7 /* XMPPRoomMemoryStorage.m in Sources */, D9DCD5831E6258CF0010D1C7 /* XMPPAnonymousAuthentication.m in Sources */, D9DCD5841E6258CF0010D1C7 /* XMPPMUCLight.m in Sources */, D9DCD5851E6258CF0010D1C7 /* XMPPPing.m in Sources */, D9DCD5861E6258CF0010D1C7 /* XMPPProcessOne.m in Sources */, D9DCD5871E6258CF0010D1C7 /* XMPPMessageArchiveManagement.m in Sources */, + DD8924DD1F7AA2D800E7D917 /* XMPPMessageCoreDataStorage+XEP_0245.m in Sources */, D9DCD5881E6258CF0010D1C7 /* XMPPStreamManagementMemoryStorage.m in Sources */, D9DCD5891E6258CF0010D1C7 /* XMPPRoomLightMessageCoreDataStorageObject.m in Sources */, D9DCD58A1E6258CF0010D1C7 /* XMPPStream.m in Sources */, D9DCD58B1E6258CF0010D1C7 /* XMPPvCardTempBase.m in Sources */, D9DCD58C1E6258CF0010D1C7 /* XMPPDeprecatedPlainAuthentication.m in Sources */, + DD26F5751F7CD9B500F54F18 /* XMPPOneToOneChat.m in Sources */, D9DCD58D1E6258CF0010D1C7 /* XMPPMessage+XEP_0224.m in Sources */, D9DCD58E1E6258CF0010D1C7 /* XMPPCapabilitiesCoreDataStorage.m in Sources */, D9DCD58F1E6258CF0010D1C7 /* XMPPPlainAuthentication.m in Sources */, @@ -3718,6 +4109,7 @@ D9DCD5951E6258CF0010D1C7 /* XMPPBandwidthMonitor.m in Sources */, D9DCD5961E6258CF0010D1C7 /* NSXMLElement+XEP_0203.m in Sources */, D9DCD5971E6258CF0010D1C7 /* XMPPXOAuth2Google.m in Sources */, + DD1784261F3C9FA800D662A6 /* XMPPMessageCoreDataStorage.m in Sources */, D9DCD5981E6258CF0010D1C7 /* XMPPMessageArchiving.xcdatamodeld in Sources */, D9DCD5991E6258CF0010D1C7 /* XMPPMessage+XEP0045.m in Sources */, D9DCD59A1E6258CF0010D1C7 /* XMPPTransports.m in Sources */, @@ -3726,6 +4118,7 @@ D9DCD59D1E6258CF0010D1C7 /* XMPPJabberRPCModule.m in Sources */, D9DCD59E1E6258CF0010D1C7 /* XMPPGoogleSharedStatus.m in Sources */, D9DCD59F1E6258CF0010D1C7 /* XMPPResultSet.m in Sources */, + DD203B991F7A748B00CA359C /* XMPPMessageCoreDataStorage+XEP_0308.m in Sources */, D9DCD5A01E6258CF0010D1C7 /* XMPPJID.m in Sources */, D9DCD5A11E6258CF0010D1C7 /* XMPPPresence.m in Sources */, D9DCD5A21E6258CF0010D1C7 /* XMPPRoomMessageHybridCoreDataStorageObject.m in Sources */, @@ -3734,12 +4127,15 @@ D9DCD5A51E6258CF0010D1C7 /* OMEMOPreKey.m in Sources */, D9DCD5A61E6258CF0010D1C7 /* XMPPStreamManagement.m in Sources */, D9DCD5A71E6258CF0010D1C7 /* XMPPResourceCoreDataStorageObject.m in Sources */, + DD4004301F752B970078D144 /* XMPPMessageCoreDataStorage+XEP_0066.m in Sources */, + DDA11A621F851B1D00591D1B /* XMPPMessageCoreDataStorage+XEP_0198.m in Sources */, D9DCD5A81E6258CF0010D1C7 /* XMPPPrivacy.m in Sources */, D9DCD5A91E6258CF0010D1C7 /* XMPPRoomCoreDataStorage.m in Sources */, D9DCD5AA1E6258CF0010D1C7 /* NSXMLElement+OMEMO.m in Sources */, D9DCD5AB1E6258CF0010D1C7 /* XMPPPubSub.m in Sources */, D9DCD5AC1E6258CF0010D1C7 /* XMPPCapabilities.xcdatamodel in Sources */, D9DCD5AD1E6258CF0010D1C7 /* XMPPSlot.m in Sources */, + DDA9388D1F7913D100979230 /* XMPPCapabilities+XEP_0308.m in Sources */, D9DCD5AE1E6258CF0010D1C7 /* XMPPMessage+XEP_0172.m in Sources */, D9DCD5AF1E6258CF0010D1C7 /* XMPPvCardCoreDataStorage.m in Sources */, D9DCD5B01E6258CF0010D1C7 /* XMPPURI.m in Sources */, @@ -3759,6 +4155,8 @@ D9DCD5BE1E6258CF0010D1C7 /* XMPPIncomingFileTransfer.m in Sources */, D9DCD5BF1E6258CF0010D1C7 /* XMPPSCRAMSHA1Authentication.m in Sources */, D9DCD5C01E6258CF0010D1C7 /* XMPPIQ+XEP_0357.m in Sources */, + DD1C59881F4429FD003D73DB /* XMPPDelayedDelivery.m in Sources */, + DD855F961F74F2B000E12330 /* XMPPOutOfBandResourceMessaging.m in Sources */, D9DCD5C11E6258CF0010D1C7 /* XMPPvCardAvatarModule.m in Sources */, D9DCD5C21E6258CF0010D1C7 /* XMPPReconnect.m in Sources */, D9DCD5C31E6258CF0010D1C7 /* XMPPvCardTempAdr.m in Sources */, @@ -3776,17 +4174,22 @@ D9DCD5CF1E6258CF0010D1C7 /* XMPPRoom.m in Sources */, D9DCD5D01E6258CF0010D1C7 /* OMEMOBundle.m in Sources */, D9DCD5D11E6258CF0010D1C7 /* XMPPCapsCoreDataStorageObject.m in Sources */, + DD17841A1F3C9FA800D662A6 /* XMPPMessageCoreDataStorageObject.m in Sources */, D9DCD5D21E6258CF0010D1C7 /* XMPPRegistration.m in Sources */, D9DCD5D31E6258CF0010D1C7 /* XMPPRoster.m in Sources */, D9DCD5D41E6258CF0010D1C7 /* XMPPStreamManagementStanzas.m in Sources */, D9DCD5D51E6258CF0010D1C7 /* XMPPvCard.xcdatamodeld in Sources */, D9DCD5D61E6258CF0010D1C7 /* NSDate+XMPPDateTimeProfiles.m in Sources */, + DD2AD6ED1F84B49200E0FED2 /* XMPPManagedMessaging.m in Sources */, D9DCD5D71E6258CF0010D1C7 /* XMPPRoomLight.m in Sources */, + DDA938851F790FAC00979230 /* XMPPLastMessageCorrection.m in Sources */, D9DCD5D81E6258CF0010D1C7 /* XMPPElement.m in Sources */, + DD21471A1F72B1F800D98E31 /* XMPPMessageCoreDataStorage+XEP_0313.m in Sources */, D9DCD5D91E6258CF0010D1C7 /* GCDMulticastDelegate.m in Sources */, D96D6E7D1F8D9701006DEC58 /* XMPPPushModule.m in Sources */, D9DCD5DA1E6258CF0010D1C7 /* XMPPIQ+JabberRPCResonse.m in Sources */, D9DCD5DB1E6258CF0010D1C7 /* XMPPMessageArchiving_Message_CoreDataObject.m in Sources */, + DD1C59AC1F444164003D73DB /* XMPPMessageCoreDataStorage+XEP_0203.m in Sources */, D9DCD5DC1E6258CF0010D1C7 /* OMEMOModule.m in Sources */, D9DCD5DD1E6258CF0010D1C7 /* XMPPIQ+XEP_0066.m in Sources */, D9DCD5DE1E6258CF0010D1C7 /* XMPPMUC.m in Sources */, @@ -3799,12 +4202,17 @@ D9DCD5E51E6258CF0010D1C7 /* XMPPSoftwareVersion.m in Sources */, D9DCD5E61E6258CF0010D1C7 /* XMPPIQ+LastActivity.m in Sources */, D9DCD5E71E6258CF0010D1C7 /* XMPPvCardCoreDataStorageObject.m in Sources */, + DDFFF40F1F3DE10700B99353 /* NSManagedObject+XMPPCoreDataStorage.m in Sources */, + DD06EA4F1F78EDA0008FA8C2 /* XMPPMessageCoreDataStorage+XEP_0184.m in Sources */, D9DCD5E81E6258CF0010D1C7 /* XMPPvCardTempCoreDataStorageObject.m in Sources */, D9DCD5E91E6258CF0010D1C7 /* XMPPMessage+XEP_0333.m in Sources */, D9DCD5EA1E6258CF0010D1C7 /* XMPPRoomMessageMemoryStorageObject.m in Sources */, D9DCD5EB1E6258CF0010D1C7 /* XMPPRosterCoreDataStorage.m in Sources */, D9DCD5EC1E6258CF0010D1C7 /* DDList.m in Sources */, + DD1784201F3C9FA800D662A6 /* XMPPMessageContextCoreDataStorageObject.m in Sources */, D9DCD5ED1E6258CF0010D1C7 /* XMPPSRVResolver.m in Sources */, + DD19E4121F8CA3E200CED8EF /* XMPPMessageCoreDataStorageObject+ContextHelpers.m in Sources */, + DD1784141F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld in Sources */, D9DCD5EE1E6258CF0010D1C7 /* XMPPMessage+XEP_0280.m in Sources */, D9DCD5EF1E6258CF0010D1C7 /* XMPPIDTracker.m in Sources */, D9DCD5F01E6258CF0010D1C7 /* XMPPMessage+XEP_0085.m in Sources */, @@ -4216,6 +4624,16 @@ sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; + DD1784061F3C9FA800D662A6 /* XMPPMessage.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + DD1784071F3C9FA800D662A6 /* XMPPMessage.xcdatamodel */, + ); + currentVersion = DD1784071F3C9FA800D662A6 /* XMPPMessage.xcdatamodel */; + path = XMPPMessage.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; /* End XCVersionGroup section */ }; rootObject = 0D44BAE11E537066000930E0 /* Project object */; diff --git a/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj b/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj index 9106cb5573..d1b7fcbb41 100644 --- a/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj +++ b/Xcode/Testing-Carthage/XMPPFrameworkTests.xcodeproj/project.pbxproj @@ -61,6 +61,27 @@ D9DCD70E1E625C560010D1C7 /* OMEMOTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E0C1D99C48100FB068A /* OMEMOTestStorage.m */; }; D9DCD7191E625CAE0010D1C7 /* XMPPFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9DCD6B01E625A9B0010D1C7 /* XMPPFramework.framework */; }; D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */; }; + DD26F5881F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F5871F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m */; }; + DD26F5891F7CF1B400F54F18 /* XMPPOneToOneChatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F5871F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m */; }; + DD26F58A1F7CF1B400F54F18 /* XMPPOneToOneChatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F5871F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m */; }; + DD4003F91F7528A90078D144 /* XMPPDelayedDeliveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003F81F7528A90078D144 /* XMPPDelayedDeliveryTests.m */; }; + DD4003FA1F7528B40078D144 /* XMPPDelayedDeliveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003F81F7528A90078D144 /* XMPPDelayedDeliveryTests.m */; }; + DD4003FB1F7528B40078D144 /* XMPPDelayedDeliveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003F81F7528A90078D144 /* XMPPDelayedDeliveryTests.m */; }; + DDA11A4C1F8518AE00591D1B /* XMPPManagedMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A4B1F8518AE00591D1B /* XMPPManagedMessagingTests.m */; }; + DDA11A4D1F8518BA00591D1B /* XMPPManagedMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A4B1F8518AE00591D1B /* XMPPManagedMessagingTests.m */; }; + DDA11A4E1F8518BB00591D1B /* XMPPManagedMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A4B1F8518AE00591D1B /* XMPPManagedMessagingTests.m */; }; + DD06EA451F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA441F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */; }; + DD06EA461F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA441F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */; }; + DD06EA471F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA441F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */; }; + DD4003E81F75283D0078D144 /* XMPPOutOfBandResourceMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDB40BAA1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m */; }; + DD4003E91F75283E0078D144 /* XMPPOutOfBandResourceMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDB40BAA1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m */; }; + DDB40BAB1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDB40BAA1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m */; }; + DD203B8A1F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B891F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m */; }; + DD203B8B1F7A6FEE00CA359C /* XMPPLastMessageCorrectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B891F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m */; }; + DD203B8C1F7A6FEE00CA359C /* XMPPLastMessageCorrectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B891F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m */; }; + DD4003DC1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */; }; + DD4003DD1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */; }; + DD4003DE1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -133,6 +154,13 @@ D9DCD3EC1E6255E10010D1C7 /* XMPPFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XMPPFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D9DCD7151E625C560010D1C7 /* XMPPFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XMPPFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOElementTests.m; path = "../Testing-Shared/OMEMOElementTests.m"; sourceTree = SOURCE_ROOT; }; + DD26F5871F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPOneToOneChatTests.m; sourceTree = ""; }; + DD4003F81F7528A90078D144 /* XMPPDelayedDeliveryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPDelayedDeliveryTests.m; sourceTree = ""; }; + DDA11A4B1F8518AE00591D1B /* XMPPManagedMessagingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPManagedMessagingTests.m; sourceTree = ""; }; + DD06EA441F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageDeliveryReceiptsTests.m; sourceTree = ""; }; + DDB40BAA1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPOutOfBandResourceMessagingTests.m; sourceTree = ""; }; + DD203B891F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPLastMessageCorrectionTests.m; sourceTree = ""; }; + DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XMPPMessageCoreDataStorageTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -202,6 +230,13 @@ D973A0791D2F18040096F3ED /* XMPPStorageHintTests.m */, D973A07A1D2F18040096F3ED /* XMPPURITests.m */, D973A07B1D2F18040096F3ED /* XMPPvCardTests.m */, + DD26F5871F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m */, + DD4003F81F7528A90078D144 /* XMPPDelayedDeliveryTests.m */, + DDA11A4B1F8518AE00591D1B /* XMPPManagedMessagingTests.m */, + DD06EA441F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */, + DDB40BAA1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m */, + DD203B891F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m */, + DD4003DB1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m */, 63F50D971C60208200CA0201 /* Info.plist */, ); name = XMPPFrameworkTests; @@ -381,15 +416,22 @@ buildActionMask = 2147483647; files = ( D973A07C1D2F18040096F3ED /* CapabilitiesHashingTest.m in Sources */, + DD4003F91F7528A90078D144 /* XMPPDelayedDeliveryTests.m in Sources */, + DD4003DC1F7527B10078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */, D973A0811D2F18040096F3ED /* XMPPMUCLightTests.m in Sources */, + DD203B8A1F7A6FE900CA359C /* XMPPLastMessageCorrectionTests.m in Sources */, D973A07D1D2F18040096F3ED /* EncodeDecodeTest.m in Sources */, D973A0831D2F18040096F3ED /* XMPPRoomLightCoreDataStorageTests.m in Sources */, D973A0801D2F18040096F3ED /* XMPPMockStream.m in Sources */, + DD26F5881F7CF1AE00F54F18 /* XMPPOneToOneChatTests.m in Sources */, D973A0841D2F18040096F3ED /* XMPPRoomLightTests.m in Sources */, D97509281D9C82DB002E6F51 /* OMEMOServerTests.m in Sources */, + DD06EA451F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */, D99C5E0D1D99C48100FB068A /* OMEMOModuleTests.m in Sources */, D973A0861D2F18040096F3ED /* XMPPURITests.m in Sources */, + DDA11A4C1F8518AE00591D1B /* XMPPManagedMessagingTests.m in Sources */, D973A07F1D2F18040096F3ED /* XMPPMessageArchiveManagementTests.m in Sources */, + DDB40BAB1F75255100B82A93 /* XMPPOutOfBandResourceMessagingTests.m in Sources */, D973A07E1D2F18040096F3ED /* XMPPHTTPFileUploadTests.m in Sources */, D973A0821D2F18040096F3ED /* XMPPPushTests.swift in Sources */, D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */, @@ -405,15 +447,22 @@ buildActionMask = 2147483647; files = ( D9DCD3D51E6255E10010D1C7 /* CapabilitiesHashingTest.m in Sources */, + DD4003FA1F7528B40078D144 /* XMPPDelayedDeliveryTests.m in Sources */, + DD4003DD1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */, D9DCD3D61E6255E10010D1C7 /* XMPPMUCLightTests.m in Sources */, + DD203B8B1F7A6FEE00CA359C /* XMPPLastMessageCorrectionTests.m in Sources */, D9DCD3D71E6255E10010D1C7 /* EncodeDecodeTest.m in Sources */, D9DCD3D81E6255E10010D1C7 /* XMPPRoomLightCoreDataStorageTests.m in Sources */, D9DCD3D91E6255E10010D1C7 /* XMPPMockStream.m in Sources */, + DD26F5891F7CF1B400F54F18 /* XMPPOneToOneChatTests.m in Sources */, D9DCD3DA1E6255E10010D1C7 /* XMPPRoomLightTests.m in Sources */, D9DCD3DB1E6255E10010D1C7 /* OMEMOServerTests.m in Sources */, + DD06EA461F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */, D9DCD3DC1E6255E10010D1C7 /* OMEMOModuleTests.m in Sources */, D9DCD3DD1E6255E10010D1C7 /* XMPPURITests.m in Sources */, + DDA11A4D1F8518BA00591D1B /* XMPPManagedMessagingTests.m in Sources */, D9DCD3DE1E6255E10010D1C7 /* XMPPMessageArchiveManagementTests.m in Sources */, + DD4003E81F75283D0078D144 /* XMPPOutOfBandResourceMessagingTests.m in Sources */, D9DCD3DF1E6255E10010D1C7 /* XMPPHTTPFileUploadTests.m in Sources */, D9DCD3E01E6255E10010D1C7 /* XMPPPushTests.swift in Sources */, D9DCD3E11E6255E10010D1C7 /* OMEMOElementTests.m in Sources */, @@ -429,15 +478,22 @@ buildActionMask = 2147483647; files = ( D9DCD6FE1E625C560010D1C7 /* CapabilitiesHashingTest.m in Sources */, + DD4003FB1F7528B40078D144 /* XMPPDelayedDeliveryTests.m in Sources */, + DD4003DE1F7527D70078D144 /* XMPPMessageCoreDataStorageTests.m in Sources */, D9DCD6FF1E625C560010D1C7 /* XMPPMUCLightTests.m in Sources */, + DD203B8C1F7A6FEE00CA359C /* XMPPLastMessageCorrectionTests.m in Sources */, D9DCD7001E625C560010D1C7 /* EncodeDecodeTest.m in Sources */, D9DCD7011E625C560010D1C7 /* XMPPRoomLightCoreDataStorageTests.m in Sources */, D9DCD7021E625C560010D1C7 /* XMPPMockStream.m in Sources */, + DD26F58A1F7CF1B400F54F18 /* XMPPOneToOneChatTests.m in Sources */, D9DCD7031E625C560010D1C7 /* XMPPRoomLightTests.m in Sources */, D9DCD7041E625C560010D1C7 /* OMEMOServerTests.m in Sources */, + DD06EA471F78EBFD008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */, D9DCD7051E625C560010D1C7 /* OMEMOModuleTests.m in Sources */, D9DCD7061E625C560010D1C7 /* XMPPURITests.m in Sources */, + DDA11A4E1F8518BB00591D1B /* XMPPManagedMessagingTests.m in Sources */, D9DCD7071E625C560010D1C7 /* XMPPMessageArchiveManagementTests.m in Sources */, + DD4003E91F75283E0078D144 /* XMPPOutOfBandResourceMessagingTests.m in Sources */, D9DCD7081E625C560010D1C7 /* XMPPHTTPFileUploadTests.m in Sources */, D9DCD7091E625C560010D1C7 /* XMPPPushTests.swift in Sources */, D9DCD70A1E625C560010D1C7 /* OMEMOElementTests.m in Sources */, diff --git a/Xcode/Testing-Shared/XMPPDelayedDeliveryTests.m b/Xcode/Testing-Shared/XMPPDelayedDeliveryTests.m new file mode 100644 index 0000000000..34b165c8b4 --- /dev/null +++ b/Xcode/Testing-Shared/XMPPDelayedDeliveryTests.m @@ -0,0 +1,131 @@ +#import +#import "XMPPMockStream.h" + +@class XMPPDelayedDeliveryTestCallbackResult; + +@interface XMPPDelayedDeliveryTests : XCTestCase + +@property (nonatomic, strong) XMPPMockStream *mockStream; +@property (nonatomic, strong) XMPPDelayedDelivery *delayedDelivery; +@property (nonatomic, strong) XCTestExpectation *delegateCallbackExpectation; + +@end + +@implementation XMPPDelayedDeliveryTests + +- (void)setUp { + [super setUp]; + + self.mockStream = [[XMPPMockStream alloc] init]; + + self.delayedDelivery = [[XMPPDelayedDelivery alloc] init]; + [self.delayedDelivery addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [self.delayedDelivery activate:self.mockStream]; +} + +- (void)testMessageDelegateCallback +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Test message delegate callback expectation"]; + + [self fakeDelayedDeliveryMessage]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testPresenceDelegateCallback +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Test presence delegate callback expectation"]; + + [self fakeDelayedDeliveryPresence]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testStanzaSkipping +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Test skipped delegate callback expectation"]; + self.delegateCallbackExpectation.inverted = YES; + + [self fakePlainMessage]; + [self fakePlainPresence]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)xmppDelayedDelivery:(XMPPDelayedDelivery *)xmppDelayedDelivery didReceiveDelayedMessage:(XMPPMessage *)delayedMessage +{ + if ([self.delegateCallbackExpectation isInverted] || + ([[delayedMessage delayedDeliveryDate] isEqualToDate:[NSDate dateWithXmppDateTimeString:@"2002-09-10T23:08:25Z"]] + && [[delayedMessage delayedDeliveryFrom] isEqualToJID:[XMPPJID jidWithString:@"capulet.com"]] + && [[delayedMessage delayedDeliveryReasonDescription] isEqualToString:@"Offline Storage"])) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)xmppDelayedDelivery:(XMPPDelayedDelivery *)xmppDelayedDelivery didReceiveDelayedPresence:(XMPPPresence *)delayedPresence +{ + if ([self.delegateCallbackExpectation isInverted] || + ([[delayedPresence delayedDeliveryDate] isEqualToDate:[NSDate dateWithXmppDateTimeString:@"2002-09-10T23:41:07Z"]] + && [[delayedPresence delayedDeliveryFrom] isEqualToJID:[XMPPJID jidWithString:@"juliet@capulet.com/balcony"]] + && [[delayedPresence delayedDeliveryReasonDescription] isEqualToString:@""])) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)fakeDelayedDeliveryMessage +{ + [self.mockStream fakeMessageResponse: + [[XMPPMessage alloc] initWithXMLString: + @"" + @"" + @"O blessed, blessed night! I am afeard." + @"Being in night, all this is but a dream," + @"Too flattering-sweet to be substantial." + @"" + @"" + @"Offline Storage" + @"" + @"" + error:nil]]; +} + +- (void)fakeDelayedDeliveryPresence +{ + [self.mockStream fakeResponse: + [[XMPPPresence alloc] initWithXMLString: + @"" + @"anon!" + @"xa" + @"1" + @"" + @"" + error:nil]]; +} + +- (void)fakePlainMessage +{ + [self.mockStream fakeMessageResponse: + [[XMPPMessage alloc] initWithXMLString: + @"" + @"" + @"O blessed, blessed night! I am afeard." + @"Being in night, all this is but a dream," + @"Too flattering-sweet to be substantial." + @"" + @"" + error:nil]]; +} + +- (void)fakePlainPresence +{ + [self.mockStream fakeResponse: + [[XMPPPresence alloc] initWithXMLString: + @"" + @"anon!" + @"xa" + @"1" + @"" + error:nil]]; +} + +@end diff --git a/Xcode/Testing-Shared/XMPPLastMessageCorrectionTests.m b/Xcode/Testing-Shared/XMPPLastMessageCorrectionTests.m new file mode 100644 index 0000000000..b1943f3275 --- /dev/null +++ b/Xcode/Testing-Shared/XMPPLastMessageCorrectionTests.m @@ -0,0 +1,184 @@ +#import +#import "XMPPMockStream.h" + +@interface XMPPLastMessageCorrectionTests : XCTestCase + +@property (nonatomic, strong) XMPPMockStream *mockStream; +@property (nonatomic, strong) XMPPLastMessageCorrection *lastMessageCorrection; +@property (nonatomic, strong) XCTestExpectation *delegateCallbackExpectation; +@property (nonatomic, strong) XCTestExpectation *outgoingMessageModuleProcessingScheduledExpectation; + +@end + +@implementation XMPPLastMessageCorrectionTests + +- (void)setUp +{ + [super setUp]; + + self.mockStream = [[XMPPMockStream alloc] init]; + self.mockStream.myJID = [XMPPJID jidWithString:@"romeo@montague.net/home"]; + [self.mockStream addDelegate:self delegateQueue:dispatch_get_main_queue()]; + self.lastMessageCorrection = [[XMPPLastMessageCorrection alloc] init]; + [self.lastMessageCorrection addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [self.lastMessageCorrection activate:self.mockStream]; +} + +- (void)testIncomingMessageCorrection +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Incoming message correction filtering delegate callback expectation"]; + + [self fakeIncomingMessageWithID:@"bad" + body:@"O Romeo, Romeo! wherefore art thee Romeo?" + correctedMessageID:nil + senderJID:[XMPPJID jidWithString:@"juliet@capulet.net/balcony1"]]; + + [self fakeIncomingMessageWithID:@"good" + body:@"O Romeo, Romeo! wherefore art thou Romeo?" + correctedMessageID:@"bad" + senderJID:[XMPPJID jidWithString:@"juliet@capulet.net/balcony1"]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)xmppLastMessageCorrection:(XMPPLastMessageCorrection *)xmppLastMessageCorrection didReceiveCorrectedMessage:(XMPPMessage *)correctedMessage +{ + if ([[correctedMessage elementID] isEqualToString:@"good"]) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)testOutgoingMessageCorrectionEligibilty +{ + self.outgoingMessageModuleProcessingScheduledExpectation = [self expectationWithDescription:@"Fake messages sent"]; + self.outgoingMessageModuleProcessingScheduledExpectation.expectedFulfillmentCount = 3; + + [self fakeSendingMessageWithID:@"bad1" recipientJID:[XMPPJID jidWithString:@"juliet@capulet.net/balcony1"]]; + [self fakeSendingMessageWithID:@"bad2" recipientJID:[XMPPJID jidWithString:@"juliet@capulet.net/balcony2"]]; + [self fakeSendingMessageWithID:@"bad3" recipientJID:[XMPPJID jidWithString:@"nurse@capulet.net/balcony"]]; + [self waitForExpectationsWithTimeout:5 handler:nil]; + + XCTAssertFalse([self.lastMessageCorrection canCorrectSentMessageWithID:@"bad1"]); + XCTAssertTrue([self.lastMessageCorrection canCorrectSentMessageWithID:@"bad2"]); + XCTAssertTrue([self.lastMessageCorrection canCorrectSentMessageWithID:@"bad3"]); +} + +- (void)testMUCPostRejoinOutgoingMessageCorrectionEligibilty +{ + self.outgoingMessageModuleProcessingScheduledExpectation = [self expectationWithDescription:@"Fake message sent"]; + + [self fakeSendingMessageWithID:@"bad1" recipientJID:[XMPPJID jidWithString:@"coven@chat.shakespeare.lit"]]; + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [self fakeRejoiningMUCRoomWithJID:[XMPPJID jidWithString:@"coven@chat.shakespeare.lit"]]; + + XCTAssertFalse([self.lastMessageCorrection canCorrectSentMessageWithID:@"bad1"]); +} + +- (void)testMUCLightPostRejoinOutgoingMessageCorrectionEligibilty +{ + self.outgoingMessageModuleProcessingScheduledExpectation = [self expectationWithDescription:@"Fake message sent"]; + + [self fakeSendingMessageWithID:@"bad1" recipientJID:[XMPPJID jidWithString:@"coven@muclight.shakespeare.lit"]]; + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [self fakeRejoiningMUCLightRoomWithJID:[XMPPJID jidWithString:@"coven@muclight.shakespeare.lit"]]; + + XCTAssertFalse([self.lastMessageCorrection canCorrectSentMessageWithID:@"bad1"]); +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + dispatch_async(sender.xmppQueue, ^{ + [self.outgoingMessageModuleProcessingScheduledExpectation fulfill]; + }); +} + +- (void)testCapabilitiesReporting +{ + NSXMLElement *capabilitiesQuery = [self fakeCapabilitiesQuery]; + + NSInteger messageCorrectionFeatureElementCount = 0; + for (NSXMLElement *child in capabilitiesQuery.children) { + if ([child.name isEqualToString:@"feature"] && + [[child attributeForName:@"var"].stringValue isEqualToString:@"urn:xmpp:message-correct:0"]) { + ++messageCorrectionFeatureElementCount; + } + } + + XCTAssertEqual(messageCorrectionFeatureElementCount, 1); +} + +- (void)fakeSendingMessageWithID:(NSString *)messageID recipientJID:(XMPPJID *)toJID +{ + [self.mockStream sendElement: + [[XMPPMessage alloc] initWithXMLString: + [NSString stringWithFormat: + @"" + @" But soft, what light through yonder airlock breaks?" + @"", [toJID full], messageID] + error:nil]]; +} + +- (void)fakeIncomingMessageWithID:(NSString *)messageID body:(NSString *)body correctedMessageID:(NSString *)correctedMessageID senderJID:(XMPPJID *)senderJID +{ + XMPPMessage *fakeMessage = [[XMPPMessage alloc] initWithXMLString: + [NSString stringWithFormat: + @"" + @" O Romeo, Romeo! wherefore art thou Romeo?" + @"", [senderJID full], messageID] + error:nil]; + if (correctedMessageID) { + [fakeMessage addChild:[[NSXMLElement alloc] initWithXMLString: + [NSString stringWithFormat: + @"", correctedMessageID] + error:nil]]; + } + [self.mockStream fakeMessageResponse:fakeMessage]; +} + +- (void)fakeRejoiningMUCRoomWithJID:(XMPPJID *)roomJID +{ + XMPPRoom *fakeRoom = [[XMPPRoom alloc] initWithRoomStorage:[[XMPPRoomMemoryStorage alloc] init] jid:roomJID]; + [fakeRoom activate:self.mockStream]; + + dispatch_sync(self.mockStream.xmppQueue, ^{ + [(id)fakeRoom.multicastDelegate xmppRoomDidJoin:fakeRoom]; + }); +} + +- (void)fakeRejoiningMUCLightRoomWithJID:(XMPPJID *)roomJID +{ + XMPPMUCLight *fakeMUCLight = [[XMPPMUCLight alloc] init]; + [fakeMUCLight activate:self.mockStream]; + + dispatch_sync(self.mockStream.xmppQueue, ^{ + [[fakeMUCLight valueForKey:@"multicastDelegate"] xmppMUCLight:fakeMUCLight + changedAffiliation:@"member" + userJID:[self.mockStream.myJID bareJID] + roomJID:roomJID]; + }); +} + +- (NSXMLElement *)fakeCapabilitiesQuery +{ + XMPPCapabilities *testCapabilities = [[XMPPCapabilities alloc] initWithCapabilitiesStorage:[[XMPPCapabilitiesCoreDataStorage alloc] initWithInMemoryStore]]; + [testCapabilities activate:self.mockStream]; + + NSXMLElement *query = [[NSXMLElement alloc] initWithXMLString:@"" error:nil]; + + dispatch_sync(self.mockStream.xmppQueue, ^{ + GCDMulticastDelegateEnumerator *delegateEnumerator = [[testCapabilities valueForKey:@"multicastDelegate"] delegateEnumerator]; + id delegate; + dispatch_queue_t delegateQueue; + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue forSelector:@selector(xmppCapabilities:collectingMyCapabilities:)]) { + dispatch_sync(delegateQueue, ^{ + [delegate xmppCapabilities:testCapabilities collectingMyCapabilities:query]; + }); + } + }); + + return query; +} + +@end diff --git a/Xcode/Testing-Shared/XMPPMUCLightTests.m b/Xcode/Testing-Shared/XMPPMUCLightTests.m index 2d2f2fca13..e536fd53ad 100644 --- a/Xcode/Testing-Shared/XMPPMUCLightTests.m +++ b/Xcode/Testing-Shared/XMPPMUCLightTests.m @@ -263,7 +263,7 @@ - (void)testChangeAffiliation { }]; } -- (void)xmppMUCLight:(XMPPMUCLight *)sender changedAffiliation:(NSString *)affiliation roomJID:(XMPPJID *)roomJID { +- (void)xmppMUCLight:(XMPPMUCLight *)sender changedAffiliation:(NSString *)affiliation userJID:(XMPPJID *)userJID roomJID:(XMPPJID *)roomJID { XCTAssertEqualObjects(affiliation, @"member"); XCTAssertEqualObjects(roomJID.full, @"coven@muclight.shakespeare.lit"); [self.delegateResponseExpectation fulfill]; diff --git a/Xcode/Testing-Shared/XMPPManagedMessagingTests.m b/Xcode/Testing-Shared/XMPPManagedMessagingTests.m new file mode 100644 index 0000000000..f469063cc1 --- /dev/null +++ b/Xcode/Testing-Shared/XMPPManagedMessagingTests.m @@ -0,0 +1,214 @@ +// +// XMPPManagedMessagingTests.m +// XMPPFrameworkTests +// +// Created by Piotr Wegrzynek on 04/10/2017. +// + +#import +#import "XMPPMockStream.h" + +@class XMPPFakeStreamManagement; + +@interface XMPPManagedMessagingTests : XCTestCase + +@property (nonatomic, strong) XMPPMockStream *mockStream; +@property (nonatomic, strong) XMPPFakeStreamManagement *fakeStreamManagement; +@property (nonatomic, strong) XMPPManagedMessaging *managedMessaging; +@property (nonatomic, strong) XCTestExpectation *delegateCallbackExpectation; + +@end + +@interface XMPPFakeStreamManagement : XMPPStreamManagement + +@property (nonatomic, copy) NSArray *resumeStanzaIDs; + +- (void)fakeReceivingAckForStanzaIDs:(NSArray *)stanzaIDs; + +@end + +@implementation XMPPManagedMessagingTests + +- (void)setUp +{ + [super setUp]; + + self.mockStream = [[XMPPMockStream alloc] init]; + + self.fakeStreamManagement = [[XMPPFakeStreamManagement alloc] initWithStorage:[[XMPPStreamManagementMemoryStorage alloc] init]]; + + self.managedMessaging = [[XMPPManagedMessaging alloc] init]; + [self.managedMessaging addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [self.managedMessaging activate:self.mockStream]; +} + +- (void)testStreamManagementDependency +{ + [self.mockStream addDelegate:self delegateQueue:dispatch_get_main_queue()]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Stream management dependency setup expectation"]; + + [self.fakeStreamManagement activate:self.mockStream]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module +{ + if (module == self.fakeStreamManagement && [[module valueForKey:@"multicastDelegate"] countOfClass:[XMPPManagedMessaging class]] == 1) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)testMessageRegistration +{ + XMPPMessage *message = [[XMPPMessage alloc] initWithXMLString:@"" error:NULL]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Message registration delegate callback expectation"]; + + [self.mockStream sendElement:message]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testMessageWithoutIDHandling +{ + XMPPMessage *message = [[XMPPMessage alloc] initWithXMLString:@"" error:NULL]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Message without ID registration delegate callback expectation"]; + self.delegateCallbackExpectation.inverted = YES; + + [self.mockStream sendElement:message]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)xmppManagedMessaging:(XMPPManagedMessaging *)sender didBeginMonitoringOutgoingMessage:(XMPPMessage *)message +{ + if (![message elementID] || [[message elementID] isEqualToString:@"elementID"]) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)testStanzaIDAssignment +{ + XMPPMessage *message = [[XMPPMessage alloc] initWithXMLString:@"" error:NULL]; + + __block id messageID; + dispatch_sync(self.managedMessaging.moduleQueue, ^{ + messageID = [(id)self.managedMessaging xmppStreamManagement:self.fakeStreamManagement stanzaIdForSentElement:message]; + }); + + XCTAssertEqualObjects(messageID, [NSURL URLWithString:@"xmppmanagedmessage:elementID"]); +} + +- (void)testBasicMessageAcknowledgement +{ + NSURL *managedMessageURL = [NSURL URLWithString:@"xmppmanagedmessage:elementID"]; + [self.fakeStreamManagement activate:self.mockStream]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Basic message acknowledgement delegate callback expectation"]; + + [self.fakeStreamManagement fakeReceivingAckForStanzaIDs:@[managedMessageURL]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testStreamResumptionMessageAcknowledgement +{ + self.fakeStreamManagement.resumeStanzaIDs = @[[NSURL URLWithString:@"xmppmanagedmessage:elementID"]]; + [self.fakeStreamManagement activate:self.mockStream]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Stream resumption message acknowledgement delegate callback expectation"]; + self.delegateCallbackExpectation.expectedFulfillmentCount = 2; + + [[self.mockStream valueForKey:@"multicastDelegate"] xmppStreamDidAuthenticate:self.mockStream]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testStreamResumptionDuplicateMessageAcknowledgementHandling +{ + self.fakeStreamManagement.resumeStanzaIDs = @[[NSURL URLWithString:@"xmppmanagedmessage:elementID"]]; + [self.fakeStreamManagement activate:self.mockStream]; + NSURL *managedMessageURL = [NSURL URLWithString:@"xmppmanagedmessage:elementID"]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Stream resumption duplicate message acknowledgement delegate callback expectation"]; + self.delegateCallbackExpectation.inverted = YES; + + [self.fakeStreamManagement fakeReceivingAckForStanzaIDs:@[managedMessageURL]]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testAuxiliaryIQHandling +{ + XMPPIQ *iq = [[XMPPIQ alloc] initWithXMLString:@"" error:NULL]; + [self.fakeStreamManagement activate:self.mockStream]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Non-message registration delegate callback expectation"]; + self.delegateCallbackExpectation.inverted = YES; + + [self.mockStream sendElement:iq]; + [self.fakeStreamManagement fakeReceivingAckForStanzaIDs:@[@"elementID"]]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testAuxiliaryPresenceHandling +{ + XMPPPresence *presence = [[XMPPPresence alloc] initWithXMLString:@"" error:NULL]; + [self.fakeStreamManagement activate:self.mockStream]; + + self.delegateCallbackExpectation = [self expectationWithDescription:@"Non-message registration delegate callback expectation"]; + self.delegateCallbackExpectation.inverted = YES; + + [self.mockStream sendElement:presence]; + [self.fakeStreamManagement fakeReceivingAckForStanzaIDs:@[@"elementID"]]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)xmppManagedMessaging:(XMPPManagedMessaging *)sender didConfirmSentMessagesWithIDs:(NSArray *)messageIDs +{ + if ([messageIDs isEqualToArray:@[@"elementID"]]) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)xmppManagedMessagingDidFinishProcessingPreviousStreamConfirmations:(XMPPManagedMessaging *)sender +{ + [self.delegateCallbackExpectation fulfill]; +} + +- (void)fakeIncomingManagedMessagingDelegateCallbackWithBlock:(void (^)(id managedMessaging))block +{ + dispatch_async(self.managedMessaging.moduleQueue, ^{ + block(self.managedMessaging); + }); +} + +@end + +@implementation XMPPFakeStreamManagement + +- (Class)class +{ + // Required by XMPPStream auto delegates feature + return [XMPPStreamManagement class]; +} + +- (void)fakeReceivingAckForStanzaIDs:(NSArray *)stanzaIDs +{ + dispatch_async(self.moduleQueue, ^{ + [multicastDelegate xmppStreamManagement:self didReceiveAckForStanzaIds:stanzaIDs]; + }); +} + +- (BOOL)didResumeWithAckedStanzaIds:(NSArray *__autoreleasing *)stanzaIdsPtr serverResponse:(NSXMLElement *__autoreleasing *)responsePtr +{ + *stanzaIdsPtr = self.resumeStanzaIDs; + return YES; +} + +@end diff --git a/Xcode/Testing-Shared/XMPPMessageArchiveManagementTests.m b/Xcode/Testing-Shared/XMPPMessageArchiveManagementTests.m index 3d78399b96..f39eb7eff4 100644 --- a/Xcode/Testing-Shared/XMPPMessageArchiveManagementTests.m +++ b/Xcode/Testing-Shared/XMPPMessageArchiveManagementTests.m @@ -9,9 +9,9 @@ #import #import "XMPPMockStream.h" -@interface XMPPMessageArchiveManagementTests : XCTestCase +@interface XMPPMessageArchiveManagementTests : XCTestCase -@property (nonatomic, strong) XCTestExpectation *delegateExpectation; +@property (nonatomic, copy) NSDictionary *delegateExpectations; @end @@ -117,8 +117,9 @@ - (void)testRetrieveTargetedMessageArchive { } - (void)testDelegateDidReceiveMAMMessage { - self.delegateExpectation = [self expectationWithDescription:@"Delegate"]; - + self.delegateExpectations = @{ NSStringFromSelector(@selector(xmppMessageArchiveManagement:didReceiveMAMMessage:)) : + [self expectationWithDescription:@"Did receive MAM message"] }; + XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; XMPPMessageArchiveManagement *messageArchiveManagement = [[XMPPMessageArchiveManagement alloc] init]; @@ -145,11 +146,14 @@ - (void)testDelegateDidReceiveMAMMessage { - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didReceiveMAMMessage:(XMPPMessage *)message{ - [self.delegateExpectation fulfill]; + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; } - (void)testDelegateDidReceiveIQ { - self.delegateExpectation = [self expectationWithDescription:@"Delegate"]; + self.delegateExpectations = @{ NSStringFromSelector(@selector(xmppMessageArchiveManagement:didFinishReceivingMessagesWithArchiveIDs:)) : + [self expectationWithDescription:@"Did finish receiving messages with archive IDs"], + NSStringFromSelector(@selector(xmppMessageArchiveManagement:didFinishReceivingMessagesWithSet:)) : + [self expectationWithDescription:@"Did finish receiving messages with set"] }; XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; @@ -159,6 +163,10 @@ - (void)testDelegateDidReceiveIQ { __weak typeof(XMPPMockStream) *weakStreamTest = streamTest; streamTest.elementReceived = ^void(NSXMLElement *element) { + NSString *queryID = [[element elementForName:@"query"] attributeStringValueForName:@"queryid"]; + XMPPMessage *fakeMessageResponse = [self fakeMessageWithQueryID:queryID eid:@"responseID"]; + [weakStreamTest fakeMessageResponse:fakeMessageResponse]; + NSString *elementID = [element attributeForName:@"id"].stringValue; XMPPIQ *fakeIQResponse = [self fakeIQWithID:elementID]; [weakStreamTest fakeIQResponse:fakeIQResponse]; @@ -173,6 +181,13 @@ - (void)testDelegateDidReceiveIQ { }]; } +- (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFinishReceivingMessagesWithArchiveIDs:(NSArray *)archiveIDs +{ + XCTAssertEqualObjects(@[@"28482-98726-73623"], archiveIDs); + + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; +} + - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFinishReceivingMessagesWithSet:(XMPPResultSet *)resultSet { XCTAssertEqualObjects(@"28482-98726-73623", resultSet.first); @@ -180,11 +195,12 @@ - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessage XCTAssertEqual(20, resultSet.count); XCTAssertEqual(0, resultSet.firstIndex); - [self.delegateExpectation fulfill]; + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; } - (void)testDelegateDidReceiveError { - self.delegateExpectation = [self expectationWithDescription:@"Delegate"]; + self.delegateExpectations = @{ NSStringFromSelector(@selector(xmppMessageArchiveManagement:didFailToReceiveMessages:)) : + [self expectationWithDescription:@"Did fail to receive messages"] }; XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; @@ -209,12 +225,13 @@ - (void)testDelegateDidReceiveError { } - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFailToReceiveMessages:(XMPPIQ *)error { - [self.delegateExpectation fulfill]; + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; } - (void)testRetrievingFormFields { - self.delegateExpectation = [self expectationWithDescription:@"Delegate"]; + self.delegateExpectations = @{ NSStringFromSelector(@selector(xmppMessageArchiveManagement:didReceiveFormFields:)) : + [self expectationWithDescription:@"Did receive form fields"] }; XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; @@ -239,11 +256,12 @@ - (void)testRetrievingFormFields { } - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didReceiveFormFields:(XMPPIQ *)iq { - [self.delegateExpectation fulfill]; + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; } - (void)testFailToRetrievingFormFields { - self.delegateExpectation = [self expectationWithDescription:@"Delegate"]; + self.delegateExpectations = @{ NSStringFromSelector(@selector(xmppMessageArchiveManagement:didFailToReceiveFormFields:)) : + [self expectationWithDescription:@"Did fail to receive form fields"] }; XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; @@ -268,7 +286,7 @@ - (void)testFailToRetrievingFormFields { } - (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didFailToReceiveFormFields:(XMPPIQ *)iq { - [self.delegateExpectation fulfill]; + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; } - (void)testResultAutomaticPaging { @@ -306,6 +324,49 @@ - (void)testResultAutomaticPaging { }]; } +- (void)testPayloadMessageSubmission { + self.delegateExpectations = @{ NSStringFromSelector(@selector(xmppMessageArchiveManagement:didSubmitPayloadMessageFromQueryResult:)) : + [self expectationWithDescription:@"Did submit payload message from query result"], + NSStringFromSelector(@selector(xmppStream:didReceiveMessage:)) : + [self expectationWithDescription:@"Did receive message"] }; + + XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; + [streamTest addDelegate:self delegateQueue:dispatch_get_main_queue()]; + + __weak XMPPMockStream *weakStreamTest = streamTest; + streamTest.elementReceived = ^void(NSXMLElement *element) { + XMPPIQ *iq = [XMPPIQ iqFromElement:element]; + NSString *queryID = [[iq elementForName:@"query"] attributeStringValueForName:@"queryid"]; + XMPPMessage *fakeMessage = [self fakeMessageWithQueryID:queryID eid:@"responseID"]; + [weakStreamTest fakeMessageResponse:fakeMessage]; + }; + + XMPPMessageArchiveManagement *messageArchiveManagement = [[XMPPMessageArchiveManagement alloc] init]; + messageArchiveManagement.submitsPayloadMessagesForStreamProcessing = YES; + [messageArchiveManagement addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [messageArchiveManagement activate:streamTest]; + [messageArchiveManagement retrieveMessageArchiveWithFields:nil withResultSet:nil]; + + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)xmppMessageArchiveManagement:(XMPPMessageArchiveManagement *)xmppMessageArchiveManagement didSubmitPayloadMessageFromQueryResult:(NSXMLElement *)result +{ + if ([[[result forwardedMessage] body] isEqualToString:@"Hail to thee"]) { + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message { + if ([[message body] isEqualToString:@"Hail to thee"]) { + [self.delegateExpectations[NSStringFromSelector(_cmd)] fulfill]; + } +} + - (XMPPMessage *)fakeMessageWithQueryID:(NSString *)queryID eid:(NSString*)eid{ NSString *resultOpenXML = [NSString stringWithFormat:@"",queryID]; diff --git a/Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m b/Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m new file mode 100644 index 0000000000..09eaa96eaa --- /dev/null +++ b/Xcode/Testing-Shared/XMPPMessageCoreDataStorageTests.m @@ -0,0 +1,1754 @@ +// +// XMPPMessageCoreDataStorageTests.m +// XMPPFrameworkTests +// +// Created by Piotr Wegrzynek on 10/08/2017. +// +// + +#import +#import "XMPPMockStream.h" +@import XMPPFramework; + +@interface XMPPMessageCoreDataStorageTests : XCTestCase + +@property (nonatomic, strong) XMPPMessageCoreDataStorage *storage; + +@end + +@implementation XMPPMessageCoreDataStorageTests + +- (void)setUp +{ + [super setUp]; + + self.storage = [[XMPPMessageCoreDataStorage alloc] initWithDatabaseFilename:NSStringFromSelector(self.invocation.selector) + storeOptions:nil]; + self.storage.autoRemovePreviousDatabaseFile = YES; +} + +- (void)testMessageTransientPropertyDirectUpdates +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.fromJID = [XMPPJID jidWithString:@"user1@domain1/resource1"]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + + [self.storage.mainThreadManagedObjectContext save:NULL]; + [self.storage.mainThreadManagedObjectContext refreshObject:message mergeChanges:NO]; + + XCTAssertEqualObjects(message.fromJID, [XMPPJID jidWithString:@"user1@domain1/resource1"]); + XCTAssertEqualObjects(message.toJID, [XMPPJID jidWithString:@"user2@domain2/resource2"]); +} + +- (void)testMessageTransientPropertyMergeUpdates +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.fromJID = [XMPPJID jidWithString:@"user1@domain1/resource1"]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSRefreshedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self.storage scheduleBlock:^{ + XMPPMessageCoreDataStorageObject *storageMessage = [self.storage.managedObjectContext objectWithID:message.objectID]; + storageMessage.fromJID = [XMPPJID jidWithString:@"user1a@domain1a/resource1a"]; + storageMessage.toJID = [XMPPJID jidWithString:@"user2a@domain2a/resource2a"]; + [self.storage save]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssert([message.fromJID isEqualToJID:[XMPPJID jidWithString:@"user1a@domain1a/resource1a"]]); + XCTAssert([message.toJID isEqualToJID:[XMPPJID jidWithString:@"user2a@domain2a/resource2a"]]); + }]; +} + +- (void)testMessageTransientPropertyKeyValueObserving +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + [self keyValueObservingExpectationForObject:message + keyPath:NSStringFromSelector(@selector(fromJID)) + expectedValue:[XMPPJID jidWithString:@"user1@domain1/resource1"]]; + [self keyValueObservingExpectationForObject:message + keyPath:NSStringFromSelector(@selector(toJID)) + expectedValue:[XMPPJID jidWithString:@"user2@domain2/resource2"]]; + + message.fromJID = [XMPPJID jidWithString:@"user1@domain1/resource1"]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + + [self waitForExpectationsWithTimeout:0 handler:nil]; +} + +- (void)testIncomingMessageRegistration +{ + NSDictionary *messageTypes = @{@"chat": @(XMPPMessageTypeChat), + @"error": @(XMPPMessageTypeError), + @"groupchat": @(XMPPMessageTypeGroupchat), + @"headline": @(XMPPMessageTypeHeadline), + @"normal": @(XMPPMessageTypeNormal)}; + + for (NSString *typeString in messageTypes) { + NSMutableString *messageString = [NSMutableString string]; + [messageString appendFormat: @"", typeString]; + [messageString appendString: @" body"]; + [messageString appendString: @" subject"]; + [messageString appendString: @" thread"]; + [messageString appendString: @""]; + + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionIncoming; + [message registerIncomingMessageStreamEventID:[NSString stringWithFormat:@"eventID_%@", typeString] + streamJID:[XMPPJID jidWithString:@"user2@domain2/resource2"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [message registerIncomingMessageCore:[[XMPPMessage alloc] initWithXMLString:messageString error:NULL]]; + + XCTAssertEqualObjects(message.fromJID, [XMPPJID jidWithString:@"user1@domain1/resource1"]); + XCTAssertEqualObjects(message.toJID, [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects(message.body, @"body"); + XCTAssertEqualObjects(message.stanzaID, @"messageID"); + XCTAssertEqualObjects(message.subject, @"subject"); + XCTAssertEqualObjects(message.thread, @"thread"); + XCTAssertEqual(message.type, messageTypes[typeString].intValue); + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + } +} + +- (void)testOutgoingMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"outgoingMessageEventID"]; + + XMPPMessageCoreDataStorageObject *foundMessage = [XMPPMessageCoreDataStorageObject findWithStreamEventID:@"outgoingMessageEventID" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + XCTAssertEqualObjects(message, foundMessage); +} + +- (void)testSentMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"outgoingMessageEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user@domain/resource"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); +} + +- (void)testRepeatedSentMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"initialEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user1@domain1/resource1"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [message registerOutgoingMessageStreamEventID:@"subsequentEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user2@domain2/resource2"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:1]); +} + +- (void)testRetiredSentMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"eventID"]; + [message retireStreamTimestamp]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"user@domain/resource"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); +} + +- (void)testBasicStreamTimestampMessageContextFetch +{ + XMPPMessageCoreDataStorageObject *firstMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + firstMessage.direction = XMPPMessageDirectionIncoming; + [firstMessage registerIncomingMessageStreamEventID:@"firstMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XMPPMessageCoreDataStorageObject *secondMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + secondMessage.direction = XMPPMessageDirectionIncoming; + [secondMessage registerIncomingMessageStreamEventID:@"secondMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:[XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate] + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 2); + XCTAssertEqualObjects(result[0].message, firstMessage); + XCTAssertEqualObjects(result[1].message, secondMessage); +} + +- (void)testRetiredStreamTimestampMessageContextFetch +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"retiredMessageEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [message registerOutgoingMessageStreamEventID:@"retiringMessageEventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:[XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate] + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects([result[0].message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:1]); +} + +- (void)testRelevantMessageJIDContextFetch +{ + XMPPMessageCoreDataStorageObject *incomingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + incomingMessage.direction = XMPPMessageDirectionIncoming; + [incomingMessage registerIncomingMessageStreamEventID:@"incomingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user1@domain1/resource1"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [incomingMessage registerIncomingMessageCore:[[XMPPMessage alloc] initWithXMLString:@"" error:NULL]]; + + XMPPMessageCoreDataStorageObject *outgoingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + outgoingMessage.direction = XMPPMessageDirectionOutgoing; + outgoingMessage.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + [outgoingMessage registerOutgoingMessageStreamEventID:@"outgoingMessageEventID"]; + [outgoingMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"user1@domain1/resource1"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSPredicate *fromJIDPredicate = + [XMPPMessageContextItemCoreDataStorageObject messageFromJIDPredicateWithValue:[XMPPJID jidWithString:@"user2@domain2/resource2"] + compareOptions:XMPPJIDCompareFull]; + NSFetchRequest *fromJIDFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:fromJIDPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fromJIDResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:fromJIDFetchRequest error:NULL]; + + NSPredicate *toJIDPredicate = [XMPPMessageContextItemCoreDataStorageObject messageToJIDPredicateWithValue:[XMPPJID jidWithString:@"user2@domain2/resource2"] + compareOptions:XMPPJIDCompareFull]; + NSFetchRequest *toJIDFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:toJIDPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *toJIDResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:toJIDFetchRequest error:NULL]; + + NSPredicate *remotePartyJIDPredicate = + [XMPPMessageContextItemCoreDataStorageObject messageRemotePartyJIDPredicateWithValue:[XMPPJID jidWithString:@"user2@domain2/resource2"] + compareOptions:XMPPJIDCompareFull]; + NSFetchRequest *remotePartyJIDFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:remotePartyJIDPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *remotePartyJIDResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:remotePartyJIDFetchRequest error:NULL]; + + XCTAssertEqual(fromJIDResult.count, 1); + XCTAssertEqualObjects(fromJIDResult[0].message, incomingMessage); + + XCTAssertEqual(toJIDResult.count, 1); + XCTAssertEqualObjects(toJIDResult[0].message, outgoingMessage); + + XCTAssertEqual(remotePartyJIDResult.count, 2); + XCTAssertEqualObjects(remotePartyJIDResult[0].message, incomingMessage); + XCTAssertEqualObjects(remotePartyJIDResult[1].message, outgoingMessage); +} + +- (void)testTimestampRangeContextFetch +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionIncoming; + [message registerIncomingMessageStreamEventID:@"eventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + NSPredicate *startEndPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:-1] + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + NSFetchRequest *startEndFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startEndPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startEndResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startEndFetchRequest error:NULL]; + + NSPredicate *startEndEdgeCasePredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + NSFetchRequest *startEndEdgeCaseFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startEndEdgeCasePredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startEndEdgeCaseResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startEndEdgeCaseFetchRequest error:NULL]; + + NSPredicate *startPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:-1] + endValue:nil]; + NSFetchRequest *startFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startFetchRequest error:NULL]; + + NSPredicate *startEdgeCasePredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + endValue:nil]; + NSFetchRequest *startEdgeCaseFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:startEdgeCasePredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *startEdgeCaseResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:startEdgeCaseFetchRequest error:NULL]; + + NSPredicate *endPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:nil + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + NSFetchRequest *endFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:endPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *endResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:endFetchRequest error:NULL]; + + NSPredicate *endEdgeCasePredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:nil + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + NSFetchRequest *endEdgeCaseFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:endEdgeCasePredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *endEdgeCaseResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:endEdgeCaseFetchRequest error:NULL]; + + NSPredicate *missPredicate = + [XMPPMessageContextItemCoreDataStorageObject timestampRangePredicateWithStartValue:[NSDate dateWithTimeIntervalSinceReferenceDate:1] + endValue:[NSDate dateWithTimeIntervalSinceReferenceDate:2]]; + NSFetchRequest *missFetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:missPredicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *missResult = + [self.storage.mainThreadManagedObjectContext executeFetchRequest:missFetchRequest error:NULL]; + + XCTAssertEqual(startEndResult.count, 1); + XCTAssertEqualObjects(startEndResult[0].message, message); + XCTAssertEqual(startEndEdgeCaseResult.count, 1); + XCTAssertEqualObjects(startEndEdgeCaseResult[0].message, message); + + XCTAssertEqual(startResult.count, 1); + XCTAssertEqualObjects(startResult[0].message, message); + XCTAssertEqual(startEdgeCaseResult.count, 1); + XCTAssertEqualObjects(startEdgeCaseResult[0].message, message); + + XCTAssertEqual(endResult.count, 1); + XCTAssertEqualObjects(endResult[0].message, message); + XCTAssertEqual(endEdgeCaseResult.count, 1); + XCTAssertEqualObjects(endEdgeCaseResult[0].message, message); + + XCTAssertEqual(missResult.count, 0); +} + +- (void)testMessageSubjectContextFetch +{ + XMPPMessageCoreDataStorageObject *matchingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingMessage.direction = XMPPMessageDirectionIncoming; + [matchingMessage registerIncomingMessageStreamEventID:@"matchingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + matchingMessage.subject = @"I implore you!"; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XMPPMessageContentCompareOptions options = XMPPMessageContentCompareCaseInsensitive|XMPPMessageContentCompareDiacriticInsensitive; + + NSPredicate *equalityPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"I implore you!" + compareOperator:XMPPMessageContentCompareOperatorEquals + options:options]; + NSPredicate *prefixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"i implore" + compareOperator:XMPPMessageContentCompareOperatorBeginsWith + options:options]; + NSPredicate *containmentPredicate = + [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"implore" + compareOperator:XMPPMessageContentCompareOperatorContains + options:options]; + NSPredicate *suffixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"you!" + compareOperator:XMPPMessageContentCompareOperatorEndsWith + options:options]; + NSPredicate *likePredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"I implore *!" + compareOperator:XMPPMessageContentCompareOperatorLike + options:options]; + NSPredicate *matchPredicate = [XMPPMessageContextItemCoreDataStorageObject messageSubjectPredicateWithValue:@"I implore .*!" + compareOperator:XMPPMessageContentCompareOperatorMatches + options:options]; + + for (NSPredicate *predicate in @[equalityPredicate, prefixPredicate, containmentPredicate, suffixPredicate, likePredicate, matchPredicate]) { + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message, matchingMessage); + } +} + +- (void)testMessageBodyContextFetch +{ + XMPPMessageCoreDataStorageObject *matchingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingMessage.direction = XMPPMessageDirectionIncoming; + [matchingMessage registerIncomingMessageStreamEventID:@"matchingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + matchingMessage.body = @"Wherefore art thou, Romeo?"; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XMPPMessageContentCompareOptions options = XMPPMessageContentCompareCaseInsensitive|XMPPMessageContentCompareDiacriticInsensitive; + + NSPredicate *equalityPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"Wherefore art thou, Romeo?" + compareOperator:XMPPMessageContentCompareOperatorEquals + options:options]; + NSPredicate *prefixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"wherefore" + compareOperator:XMPPMessageContentCompareOperatorBeginsWith + options:options]; + NSPredicate *containmentPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"art thou" + compareOperator:XMPPMessageContentCompareOperatorContains + options:options]; + NSPredicate *suffixPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"romeo?" + compareOperator:XMPPMessageContentCompareOperatorEndsWith + options:options]; + NSPredicate *likePredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"Wherefore art thou, *" + compareOperator:XMPPMessageContentCompareOperatorLike + options:options]; + NSPredicate *matchPredicate = [XMPPMessageContextItemCoreDataStorageObject messageBodyPredicateWithValue:@"Wherefore art thou, .*" + compareOperator:XMPPMessageContentCompareOperatorMatches + options:options]; + + for (NSPredicate *predicate in @[equalityPredicate, prefixPredicate, containmentPredicate, suffixPredicate, likePredicate, matchPredicate]) { + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message, matchingMessage); + } +} + +- (void)testMessageThreadContextFetch +{ + XMPPMessageCoreDataStorageObject *matchingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingMessage.direction = XMPPMessageDirectionIncoming; + [matchingMessage registerIncomingMessageStreamEventID:@"matchingMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + matchingMessage.thread = @"e0ffe42b28561960c6b12b944a092794b9683a38"; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject messageThreadPredicateWithValue:@"e0ffe42b28561960c6b12b944a092794b9683a38"]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message, matchingMessage); +} + +- (void)testMessageTypeContextFetch +{ + NSArray *messageTypes = @[@(XMPPMessageTypeChat), + @(XMPPMessageTypeError), + @(XMPPMessageTypeGroupchat), + @(XMPPMessageTypeHeadline), + @(XMPPMessageTypeNormal)]; + for (NSNumber *typeNumber in messageTypes) { + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionIncoming; + [message registerIncomingMessageStreamEventID:[NSString stringWithFormat:@"message%@EventID", typeNumber] + streamJID:[XMPPJID jidWithString:@"user@domain/resource"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + message.stanzaID = [NSString stringWithFormat:@"message%@ID", typeNumber]; + message.type = typeNumber.integerValue; + } + + for (NSNumber *typeNumber in messageTypes) { + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject messageTypePredicateWithValue:typeNumber.integerValue]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest error:NULL]; + + XCTAssertEqual(result.count, 1); + XCTAssertEqualObjects(result[0].message.stanzaID, ([NSString stringWithFormat:@"message%@ID", typeNumber])); + } +} + +- (void)testCoreMessageCreation +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.toJID = [XMPPJID jidWithString:@"user2@domain2/resource2"]; + message.body = @"body"; + message.stanzaID = @"messageID"; + message.subject = @"subject"; + message.thread = @"thread"; + + NSDictionary *messageTypes = @{@"chat": @(XMPPMessageTypeChat), + @"error": @(XMPPMessageTypeError), + @"groupchat": @(XMPPMessageTypeGroupchat), + @"headline": @(XMPPMessageTypeHeadline), + @"normal": @(XMPPMessageTypeNormal)}; + + for (NSString *typeString in messageTypes){ + message.type = messageTypes[typeString].intValue; + + XMPPMessage *xmppMessage = [message coreMessage]; + + XCTAssertEqualObjects([xmppMessage to], [XMPPJID jidWithString:@"user2@domain2/resource2"]); + XCTAssertEqualObjects([xmppMessage body], @"body"); + XCTAssertEqualObjects([xmppMessage elementID], @"messageID"); + XCTAssertEqualObjects([xmppMessage subject], @"subject"); + XCTAssertEqualObjects([xmppMessage thread], @"thread"); + XCTAssertEqualObjects([xmppMessage type], typeString); + } +} + +- (void)testIncomingMessageEventStorageTransactionBatching +{ + // Delayed saving would interfere with the test objective, i.e. ensuring the actions are performed in a single batch + self.storage.saveThreshold = 0; + + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"romeo@example.net"]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + messageObject.fromJID = [XMPPJID jidWithString:@"juliet@example.com"]; + messageObject.toJID = [XMPPJID jidWithString:@"romeo@example.net"]; + }]; + + [transaction scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + messageObject.body = @"Art thou not Romeo, and a Montague?"; + }]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XMPPMessageCoreDataStorageObject *message = [XMPPMessageCoreDataStorageObject findWithStreamEventID:@"eventID" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + XCTAssertNotNil(message); + XCTAssertEqual(message.direction, XMPPMessageDirectionIncoming); + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"romeo@example.net"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + XCTAssertEqualObjects([message fromJID], [XMPPJID jidWithString:@"juliet@example.com"]); + XCTAssertEqualObjects([message toJID], [XMPPJID jidWithString:@"romeo@example.net"]); + XCTAssertEqualObjects([message body], @"Art thou not Romeo, and a Montague?"); + }]; +} + +- (void)testOutgoingMessageStorageInsertion +{ + XMPPMessageCoreDataStorageObject *message = [self.storage insertOutgoingMessageStorageObject]; + XCTAssertEqual(message.direction, XMPPMessageDirectionOutgoing); +} + +- (void)testOutgoingMessageEventStorageTransactionBatching +{ + // Delayed saving would interfere with the test objective, i.e. ensuring the actions are performed in a single batch + self.storage.saveThreshold = 0; + + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"juliet@example.com"]; + + XMPPMessageCoreDataStorageObject *message = [self.storage insertOutgoingMessageStorageObject]; + [message registerOutgoingMessageStreamEventID:@"eventID"]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSRefreshedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeOutgoingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + messageObject.fromJID = [XMPPJID jidWithString:@"juliet@example.com"]; + messageObject.toJID = [XMPPJID jidWithString:@"romeo@example.net"]; + }]; + + [transaction scheduleStorageUpdateWithBlock:^(XMPPMessageCoreDataStorageObject * _Nonnull messageObject) { + messageObject.body = @"Art thou not Romeo, and a Montague?"; + }]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XMPPMessageCoreDataStorageObject *updatedMessage = [XMPPMessageCoreDataStorageObject findWithStreamEventID:@"eventID" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + XCTAssertEqualObjects(updatedMessage, message); + XCTAssertEqualObjects([updatedMessage streamJID], [XMPPJID jidWithString:@"juliet@example.com"]); + XCTAssertEqualObjects([updatedMessage streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + XCTAssertEqualObjects([updatedMessage fromJID], [XMPPJID jidWithString:@"juliet@example.com"]); + XCTAssertEqualObjects([updatedMessage toJID], [XMPPJID jidWithString:@"romeo@example.net"]); + XCTAssertEqualObjects([updatedMessage body], @"Art thou not Romeo, and a Montague?"); + }]; +} + +- (void)provideTransactionForFakeIncomingMessageEventInStream:(XMPPMockStream *)stream withID:(NSString *)eventID timestamp:(NSDate *)timestamp block:(void (^)(XMPPMessageCoreDataStorageTransaction *transaction))block +{ + [stream fakeCurrentEventWithID:eventID timestamp:timestamp forActionWithBlock:^{ + [self.storage provideTransactionForIncomingMessageEvent:[stream currentElementEvent] withHandler:block]; + }]; +} + +- (void)provideTransactionForFakeOutgoingMessageEventInStream:(XMPPMockStream *)stream withID:(NSString *)eventID timestamp:(NSDate *)timestamp block:(void (^)(XMPPMessageCoreDataStorageTransaction *transaction))block +{ + [stream fakeCurrentEventWithID:eventID timestamp:timestamp forActionWithBlock:^{ + [self.storage provideTransactionForOutgoingMessageEvent:[stream currentElementEvent] withHandler:block]; + }]; +} + +- (XCTestExpectation *)expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:(NSString *)userInfoKey count:(NSInteger)expectedObjectCount handler:(BOOL (^)(__kindof NSManagedObject *object))handler +{ + return [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler: + ^BOOL(NSNotification * _Nonnull notification) { + return [notification.userInfo[userInfoKey] objectsPassingTest:^BOOL(id _Nonnull obj, BOOL * _Nonnull stop) { + return handler ? handler(obj) : YES; + }].count == expectedObjectCount; + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPOneToOneChat) + +- (void)testIncomingChatMessageHandling +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"romeo@example.net"]; + + XMPPMessage *message = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Art thou not Romeo, and a Montague?" + @"" + error:nil]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeReceivedChatMessage:message]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSFetchRequest *fetchRequest = [XMPPMessageCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects(fetchResult.firstObject.fromJID, [XMPPJID jidWithString:@"juliet@example.com"]); + XCTAssertEqualObjects(fetchResult.firstObject.toJID, [XMPPJID jidWithString:@"romeo@example.net"]); + XCTAssertEqualObjects(fetchResult.firstObject.body, @"Art thou not Romeo, and a Montague?"); + XCTAssertEqual(fetchResult.firstObject.direction, XMPPMessageDirectionIncoming); + XCTAssertEqual(fetchResult.firstObject.type, XMPPMessageTypeChat); + XCTAssertEqualObjects([fetchResult.firstObject streamJID], [XMPPJID jidWithString:@"romeo@example.net"]); + XCTAssertEqualObjects([fetchResult.firstObject streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + }]; +} + +- (void)testSentChatMessageHandling +{ + XMPPMessageCoreDataStorageObject *message = [self.storage insertOutgoingMessageStorageObject]; + [message registerOutgoingMessageStreamEventID:@"eventID"]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"juliet@example.com"]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self provideTransactionForFakeOutgoingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerSentChatMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"juliet@example.com"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPMUCLight) + +- (void)testIncomingRoomLightMessageHandling +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"crone1@shakespeare.lit"]; + + XMPPMessage *message = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Harpier cries: 'tis time, 'tis time." + @"" + error:nil]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeReceivedRoomLightMessage:message]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XMPPMessageCoreDataStorageObject *message = [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:@"msg111" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + XCTAssertNotNil(message); + XCTAssertEqualObjects(message.fromJID, [XMPPJID jidWithString:@"coven@muclight.shakespeare.lit/hag66@shakespeare.lit"]); + XCTAssertEqualObjects(message.toJID, [XMPPJID jidWithString:@"crone1@shakespeare.lit"]); + XCTAssertEqualObjects(message.body, @"Harpier cries: 'tis time, 'tis time."); + XCTAssertEqual(message.direction, XMPPMessageDirectionIncoming); + XCTAssertEqualObjects(message.stanzaID, @"msg111"); + XCTAssertEqual(message.type, XMPPMessageTypeGroupchat); + XCTAssertEqualObjects([message streamJID], [XMPPJID jidWithString:@"crone1@shakespeare.lit"]); + XCTAssertEqualObjects([message streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + }]; +} + +- (void)testOutgoingRoomLightMessageHandling +{ + XMPPMessageCoreDataStorageObject *messageObject = [self.storage insertOutgoingMessageStorageObject]; + messageObject.stanzaID = @"msg111"; + [messageObject registerOutgoingMessageStreamEventID:@"eventID"]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self provideTransactionForFakeOutgoingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerSentRoomLightMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XMPPMessageCoreDataStorageObject *messageObject = [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:@"msg111" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + XCTAssertEqualObjects([messageObject streamJID], [XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"]); + XCTAssertEqualObjects([messageObject streamTimestamp], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + }]; +} + +- (void)testPingbackRoomLightMessageHandling +{ + XMPPMessageCoreDataStorageObject *sentMessageObject = [self.storage insertOutgoingMessageStorageObject]; + sentMessageObject.stanzaID = @"msg111"; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + + XMPPMessage *pingbackMessage = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Harpier cries: 'tis time, 'tis time." + @"" + error:nil]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification + object:self.storage.mainThreadManagedObjectContext + handler:nil].inverted = YES; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeReceivedRoomLightMessage:pingbackMessage]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testMyIncomingRoomLightMessageCheck +{ + XMPPMessageCoreDataStorageObject *myMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + myMessage.direction = XMPPMessageDirectionIncoming; + myMessage.type = XMPPMessageTypeGroupchat; + myMessage.fromJID = [XMPPJID jidWithString:@"coven@muclight.shakespeare.lit/hag66@shakespeare.lit"]; + [myMessage registerIncomingMessageStreamEventID:@"myMessageEventID" + streamJID:[XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XMPPMessageCoreDataStorageObject *otherMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherMessage.direction = XMPPMessageDirectionIncoming; + otherMessage.type = XMPPMessageTypeGroupchat; + otherMessage.fromJID = [XMPPJID jidWithString:@"coven@muclight.shakespeare.lit/crone1@shakespeare.lit"]; + [otherMessage registerIncomingMessageStreamEventID:@"otherMessageEventID" + streamJID:[XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XCTAssertTrue([myMessage isMyIncomingRoomLightMessage]); + XCTAssertFalse([otherMessage isMyIncomingRoomLightMessage]); +} + +- (void)testRoomLightMessageLookup +{ + XMPPMessageCoreDataStorageObject *matchingOutgoingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingOutgoingMessage.direction = XMPPMessageDirectionOutgoing; + matchingOutgoingMessage.toJID = [XMPPJID jidWithString:@"coven@muclight.shakespeare.lit"]; + [matchingOutgoingMessage registerOutgoingMessageStreamEventID:@"eventID1"]; + [matchingOutgoingMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XMPPMessageCoreDataStorageObject *otherOutgoingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherOutgoingMessage.direction = XMPPMessageDirectionOutgoing; + otherOutgoingMessage.toJID = [XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"]; + [otherOutgoingMessage registerOutgoingMessageStreamEventID:@"eventID2"]; + [otherOutgoingMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + XMPPMessageCoreDataStorageObject *matchingIncomingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + matchingIncomingMessage.direction = XMPPMessageDirectionIncoming; + [matchingIncomingMessage registerIncomingMessageStreamEventID:@"eventID3" + streamJID:[XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:2]]; + matchingIncomingMessage.fromJID = [XMPPJID jidWithString:@"coven@muclight.shakespeare.lit/hag66@shakespeare.lit"]; + + XMPPMessageCoreDataStorageObject *otherIncomingMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + otherIncomingMessage.direction = XMPPMessageDirectionIncoming; + [otherIncomingMessage registerIncomingMessageStreamEventID:@"eventID4" + streamJID:[XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:3]]; + otherIncomingMessage.fromJID = [XMPPJID jidWithString:@"hag66@shakespeare.lit/pda"]; + + NSPredicate *predicate = + [XMPPMessageContextItemCoreDataStorageObject messageRemotePartyJIDPredicateWithValue:[XMPPJID jidWithString:@"coven@muclight.shakespeare.lit"] + compareOptions:XMPPJIDCompareBare]; + NSFetchRequest *fetchRequest = [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 2); + XCTAssertEqualObjects(fetchResult[0].message, matchingOutgoingMessage); + XCTAssertEqualObjects(fetchResult[1].message, matchingIncomingMessage); +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPDelayedDeliveryMessageStorage) + +- (void)testDelayedDeliveryDirectStorage +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + + [message setDelayedDeliveryDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + from:[XMPPJID jidWithString:@"domain"] + reasonDescription:@"Test"]; + + XCTAssertEqualObjects([message delayedDeliveryDate], [NSDate dateWithTimeIntervalSinceReferenceDate:0]); + XCTAssertEqualObjects([message delayedDeliveryFrom], [XMPPJID jidWithString:@"domain"]); + XCTAssertEqualObjects([message delayedDeliveryReasonDescription], @"Test"); +} + +- (void)testDelayedDeliveryStreamEventHandling +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + + XMPPMessage *message = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Offline Storage" + @"" + error:nil]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerDelayedDeliveryForReceivedMessage:message]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSFetchRequest *fetchRequest = [XMPPMessageCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects([fetchResult.firstObject delayedDeliveryDate], [NSDate dateWithXmppDateTimeString:@"2002-09-10T23:08:25Z"]); + XCTAssertEqualObjects([fetchResult.firstObject delayedDeliveryFrom], [XMPPJID jidWithString:@"capulet.com"]); + XCTAssertEqualObjects([fetchResult.firstObject delayedDeliveryReasonDescription], @"Offline Storage"); + }]; +} + +- (void)testDelayedDeliveryTimestampMessageContextFetch +{ + XMPPMessageCoreDataStorageObject *shorterDelayMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + shorterDelayMessage.direction = XMPPMessageDirectionOutgoing; + [shorterDelayMessage registerOutgoingMessageStreamEventID:@"earlierEventID"]; + [shorterDelayMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"juliet@example.com"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XMPPMessageCoreDataStorageObject *longerDelayMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + longerDelayMessage.direction = XMPPMessageDirectionOutgoing; + [longerDelayMessage registerOutgoingMessageStreamEventID:@"laterEventID"]; + [longerDelayMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"juliet@example.com"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + [shorterDelayMessage setDelayedDeliveryDate:[NSDate dateWithTimeIntervalSinceReferenceDate:-1] from:nil reasonDescription:@"Shorter delay"]; + [longerDelayMessage setDelayedDeliveryDate:[NSDate dateWithTimeIntervalSinceReferenceDate:-2] from:nil reasonDescription:@"Longer delay"]; + + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject delayedDeliveryTimestampKindPredicate]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 2); + XCTAssertEqualObjects(result[0].message, longerDelayMessage); + XCTAssertEqualObjects(result[1].message, shorterDelayMessage); +} + +- (void)testDelayedDeliveryStreamTimestampDisplacementMessageContextFetch +{ + XMPPMessageCoreDataStorageObject *liveMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + liveMessage.direction = XMPPMessageDirectionOutgoing; + liveMessage.stanzaID = @"liveMessageID"; + [liveMessage registerOutgoingMessageStreamEventID:@"liveMessageEventID"]; + [liveMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"juliet@example.com"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XMPPMessageCoreDataStorageObject *delayedDeliveryMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + delayedDeliveryMessage.direction = XMPPMessageDirectionOutgoing; + [delayedDeliveryMessage registerOutgoingMessageStreamEventID:@"delayedDeliveryMessageEventID"]; + [delayedDeliveryMessage registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"juliet@example.com"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1]]; + + [delayedDeliveryMessage setDelayedDeliveryDate:[NSDate dateWithTimeIntervalSinceReferenceDate:-1] from:nil reasonDescription:nil]; + + NSPredicate *predicate = + [NSCompoundPredicate orPredicateWithSubpredicates:@[[XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate], + [XMPPMessageContextItemCoreDataStorageObject delayedDeliveryTimestampKindPredicate]]]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 2); + XCTAssertEqualObjects(result[0].message, delayedDeliveryMessage); + XCTAssertEqualObjects(result[1].message, liveMessage); +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPMessageArchiveManagementLocalStorage) + +- (void)testMessageArchiveBasicStorage +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"28482-98726-73623" includingPayload:YES]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeComplete]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSFetchRequest *fetchRequest = [XMPPMessageCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects([fetchResult.firstObject messageArchiveID], @"28482-98726-73623"); + XCTAssertEqualObjects([fetchResult.firstObject messageArchiveDate], [NSDate dateWithXmppDateTimeString:@"2010-07-10T23:08:25Z"]); + XCTAssertEqualObjects([fetchResult.firstObject body], @"Hail to thee"); + }]; +} + +- (void)testMessageArchivePartialResultPageTimestampContextFetch +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"28482-98726-73623" includingPayload:YES]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject + messageArchiveTimestampKindPredicateWithOptions:XMPPMessageArchiveTimestampContextIncludingPartialResultPages]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects([fetchResult.firstObject.message messageArchiveID], @"28482-98726-73623"); + }]; +} + +- (void)testMessageArchiveFinalizedResultPageTimestampContextFetch +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + NSXMLElement *partialResultPageItem = [self fakeMessageArchiveResultItemWithID:@"partialResultPageArchiveID" includingPayload:YES]; + NSXMLElement *completeResultPageItem = [self fakeMessageArchiveResultItemWithID:@"completeResultPageArchiveID" includingPayload:YES]; + + for (NSString *messageID in @[@"partialResultPageArchiveID", @"completeResultPageArchiveID"]) { + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]] && [[object messageArchiveID] isEqualToString:messageID]; + }]; + } + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"partialResultPageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:partialResultPageItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"completeResultPageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:completeResultPageItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self.storage finalizeResultSetPageWithMessageArchiveIDs:@[@"completeResultPageArchiveID"]]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject messageArchiveTimestampKindPredicateWithOptions:0]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects([fetchResult.firstObject.message messageArchiveID], @"completeResultPageArchiveID"); + }]; +} + +- (void)testMessageArchiveDeletedResultItemTimestampContextFetch +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"partialResultPageArchiveID" includingPayload:YES]; + NSXMLElement *resultPlaceholderItem = [self fakeMessageArchiveResultItemWithID:@"deletedResultItemArchiveID" includingPayload:NO]; + + for (NSString *archiveID in @[@"partialResultPageArchiveID", @"deletedResultItemArchiveID"]) { + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]] && [[object messageArchiveID] isEqualToString:archiveID]; + }]; + } + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"partialResultPageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"deletedResultItemEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultPlaceholderItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSPredicate *predicate = [XMPPMessageContextItemCoreDataStorageObject + messageArchiveTimestampKindPredicateWithOptions:XMPPMessageArchiveTimestampContextIncludingDeletedResultItems]; + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects([fetchResult.firstObject.message messageArchiveID], @"deletedResultItemArchiveID"); + }]; +} + +- (void)testMessageArchiveDuplicateArchiveID +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"28482-98726-73623" includingPayload:YES]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"originalEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }].inverted = YES; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"duplicateEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testMessageArchiveDuplicateStanzaID +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + + XMPPMessageCoreDataStorageObject *liveMessageObject = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + liveMessageObject.direction = XMPPMessageDirectionIncoming; + liveMessageObject.stanzaID = @"123"; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"28482-98726-73623" includingPayload:YES]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }].inverted = YES; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"archivedMessageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testMessageArchiveStreamTimestampDisplacementContextFetch +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + + XMPPMessageCoreDataStorageObject *liveMessageObject = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + liveMessageObject.direction = XMPPMessageDirectionIncoming; + liveMessageObject.stanzaID = @"liveMessageID"; + [liveMessageObject registerIncomingMessageStreamEventID:@"liveMessageEventID" + streamJID:mockStream.myJID + streamEventTimestamp:[NSDate dateWithXmppDateTimeString:@"2010-07-10T23:08:26Z"]]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"28482-98726-73623" includingPayload:YES]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream + withID:@"archivedMessageEventID" + timestamp:[NSDate dateWithXmppDateTimeString:@"2010-07-10T23:08:27Z"] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeMetadataOnly]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSPredicate *streamTimestampPredicate = [XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate]; + NSPredicate *messageArchiveTimestampPredicate = + [XMPPMessageContextItemCoreDataStorageObject + messageArchiveTimestampKindPredicateWithOptions:XMPPMessageArchiveTimestampContextIncludingPartialResultPages]; + NSPredicate *predicate = [NSCompoundPredicate orPredicateWithSubpredicates:@[streamTimestampPredicate, messageArchiveTimestampPredicate]]; + + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:predicate + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *result = [self.storage.mainThreadManagedObjectContext executeFetchRequest:fetchRequest + error:NULL]; + + XCTAssertEqual(result.count, 2); + XCTAssertEqualObjects([result[0].message messageArchiveID], @"28482-98726-73623"); + XCTAssertEqualObjects(result[1].message.stanzaID, @"liveMessageID"); + }]; +} + +- (void)testMyArchivedChatMessage +{ + XMPPMockStream *mockStream = [[XMPPMockStream alloc] init]; + mockStream.myJID = [XMPPJID jidWithString:@"witch@shakespeare.lit"]; + + NSXMLElement *resultItem = [self fakeMessageArchiveResultItemWithID:@"28482-98726-73623" includingPayload:YES]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:mockStream withID:@"eventID" timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeMessageArchiveQueryResultItem:resultItem inMode:XMPPMessageArchiveQueryResultStorageModeComplete]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSFetchRequest *fetchRequest = [XMPPMessageCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertTrue([fetchResult.firstObject isMyArchivedChatMessage]); + }]; +} + +- (NSXMLElement *)fakeMessageArchiveResultItemWithID:(NSString *)messageArchiveID includingPayload:(BOOL)shouldIncludePayload +{ + NSMutableString *resultItemString = [[NSMutableString alloc] init]; + [resultItemString appendFormat:@"", messageArchiveID]; + [resultItemString appendString:@" " + @" "]; + if (shouldIncludePayload) { + [resultItemString appendString:@"" + @" Hail to thee" + @""]; + } + [resultItemString appendString:@" " + @""]; + + return [[NSXMLElement alloc] initWithXMLString:resultItemString error:NULL]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPManagedMessagingStorage) + +- (void)testManagedMessagingPlainMessageUnspecifiedStatus +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + XCTAssertEqual([message managedMessagingStatus], XMPPManagedMessagingStatusUnspecified); +} + +- (void)testManagedMessagingOutgoingMessageRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"managedMessageEventID"]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self provideTransactionForFakeOutgoingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"managedMessageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerSentManagedMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssertEqual([message managedMessagingStatus], XMPPManagedMessagingStatusPendingAcknowledgement); + }]; +} + +- (void)testManagedMessagingSentMessageConfirmation +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.stanzaID = @"confirmedMessageID"; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"confirmedMessageEventID"]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self provideTransactionForFakeOutgoingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"confirmedMessageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerSentManagedMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self.storage registerAcknowledgedManagedMessageIDs:@[@"confirmedMessageID"]]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssertEqual([message managedMessagingStatus], XMPPManagedMessagingStatusAcknowledged); + }]; +} + +- (void)testManagedMessagingFailureRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + [message registerOutgoingMessageStreamEventID:@"unconfirmedMessageEventID"]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self provideTransactionForFakeOutgoingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"unconfirmedMessageEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerSentManagedMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + [self expectationForNotification:NSManagedObjectContextObjectsDidChangeNotification object:self.storage.mainThreadManagedObjectContext handler:nil]; + + [self.storage registerFailureForUnacknowledgedManagedMessages]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssertEqual([message managedMessagingStatus], XMPPManagedMessagingStatusUnacknowledged); + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPMessageDeliveryReceiptsStorage) + +- (void)testMessageDeliveryReceiptStorage +{ + XMPPMessageCoreDataStorageObject *fakeSentMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + fakeSentMessage.stanzaID = @"richard2-4.1.247"; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"deliveryReceiptEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeReceivedDeliveryReceiptResponseMessage:[self fakeDeliveryReceiptResponseMessage]]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XCTAssertTrue([fakeSentMessage hasAssociatedDeliveryReceiptResponseMessage]); + + XMPPMessageCoreDataStorageObject *deliveryReceiptMessage = + [XMPPMessageCoreDataStorageObject findWithUniqueStanzaID:@"bi29sg183b4v" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + XCTAssertEqualObjects([deliveryReceiptMessage messageDeliveryReceiptResponseID], @"richard2-4.1.247"); + }]; +} + +- (void)testMessageDeliveryReceiptLookup +{ + XMPPMessageCoreDataStorageObject *fakeSentMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + fakeSentMessage.stanzaID = @"richard2-4.1.247"; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"deliveryReceiptEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction storeReceivedDeliveryReceiptResponseMessage:[self fakeDeliveryReceiptResponseMessage]]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XMPPMessageCoreDataStorageObject *deliveryReceiptMessage = + [XMPPMessageCoreDataStorageObject findDeliveryReceiptResponseForMessageWithID:@"richard2-4.1.247" + inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + XCTAssertEqualObjects(deliveryReceiptMessage.stanzaID, @"bi29sg183b4v"); + }]; +} + +- (XMPPMessage *)fakeDeliveryReceiptResponseMessage +{ + return [[XMPPMessage alloc] initWithXMLString: + @"" + @" " + @"" + error:NULL]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPOutOfBandResourceMessagingStorage) + +- (void)testOutOfBandResourceAssignment +{ + XMPPMessageCoreDataStorageObject *resourceWithDescriptionMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + resourceWithDescriptionMessage.direction = XMPPMessageDirectionOutgoing; + + XMPPMessageCoreDataStorageObject *resourceWithoutDescriptionMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + resourceWithoutDescriptionMessage.direction = XMPPMessageDirectionOutgoing; + + [resourceWithDescriptionMessage assignOutOfBandResourceWithInternalID:@"resourceID1" description:@"A license to Jabber!"]; + [resourceWithoutDescriptionMessage assignOutOfBandResourceWithInternalID:@"resourceID2" description:nil]; + + XCTAssertEqualObjects([resourceWithDescriptionMessage outOfBandResourceInternalID], @"resourceID1"); + XCTAssertEqualObjects([resourceWithDescriptionMessage outOfBandResourceDescription], @"A license to Jabber!"); + XCTAssertEqualObjects([resourceWithoutDescriptionMessage outOfBandResourceInternalID], @"resourceID2"); + XCTAssertNil([resourceWithoutDescriptionMessage outOfBandResourceDescription]); +} + +- (void)testOutOfBandResourceURIRegistration +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + + [message assignOutOfBandResourceWithInternalID:@"resourceID1" description:@"A license to Jabber!"]; + [message setAssignedOutOfBandResourceURIString:@"http://www.jabber.org/images/psa-license.jpg"]; + + XCTAssertEqualObjects([message outOfBandResourceURIString], @"http://www.jabber.org/images/psa-license.jpg"); +} + +- (void)testOutOfBandResourceIncomingMessageStorage +{ + XMPPMessage *outOfBandResourceMessage = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Yeah, but do you have a license to Jabber?" + @" " + @" http://www.jabber.org/images/psa-license.jpg" + @" A license to Jabber!" + @" " + @"" + error:NULL]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"eventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerOutOfBandResourceForReceivedMessage:outOfBandResourceMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSFetchRequest *fetchRequest = [XMPPMessageCoreDataStorageObject xmpp_fetchRequestInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertNotNil([fetchResult.firstObject outOfBandResourceInternalID]); + XCTAssertEqualObjects([fetchResult.firstObject outOfBandResourceURIString], @"http://www.jabber.org/images/psa-license.jpg"); + XCTAssertEqualObjects([fetchResult.firstObject outOfBandResourceDescription], @"A license to Jabber!"); + }]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XMPPLastMessageCorrectionStorage) + +- (void)testMessageCorrectionDirectStorage +{ + XMPPMessageCoreDataStorageObject *originalMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + originalMessage.stanzaID = @"originalMessageID"; + + XMPPMessageCoreDataStorageObject *correctedMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + correctedMessage.direction = XMPPMessageDirectionOutgoing; + [correctedMessage assignMessageCorrectionID:@"originalMessageID"]; + + XCTAssertTrue([originalMessage hasAssociatedCorrectionMessage]); + XCTAssertEqualObjects([correctedMessage messageCorrectionID], @"originalMessageID"); +} + +- (void)testMessageCorrectionStreamEventHandling +{ + XMPPMessageCoreDataStorageObject *originalMessageObject = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + originalMessageObject.stanzaID = @"originalMessageID"; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + XMPPMessage *correctedMessage = [self fakeCorrectedMessageWithOriginalMessageID:@"originalMessageID"]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"messageCorrectionEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerOriginalMessageIDForReceivedCorrectedMessage:correctedMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + XMPPMessageCoreDataStorageObject *correctedMessage = + [XMPPMessageCoreDataStorageObject findWithStreamEventID:@"messageCorrectionEventID" inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + XCTAssertTrue([originalMessageObject hasAssociatedCorrectionMessage]); + XCTAssertEqualObjects([correctedMessage messageCorrectionID], originalMessageObject.stanzaID); + }]; +} + +- (void)testMessageCorrectionLookup +{ + XMPPMessageCoreDataStorageObject *originalMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + originalMessage.stanzaID = @"originalMessageID"; + + XMPPMessageCoreDataStorageObject *correctedMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + correctedMessage.direction = XMPPMessageDirectionOutgoing; + [correctedMessage assignMessageCorrectionID:@"originalMessageID"]; + + XMPPMessageCoreDataStorageObject *lookedUpCorrectedMessage = + [XMPPMessageCoreDataStorageObject findCorrectionForMessageWithID:@"originalMessageID" inManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + + XCTAssertEqualObjects(correctedMessage, lookedUpCorrectedMessage); +} + +- (void)testMessageCorrectionStreamContextFetch +{ + XMPPMessageCoreDataStorageObject *originalMessageObject = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + originalMessageObject.direction = XMPPMessageDirectionIncoming; + originalMessageObject.stanzaID = @"originalMessageID"; + [originalMessageObject registerIncomingMessageStreamEventID:@"originalMessageEventID" + streamJID:[[XMPPMockStream alloc] init].myJID + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + [self.storage.mainThreadManagedObjectContext save:NULL]; + + XMPPMessage *correctedMessage = [self fakeCorrectedMessageWithOriginalMessageID:@"originalMessageID"]; + + [self expectationForMainThreadStorageManagedObjectsChangeNotificationWithUserInfoKey:NSInsertedObjectsKey count:1 handler: + ^BOOL(__kindof NSManagedObject *object) { + return [object isKindOfClass:[XMPPMessageCoreDataStorageObject class]]; + }]; + + [self provideTransactionForFakeIncomingMessageEventInStream:[[XMPPMockStream alloc] init] + withID:@"messageCorrectionEventID" + timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:1] + block: + ^(XMPPMessageCoreDataStorageTransaction *transaction) { + [transaction registerOriginalMessageIDForReceivedCorrectedMessage:correctedMessage]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + NSFetchRequest *fetchRequest = + [XMPPMessageContextItemCoreDataStorageObject requestByTimestampsWithPredicate:[XMPPMessageContextItemCoreDataStorageObject streamTimestampKindPredicate] + inAscendingOrder:YES + fromManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + NSArray *fetchResult = + [self.storage.mainThreadManagedObjectContext xmpp_executeForcedSuccessFetchRequest:fetchRequest]; + + XCTAssertEqual(fetchResult.count, 1); + XCTAssertEqualObjects(fetchResult.firstObject.message, originalMessageObject); + }]; +} + +- (XMPPMessage *)fakeCorrectedMessageWithOriginalMessageID:(NSString *)originalMessageID +{ + return [[XMPPMessage alloc] initWithXMLString: + [NSString stringWithFormat: + @"" + @" But soft, what light through yonder window breaks?" + @" " + @"", originalMessageID] + error:NULL]; +} + +@end + +@implementation XMPPMessageCoreDataStorageTests (XEP_0245) + +- (void)testMeCommandPrefixDetection +{ + XMPPMessageCoreDataStorageObject *meCommandMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + meCommandMessage.body = @"/me shrugs in disgust"; + + XMPPMessageCoreDataStorageObject *plainMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + plainMessage.body = @"Atlas shrugs in disgust"; + + XMPPMessageCoreDataStorageObject *nonAnchoredMePrefixMessage = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + nonAnchoredMePrefixMessage.body = @" /me shrugs in disgust"; + + XCTAssertEqualObjects([meCommandMessage meCommandText], @"shrugs in disgust"); + XCTAssertNil([plainMessage meCommandText]); + XCTAssertNil([nonAnchoredMePrefixMessage meCommandText]); +} + +- (void)testIncomingMessageMeCommandSubjectJID +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.fromJID = [XMPPJID jidWithString:@"olympians@chat.gods.lit/Atlas"]; + message.body = @"/me shrugs in disgust"; + + XCTAssertEqualObjects([message meCommandSubjectJID], [XMPPJID jidWithString:@"olympians@chat.gods.lit/Atlas"]); +} + +- (void)testOutgoingMessageMeCommandSubjectJID +{ + XMPPMessageCoreDataStorageObject *message = + [XMPPMessageCoreDataStorageObject xmpp_insertNewObjectInManagedObjectContext:self.storage.mainThreadManagedObjectContext]; + message.direction = XMPPMessageDirectionOutgoing; + message.body = @"/me shrugs in disgust"; + [message registerOutgoingMessageStreamEventID:@"eventID"]; + [message registerOutgoingMessageStreamJID:[XMPPJID jidWithString:@"atlas@chat.gods.lit"] + streamEventTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0]]; + + XCTAssertEqualObjects([message meCommandSubjectJID], [XMPPJID jidWithString:@"atlas@chat.gods.lit"]); +} + +@end diff --git a/Xcode/Testing-Shared/XMPPMessageDeliveryReceiptsTests.m b/Xcode/Testing-Shared/XMPPMessageDeliveryReceiptsTests.m new file mode 100644 index 0000000000..e6e8e6586a --- /dev/null +++ b/Xcode/Testing-Shared/XMPPMessageDeliveryReceiptsTests.m @@ -0,0 +1,47 @@ +#import +#import "XMPPMockStream.h" + +@interface XMPPMessageDeliveryReceiptsTests : XCTestCase + +@property (strong, nonatomic) XMPPMockStream *mockStream; +@property (strong, nonatomic) XMPPMessageDeliveryReceipts *messageDeliveryReceipts; +@property (strong, nonatomic) XCTestExpectation *delegateCallbackExpectation; + +@end + +@implementation XMPPMessageDeliveryReceiptsTests + +- (void)setUp +{ + [super setUp]; + self.mockStream = [[XMPPMockStream alloc] init]; + self.messageDeliveryReceipts = [[XMPPMessageDeliveryReceipts alloc] init]; + [self.messageDeliveryReceipts addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [self.messageDeliveryReceipts activate:self.mockStream]; +} + +- (void)testReceiptResponseDelegateCallback +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Delegate callback expectation"]; + + [self.mockStream fakeMessageResponse: + [[XMPPMessage alloc] initWithXMLString: + @"" + @" " + @"" + error:nil]]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)xmppMessageDeliveryReceipts:(XMPPMessageDeliveryReceipts *)xmppMessageDeliveryReceipts didReceiveReceiptResponseMessage:(XMPPMessage *)message +{ + if ([message hasReceiptResponse]) { + [self.delegateCallbackExpectation fulfill]; + } +} + +@end diff --git a/Xcode/Testing-Shared/XMPPMockStream.h b/Xcode/Testing-Shared/XMPPMockStream.h index cab8fbfae0..81b63fe155 100644 --- a/Xcode/Testing-Shared/XMPPMockStream.h +++ b/Xcode/Testing-Shared/XMPPMockStream.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)fakeIQResponse:(XMPPIQ *) iq; - (void)fakeMessageResponse:(XMPPMessage *) message; +- (void)fakeCurrentEventWithID:(NSString *)fakeEventID timestamp:(NSDate *)fakeEventTimestamp forActionWithBlock:(dispatch_block_t)block; + /** This is always called on XMPPStream's xmppQueue */ @property (nonatomic, copy, nullable) void (^elementReceived)(__kindof XMPPElement *element); diff --git a/Xcode/Testing-Shared/XMPPMockStream.m b/Xcode/Testing-Shared/XMPPMockStream.m index 43c326b6a6..3b65b8c8d2 100644 --- a/Xcode/Testing-Shared/XMPPMockStream.m +++ b/Xcode/Testing-Shared/XMPPMockStream.m @@ -8,11 +8,20 @@ #import "XMPPMockStream.h" +@interface XMPPElementEvent (PrivateAPI) + +@property (nonatomic, assign, readwrite, getter=isProcessingCompleted) BOOL processingCompleted; + +- (instancetype)initWithStream:(XMPPStream *)xmppStream uniqueID:(NSString *)uniqueID myJID:(XMPPJID *)myJID timestamp:(NSDate *)timestamp; + +@end + @implementation XMPPMockStream - (id) init { if (self = [super init]) { [super setValue:@(STATE_XMPP_CONNECTED) forKey:@"state"]; + [super setValue:[XMPPJID jidWithString:@"user@domain/resource"] forKey:@"myJID"]; } return self; } @@ -33,6 +42,19 @@ - (void)fakeIQResponse:(XMPPIQ *) iq { [self injectElement:iq]; } +- (void)fakeCurrentEventWithID:(NSString *)fakeEventID timestamp:(NSDate *)fakeEventTimestamp forActionWithBlock:(dispatch_block_t)block +{ + XMPPElementEvent *fakeEvent = [[XMPPElementEvent alloc] initWithStream:self uniqueID:fakeEventID myJID:self.myJID timestamp:fakeEventTimestamp]; + GCDMulticastDelegateInvocationContext *fakeInvocationContext = [[GCDMulticastDelegateInvocationContext alloc] initWithValue:fakeEvent]; + + [fakeInvocationContext becomeCurrentOnQueue:self.xmppQueue forActionWithBlock:block]; + + dispatch_group_notify(fakeInvocationContext.continuityGroup, self.xmppQueue, ^{ + fakeEvent.processingCompleted = YES; + [[self valueForKey:@"multicastDelegate"] xmppStream:self didFinishProcessingElementEvent:fakeEvent]; + }); +} + - (void)sendElement:(XMPPElement *)element { [super sendElement:element]; if(self.elementReceived) { diff --git a/Xcode/Testing-Shared/XMPPOneToOneChatTests.m b/Xcode/Testing-Shared/XMPPOneToOneChatTests.m new file mode 100644 index 0000000000..0dcdb09427 --- /dev/null +++ b/Xcode/Testing-Shared/XMPPOneToOneChatTests.m @@ -0,0 +1,76 @@ +#import +#import "XMPPMockStream.h" + +@interface XMPPOneToOneChatTests : XCTestCase + +@property (nonatomic, strong) XMPPMockStream *mockStream; +@property (nonatomic, strong) XMPPOneToOneChat *oneToOneChat; +@property (nonatomic, strong) XCTestExpectation *delegateCallbackExpectation; + +@end + +@implementation XMPPOneToOneChatTests + +- (void)setUp +{ + [super setUp]; + self.mockStream = [[XMPPMockStream alloc] init]; + self.oneToOneChat = [[XMPPOneToOneChat alloc] init]; + [self.oneToOneChat addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [self.oneToOneChat activate:self.mockStream]; +} + +- (void)testIncomingMessageHandling +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Incoming message delegate callback expectation"]; + + XMPPMessage *chatMessage = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Art thou not Romeo, and a Montague?" + @"" + error:nil]; + + XMPPMessage *emptyMessage = [[XMPPMessage alloc] initWithXMLString: + @"" + error:nil]; + + [self.mockStream fakeMessageResponse:chatMessage]; + [self.mockStream fakeMessageResponse:emptyMessage]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testOutgoingMessageHandling +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Sent message delegate callback expectation"]; + + XMPPMessage *chatMessage = [[XMPPMessage alloc] initWithXMLString: + @"" + @" Art thou not Romeo, and a Montague?" + @"" + error:nil]; + + XMPPMessage *emptyMessage = [[XMPPMessage alloc] initWithXMLString: + @"" + error:nil]; + + [self.mockStream sendElement:chatMessage]; + [self.mockStream sendElement:emptyMessage]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)xmppOneToOneChat:(XMPPOneToOneChat *)xmppOneToOneChat didReceiveChatMessage:(XMPPMessage *)message +{ + [self.delegateCallbackExpectation fulfill]; +} + +- (void)xmppOneToOneChat:(XMPPOneToOneChat *)xmppOneToOneChat didSendChatMessage:(XMPPMessage *)message +{ + [self.delegateCallbackExpectation fulfill]; +} + +@end diff --git a/Xcode/Testing-Shared/XMPPOutOfBandResourceMessagingTests.m b/Xcode/Testing-Shared/XMPPOutOfBandResourceMessagingTests.m new file mode 100644 index 0000000000..0e7746db53 --- /dev/null +++ b/Xcode/Testing-Shared/XMPPOutOfBandResourceMessagingTests.m @@ -0,0 +1,65 @@ +#import +#import "XMPPMockStream.h" + +@interface XMPPOutOfBandResourceMessagingTests : XCTestCase + +@property (strong, nonatomic) XMPPMockStream *mockStream; +@property (strong, nonatomic) XMPPOutOfBandResourceMessaging *outOfBandResourceMessaging; +@property (strong, nonatomic) XCTestExpectation *delegateCallbackExpectation; + +@end + +@implementation XMPPOutOfBandResourceMessagingTests + +- (void)setUp +{ + [super setUp]; + self.mockStream = [[XMPPMockStream alloc] init]; + self.outOfBandResourceMessaging = [[XMPPOutOfBandResourceMessaging alloc] init]; + [self.outOfBandResourceMessaging addDelegate:self delegateQueue:dispatch_get_main_queue()]; + [self.outOfBandResourceMessaging activate:self.mockStream]; +} + +- (void)testDelegateCallback +{ + self.delegateCallbackExpectation = [self expectationWithDescription:@"Delegate callback received"]; + + [self fakeOutOfBandResourceMessage]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testURLSchemeFiltering +{ + self.outOfBandResourceMessaging.relevantURLSchemes = [NSSet setWithObject:@"ftp"]; + self.delegateCallbackExpectation = [self expectationWithDescription:@"Delegate callback not received"]; + self.delegateCallbackExpectation.inverted = YES; + + [self fakeOutOfBandResourceMessage]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)xmppOutOfBandResourceMessaging:(XMPPOutOfBandResourceMessaging *)xmppOutOfBandResourceMessaging didReceiveOutOfBandResourceMessage:(XMPPMessage *)message +{ + if ([[message body] isEqualToString:@"Yeah, but do you have a license to Jabber?"]) { + [self.delegateCallbackExpectation fulfill]; + } +} + +- (void)fakeOutOfBandResourceMessage +{ + [self.mockStream fakeMessageResponse: + [[XMPPMessage alloc] initWithXMLString: + @"" + @" Yeah, but do you have a license to Jabber?" + @" " + @" http://www.jabber.org/images/psa-license.jpg" + @" A license to Jabber!" + @" " + @"" + error:nil]]; +} + +@end diff --git a/Xcode/Testing-Shared/XMPPRoomLightCoreDataStorageTests.m b/Xcode/Testing-Shared/XMPPRoomLightCoreDataStorageTests.m index d686085465..14bca99631 100644 --- a/Xcode/Testing-Shared/XMPPRoomLightCoreDataStorageTests.m +++ b/Xcode/Testing-Shared/XMPPRoomLightCoreDataStorageTests.m @@ -31,7 +31,7 @@ - (void)testReceiveMessageWithoutStorage{ [roomLight addDelegate:self delegateQueue:dispatch_get_main_queue()]; [roomLight activate:streamTest]; - [streamTest fakeMessageResponse:[self fakeIncomingMessage]]; + [streamTest fakeMessageResponse:[self fakeIncomingMessageWithBody:YES]]; [self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) { if(error){ XCTFail(@"Expectation Failed with error: %@", error); @@ -53,7 +53,7 @@ - (void)testReceiveMessageWithStorage{ [roomLight addDelegate:self delegateQueue:dispatch_get_main_queue()]; [roomLight activate:streamTest]; - [streamTest fakeMessageResponse:[self fakeIncomingMessage]]; + [streamTest fakeMessageResponse:[self fakeIncomingMessageWithBody:YES]]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSManagedObjectContext *context = [storage mainThreadManagedObjectContext]; @@ -135,6 +135,52 @@ - (void)testReceiveAffiliationMessageWithStorage { }]; } +- (void)testReceiveMessageWithoutBody { + self.checkDelegate = false; + + XCTestExpectation *expectation = [self expectationWithDescription:@"receive message without body and correctly stored"]; + + XMPPRoomLightCoreDataStorage *storage = [[XMPPRoomLightCoreDataStorage alloc] initWithDatabaseFilename:@"testReceiveMessageWithoutBody.sqlite" + storeOptions:nil]; + storage.autoRemovePreviousDatabaseFile = YES; + + XMPPMockStream *streamTest = [[XMPPMockStream alloc] init]; + streamTest.myJID = [XMPPJID jidWithString:@"myUser@domain.com"]; + XMPPJID *jid = [XMPPJID jidWithString:@"room@domain.com"]; + XMPPRoomLight *roomLight = [[XMPPRoomLight alloc] initWithRoomLightStorage:storage jid:jid roomname:@"test" dispatchQueue:nil]; + roomLight.shouldHandleMemberMessagesWithoutBody = YES; + [roomLight activate:streamTest]; + + [streamTest fakeMessageResponse:[self fakeIncomingMessageWithBody:NO]]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + NSManagedObjectContext *context = [storage mainThreadManagedObjectContext]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPRoomLightMessageCoreDataStorageObject" + inManagedObjectContext:context]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"roomJIDStr = %@", jid.full]; + + NSFetchRequest *request = [[NSFetchRequest alloc] init]; + request.entity = entity; + request.predicate = predicate; + + NSError *error; + XMPPRoomLightMessageCoreDataStorageObject *roomMessage = [[context executeFetchRequest:request error:&error] firstObject]; + XCTAssertNil(error); + XCTAssertEqualObjects(roomMessage.jid.full, @"room@domain.com/test.user@erlang-solutions.com"); + XCTAssertNil(roomMessage.body); + XCTAssertEqualObjects(roomMessage.nickname, @"test.user@erlang-solutions.com"); + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) { + if(error){ + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + - (void)testImportMessage { self.checkDelegate = false; @@ -148,7 +194,10 @@ - (void)testImportMessage { XMPPJID *jid = [XMPPJID jidWithString:@"room@domain.com"]; XMPPRoomLight *roomLight = [[XMPPRoomLight alloc] initWithRoomLightStorage:storage jid:jid roomname:@"test" dispatchQueue:nil]; - [storage importRemoteArchiveMessage:[self fakeIncomingMessage] withTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] inRoom:roomLight fromStream:streamTest]; + [storage importRemoteArchiveMessage:[self fakeIncomingMessageWithBody:YES] + withTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + inRoom:roomLight + fromStream:streamTest]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSManagedObjectContext *context = [storage mainThreadManagedObjectContext]; @@ -194,8 +243,14 @@ - (void)testImportMessageUniquing { XMPPJID *jid = [XMPPJID jidWithString:@"room@domain.com"]; XMPPRoomLight *roomLight = [[XMPPRoomLight alloc] initWithRoomLightStorage:storage jid:jid roomname:@"test" dispatchQueue:nil]; - [storage importRemoteArchiveMessage:[self fakeIncomingMessage] withTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] inRoom:roomLight fromStream:streamTest]; - [storage importRemoteArchiveMessage:[self fakeIncomingMessage] withTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] inRoom:roomLight fromStream:streamTest]; + [storage importRemoteArchiveMessage:[self fakeIncomingMessageWithBody:YES] + withTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + inRoom:roomLight + fromStream:streamTest]; + [storage importRemoteArchiveMessage:[self fakeIncomingMessageWithBody:YES] + withTimestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:0] + inRoom:roomLight + fromStream:streamTest]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSManagedObjectContext *context = [storage mainThreadManagedObjectContext]; @@ -231,14 +286,16 @@ - (void)xmppRoomLight:(XMPPRoomLight *)sender didReceiveMessage:(XMPPMessage *)m } } -- (XMPPMessage *)fakeIncomingMessage{ +- (XMPPMessage *)fakeIncomingMessageWithBody:(BOOL)shouldIncludeBody { NSMutableString *s = [NSMutableString string]; [s appendString: @""]; - [s appendString: @" Yo! 13'"]; + if (shouldIncludeBody) { + [s appendString: @"Yo! 13'"]; + } [s appendString: @""]; NSError *error; diff --git a/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj b/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj index 393a79e644..241b5b445c 100644 --- a/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj +++ b/Xcode/Testing-iOS/XMPPFrameworkTests.xcodeproj/project.pbxproj @@ -24,7 +24,14 @@ D99C5E0D1D99C48100FB068A /* OMEMOModuleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E0A1D99C48100FB068A /* OMEMOModuleTests.m */; }; D99C5E0E1D99C48100FB068A /* OMEMOTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E0C1D99C48100FB068A /* OMEMOTestStorage.m */; }; D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */; }; + DD06EA401F78E566008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA3F1F78E566008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */; }; DD1E732C1ED86B7D009B529B /* XMPPPubSubTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD1E732B1ED86B7D009B529B /* XMPPPubSubTests.m */; }; + DD26F5831F7CECD100F54F18 /* XMPPOneToOneChatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F5821F7CECD100F54F18 /* XMPPOneToOneChatTests.m */; }; + DD24E0251F7119A300FA813C /* XMPPDelayedDeliveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD24E0241F7119A300FA813C /* XMPPDelayedDeliveryTests.m */; }; + DD2AD6F01F84CB0400E0FED2 /* XMPPManagedMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD6EF1F84CB0400E0FED2 /* XMPPManagedMessagingTests.m */; }; + DDB40BA81F750B0800B82A93 /* XMPPOutOfBandResourceMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDB40BA71F750B0800B82A93 /* XMPPOutOfBandResourceMessagingTests.m */; }; + DD203B851F7A297C00CA359C /* XMPPLastMessageCorrectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B841F7A297C00CA359C /* XMPPLastMessageCorrectionTests.m */; }; + DD3559711F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3559701F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m */; }; FDD2AB232C05507F2045FFFC /* Pods_XMPPFrameworkTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD0B17267211A912DE2098E /* Pods_XMPPFrameworkTests.framework */; }; /* End PBXBuildFile section */ @@ -54,7 +61,14 @@ D99C5E0B1D99C48100FB068A /* OMEMOTestStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OMEMOTestStorage.h; path = "../../Testing-Shared/OMEMOTestStorage.h"; sourceTree = ""; }; D99C5E0C1D99C48100FB068A /* OMEMOTestStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOTestStorage.m; path = "../../Testing-Shared/OMEMOTestStorage.m"; sourceTree = ""; }; D9E35E6F1D90B894002E7CF7 /* OMEMOElementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOElementTests.m; path = "../../Testing-Shared/OMEMOElementTests.m"; sourceTree = ""; }; + DD06EA3F1F78E566008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = XMPPMessageDeliveryReceiptsTests.m; path = "../../Testing-Shared/XMPPMessageDeliveryReceiptsTests.m"; sourceTree = ""; }; DD1E732B1ED86B7D009B529B /* XMPPPubSubTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPPubSubTests.m; path = "../../Testing-Shared/XMPPPubSubTests.m"; sourceTree = ""; }; + DD26F5821F7CECD100F54F18 /* XMPPOneToOneChatTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = XMPPOneToOneChatTests.m; path = "../../Testing-Shared/XMPPOneToOneChatTests.m"; sourceTree = ""; }; + DD24E0241F7119A300FA813C /* XMPPDelayedDeliveryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPDelayedDeliveryTests.m; path = "../../Testing-Shared/XMPPDelayedDeliveryTests.m"; sourceTree = ""; }; + DD2AD6EF1F84CB0400E0FED2 /* XMPPManagedMessagingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = XMPPManagedMessagingTests.m; path = "../../Testing-Shared/XMPPManagedMessagingTests.m"; sourceTree = ""; }; + DDB40BA71F750B0800B82A93 /* XMPPOutOfBandResourceMessagingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = XMPPOutOfBandResourceMessagingTests.m; path = "../../Testing-Shared/XMPPOutOfBandResourceMessagingTests.m"; sourceTree = ""; }; + DD203B841F7A297C00CA359C /* XMPPLastMessageCorrectionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = XMPPLastMessageCorrectionTests.m; path = "../../Testing-Shared/XMPPLastMessageCorrectionTests.m"; sourceTree = ""; }; + DD3559701F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPMessageCoreDataStorageTests.m; path = "../../Testing-Shared/XMPPMessageCoreDataStorageTests.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -108,6 +122,13 @@ D973A07A1D2F18040096F3ED /* XMPPURITests.m */, D973A07B1D2F18040096F3ED /* XMPPvCardTests.m */, DD1E732B1ED86B7D009B529B /* XMPPPubSubTests.m */, + DD26F5821F7CECD100F54F18 /* XMPPOneToOneChatTests.m */, + DD24E0241F7119A300FA813C /* XMPPDelayedDeliveryTests.m */, + DD2AD6EF1F84CB0400E0FED2 /* XMPPManagedMessagingTests.m */, + DD06EA3F1F78E566008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */, + DDB40BA71F750B0800B82A93 /* XMPPOutOfBandResourceMessagingTests.m */, + DD203B841F7A297C00CA359C /* XMPPLastMessageCorrectionTests.m */, + DD3559701F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m */, 63F50D971C60208200CA0201 /* Info.plist */, D973A06E1D2F18030096F3ED /* XMPPFrameworkTests-Bridging-Header.h */, ); @@ -250,11 +271,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DDB40BA81F750B0800B82A93 /* XMPPOutOfBandResourceMessagingTests.m in Sources */, D973A07C1D2F18040096F3ED /* CapabilitiesHashingTest.m in Sources */, + DD26F5831F7CECD100F54F18 /* XMPPOneToOneChatTests.m in Sources */, + DD24E0251F7119A300FA813C /* XMPPDelayedDeliveryTests.m in Sources */, D973A0811D2F18040096F3ED /* XMPPMUCLightTests.m in Sources */, D973A07D1D2F18040096F3ED /* EncodeDecodeTest.m in Sources */, D973A0831D2F18040096F3ED /* XMPPRoomLightCoreDataStorageTests.m in Sources */, + DD203B851F7A297C00CA359C /* XMPPLastMessageCorrectionTests.m in Sources */, D973A0801D2F18040096F3ED /* XMPPMockStream.m in Sources */, + DD2AD6F01F84CB0400E0FED2 /* XMPPManagedMessagingTests.m in Sources */, D973A0841D2F18040096F3ED /* XMPPRoomLightTests.m in Sources */, D97509281D9C82DB002E6F51 /* OMEMOServerTests.m in Sources */, D99C5E0D1D99C48100FB068A /* OMEMOModuleTests.m in Sources */, @@ -263,9 +289,11 @@ D973A07E1D2F18040096F3ED /* XMPPHTTPFileUploadTests.m in Sources */, D973A0821D2F18040096F3ED /* XMPPPushTests.swift in Sources */, D9E35E701D90B894002E7CF7 /* OMEMOElementTests.m in Sources */, + DD3559711F3CA50C000D25BA /* XMPPMessageCoreDataStorageTests.m in Sources */, D973A0851D2F18040096F3ED /* XMPPStorageHintTests.m in Sources */, D973A0891D2F18310096F3ED /* XMPPSwift.swift in Sources */, DD1E732C1ED86B7D009B529B /* XMPPPubSubTests.m in Sources */, + DD06EA401F78E566008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */, D973A0871D2F18040096F3ED /* XMPPvCardTests.m in Sources */, D99C5E0E1D99C48100FB068A /* OMEMOTestStorage.m in Sources */, ); diff --git a/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj b/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj index 3c8030996f..01b7a4aa24 100644 --- a/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj +++ b/Xcode/Testing-macOS/XMPPFrameworkTests.xcodeproj/project.pbxproj @@ -25,6 +25,13 @@ D99C5E091D95EBA100FB068A /* OMEMOTestStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = D99C5E081D95EBA100FB068A /* OMEMOTestStorage.m */; }; D9E35E6E1D90B2C5002E7CF7 /* OMEMOElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E35E6D1D90B2C5002E7CF7 /* OMEMOElementTests.m */; }; D9F20D011D836080002A8D6F /* OMEMOModuleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F20D001D836080002A8D6F /* OMEMOModuleTests.m */; }; + DD26F5851F7CF17200F54F18 /* XMPPOneToOneChatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD26F5841F7CF17200F54F18 /* XMPPOneToOneChatTests.m */; }; + DD24E0271F71463C00FA813C /* XMPPDelayedDeliveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD24E0261F71463C00FA813C /* XMPPDelayedDeliveryTests.m */; }; + DDA11A491F8517C300591D1B /* XMPPManagedMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DDA11A481F8517C200591D1B /* XMPPManagedMessagingTests.m */; }; + DD06EA421F78EB3B008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD06EA411F78EB3B008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */; }; + DD4003D51F7525A50078D144 /* XMPPOutOfBandResourceMessagingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD4003D41F7525A50078D144 /* XMPPOutOfBandResourceMessagingTests.m */; }; + DD203B871F7A6F5200CA359C /* XMPPLastMessageCorrectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD203B861F7A6F5200CA359C /* XMPPLastMessageCorrectionTests.m */; }; + DD24E0031F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DD24E0021F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -52,6 +59,13 @@ D99C5E081D95EBA100FB068A /* OMEMOTestStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOTestStorage.m; path = "../../Testing-Shared/OMEMOTestStorage.m"; sourceTree = ""; }; D9E35E6D1D90B2C5002E7CF7 /* OMEMOElementTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOElementTests.m; path = "../../Testing-Shared/OMEMOElementTests.m"; sourceTree = ""; }; D9F20D001D836080002A8D6F /* OMEMOModuleTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OMEMOModuleTests.m; path = "../../Testing-Shared/OMEMOModuleTests.m"; sourceTree = ""; }; + DD26F5841F7CF17200F54F18 /* XMPPOneToOneChatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPOneToOneChatTests.m; path = "../../Testing-Shared/XMPPOneToOneChatTests.m"; sourceTree = ""; }; + DD24E0261F71463C00FA813C /* XMPPDelayedDeliveryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPDelayedDeliveryTests.m; path = "../../Testing-Shared/XMPPDelayedDeliveryTests.m"; sourceTree = ""; }; + DDA11A481F8517C200591D1B /* XMPPManagedMessagingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPManagedMessagingTests.m; path = "../../Testing-Shared/XMPPManagedMessagingTests.m"; sourceTree = ""; }; + DD06EA411F78EB3B008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPMessageDeliveryReceiptsTests.m; path = "../../Testing-Shared/XMPPMessageDeliveryReceiptsTests.m"; sourceTree = ""; }; + DD4003D41F7525A50078D144 /* XMPPOutOfBandResourceMessagingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPOutOfBandResourceMessagingTests.m; path = "../../Testing-Shared/XMPPOutOfBandResourceMessagingTests.m"; sourceTree = ""; }; + DD203B861F7A6F5200CA359C /* XMPPLastMessageCorrectionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPLastMessageCorrectionTests.m; path = "../../Testing-Shared/XMPPLastMessageCorrectionTests.m"; sourceTree = ""; }; + DD24E0021F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = XMPPMessageCoreDataStorageTests.m; path = "../../Testing-Shared/XMPPMessageCoreDataStorageTests.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -108,6 +122,13 @@ D973A0A11D2F1EF60096F3ED /* XMPPSwift.swift */, D973A0A21D2F1EF60096F3ED /* XMPPURITests.m */, D973A0A31D2F1EF60096F3ED /* XMPPvCardTests.m */, + DD26F5841F7CF17200F54F18 /* XMPPOneToOneChatTests.m */, + DD24E0261F71463C00FA813C /* XMPPDelayedDeliveryTests.m */, + DDA11A481F8517C200591D1B /* XMPPManagedMessagingTests.m */, + DD06EA411F78EB3B008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m */, + DD4003D41F7525A50078D144 /* XMPPOutOfBandResourceMessagingTests.m */, + DD203B861F7A6F5200CA359C /* XMPPLastMessageCorrectionTests.m */, + DD24E0021F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m */, D973A0921D2F1EB10096F3ED /* Info.plist */, ); path = XMPPFrameworkTests; @@ -245,12 +266,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DD4003D51F7525A50078D144 /* XMPPOutOfBandResourceMessagingTests.m in Sources */, D973A0A41D2F1EF60096F3ED /* CapabilitiesHashingTest.m in Sources */, + DD06EA421F78EB3B008FA8C2 /* XMPPMessageDeliveryReceiptsTests.m in Sources */, + DD24E0031F70F31300FA813C /* XMPPMessageCoreDataStorageTests.m in Sources */, D9F20D011D836080002A8D6F /* OMEMOModuleTests.m in Sources */, D973A0A91D2F1EF60096F3ED /* XMPPMUCLightTests.m in Sources */, + DD24E0271F71463C00FA813C /* XMPPDelayedDeliveryTests.m in Sources */, D973A0A51D2F1EF60096F3ED /* EncodeDecodeTest.m in Sources */, + DD203B871F7A6F5200CA359C /* XMPPLastMessageCorrectionTests.m in Sources */, D973A0AB1D2F1EF60096F3ED /* XMPPRoomLightCoreDataStorageTests.m in Sources */, D973A0AE1D2F1EF60096F3ED /* XMPPSwift.swift in Sources */, + DDA11A491F8517C300591D1B /* XMPPManagedMessagingTests.m in Sources */, D9669B591D9B13FF0018533D /* OMEMOServerTests.m in Sources */, D973A0AF1D2F1EF60096F3ED /* XMPPURITests.m in Sources */, D973A0A81D2F1EF60096F3ED /* XMPPMockStream.m in Sources */, @@ -258,6 +285,7 @@ D973A0B01D2F1EF60096F3ED /* XMPPvCardTests.m in Sources */, D973A0A71D2F1EF60096F3ED /* XMPPMessageArchiveManagementTests.m in Sources */, D9E35E6E1D90B2C5002E7CF7 /* OMEMOElementTests.m in Sources */, + DD26F5851F7CF17200F54F18 /* XMPPOneToOneChatTests.m in Sources */, D973A0A61D2F1EF60096F3ED /* XMPPHTTPFileUploadTests.m in Sources */, D973A0AA1D2F1EF60096F3ED /* XMPPPushTests.swift in Sources */, D973A0AD1D2F1EF60096F3ED /* XMPPStorageHintTests.m in Sources */, From b94e92efb6b76e4dff11b263d7c6506bfac65a8d Mon Sep 17 00:00:00 2001 From: Piotr Wegrzynek Date: Thu, 2 Nov 2017 15:12:57 +0100 Subject: [PATCH 2/2] Adapt the client message storage implementation to the modern Obj-C updates in master --- Core/XMPPStream.h | 6 +++--- .../CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h | 4 ++++ Extensions/XEP-0203/NSXMLElement+XEP_0203.h | 4 ++-- Xcode/Testing-Shared/XMPPManagedMessagingTests.m | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Core/XMPPStream.h b/Core/XMPPStream.h index d601a873f4..70dbea6212 100644 --- a/Core/XMPPStream.h +++ b/Core/XMPPStream.h @@ -646,7 +646,7 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; * Even if you close the xmpp stream after this point, the OS will still do everything it can to send the data. **/ - (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt * _Nullable * _Nullable)receiptPtr; -- (void)sendElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID andGetReceipt:(XMPPElementReceipt **)receiptPtr; +- (void)sendElement:(NSXMLElement *)element registeringEventWithID:(NSString *)eventID andGetReceipt:(XMPPElementReceipt * _Nullable * _Nullable)receiptPtr; /** * Fetches and resends the myPresence element (if available) in a single atomic operation. @@ -747,7 +747,7 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; * This method returns nil if called outside of those callbacks. * For more details, please refer to @c XMPPElementEvent documentation. */ -- (XMPPElementEvent *)currentElementEvent; +- (nullable XMPPElementEvent *)currentElementEvent; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities @@ -822,7 +822,7 @@ extern const NSTimeInterval XMPPStreamTimeoutNone; @property (nonatomic, copy, readonly) NSString *uniqueID; /// The value of the stream's @c myJID property at the time when the event occured. -@property (nonatomic, strong, readonly) XMPPJID *myJID; +@property (nonatomic, strong, readonly, nullable) XMPPJID *myJID; /// The local device time when the event occured. @property (nonatomic, strong, readonly) NSDate *timestamp; diff --git a/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h index add2dd60fa..f76cf79c3d 100644 --- a/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h +++ b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h @@ -1,6 +1,8 @@ #import #import "XMPPJID.h" +NS_ASSUME_NONNULL_BEGIN + @interface NSManagedObject (XMPPCoreDataStorage) /// @brief Inserts a managed object with an entity whose name matches the class name. @@ -27,3 +29,5 @@ - (NSArray *)xmpp_executeForcedSuccessFetchRequest:(NSFetchRequest *)fetchRequest; @end + +NS_ASSUME_NONNULL_END diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.h b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h index 528997c318..84fd413ba9 100644 --- a/Extensions/XEP-0203/NSXMLElement+XEP_0203.h +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h @@ -7,7 +7,7 @@ @property (nonatomic, readonly) BOOL wasDelayed; @property (nonatomic, readonly, nullable) NSDate *delayedDeliveryDate; -- (XMPPJID *)delayedDeliveryFrom; -- (NSString *)delayedDeliveryReasonDescription; +@property (nonatomic, readonly, nullable) XMPPJID *delayedDeliveryFrom; +@property (nonatomic, readonly, nullable) NSString *delayedDeliveryReasonDescription; @end diff --git a/Xcode/Testing-Shared/XMPPManagedMessagingTests.m b/Xcode/Testing-Shared/XMPPManagedMessagingTests.m index f469063cc1..d0768cc673 100644 --- a/Xcode/Testing-Shared/XMPPManagedMessagingTests.m +++ b/Xcode/Testing-Shared/XMPPManagedMessagingTests.m @@ -205,7 +205,7 @@ - (void)fakeReceivingAckForStanzaIDs:(NSArray *)stanzaIDs }); } -- (BOOL)didResumeWithAckedStanzaIds:(NSArray *__autoreleasing *)stanzaIdsPtr serverResponse:(NSXMLElement *__autoreleasing *)responsePtr +- (BOOL)didResumeWithAckedStanzaIds:(NSArray *__autoreleasing _Nullable *)stanzaIdsPtr serverResponse:(NSXMLElement *__autoreleasing _Nullable *)responsePtr { *stanzaIdsPtr = self.resumeStanzaIDs; return YES;