#import "XMPPRoomHybridStorage.h" #import "XMPPRoomPrivate.h" #import "XMPPCoreDataStorageProtected.h" #import "NSXMLElement+XEP_0203.h" #import "XMPPLogging.h" #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif // Log levels: off, error, warn, info, verbose #if DEBUG static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; #else static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; #endif #define AssertPrivateQueue() \ NSAssert(dispatch_get_specific(storageQueueTag), @"Private method: MUST run on storageQueue"); @interface XMPPRoomHybridStorage () { // Protected variables are listed in the header file. // These are the private variables. NSString *messageEntityName; Class occupantClass; NSTimeInterval maxMessageAge; NSTimeInterval deleteInterval; NSMutableSet *pausedMessageDeletion; dispatch_time_t lastDeleteTime; dispatch_source_t deleteTimer; } - (void)performDelete; - (void)destroyDeleteTimer; - (void)updateDeleteTimer; - (void)createAndStartDeleteTimer; @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation XMPPRoomHybridStorage static XMPPRoomHybridStorage *sharedInstance; + (instancetype)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[XMPPRoomHybridStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; }); return sharedInstance; } - (void)commonInit { XMPPLogTrace(); [super commonInit]; // This method is invoked by all public init methods of the superclass occupantsGlobalDict = [[NSMutableDictionary alloc] init]; messageEntityName = NSStringFromClass([XMPPRoomMessageHybridCoreDataStorageObject class]); occupantClass = [XMPPRoomOccupantHybridMemoryStorageObject class]; maxMessageAge = (60 * 60 * 24 * 7); // 7 days deleteInterval = (60 * 5); // 5 days pausedMessageDeletion = [[NSMutableSet alloc] init]; autoRecreateDatabaseFile = YES; } /** * Documentation from the superclass (XMPPCoreDataStorage): * * Override me, if needed, to provide customized behavior. * * This method is queried to get the name of the ManagedObjectModel within the app bundle. * It should return the name of the appropriate file (*.xdatamodel / *.mom / *.momd) sans file extension. * * The default implementation returns the name of the subclass, stripping any suffix of "CoreDataStorage". * E.g., if your subclass was named "XMPPExtensionCoreDataStorage", then this method would return "XMPPExtension". * * Note that a file extension should NOT be included. **/ - (NSString *)managedObjectModelName { // Optional hook // // The default implementation would return "XMPPPRoomHybridStorage". // We prefer a slightly shorter version. return @"XMPPRoomHybrid"; } - (void)dealloc { [self destroyDeleteTimer]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @synthesize messageEntityName; @synthesize occupantClass; - (NSTimeInterval)maxMessageAge { __block NSTimeInterval result = 0; dispatch_block_t block = ^{ result = maxMessageAge; }; if (dispatch_get_specific(storageQueueTag)) block(); else dispatch_sync(storageQueue, block); return result; } - (void)setMaxMessageAge:(NSTimeInterval)age { dispatch_block_t block = ^{ @autoreleasepool { NSTimeInterval oldMaxMessageAge = maxMessageAge; NSTimeInterval newMaxMessageAge = age; maxMessageAge = age; // There are several cases we need to handle here. // // 1. If the maxAge was previously enabled and it just got disabled, // then we need to stop the deleteTimer. (And we might as well release it.) // // 2. If the maxAge was previously disabled and it just got enabled, // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) // // 3. If the maxAge was increased, // then we don't need to do anything. // // 4. If the maxAge was decreased, // then we should do an immediate delete. BOOL shouldDeleteNow = NO; if (oldMaxMessageAge > 0.0) { if (newMaxMessageAge <= 0.0) { // Handles #1 [self destroyDeleteTimer]; } else if (oldMaxMessageAge > newMaxMessageAge) { // Handles #4 shouldDeleteNow = YES; } else { // Handles #3 // Nothing to do now } } else if (newMaxMessageAge > 0.0) { // Handles #2 shouldDeleteNow = YES; } if (shouldDeleteNow) { [self performDelete]; if (deleteTimer) [self updateDeleteTimer]; else [self createAndStartDeleteTimer]; } }}; if (dispatch_get_specific(storageQueueTag)) block(); else dispatch_async(storageQueue, block); } - (NSTimeInterval)deleteInterval { __block NSTimeInterval result = 0; dispatch_block_t block = ^{ result = deleteInterval; }; if (dispatch_get_specific(storageQueueTag)) block(); else dispatch_sync(storageQueue, block); return result; } - (void)setDeleteInterval:(NSTimeInterval)interval { dispatch_block_t block = ^{ @autoreleasepool { deleteInterval = interval; // There are several cases we need to handle here. // // 1. If the deleteInterval was previously enabled and it just got disabled, // then we need to stop the deleteTimer. (And we might as well release it.) // // 2. If the deleteInterval was previously disabled and it just got enabled, // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) // // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. // // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. // (Plus we might need to do an immediate delete.) if (deleteInterval > 0.0) { if (deleteTimer == NULL) { // Handles #2 // // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, // if a delete is needed the timer will fire immediately. [self createAndStartDeleteTimer]; } else { // Handles #3 // Handles #4 // // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, // if a save is needed the timer will fire immediately. [self updateDeleteTimer]; } } else if (deleteTimer) { // Handles #1 [self destroyDeleteTimer]; } }}; if (dispatch_get_specific(storageQueueTag)) block(); else dispatch_async(storageQueue, block); } - (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID { dispatch_block_t block = ^{ @autoreleasepool { [pausedMessageDeletion addObject:[roomJID bareJID]]; }}; if (dispatch_get_specific(storageQueueTag)) block(); else dispatch_async(storageQueue, block); } - (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID { dispatch_block_t block = ^{ @autoreleasepool { [pausedMessageDeletion removeObject:[roomJID bareJID]]; [self performDelete]; }}; if (dispatch_get_specific(storageQueueTag)) block(); else dispatch_async(storageQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Internal API //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)performDelete { if (maxMessageAge <= 0.0) return; NSDate *minLocalTimestamp = [NSDate dateWithTimeIntervalSinceNow:(maxMessageAge * -1.0)]; NSManagedObjectContext *moc = [self managedObjectContext]; NSEntityDescription *messageEntity = [self messageEntity:moc]; NSPredicate *predicate; if ([pausedMessageDeletion count] > 0) { predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@ AND roomJIDStr NOT IN %@", minLocalTimestamp, pausedMessageDeletion]; } else { predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@", minLocalTimestamp]; } NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; [fetchRequest setEntity:messageEntity]; [fetchRequest setPredicate:predicate]; [fetchRequest setFetchBatchSize:saveThreshold]; NSError *error = nil; NSArray *oldMessages = [moc executeFetchRequest:fetchRequest error:&error]; if (error) { XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error); } NSUInteger unsavedCount = [self numberOfUnsavedChanges]; for (XMPPRoomMessageHybridCoreDataStorageObject *oldMessage in oldMessages) { [moc deleteObject:oldMessage]; if (++unsavedCount >= saveThreshold) { [self save]; unsavedCount = 0; } } lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); } - (void)destroyDeleteTimer { if (deleteTimer) { dispatch_source_cancel(deleteTimer); #if !OS_OBJECT_USE_OBJC dispatch_release(deleteTimer); #endif deleteTimer = NULL; } } - (void)updateDeleteTimer { if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0)) { uint64_t interval = deleteInterval * NSEC_PER_SEC; dispatch_time_t startTime; if (lastDeleteTime > 0) startTime = dispatch_time(lastDeleteTime, interval); else startTime = dispatch_time(DISPATCH_TIME_NOW, interval); dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); } } - (void)createAndStartDeleteTimer { if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0)) { deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, storageQueue); dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { [self performDelete]; }}); [self updateDeleteTimer]; if (deleteTimer) dispatch_resume(deleteTimer); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Protected API //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Optional override hook. **/ - (BOOL)existsMessage:(XMPPMessage *)message forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream { NSDate *remoteTimestamp = [message delayedDeliveryDate]; if (remoteTimestamp == nil) { // When the xmpp server sends us a room message, it will always timestamp delayed messages. // For example, when retrieving the discussion history, all messages will include the original timestamp. // If a message doesn't include such timestamp, then we know we're getting it in "real time". return NO; } // Does this message already exist in the database? // How can we tell if two XMPPRoomMessages are the same? // // 1. Same streamBareJidStr // 2. Same jid // 3. Same text // 4. Approximately the same timestamps // // This is actually a rather difficult question. // What if the same user sends the exact same message multiple times? // // If we first received the message while already in the room, it won't contain a remoteTimestamp. // Returning to the room later and downloading the discussion history will return the same message, // this time with a remote timestamp. // // So if the message doesn't have a remoteTimestamp, // but it's localTimestamp is approximately the same as the remoteTimestamp, // then this is enough evidence to consider the messages the same. // // Note: Predicate order matters. Most unique key should be first, least unique should be last. NSManagedObjectContext *moc = [self managedObjectContext]; NSEntityDescription *messageEntity = [self messageEntity:moc]; NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; XMPPJID *messageJID = [message from]; NSString *messageBody = [[message elementForName:@"body"] stringValue]; NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60]; NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60]; NSString *predicateFormat = @" body == %@ " @"AND jidStr == %@ " @"AND streamBareJidStr == %@ " @"AND " @"(" @" (remoteTimestamp == %@) " @" OR (remoteTimestamp == NIL && localTimestamp BETWEEN {%@, %@})" @")"; NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat, messageBody, messageJID, streamBareJidStr, remoteTimestamp, minLocalTimestamp, maxLocalTimestamp]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; [fetchRequest setEntity:messageEntity]; [fetchRequest setPredicate:predicate]; [fetchRequest setFetchLimit:1]; NSError *error = nil; NSArray *results = [moc executeFetchRequest:fetchRequest error:&error]; if (error) { XMPPLogError(@"%@: %@ - Fetch error: %@", THIS_FILE, THIS_METHOD, error); } return ([results count] > 0); } /** * Optional override hook for general extensions. * * @see insertMessage:outgoing:forRoom:stream: **/ - (void)didInsertMessage:(XMPPRoomMessageHybridCoreDataStorageObject *)message { // Override me if you're extending the XMPPRoomMessageHybridCoreDataStorageObject class // to add additional properties, which you can set here. // // At this point the standard properties have already been set. // So you can, for example, access the XMPPMessage via message.message. } /** * Optional override hook for complete customization. * Override me if you need to do specific custom work when inserting a message in a room. * * @see didInsertMessage: **/ - (void)insertMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream { // Extract needed information XMPPJID *roomJID = room.roomJID; XMPPJID *messageJID = isOutgoing ? room.myRoomJID : [message from]; NSDate *localTimestamp; NSDate *remoteTimestamp; if (isOutgoing) { localTimestamp = [[NSDate alloc] init]; remoteTimestamp = nil; } else { remoteTimestamp = [message delayedDeliveryDate]; if (remoteTimestamp) { localTimestamp = remoteTimestamp; } else { localTimestamp = [[NSDate alloc] init]; } } NSString *messageBody = [[message elementForName:@"body"] stringValue]; NSManagedObjectContext *moc = [self managedObjectContext]; NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; NSEntityDescription *messageEntity = [self messageEntity:moc]; // Add to database XMPPRoomMessageHybridCoreDataStorageObject *roomMessage = (XMPPRoomMessageHybridCoreDataStorageObject *) [[NSManagedObject alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:nil]; roomMessage.message = message; roomMessage.roomJID = roomJID; roomMessage.jid = messageJID; roomMessage.nickname = [messageJID resource]; roomMessage.body = messageBody; roomMessage.localTimestamp = localTimestamp; roomMessage.remoteTimestamp = remoteTimestamp; roomMessage.isFromMe = isOutgoing; roomMessage.streamBareJidStr = streamBareJidStr; [moc insertObject:roomMessage]; // Hook if subclassing XMPPRoomMessageHybridCDSO (awakeFromInsert) [self didInsertMessage:roomMessage]; // Hook if subclassing XMPPRoomHybridStorage } /** * Optional override hook for general extensions. * * @see insertOccupantWithPresence:room:stream: **/ - (void)didInsertOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant { // Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class // to add additional properties, which you can set here. // // At this point the standard properties have already been set. // So you can, for example, access the XMPPPresence via occupant.presece. } /** * Optional override hook for general extensions. * * @see updateOccupant:withPresence:room:stream: **/ - (void)didUpdateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant { // Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class, // and you have additional properties that may need to be updated. // // At this point the standard properties have already been updated. } /** * Optional override hook for general extensions. **/ - (void)willRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant { // Override me if you have any custom work to do before an occupant leaves (is removed from storage). } /** * Optional override hook for general extensions. **/ - (void)didRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant { // Override me if you have any custom work to do after an occupant leaves (is removed from storage). } /** * Optional override hook for complete customization. * Override me if you need to do custom work when inserting an occupant in a room. **/ - (XMPPRoomOccupantHybridMemoryStorageObject *)insertOccupantWithPresence:(XMPPPresence *)presence room:(XMPPRoom *)room stream:(XMPPStream *)xmppStream { XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; XMPPJID *roomJid = room.roomJID; NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; if (occupantsRoomsDict == nil) { occupantsRoomsDict = [[NSMutableDictionary alloc] init]; occupantsGlobalDict[streamFullJid] = occupantsRoomsDict; } NSMutableDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; if (occupantsRoomDict == nil) { occupantsRoomDict = [[NSMutableDictionary alloc] init]; occupantsRoomsDict[roomJid] = occupantsRoomDict; } XMPPRoomOccupantHybridMemoryStorageObject *occupant = (XMPPRoomOccupantHybridMemoryStorageObject *) [[self.occupantClass alloc] initWithPresence:presence streamFullJid:streamFullJid]; occupantsRoomDict[occupant.jid] = occupant; return occupant; } /** * Optional override hook for complete customization. * Override me if you need to do custom work when updating an occupant in a room. **/ - (void)updateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant withPresence:(XMPPPresence *)presence room:(XMPPRoom *)room stream:(XMPPStream *)stream { [occupant updateWithPresence:presence]; } /** * Optional override hook for complete customization. * Override me if you need to do custom work when removing an occupant from a room. **/ - (void)removeOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant withPresence:(XMPPPresence *)presence room:(XMPPRoom *)room stream:(XMPPStream *)stream { // Remove from dictionary XMPPJID *streamFullJid = [self myJIDForXMPPStream:stream]; XMPPJID *roomJid = occupant.roomJID; NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; NSMutableDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; [occupantsRoomDict removeObjectForKey:occupant.jid]; // Remove occupant if ([occupantsRoomDict count] == 0) { [occupantsRoomsDict removeObjectForKey:roomJid]; // Remove room if now empty } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Public API //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc { // This method should be thread-safe. // So be sure to access the entity name through the property accessor. if (moc == nil) { XMPPLogWarn(@"%@: %@ - Invalid parameter, moc is nil", THIS_FILE, THIS_METHOD); return nil; } NSString *entityName = self.messageEntityName; return [NSEntityDescription entityForName:entityName inManagedObjectContext:moc]; } - (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID stream:(XMPPStream *)xmppStream inContext:(NSManagedObjectContext *)inMoc { if (roomJID == nil) return nil; // It's possible to use our internal managedObjectContext only because we're not returning a NSManagedObject. __block NSDate *result = nil; dispatch_block_t block = ^{ @autoreleasepool { NSManagedObjectContext *moc = inMoc ? : [self managedObjectContext]; NSEntityDescription *entity = [self messageEntity:moc]; NSPredicate *predicate; if (xmppStream) { NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; NSString *predicateFormat = @"roomJIDStr == %@ AND streamBareJidStr == %@"; predicate = [NSPredicate predicateWithFormat:predicateFormat, roomJID, streamBareJidStr]; } else { predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", roomJID]; } NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"localTimestamp" ascending:NO]; NSArray *sortDescriptors = @[sortDescriptor]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; [fetchRequest setEntity:entity]; [fetchRequest setPredicate:predicate]; [fetchRequest setSortDescriptors:sortDescriptors]; [fetchRequest setFetchLimit:1]; NSError *error = nil; XMPPRoomMessageHybridCoreDataStorageObject *message = (XMPPRoomMessageHybridCoreDataStorageObject *) [[moc executeFetchRequest:fetchRequest error:&error] lastObject]; if (error) { XMPPLogError(@"%@: %@ - fetchRequest error: %@", THIS_FILE, THIS_METHOD, error); } else { result = [message.localTimestamp copy]; } }}; if (inMoc == nil) dispatch_sync(storageQueue, block); else block(); return result; } - (XMPPRoomOccupantHybridMemoryStorageObject *)occupantForJID:(XMPPJID *)occupantJid stream:(XMPPStream *)xmppStream { if (occupantJid == nil) return nil; __block XMPPRoomOccupantHybridMemoryStorageObject *occupant = nil; void (^block)(BOOL) = ^(BOOL shouldCopy){ @autoreleasepool { XMPPJID *roomJid = [occupantJid bareJID]; if (xmppStream) { XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; occupant = occupantsRoomDict[occupantJid]; } else { for (XMPPJID *streamFullJid in occupantsGlobalDict) { NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; occupant = occupantsRoomDict[occupantJid]; if (occupant) break; } } if (shouldCopy) { occupant = [occupant copy]; } }}; if (dispatch_get_specific(storageQueueTag)) block(NO); else dispatch_sync(storageQueue, ^{ block(YES); }); return occupant; } - (NSArray *)occupantsForRoom:(XMPPJID *)roomJid stream:(XMPPStream *)xmppStream { roomJid = [roomJid bareJID]; // Just in case a full jid is accidentally passed __block NSArray *results = nil; void (^block)(BOOL) = ^(BOOL shouldCopy){ @autoreleasepool { if (xmppStream) { XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; results = [occupantsRoomDict allValues]; } else { for (XMPPJID *streamFullJid in occupantsGlobalDict) { NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; if (occupantsRoomDict) { results = [occupantsRoomDict allValues]; break; } } } if (shouldCopy) { NSArray *temp = results; results = [[NSArray alloc] initWithArray:temp copyItems:YES]; } }}; if (dispatch_get_specific(storageQueueTag)) block(NO); else dispatch_sync(storageQueue, ^{ block(YES); }); return results; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark XMPPRoomStorage Protocol //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room { XMPPLogTrace(); dispatch_queue_t roomQueue = room.moduleQueue; XMPPStream *xmppStream = room.xmppStream; [self scheduleBlock:^{ XMPPJID *from = [presence from]; if ([[presence type] isEqualToString:@"unavailable"]) { XMPPRoomOccupantHybridMemoryStorageObject *occupant = [self occupantForJID:from stream:xmppStream]; if (occupant) { // Occupant did leave - remove [self willRemoveOccupant:occupant]; [self removeOccupant:occupant withPresence:presence room:room stream:xmppStream]; [self didRemoveOccupant:occupant]; // Notify delegate(s) XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy]; dispatch_async(roomQueue, ^{ @autoreleasepool { GCDMulticastDelegate *roomMulticastDelegate = (GCDMulticastDelegate *)[room multicastDelegate]; [roomMulticastDelegate xmppRoomHybridStorage:self occupantDidLeave:occupantCopy]; }}); } } else { XMPPRoomOccupantHybridMemoryStorageObject *occupant = [self occupantForJID:from stream:xmppStream]; if (occupant == nil) { // Occupant did join - add occupant = [self insertOccupantWithPresence:presence room:room stream:xmppStream]; if (occupant == nil) { // Subclasses may choose to ignore occupants for whatever reason. return; } [self didInsertOccupant:occupant]; // Notify delegate(s) XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy]; dispatch_async(roomQueue, ^{ @autoreleasepool { GCDMulticastDelegate *roomMulticastDelegate = (GCDMulticastDelegate *)[room multicastDelegate]; [roomMulticastDelegate xmppRoomHybridStorage:self occupantDidJoin:occupantCopy]; }}); } else { // Occupant did update - move [self updateOccupant:occupant withPresence:presence room:room stream:xmppStream]; [self didUpdateOccupant:occupant]; // Notify delegate(s) XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy]; dispatch_async(roomQueue, ^{ @autoreleasepool { GCDMulticastDelegate *roomMulticastDelegate = (GCDMulticastDelegate *)[room multicastDelegate]; [roomMulticastDelegate xmppRoomHybridStorage:self occupantDidUpdate:occupantCopy]; }}); } } }]; } - (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room { XMPPLogTrace(); XMPPStream *xmppStream = room.xmppStream; [self scheduleBlock:^{ [self insertMessage:message outgoing:YES forRoom:room stream:xmppStream]; }]; } - (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room { XMPPLogTrace(); XMPPJID *myRoomJID = room.myRoomJID; XMPPJID *messageJID = [message from]; if ([myRoomJID isEqualToJID:messageJID]) { if (![message wasDelayed]) { // Ignore - we already stored message in handleOutgoingMessage:room: return; } } XMPPStream *xmppStream = room.xmppStream; [self scheduleBlock:^{ if ([self existsMessage:message forRoom:room stream:xmppStream]) { XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD); } else { [self insertMessage:message outgoing:NO forRoom:room stream:xmppStream]; } }]; } - (void)handleDidLeaveRoom:(XMPPRoom *)room { XMPPLogTrace(); XMPPJID *roomJid = room.roomJID; XMPPStream *xmppStream = room.xmppStream; [self scheduleBlock:^{ XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; [occupantsRoomsDict removeObjectForKey:roomJid]; // Remove room (and all associated occupants) }]; } @end