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..70dbea6212 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 * _Nullable * _Nullable)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. + */ +- (nullable 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, nullable) 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..f76cf79c3d --- /dev/null +++ b/Extensions/CoreDataStorage/NSManagedObject+XMPPCoreDataStorage.h @@ -0,0 +1,33 @@ +#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. +/// @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 + +NS_ASSUME_NONNULL_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 0000000000..5d2171caf1 Binary files /dev/null and b/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/elements differ diff --git a/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/layout b/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/layout new file mode 100644 index 0000000000..9a4906ad29 Binary files /dev/null and b/Extensions/MessageStorage/XMPPMessage.xcdatamodeld/XMPPMessage.xcdatamodel/layout differ 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..84fd413ba9 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; +@property (nonatomic, readonly, nullable) XMPPJID *delayedDeliveryFrom; +@property (nonatomic, readonly, nullable) 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..d0768cc673 --- /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 _Nullable *)stanzaIdsPtr serverResponse:(NSXMLElement *__autoreleasing _Nullable *)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 */,