- Update
This commit is contained in:
1 parent
d281d765ea
commit
5b074a5176
1261 files changed
+199158
-7303
No files matched your search
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>XMPPRoom.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="878" systemVersion="11C74" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="XMPPRoomMessageCoreDataStorageObject" representedClassName="XMPPRoomMessageCoreDataStorageObject" syncable="YES">
|
||||
<attribute name="body" optional="YES" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="fromMe" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
|
||||
<attribute name="jid" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="jidStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="localTimestamp" attributeType="Date" indexed="YES" syncable="YES"/>
|
||||
<attribute name="message" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="messageStr" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="nickname" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="remoteTimestamp" optional="YES" attributeType="Date" indexed="YES" syncable="YES"/>
|
||||
<attribute name="roomJID" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="roomJIDStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="streamBareJidStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="XMPPRoomOccupantCoreDataStorageObject" representedClassName="XMPPRoomOccupantCoreDataStorageObject" syncable="YES">
|
||||
<attribute name="affiliation" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="createdAt" attributeType="Date" defaultDateTimeInterval="0" defaultValueString="0" indexed="YES" syncable="YES"/>
|
||||
<attribute name="jid" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="jidStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="nickname" optional="YES" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="presence" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="presenceStr" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="realJID" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="realJIDStr" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="roomJID" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="roomJIDStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="streamBareJidStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="XMPPRoomMessageCoreDataStorageObject" positionX="160" positionY="192" width="128" height="240"/>
|
||||
<element name="XMPPRoomOccupantCoreDataStorageObject" positionX="160" positionY="192" width="128" height="240"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,125 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoom.h"
|
||||
#import "XMPPRoomMessageCoreDataStorageObject.h"
|
||||
#import "XMPPRoomOccupantCoreDataStorageObject.h"
|
||||
#import "XMPPCoreDataStorage.h"
|
||||
|
||||
|
||||
@interface XMPPRoomCoreDataStorage : XMPPCoreDataStorage <XMPPRoomStorage>
|
||||
|
||||
/**
|
||||
* Convenience method to get an instance with the default database name.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* You are NOT required to use the sharedInstance.
|
||||
*
|
||||
* If your application makes extensive use of MUC, and you use a sharedInstance of this class,
|
||||
* then all of your MUC rooms share the same database store. You might get better performance if you create
|
||||
* multiple instances of this class instead (using different database filenames), as this way you can have
|
||||
* concurrent writes to multiple databases.
|
||||
**/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
|
||||
/* Inherited from XMPPCoreDataStorage
|
||||
* Please see the XMPPCoreDataStorage header file for extensive documentation.
|
||||
|
||||
- (id)initWithDatabaseFilename:(NSString *)databaseFileName storeOptions:(NSDictionary *)storeOptions;
|
||||
- (id)initWithInMemoryStore;
|
||||
|
||||
@property (readonly) NSString *databaseFileName;
|
||||
|
||||
@property (readwrite) NSUInteger saveThreshold;
|
||||
|
||||
@property (readonly) NSManagedObjectModel *managedObjectModel;
|
||||
@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* You may choose to extend this class, and/or the message/occupant classes for customized functionality.
|
||||
* These properties allow for such customization.
|
||||
*
|
||||
* You must set your desired entity names, if different from default, before you begin using the storage class.
|
||||
**/
|
||||
@property (strong, readwrite) NSString * messageEntityName;
|
||||
@property (strong, readwrite) NSString * occupantEntityName;
|
||||
|
||||
/**
|
||||
* It is likely you don't want the message history to persist forever.
|
||||
* Doing so would allow the database to grow infinitely large over time.
|
||||
*
|
||||
* The maxMessageAge property provides a way to specify how old a message can get
|
||||
* before it should get deleted from the database.
|
||||
*
|
||||
* The deleteInterval specifies how often to sweep for old messages.
|
||||
* Since deleting is an expensive operation (disk io) it is done on a fixed interval.
|
||||
*
|
||||
* You can optionally disable the maxMessageAge by setting it to zero (or a negative value).
|
||||
* If you disable the maxMessageAge then old messages are not deleted.
|
||||
*
|
||||
* You can optionally disable the deleteInterval by setting it to zero (or a negative value).
|
||||
*
|
||||
* The default maxAge is 7 days.
|
||||
* The default deleteInterval is 5 minutes.
|
||||
**/
|
||||
@property (assign, readwrite) NSTimeInterval maxMessageAge;
|
||||
@property (assign, readwrite) NSTimeInterval deleteInterval;
|
||||
|
||||
/**
|
||||
* You may optionally prevent old message deletion for particular rooms.
|
||||
**/
|
||||
- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID;
|
||||
- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID;
|
||||
|
||||
/**
|
||||
* Convenience method to get the message/occupant entity description.
|
||||
*
|
||||
* @see messageEntityName
|
||||
* @see occupantEntityName
|
||||
**/
|
||||
- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc;
|
||||
- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc;
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the most recent message stored in the database for the given room.
|
||||
* This may be used when requesting the message history from the server,
|
||||
* to prevent redownloading messages you already have.
|
||||
*
|
||||
* @param roomJID - The JID of the room (a bare JID)
|
||||
*
|
||||
* @param xmppStream - This class can support multiple concurrent xmppStreams.
|
||||
* Optionally pass the xmppStream the room applies to.
|
||||
* If you're using this claass with a single xmppStream, you can pass nil.
|
||||
*
|
||||
* @param moc - The managedObjectContext to use when doing the lookups.
|
||||
* If non-nil, this should match the thread you're currently using.
|
||||
* If nil, the operation is dispatch_sync'd onto the internal queue,
|
||||
* and uses the internal managedObjectContext.
|
||||
*
|
||||
* The moc may optionally be nil strictly because this method does not return a NSManagedObject.
|
||||
**/
|
||||
- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID
|
||||
stream:(XMPPStream *)xmppStream
|
||||
inContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
/**
|
||||
* Returns the occupant for the given full jid.
|
||||
*
|
||||
* @param jid - The full jid of the room occupant (including resource).
|
||||
*
|
||||
* @param xmppStream - This class can support multiple concurrent xmppStreams.
|
||||
* Optionally pass the xmppStream the room applies to.
|
||||
* If you're using this claass with a single xmppStream, you can pass nil.
|
||||
*
|
||||
* @param moc - The managedObjectContext to use when doing the lookups.
|
||||
* This must not be nil, and should match the thread you're currently using.
|
||||
**/
|
||||
- (XMPPRoomOccupantCoreDataStorageObject *)occupantForJID:(XMPPJID *)jid
|
||||
stream:(XMPPStream *)xmppStream
|
||||
inContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,999 @@
|
||||
#import "XMPPRoomCoreDataStorage.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; // | XMPP_LOG_FLAG_TRACE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
#define AssertPrivateQueue() \
|
||||
NSAssert(dispatch_get_specific(storageQueueTag), @"Private method: MUST run on storageQueue");
|
||||
|
||||
@interface XMPPRoomCoreDataStorage ()
|
||||
{
|
||||
/* Inherited from XMPPCoreDataStorage
|
||||
|
||||
NSString *databaseFileName;
|
||||
NSUInteger saveThreshold;
|
||||
|
||||
dispatch_queue_t storageQueue;
|
||||
|
||||
*/
|
||||
|
||||
NSString *messageEntityName;
|
||||
NSString *occupantEntityName;
|
||||
|
||||
NSTimeInterval maxMessageAge;
|
||||
NSTimeInterval deleteInterval;
|
||||
|
||||
NSMutableSet *pausedMessageDeletion;
|
||||
|
||||
dispatch_time_t lastDeleteTime;
|
||||
dispatch_source_t deleteTimer;
|
||||
}
|
||||
|
||||
- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc;
|
||||
- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc;
|
||||
|
||||
- (void)performDelete;
|
||||
- (void)destroyDeleteTimer;
|
||||
- (void)updateDeleteTimer;
|
||||
- (void)createAndStartDeleteTimer;
|
||||
|
||||
- (void)clearAllOccupantsFromRoom:(XMPPJID *)roomJID;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRoomCoreDataStorage
|
||||
|
||||
static XMPPRoomCoreDataStorage *sharedInstance;
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
sharedInstance = [[XMPPRoomCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil];
|
||||
});
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
XMPPLogTrace();
|
||||
[super commonInit];
|
||||
|
||||
// This method is invoked by all public init methods of the superclass
|
||||
|
||||
messageEntityName = NSStringFromClass([XMPPRoomMessageCoreDataStorageObject class]);
|
||||
occupantEntityName = NSStringFromClass([XMPPRoomOccupantCoreDataStorageObject class]);
|
||||
|
||||
maxMessageAge = (60 * 60 * 24 * 7); // 7 days
|
||||
deleteInterval = (60 * 5); // 5 days
|
||||
|
||||
pausedMessageDeletion = [[NSMutableSet alloc] init];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self destroyDeleteTimer];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Configuration
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSString *)messageEntityName
|
||||
{
|
||||
__block NSString *result = nil;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = messageEntityName;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(storageQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(storageQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setMessageEntityName:(NSString *)newMessageEntityName
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
messageEntityName = newMessageEntityName;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(storageQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(storageQueue, block);
|
||||
}
|
||||
|
||||
- (NSString *)occupantEntityName
|
||||
{
|
||||
__block NSString *result = nil;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = occupantEntityName;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(storageQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(storageQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setOccupantEntityName:(NSString *)newOccupantEntityName
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
occupantEntityName = newOccupantEntityName;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(storageQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(storageQueue, block);
|
||||
}
|
||||
|
||||
- (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 Overrides
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)didCreateManagedObjectContext
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self clearAllOccupantsFromRoom:nil];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#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 NOT roomJIDStr 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 (XMPPRoomMessageCoreDataStorageObject *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 != NULL)
|
||||
{
|
||||
dispatch_resume(deleteTimer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clearAllOccupantsFromRoom:(XMPPJID *)roomJID
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertPrivateQueue();
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
NSEntityDescription *entity = [self occupantEntity:moc];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setFetchBatchSize:saveThreshold];
|
||||
|
||||
if (roomJID)
|
||||
{
|
||||
NSPredicate *predicate;
|
||||
predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", [roomJID bare]];
|
||||
|
||||
[fetchRequest setPredicate:predicate];
|
||||
}
|
||||
|
||||
NSArray *allOccupants = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
NSUInteger unsavedCount = [self numberOfUnsavedChanges];
|
||||
|
||||
for (XMPPRoomOccupantCoreDataStorageObject *occupant in allOccupants)
|
||||
{
|
||||
[moc deleteObject:occupant];
|
||||
|
||||
if (++unsavedCount >= saveThreshold)
|
||||
{
|
||||
[self save];
|
||||
unsavedCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#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:(XMPPRoomMessageCoreDataStorageObject *)message
|
||||
{
|
||||
// Override me if you're extending the XMPPRoomMessageCoreDataStorageObject class to add additional properties.
|
||||
// You can update your additional properties 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
|
||||
|
||||
XMPPRoomMessageCoreDataStorageObject *roomMessage = (XMPPRoomMessageCoreDataStorageObject *)
|
||||
[[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 XMPPRoomMessageCoreDataStorageObject (awakeFromInsert)
|
||||
[self didInsertMessage:roomMessage]; // Hook if subclassing XMPPRoomCoreDataStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional override hook for general extensions.
|
||||
*
|
||||
* @see insertOccupantWithPresence:room:stream:
|
||||
**/
|
||||
- (void)didInsertOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant
|
||||
{
|
||||
// Override me if you're extending the XMPPRoomOccupantCoreDataStorageObject class to add additional properties.
|
||||
// You can update your additional properties here.
|
||||
//
|
||||
// At this point the standard XMPPRoomOccupantCDSO properties have already been set.
|
||||
// So you can, for example, access the XMPPPresence via occupant.presence.
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional override hook for general extensions.
|
||||
*
|
||||
* @see updateOccupant:withPresence:room:stream:
|
||||
**/
|
||||
- (void)didUpdateOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant
|
||||
{
|
||||
// Override me if you're extending the XMPPRoomOccupantCoreDataStorageObject class to add additional properties.
|
||||
// You can update your additional properties here.
|
||||
//
|
||||
// At this point the standard XMPPRoomOccupantCDSO properties have already been updated.
|
||||
// So you can, for example, access the XMPPPresence via occupant.presence.
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional override hook for complete customization.
|
||||
* Override me if you need to do specific custom work when inserting an occupant in a room.
|
||||
*
|
||||
* @see didInsertOccupant:
|
||||
**/
|
||||
- (void)insertOccupantWithPresence:(XMPPPresence *)presence
|
||||
room:(XMPPRoom *)room
|
||||
stream:(XMPPStream *)xmppStream
|
||||
{
|
||||
// Extract needed information
|
||||
|
||||
XMPPJID *roomJID = room.roomJID;
|
||||
XMPPJID *presenceJID = [presence from];
|
||||
|
||||
NSString *role = nil;
|
||||
NSString *affiliation = nil;
|
||||
XMPPJID *realJID = nil;
|
||||
|
||||
NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"];
|
||||
NSXMLElement *item = [x elementForName:@"item"];
|
||||
if (item)
|
||||
{
|
||||
role = [[item attributeStringValueForName:@"role"] lowercaseString];
|
||||
affiliation = [[item attributeStringValueForName:@"affiliation"] lowercaseString];
|
||||
|
||||
NSString *realJIDStr = [item attributeStringValueForName:@"jid"];
|
||||
if (realJIDStr)
|
||||
{
|
||||
realJID = [XMPPJID jidWithString:realJIDStr];
|
||||
}
|
||||
}
|
||||
|
||||
// Add to database
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
|
||||
|
||||
NSEntityDescription *occupantEntity = [self occupantEntity:moc];
|
||||
|
||||
XMPPRoomOccupantCoreDataStorageObject *occupant = (XMPPRoomOccupantCoreDataStorageObject *)
|
||||
[[NSManagedObject alloc] initWithEntity:occupantEntity insertIntoManagedObjectContext:nil];
|
||||
|
||||
occupant.presence = presence;
|
||||
occupant.roomJID = roomJID;
|
||||
occupant.jid = presenceJID;
|
||||
occupant.nickname = [presenceJID resource];
|
||||
occupant.role = role;
|
||||
occupant.affiliation = affiliation;
|
||||
occupant.realJID = realJID;
|
||||
occupant.createdAt = [NSDate date];
|
||||
occupant.streamBareJidStr = streamBareJidStr;
|
||||
|
||||
[moc insertObject:occupant]; // Hook if subclassing XMPPRoomOccupantCoreDataStorageObject (awakeFromInsert)
|
||||
[self didInsertOccupant:occupant]; // Hook if subclassing XMPPRoomCoreDataStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional override hook for complete customization.
|
||||
* Override me if you need to do specific custom work when updating an occupant in a room.
|
||||
*
|
||||
* @see didUpdateOccupant:
|
||||
**/
|
||||
- (void)updateOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant
|
||||
withPresence:(XMPPPresence *)presence
|
||||
room:(XMPPRoom *)room
|
||||
stream:(XMPPStream *)stream
|
||||
{
|
||||
// Extract needed information
|
||||
|
||||
NSString *role = nil;
|
||||
NSString *affiliation = nil;
|
||||
XMPPJID *realJID = nil;
|
||||
|
||||
NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"];
|
||||
NSXMLElement *item = [x elementForName:@"item"];
|
||||
if (item)
|
||||
{
|
||||
role = [[item attributeStringValueForName:@"role"] lowercaseString];
|
||||
affiliation = [[item attributeStringValueForName:@"affiliation"] lowercaseString];
|
||||
|
||||
NSString *realJIDStr = [item attributeStringValueForName:@"jid"];
|
||||
if (realJIDStr)
|
||||
{
|
||||
realJID = [XMPPJID jidWithString:realJIDStr];
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
|
||||
occupant.presence = presence;
|
||||
occupant.role = role;
|
||||
occupant.affiliation = affiliation;
|
||||
occupant.realJID = realJID;
|
||||
|
||||
[self didUpdateOccupant:occupant]; // Hook if subclassing XMPPRoomCoreDataStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional override hook.
|
||||
**/
|
||||
- (void)removeOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant
|
||||
withPresence:(XMPPPresence *)presence
|
||||
room:(XMPPRoom *)room
|
||||
stream:(XMPPStream *)stream
|
||||
{
|
||||
// Delete from database
|
||||
|
||||
[[occupant managedObjectContext] deleteObject:occupant];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#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.
|
||||
|
||||
return [NSEntityDescription entityForName:[self messageEntityName] inManagedObjectContext:moc];
|
||||
}
|
||||
|
||||
- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc
|
||||
{
|
||||
// This method should be thread-safe.
|
||||
// So be sure to access the entity name through the property accessor.
|
||||
|
||||
return [NSEntityDescription entityForName:[self occupantEntityName] 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.bare, streamBareJidStr];
|
||||
}
|
||||
else
|
||||
{
|
||||
predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", roomJID.bare];
|
||||
}
|
||||
|
||||
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;
|
||||
XMPPRoomMessageCoreDataStorageObject *message = [[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;
|
||||
}
|
||||
|
||||
- (XMPPRoomOccupantCoreDataStorageObject *)occupantForJID:(XMPPJID *)jid
|
||||
stream:(XMPPStream *)xmppStream
|
||||
inContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
if (jid == nil) return nil;
|
||||
if (moc == nil) return nil;
|
||||
|
||||
NSEntityDescription *entity = [self occupantEntity:moc];
|
||||
|
||||
NSPredicate *predicate;
|
||||
if (xmppStream)
|
||||
{
|
||||
NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
|
||||
|
||||
NSString *predicateFormat = @"jidStr == %@ AND streamBareJidStr == %@";
|
||||
predicate = [NSPredicate predicateWithFormat:predicateFormat, jid, streamBareJidStr];
|
||||
}
|
||||
else
|
||||
{
|
||||
predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", jid];
|
||||
}
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setPredicate:predicate];
|
||||
[fetchRequest setFetchBatchSize:1];
|
||||
|
||||
NSError *error = nil;
|
||||
NSArray *results = [moc executeFetchRequest:fetchRequest error:&error];
|
||||
|
||||
if (error)
|
||||
{
|
||||
XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error);
|
||||
}
|
||||
|
||||
return (XMPPRoomOccupantCoreDataStorageObject *)[results lastObject];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPRoomStorage Protocol
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
|
||||
XMPPJID *presenceJID = [presence from];
|
||||
XMPPStream *xmppStream = room.xmppStream;
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
// Is occupant already in database?
|
||||
|
||||
XMPPRoomOccupantCoreDataStorageObject *occupant =
|
||||
[self occupantForJID:presenceJID stream:xmppStream inContext:moc];
|
||||
|
||||
// Is occupant available or unavailable?
|
||||
|
||||
if ([[presence type] isEqualToString:@"unavailable"])
|
||||
{
|
||||
// Remove occupant record from database
|
||||
|
||||
if (occupant)
|
||||
{
|
||||
[self removeOccupant:occupant withPresence:presence room:room stream:xmppStream];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Insert or update occupant in database
|
||||
|
||||
if (occupant)
|
||||
{
|
||||
[self updateOccupant:occupant withPresence:presence room:room stream:xmppStream];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self insertOccupantWithPresence:presence room:room stream:xmppStream];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (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;
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
[self clearAllOccupantsFromRoom:roomJID];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,47 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoom.h"
|
||||
|
||||
|
||||
@interface XMPPRoomMessageCoreDataStorageObject : NSManagedObject <XMPPRoomMessage>
|
||||
|
||||
/**
|
||||
* The properties below are documented in the XMPPRoomMessage protocol.
|
||||
**/
|
||||
|
||||
@property (nonatomic, retain) XMPPMessage * message; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, retain) NSString * messageStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, retain) XMPPJID * jid; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, retain) NSString * jidStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, retain) NSString * nickname;
|
||||
@property (nonatomic, retain) NSString * body;
|
||||
|
||||
@property (nonatomic, retain) NSDate * localTimestamp;
|
||||
@property (nonatomic, strong) NSDate * remoteTimestamp;
|
||||
|
||||
@property (nonatomic, assign) BOOL isFromMe;
|
||||
@property (nonatomic, strong) NSNumber * fromMe;
|
||||
|
||||
/**
|
||||
* The 'type' property can be used to inject event messages.
|
||||
* For example: "JohnDoe entered the room".
|
||||
*
|
||||
* You can define your own types to suit your needs.
|
||||
* All normal messages will have a type of zero.
|
||||
**/
|
||||
@property (nonatomic, strong) NSNumber * type;
|
||||
|
||||
/**
|
||||
* If a single instance of XMPPRoomCoreDataStorage is shared between multiple xmppStream's,
|
||||
* this may be needed to distinguish between the streams.
|
||||
**/
|
||||
@property (nonatomic, strong) NSString *streamBareJidStr;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,205 @@
|
||||
#import "XMPPRoomMessageCoreDataStorageObject.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
|
||||
@interface XMPPRoomMessageCoreDataStorageObject ()
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveRoomJID;
|
||||
@property(nonatomic,strong) NSString * primitiveRoomJIDStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveJid;
|
||||
@property(nonatomic,strong) NSString * primitiveJidStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPMessage * primitiveMessage;
|
||||
@property(nonatomic,strong) NSString * primitiveMessageStr;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRoomMessageCoreDataStorageObject
|
||||
|
||||
@dynamic message;
|
||||
@dynamic messageStr;
|
||||
|
||||
@dynamic roomJID, primitiveRoomJID;
|
||||
@dynamic roomJIDStr, primitiveRoomJIDStr;
|
||||
|
||||
@dynamic jid, primitiveJid;
|
||||
@dynamic jidStr, primitiveJidStr;
|
||||
|
||||
@dynamic nickname;
|
||||
@dynamic body;
|
||||
@dynamic localTimestamp;
|
||||
@dynamic remoteTimestamp;
|
||||
@dynamic isFromMe;
|
||||
@dynamic fromMe;
|
||||
|
||||
@dynamic type;
|
||||
|
||||
@dynamic streamBareJidStr;
|
||||
|
||||
@dynamic primitiveMessage;
|
||||
@dynamic primitiveMessageStr;
|
||||
|
||||
#pragma mark Transient roomJID
|
||||
|
||||
- (XMPPJID *)roomJID
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"roomJID"];
|
||||
XMPPJID *tmp = self.primitiveRoomJID;
|
||||
[self didAccessValueForKey:@"roomJID"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *roomJIDStr = self.roomJIDStr;
|
||||
if (roomJIDStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:roomJIDStr];
|
||||
self.primitiveRoomJID = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setRoomJID:(XMPPJID *)roomJID
|
||||
{
|
||||
[self willChangeValueForKey:@"roomJID"];
|
||||
[self willChangeValueForKey:@"roomJIDStr"];
|
||||
|
||||
self.primitiveRoomJID = roomJID;
|
||||
self.primitiveRoomJIDStr = [roomJID full];
|
||||
|
||||
[self didChangeValueForKey:@"roomJID"];
|
||||
[self didChangeValueForKey:@"roomJIDStr"];
|
||||
}
|
||||
|
||||
- (void)setRoomJIDStr:(NSString *)roomJIDStr
|
||||
{
|
||||
[self willChangeValueForKey:@"roomJID"];
|
||||
[self willChangeValueForKey:@"roomJIDStr"];
|
||||
|
||||
self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr];
|
||||
self.primitiveRoomJIDStr = roomJIDStr;
|
||||
|
||||
[self didChangeValueForKey:@"roomJID"];
|
||||
[self didChangeValueForKey:@"roomJIDStr"];
|
||||
}
|
||||
|
||||
#pragma mark Transient jid
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"jid"];
|
||||
XMPPJID *tmp = self.primitiveJid;
|
||||
[self didAccessValueForKey:@"jid"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *jidStr = self.jidStr;
|
||||
if (jidStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:jidStr];
|
||||
self.primitiveJid = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setJid:(XMPPJID *)jid
|
||||
{
|
||||
[self willChangeValueForKey:@"jid"];
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
|
||||
self.primitiveJid = jid;
|
||||
self.primitiveJidStr = [jid full];
|
||||
|
||||
[self didChangeValueForKey:@"jid"];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
}
|
||||
|
||||
- (void)setJidStr:(NSString *)jidStr
|
||||
{
|
||||
[self willChangeValueForKey:@"jid"];
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
|
||||
self.primitiveJid = [XMPPJID jidWithString:jidStr];
|
||||
self.primitiveJidStr = jidStr;
|
||||
|
||||
[self didChangeValueForKey:@"jid"];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
}
|
||||
|
||||
#pragma mark Scalar
|
||||
|
||||
- (BOOL)isFromMe
|
||||
{
|
||||
return [[self fromMe] boolValue];
|
||||
}
|
||||
|
||||
- (void)setIsFromMe:(BOOL)value
|
||||
{
|
||||
self.fromMe = @(value);
|
||||
}
|
||||
|
||||
#pragma mark - Message
|
||||
- (XMPPMessage *)message
|
||||
{
|
||||
// Create and cache on demand
|
||||
[self willAccessValueForKey:@"message"];
|
||||
|
||||
XMPPMessage *message = self.primitiveMessage;
|
||||
|
||||
[self didAccessValueForKey:@"message"];
|
||||
|
||||
if (message == nil)
|
||||
{
|
||||
NSString *messageStr = self.messageStr;
|
||||
|
||||
if (messageStr)
|
||||
{
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil];
|
||||
|
||||
message = [XMPPMessage messageFromElement:element];
|
||||
self.primitiveMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
- (void)setMessage:(XMPPMessage *)message
|
||||
{
|
||||
[self willChangeValueForKey:@"message"];
|
||||
[self willChangeValueForKey:@"messageStr"];
|
||||
self.primitiveMessage = message;
|
||||
self.primitiveMessageStr = [message compactXMLString];
|
||||
[self didChangeValueForKey:@"message"];
|
||||
[self didChangeValueForKey:@"messageStr"];
|
||||
}
|
||||
|
||||
- (void)setMessageStr:(NSString *)messageStr
|
||||
{
|
||||
[self willChangeValueForKey:@"message"];
|
||||
[self willChangeValueForKey:@"messageStr"];
|
||||
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil];
|
||||
|
||||
self.primitiveMessage = [XMPPMessage messageFromElement:element];
|
||||
self.primitiveMessageStr = messageStr;
|
||||
[self didChangeValueForKey:@"message"];
|
||||
[self didChangeValueForKey:@"messageStr"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,39 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoom.h"
|
||||
|
||||
|
||||
@interface XMPPRoomOccupantCoreDataStorageObject : NSManagedObject <XMPPRoomOccupant>
|
||||
|
||||
/**
|
||||
* The properties below are documented in the XMPPRoomOccupant protocol.
|
||||
**/
|
||||
|
||||
@property (nonatomic, strong) XMPPPresence * presence; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, strong) NSString * presenceStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, strong) XMPPJID * jid; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, strong) NSString * jidStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, strong) NSString * nickname;
|
||||
|
||||
@property (nonatomic, strong) NSString * role;
|
||||
@property (nonatomic, strong) NSString * affiliation;
|
||||
|
||||
@property (nonatomic, strong) XMPPJID * realJID; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, strong) NSString * realJIDStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, strong) NSDate * createdAt;
|
||||
|
||||
/**
|
||||
* If a single instance of XMPPRoomCoreDataStorage is shared between multiple xmppStream's,
|
||||
* this may be needed to distinguish between the streams.
|
||||
**/
|
||||
@property (nonatomic, strong) NSString * streamBareJidStr;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,234 @@
|
||||
#import "XMPPRoomOccupantCoreDataStorageObject.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
|
||||
@interface XMPPRoomOccupantCoreDataStorageObject ()
|
||||
|
||||
@property(nonatomic,strong) XMPPPresence * primitivePresence;
|
||||
@property(nonatomic,strong) NSString * primitivePresenceStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveRoomJID;
|
||||
@property(nonatomic,strong) NSString * primitiveRoomJIDStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveJid;
|
||||
@property(nonatomic,strong) NSString * primitiveJidStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveRealJID;
|
||||
@property(nonatomic,strong) NSString * primitiveRealJIDStr;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRoomOccupantCoreDataStorageObject
|
||||
|
||||
@dynamic presence, primitivePresence;
|
||||
@dynamic presenceStr, primitivePresenceStr;
|
||||
@dynamic roomJID, primitiveRoomJID;
|
||||
@dynamic roomJIDStr, primitiveRoomJIDStr;
|
||||
@dynamic jid, primitiveJid;
|
||||
@dynamic jidStr, primitiveJidStr;
|
||||
@dynamic nickname;
|
||||
@dynamic role;
|
||||
@dynamic affiliation;
|
||||
@dynamic realJID, primitiveRealJID;
|
||||
@dynamic realJIDStr, primitiveRealJIDStr;
|
||||
@dynamic createdAt;
|
||||
@dynamic streamBareJidStr;
|
||||
|
||||
#pragma mark Transient presence
|
||||
|
||||
- (XMPPPresence *)presence
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"presence"];
|
||||
XMPPPresence *presence = self.primitivePresence;
|
||||
[self didAccessValueForKey:@"presence"];
|
||||
|
||||
if (presence == nil)
|
||||
{
|
||||
NSString *presenceStr = self.presenceStr;
|
||||
if (presenceStr)
|
||||
{
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil];
|
||||
presence = [XMPPPresence presenceFromElement:element];
|
||||
self.primitivePresence = presence;
|
||||
}
|
||||
}
|
||||
|
||||
return presence;
|
||||
}
|
||||
|
||||
- (void)setPresence:(XMPPPresence *)newPresence
|
||||
{
|
||||
[self willChangeValueForKey:@"presence"];
|
||||
[self willChangeValueForKey:@"presenceStr"];
|
||||
|
||||
self.primitivePresence = newPresence;
|
||||
self.primitivePresenceStr = [newPresence compactXMLString];
|
||||
|
||||
[self didChangeValueForKey:@"presence"];
|
||||
[self didChangeValueForKey:@"presenceStr"];
|
||||
}
|
||||
|
||||
- (void)setPresenceStr:(NSString *)presenceStr
|
||||
{
|
||||
[self willChangeValueForKey:@"presence"];
|
||||
[self willChangeValueForKey:@"presenceStr"];
|
||||
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil];
|
||||
self.primitivePresence = [XMPPPresence presenceFromElement:element];
|
||||
self.primitivePresenceStr = presenceStr;
|
||||
|
||||
[self didChangeValueForKey:@"presence"];
|
||||
[self didChangeValueForKey:@"presenceStr"];
|
||||
}
|
||||
|
||||
#pragma mark Transient roomJID
|
||||
|
||||
- (XMPPJID *)roomJID
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"roomJID"];
|
||||
XMPPJID *tmp = self.primitiveRoomJID;
|
||||
[self didAccessValueForKey:@"roomJID"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *roomJIDStr = self.roomJIDStr;
|
||||
if (roomJIDStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:roomJIDStr];
|
||||
self.primitiveRoomJID = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setRoomJID:(XMPPJID *)roomJID
|
||||
{
|
||||
[self willChangeValueForKey:@"roomJID"];
|
||||
[self willChangeValueForKey:@"roomJIDStr"];
|
||||
|
||||
self.primitiveRoomJID = roomJID;
|
||||
self.primitiveRoomJIDStr = [roomJID full];
|
||||
|
||||
[self didChangeValueForKey:@"roomJID"];
|
||||
[self didChangeValueForKey:@"roomJIDStr"];
|
||||
}
|
||||
|
||||
- (void)setRoomJIDStr:(NSString *)roomJIDStr
|
||||
{
|
||||
[self willChangeValueForKey:@"roomJID"];
|
||||
[self willChangeValueForKey:@"roomJIDStr"];
|
||||
|
||||
self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr];
|
||||
self.primitiveRoomJIDStr = roomJIDStr;
|
||||
|
||||
[self didChangeValueForKey:@"roomJID"];
|
||||
[self didChangeValueForKey:@"roomJIDStr"];
|
||||
}
|
||||
|
||||
#pragma mark Transient jid
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"jid"];
|
||||
XMPPJID *tmp = self.primitiveJid;
|
||||
[self didAccessValueForKey:@"jid"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *jidStr = self.jidStr;
|
||||
if (jidStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:jidStr];
|
||||
self.primitiveJid = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setJid:(XMPPJID *)jid
|
||||
{
|
||||
[self willChangeValueForKey:@"jid"];
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
|
||||
self.primitiveJid = jid;
|
||||
self.primitiveJidStr = [jid full];
|
||||
|
||||
[self didChangeValueForKey:@"jid"];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
}
|
||||
|
||||
- (void)setJidStr:(NSString *)jidStr
|
||||
{
|
||||
[self willChangeValueForKey:@"jid"];
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
|
||||
self.primitiveJid = [XMPPJID jidWithString:jidStr];
|
||||
self.primitiveJidStr = jidStr;
|
||||
|
||||
[self didChangeValueForKey:@"jid"];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
}
|
||||
|
||||
#pragma mark Transient realJID
|
||||
|
||||
- (XMPPJID *)realJID
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"realJID"];
|
||||
XMPPJID *tmp = self.primitiveRealJID;
|
||||
[self didAccessValueForKey:@"realJID"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *realJIDStr = self.realJIDStr;
|
||||
if (realJIDStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:realJIDStr];
|
||||
self.primitiveRealJID = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setRealJID:(XMPPJID *)realJID
|
||||
{
|
||||
[self willChangeValueForKey:@"realJID"];
|
||||
[self willChangeValueForKey:@"realJIDStr"];
|
||||
|
||||
self.primitiveRealJID = realJID;
|
||||
self.primitiveRealJIDStr = [realJID full];
|
||||
|
||||
[self didChangeValueForKey:@"realJID"];
|
||||
[self didChangeValueForKey:@"realJIDStr"];
|
||||
}
|
||||
|
||||
- (void)setRealJIDStr:(NSString *)realJIDStr
|
||||
{
|
||||
[self willChangeValueForKey:@"realJID"];
|
||||
[self willChangeValueForKey:@"realJIDStr"];
|
||||
|
||||
self.primitiveRealJID = [XMPPJID jidWithString:realJIDStr];
|
||||
self.primitiveRealJIDStr = realJIDStr;
|
||||
|
||||
[self didChangeValueForKey:@"realJID"];
|
||||
[self didChangeValueForKey:@"realJIDStr"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>XMPPRoomHybrid.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="878" systemVersion="11C74" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="XMPPRoomMessageHybridCoreDataStorageObject" representedClassName="XMPPRoomMessageHybridCoreDataStorageObject" syncable="YES">
|
||||
<attribute name="body" optional="YES" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="fromMe" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
|
||||
<attribute name="jid" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="jidStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="localTimestamp" attributeType="Date" indexed="YES" syncable="YES"/>
|
||||
<attribute name="message" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="messageStr" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="nickname" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="remoteTimestamp" optional="YES" attributeType="Date" indexed="YES" syncable="YES"/>
|
||||
<attribute name="roomJID" optional="YES" transient="YES" syncable="YES"/>
|
||||
<attribute name="roomJIDStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="streamBareJidStr" attributeType="String" indexed="YES" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="XMPPRoomMessageHybridCoreDataStorageObject" positionX="160" positionY="192" width="128" height="240"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,197 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoom.h"
|
||||
#import "XMPPRoomMessageHybridCoreDataStorageObject.h"
|
||||
#import "XMPPRoomOccupantHybridMemoryStorageObject.h"
|
||||
#import "XMPPCoreDataStorage.h"
|
||||
|
||||
/**
|
||||
* This class is an example implementation of the XMPPRoomStorage protocol.
|
||||
* It stores messages in a database using core data, and stores occupants in memory (as they are temporary).
|
||||
*
|
||||
* You are free to substitute your own storage class.
|
||||
**/
|
||||
|
||||
@interface XMPPRoomHybridStorage : XMPPCoreDataStorage <XMPPRoomStorage>
|
||||
{
|
||||
@protected
|
||||
|
||||
/* Inherited from XMPPCoreDataStorage
|
||||
|
||||
NSString *databaseFileName;
|
||||
NSUInteger saveThreshold;
|
||||
|
||||
dispatch_queue_t storageQueue;
|
||||
|
||||
*/
|
||||
|
||||
// The occupantsGlobalDict holds all occupants in a heirarchy.
|
||||
// It is a dictionary of dictionaries of dictionaries.
|
||||
|
||||
NSMutableDictionary * occupantsGlobalDict; // Key=xmppStream.myJid, Value=occupantsRoomsDict
|
||||
// NSMutableDictionary * occupantsRoomsDict; // Key=xmppRoomJid, Value=occupantsRoomDict
|
||||
// NSMutableDictionary * occupantsRoomDict; // Key=occupantJid, Value=XMPPRoomOccupantHybridMemoryStorageObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to get an instance with the default database name.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* You are NOT required to use the sharedInstance.
|
||||
*
|
||||
* If your application makes extensive use of MUC, and you use a sharedInstance of this class,
|
||||
* then all of your MUC rooms share the same database store. You might get better performance if you create
|
||||
* multiple instances of this class instead (using different database filenames), as this way you can have
|
||||
* concurrent writes to multiple databases.
|
||||
**/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
/* Inherited from XMPPCoreDataStorage
|
||||
* Please see the XMPPCoreDataStorage header file for extensive documentation.
|
||||
|
||||
- (id)initWithDatabaseFilename:(NSString *)databaseFileName;
|
||||
- (id)initWithInMemoryStore;
|
||||
|
||||
@property (readonly) NSString *databaseFileName;
|
||||
|
||||
@property (readwrite) NSUInteger saveThreshold;
|
||||
|
||||
@property (readonly) NSManagedObjectModel *managedObjectModel;
|
||||
@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* You may choose to extend this class, and/or the message class for customized functionality.
|
||||
* These properties allow for such customization.
|
||||
*
|
||||
* You must set your desired entity name, if different from default, before you begin using the storage class.
|
||||
**/
|
||||
@property (strong, readwrite) NSString * messageEntityName;
|
||||
|
||||
/**
|
||||
* You can optionally extend the XMPPRoomOccupantMemoryStorageObject class.
|
||||
* Then just set the class here, and your subclass will automatically get used.
|
||||
*
|
||||
* You must set your desired class, if different from default, before you begin using the storage class.
|
||||
**/
|
||||
@property (assign, readwrite) Class occupantClass;
|
||||
|
||||
/**
|
||||
* It is likely you don't want the message history to persist forever.
|
||||
* Doing so would allow the database to grow infinitely large over time.
|
||||
*
|
||||
* The maxMessageAge property provides a way to specify how old a message can get
|
||||
* before it should get deleted from the database.
|
||||
*
|
||||
* The deleteInterval specifies how often to sweep for old messages.
|
||||
* Since deleting is an expensive operation (disk io) it is done on a fixed interval.
|
||||
*
|
||||
* You can optionally disable the maxMessageAge by setting it to zero (or a negative value).
|
||||
* If you disable the maxMessageAge then old messages are not deleted.
|
||||
*
|
||||
* You can optionally disable the deleteInterval by setting it to zero (or a negative value).
|
||||
*
|
||||
* The default maxAge is 7 days.
|
||||
* The default deleteInterval is 5 minutes.
|
||||
**/
|
||||
@property (assign, readwrite) NSTimeInterval maxMessageAge;
|
||||
@property (assign, readwrite) NSTimeInterval deleteInterval;
|
||||
|
||||
/**
|
||||
* You may optionally prevent old message deletion for particular rooms.
|
||||
**/
|
||||
- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID;
|
||||
- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID;
|
||||
|
||||
/**
|
||||
* Convenience method to get the message entity description.
|
||||
*
|
||||
* @see messageEntityName
|
||||
**/
|
||||
- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc;
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the most recent message stored in the database for the given room.
|
||||
* This may be used when requesting the message history from the server,
|
||||
* to prevent redownloading messages you already have.
|
||||
*
|
||||
* @param roomJID - The JID of the room (a bare JID)
|
||||
*
|
||||
* @param xmppStream - This class can support multiple concurrent xmppStreams.
|
||||
* Optionally pass the xmppStream the room applies to.
|
||||
* If you're using this claass with a single xmppStream, you can pass nil.
|
||||
*
|
||||
* @param moc - The managedObjectContext to use when doing the lookups.
|
||||
* If non-nil, this should match the thread you're currently using.
|
||||
* If nil, the operation is dispatch_sync'd onto the internal queue,
|
||||
* and uses the internal managedObjectContext.
|
||||
*
|
||||
* The moc may optionally be nil strictly because this method does not return a NSManagedObject.
|
||||
**/
|
||||
- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID
|
||||
stream:(XMPPStream *)xmppStream
|
||||
inContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
/**
|
||||
* Returns the occupant for the given full jid.
|
||||
*
|
||||
* @param jid - The full jid of the room occupant (including resource).
|
||||
* E.g. xmppDevelopers@conf.xmpp.org/robbiehanson
|
||||
*
|
||||
* @param xmppStream - This class can support multiple concurrent xmppStreams.
|
||||
* Optionally pass the xmppStream the room applies to.
|
||||
* If you're using this claass with a single xmppStream, you can pass nil.
|
||||
**/
|
||||
- (XMPPRoomOccupantHybridMemoryStorageObject *)occupantForJID:(XMPPJID *)jid stream:(XMPPStream *)xmppStream;
|
||||
|
||||
/**
|
||||
* Returns all occupants in the given room.
|
||||
* Each occupant instance will be of kind XMPPRoomOccupantHybridMemoryStorageObject.
|
||||
*
|
||||
* @param roomJid - The JID of the room (a bare JID).
|
||||
* E.g. xmppDevelopers@conf.xmpp.org
|
||||
*
|
||||
* @param xmppStream - This class can support multiple concurrent xmppStreams.
|
||||
* Optionally pass the xmppStream the room applies to.
|
||||
* If you're using this claass with a single xmppStream, you can pass nil.
|
||||
**/
|
||||
- (NSArray *)occupantsForRoom:(XMPPJID *)roomJid stream:(XMPPStream *)xmppStream;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRoomHybridStorageDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
//
|
||||
// XMPPRoomHybridStorage automatically uses the delegate(s) of its parent XMPPRoom.
|
||||
//
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:occupantDidJoin:withPresence: method.
|
||||
* This method provides the delegate with the occupant storage instance.
|
||||
**/
|
||||
- (void)xmppRoomHybridStorage:(XMPPRoomHybridStorage *)sender
|
||||
occupantDidJoin:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant;
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:occupantDidLeave:withPresence: method.
|
||||
* This method provides the delegate with the occupant storage instance.
|
||||
**/
|
||||
- (void)xmppRoomHybridStorage:(XMPPRoomHybridStorage *)sender
|
||||
occupantDidLeave:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant;
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:occupantDidUpdate:withPresence: method.
|
||||
* This method provides the delegate with the occupant storage instance.
|
||||
**/
|
||||
- (void)xmppRoomHybridStorage:(XMPPRoomHybridStorage *)sender
|
||||
occupantDidUpdate:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,990 @@
|
||||
#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 <XMPPRoomHybridStorageDelegate> *roomMulticastDelegate =
|
||||
(GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *)[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 <XMPPRoomHybridStorageDelegate> *roomMulticastDelegate =
|
||||
(GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *)[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 <XMPPRoomHybridStorageDelegate> *roomMulticastDelegate =
|
||||
(GCDMulticastDelegate <XMPPRoomHybridStorageDelegate> *)[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
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* The XMPPRoomHybridStorage class is designed to be easily extensible.
|
||||
* The class has several protected methods that act as hooks,
|
||||
* allowing you to override various methods to customize the functionality how you see fit.
|
||||
*
|
||||
* This header file lists the protected methods,
|
||||
* and you may need to import it in your subclass if you ever need to invoke these methods directly.
|
||||
*
|
||||
* E.g. [super insertOccupant...]
|
||||
**/
|
||||
|
||||
@interface XMPPRoomHybridStorage (Protected)
|
||||
|
||||
/**
|
||||
* Returns whether or not the given message already exists in storage.
|
||||
* If YES, then the message is ignored. Otherwise it is passed to the insert routines.
|
||||
**/
|
||||
- (BOOL)existsMessage:(XMPPMessage *)message forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
- (void)didInsertMessage:(XMPPRoomMessageHybridCoreDataStorageObject *)message;
|
||||
|
||||
/**
|
||||
* Optional override hook for complete customization.
|
||||
* Override me if you need to do specific custom work when inserting a message in a room.
|
||||
**/
|
||||
- (void)insertMessage:(XMPPMessage *)message
|
||||
outgoing:(BOOL)isOutgoing
|
||||
forRoom:(XMPPRoom *)room
|
||||
stream:(XMPPStream *)xmppStream;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
**/
|
||||
- (void)didInsertOccupant:(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.
|
||||
**/
|
||||
- (void)didUpdateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant;
|
||||
|
||||
/**
|
||||
* Override me if you have any custom work to do before an occupant leaves (is removed from storage).
|
||||
**/
|
||||
- (void)willRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant;
|
||||
|
||||
/**
|
||||
* Override me if you have any custom work to do after an occupant leaves (is removed from storage).
|
||||
**/
|
||||
- (void)didRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,47 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoom.h"
|
||||
|
||||
|
||||
@interface XMPPRoomMessageHybridCoreDataStorageObject : NSManagedObject <XMPPRoomMessage>
|
||||
|
||||
/**
|
||||
* The properties below are documented in the XMPPRoomMessage protocol.
|
||||
**/
|
||||
|
||||
@property (nonatomic, retain) XMPPMessage * message; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, retain) NSString * messageStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, retain) XMPPJID * jid; // Transient (proper type, not on disk)
|
||||
@property (nonatomic, retain) NSString * jidStr; // Shadow (binary data, written to disk)
|
||||
|
||||
@property (nonatomic, retain) NSString * nickname;
|
||||
@property (nonatomic, retain) NSString * body;
|
||||
|
||||
@property (nonatomic, retain) NSDate * localTimestamp;
|
||||
@property (nonatomic, strong) NSDate * remoteTimestamp;
|
||||
|
||||
@property (nonatomic, assign) BOOL isFromMe;
|
||||
@property (nonatomic, strong) NSNumber * fromMe;
|
||||
|
||||
/**
|
||||
* The 'type' property can be used to inject event messages.
|
||||
* For example: "JohnDoe entered the room".
|
||||
*
|
||||
* You can define your own types to suit your needs.
|
||||
* All normal messages will have a type of zero.
|
||||
**/
|
||||
@property (nonatomic, strong) NSNumber * type;
|
||||
|
||||
/**
|
||||
* If a single instance of XMPPRoomHybridStorage is shared between multiple xmppStream's,
|
||||
* this may be needed to distinguish between the streams.
|
||||
**/
|
||||
@property (nonatomic, strong) NSString *streamBareJidStr;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,206 @@
|
||||
#import "XMPPRoomMessageHybridCoreDataStorageObject.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
|
||||
@interface XMPPRoomMessageHybridCoreDataStorageObject ()
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveRoomJID;
|
||||
@property(nonatomic,strong) NSString * primitiveRoomJIDStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPJID * primitiveJid;
|
||||
@property(nonatomic,strong) NSString * primitiveJidStr;
|
||||
|
||||
@property(nonatomic,strong) XMPPMessage * primitiveMessage;
|
||||
@property(nonatomic,strong) NSString * primitiveMessageStr;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRoomMessageHybridCoreDataStorageObject
|
||||
|
||||
@dynamic message;
|
||||
@dynamic messageStr;
|
||||
|
||||
@dynamic roomJID, primitiveRoomJID;
|
||||
@dynamic roomJIDStr, primitiveRoomJIDStr;
|
||||
|
||||
@dynamic jid, primitiveJid;
|
||||
@dynamic jidStr, primitiveJidStr;
|
||||
|
||||
@dynamic nickname;
|
||||
@dynamic body;
|
||||
@dynamic localTimestamp;
|
||||
@dynamic remoteTimestamp;
|
||||
@dynamic isFromMe;
|
||||
@dynamic fromMe;
|
||||
|
||||
@dynamic type;
|
||||
|
||||
@dynamic streamBareJidStr;
|
||||
|
||||
@dynamic primitiveMessage;
|
||||
@dynamic primitiveMessageStr;
|
||||
|
||||
#pragma mark Transient roomJID
|
||||
|
||||
- (XMPPJID *)roomJID
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"roomJID"];
|
||||
XMPPJID *tmp = self.primitiveRoomJID;
|
||||
[self didAccessValueForKey:@"roomJID"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *roomJIDStr = self.roomJIDStr;
|
||||
if (roomJIDStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:roomJIDStr];
|
||||
self.primitiveRoomJID = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setRoomJID:(XMPPJID *)roomJID
|
||||
{
|
||||
[self willChangeValueForKey:@"roomJID"];
|
||||
[self willChangeValueForKey:@"roomJIDStr"];
|
||||
|
||||
self.primitiveRoomJID = roomJID;
|
||||
self.primitiveRoomJIDStr = [roomJID full];
|
||||
|
||||
[self didChangeValueForKey:@"roomJID"];
|
||||
[self didChangeValueForKey:@"roomJIDStr"];
|
||||
}
|
||||
|
||||
- (void)setRoomJIDStr:(NSString *)roomJIDStr
|
||||
{
|
||||
[self willChangeValueForKey:@"roomJID"];
|
||||
[self willChangeValueForKey:@"roomJIDStr"];
|
||||
|
||||
self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr];
|
||||
self.primitiveRoomJIDStr = roomJIDStr;
|
||||
|
||||
[self didChangeValueForKey:@"roomJID"];
|
||||
[self didChangeValueForKey:@"roomJIDStr"];
|
||||
}
|
||||
|
||||
#pragma mark Transient jid
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
// Create and cache on demand
|
||||
|
||||
[self willAccessValueForKey:@"jid"];
|
||||
XMPPJID *tmp = self.primitiveJid;
|
||||
[self didAccessValueForKey:@"jid"];
|
||||
|
||||
if (tmp == nil)
|
||||
{
|
||||
NSString *jidStr = self.jidStr;
|
||||
if (jidStr)
|
||||
{
|
||||
tmp = [XMPPJID jidWithString:jidStr];
|
||||
self.primitiveJid = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setJid:(XMPPJID *)jid
|
||||
{
|
||||
[self willChangeValueForKey:@"jid"];
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
|
||||
self.primitiveJid = jid;
|
||||
self.primitiveJidStr = [jid full];
|
||||
|
||||
[self didChangeValueForKey:@"jid"];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
}
|
||||
|
||||
- (void)setJidStr:(NSString *)jidStr
|
||||
{
|
||||
[self willChangeValueForKey:@"jid"];
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
|
||||
self.primitiveJid = [XMPPJID jidWithString:jidStr];
|
||||
self.primitiveJidStr = jidStr;
|
||||
|
||||
[self didChangeValueForKey:@"jid"];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
}
|
||||
|
||||
#pragma mark Scalar
|
||||
|
||||
- (BOOL)isFromMe
|
||||
{
|
||||
return [[self fromMe] boolValue];
|
||||
}
|
||||
|
||||
- (void)setIsFromMe:(BOOL)value
|
||||
{
|
||||
self.fromMe = @(value);
|
||||
}
|
||||
|
||||
#pragma mark - Message
|
||||
- (XMPPMessage *)message
|
||||
{
|
||||
// Create and cache on demand
|
||||
[self willAccessValueForKey:@"message"];
|
||||
|
||||
XMPPMessage *message = self.primitiveMessage;
|
||||
|
||||
[self didAccessValueForKey:@"message"];
|
||||
|
||||
if (message == nil)
|
||||
{
|
||||
NSString *messageStr = self.messageStr;
|
||||
if (messageStr)
|
||||
{
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:
|
||||
messageStr error:nil];
|
||||
message = [XMPPMessage messageFromElement:element];
|
||||
self.primitiveMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
- (void)setMessage:(XMPPMessage *)message
|
||||
{
|
||||
[self willChangeValueForKey:@"message"];
|
||||
[self willChangeValueForKey:@"messageStr"];
|
||||
|
||||
self.primitiveMessage = message;
|
||||
self.primitiveMessageStr = [message compactXMLString];
|
||||
|
||||
[self didChangeValueForKey:@"message"];
|
||||
[self didChangeValueForKey:@"messageStr"];
|
||||
}
|
||||
|
||||
- (void)setMessageStr:(NSString *)messageStr
|
||||
{
|
||||
[self willChangeValueForKey:@"message"];
|
||||
[self willChangeValueForKey:@"messageStr"];
|
||||
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil];
|
||||
self.primitiveMessage = [XMPPMessage messageFromElement:element];
|
||||
self.primitiveMessageStr = messageStr;
|
||||
|
||||
[self didChangeValueForKey:@"message"];
|
||||
[self didChangeValueForKey:@"messageStr"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,41 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPRoom.h"
|
||||
#import "XMPPRoomOccupant.h"
|
||||
|
||||
|
||||
@interface XMPPRoomOccupantHybridMemoryStorageObject : NSObject <XMPPRoomOccupant, NSCopying, NSSecureCoding>
|
||||
|
||||
- (id)initWithPresence:(XMPPPresence *)presence streamFullJid:(XMPPJID *)streamFullJid;
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence;
|
||||
|
||||
/**
|
||||
* The properties below are documented in the XMPPRoomOccupant protocol.
|
||||
**/
|
||||
|
||||
@property (readonly) XMPPPresence *presence;
|
||||
|
||||
@property (readonly) XMPPJID * jid;
|
||||
@property (readonly) XMPPJID * roomJID;
|
||||
@property (readonly) NSString * nickname;
|
||||
|
||||
@property (readonly) NSString * role;
|
||||
@property (readonly) NSString * affiliation;
|
||||
@property (readonly) XMPPJID * realJID;
|
||||
|
||||
@property (readonly) NSDate * createdAt;
|
||||
|
||||
/**
|
||||
* Since XMPPRoomHybridStorage supports multiple xmppStreams,
|
||||
* this property may be used to differentiate between occupant objects.
|
||||
**/
|
||||
@property (readonly) XMPPJID * streamFullJid;
|
||||
|
||||
/**
|
||||
* Sample comparison methods.
|
||||
**/
|
||||
|
||||
- (NSComparisonResult)compareByNickname:(XMPPRoomOccupantHybridMemoryStorageObject *)another;
|
||||
|
||||
- (NSComparisonResult)compareByCreatedAt:(XMPPRoomOccupantHybridMemoryStorageObject *)another;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,237 @@
|
||||
#import "XMPPRoomOccupantHybridMemoryStorageObject.h"
|
||||
|
||||
|
||||
@implementation XMPPRoomOccupantHybridMemoryStorageObject
|
||||
{
|
||||
XMPPPresence *presence;
|
||||
XMPPJID *jid;
|
||||
NSDate *createdAt;
|
||||
XMPPJID *streamFullJid;
|
||||
}
|
||||
|
||||
- (id)initWithPresence:(XMPPPresence *)inPresence streamFullJid:(XMPPJID *)inStreamFullJid
|
||||
{
|
||||
NSParameterAssert(inPresence != nil);
|
||||
NSParameterAssert(inStreamFullJid != nil);
|
||||
|
||||
if ((self = [super init]))
|
||||
{
|
||||
presence = inPresence;
|
||||
jid = [presence from];
|
||||
createdAt = [[NSDate alloc] init];
|
||||
streamFullJid = inStreamFullJid;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Encoding, Decoding
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder
|
||||
{
|
||||
if ([encoder isBycopy])
|
||||
return self;
|
||||
else
|
||||
return [super replacementObjectForPortCoder:encoder];
|
||||
// return [NSDistantObject proxyWithLocal:self connection:[encoder connection]];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
if([coder respondsToSelector:@selector(requiresSecureCoding)] &&
|
||||
[coder requiresSecureCoding])
|
||||
{
|
||||
presence = [coder decodeObjectOfClass:[XMPPPresence class] forKey:@"presence"];
|
||||
jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"];
|
||||
createdAt = [coder decodeObjectOfClass:[NSDate class] forKey:@"createdAt"];
|
||||
streamFullJid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"streamFullJid"];
|
||||
}
|
||||
else
|
||||
{
|
||||
presence = [coder decodeObjectForKey:@"presence"];
|
||||
jid = [coder decodeObjectForKey:@"jid"];
|
||||
createdAt = [coder decodeObjectForKey:@"createdAt"];
|
||||
streamFullJid = [coder decodeObjectForKey:@"streamFullJid"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
presence = [coder decodeObject];
|
||||
jid = [coder decodeObject];
|
||||
createdAt = [coder decodeObject];
|
||||
streamFullJid = [coder decodeObject];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
[coder encodeObject:presence forKey:@"presence"];
|
||||
[coder encodeObject:jid forKey:@"jid"];
|
||||
[coder encodeObject:createdAt forKey:@"createdAt"];
|
||||
[coder encodeObject:streamFullJid forKey:@"streamFullJid"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[coder encodeObject:presence];
|
||||
[coder encodeObject:jid];
|
||||
[coder encodeObject:createdAt];
|
||||
[coder encodeObject:streamFullJid];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Copying
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
// We use [self class] to support subclassing
|
||||
|
||||
XMPPRoomOccupantHybridMemoryStorageObject *deepCopy;
|
||||
deepCopy = (XMPPRoomOccupantHybridMemoryStorageObject *)[[[self class] alloc] init];
|
||||
|
||||
deepCopy->presence = [presence copy];
|
||||
deepCopy->jid = [jid copy];
|
||||
deepCopy->createdAt = [createdAt copy];
|
||||
deepCopy->streamFullJid = [streamFullJid copy];
|
||||
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Updates
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)inPresence
|
||||
{
|
||||
presence = inPresence;
|
||||
jid = [inPresence from];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Properties
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPPresence *)presence
|
||||
{
|
||||
return presence;
|
||||
}
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
return jid;
|
||||
}
|
||||
|
||||
- (XMPPJID *)roomJID
|
||||
{
|
||||
return [jid bareJID];
|
||||
}
|
||||
|
||||
- (NSString *)nickname
|
||||
{
|
||||
return [jid resource];
|
||||
}
|
||||
|
||||
- (NSString *)itemAttributeStringValueForName:(NSString *)attrName
|
||||
{
|
||||
NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"];
|
||||
NSXMLElement *item = [x elementForName:@"item"];
|
||||
if (item)
|
||||
{
|
||||
NSString *result = [item attributeStringValueForName:attrName];
|
||||
if (result)
|
||||
{
|
||||
return [result lowercaseString];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)role
|
||||
{
|
||||
return [self itemAttributeStringValueForName:@"role"];
|
||||
}
|
||||
|
||||
- (NSString *)affiliation
|
||||
{
|
||||
return [self itemAttributeStringValueForName:@"affiliation"];
|
||||
}
|
||||
|
||||
- (XMPPJID *)realJID
|
||||
{
|
||||
NSString *jidStr = [self itemAttributeStringValueForName:@"jid"];
|
||||
if (jidStr)
|
||||
return [XMPPJID jidWithString:jidStr];
|
||||
else
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSDate *)createdAt
|
||||
{
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
- (XMPPJID *)streamFullJid
|
||||
{
|
||||
return streamFullJid;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Compare
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSComparisonResult)compareByNickname:(XMPPRoomOccupantHybridMemoryStorageObject *)another
|
||||
{
|
||||
return [self.nickname compare:another.nickname];
|
||||
}
|
||||
|
||||
- (NSComparisonResult)compareByCreatedAt:(XMPPRoomOccupantHybridMemoryStorageObject *)another
|
||||
{
|
||||
return [self.createdAt compare:another.createdAt];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark NSObject Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [jid hash];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
if ([anObject isMemberOfClass:[self class]])
|
||||
{
|
||||
XMPPRoomOccupantHybridMemoryStorageObject *another = (XMPPRoomOccupantHybridMemoryStorageObject *)anObject;
|
||||
|
||||
if ([jid isEqualToJID:[another jid]])
|
||||
{
|
||||
if ([streamFullJid isEqualToJID:[another streamFullJid]])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,130 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPRoom.h"
|
||||
#import "XMPPRoomOccupant.h"
|
||||
#import "XMPPRoomMessageMemoryStorageObject.h"
|
||||
#import "XMPPRoomOccupantMemoryStorageObject.h"
|
||||
|
||||
|
||||
|
||||
@interface XMPPRoomMemoryStorage : NSObject <XMPPRoomStorage>
|
||||
|
||||
- (id)init;
|
||||
|
||||
@property (readonly) XMPPRoom *parent;
|
||||
|
||||
/**
|
||||
* You can optionally extend the XMPPRoomMessageMemoryStorageObject and XMPPRoomOccupantMemoryStorageObject classes.
|
||||
* Then just set the classes here, and your subclasses will automatically get used.
|
||||
*
|
||||
* You must set your desired class(es), if different from default, before you begin using the storage class.
|
||||
**/
|
||||
@property (assign, readwrite) Class messageClass;
|
||||
@property (assign, readwrite) Class occupantClass;
|
||||
|
||||
/**
|
||||
* Returns the occupant with the given full JID.
|
||||
**/
|
||||
- (XMPPRoomOccupantMemoryStorageObject *)occupantForJID:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* Returns the messages in sorted order.
|
||||
* The messages are sorted via the [messageClass compare:] method, which may optionally be overriden in subclasses.
|
||||
**/
|
||||
- (NSArray *)messages;
|
||||
|
||||
/**
|
||||
* Returns the occupants in sorted order.
|
||||
* The occupants are sorted via the [occupantsClass compare:] method, which may optionally be overriden in subclasses.
|
||||
**/
|
||||
- (NSArray *)occupants;
|
||||
|
||||
/**
|
||||
* This method is designed for subclasses of XMPPRoomMessageMemoryStorageObject
|
||||
* which may dynamically change the operation of the overriden compare: method.
|
||||
*
|
||||
* If changed, this method should be invoked to force a resort.
|
||||
**/
|
||||
- (NSArray *)resortMessages;
|
||||
|
||||
/**
|
||||
* This method is designed for subclasses of XMPPRoomOccupantMemoryStorageObject
|
||||
* which may dynamically change the operation of the overriden compare: method.
|
||||
*
|
||||
* If changed, this method should be invoked to force a resort.
|
||||
**/
|
||||
- (NSArray *)resortOccupants;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRoomMemoryStorageDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
//
|
||||
// XMPPRoomMemoryStorage automatically uses the delegate(s) of its parent XMPPRoom.
|
||||
//
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:occupantDidJoin:withPresence: method.
|
||||
*
|
||||
* This method provides the delegate with the occupant storage instance,
|
||||
* as well as the current snapshot of the occupants array,
|
||||
* and the index at which the new occupant was inserted.
|
||||
*
|
||||
* The given occupant will be included in the given array at the given index.
|
||||
**/
|
||||
- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender
|
||||
occupantDidJoin:(XMPPRoomOccupantMemoryStorageObject *)occupant
|
||||
atIndex:(NSUInteger)index
|
||||
inArray:(NSArray *)allOccupants;
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:occupantDidLeave:withPresence: method.
|
||||
*
|
||||
* This method provides the delegate with the occupant storage instance,
|
||||
* as well as the current snapshot of the occupants array,
|
||||
* and the index at which the occupant used to reside.
|
||||
*
|
||||
* The given occupant will not be included in the given array.
|
||||
* It's previous location is given by the index.
|
||||
**/
|
||||
- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender
|
||||
occupantDidLeave:(XMPPRoomOccupantMemoryStorageObject *)occupant
|
||||
atIndex:(NSUInteger)index
|
||||
fromArray:(NSArray *)allOccupants;
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:occupantDidUpdate:withPresence: method.
|
||||
*
|
||||
* This method provides the delegate with the occupant storage instance,
|
||||
* as well as the current snapshot of the occupants array,
|
||||
* including the old and new index of the occupant.
|
||||
*
|
||||
* The given occupant will be included in the given array at the given newIndex.
|
||||
* If the location of the occupant didn't change, then the oldIndex and newIndex will be the same.
|
||||
**/
|
||||
- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender
|
||||
occupantDidUpdate:(XMPPRoomOccupantMemoryStorageObject *)occupant
|
||||
fromIndex:(NSUInteger)oldIndex
|
||||
toIndex:(NSUInteger)newIndex
|
||||
inArray:(NSArray *)allOccupants;
|
||||
|
||||
/**
|
||||
* Similar to XMPPRoomDelegate's xmppRoom:didReceiveMessage:fromOccupant: method.
|
||||
*
|
||||
* This method provides the delegate with the occupant and message storage instance,
|
||||
* as well as the current snapshot of the messages array,
|
||||
* including the new index of the message.
|
||||
*
|
||||
* The given message will be included in the given array at the given index.
|
||||
**/
|
||||
- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender
|
||||
didReceiveMessage:(XMPPRoomMessageMemoryStorageObject *)message
|
||||
fromOccupant:(XMPPRoomOccupantMemoryStorageObject *)occupant
|
||||
atIndex:(NSUInteger)index
|
||||
inArray:(NSArray *)allMessages;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,707 @@
|
||||
#import "XMPPRoomMemoryStorage.h"
|
||||
#import "XMPPRoomPrivate.h"
|
||||
#import "XMPP.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; // | XMPP_LOG_FLAG_TRACE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
#define AssertPrivateQueue() \
|
||||
NSAssert(dispatch_get_specific(parentQueueTag), @"Private method: MUST run on parentQueue");
|
||||
|
||||
#define AssertParentQueue() \
|
||||
NSAssert(dispatch_get_specific(parentQueueTag), @"Private protocol method: MUST run on parentQueue");
|
||||
|
||||
@interface XMPPRoomMemoryStorage ()
|
||||
{
|
||||
#if __has_feature(objc_arc_weak)
|
||||
__weak XMPPRoom *parent;
|
||||
#else
|
||||
__unsafe_unretained XMPPRoom *parent;
|
||||
#endif
|
||||
dispatch_queue_t parentQueue;
|
||||
void *parentQueueTag;
|
||||
|
||||
NSMutableArray * messages;
|
||||
NSMutableArray * occupantsArray;
|
||||
NSMutableDictionary * occupantsDict;
|
||||
|
||||
Class messageClass;
|
||||
Class occupantClass;
|
||||
}
|
||||
|
||||
@property (readonly) dispatch_queue_t parentQueue;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRoomMemoryStorage
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
messages = [[NSMutableArray alloc] init];
|
||||
occupantsArray = [[NSMutableArray alloc] init];
|
||||
occupantsDict = [[NSMutableDictionary alloc] init];
|
||||
|
||||
messageClass = [XMPPRoomMessageMemoryStorageObject class];
|
||||
occupantClass = [XMPPRoomOccupantMemoryStorageObject class];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)configureWithParent:(XMPPRoom *)aParent queue:(dispatch_queue_t)queue
|
||||
{
|
||||
NSParameterAssert(aParent != nil);
|
||||
NSParameterAssert(queue != NULL);
|
||||
|
||||
BOOL result = NO;
|
||||
|
||||
@synchronized(self)
|
||||
{
|
||||
if ((parent == nil) && (parentQueue == NULL))
|
||||
{
|
||||
parent = aParent;
|
||||
parentQueue = queue;
|
||||
parentQueueTag = &parentQueueTag;
|
||||
dispatch_queue_set_specific(parentQueue, parentQueueTag, parentQueueTag, NULL);
|
||||
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_retain(parentQueue);
|
||||
#endif
|
||||
|
||||
result = YES;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (GCDMulticastDelegate <XMPPRoomMemoryStorageDelegate> *)multicastDelegate
|
||||
{
|
||||
return (GCDMulticastDelegate <XMPPRoomMemoryStorageDelegate> *)[parent multicastDelegate];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
if (parentQueue)
|
||||
dispatch_release(parentQueue);
|
||||
#endif
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Properties
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@synthesize messageClass;
|
||||
@synthesize occupantClass;
|
||||
|
||||
- (XMPPRoom *)parent
|
||||
{
|
||||
XMPPRoom *result = nil;
|
||||
|
||||
@synchronized(self) // synchronized with configureWithParent:queue:
|
||||
{
|
||||
result = parent;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (dispatch_queue_t)parentQueue
|
||||
{
|
||||
dispatch_queue_t result = NULL;
|
||||
|
||||
@synchronized(self) // synchronized with configureWithParent:queue:
|
||||
{
|
||||
result = parentQueue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Internal API
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)existsMessage:(XMPPMessage *)message
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if ([messages count] == 0)
|
||||
{
|
||||
// Safety net for binary search algorithm used below
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
// Does this message already exist in the messages array?
|
||||
// How can we tell if two XMPPRoomMessages are the same?
|
||||
//
|
||||
// 1. Same jid
|
||||
// 2. Same text
|
||||
// 3. Same remoteTimestamp
|
||||
// 4. Approximately the same localTimestamps (if existing message doesn't have set remoteTimestamp)
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Algorithm overview:
|
||||
//
|
||||
// Since the clock of the client and server may be out of sync,
|
||||
// a localTimestamp and remoteTimestamp may be off by several seconds.
|
||||
// So we're going to search a range of messages, bounded by a min and max localTimestamp.
|
||||
//
|
||||
// We find the first message that has a localTimestamp >= minLocalTimestamp.
|
||||
// We then search from there to the first message that has a localTimestamp > maxLocalTimestamp.
|
||||
//
|
||||
// This represents our range of messages to search.
|
||||
// Then we can simply iterate over these messages to see if any have the same jid and text.
|
||||
|
||||
NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60];
|
||||
NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60];
|
||||
|
||||
// Use binary search to locate first message with localTimestamp >= minLocalTimestamp.
|
||||
|
||||
NSInteger mid;
|
||||
NSInteger min = 0;
|
||||
NSInteger max = [messages count] - 1;
|
||||
|
||||
while (YES)
|
||||
{
|
||||
mid = (min + max) / 2;
|
||||
XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid];
|
||||
|
||||
NSComparisonResult cmp = [minLocalTimestamp compare:[currentMessage localTimestamp]];
|
||||
if (cmp == NSOrderedAscending)
|
||||
{
|
||||
// minLocalTimestamp < currentMessage.localTimestamp
|
||||
|
||||
if (mid == min)
|
||||
break;
|
||||
else
|
||||
max = mid - 1;
|
||||
}
|
||||
else // Descending || Same
|
||||
{
|
||||
// minLocalTimestamp >= currentMessage.localTimestamp
|
||||
|
||||
if (mid == max) {
|
||||
mid++;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
min = mid + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The 'mid' variable now points to the index of the first message in the sorted messages array
|
||||
// that has a localTimestamp >= minLocalTimestamp.
|
||||
//
|
||||
// We're going to start looking for matching messages here,
|
||||
// and break if we find a message with localTimestamp <= maxLocalTimestamp.
|
||||
|
||||
XMPPJID *messageJid = [message from];
|
||||
NSString *messageBody = [[message elementForName:@"body"] stringValue];
|
||||
|
||||
NSInteger index;
|
||||
for (index = mid; index < [messages count]; index++)
|
||||
{
|
||||
XMPPRoomMessageMemoryStorageObject *currentMessage = messages[index];
|
||||
|
||||
NSComparisonResult cmp = [maxLocalTimestamp compare:[currentMessage localTimestamp]];
|
||||
if (cmp != NSOrderedAscending)
|
||||
{
|
||||
// maxLocalTimestamp >= currentMessage.localTimestamp
|
||||
break;
|
||||
}
|
||||
|
||||
if ([currentMessage.jid isEqualToJID:messageJid])
|
||||
{
|
||||
if ([currentMessage.body isEqualToString:messageBody])
|
||||
{
|
||||
if (currentMessage.remoteTimestamp)
|
||||
{
|
||||
if ([currentMessage.remoteTimestamp isEqualToDate:remoteTimestamp])
|
||||
{
|
||||
// 1. jid matches
|
||||
// 2. body matches
|
||||
// 3. remoteTimestamp matches
|
||||
//
|
||||
// => Incoming message already exists in the array.
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 1. jid matches
|
||||
// 2. body matches
|
||||
// 3. existing message in array doesn't have set remoteTimestamp
|
||||
// 4. existing message has approximately the same localTimestamp
|
||||
//
|
||||
// => Incoming message already exists in the array.
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSUInteger)insertMessage:(XMPPRoomMessageMemoryStorageObject *)message
|
||||
{
|
||||
NSUInteger count = [messages count];
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
[messages addObject:message];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Shortcut - Most (if not all) messages are inserted at the end
|
||||
|
||||
XMPPRoomMessageMemoryStorageObject *lastMessage = messages[count - 1];
|
||||
if ([message compare:lastMessage] != NSOrderedAscending)
|
||||
{
|
||||
[messages addObject:message];
|
||||
return count;
|
||||
}
|
||||
|
||||
// Shortcut didn't work.
|
||||
// Find location using binary search algorithm.
|
||||
|
||||
NSInteger mid;
|
||||
NSInteger min = 0;
|
||||
NSInteger max = count - 1;
|
||||
|
||||
while (YES)
|
||||
{
|
||||
mid = (min + max) / 2;
|
||||
XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid];
|
||||
|
||||
NSComparisonResult cmp = [message compare:currentMessage];
|
||||
if (cmp == NSOrderedAscending)
|
||||
{
|
||||
// message < currentMessage
|
||||
|
||||
if (mid == min)
|
||||
break;
|
||||
else
|
||||
max = mid - 1;
|
||||
}
|
||||
else // Descending || Same
|
||||
{
|
||||
// message >= currentMessage
|
||||
|
||||
if (mid == max) {
|
||||
mid++;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
min = mid + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Algorithm check:
|
||||
//
|
||||
// Insert in array[length] at index (index)
|
||||
// : min, mid, max (cmp_result)
|
||||
//
|
||||
//
|
||||
// Insert in array[3] at index (0)
|
||||
// : 0, 1, 2 (asc)
|
||||
// : 0, 0, 0 (asc) break
|
||||
//
|
||||
// Insert in array[3] at index (1)
|
||||
// : 0, 1, 2 (asc)
|
||||
// : 0, 0, 0 (dsc) mid++, break
|
||||
//
|
||||
// Insert in array[3] at index (2)
|
||||
// : 0, 1, 2 (dsc)
|
||||
// : 2, 2, 2 (asc) break
|
||||
//
|
||||
// Insert in array[3] at index (3)
|
||||
// : 0, 1, 2 (dsc)
|
||||
// : 2, 2, 2 (dsc) mid++, break
|
||||
|
||||
[messages insertObject:message atIndex:mid];
|
||||
return (NSUInteger)mid;
|
||||
}
|
||||
|
||||
- (void)addMessage:(XMPPRoomMessageMemoryStorageObject *)roomMsg
|
||||
{
|
||||
NSUInteger index = [self insertMessage:roomMsg];
|
||||
|
||||
XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[[roomMsg jid]];
|
||||
|
||||
XMPPRoomMessageMemoryStorageObject *roomMsgCopy = [roomMsg copy];
|
||||
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
|
||||
NSArray *messagesCopy = [[NSArray alloc] initWithArray:messages copyItems:YES];
|
||||
|
||||
[[self multicastDelegate] xmppRoomMemoryStorage:self
|
||||
didReceiveMessage:roomMsgCopy
|
||||
fromOccupant:occupantCopy
|
||||
atIndex:index
|
||||
inArray:messagesCopy];
|
||||
}
|
||||
|
||||
- (NSUInteger)insertOccupant:(XMPPRoomOccupantMemoryStorageObject *)occupant
|
||||
{
|
||||
NSUInteger count = [occupantsArray count];
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
[occupantsArray addObject:occupant];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find location using binary search algorithm.
|
||||
|
||||
NSInteger mid;
|
||||
NSInteger min = 0;
|
||||
NSInteger max = count - 1;
|
||||
|
||||
while (YES)
|
||||
{
|
||||
mid = (min + max) / 2;
|
||||
XMPPRoomOccupantMemoryStorageObject *currentOccupant = occupantsArray[mid];
|
||||
|
||||
NSComparisonResult cmp = [occupant compare:currentOccupant];
|
||||
if (cmp == NSOrderedAscending)
|
||||
{
|
||||
// occupant < currentOccupant
|
||||
|
||||
if (mid == min)
|
||||
break;
|
||||
else
|
||||
max = mid - 1;
|
||||
}
|
||||
else // Descending || Same
|
||||
{
|
||||
// occupant >= currentOccupant
|
||||
|
||||
if (mid == max) {
|
||||
mid++;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
min = mid + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[occupantsArray insertObject:occupant atIndex:mid];
|
||||
return (NSUInteger)mid;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Public API
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPRoomOccupantMemoryStorageObject *)occupantForJID:(XMPPJID *)jid
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return occupantsDict[jid];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block XMPPRoomOccupantMemoryStorageObject *occupant = nil;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
occupant = [occupantsDict[jid] copy];
|
||||
}});
|
||||
|
||||
return occupant;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)messages
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return messages;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result = nil;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
result = [[NSArray alloc] initWithArray:messages copyItems:YES];
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)occupants
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return occupantsArray;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result = nil;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)resortMessages
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
[messages sortUsingSelector:@selector(compare:)];
|
||||
return messages;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result = nil;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
[messages sortUsingSelector:@selector(compare:)];
|
||||
result = [[NSArray alloc] initWithArray:messages copyItems:YES];
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)resortOccupants
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
[occupantsArray sortUsingSelector:@selector(compare:)];
|
||||
return occupantsArray;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result = nil;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
[occupantsArray sortUsingSelector:@selector(compare:)];
|
||||
result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPRoomStorage Protocol
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
XMPPJID *from = [presence from];
|
||||
|
||||
if ([[presence type] isEqualToString:@"unavailable"])
|
||||
{
|
||||
XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from];
|
||||
if (occupant)
|
||||
{
|
||||
// Occupant did leave - remove
|
||||
|
||||
NSUInteger index = [occupantsArray indexOfObjectIdenticalTo:occupant];
|
||||
|
||||
[occupantsArray removeObjectAtIndex:index];
|
||||
[occupantsDict removeObjectForKey:from];
|
||||
|
||||
// Notify delegate(s)
|
||||
|
||||
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
|
||||
NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
|
||||
|
||||
[[self multicastDelegate] xmppRoomMemoryStorage:self
|
||||
occupantDidLeave:occupantCopy
|
||||
atIndex:index
|
||||
fromArray:occupantsCopy];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from];
|
||||
if (occupant == nil)
|
||||
{
|
||||
// Occupant did join - add
|
||||
|
||||
occupant = [[self.occupantClass alloc] initWithPresence:presence];
|
||||
|
||||
NSUInteger index = [self insertOccupant:occupant];
|
||||
occupantsDict[from] = occupant;
|
||||
|
||||
// Notify delegate(s)
|
||||
|
||||
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
|
||||
NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
|
||||
|
||||
[[self multicastDelegate] xmppRoomMemoryStorage:self
|
||||
occupantDidJoin:occupantCopy
|
||||
atIndex:index
|
||||
inArray:occupantsCopy];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Occupant did update - move
|
||||
|
||||
[occupant updateWithPresence:presence];
|
||||
|
||||
NSUInteger oldIndex = [occupantsArray indexOfObjectIdenticalTo:occupant];
|
||||
[occupantsArray removeObjectAtIndex:oldIndex];
|
||||
NSUInteger newIndex = [self insertOccupant:occupant];
|
||||
|
||||
// Notify delegate(s)
|
||||
|
||||
XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy];
|
||||
NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES];
|
||||
|
||||
[[self multicastDelegate] xmppRoomMemoryStorage:self
|
||||
occupantDidUpdate:occupantCopy
|
||||
fromIndex:oldIndex
|
||||
toIndex:newIndex
|
||||
inArray:occupantsCopy];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
XMPPJID *msgJID = room.myRoomJID;
|
||||
|
||||
XMPPRoomMessageMemoryStorageObject *roomMsg;
|
||||
roomMsg = [[self.messageClass alloc] initWithOutgoingMessage:message jid:msgJID];
|
||||
|
||||
[self addMessage:roomMsg];
|
||||
}
|
||||
|
||||
- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
XMPPJID *myRoomJID = room.myRoomJID;
|
||||
XMPPJID *messageJID = [message from];
|
||||
|
||||
if ([myRoomJID isEqualToJID:messageJID])
|
||||
{
|
||||
if (![message wasDelayed])
|
||||
{
|
||||
// Ignore - we already stored message in handleOutgoingMessage:room:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ([self existsMessage:message])
|
||||
{
|
||||
XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD);
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPRoomMessageMemoryStorageObject *roomMessage = [[self.messageClass alloc] initWithIncomingMessage:message];
|
||||
[self addMessage:roomMessage];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleDidLeaveRoom:(XMPPRoom *)room
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
[occupantsDict removeAllObjects];
|
||||
[occupantsArray removeAllObjects];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,34 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPRoomMessage.h"
|
||||
|
||||
|
||||
@interface XMPPRoomMessageMemoryStorageObject : NSObject <XMPPRoomMessage, NSCopying, NSSecureCoding>
|
||||
|
||||
- (id)initWithIncomingMessage:(XMPPMessage *)message;
|
||||
- (id)initWithOutgoingMessage:(XMPPMessage *)message jid:(XMPPJID *)myRoomJID;
|
||||
|
||||
/**
|
||||
* The properties below are documented in the XMPPRoomMessage protocol.
|
||||
**/
|
||||
|
||||
@property (nonatomic, readonly) XMPPMessage *message;
|
||||
|
||||
@property (nonatomic, readonly) XMPPJID * roomJID;
|
||||
|
||||
@property (nonatomic, readonly) XMPPJID * jid;
|
||||
@property (nonatomic, readonly) NSString * nickname;
|
||||
|
||||
@property (nonatomic, readonly) NSDate * localTimestamp;
|
||||
@property (nonatomic, readonly) NSDate * remoteTimestamp;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isFromMe;
|
||||
|
||||
/**
|
||||
* Compares two messages based on the localTimestamp.
|
||||
*
|
||||
* This method provides the ordering used by XMPPRoomMemoryStorage.
|
||||
* Subclasses may override this method to provide an alternative sorting mechanism.
|
||||
**/
|
||||
- (NSComparisonResult)compare:(XMPPRoomMessageMemoryStorageObject *)another;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,175 @@
|
||||
#import "XMPPRoomMessageMemoryStorageObject.h"
|
||||
#import "XMPP.h"
|
||||
#import "NSXMLElement+XEP_0203.h"
|
||||
|
||||
|
||||
@implementation XMPPRoomMessageMemoryStorageObject
|
||||
{
|
||||
XMPPMessage *message;
|
||||
XMPPJID *jid;
|
||||
NSDate *localTimestamp;
|
||||
NSDate *remoteTimestamp;
|
||||
BOOL isFromMe;
|
||||
}
|
||||
|
||||
- (id)initWithIncomingMessage:(XMPPMessage *)inMessage
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
message = inMessage;
|
||||
jid = [inMessage from];
|
||||
isFromMe = NO;
|
||||
|
||||
remoteTimestamp = [inMessage delayedDeliveryDate];
|
||||
if (remoteTimestamp)
|
||||
localTimestamp = remoteTimestamp;
|
||||
else
|
||||
localTimestamp = [[NSDate alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithOutgoingMessage:(XMPPMessage *)inMessage jid:(XMPPJID *)myRoomJID
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
message = inMessage;
|
||||
jid = myRoomJID;
|
||||
isFromMe = YES;
|
||||
|
||||
localTimestamp = [[NSDate alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Encoding, Decoding
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder
|
||||
{
|
||||
if ([encoder isBycopy])
|
||||
return self;
|
||||
else
|
||||
return [super replacementObjectForPortCoder:encoder];
|
||||
// return [NSDistantObject proxyWithLocal:self connection:[encoder connection]];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
if([coder respondsToSelector:@selector(requiresSecureCoding)] &&
|
||||
[coder requiresSecureCoding])
|
||||
{
|
||||
message = [coder decodeObjectOfClass:[XMPPMessage class] forKey:@"message"];
|
||||
jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"];
|
||||
localTimestamp = [coder decodeObjectOfClass:[NSDate class] forKey:@"localTimestamp"];
|
||||
remoteTimestamp = [coder decodeObjectOfClass:[NSDate class] forKey:@"remoteTimestamp"];
|
||||
isFromMe = [coder decodeBoolForKey:@"isFromMe"];
|
||||
}
|
||||
else
|
||||
{
|
||||
message = [coder decodeObjectForKey:@"message"];
|
||||
jid = [coder decodeObjectForKey:@"jid"];
|
||||
localTimestamp = [coder decodeObjectForKey:@"localTimestamp"];
|
||||
remoteTimestamp = [coder decodeObjectForKey:@"remoteTimestamp"];
|
||||
isFromMe = [coder decodeBoolForKey:@"isFromMe"];
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
message = [coder decodeObject];
|
||||
jid = [coder decodeObject];
|
||||
localTimestamp = [coder decodeObject];
|
||||
remoteTimestamp = [coder decodeObject];
|
||||
isFromMe = [[coder decodeObject] boolValue];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
[coder encodeObject:message forKey:@"message"];
|
||||
[coder encodeObject:jid forKey:@"jid"];
|
||||
[coder encodeObject:localTimestamp forKey:@"timestamp"];
|
||||
[coder encodeObject:remoteTimestamp forKey:@"remoteTimestamp"];
|
||||
[coder encodeBool:isFromMe forKey:@"isFromMe"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[coder encodeObject:message];
|
||||
[coder encodeObject:jid];
|
||||
[coder encodeObject:localTimestamp];
|
||||
[coder encodeObject:remoteTimestamp];
|
||||
[coder encodeObject:@(isFromMe)];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Copying
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
// We use [self class] to support subclassing
|
||||
|
||||
XMPPRoomMessageMemoryStorageObject *deepCopy = (XMPPRoomMessageMemoryStorageObject *)[[[self class] alloc] init];
|
||||
|
||||
deepCopy->message = [message copy];
|
||||
deepCopy->jid = [jid copy];
|
||||
deepCopy->localTimestamp = [localTimestamp copy];
|
||||
deepCopy->remoteTimestamp = [remoteTimestamp copy];
|
||||
deepCopy->isFromMe = isFromMe;
|
||||
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Properties
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@synthesize message;
|
||||
@synthesize jid;
|
||||
@synthesize localTimestamp;
|
||||
@synthesize remoteTimestamp;
|
||||
@synthesize isFromMe;
|
||||
|
||||
- (XMPPJID *)roomJID
|
||||
{
|
||||
return [jid bareJID];
|
||||
}
|
||||
|
||||
- (NSString *)nickname
|
||||
{
|
||||
return [jid resource];
|
||||
}
|
||||
|
||||
- (NSString *)body
|
||||
{
|
||||
return [[message elementForName:@"body"] stringValue];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Comparisons
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSComparisonResult)compare:(XMPPRoomMessageMemoryStorageObject *)another
|
||||
{
|
||||
return [localTimestamp compare:[another localTimestamp]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPRoom.h"
|
||||
#import "XMPPRoomOccupant.h"
|
||||
|
||||
|
||||
@interface XMPPRoomOccupantMemoryStorageObject : NSObject <XMPPRoomOccupant, NSCopying, NSSecureCoding>
|
||||
|
||||
- (id)initWithPresence:(XMPPPresence *)presence;
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence;
|
||||
|
||||
/**
|
||||
* The properties below are documented in the XMPPRoomOccupant protocol.
|
||||
**/
|
||||
|
||||
@property (readonly) XMPPPresence *presence;
|
||||
|
||||
@property (readonly) XMPPJID * jid;
|
||||
@property (readonly) XMPPJID * roomJID;
|
||||
@property (readonly) NSString * nickname;
|
||||
|
||||
@property (readonly) NSString * role;
|
||||
@property (readonly) NSString * affiliation;
|
||||
@property (readonly) XMPPJID * realJID;
|
||||
|
||||
/**
|
||||
* Compares two occupants based on the nickname.
|
||||
*
|
||||
* This method provides the ordering used by XMPPRoomMemoryStorage.
|
||||
* Subclasses may override this method to provide an alternative sorting mechanism.
|
||||
**/
|
||||
- (NSComparisonResult)compare:(XMPPRoomOccupantMemoryStorageObject *)another;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,196 @@
|
||||
#import "XMPPRoomOccupantMemoryStorageObject.h"
|
||||
|
||||
|
||||
@implementation XMPPRoomOccupantMemoryStorageObject
|
||||
{
|
||||
XMPPPresence *presence;
|
||||
XMPPJID *jid;
|
||||
}
|
||||
|
||||
- (id)initWithPresence:(XMPPPresence *)inPresence
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
presence = inPresence;
|
||||
jid = [inPresence from];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Encoding, Decoding
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if ! TARGET_OS_IPHONE
|
||||
- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder
|
||||
{
|
||||
if ([encoder isBycopy])
|
||||
return self;
|
||||
else
|
||||
return [super replacementObjectForPortCoder:encoder];
|
||||
// return [NSDistantObject proxyWithLocal:self connection:[encoder connection]];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
if([coder respondsToSelector:@selector(requiresSecureCoding)] &&
|
||||
[coder requiresSecureCoding])
|
||||
{
|
||||
presence = [coder decodeObjectOfClass:[XMPPPresence class] forKey:@"presence"];
|
||||
jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"];
|
||||
}
|
||||
else
|
||||
{
|
||||
presence = [coder decodeObjectForKey:@"presence"];
|
||||
jid = [coder decodeObjectForKey:@"jid"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
presence = [coder decodeObject];
|
||||
jid = [coder decodeObject];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
[coder encodeObject:presence forKey:@"presence"];
|
||||
[coder encodeObject:jid forKey:@"jid"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[coder encodeObject:presence];
|
||||
[coder encodeObject:jid];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Copying
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
// We use [self class] to support subclassing
|
||||
|
||||
XMPPRoomOccupantMemoryStorageObject *deepCopy = (XMPPRoomOccupantMemoryStorageObject *)[[[self class] alloc] init];
|
||||
|
||||
deepCopy->presence = [presence copy];
|
||||
deepCopy->jid = [jid copy];
|
||||
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Updates
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)inPresence
|
||||
{
|
||||
presence = inPresence;
|
||||
jid = [inPresence from];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Properties
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPJID *)roomJID
|
||||
{
|
||||
return [jid bareJID];
|
||||
}
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
return jid;
|
||||
}
|
||||
|
||||
- (NSString *)nickname
|
||||
{
|
||||
return [jid resource];
|
||||
}
|
||||
|
||||
- (NSString *)itemAttributeStringValueForName:(NSString *)attrName
|
||||
{
|
||||
NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"];
|
||||
NSXMLElement *item = [x elementForName:@"item"];
|
||||
if (item)
|
||||
{
|
||||
NSString *result = [item attributeStringValueForName:attrName];
|
||||
if (result)
|
||||
{
|
||||
return [result lowercaseString];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)role
|
||||
{
|
||||
return [self itemAttributeStringValueForName:@"role"];
|
||||
}
|
||||
|
||||
- (NSString *)affiliation
|
||||
{
|
||||
return [self itemAttributeStringValueForName:@"affiliation"];
|
||||
}
|
||||
|
||||
- (XMPPJID *)realJID
|
||||
{
|
||||
NSString *jidStr = [self itemAttributeStringValueForName:@"jid"];
|
||||
if (jidStr)
|
||||
return [XMPPJID jidWithString:jidStr];
|
||||
else
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (XMPPPresence *)presence
|
||||
{
|
||||
return presence;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Comparisons
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSComparisonResult)compare:(XMPPRoomOccupantMemoryStorageObject *)another
|
||||
{
|
||||
return [self.nickname compare:another.nickname];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark NSObject Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [jid hash];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
if ([anObject isMemberOfClass:[self class]])
|
||||
{
|
||||
XMPPRoomOccupantMemoryStorageObject *another = (XMPPRoomOccupantMemoryStorageObject *)anObject;
|
||||
|
||||
return [jid isEqualToJID:[another jid]];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,131 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoom.h"
|
||||
|
||||
#define _XMPP_MUC_H
|
||||
|
||||
@class XMPPIDTracker;
|
||||
|
||||
/**
|
||||
* The XMPPMUC module, combined with XMPPRoom and associated storage classes,
|
||||
* provides an implementation of XEP-0045 Multi-User Chat.
|
||||
*
|
||||
* The bulk of the code resides in XMPPRoom, which handles the xmpp technical details
|
||||
* such as surrounding joining/leaving a room, sending/receiving messages, etc.
|
||||
*
|
||||
* The XMPPMUC class provides general (but important) tasks relating to MUC:
|
||||
* - It integrates with XMPPCapabilities (if available) to properly advertise support for MUC.
|
||||
* - It monitors active XMPPRoom instances on the xmppStream,
|
||||
* and provides an efficient query to see if a presence or message element is targeted at a room.
|
||||
* - It listens for MUC room invitations sent from other users.
|
||||
**/
|
||||
@interface XMPPMUC : XMPPModule
|
||||
{
|
||||
/* Inherited from XMPPModule:
|
||||
|
||||
XMPPStream *xmppStream;
|
||||
|
||||
dispatch_queue_t moduleQueue;
|
||||
*/
|
||||
|
||||
NSMutableSet *rooms;
|
||||
|
||||
XMPPIDTracker *xmppIDTracker;
|
||||
}
|
||||
|
||||
/* Inherited from XMPPModule:
|
||||
|
||||
- (id)init;
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue;
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)xmppStream;
|
||||
- (void)deactivate;
|
||||
|
||||
@property (readonly) XMPPStream *xmppStream;
|
||||
|
||||
- (NSString *)moduleName;
|
||||
|
||||
*/
|
||||
|
||||
- (BOOL)isMUCRoomPresence:(XMPPPresence *)presence;
|
||||
- (BOOL)isMUCRoomMessage:(XMPPMessage *)message;
|
||||
|
||||
/**
|
||||
* This method will attempt to discover existing services for the domain found in xmppStream.myJID.
|
||||
*
|
||||
* @see xmppMUC:didDiscoverServices:
|
||||
* @see xmppMUCFailedToDiscoverServices:withError:
|
||||
*/
|
||||
- (void)discoverServices;
|
||||
|
||||
/**
|
||||
* This method will attempt to discover existing rooms (that are not hidden) for a given service.
|
||||
*
|
||||
* @see xmppMUC:didDiscoverRooms:forServiceNamed:
|
||||
* @see xmppMUC:failedToDiscoverRoomsForServiceNamed:withError:
|
||||
*
|
||||
* @param serviceName The name of the service for which to discover rooms. Normally in the form
|
||||
* of "chat.shakespeare.lit".
|
||||
*
|
||||
* @return NO if a serviceName is not provided, otherwise YES
|
||||
*/
|
||||
- (BOOL)discoverRoomsForServiceNamed:(NSString *)serviceName;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPMUCDelegate
|
||||
@optional
|
||||
|
||||
- (void)xmppMUC:(XMPPMUC *)sender roomJID:(XMPPJID *)roomJID didReceiveInvitation:(XMPPMessage *)message;
|
||||
- (void)xmppMUC:(XMPPMUC *)sender roomJID:(XMPPJID *)roomJID didReceiveInvitationDecline:(XMPPMessage *)message;
|
||||
|
||||
/**
|
||||
* Implement this method when calling [mucInstance discoverServices]. It will be invoked if the request
|
||||
* for discovering services is successfully executed and receives a successful response.
|
||||
*
|
||||
* @param sender XMPPMUC object invoking this delegate method.
|
||||
* @param services An array of NSXMLElements in the form shown below. You will need to extract the data you
|
||||
* wish to use.
|
||||
*
|
||||
* <item jid='chat.shakespeare.lit' name='Chatroom Service'/>
|
||||
*/
|
||||
- (void)xmppMUC:(XMPPMUC *)sender didDiscoverServices:(NSArray *)services;
|
||||
|
||||
/**
|
||||
* Implement this method when calling [mucInstanse discoverServices]. It will be invoked if the request
|
||||
* for discovering services is unsuccessfully executed or receives an unsuccessful response.
|
||||
*
|
||||
* @param sender XMPPMUC object invoking this delegate method.
|
||||
* @param error NSError containing more details of the failure.
|
||||
*/
|
||||
- (void)xmppMUCFailedToDiscoverServices:(XMPPMUC *)sender withError:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Implement this method when calling [mucInstance discoverRoomsForServiceNamed:]. It will be invoked if
|
||||
* the request for discovering rooms is successfully executed and receives a successful response.
|
||||
*
|
||||
* @param sender XMPPMUC object invoking this delegate method.
|
||||
* @param rooms An array of NSXMLElements in the form shown below. You will need to extract the data you
|
||||
* wish to use.
|
||||
*
|
||||
* <item jid='forres@chat.shakespeare.lit' name='The Palace'/>
|
||||
*
|
||||
* @param serviceName The name of the service for which rooms were discovered.
|
||||
*/
|
||||
- (void)xmppMUC:(XMPPMUC *)sender didDiscoverRooms:(NSArray *)rooms forServiceNamed:(NSString *)serviceName;
|
||||
|
||||
/**
|
||||
* Implement this method when calling [mucInstance discoverRoomsForServiceNamed:]. It will be invoked if
|
||||
* the request for discovering rooms is unsuccessfully executed or receives an unsuccessful response.
|
||||
*
|
||||
* @param sender XMPPMUC object invoking this delegate method.
|
||||
* @param serviceName The name of the service for which rooms were attempted to be discovered.
|
||||
* @param error NSError containing more details of the failure.
|
||||
*/
|
||||
- (void)xmppMUC:(XMPPMUC *)sender failedToDiscoverRoomsForServiceNamed:(NSString *)serviceName withError:(NSError *)error;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,423 @@
|
||||
#import "XMPPMUC.h"
|
||||
#import "XMPPFramework.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "XMPPIDTracker.h"
|
||||
|
||||
#if DEBUG
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
NSString *const XMPPDiscoverItemsNamespace = @"http://jabber.org/protocol/disco#items";
|
||||
NSString *const XMPPMUCErrorDomain = @"XMPPMUCErrorDomain";
|
||||
|
||||
@interface XMPPMUC()
|
||||
{
|
||||
BOOL hasRequestedServices;
|
||||
BOOL hasRequestedRooms;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation XMPPMUC
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
if ((self = [super initWithDispatchQueue:queue])) {
|
||||
rooms = [[NSMutableSet alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)aXmppStream
|
||||
{
|
||||
if ([super activate:aXmppStream])
|
||||
{
|
||||
XMPPLogVerbose(@"%@: Activated", THIS_FILE);
|
||||
|
||||
xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream
|
||||
dispatchQueue:moduleQueue];
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
[xmppStream autoAddDelegate:self
|
||||
delegateQueue:moduleQueue
|
||||
toModulesOfClass:[XMPPCapabilities class]];
|
||||
#endif
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
[xmppIDTracker removeAllIDs];
|
||||
xmppIDTracker = nil;
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
[xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]];
|
||||
#endif
|
||||
|
||||
[super deactivate];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Public API
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)isMUCRoomElement:(XMPPElement *)element
|
||||
{
|
||||
XMPPJID *bareFrom = [[element from] bareJID];
|
||||
if (bareFrom == nil)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
__block BOOL result = NO;
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
|
||||
result = [rooms containsObject:bareFrom];
|
||||
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)isMUCRoomPresence:(XMPPPresence *)presence
|
||||
{
|
||||
return [self isMUCRoomElement:presence];
|
||||
}
|
||||
|
||||
- (BOOL)isMUCRoomMessage:(XMPPMessage *)message
|
||||
{
|
||||
return [self isMUCRoomElement:message];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method provides functionality of XEP-0045 6.1 Discovering a MUC Service.
|
||||
*
|
||||
* @link {http://xmpp.org/extensions/xep-0045.html#disco-service}
|
||||
*
|
||||
* Example 1. Entity Queries Server for Associated Services
|
||||
*
|
||||
* <iq from='hag66@shakespeare.lit/pda'
|
||||
* id='h7ns81g'
|
||||
* to='shakespeare.lit'
|
||||
* type='get'>
|
||||
* <query xmlns='http://jabber.org/protocol/disco#items'/>
|
||||
* </iq>
|
||||
*/
|
||||
- (void)discoverServices
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
if (hasRequestedServices) return; // We've already requested services
|
||||
|
||||
NSString *toStr = xmppStream.myJID.domain;
|
||||
NSXMLElement *query = [NSXMLElement elementWithName:@"query"
|
||||
xmlns:XMPPDiscoverItemsNamespace];
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"get"
|
||||
to:[XMPPJID jidWithString:toStr]
|
||||
elementID:[xmppStream generateUUID]
|
||||
child:query];
|
||||
|
||||
[xmppIDTracker addElement:iq
|
||||
target:self
|
||||
selector:@selector(handleDiscoverServicesQueryIQ:withInfo:)
|
||||
timeout:60];
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
hasRequestedServices = YES;
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method provides functionality of XEP-0045 6.3 Discovering Rooms
|
||||
*
|
||||
* @link {http://xmpp.org/extensions/xep-0045.html#disco-rooms}
|
||||
*
|
||||
* Example 5. Entity Queries Chat Service for Rooms
|
||||
*
|
||||
* <iq from='hag66@shakespeare.lit/pda'
|
||||
* id='zb8q41f4'
|
||||
* to='chat.shakespeare.lit'
|
||||
* type='get'>
|
||||
* <query xmlns='http://jabber.org/protocol/disco#items'/>
|
||||
* </iq>
|
||||
*/
|
||||
- (BOOL)discoverRoomsForServiceNamed:(NSString *)serviceName
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (serviceName.length < 2)
|
||||
return NO;
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
if (hasRequestedRooms) return; // We've already requested rooms
|
||||
|
||||
NSXMLElement *query = [NSXMLElement elementWithName:@"query"
|
||||
xmlns:XMPPDiscoverItemsNamespace];
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"get"
|
||||
to:[XMPPJID jidWithString:serviceName]
|
||||
elementID:[xmppStream generateUUID]
|
||||
child:query];
|
||||
|
||||
[xmppIDTracker addElement:iq
|
||||
target:self
|
||||
selector:@selector(handleDiscoverRoomsQueryIQ:withInfo:)
|
||||
timeout:60];
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
hasRequestedRooms = YES;
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPIDTracker
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* This method handles the response received (or not received) after calling discoverServices.
|
||||
*/
|
||||
- (void)handleDiscoverServicesQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info
|
||||
{
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
NSXMLElement *errorElem = [iq elementForName:@"error"];
|
||||
|
||||
if (errorElem) {
|
||||
NSString *errMsg = [errorElem.children componentsJoinedByString:@", "];
|
||||
NSDictionary *dict = @{NSLocalizedDescriptionKey : errMsg};
|
||||
NSError *error = [NSError errorWithDomain:XMPPMUCErrorDomain
|
||||
code:[errorElem attributeIntegerValueForName:@"code"
|
||||
withDefaultValue:0]
|
||||
userInfo:dict];
|
||||
|
||||
[multicastDelegate xmppMUCFailedToDiscoverServices:self
|
||||
withError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
NSXMLElement *query = [iq elementForName:@"query"
|
||||
xmlns:XMPPDiscoverItemsNamespace];
|
||||
|
||||
NSArray *items = [query elementsForName:@"item"];
|
||||
[multicastDelegate xmppMUC:self didDiscoverServices:items];
|
||||
hasRequestedServices = NO; // Set this back to NO to allow for future requests
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the response received (or not received) after calling discoverRoomsForServiceNamed:.
|
||||
*/
|
||||
- (void)handleDiscoverRoomsQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info
|
||||
{
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
NSXMLElement *errorElem = [iq elementForName:@"error"];
|
||||
NSString *serviceName = [iq attributeStringValueForName:@"from" withDefaultValue:@""];
|
||||
|
||||
if (errorElem) {
|
||||
NSString *errMsg = [errorElem.children componentsJoinedByString:@", "];
|
||||
NSDictionary *dict = @{NSLocalizedDescriptionKey : errMsg};
|
||||
NSError *error = [NSError errorWithDomain:XMPPMUCErrorDomain
|
||||
code:[errorElem attributeIntegerValueForName:@"code"
|
||||
withDefaultValue:0]
|
||||
userInfo:dict];
|
||||
[multicastDelegate xmppMUC:self
|
||||
failedToDiscoverRoomsForServiceNamed:serviceName
|
||||
withError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
NSXMLElement *query = [iq elementForName:@"query"
|
||||
xmlns:XMPPDiscoverItemsNamespace];
|
||||
|
||||
NSArray *items = [query elementsForName:@"item"];
|
||||
[multicastDelegate xmppMUC:self
|
||||
didDiscoverRooms:items
|
||||
forServiceNamed:serviceName];
|
||||
hasRequestedRooms = NO; // Set this back to NO to allow for future requests
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPStream Delegate
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module
|
||||
{
|
||||
if ([module isKindOfClass:[XMPPRoom class]])
|
||||
{
|
||||
XMPPJID *roomJID = [(XMPPRoom *)module roomJID];
|
||||
|
||||
[rooms addObject:roomJID];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module
|
||||
{
|
||||
if ([module isKindOfClass:[XMPPRoom class]])
|
||||
{
|
||||
XMPPJID *roomJID = [(XMPPRoom *)module roomJID];
|
||||
|
||||
// It's common for the room to get deactivated and deallocated before
|
||||
// we've received the goodbye presence from the server.
|
||||
// So we're going to postpone for a bit removing the roomJID from the list.
|
||||
// This way the isMUCRoomElement will still remain accurate
|
||||
// for presence elements that may arrive momentarily.
|
||||
|
||||
double delayInSeconds = 30.0;
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
|
||||
dispatch_after(popTime, moduleQueue, ^{ @autoreleasepool {
|
||||
|
||||
[rooms removeObject:roomJID];
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
|
||||
{
|
||||
// Examples from XEP-0045:
|
||||
//
|
||||
//
|
||||
// Example 124. Room Sends Invitation to New Member:
|
||||
//
|
||||
// <message from='darkcave@chat.shakespeare.lit' to='hecate@shakespeare.lit'>
|
||||
// <x xmlns='http://jabber.org/protocol/muc#user'>
|
||||
// <invite from='bard@shakespeare.lit'/>
|
||||
// <password>cauldronburn</password>
|
||||
// </x>
|
||||
// </message>
|
||||
//
|
||||
//
|
||||
// Example 125. Service Returns Error on Attempt by Mere Member to Invite Others to a Members-Only Room
|
||||
//
|
||||
// <message from='darkcave@chat.shakespeare.lit' to='hag66@shakespeare.lit/pda' type='error'>
|
||||
// <x xmlns='http://jabber.org/protocol/muc#user'>
|
||||
// <invite to='hecate@shakespeare.lit'>
|
||||
// <reason>
|
||||
// Hey Hecate, this is the place for all good witches!
|
||||
// </reason>
|
||||
// </invite>
|
||||
// </x>
|
||||
// <error type='auth'>
|
||||
// <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
// </error>
|
||||
// </message>
|
||||
//
|
||||
//
|
||||
// Example 50. Room Informs Invitor that Invitation Was Declined
|
||||
//
|
||||
// <message from='darkcave@chat.shakespeare.lit' to='crone1@shakespeare.lit/desktop'>
|
||||
// <x xmlns='http://jabber.org/protocol/muc#user'>
|
||||
// <decline from='hecate@shakespeare.lit'>
|
||||
// <reason>
|
||||
// Sorry, I'm too busy right now.
|
||||
// </reason>
|
||||
// </decline>
|
||||
// </x>
|
||||
// </message>
|
||||
//
|
||||
//
|
||||
// Examples from XEP-0249:
|
||||
//
|
||||
//
|
||||
// Example 1. A direct invitation
|
||||
//
|
||||
// <message from='crone1@shakespeare.lit/desktop' to='hecate@shakespeare.lit'>
|
||||
// <x xmlns='jabber:x:conference'
|
||||
// jid='darkcave@macbeth.shakespeare.lit'
|
||||
// password='cauldronburn'
|
||||
// reason='Hey Hecate, this is the place for all good witches!'/>
|
||||
// </message>
|
||||
|
||||
NSXMLElement * x = [message elementForName:@"x" xmlns:XMPPMUCUserNamespace];
|
||||
NSXMLElement * invite = [x elementForName:@"invite"];
|
||||
NSXMLElement * decline = [x elementForName:@"decline"];
|
||||
|
||||
NSXMLElement * directInvite = [message elementForName:@"x" xmlns:@"jabber:x:conference"];
|
||||
|
||||
XMPPJID * roomJID = [message from];
|
||||
|
||||
if (invite || directInvite)
|
||||
{
|
||||
[multicastDelegate xmppMUC:self roomJID:roomJID didReceiveInvitation:message];
|
||||
}
|
||||
else if (decline)
|
||||
{
|
||||
[multicastDelegate xmppMUC:self roomJID:roomJID didReceiveInvitationDecline:message];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)xmppStream:(XMPPStream *)stream didReceiveIQ:(XMPPIQ *)iq
|
||||
{
|
||||
NSString *type = [iq type];
|
||||
|
||||
if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) {
|
||||
return [xmppIDTracker invokeForElement:iq withObject:iq];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
/**
|
||||
* If an XMPPCapabilites instance is used we want to advertise our support for MUC.
|
||||
**/
|
||||
- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query
|
||||
{
|
||||
// This method is invoked on our moduleQueue.
|
||||
|
||||
// <query xmlns="http://jabber.org/protocol/disco#info">
|
||||
// ...
|
||||
// <feature var='http://jabber.org/protocol/muc'/>
|
||||
// ...
|
||||
// </query>
|
||||
|
||||
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature addAttributeWithName:@"var" stringValue:@"http://jabber.org/protocol/muc"];
|
||||
|
||||
[query addChild:feature];
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,11 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPMessage.h"
|
||||
|
||||
|
||||
@interface XMPPMessage(XEP0045)
|
||||
|
||||
- (BOOL)isGroupChatMessage;
|
||||
- (BOOL)isGroupChatMessageWithBody;
|
||||
- (BOOL)isGroupChatMessageWithSubject;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,36 @@
|
||||
#import "XMPPMessage+XEP0045.h"
|
||||
#import "NSXMLElement+XMPP.h"
|
||||
|
||||
|
||||
@implementation XMPPMessage(XEP0045)
|
||||
|
||||
- (BOOL)isGroupChatMessage
|
||||
{
|
||||
return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"groupchat"];
|
||||
}
|
||||
|
||||
- (BOOL)isGroupChatMessageWithBody
|
||||
{
|
||||
if ([self isGroupChatMessage])
|
||||
{
|
||||
NSString *body = [[self elementForName:@"body"] stringValue];
|
||||
|
||||
return ([body length] > 0);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isGroupChatMessageWithSubject
|
||||
{
|
||||
if ([self isGroupChatMessage])
|
||||
{
|
||||
NSString *subject = [[self elementForName:@"subject"] stringValue];
|
||||
|
||||
return ([subject length] > 0);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,314 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRoomMessage.h"
|
||||
#import "XMPPRoomOccupant.h"
|
||||
|
||||
#define _XMPP_ROOM_H
|
||||
|
||||
@class XMPPIDTracker;
|
||||
@protocol XMPPRoomStorage;
|
||||
@protocol XMPPRoomDelegate;
|
||||
|
||||
static NSString *const XMPPMUCNamespace = @"http://jabber.org/protocol/muc";
|
||||
static NSString *const XMPPMUCUserNamespace = @"http://jabber.org/protocol/muc#user";
|
||||
static NSString *const XMPPMUCAdminNamespace = @"http://jabber.org/protocol/muc#admin";
|
||||
static NSString *const XMPPMUCOwnerNamespace = @"http://jabber.org/protocol/muc#owner";
|
||||
|
||||
|
||||
@interface XMPPRoom : XMPPModule
|
||||
{
|
||||
/* Inherited from XMPPModule:
|
||||
|
||||
XMPPStream *xmppStream;
|
||||
|
||||
dispatch_queue_t moduleQueue;
|
||||
id multicastDelegate;
|
||||
*/
|
||||
|
||||
__strong id <XMPPRoomStorage> xmppRoomStorage;
|
||||
|
||||
__strong XMPPJID *roomJID;
|
||||
|
||||
__strong XMPPJID *myRoomJID;
|
||||
__strong NSString *myNickname;
|
||||
__strong NSString *myOldNickname;
|
||||
|
||||
__strong NSString *roomSubject;
|
||||
|
||||
XMPPIDTracker *responseTracker;
|
||||
|
||||
uint16_t state;
|
||||
}
|
||||
|
||||
- (id)initWithRoomStorage:(id <XMPPRoomStorage>)storage jid:(XMPPJID *)roomJID;
|
||||
- (id)initWithRoomStorage:(id <XMPPRoomStorage>)storage jid:(XMPPJID *)roomJID dispatchQueue:(dispatch_queue_t)queue;
|
||||
|
||||
/* Inherited from XMPPModule:
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)xmppStream;
|
||||
- (void)deactivate;
|
||||
|
||||
@property (readonly) XMPPStream *xmppStream;
|
||||
|
||||
- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
|
||||
- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
|
||||
- (void)removeDelegate:(id)delegate;
|
||||
|
||||
- (NSString *)moduleName;
|
||||
|
||||
*/
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
@property (readonly) id <XMPPRoomStorage> xmppRoomStorage;
|
||||
|
||||
@property (readonly) XMPPJID * roomJID; // E.g. xmpp-development@conference.deusty.com
|
||||
|
||||
@property (readonly) XMPPJID * myRoomJID; // E.g. xmpp-development@conference.deusty.com/robbiehanson
|
||||
@property (readonly) NSString * myNickname; // E.g. robbiehanson
|
||||
|
||||
@property (readonly) NSString *roomSubject;
|
||||
|
||||
@property (readonly) BOOL isJoined;
|
||||
|
||||
#pragma mark Room Lifecycle
|
||||
|
||||
/**
|
||||
* Sends a presence element to the join room.
|
||||
*
|
||||
* If the room already exists, then the xmppRoomDidJoin: delegate method will be invoked upon
|
||||
* notifiaction from the server that we successfully joined the room.
|
||||
*
|
||||
* If the room did not already exist, and the authenticated user is allowed to create the room,
|
||||
* then the server will automatically create the room,
|
||||
* and the xmppRoomDidCreate: delegate method will be invoked (followed by xmppRoomDidJoin:).
|
||||
* You'll then need to configure the room before others can join.
|
||||
*
|
||||
* @param desiredNickname (required)
|
||||
* The nickname to use within the room.
|
||||
* If the room is anonymous, this is the only identifier other occupants of the room will see.
|
||||
*
|
||||
* @param history (optional)
|
||||
* A history element specifying how much discussion history to request from the server.
|
||||
* E.g. <history maxstanzas='100'/>
|
||||
* For more information, please see XEP-0045, Section 7.1.16 - Managing Discussion History.
|
||||
* You may also want to query your storage module to see how old the most recent stored message for this room is.
|
||||
*
|
||||
* @see fetchConfigurationForm
|
||||
* @see configureRoomUsingOptions:
|
||||
**/
|
||||
- (void)joinRoomUsingNickname:(NSString *)desiredNickname history:(NSXMLElement *)history;
|
||||
- (void)joinRoomUsingNickname:(NSString *)desiredNickname history:(NSXMLElement *)history password:(NSString *)passwd;
|
||||
|
||||
/**
|
||||
* There are two ways to configure a room.
|
||||
* 1.) Accept the default configuration
|
||||
* 2.) Send a custom configuration
|
||||
*
|
||||
* To see which configuration options the server supports,
|
||||
* or to inspect the default options, you'll need to fetch the configuration form.
|
||||
*
|
||||
* @see configureRoomUsingOptions:
|
||||
**/
|
||||
- (void)fetchConfigurationForm;
|
||||
|
||||
/**
|
||||
* Pass nil to accept the default configuration.
|
||||
**/
|
||||
- (void)configureRoomUsingOptions:(NSXMLElement *)roomConfigForm;
|
||||
|
||||
- (void)leaveRoom;
|
||||
- (void)destroyRoom;
|
||||
|
||||
#pragma mark Room Interaction
|
||||
|
||||
- (void)changeNickname:(NSString *)newNickname;
|
||||
- (void)changeRoomSubject:(NSString *)newRoomSubject;
|
||||
|
||||
- (void)inviteUser:(XMPPJID *)jid withMessage:(NSString *)invitationMessage;
|
||||
|
||||
- (void)sendMessage:(XMPPMessage *)message;
|
||||
|
||||
- (void)sendMessageWithBody:(NSString *)messageBody;
|
||||
|
||||
#pragma mark Room Moderation
|
||||
|
||||
- (void)fetchBanList;
|
||||
- (void)fetchMembersList;
|
||||
- (void)fetchModeratorsList;
|
||||
|
||||
/**
|
||||
* The ban list, member list, and moderator list are simply subsets of the room privileges list.
|
||||
* That is, a user's status as 'banned', 'member', 'moderator', etc,
|
||||
* are simply different priveleges that may be assigned to a user.
|
||||
*
|
||||
* You may edit the list of privileges using this method.
|
||||
* The array of items corresponds with the <item/> stanzas of Section 9 of XEP-0045.
|
||||
* This class provides helper methods to create these item elements.
|
||||
*
|
||||
* @see itemWithAffiliation:jid:
|
||||
* @see itemWithRole:jid:
|
||||
*
|
||||
* The authenticated user must be an admin or owner of the room, or the server will deny the request.
|
||||
*
|
||||
* To add a member: <item
|
||||
*
|
||||
*
|
||||
* @return The id of the XMPPIQ that was sent.
|
||||
* This may be used to match multiple change requests with the responses in xmppRoom:didEditPrivileges:.
|
||||
**/
|
||||
- (NSString *)editRoomPrivileges:(NSArray *)items;
|
||||
|
||||
+ (NSXMLElement *)itemWithAffiliation:(NSString *)affiliation jid:(XMPPJID *)jid;
|
||||
+ (NSXMLElement *)itemWithRole:(NSString *)role jid:(XMPPJID *)jid;
|
||||
|
||||
@end
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRoomStorage <NSObject>
|
||||
@required
|
||||
|
||||
//
|
||||
//
|
||||
// -- PUBLIC METHODS --
|
||||
//
|
||||
// There are no public methods required by this protocol.
|
||||
//
|
||||
// Each individual storage class will provide a proper way to access/enumerate the
|
||||
// occupants/messages according to the underlying storage mechanism.
|
||||
//
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// -- PRIVATE METHODS --
|
||||
//
|
||||
// These methods are designed to be used ONLY by the XMPPRoom class.
|
||||
//
|
||||
//
|
||||
|
||||
/**
|
||||
* Configures the storage class, passing it's parent and parent's dispatch queue.
|
||||
*
|
||||
* This method is called by the init method of the XMPPRoom class.
|
||||
* This method is designed to inform the storage class of it's parent
|
||||
* and of the dispatch queue the parent will be operating on.
|
||||
*
|
||||
* A storage class may choose to operate on the same queue as it's parent,
|
||||
* as the majority of the time it will be getting called by the parent.
|
||||
* If both are operating on the same queue, the combination may run faster.
|
||||
*
|
||||
* Some storage classes support multiple xmppStreams,
|
||||
* and may choose to operate on their own internal queue.
|
||||
*
|
||||
* This method should return YES if it was configured properly.
|
||||
* It should return NO only if configuration failed.
|
||||
* For example, a storage class designed to be used only with a single xmppStream is being added to a second stream.
|
||||
* The XMPPCapabilites class is configured to ignore the passed
|
||||
* storage class in it's init method if this method returns NO.
|
||||
**/
|
||||
- (BOOL)configureWithParent:(XMPPRoom *)aParent queue:(dispatch_queue_t)queue;
|
||||
|
||||
/**
|
||||
* Updates and returns the occupant for the given presence element.
|
||||
* If the presence type is "available", and the occupant doesn't already exist, then one should be created.
|
||||
**/
|
||||
- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room;
|
||||
|
||||
/**
|
||||
* Stores or otherwise handles the given message element.
|
||||
**/
|
||||
- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room;
|
||||
- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room;
|
||||
|
||||
/**
|
||||
* Handles leaving the room, which generally means clearing the list of occupants.
|
||||
**/
|
||||
- (void)handleDidLeaveRoom:(XMPPRoom *)room;
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
* May be used if there's anything special to do when joining a room.
|
||||
**/
|
||||
- (void)handleDidJoinRoom:(XMPPRoom *)room withNickname:(NSString *)nickname;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRoomDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
- (void)xmppRoomDidCreate:(XMPPRoom *)sender;
|
||||
|
||||
/**
|
||||
* Invoked with the results of a request to fetch the configuration form.
|
||||
* The given config form will look something like:
|
||||
*
|
||||
* <x xmlns='jabber:x:data' type='form'>
|
||||
* <title>Configuration for MUC Room</title>
|
||||
* <field type='hidden'
|
||||
* var='FORM_TYPE'>
|
||||
* <value>http://jabber.org/protocol/muc#roomconfig</value>
|
||||
* </field>
|
||||
* <field label='Natural-Language Room Name'
|
||||
* type='text-single'
|
||||
* var='muc#roomconfig_roomname'/>
|
||||
* <field label='Enable Public Logging?'
|
||||
* type='boolean'
|
||||
* var='muc#roomconfig_enablelogging'>
|
||||
* <value>0</value>
|
||||
* </field>
|
||||
* ...
|
||||
* </x>
|
||||
*
|
||||
* The form is to be filled out and then submitted via the configureRoomUsingOptions: method.
|
||||
*
|
||||
* @see fetchConfigurationForm:
|
||||
* @see configureRoomUsingOptions:
|
||||
**/
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didFetchConfigurationForm:(NSXMLElement *)configForm;
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender willSendConfiguration:(XMPPIQ *)roomConfigForm;
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didConfigure:(XMPPIQ *)iqResult;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didNotConfigure:(XMPPIQ *)iqResult;
|
||||
|
||||
- (void)xmppRoomDidJoin:(XMPPRoom *)sender;
|
||||
- (void)xmppRoomDidLeave:(XMPPRoom *)sender;
|
||||
|
||||
|
||||
- (void)xmppRoomDidDestroy:(XMPPRoom *)sender;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didFailToDestroy:(XMPPIQ *)iqError;
|
||||
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender occupantDidJoin:(XMPPJID *)occupantJID withPresence:(XMPPPresence *)presence;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender occupantDidLeave:(XMPPJID *)occupantJID withPresence:(XMPPPresence *)presence;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender occupantDidUpdate:(XMPPJID *)occupantJID withPresence:(XMPPPresence *)presence;
|
||||
|
||||
/**
|
||||
* Invoked when a message is received.
|
||||
* The occupant parameter may be nil if the message came directly from the room, or from a non-occupant.
|
||||
**/
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didReceiveMessage:(XMPPMessage *)message fromOccupant:(XMPPJID *)occupantJID;
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didFetchBanList:(NSArray *)items;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didNotFetchBanList:(XMPPIQ *)iqError;
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didFetchMembersList:(NSArray *)items;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didNotFetchMembersList:(XMPPIQ *)iqError;
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didFetchModeratorsList:(NSArray *)items;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didNotFetchModeratorsList:(XMPPIQ *)iqError;
|
||||
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didEditPrivileges:(XMPPIQ *)iqResult;
|
||||
- (void)xmppRoom:(XMPPRoom *)sender didNotEditPrivileges:(XMPPIQ *)iqError;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,58 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class XMPPJID;
|
||||
@class XMPPMessage;
|
||||
|
||||
|
||||
@protocol XMPPRoomMessage <NSObject>
|
||||
|
||||
/**
|
||||
* The raw message that was sent / received.
|
||||
**/
|
||||
- (XMPPMessage *)message;
|
||||
|
||||
/**
|
||||
* The JID of the MUC room.
|
||||
**/
|
||||
- (XMPPJID *)roomJID;
|
||||
|
||||
/**
|
||||
* Who sent the message.
|
||||
* A typical MUC room jid is of the form "room_name@conference.domain.tld/some_nickname".
|
||||
**/
|
||||
- (XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* The nickname of the user who sent the message.
|
||||
* This is a convenience method for [jid resource].
|
||||
**/
|
||||
- (NSString *)nickname;
|
||||
|
||||
/**
|
||||
* Convenience method to access the body of the message.
|
||||
**/
|
||||
- (NSString *)body;
|
||||
|
||||
/**
|
||||
* When the message was sent / received (as recorded by us).
|
||||
*
|
||||
* If the message was originally sent by us, the localTimestamp is recorded automatically.
|
||||
* If the message was received, the server may have included a delayed delivery date timestamp.
|
||||
* This is the case when first joining a room, and downloading the discussion history.
|
||||
* In such a case, the localTimestamp will be a reflection of the serverTimestamp.
|
||||
**/
|
||||
- (NSDate *)localTimestamp;
|
||||
|
||||
/**
|
||||
* When the message was sent / received (as recorded by the server).
|
||||
*
|
||||
* Only set when the server includes a delayedDelivery timestamp within the message.
|
||||
**/
|
||||
- (NSDate *)remoteTimestamp;
|
||||
|
||||
/**
|
||||
* Whether or not the message was sent by us.
|
||||
**/
|
||||
- (BOOL)isFromMe;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,57 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class XMPPJID;
|
||||
@class XMPPPresence;
|
||||
|
||||
|
||||
@protocol XMPPRoomOccupant <NSObject>
|
||||
|
||||
/**
|
||||
* Most recent presence message from occupant.
|
||||
**/
|
||||
- (XMPPPresence *)presence;
|
||||
|
||||
/**
|
||||
* The MUC room the occupant is associated with.
|
||||
**/
|
||||
- (XMPPJID *)roomJID;
|
||||
|
||||
/**
|
||||
* The JID of the occupant as reported by the room.
|
||||
* A typical MUC room will use JIDs of the form: "room_name@conference.domain.tl/some_nickname".
|
||||
**/
|
||||
- (XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* The nickname of the user.
|
||||
* In other words, the resource portion of the occupants JID.
|
||||
**/
|
||||
- (NSString *)nickname;
|
||||
|
||||
/**
|
||||
* The 'role' and 'affiliation' of the occupant within the MUC room.
|
||||
*
|
||||
* From XEP-0045, Section 5 - Roles and Affiliations:
|
||||
*
|
||||
* There are two dimensions along which we can measure a user's connection with or position in a room.
|
||||
* One is the user's long-lived affiliation with a room -- e.g., a user's status as an owner or an outcast.
|
||||
* The other is a user's role while an occupant of a room -- e.g., an occupant's position as a moderator with the
|
||||
* ability to kick visitors and participants. These two dimensions are distinct from each other, since an affiliation
|
||||
* lasts across visits, while a role lasts only for the duration of a visit. In addition, there is no one-to-one
|
||||
* correspondence between roles and affiliations; for example, someone who is not affiliated with a room may be
|
||||
* a (temporary) moderator, and a member may be a participant or a visitor in a moderated room.
|
||||
*
|
||||
* For more information, please see XEP-0045.
|
||||
**/
|
||||
- (NSString *)role;
|
||||
- (NSString *)affiliation;
|
||||
|
||||
/**
|
||||
* If the MUC room is non-anonymous, the real JID of the user will be broadcast.
|
||||
*
|
||||
* An anonymous room uses JID's of the form: "room_name@conference.domain.tld/some_nickname".
|
||||
* A non-anonymous room also includes the occupants real full JID in the presence broadcast.
|
||||
**/
|
||||
- (XMPPJID *)realJID;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
#import "XMPPRoom.h"
|
||||
|
||||
|
||||
@interface XMPPRoom (PrivateInternalAPI)
|
||||
|
||||
/**
|
||||
* XMPPRoomStorage classes may optionally use the same delegate(s) as their parent XMPPRoom.
|
||||
* This method allows such storage classes to access the delegate(s).
|
||||
*
|
||||
* Note: If the storage class operates on a different queue than its parent,
|
||||
* it MUST dispatch all calls to the multicastDelegate onto its parent's queue.
|
||||
* The parent's dispatch queue is passed in the configureWithParent:queue: method,
|
||||
* or may be obtained via the moduleQueue method below.
|
||||
**/
|
||||
- (GCDMulticastDelegate *)multicastDelegate;
|
||||
|
||||
- (dispatch_queue_t)moduleQueue;
|
||||
|
||||
@end
|
||||
Reference in new issue
Block a user