- Update
This commit is contained in:
1 parent
d281d765ea
commit
5b074a5176
1261 files changed
+199158
-7303
No files matched your search
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Simple XMPP module that tracks average bandwidth of the xmpp stream.
|
||||
*
|
||||
* For now, this is a really simple module.
|
||||
* But perhaps in the future, as developers adapt this module,
|
||||
* they will open source their additions and improvements.
|
||||
**/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPP.h"
|
||||
|
||||
#define _XMPP_BANDWIDTH_MONITOR_H
|
||||
|
||||
@interface XMPPBandwidthMonitor : XMPPModule
|
||||
|
||||
@property (readonly) double outgoingBandwidth;
|
||||
@property (readonly) double incomingBandwidth;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,191 @@
|
||||
#import "XMPPBandwidthMonitor.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "NSXMLElement+XMPP.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_VERBOSE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
|
||||
@implementation XMPPBandwidthMonitor
|
||||
{
|
||||
dispatch_source_t timer;
|
||||
|
||||
uint64_t lastNumberOfBytesSent;
|
||||
uint64_t lastNumberOfBytesReceived;
|
||||
|
||||
double smoothedAverageOutgoingBandwidth;
|
||||
double smoothedAverageIncomingBandwidth;
|
||||
}
|
||||
|
||||
- (double)outgoingBandwidth
|
||||
{
|
||||
__block double result = 0.0;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = smoothedAverageOutgoingBandwidth;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (double)incomingBandwidth
|
||||
{
|
||||
__block double result = 0.0;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = smoothedAverageIncomingBandwidth;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)updateBandwidth
|
||||
{
|
||||
uint64_t currentNumberOfBytesSent = 0;
|
||||
uint64_t currentNumberOfBytesReceived = 0;
|
||||
|
||||
[xmppStream getNumberOfBytesSent:¤tNumberOfBytesSent numberOfBytesReceived:¤tNumberOfBytesReceived];
|
||||
|
||||
double currentOutgoingBandwidth = currentNumberOfBytesSent - lastNumberOfBytesSent; // Bytes per second
|
||||
double currentIncomingBandwidth = currentNumberOfBytesReceived - lastNumberOfBytesReceived; // Bytes per second
|
||||
|
||||
if (smoothedAverageOutgoingBandwidth || smoothedAverageIncomingBandwidth)
|
||||
{
|
||||
// Apply Brown's Simple Exponential Smoothing algorithm
|
||||
|
||||
double smoothingFactor = 0.3;
|
||||
|
||||
smoothedAverageOutgoingBandwidth = (smoothingFactor * currentOutgoingBandwidth)
|
||||
+ ((1 - smoothingFactor) * smoothedAverageOutgoingBandwidth);
|
||||
|
||||
smoothedAverageIncomingBandwidth = (smoothingFactor * currentIncomingBandwidth)
|
||||
+ ((1 - smoothingFactor) * smoothedAverageIncomingBandwidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No previous data
|
||||
|
||||
smoothedAverageOutgoingBandwidth = currentOutgoingBandwidth;
|
||||
smoothedAverageIncomingBandwidth = currentIncomingBandwidth;
|
||||
}
|
||||
|
||||
lastNumberOfBytesSent = currentNumberOfBytesSent;
|
||||
lastNumberOfBytesReceived = currentNumberOfBytesReceived;
|
||||
|
||||
XMPPLogVerbose(@"Bandwidth = Out(%.0f, %.0f) In(%.0f, %.0f)",
|
||||
currentOutgoingBandwidth, smoothedAverageOutgoingBandwidth,
|
||||
currentIncomingBandwidth, smoothedAverageIncomingBandwidth);
|
||||
}
|
||||
|
||||
- (void)startTimer
|
||||
{
|
||||
if (timer == NULL)
|
||||
{
|
||||
uint64_t numberOfBytesSent = 0;
|
||||
uint64_t numberOfBytesReceived = 0;
|
||||
|
||||
[xmppStream getNumberOfBytesSent:&numberOfBytesSent numberOfBytesReceived:&numberOfBytesReceived];
|
||||
|
||||
lastNumberOfBytesSent = numberOfBytesSent;
|
||||
lastNumberOfBytesReceived = numberOfBytesReceived;
|
||||
|
||||
smoothedAverageOutgoingBandwidth = 0.0;
|
||||
smoothedAverageIncomingBandwidth = 0.0;
|
||||
|
||||
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
|
||||
|
||||
dispatch_source_set_event_handler(timer, ^{ @autoreleasepool {
|
||||
|
||||
[self updateBandwidth];
|
||||
|
||||
}});
|
||||
|
||||
NSTimeInterval interval = 1.0; // Update every 1 second(s)
|
||||
|
||||
uint64_t intervalTime = interval * NSEC_PER_SEC;
|
||||
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, intervalTime);
|
||||
|
||||
dispatch_source_set_timer(timer, startTime, intervalTime, 0.25);
|
||||
dispatch_resume(timer);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopTimer
|
||||
{
|
||||
if (timer)
|
||||
{
|
||||
lastNumberOfBytesSent = 0;
|
||||
lastNumberOfBytesReceived = 0;
|
||||
|
||||
smoothedAverageOutgoingBandwidth = 0.0;
|
||||
smoothedAverageIncomingBandwidth = 0.0;
|
||||
|
||||
dispatch_source_cancel(timer);
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_release(timer);
|
||||
#endif
|
||||
timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)aXmppStream
|
||||
{
|
||||
if ([super activate:aXmppStream])
|
||||
{
|
||||
if ([xmppStream isConnected])
|
||||
{
|
||||
[self startTimer];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
|
||||
[self stopTimer];
|
||||
[super deactivate];
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPStream Delegate
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)xmppStreamDidStartNegotiation:(XMPPStream *)sender;
|
||||
{
|
||||
[self startTimer];
|
||||
}
|
||||
|
||||
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
|
||||
{
|
||||
[self stopTimer];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,167 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
/**
|
||||
* This class provides an optional base class that may be used to implement
|
||||
* a CoreDataStorage class for an xmpp extension (or perhaps any core data storage class).
|
||||
*
|
||||
* It operates on its own dispatch queue which allows it to easily provide storage for multiple extension instance.
|
||||
* More importantly, it smartly buffers its save operations to maximize performance!
|
||||
*
|
||||
* It does this using two techniques:
|
||||
*
|
||||
* First, it monitors the number of pending requests.
|
||||
* When a operation is requested of the class, it increments an atomic variable, and schedules the request.
|
||||
* After the request has been processed, it decrements the atomic variable.
|
||||
* At this point it knows if there are other pending requests,
|
||||
* and it uses the information to decide if it should save now,
|
||||
* or postpone the save operation until the pending requests have been executed.
|
||||
*
|
||||
* Second, it monitors the number of unsaved changes.
|
||||
* Since NSManagedObjectContext retains any changed objects until they are saved to disk
|
||||
* it is an important memory management concern to keep the number of changed objects within a healthy range.
|
||||
* This class uses a configurable saveThreshold to save at appropriate times.
|
||||
*
|
||||
* This class also offers several useful features such as
|
||||
* preventing multiple instances from using the same database file (conflict)
|
||||
* and caching of xmppStream.myJID to improve performance.
|
||||
*
|
||||
* For more information on how to extend this class,
|
||||
* please see the XMPPCoreDataStorageProtected.h header file.
|
||||
*
|
||||
* The framework comes with several classes that extend this base class such as:
|
||||
* - XMPPRosterCoreDataStorage (Extensions/Roster)
|
||||
* - XMPPCapabilitiesCoreDataStorage (Extensions/XEP-0115)
|
||||
* - XMPPvCardCoreDataStorage (Extensions/XEP-0054)
|
||||
*
|
||||
* Feel free to skim over these as reference implementations.
|
||||
**/
|
||||
|
||||
@interface XMPPCoreDataStorage : NSObject {
|
||||
@private
|
||||
|
||||
NSMutableDictionary *myJidCache;
|
||||
|
||||
int32_t pendingRequests;
|
||||
|
||||
NSManagedObjectModel *managedObjectModel;
|
||||
NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
NSManagedObjectContext *managedObjectContext;
|
||||
NSManagedObjectContext *mainThreadManagedObjectContext;
|
||||
|
||||
NSMutableArray *willSaveManagedObjectContextBlocks;
|
||||
NSMutableArray *didSaveManagedObjectContextBlocks;
|
||||
|
||||
@protected
|
||||
|
||||
NSString *databaseFileName;
|
||||
NSDictionary *storeOptions;
|
||||
NSUInteger saveThreshold;
|
||||
NSUInteger saveCount;
|
||||
|
||||
BOOL autoRemovePreviousDatabaseFile;
|
||||
BOOL autoRecreateDatabaseFile;
|
||||
BOOL autoAllowExternalBinaryDataStorage;
|
||||
|
||||
dispatch_queue_t storageQueue;
|
||||
void *storageQueueTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a core data storage instance, backed by SQLite, with the given database store filename.
|
||||
* It is recommended your database filname use the "sqlite" file extension (e.g. "XMPPRoster.sqlite").
|
||||
* If you pass nil, a default database filename is automatically used.
|
||||
* This default is derived from the classname,
|
||||
* meaning subclasses will get a default database filename derived from the subclass classname.
|
||||
*
|
||||
* If you attempt to create an instance of this class with the same databaseFileName as another existing instance,
|
||||
* this method will return nil.
|
||||
**/
|
||||
- (id)initWithDatabaseFilename:(NSString *)databaseFileName storeOptions:(NSDictionary *)storeOptions;
|
||||
|
||||
/**
|
||||
* Initializes a core data storage instance, backed by an in-memory store.
|
||||
**/
|
||||
- (id)initWithInMemoryStore;
|
||||
|
||||
/**
|
||||
* Readonly access to the databaseFileName used during initialization.
|
||||
* If nil was passed to the init method, returns the actual databaseFileName being used (the default filename).
|
||||
**/
|
||||
@property (readonly) NSString *databaseFileName;
|
||||
|
||||
/**
|
||||
* Readonly access to the databaseOptions used during initialization.
|
||||
* If nil was passed to the init method, returns the actual databaseOptions being used (the default databaseOptions).
|
||||
**/
|
||||
@property (readonly) NSDictionary *storeOptions;
|
||||
|
||||
/**
|
||||
* The saveThreshold specifies the maximum number of unsaved changes to NSManagedObjects before a save is triggered.
|
||||
*
|
||||
* Since NSManagedObjectContext retains any changed objects until they are saved to disk
|
||||
* it is an important memory management concern to keep the number of changed objects within a healthy range.
|
||||
*
|
||||
* Default 500
|
||||
**/
|
||||
@property (readwrite) NSUInteger saveThreshold;
|
||||
|
||||
/**
|
||||
* Provides access to the the thread-safe components of the CoreData stack.
|
||||
*
|
||||
* Please note:
|
||||
* The managedObjectContext is private to the storageQueue.
|
||||
* If you're on the main thread you can use the mainThreadManagedObjectContext.
|
||||
* Otherwise you must create and use your own managedObjectContext.
|
||||
*
|
||||
* If you think you can simply add a property for the private managedObjectContext,
|
||||
* then you need to go read the documentation for core data,
|
||||
* specifically the section entitled "Concurrency with Core Data".
|
||||
*
|
||||
* @see mainThreadManagedObjectContext
|
||||
**/
|
||||
@property (strong, readonly) NSManagedObjectModel *managedObjectModel;
|
||||
@property (strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
|
||||
/**
|
||||
* Convenience method to get a managedObjectContext appropriate for use on the main thread.
|
||||
* This context should only be used from the main thread.
|
||||
*
|
||||
* NSManagedObjectContext is a light-weight thread-UNsafe component of the CoreData stack.
|
||||
* Thus a managedObjectContext should only be accessed from a single thread, or from a serialized queue.
|
||||
*
|
||||
* A managedObjectContext is associated with a persistent store.
|
||||
* In most cases the persistent store is an sqlite database file.
|
||||
* So think of a managedObjectContext as a thread-specific cache for the underlying database.
|
||||
*
|
||||
* This method lazily creates a proper managedObjectContext,
|
||||
* associated with the persistent store of this instance,
|
||||
* and configured to automatically merge changesets from other threads.
|
||||
**/
|
||||
@property (strong, readonly) NSManagedObjectContext *mainThreadManagedObjectContext;
|
||||
|
||||
/**
|
||||
* The Previous Database File is removed before creating a persistant store.
|
||||
*
|
||||
* Default NO
|
||||
**/
|
||||
|
||||
@property (readwrite) BOOL autoRemovePreviousDatabaseFile;
|
||||
|
||||
/**
|
||||
* The Database File is automatically recreated if the persistant store cannot read it e.g. the model changed or the file became corrupt.
|
||||
* For greater control overide didNotAddPersistentStoreWithPath:
|
||||
*
|
||||
* Default NO
|
||||
**/
|
||||
@property (readwrite) BOOL autoRecreateDatabaseFile;
|
||||
|
||||
/**
|
||||
* This method calls setAllowsExternalBinaryDataStorage:YES for all Binary Data Attributes in the Managed Object Model.
|
||||
* On OS Versions that do not support external binary data storage, this property does nothing.
|
||||
*
|
||||
* Default NO
|
||||
**/
|
||||
@property (readwrite) BOOL autoAllowExternalBinaryDataStorage;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,334 @@
|
||||
#import "XMPPCoreDataStorage.h"
|
||||
|
||||
@class XMPPJID;
|
||||
@class XMPPStream;
|
||||
|
||||
/**
|
||||
* The methods in this class are to be used ONLY by subclasses of XMPPCoreDataStorage.
|
||||
**/
|
||||
|
||||
@interface XMPPCoreDataStorage (Protected)
|
||||
|
||||
#pragma mark Override Me
|
||||
|
||||
/**
|
||||
* If your subclass needs to do anything for init, it can do so easily by overriding this method.
|
||||
* All public init methods will invoke this method at the end of their implementation.
|
||||
*
|
||||
* Important: If overriden you must invoke [super commonInit] at some point.
|
||||
**/
|
||||
- (void)commonInit;
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* This method is queried to get the name of the ManagedObjectModel within a 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;
|
||||
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* This method is queried to get the bundle containing the ManagedObjectModel.
|
||||
**/
|
||||
- (NSBundle *)managedObjectModelBundle;
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* This method is queried if the initWithDatabaseFileName:storeOptions: method is invoked with a nil parameter for databaseFileName.
|
||||
* The default implementation returns:
|
||||
*
|
||||
* [NSString stringWithFormat:@"%@.sqlite", [self managedObjectModelName]];
|
||||
*
|
||||
* You are encouraged to use the sqlite file extension.
|
||||
**/
|
||||
- (NSString *)defaultDatabaseFileName;
|
||||
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* This method is queried if the initWithDatabaseFileName:storeOptions method is invoked with a nil parameter for storeOptions.
|
||||
* The default implementation returns the following:
|
||||
*
|
||||
* @{ NSMigratePersistentStoresAutomaticallyOption: @(YES),
|
||||
* NSInferMappingModelAutomaticallyOption : @(YES) };
|
||||
**/
|
||||
- (NSDictionary *)defaultStoreOptions;
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* If you are using a database file with pure non-persistent data (e.g. for memory optimization purposes on iOS),
|
||||
* you may want to delete the database file if it already exists on disk.
|
||||
*
|
||||
* If this instance was created via initWithDatabaseFilename, then the storePath parameter will be non-nil.
|
||||
* If this instance was created via initWithInMemoryStore, then the storePath parameter will be nil.
|
||||
*
|
||||
* The default implementation does nothing.
|
||||
**/
|
||||
- (void)willCreatePersistentStoreWithPath:(NSString *)storePath options:(NSDictionary *)storeOptions;
|
||||
|
||||
/**
|
||||
* Override me, if needed, to completely customize the persistent store.
|
||||
*
|
||||
* Adds the persistent store path to the persistent store coordinator.
|
||||
* Returns true if the persistent store is created.
|
||||
*
|
||||
* If this instance was created via initWithDatabaseFilename, then the storePath parameter will be non-nil.
|
||||
* If this instance was created via initWithInMemoryStore, then the storePath parameter will be nil.
|
||||
**/
|
||||
- (BOOL)addPersistentStoreWithPath:(NSString *)storePath options:(NSDictionary *)storeOptions error:(NSError **)errorPtr;
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* For example, if you are using the database for non-persistent data and the model changes, you may want
|
||||
* to delete the database file if it already exists on disk and a core data migration is not worthwhile.
|
||||
*
|
||||
* If this instance was created via initWithDatabaseFilename, then the storePath parameter will be non-nil.
|
||||
* If this instance was created via initWithInMemoryStore, then the storePath parameter will be nil.
|
||||
*
|
||||
* The default implementation simply writes to the XMPP error log.
|
||||
**/
|
||||
- (void)didNotAddPersistentStoreWithPath:(NSString *)storePath options:(NSDictionary *)storeOptions error:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Override me, if needed, to provide customized behavior.
|
||||
*
|
||||
* For example, you may want to perform cleanup of any non-persistent data before you start using the database.
|
||||
*
|
||||
* The default implementation does nothing.
|
||||
**/
|
||||
- (void)didCreateManagedObjectContext;
|
||||
|
||||
/**
|
||||
* Override me if you need to do anything special just before changes are saved to disk.
|
||||
*
|
||||
* This method will be invoked on the storageQueue.
|
||||
* The default implementation does nothing.
|
||||
**/
|
||||
- (void)willSaveManagedObjectContext;
|
||||
|
||||
/**
|
||||
* Override me if you need to do anything special after changes have been saved to disk.
|
||||
*
|
||||
* This method will be invoked on the storageQueue.
|
||||
* The default implementation does nothing.
|
||||
**/
|
||||
- (void)didSaveManagedObjectContext;
|
||||
|
||||
/**
|
||||
* This method will be invoked on the main thread,
|
||||
* after the mainThreadManagedObjectContext has merged changes from another context.
|
||||
*
|
||||
* This method may be useful if you have code dependent upon when changes the datastore hit the user interface.
|
||||
* For example, you want to play a sound when a message is received.
|
||||
* You could play the sound right away, from the background queue, but the timing may be slightly off because
|
||||
* the user interface won't update til the changes have been saved to disk,
|
||||
* and then propogated to the managedObjectContext of the main thread.
|
||||
* Alternatively you could set a flag, and then hook into this method
|
||||
* to play the sound at the exact moment the propogation hits the main thread.
|
||||
*
|
||||
* The default implementation does nothing.
|
||||
**/
|
||||
- (void)mainThreadManagedObjectContextDidMergeChanges;
|
||||
|
||||
#pragma mark Setup
|
||||
|
||||
/**
|
||||
* This is the standard configure method used by xmpp extensions to configure a storage class.
|
||||
*
|
||||
* Feel free to override this method if needed,
|
||||
* and just invoke super at some point to make sure everything is kosher at this level as well.
|
||||
*
|
||||
* Note that the default implementation allows the storage class to be used by multiple xmpp streams.
|
||||
* If you design your storage class to be used by a single stream, then you should implement this method
|
||||
* to ensure that your class can only be configured by one parent.
|
||||
* If you do, again, don't forget to invoke super at some point.
|
||||
**/
|
||||
- (BOOL)configureWithParent:(id)aParent queue:(dispatch_queue_t)queue;
|
||||
|
||||
#pragma mark Stream JID caching
|
||||
|
||||
/**
|
||||
* This class provides a caching service for xmppStream.myJID to avoid constantly querying the xmppStream for it.
|
||||
*
|
||||
* The motivation behind this is the fact that to query the xmppStream for its myJID
|
||||
* requires going through the xmppStream's internal dispatch queue. (A dispatch_sync).
|
||||
* It's not necessarily that this is an expensive operation,
|
||||
* but storage classes sometimes require this information for just about every operation they perform.
|
||||
* For a variable that changes infrequently, caching the value can reduce some overhead.
|
||||
* In addition, if we can stay out of xmppStream's internal dispatch queue,
|
||||
* we free it to perform more xmpp processing tasks.
|
||||
*
|
||||
* If the xmppStream.myJID changes, the cache will automatically be updated.
|
||||
*
|
||||
* If you store any variant of xmppStream.myJID (bare, full, domain, etc) in your database
|
||||
* you are strongly encouraged to use the caching service.
|
||||
*
|
||||
* For example, say you're implementing a core data storage caching mechanism for Private XML Storage (XEP-0049).
|
||||
* The data you're caching is explictly tied to the stream's bare myJID. ([xmppStream.myJID bare])
|
||||
* You want your storage class to support multiple xmpp streams,
|
||||
* so you add a field to the database called streamBareJidStr (or whatever).
|
||||
* Given an xmppStream, you can use the built-in cache to quickly get the xmppStream.myJid property:
|
||||
*
|
||||
* [self myJidForXMPPStream:stream]
|
||||
*
|
||||
* This method will retrieve the myJID property of the given xmppStream the first time,
|
||||
* and then cache it for future lookups. The cache is automatically updated if the xmppStream.myJID ever changes.
|
||||
**/
|
||||
- (XMPPJID *)myJIDForXMPPStream:(XMPPStream *)stream;
|
||||
|
||||
/**
|
||||
* This method is invoked if the cached myJID changes for a particular xmpp stream.
|
||||
*
|
||||
* This method works in correlation with the myJIDForXMPPStream method.
|
||||
* In other words, calling myJIDForXMPPStream will cache the value.
|
||||
* If that value later changes, this method is invoked.
|
||||
*
|
||||
* So if the myJID of an xmpp stream changes, but there was no cached value for that xmpp stream,
|
||||
* then this method is never called. E.g. this method is only called for streams we're actually interested in.
|
||||
*
|
||||
* You may wish to override this method if your storage class prefetches data related to the current user.
|
||||
*
|
||||
* This method will be invoked on the storageQueue.
|
||||
* The default implementation does nothing.
|
||||
**/
|
||||
- (void)didChangeCachedMyJID:(XMPPJID *)cachedMyJID forXMPPStream:(XMPPStream *)stream;
|
||||
|
||||
#pragma mark Core Data
|
||||
|
||||
/**
|
||||
* The standard persistentStoreDirectory method.
|
||||
**/
|
||||
- (NSString *)persistentStoreDirectory;
|
||||
|
||||
/**
|
||||
* Provides access to the managedObjectContext.
|
||||
*
|
||||
* Keep in mind that NSManagedObjectContext is NOT thread-safe.
|
||||
* So you can ONLY access this property from within the context of the storageQueue.
|
||||
*
|
||||
* Important:
|
||||
* The primary purpose of this class is to optimize disk IO by buffering save operations to the managedObjectContext.
|
||||
* It does this using the methods outlined in the 'Performance Optimizations' section below.
|
||||
* If you manually save the managedObjectContext you are destroying these optimizations.
|
||||
* See the documentation for executeBlock & scheduleBlock below for proper usage surrounding the optimizations.
|
||||
**/
|
||||
@property (readonly) NSManagedObjectContext *managedObjectContext;
|
||||
|
||||
#pragma mark Performance Optimizations
|
||||
|
||||
/**
|
||||
* Queries the managedObjectContext to determine the number of unsaved managedObjects.
|
||||
**/
|
||||
- (NSUInteger)numberOfUnsavedChanges;
|
||||
|
||||
/**
|
||||
* You will not often need to manually call this method.
|
||||
* It is called automatically, at appropriate and optimized times, via the executeBlock and scheduleBlock methods.
|
||||
*
|
||||
* The one exception to this is when you are inserting/deleting/updating a large number of objects in a loop.
|
||||
* It is recommended that you invoke save from within the loop.
|
||||
* E.g.:
|
||||
*
|
||||
* NSUInteger unsavedCount = [self numberOfUnsavedChanges];
|
||||
* for (NSManagedObject *obj in fetchResults)
|
||||
* {
|
||||
* [[self managedObjectContext] deleteObject:obj];
|
||||
*
|
||||
* if (++unsavedCount >= saveThreshold)
|
||||
* {
|
||||
* [self save];
|
||||
* unsavedCount = 0;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* See also the documentation for executeBlock and scheduleBlock below.
|
||||
**/
|
||||
- (void)save; // Read the comments above !
|
||||
|
||||
/**
|
||||
* You will rarely need to manually call this method.
|
||||
* It is called automatically, at appropriate and optimized times, via the executeBlock and scheduleBlock methods.
|
||||
*
|
||||
* This method makes informed decisions as to whether it should save the managedObjectContext changes to disk.
|
||||
* Since this disk IO is a slow process, it is better to buffer writes during high demand.
|
||||
* This method takes into account the number of pending requests waiting on the storage instance,
|
||||
* as well as the number of unsaved changes (which reside in NSManagedObjectContext's internal memory).
|
||||
*
|
||||
* Please see the documentation for executeBlock and scheduleBlock below.
|
||||
**/
|
||||
- (void)maybeSave; // Read the comments above !
|
||||
|
||||
/**
|
||||
* This method synchronously invokes the given block (dispatch_sync) on the storageQueue.
|
||||
*
|
||||
* Prior to dispatching the block it increments (atomically) the number of pending requests.
|
||||
* After the block has been executed, it decrements (atomically) the number of pending requests,
|
||||
* and then invokes the maybeSave method which implements the logic behind the optimized disk IO.
|
||||
*
|
||||
* If you use the executeBlock and scheduleBlock methods for all your database operations,
|
||||
* you will automatically inherit optimized disk IO for free.
|
||||
*
|
||||
* If you manually invoke [managedObjectContext save:] you are destroying the optimizations provided by this class.
|
||||
*
|
||||
* The block handed to this method is automatically wrapped in a NSAutoreleasePool,
|
||||
* so there is no need to create these yourself as this method automatically handles it for you.
|
||||
*
|
||||
* The architecture of this class purposefully puts the CoreDataStorage instance on a separate dispatch_queue
|
||||
* from the parent XmppExtension. Not only does this allow a single storage instance to service multiple extension
|
||||
* instances, but it provides the mechanism for the disk IO optimizations. The theory behind the optimizations
|
||||
* is to delay a save of the data (a slow operation) until the storage class is no longer being used. With xmpp
|
||||
* it is often the case that a burst of data causes a flurry of queries and/or updates for a storage class.
|
||||
* Thus the theory is to delay the slow save operation until later when the flurry has ended and the storage
|
||||
* class no longer has any pending requests.
|
||||
*
|
||||
* This method is designed to be invoked from within the XmppExtension storage protocol methods.
|
||||
* In other words, it is expecting to be invoked from a dispatch_queue other than the storageQueue.
|
||||
* If you attempt to invoke this method from within the storageQueue, an exception is thrown.
|
||||
* Therefore care should be taken when designing your implementation.
|
||||
* The recommended procedure is as follows:
|
||||
*
|
||||
* All of the methods that implement the XmppExtension storage protocol invoke either executeBlock or scheduleBlock.
|
||||
* However, none of these methods invoke each other (they are only to be invoked from the XmppExtension instance).
|
||||
* Instead, create internal utility methods that may be invoked.
|
||||
*
|
||||
* For an example, see the XMPPRosterCoreDataStorage implementation's _userForJID:xmppStream: method.
|
||||
**/
|
||||
- (void)executeBlock:(dispatch_block_t)block;
|
||||
|
||||
/**
|
||||
* This method asynchronously invokes the given block (dispatch_async) on the storageQueue.
|
||||
*
|
||||
* It works very similarly to the executeBlock method.
|
||||
* See the executeBlock method above for a full discussion.
|
||||
**/
|
||||
- (void)scheduleBlock:(dispatch_block_t)block;
|
||||
|
||||
/**
|
||||
* Sometimes you want to call a method before calling save on a Managed Object Context e.g. willSaveObject:
|
||||
*
|
||||
* addWillSaveManagedObjectContextBlock allows you to add a block of code to be called before saving a Managed Object Context,
|
||||
* without the overhead of having to call save at that moment.
|
||||
**/
|
||||
- (void)addWillSaveManagedObjectContextBlock:(void (^)(void))willSaveBlock;
|
||||
|
||||
/**
|
||||
* Sometimes you want to call a method after calling save on a Managed Object Context e.g. didSaveObject:
|
||||
*
|
||||
* addDidSaveManagedObjectContextBlock allows you to add a block of code to be after saving a Managed Object Context,
|
||||
* without the overhead of having to call save at that moment.
|
||||
**/
|
||||
- (void)addDidSaveManagedObjectContextBlock:(void (^)(void))didSaveBlock;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// Created by Jonathon Staff on 10/21/14.
|
||||
// Copyright (c) 2014 nplexity, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPP.h"
|
||||
#import "XMPPModule.h"
|
||||
#import "TURNSocket.h"
|
||||
#import "GCDAsyncSocket.h"
|
||||
|
||||
@class XMPPIDTracker;
|
||||
|
||||
typedef NS_OPTIONS(uint8_t, XMPPFileTransferStreamMethod) {
|
||||
XMPPFileTransferStreamMethodBytestreams = 1 << 0, // If set, SOCKS5 connections allowed
|
||||
XMPPFileTransferStreamMethodIBB = 1 << 1, // If set, IBB connections allowed
|
||||
// XMPPFileTransferStreamMethodJingle = 1 << 2 // If set, Jingle connections allowed
|
||||
// Note that Jingle is not yet implemented
|
||||
};
|
||||
|
||||
/**
|
||||
* This class defines common elements of the file transfer process that apply to
|
||||
* both outgoing and incoming transfers.
|
||||
*
|
||||
* You'll find more detailed documentation in each of the implementation files.
|
||||
*
|
||||
* By default, the stream-method priority is as follows:
|
||||
*
|
||||
* 1. SOCKS5 Direct Connection (http://xmpp.org/extensions/xep-0065.html#direct)
|
||||
* 2. SOCKS5 Mediated (http://xmpp.org/extensions/xep-0065.html#mediated)
|
||||
* 3. IBB (http://xmpp.org/extensions/xep-0047.html)
|
||||
*/
|
||||
@interface XMPPFileTransfer : XMPPModule <GCDAsyncSocketDelegate> {
|
||||
XMPPFileTransferStreamMethod _streamMethods;
|
||||
XMPPIDTracker *_idTracker;
|
||||
GCDAsyncSocket *_asyncSocket;
|
||||
NSMutableArray *_streamhosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* The streamID ("sid") for the file transfer.
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *sid;
|
||||
|
||||
/**
|
||||
* Use this to disable file transfers via direct connection.
|
||||
*
|
||||
* If set to YES, SOCKS5 transfers will only take place if there is a proxy that
|
||||
* works. If set to NO, SOCKS5 transfers will attempt a direct connection first
|
||||
* and fall back to a proxy if the direct connection doesn't work.
|
||||
*
|
||||
* The default value is NO.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL disableDirectTransfers;
|
||||
|
||||
/**
|
||||
* Use this to disable file transfers via SOCKS5.
|
||||
*
|
||||
* If set to YES, SOCKS5 transfers will not be used. This means that the
|
||||
* recipient must support IBB transfers or the transfer will fail. If set to NO,
|
||||
* a SOCKS5 connection will be attempted first, since this should be the
|
||||
* preferred method of transfer.
|
||||
*
|
||||
* The default value is NO.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL disableSOCKS5;
|
||||
|
||||
/**
|
||||
* Use this to disable file transfers via IBB.
|
||||
*
|
||||
* If set to YES, IBB transfers will not be used. This means that the
|
||||
* recipient must support SOCKS5 transfers or the transfer will fail. If set to
|
||||
* NO, a SOCKS5 connection will be attempted first, since this should be the
|
||||
* preferred method of transfer.
|
||||
*
|
||||
* The default value is NO.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL disableIBB;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Created by Jonathon Staff on 10/21/14.
|
||||
// Copyright (c) 2014 nplexity, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "XMPPFileTransfer.h"
|
||||
|
||||
|
||||
@implementation XMPPFileTransfer
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// Created by Jonathon Staff on 10/21/14.
|
||||
// Copyright (c) 2014 nplexity, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPFileTransfer.h"
|
||||
|
||||
@class XMPPIQ;
|
||||
|
||||
@interface XMPPIncomingFileTransfer : XMPPFileTransfer
|
||||
|
||||
/**
|
||||
* (Optional)
|
||||
*
|
||||
* Specifies whether or not file transfers should automatically be accepted. If
|
||||
* set to YES, you will be notified of an incoming Stream Initiation Offer, but
|
||||
* it will be accepted for you.
|
||||
*
|
||||
* The default value is NO.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL autoAcceptFileTransfers;
|
||||
|
||||
/**
|
||||
* Sends a response to the file transfer initiator accepting the Stream
|
||||
* Initiation offer. It will automatically determine the best transfer method
|
||||
* (either SOCKS5 or IBB) based on what the sender offers as options.
|
||||
*
|
||||
* If you've set autoAcceptFileTransfers to YES, this method will be invoked for
|
||||
* you automatically.
|
||||
*
|
||||
* @param offer IQ stanza representing the SI offer (this should be provided by
|
||||
* the delegate to you).
|
||||
*/
|
||||
- (void)acceptSIOffer:(XMPPIQ *)offer;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - XMPPIncomingFileTransferDelegate
|
||||
|
||||
@protocol XMPPIncomingFileTransferDelegate
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Implement this method to receive notifications of a failed incoming file
|
||||
* transfer.
|
||||
*
|
||||
* @param sender XMPPIncomingFileTransfer object invoking this delegate method.
|
||||
* @param error NSError containing more details of the failure.
|
||||
*/
|
||||
- (void)xmppIncomingFileTransfer:(XMPPIncomingFileTransfer *)sender
|
||||
didFailWithError:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Implement this method to receive notification of an incoming Stream
|
||||
* Initiation offer. Keep in mind that if you haven't set
|
||||
* autoAcceptFileTransfers to YES, then it will be your responsibility to call
|
||||
* acceptSIOffer: using the sender and offer provided to you.
|
||||
*
|
||||
* @param sender XMPPIncomingFileTransfer object invoking this delegate method.
|
||||
* @param offer IQ stanza containing a Stream Initiation offer.
|
||||
*/
|
||||
- (void)xmppIncomingFileTransfer:(XMPPIncomingFileTransfer *)sender
|
||||
didReceiveSIOffer:(XMPPIQ *)offer;
|
||||
|
||||
/**
|
||||
* Implement this method to receive notifications of a successful incoming file
|
||||
* transfer. It will only be invoked if all of the data is received
|
||||
* successfully.
|
||||
*
|
||||
* @param sender XMPPIncomingFileTransfer object invoking this delegate method.
|
||||
* @param data NSData for you to handle (probably save this or display it).
|
||||
* @param named Name of the file you just received.
|
||||
*/
|
||||
- (void)xmppIncomingFileTransfer:(XMPPIncomingFileTransfer *)sender
|
||||
didSucceedWithData:(NSData *)data
|
||||
named:(NSString *)name;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,960 @@
|
||||
//
|
||||
// Created by Jonathon Staff on 10/21/14.
|
||||
// Copyright (c) 2014 nplexity, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#import "XMPPIncomingFileTransfer.h"
|
||||
#import "XMPPConstants.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "idn-int.h"
|
||||
#import "NSNumber+XMPP.h"
|
||||
#import "NSData+XMPP.h"
|
||||
|
||||
#if DEBUG
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // XMPP_LOG_LEVEL_VERBOSE | XMPP_LOG_FLAG_TRACE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Tags for _asyncSocket handling.
|
||||
*/
|
||||
#define SOCKS_TAG_WRITE_METHOD 101
|
||||
#define SOCKS_TAG_READ_METHOD 102
|
||||
#define SOCKS_TAG_WRITE_CONNECT 103
|
||||
#define SOCKS_TAG_READ_REPLY 104
|
||||
#define SOCKS_TAG_READ_ADDRESS 105
|
||||
#define SOCKS_TAG_READ_DATA 106
|
||||
|
||||
#define TIMEOUT_WRITE -1
|
||||
#define TIMEOUT_READ 5.0
|
||||
|
||||
// XMPP Incoming File Transfer State
|
||||
typedef NS_ENUM(int, XMPPIFTState) {
|
||||
XMPPIFTStateNone,
|
||||
XMPPIFTStateWaitingForSIOffer,
|
||||
XMPPIFTStateWaitingForStreamhosts,
|
||||
XMPPIFTStateConnectingToStreamhosts,
|
||||
XMPPIFTStateConnected,
|
||||
XMPPIFTStateWaitingForIBBOpen,
|
||||
XMPPIFTStateWaitingForIBBData
|
||||
};
|
||||
|
||||
NSString *const XMPPIncomingFileTransferErrorDomain = @"XMPPIncomingFileTransferErrorDomain";
|
||||
|
||||
@interface XMPPIncomingFileTransfer () {
|
||||
XMPPIFTState _transferState;
|
||||
|
||||
XMPPJID *_senderJID;
|
||||
|
||||
NSString *_streamhostsQueryId;
|
||||
NSString *_streamhostUsed;
|
||||
|
||||
NSMutableData *_receivedData;
|
||||
NSString *_receivedFileName;
|
||||
NSUInteger _totalDataSize;
|
||||
NSUInteger _receivedDataSize;
|
||||
|
||||
dispatch_source_t _ibbTimer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation XMPPIncomingFileTransfer
|
||||
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
self = [super initWithDispatchQueue:queue];
|
||||
if (self) {
|
||||
_transferState = XMPPIFTStateNone;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard deconstructor.
|
||||
*/
|
||||
- (void)dealloc
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (_transferState != XMPPIFTStateNone) {
|
||||
XMPPLogWarn(@"%@: Deallocating prior to completion or cancellation.", THIS_FILE);
|
||||
}
|
||||
|
||||
if (_ibbTimer)
|
||||
dispatch_source_cancel(_ibbTimer);
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_release(_ibbTimer);
|
||||
#endif
|
||||
_ibbTimer = NULL;
|
||||
|
||||
if (_asyncSocket.delegate == self) {
|
||||
[_asyncSocket setDelegate:nil delegateQueue:NULL];
|
||||
[_asyncSocket disconnect];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
/**
|
||||
* Public facing method for accepting a SI offer. If autoAcceptFileTransfers is
|
||||
* set to YES, this method will do nothing, since the internal method is invoked
|
||||
* automatically.
|
||||
*
|
||||
* @see sendSIOfferAcceptance:
|
||||
*/
|
||||
- (void)acceptSIOffer:(XMPPIQ *)offer
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (!_autoAcceptFileTransfers) {
|
||||
[self sendSIOfferAcceptance:offer];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
/**
|
||||
* This method will send the device's identity in response to a `disco#info`
|
||||
* query. In our case, we will send something close the following:
|
||||
*
|
||||
* <iq type="result"
|
||||
* id="purplea2da8fc9"
|
||||
* from="mephisto@sanctuary.org/kurast"
|
||||
* to="baal@sanctuary.org/worldstonechamber">
|
||||
* <query xmlns="http://jabber.org/protocol/disco#info">
|
||||
* <identity category="client" type="phone"/>
|
||||
* <feature var="http://jabber.org/protocol/si"/>
|
||||
* <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
||||
* <feature var="http://jabber.org/protocol/bytestreams"/>
|
||||
* <feature var="http://jabber.org/protocol/ibb"/>
|
||||
* </query>
|
||||
* </iq>
|
||||
*
|
||||
* This tells the requester who they're dealing with and which transfer types
|
||||
* we support. If there's a better way than hard-coding these values, I'm open
|
||||
* to suggestions.
|
||||
*/
|
||||
- (void)sendIdentity:(XMPPIQ *)request
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
|
||||
to:request.from
|
||||
elementID:request.elementID];
|
||||
[iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.full];
|
||||
|
||||
NSXMLElement
|
||||
*query = [NSXMLElement elementWithName:@"query" xmlns:XMPPDiscoInfoNamespace];
|
||||
|
||||
NSXMLElement *identity = [NSXMLElement elementWithName:@"identity"];
|
||||
[identity addAttributeWithName:@"category" stringValue:@"client"];
|
||||
[identity addAttributeWithName:@"type" stringValue:@"ios-osx"];
|
||||
[query addChild:identity];
|
||||
|
||||
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature addAttributeWithName:@"var" stringValue:XMPPSINamespace];
|
||||
[query addChild:feature];
|
||||
|
||||
NSXMLElement *feature1 = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature1 addAttributeWithName:@"var" stringValue:XMPPSIProfileFileTransferNamespace];
|
||||
[query addChild:feature1];
|
||||
|
||||
if (!self.disableSOCKS5) {
|
||||
NSXMLElement *feature2 = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature2 addAttributeWithName:@"var" stringValue:XMPPBytestreamsNamespace];
|
||||
[query addChild:feature2];
|
||||
}
|
||||
|
||||
if (!self.disableIBB) {
|
||||
NSXMLElement *feature3 = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature3 addAttributeWithName:@"var" stringValue:XMPPIBBNamespace];
|
||||
[query addChild:feature3];
|
||||
}
|
||||
|
||||
[iq addChild:query];
|
||||
[xmppStream sendElement:iq];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will send an IQ stanza accepting the SI offer. We need to choose
|
||||
* which 'stream-method' we prefer to use. For now, we will be using IBB as the
|
||||
* 'stream-method', but SOCKS5 is preferable.
|
||||
*
|
||||
* Take a look at XEP-0096 Examples 2 and 4 for more details.
|
||||
*/
|
||||
- (void)sendSIOfferAcceptance:(XMPPIQ *)offer
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
// Store the sender's JID
|
||||
_senderJID = offer.from;
|
||||
|
||||
// Store the sid for later use
|
||||
NSXMLElement *inSi = offer.childElement;
|
||||
self.sid = [inSi attributeStringValueForName:@"id"];
|
||||
|
||||
// Store the size of the incoming data for later use
|
||||
NSXMLElement *inFile = [inSi elementForName:@"file"];
|
||||
_totalDataSize = [inFile attributeUnsignedIntegerValueForName:@"size"];
|
||||
|
||||
// Store the name of the file for later use
|
||||
_receivedFileName = [inFile attributeStringValueForName:@"name"];
|
||||
|
||||
// Outgoing
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
|
||||
to:offer.from
|
||||
elementID:offer.elementID];
|
||||
|
||||
|
||||
NSXMLElement *si = [NSXMLElement elementWithName:@"si" xmlns:XMPPSINamespace];
|
||||
|
||||
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"
|
||||
xmlns:XMPPFeatureNegNamespace];
|
||||
|
||||
NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"];
|
||||
[x addAttributeWithName:@"type" stringValue:@"submit"];
|
||||
|
||||
NSXMLElement *field = [NSXMLElement elementWithName:@"field"];
|
||||
[field addAttributeWithName:@"var" stringValue:@"stream-method"];
|
||||
|
||||
NSXMLElement *value = [NSXMLElement elementWithName:@"value"];
|
||||
|
||||
// Prefer SOCKS5 if it's not disabled.
|
||||
if (!self.disableSOCKS5) {
|
||||
[value setStringValue:XMPPBytestreamsNamespace];
|
||||
_transferState = XMPPIFTStateWaitingForStreamhosts;
|
||||
} else {
|
||||
[value setStringValue:XMPPIBBNamespace];
|
||||
_transferState = XMPPIFTStateWaitingForIBBOpen;
|
||||
}
|
||||
|
||||
[field addChild:value];
|
||||
[x addChild:field];
|
||||
[feature addChild:x];
|
||||
[si addChild:feature];
|
||||
[iq addChild:si];
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - IBB Methods
|
||||
|
||||
/**
|
||||
* This method will send an IQ stanza accepting the IBB request. See XEP-0047
|
||||
* Example 2 for more details.
|
||||
*/
|
||||
- (void)sendIBBAcceptance:(XMPPIQ *)request
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
|
||||
to:request.from
|
||||
elementID:request.elementID];
|
||||
[xmppStream sendElement:iq];
|
||||
|
||||
// Prepare to receive data
|
||||
_receivedData = [NSMutableData new];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for reading the incoming data from the IQ stanza
|
||||
* and writing it to the member variable '_receivedData'. After successfully
|
||||
* reading the data, a response (XEP-0047 Example 7) will be sent back to the
|
||||
* sender.
|
||||
*/
|
||||
- (void)processReceivedIBBDataIQ:(XMPPIQ *)received
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
// Handle the scenario that the transfer is cancelled.
|
||||
[self resetIBBTimer:20];
|
||||
|
||||
// Handle incoming data
|
||||
NSXMLElement *dataElem = received.childElement;
|
||||
NSData
|
||||
*temp = [[NSData alloc] initWithBase64EncodedString:dataElem.stringValue options:0];
|
||||
[_receivedData appendData:temp];
|
||||
|
||||
// According the base64 encoding, it takes up 4/3 n bytes of space, so
|
||||
// we need to find the size of the data before base64.
|
||||
_receivedDataSize += (3 * dataElem.stringValue.length) / 4;
|
||||
|
||||
XMPPLogVerbose(@"Downloaded %lu/%lu bytes in IBB transfer.",
|
||||
(unsigned long) _receivedDataSize, (unsigned long) _totalDataSize);
|
||||
|
||||
if (_receivedDataSize < _totalDataSize) {
|
||||
// Send ack response
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
|
||||
to:received.from
|
||||
elementID:received.elementID];
|
||||
[xmppStream sendElement:iq];
|
||||
} else {
|
||||
// We're finished!
|
||||
XMPPLogInfo(@"Finished downloading IBB data.");
|
||||
[self transferSuccess];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Util Methods
|
||||
|
||||
/**
|
||||
* This method determines whether or not the IQ stanza is a `disco#info`
|
||||
* request. Should be in the following form:
|
||||
*
|
||||
* <iq xmlns="jabber:client"
|
||||
* from="baal@sanctuary.org/worldstonechamber"
|
||||
* to="mephisto@sanctuary.org/kurast"
|
||||
* type="get"
|
||||
* id="purplea2da8fc9">
|
||||
* <query xmlns="http://jabber.org/protocol/disco#info"/>
|
||||
* </iq>
|
||||
*/
|
||||
- (BOOL)isDiscoInfoIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if (!iq) return NO;
|
||||
NSXMLElement *query = iq.childElement;
|
||||
return query != nil && [query.xmlns isEqualToString:XMPPDiscoInfoNamespace];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method determines whether or not the the IQ stanza is a Stream
|
||||
* Initiation Offer (XEP-0096 Examples 1 and 3).
|
||||
*/
|
||||
- (BOOL)isSIOfferIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if (!iq) return NO;
|
||||
if (![iq.type isEqualToString:@"set"]) return NO;
|
||||
|
||||
NSXMLElement *si = iq.childElement;
|
||||
if (!si || ![si.xmlns isEqualToString:XMPPSINamespace]) return NO;
|
||||
|
||||
NSXMLElement *file = (DDXMLElement *) [si childAtIndex:0];
|
||||
if (!file || ![file.xmlns isEqualToString:XMPPSIProfileFileTransferNamespace]) return NO;
|
||||
|
||||
NSXMLElement *feature = (DDXMLElement *) [si childAtIndex:1];
|
||||
return !(!feature || ![feature.xmlns isEqualToString:XMPPFeatureNegNamespace]);
|
||||
|
||||
// Maybe there should be further verification, but I think this should be
|
||||
// plenty...
|
||||
}
|
||||
|
||||
/**
|
||||
* This method determines whether or not the IQ stanza is an IBB session request
|
||||
* (XEP-0047 Example 1).
|
||||
*/
|
||||
- (BOOL)isIBBOpenRequestIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if (!iq) return NO;
|
||||
if (![iq.type isEqualToString:@"set"]) return NO;
|
||||
|
||||
NSXMLElement *open = iq.childElement;
|
||||
return !(!open || ![open.xmlns isEqualToString:XMPPIBBNamespace]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method determines whether or not the IQ stanza is an IBB data stanza
|
||||
* (XEP-0047 Example 6).
|
||||
*/
|
||||
- (BOOL)isIBBDataIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if (!iq) return NO;
|
||||
if (![iq.type isEqualToString:@"set"]) return NO;
|
||||
|
||||
NSXMLElement *data = iq.childElement;
|
||||
return !(!data || ![data.xmlns isEqualToString:XMPPIBBNamespace]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method determines whether or not the IQ stanza contains a list of
|
||||
* streamhosts as shown in XEP-0065 Example 12.
|
||||
*/
|
||||
- (BOOL)isStreamhostsListIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if (!iq) return NO;
|
||||
if (![iq.type isEqualToString:@"set"]) return NO;
|
||||
|
||||
NSXMLElement *query = iq.childElement;
|
||||
if (!query || ![[query attributeStringValueForName:@"sid"] isEqualToString:self.sid]) return NO;
|
||||
|
||||
return [query elementsForName:@"streamhost"].count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the SHA1 hash as per XEP-0065.
|
||||
*
|
||||
* The [address] MUST be SHA1(SID + Initiator JID + Target JID) and the output
|
||||
* is hexadecimal encoded (not binary).
|
||||
*
|
||||
* Because this is an incoming file transfer, we are always the target.
|
||||
*/
|
||||
- (NSData *)sha1Hash
|
||||
{
|
||||
NSString *hashMe =
|
||||
[NSString stringWithFormat:@"%@%@%@", self.sid, _senderJID.full, xmppStream.myJID.full];
|
||||
NSData *hashRaw = [[hashMe dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest];
|
||||
NSData *hash = [[hashRaw xmpp_hexStringValue] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
XMPPLogVerbose(@"%@: hashMe : %@", THIS_FILE, hashMe);
|
||||
XMPPLogVerbose(@"%@: hashRaw: %@", THIS_FILE, hashRaw);
|
||||
XMPPLogVerbose(@"%@: hash : %@", THIS_FILE, hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called to clean up everything when the transfer fails.
|
||||
*/
|
||||
- (void)failWithReason:(NSString *)causeOfFailure
|
||||
error:
|
||||
(NSError *)error
|
||||
{
|
||||
XMPPLogTrace();
|
||||
XMPPLogInfo(@"Incoming file transfer failed because: %@", causeOfFailure);
|
||||
|
||||
if (!error && causeOfFailure) {
|
||||
NSDictionary *errInfo = @{NSLocalizedDescriptionKey : causeOfFailure};
|
||||
error = [NSError errorWithDomain:XMPPIncomingFileTransferErrorDomain
|
||||
code:-1
|
||||
userInfo:errInfo];
|
||||
}
|
||||
|
||||
_transferState = XMPPIFTStateNone;
|
||||
[multicastDelegate xmppIncomingFileTransfer:self didFailWithError:error];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when the transfer is successfully completed. It
|
||||
* handles resetting variables for another transfer and alerts the delegate of
|
||||
* the transfer completion.
|
||||
*/
|
||||
- (void)transferSuccess
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
[self cancelIBBTimer];
|
||||
|
||||
[multicastDelegate xmppIncomingFileTransfer:self
|
||||
didSucceedWithData:_receivedData
|
||||
named:_receivedFileName];
|
||||
[self cleanUp];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to reset the system for receiving new files.
|
||||
*/
|
||||
- (void)cleanUp
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (_asyncSocket) {
|
||||
[_asyncSocket setDelegate:nil];
|
||||
[_asyncSocket disconnect];
|
||||
_asyncSocket = nil;
|
||||
}
|
||||
|
||||
_streamMethods &= 0;
|
||||
_transferState = XMPPIFTStateNone;
|
||||
_senderJID = nil;
|
||||
_streamhostsQueryId = nil;
|
||||
_streamhostUsed = nil;
|
||||
_receivedData = nil;
|
||||
_receivedFileName = nil;
|
||||
_totalDataSize = 0;
|
||||
_receivedDataSize = 0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Timeouts
|
||||
|
||||
/**
|
||||
* Resets the IBB timer that will cause the transfer to formally fail if an IBB
|
||||
* data IQ stanza isn't received within the timeout.
|
||||
*/
|
||||
- (void)resetIBBTimer:(NSTimeInterval)timeout
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue.");
|
||||
|
||||
if (_ibbTimer == NULL) {
|
||||
_ibbTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
|
||||
|
||||
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC);
|
||||
|
||||
dispatch_source_set_timer(_ibbTimer, tt, DISPATCH_TIME_FOREVER, 1);
|
||||
dispatch_resume(_ibbTimer);
|
||||
} else {
|
||||
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC);
|
||||
dispatch_source_set_timer(_ibbTimer, tt, DISPATCH_TIME_FOREVER, 1);
|
||||
}
|
||||
|
||||
dispatch_source_set_event_handler(_ibbTimer, ^{
|
||||
@autoreleasepool {
|
||||
NSString *errMsg = @"The IBB transfer timed out. It's likely that the sender canceled the"
|
||||
@" transfer or has gone offline.";
|
||||
[self failWithReason:errMsg error:nil];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)cancelIBBTimer
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue.");
|
||||
|
||||
if (_ibbTimer) {
|
||||
dispatch_source_cancel(_ibbTimer);
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_release(_ibbTimer);
|
||||
#endif
|
||||
_ibbTimer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - XMPPStreamDelegate
|
||||
|
||||
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if (_transferState == XMPPIFTStateNone && [self isDiscoInfoIQ:iq]) {
|
||||
[self sendIdentity:iq];
|
||||
_transferState = XMPPIFTStateWaitingForSIOffer;
|
||||
return YES;
|
||||
}
|
||||
|
||||
if ((_transferState == XMPPIFTStateNone || _transferState == XMPPIFTStateWaitingForSIOffer)
|
||||
&& [self isSIOfferIQ:iq]) {
|
||||
// Alert the delegate that we've received a stream initiation offer
|
||||
[multicastDelegate xmppIncomingFileTransfer:self didReceiveSIOffer:iq];
|
||||
|
||||
if (_autoAcceptFileTransfers) {
|
||||
[self sendSIOfferAcceptance:iq];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (_transferState == XMPPIFTStateWaitingForStreamhosts && [self isStreamhostsListIQ:iq]) {
|
||||
[self attemptStreamhostsConnection:iq];
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (_transferState == XMPPIFTStateWaitingForIBBOpen && [self isIBBOpenRequestIQ:iq]) {
|
||||
[self sendIBBAcceptance:iq];
|
||||
_transferState = XMPPIFTStateWaitingForIBBData;
|
||||
|
||||
// Handle the scenario that the transfer is cancelled.
|
||||
[self resetIBBTimer:20];
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (_transferState == XMPPIFTStateWaitingForIBBData && [self isIBBDataIQ:iq]) {
|
||||
[self processReceivedIBBDataIQ:iq];
|
||||
}
|
||||
|
||||
return iq != nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - GCDAsyncSocketDelegate
|
||||
|
||||
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
|
||||
{
|
||||
XMPPLogVerbose(@"%@: didConnectToHost:%@ port:%d", THIS_FILE, host, port);
|
||||
|
||||
[self socks5WriteMethod];
|
||||
_transferState = XMPPIFTStateConnected;
|
||||
}
|
||||
|
||||
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
|
||||
{
|
||||
XMPPLogVerbose(@"%@: didReadData:%@ withTag:%ld", THIS_FILE, data, tag);
|
||||
|
||||
switch (tag) {
|
||||
case SOCKS_TAG_READ_METHOD:
|
||||
[self socks5ReadMethod:data];
|
||||
break;
|
||||
case SOCKS_TAG_READ_REPLY:
|
||||
[self socks5ReadReply:data];
|
||||
case SOCKS_TAG_READ_ADDRESS:
|
||||
[_asyncSocket readDataToLength:_totalDataSize
|
||||
withTimeout:TIMEOUT_READ
|
||||
tag:SOCKS_TAG_READ_DATA];
|
||||
break;
|
||||
case SOCKS_TAG_READ_DATA:
|
||||
// Success!
|
||||
_receivedData = [data mutableCopy];
|
||||
[self transferSuccess];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
|
||||
{
|
||||
XMPPLogVerbose(@"%@: didWriteDataWithTag:%ld", THIS_FILE, tag);
|
||||
|
||||
switch (tag) {
|
||||
case SOCKS_TAG_WRITE_METHOD:
|
||||
[_asyncSocket readDataToLength:2 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_METHOD];
|
||||
break;
|
||||
case SOCKS_TAG_WRITE_CONNECT:
|
||||
[_asyncSocket readDataToLength:5 withTimeout:TIMEOUT_READ
|
||||
tag:SOCKS_TAG_READ_REPLY];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if (_transferState == XMPPIFTStateConnected) {
|
||||
[self failWithReason:@"Socket disconnected before transfer complete." error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - SOCKS5
|
||||
|
||||
/**
|
||||
* This method attempts a connection to each of the streamhosts provided until
|
||||
* either a connection is established or there are no more streamhosts. In the
|
||||
* latter case, an error stanza is sent to the sender.
|
||||
*
|
||||
* @see socket:didConnectToHost:port:
|
||||
*/
|
||||
- (void)attemptStreamhostsConnection:(XMPPIQ *)iq
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
_streamhostsQueryId = iq.elementID;
|
||||
_transferState = XMPPIFTStateConnectingToStreamhosts;
|
||||
_asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:moduleQueue];
|
||||
|
||||
// Since we've already validated our IQ stanza, we can just pull the data
|
||||
NSArray *streamhosts = [iq.childElement elementsForName:@"streamhost"];
|
||||
|
||||
for (NSXMLElement *streamhost in streamhosts) {
|
||||
NSString *host = [streamhost attributeStringValueForName:@"host"];
|
||||
uint16_t port = (gl_uint16_t) [streamhost attributeUInt32ValueForName:@"port"];
|
||||
|
||||
NSError *err;
|
||||
if (![_asyncSocket connectToHost:host onPort:port error:&err]) {
|
||||
XMPPLogVerbose(@"%@: Unable to host:%@ port:%d error:%@", THIS_FILE, host, port, err);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we make it this far, we've successfully connected to one of the hosts.
|
||||
_streamhostUsed = [streamhost attributeStringValueForName:@"jid"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach this, we weren't able to connect to any of the streamhosts.
|
||||
// We'll send an error to the sender to let them know, and then we'll alert
|
||||
// the delegate of the failure.
|
||||
//
|
||||
// XEP-0065 Example 13.
|
||||
//
|
||||
// <iq from='mephisto@sanctuary.org/kurast'
|
||||
// id='hu3vax16'
|
||||
// to='baal@sanctuary.org/worldstonechamber'
|
||||
// type='error'>
|
||||
// <error type='modify'>
|
||||
// <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
// </error>
|
||||
// </iq>
|
||||
|
||||
XMPPIQ *errorIq = [XMPPIQ iqWithType:@"error" to:iq.from elementID:iq.elementID];
|
||||
|
||||
NSXMLElement *errorElem = [NSXMLElement elementWithName:@"error"];
|
||||
[errorElem addAttributeWithName:@"type" stringValue:@"modify"];
|
||||
|
||||
NSXMLElement *notAcceptable = [NSXMLElement elementWithName:@"not-acceptable"
|
||||
xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"];
|
||||
[errorElem addChild:notAcceptable];
|
||||
[errorIq addChild:errorElem];
|
||||
|
||||
[xmppStream sendElement:errorIq];
|
||||
|
||||
NSString *errMsg = @"Unable to connect to any of the provided streamhosts.";
|
||||
[self failWithReason:errMsg error:nil];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (void)socks5WriteMethod
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
// We will attempt anonymous authentication with the proxy. The request is
|
||||
// the same that we would read if this were a direct connection. The only
|
||||
// difference is this time we initiate the request as a client rather than
|
||||
// being a the 'server.'
|
||||
//
|
||||
// +----+----------+----------+
|
||||
// |VER | NMETHODS | METHODS |
|
||||
// +----+----------+----------+
|
||||
// | 1 | 1 | 1 to 255 |
|
||||
// +----+----------+----------+
|
||||
//
|
||||
// We're sending:
|
||||
//
|
||||
// VER = 5 (SOCKS5)
|
||||
// NMETHODS = 1 (number of methods)
|
||||
// METHODS = 0 (no authentication)
|
||||
|
||||
void *byteBuf = malloc(3);
|
||||
|
||||
UInt8 ver = 5;
|
||||
memcpy(byteBuf, &ver, sizeof(ver));
|
||||
|
||||
UInt8 nmethods = 1;
|
||||
memcpy(byteBuf + 1, &nmethods, sizeof(nmethods));
|
||||
|
||||
UInt8 methods = 0;
|
||||
memcpy(byteBuf + 2, &methods, sizeof(methods));
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:byteBuf length:3 freeWhenDone:YES];
|
||||
[_asyncSocket writeData:data withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_METHOD];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (void)socks5ReadMethod:(NSData *)incomingData
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
// We've sent a request to connect with no authentication. This is the
|
||||
// response:
|
||||
//
|
||||
// +----+--------+
|
||||
// |VER | METHOD |
|
||||
// +----+--------+
|
||||
// | 1 | 1 |
|
||||
// +----+--------+
|
||||
//
|
||||
// We're expecting:
|
||||
//
|
||||
// VER = 5 (SOCKS5)
|
||||
// METHOD = 0 (no authentication)
|
||||
|
||||
UInt8 version = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0];
|
||||
UInt8 method = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1];
|
||||
|
||||
if (version != 5 || method) {
|
||||
[self failWithReason:@"Proxy doesn't allow anonymous authentication." error:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *hash = [self sha1Hash];
|
||||
|
||||
// The SOCKS request is formed as follows:
|
||||
//
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
//
|
||||
// We're sending:
|
||||
//
|
||||
// VER = 5
|
||||
// CMD = 1 (connect)
|
||||
// RSV = 0 (reserved; this will always be 0)
|
||||
// ATYP = 3 (domain name)
|
||||
// DST.ADDR (varies based on ATYP)
|
||||
// DST.PORT = 0 (according to XEP-0065)
|
||||
//
|
||||
// Immediately after ATYP, we need to send the length of our address. Because
|
||||
// SHA1 is always 40 bytes, we simply send this value. After it, we append
|
||||
// the actual hash and then the port.
|
||||
|
||||
void *byteBuf = malloc(5 + 40 + 2);
|
||||
|
||||
UInt8 ver = 5;
|
||||
memcpy(byteBuf, &ver, sizeof(ver));
|
||||
|
||||
UInt8 cmd = 1;
|
||||
memcpy(byteBuf + 1, &cmd, sizeof(cmd));
|
||||
|
||||
UInt8 rsv = 0;
|
||||
memcpy(byteBuf + 2, &rsv, sizeof(rsv));
|
||||
|
||||
UInt8 atyp = 3;
|
||||
memcpy(byteBuf + 3, &atyp, sizeof(atyp));
|
||||
|
||||
UInt8 hashlen = (UInt8) hash.length;
|
||||
memcpy(byteBuf + 4, &hashlen, sizeof(hashlen));
|
||||
|
||||
memcpy(byteBuf + 5, hash.bytes, hashlen);
|
||||
|
||||
UInt8 port = 0;
|
||||
memcpy(byteBuf + 5 + hashlen, &port, sizeof(port));
|
||||
memcpy(byteBuf + 6 + hashlen, &port, sizeof(port));
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:byteBuf length:47 freeWhenDone:YES];
|
||||
[_asyncSocket writeData:data withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_CONNECT];
|
||||
|
||||
XMPPLogVerbose(@"%@: writing connect request: %@", THIS_FILE, data);
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (void)socks5ReadReply:(NSData *)incomingData
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
@autoreleasepool {
|
||||
// The server/sender will reply to our connect command with the following:
|
||||
//
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
//
|
||||
// VER = 5 (SOCKS5)
|
||||
// REP = 0 (Success)
|
||||
// RSV = 0
|
||||
// ATYP = 3 (Domain) - NOTE: Since we're using ATYP = 3, we must check the
|
||||
// length of the server's host in the next byte.
|
||||
|
||||
UInt8 ver = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0];
|
||||
UInt8 rep = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1];
|
||||
UInt8 atyp = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:3];
|
||||
UInt8 hostlen = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:4];
|
||||
|
||||
if (ver != 5 || rep || atyp != 3) {
|
||||
[self failWithReason:@"Invalid VER, REP, or ATYP." error:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
// According to XEP-0065 Example 23, we don't need to validate the
|
||||
// address we were sent (at least that is how I interpret it), so we
|
||||
// just read the next 42 bytes (hostlen + portlen) so there's no
|
||||
// conflict when reading the data and then send <streamhost-used/> to
|
||||
// the file transfer initiator. Note that the sid must be included.
|
||||
//
|
||||
// XEP-0065 Example 17:
|
||||
//
|
||||
// <iq from='mephisto@sanctuary.org/kurast'
|
||||
// id='hu3vax16'
|
||||
// to='baal@sanctuary.org/worldstonechamber'
|
||||
// type='result'>
|
||||
// <query xmlns='http://jabber.org/protocol/bytestreams'
|
||||
// sid='vxf9n471bn46'>
|
||||
// <streamhost-used jid='baal@sanctuary.org/worldstonechamber'/>
|
||||
// </query>
|
||||
// </iq>
|
||||
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:_senderJID elementID:_streamhostsQueryId];
|
||||
|
||||
NSXMLElement *query = [NSXMLElement elementWithName:@"query"
|
||||
xmlns:XMPPBytestreamsNamespace];
|
||||
[query addAttributeWithName:@"sid" stringValue:self.sid];
|
||||
|
||||
NSXMLElement *streamhostUsed = [NSXMLElement elementWithName:@"streamhost-used"];
|
||||
[streamhostUsed addAttributeWithName:@"jid"
|
||||
stringValue:_streamhostUsed];
|
||||
|
||||
[query addChild:streamhostUsed];
|
||||
[iq addChild:query];
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
|
||||
// We're basically piping these to dev/null because we don't care.
|
||||
// However, we need to tag this read so we can start to read the actual
|
||||
// data once this read is finished.
|
||||
[_asyncSocket readDataToLength:hostlen + 2
|
||||
withTimeout:TIMEOUT_READ
|
||||
tag:SOCKS_TAG_READ_ADDRESS];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// Created by Jonathon Staff on 10/21/14.
|
||||
// Copyright (c) 2014 nplexity, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPFileTransfer.h"
|
||||
|
||||
@interface XMPPOutgoingFileTransfer : XMPPFileTransfer
|
||||
|
||||
/**
|
||||
* (Required)
|
||||
*
|
||||
* The data being sent to the recipient.
|
||||
*
|
||||
* If you're using startFileTransfer:, you *MUST* set this prior to calling
|
||||
* startFileTransfer:.
|
||||
*/
|
||||
@property (nonatomic, strong) NSData *outgoingData;
|
||||
|
||||
/**
|
||||
* (Required)
|
||||
*
|
||||
* The recipient of your file transfer.
|
||||
*
|
||||
* If you're using startFileTransfer:, you *MUST* set this prior to calling
|
||||
* startFileTransfer:.
|
||||
*/
|
||||
@property (nonatomic, strong) XMPPJID *recipientJID;
|
||||
|
||||
/**
|
||||
* (Optional)
|
||||
*
|
||||
* The name of the file you're sending.
|
||||
*
|
||||
* If you don't provide a filename, one will be generated for you.
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *outgoingFileName;
|
||||
|
||||
/**
|
||||
* (Optional)
|
||||
*
|
||||
* The description of the file you're sending.
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *outgoingFileDescription;
|
||||
|
||||
/**
|
||||
* (Optional)
|
||||
*
|
||||
* Specifies whether or not a random name should be generated instead of the
|
||||
* filename provided. The randomly generated name will retain the same file
|
||||
* extension as the original name if one was provided
|
||||
*
|
||||
* The default is NO; set to YES to generate a random name.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL shouldGenerateRandomName;
|
||||
|
||||
/**
|
||||
* (Optional)
|
||||
*
|
||||
* Specifies the default block-size when using IBB file transfers. The default
|
||||
* value is 4096 (Bytes). If the file recipient requests a smaller block-size,
|
||||
* it will be halved.
|
||||
*/
|
||||
@property (nonatomic, assign) int32_t blockSize;
|
||||
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
/**
|
||||
* Starts the file transfer. This assumes that at a minimum a recipientJID and
|
||||
* outgoingData have already been provided.
|
||||
*
|
||||
* @param errPtr The address of an error which will be contain a description of
|
||||
* the problem if there is one (optional).
|
||||
*
|
||||
* @return Returns NO if there is something blatantly wrong (not authorized, no
|
||||
* recipientJID, no outgoingData); YES otherwise.
|
||||
*/
|
||||
- (BOOL)startFileTransfer:(NSError **)errPtr;
|
||||
|
||||
/**
|
||||
* Sends the provided data to the provided recipient. Use of this method is not
|
||||
* recommended, as there is no error handling, but you're free to make your own
|
||||
* choices.
|
||||
*/
|
||||
- (BOOL)sendData:(NSData *)data toRecipient:(XMPPJID *)recipient;
|
||||
|
||||
/**
|
||||
* Sends the provided data to the provided recipient. Pass nil for params you
|
||||
* don't care about.
|
||||
*
|
||||
* @param data The data you wish to send (required).
|
||||
* @param name The filename of the file you're sending (optional).
|
||||
* @param recipient The recipient of your file transfer (required). Note that a
|
||||
* resource must also be included in the JID.
|
||||
* @param description The description of the file you're sending (optional).
|
||||
* @param errPtr The address of an error which will contain a description of the
|
||||
* problem if there is one (optional).
|
||||
*/
|
||||
- (BOOL)sendData:(NSData *)data
|
||||
named:(NSString *)name
|
||||
toRecipient:(XMPPJID *)recipient
|
||||
description:(NSString *)description
|
||||
error:(NSError **)errPtr;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - XMPPOutgoingFileTransferDelegate
|
||||
|
||||
@protocol XMPPOutgoingFileTransferDelegate
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Implement this method when calling startFileTransfer: or sendData:(variants).
|
||||
* It will be invoked if the file transfer fails to execute properly. More
|
||||
* information will be given in the error.
|
||||
*
|
||||
* @param sender XMPPOutgoingFileTransfer object invoking this delegate method.
|
||||
* @param error NSError containing more details of the failure.
|
||||
*/
|
||||
- (void)xmppOutgoingFileTransfer:(XMPPOutgoingFileTransfer *)sender
|
||||
didFailWithError:(NSError *)error;
|
||||
|
||||
/**
|
||||
* Implement this method when calling startFileTransfer: or sendData:(variants).
|
||||
* It will be invoked if the outgoing file transfer was completed successfully.
|
||||
*
|
||||
* @param sender XMPPOutgoingFileTransfer object invoking this delegate method.
|
||||
*/
|
||||
- (void)xmppOutgoingFileTransferDidSucceed:(XMPPOutgoingFileTransfer *)sender;
|
||||
|
||||
/**
|
||||
* Not really sure why you would want this information, but hey, when I get
|
||||
* information, I'm happy to share.
|
||||
*/
|
||||
- (void)xmppOutgoingFileTransferIBBClosed:(XMPPOutgoingFileTransfer *)sender;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,51 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPModule.h"
|
||||
|
||||
extern NSString *const XMPPGoogleSharedStatusShow;
|
||||
extern NSString *const XMPPGoogleSharedStatusInvisible;
|
||||
extern NSString *const XMPPGoogleSharedStatusStatus;
|
||||
|
||||
extern NSString *const XMPPGoogleSharedStatusShowAvailable;
|
||||
extern NSString *const XMPPGoogleSharedStatusShowBusy;
|
||||
extern NSString *const XMPPGoogleSharedStatusShowIdle;
|
||||
|
||||
@protocol XMPPGoogleSharedStatusDelegate;
|
||||
|
||||
|
||||
#define _XMPP_GOOGLE_SHARED_STATUS_H
|
||||
|
||||
@interface XMPPGoogleSharedStatus : XMPPModule
|
||||
|
||||
@property (nonatomic, assign) BOOL sharedStatusSupported;
|
||||
@property (nonatomic, strong) NSDictionary *sharedStatus;
|
||||
|
||||
// Determines if the user is idle based on last received HID
|
||||
// event, and sends the presence as away, if the user is idle.
|
||||
@property (nonatomic, assign) BOOL assumeIdleUpdateResponsibility;
|
||||
|
||||
@property (nonatomic, strong) NSString *status;
|
||||
@property (nonatomic, strong) NSString *show;
|
||||
@property (nonatomic, assign) BOOL invisible;
|
||||
|
||||
// Convenience auto-updater for presence status.
|
||||
// Does not use XMPPGoogleSharedStatus.
|
||||
@property (nonatomic, copy) NSString *statusMessage;
|
||||
@property (nonatomic, copy) NSString *statusAvailability;
|
||||
|
||||
- (void)updateSharedStatus:(NSString *)status
|
||||
show:(NSString *)show
|
||||
invisible:(BOOL)invisible;
|
||||
|
||||
- (void)refreshSharedStatus;
|
||||
|
||||
@end
|
||||
|
||||
@protocol XMPPGoogleSharedStatusDelegate
|
||||
|
||||
@optional
|
||||
|
||||
// This delegate method is called when the server updates the shared
|
||||
// status module with new status information, or upon manual refresh.
|
||||
- (void)xmppGoogleSharedStatus:(XMPPGoogleSharedStatus *)sender didReceiveUpdatedStatus:(NSDictionary *)sharedStatus;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,369 @@
|
||||
#import "XMPPGoogleSharedStatus.h"
|
||||
#import "XMPP.h"
|
||||
#import "XMPPFramework.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#define GOOGLE_SHARED_STATUS @"google:shared-status"
|
||||
#define GOOGLE_DISCO_INFO @"http://jabber.org/protocol/disco#info"
|
||||
#define GOOGLE_PRESENCE_PRIORITY @"24"
|
||||
|
||||
// Dictionary keys to access shared status information.
|
||||
NSString *const XMPPGoogleSharedStatusShow = @"show";
|
||||
NSString *const XMPPGoogleSharedStatusInvisible = @"invisible";
|
||||
NSString *const XMPPGoogleSharedStatusStatus = @"status";
|
||||
|
||||
// Shared status display values. Note that you cannot set
|
||||
// a shared status for an idle show.
|
||||
NSString *const XMPPGoogleSharedStatusShowAvailable = @"default";
|
||||
NSString *const XMPPGoogleSharedStatusShowBusy = @"dnd";
|
||||
NSString *const XMPPGoogleSharedStatusShowIdle = @"away";
|
||||
|
||||
@interface XMPPGoogleSharedStatus () {
|
||||
|
||||
// Server specified maximum values.
|
||||
NSInteger _statusListMaxCount;
|
||||
NSInteger _statusMessageMaxLength;
|
||||
}
|
||||
|
||||
@property (nonatomic, copy) NSString *previousShow;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XMPPGoogleSharedStatus
|
||||
|
||||
#pragma mark - Object Lifecycle
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue {
|
||||
if((self = [super initWithDispatchQueue:queue])) {
|
||||
self.sharedStatus = [NSDictionary dictionary];
|
||||
_status = @"";
|
||||
_show = XMPPGoogleSharedStatusShowAvailable;
|
||||
_invisible = NO;
|
||||
|
||||
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
||||
self.assumeIdleUpdateResponsibility = YES;
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)aXmppStream
|
||||
{
|
||||
if ([super activate:aXmppStream])
|
||||
{
|
||||
|
||||
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
||||
[xmppStream autoAddDelegate:self
|
||||
delegateQueue:moduleQueue
|
||||
toModulesOfClass:[XMPPSystemInputActivityMonitor class]];
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
|
||||
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
||||
[xmppStream removeAutoDelegate:self
|
||||
delegateQueue:moduleQueue
|
||||
fromModulesOfClass:[XMPPSystemInputActivityMonitor class]];
|
||||
#endif
|
||||
|
||||
[super deactivate];
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Convenience Properties
|
||||
|
||||
// Convinience single-item setters which forward status setting
|
||||
// to the more complex update method below. Use these sparingly,
|
||||
// because they have heavy load when used in succession.
|
||||
- (void)setStatus:(NSString *)status {
|
||||
[self updateSharedStatus:status show:self.show invisible:self.invisible];
|
||||
}
|
||||
|
||||
- (void)setShow:(NSString *)show {
|
||||
[self updateSharedStatus:self.status show:show invisible:self.invisible];
|
||||
}
|
||||
|
||||
- (void)setInvisible:(BOOL)invisible {
|
||||
[self updateSharedStatus:self.status show:self.show invisible:invisible];
|
||||
}
|
||||
|
||||
#pragma mark - Status Updates
|
||||
|
||||
// This is a heavyweight method used to update all three attributes,
|
||||
// status, show, and invisible, at once. Use this method over the
|
||||
// convinience accessors above. Wherever possible, lessen the status
|
||||
// update load. This method refuses service if shared status is not
|
||||
// supported, removes previous statuses if the list reaches max
|
||||
// count, and cuts status strings to the max string length.
|
||||
- (void)updateSharedStatus:(NSString *)status show:(NSString *)show invisible:(BOOL)invisible {
|
||||
if(!self.sharedStatusSupported) {
|
||||
NSLog(@"Google Shared Status Not Supported!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a mutable copy of shared status information and chop the status.
|
||||
NSMutableDictionary *sharedStatusUpdate = [self.sharedStatus mutableCopy];
|
||||
if(status.length > _statusMessageMaxLength)
|
||||
status = [status substringToIndex:_statusMessageMaxLength];
|
||||
|
||||
// If the show has changed update it.
|
||||
if(![show isEqualToString:sharedStatusUpdate[XMPPGoogleSharedStatusShow]]) {
|
||||
[sharedStatusUpdate removeObjectForKey:XMPPGoogleSharedStatusShow];
|
||||
sharedStatusUpdate[XMPPGoogleSharedStatusShow] = show;
|
||||
}
|
||||
|
||||
// Get the current show value, and whether the status should update for it.
|
||||
BOOL displayShow = ([show isEqualToString:XMPPGoogleSharedStatusShowAvailable] ||
|
||||
[show isEqualToString:XMPPGoogleSharedStatusShowBusy]);
|
||||
NSString *currentShow = self.sharedStatus[XMPPGoogleSharedStatusShow];
|
||||
|
||||
// Since we can't show an away status, only change the status it has
|
||||
// changed and is not showing idle.
|
||||
if(![status isEqualToString:sharedStatusUpdate[XMPPGoogleSharedStatusStatus]] && displayShow) {
|
||||
[sharedStatusUpdate removeObjectForKey:XMPPGoogleSharedStatusStatus];
|
||||
sharedStatusUpdate[XMPPGoogleSharedStatusStatus] = status;
|
||||
|
||||
// Update the appropriate status list by adding the new status.
|
||||
// If the list max count was reached, truncate its earliest status.
|
||||
NSMutableArray *statusList = [sharedStatusUpdate[currentShow] mutableCopy];
|
||||
[statusList insertObject:status atIndex:0];
|
||||
if(statusList.count > _statusListMaxCount)
|
||||
[statusList removeObjectAtIndex:statusList.count-1];
|
||||
|
||||
// Remove any blank statuses.
|
||||
for(int i = 0; i < statusList.count; i++) {
|
||||
NSString *status = statusList[i];
|
||||
if([status isEqualToString:@""])
|
||||
[statusList removeObjectAtIndex:i];
|
||||
}
|
||||
|
||||
// Update the status list for this status.
|
||||
[sharedStatusUpdate removeObjectForKey:currentShow];
|
||||
sharedStatusUpdate[currentShow] = statusList;
|
||||
}
|
||||
|
||||
// Update the invisibility for this shared status.
|
||||
[sharedStatusUpdate removeObjectForKey:XMPPGoogleSharedStatusInvisible];
|
||||
sharedStatusUpdate[XMPPGoogleSharedStatusInvisible] = @(invisible);
|
||||
|
||||
// Wrap it in an XMPPIQ "set" and send it to the server.
|
||||
XMPPIQ *statusIQ = [XMPPIQ iqWithType:@"set" to:self.xmppStream.myJID.bareJID];
|
||||
[statusIQ addAttributeWithName:@"id" stringValue:self.xmppStream.myJID.resource];
|
||||
[statusIQ addChild:[self packSharedStatus:sharedStatusUpdate]];
|
||||
[self.xmppStream sendElement:statusIQ];
|
||||
|
||||
// Now update our local values, toll-free.
|
||||
_show = show;
|
||||
_status = status;
|
||||
_invisible = invisible;
|
||||
}
|
||||
|
||||
#pragma mark - Capability Discovery
|
||||
|
||||
// Discover and refresh shared status as soon as authentication.
|
||||
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
|
||||
[self discoverCapabilities];
|
||||
[self refreshSharedStatus];
|
||||
}
|
||||
|
||||
// Disable support when the stream disconnects.
|
||||
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error {
|
||||
self.sharedStatus = [NSDictionary dictionary];
|
||||
self.sharedStatusSupported = NO;
|
||||
}
|
||||
|
||||
// First discover capabilities to find google:shared-status. If it exists,
|
||||
// we can continue with the shared status support.
|
||||
- (void)discoverCapabilities {
|
||||
XMPPIQ *discoIQ = [XMPPIQ iqWithType:@"get" to:[XMPPJID jidWithString:@"gmail.com"]];
|
||||
[discoIQ addChild:[XMPPElement elementWithName:@"query" xmlns:GOOGLE_DISCO_INFO]];
|
||||
[self.xmppStream sendElement:discoIQ];
|
||||
}
|
||||
|
||||
// Manually trigger a refresh cycle for getting shared statuses. This will
|
||||
// return an XML stanza containing status-lists and current status info.
|
||||
- (void)refreshSharedStatus {
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:self.xmppStream.myJID.bareJID];
|
||||
[iq addAttributeWithName:@"id" stringValue:self.xmppStream.myJID.resource];
|
||||
|
||||
XMPPElement *query = (XMPPElement *)[XMPPElement elementWithName:@"query" xmlns:GOOGLE_SHARED_STATUS];
|
||||
[query addAttributeWithName:@"version" stringValue:@"2"];
|
||||
|
||||
[iq addChild:query];
|
||||
[self.xmppStream sendElement:iq];
|
||||
}
|
||||
|
||||
// If we received a shared-status IQ and we support it, unpack it into a
|
||||
// dictionary and notify the delegates. If it is the first shared-status
|
||||
// subscription IQ, it contains the maximum string length and list count
|
||||
// information attributes as well, so save those.
|
||||
// If we received a discovery IQ, determine shared status support.
|
||||
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq {
|
||||
XMPPElement *query = (XMPPElement *) [iq children][0];
|
||||
if([query.xmlns isEqualToString:GOOGLE_SHARED_STATUS] && self.sharedStatusSupported) {
|
||||
self.sharedStatus = [self unpackSharedStatus:query];
|
||||
_show = self.sharedStatus[XMPPGoogleSharedStatusShow];
|
||||
_status = self.sharedStatus[XMPPGoogleSharedStatusStatus];
|
||||
_invisible = [self.sharedStatus[XMPPGoogleSharedStatusInvisible] boolValue];
|
||||
[multicastDelegate xmppGoogleSharedStatus:self didReceiveUpdatedStatus:self.sharedStatus];
|
||||
|
||||
if([query attributeForName:@"status-max"])
|
||||
_statusMessageMaxLength = [[query attributeForName:@"status-max"].stringValue integerValue];
|
||||
if([query attributeForName:@"status-list-contents-max"])
|
||||
_statusListMaxCount = [[query attributeForName:@"status-list-contents-max"].stringValue integerValue];
|
||||
|
||||
} else if([query.xmlns isEqualToString:GOOGLE_DISCO_INFO]) {
|
||||
for(XMPPElement *feature in query.children)
|
||||
if([[[feature attributeForName:@"var"] stringValue] isEqualToString:GOOGLE_SHARED_STATUS])
|
||||
self.sharedStatusSupported = YES;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Packing & Unpacking
|
||||
|
||||
// Unpacks the XMPPElement "query" for a shared status and returns
|
||||
// a formatted NSDictionary with corresponding arrays and strings.
|
||||
// The XMPPElement "query" is a child of a shared status XMPPIQ.
|
||||
- (NSDictionary *)unpackSharedStatus:(XMPPElement *)sharedStatus {
|
||||
if([sharedStatus.xmlns isEqualToString:GOOGLE_SHARED_STATUS]) {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
|
||||
for(XMPPElement *element in sharedStatus.children) {
|
||||
if([element.name isEqualToString:@"status-list"]) {
|
||||
NSMutableArray *array = [NSMutableArray array];
|
||||
for(XMPPElement *status in element.children)
|
||||
[array addObject:[status stringValue]];
|
||||
dict[[[element attributeForName:XMPPGoogleSharedStatusShow] stringValue]] = array;
|
||||
} else if([element.name isEqualToString:XMPPGoogleSharedStatusStatus]) {
|
||||
dict[element.name] = [element stringValue];
|
||||
} else if([element.name isEqualToString:XMPPGoogleSharedStatusShow]) {
|
||||
dict[element.name] = [element stringValue];
|
||||
} else if([element.name isEqualToString:XMPPGoogleSharedStatusInvisible]) {
|
||||
dict[element.name] = @([[[element attributeForName:@"value"] stringValue] boolValue]);
|
||||
} else {
|
||||
NSLog(@"Missed element: %@", element);
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Packs a formatted NSDictionary with corresponding arrays and strings
|
||||
// back into an XMPPElement "query" for shared status. This must be
|
||||
// further wrapped in an XMPPIQ to send a status update.
|
||||
- (XMPPElement *)packSharedStatus:(NSDictionary *)sharedStatus {
|
||||
XMPPElement *element = (XMPPElement *)[XMPPElement elementWithName:@"query" xmlns:GOOGLE_SHARED_STATUS];
|
||||
[element addAttributeWithName:@"version" stringValue:@"2"];
|
||||
|
||||
[element addChild:[XMPPElement elementWithName:XMPPGoogleSharedStatusStatus
|
||||
stringValue:sharedStatus[XMPPGoogleSharedStatusStatus]]];
|
||||
[element addChild:[XMPPElement elementWithName:XMPPGoogleSharedStatusShow
|
||||
stringValue:sharedStatus[XMPPGoogleSharedStatusShow]]];
|
||||
|
||||
for(NSString *key in sharedStatus.allKeys.reverseObjectEnumerator) {
|
||||
if([key isEqualToString:XMPPGoogleSharedStatusShowAvailable] ||
|
||||
[key isEqualToString:XMPPGoogleSharedStatusShowBusy]) {
|
||||
|
||||
NSArray *statusList = sharedStatus[key];
|
||||
XMPPElement *statusElement = [XMPPElement elementWithName:@"status-list"];
|
||||
[statusElement addAttributeWithName:@"show" stringValue:key];
|
||||
|
||||
for(NSString *status in statusList)
|
||||
[statusElement addChild:[XMPPElement elementWithName:@"status" stringValue:status]];
|
||||
[element addChild:statusElement];
|
||||
} else if(!([key isEqualToString:XMPPGoogleSharedStatusInvisible] ||
|
||||
[key isEqualToString:XMPPGoogleSharedStatusShow] ||
|
||||
[key isEqualToString:XMPPGoogleSharedStatusStatus])) {
|
||||
NSLog(@"Invalid element: %@", key);
|
||||
}
|
||||
}
|
||||
|
||||
XMPPElement *invisible = [XMPPElement elementWithName:XMPPGoogleSharedStatusInvisible];
|
||||
[invisible addAttributeWithName:@"value"
|
||||
stringValue:[sharedStatus[XMPPGoogleSharedStatusInvisible] boolValue] ? @"true" : @"false"];
|
||||
[element addChild:invisible];
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
#pragma mark - Other Properties
|
||||
|
||||
- (void)setAssumeIdleUpdateResponsibility:(BOOL)flag {
|
||||
_assumeIdleUpdateResponsibility = flag;
|
||||
}
|
||||
|
||||
- (void)setStatusAvailability:(NSString *)statusAvailability {
|
||||
_statusAvailability = statusAvailability;
|
||||
|
||||
XMPPPresence *presence = [XMPPPresence presence];
|
||||
[presence addChild:[XMPPElement elementWithName:@"priority" stringValue:GOOGLE_PRESENCE_PRIORITY]];
|
||||
XMPPElement *show = [XMPPElement elementWithName:@"show"];
|
||||
XMPPElement *status = [XMPPElement elementWithName:@"status"];
|
||||
|
||||
if(self.statusAvailability) {
|
||||
[show setStringValue:self.statusAvailability];
|
||||
[presence addChild:show];
|
||||
}
|
||||
|
||||
if(self.statusMessage) {
|
||||
[status setStringValue:self.statusMessage];
|
||||
[presence addChild:status];
|
||||
}
|
||||
|
||||
[self.xmppStream sendElement:presence];
|
||||
}
|
||||
|
||||
- (void)setStatusMessage:(NSString *)statusMessage {
|
||||
_statusMessage = statusMessage;
|
||||
|
||||
XMPPPresence *presence = [XMPPPresence presence];
|
||||
XMPPElement *show = [XMPPElement elementWithName:@"show"];
|
||||
XMPPElement *status = [XMPPElement elementWithName:@"status"];
|
||||
|
||||
if(self.statusAvailability) {
|
||||
[show setStringValue:self.statusAvailability];
|
||||
[presence addChild:show];
|
||||
}
|
||||
|
||||
if(self.statusMessage) {
|
||||
[status setStringValue:self.statusMessage];
|
||||
[presence addChild:status];
|
||||
}
|
||||
|
||||
[self.xmppStream sendElement:presence];
|
||||
}
|
||||
|
||||
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
||||
|
||||
#pragma mark - XMPP System Input Activity Monitor Delegate
|
||||
|
||||
- (void)xmppSystemInputActivityMonitorDidBecomeActive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor
|
||||
{
|
||||
self.show = self.previousShow;
|
||||
}
|
||||
|
||||
- (void)xmppSystemInputActivityMonitorDidBecomeInactive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor
|
||||
{
|
||||
self.previousShow = self.show;
|
||||
self.show = XMPPGoogleSharedStatusShowIdle;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,117 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPSASLAuthentication.h"
|
||||
#import "XMPPStream.h"
|
||||
#import "XMPPModule.h"
|
||||
|
||||
@class XMPPRebindAuthentication;
|
||||
|
||||
|
||||
/**
|
||||
* Process One has a proprietary module they sell for ejabberd that enables several
|
||||
* features such as push notifications and fast reconnect.
|
||||
*
|
||||
* This file implements the client side functionality for XMPPFramework.
|
||||
**/
|
||||
@interface XMPPProcessOne : XMPPModule
|
||||
|
||||
/**
|
||||
* Once a connection is authenticated, the module automatically stores the session ID and related JID.
|
||||
* The information is stored in the user defaults system, thus it is persisted across launches of the application.
|
||||
*
|
||||
* If the session information is available, and the server supports rebind, fast reconnect may be possible.
|
||||
**/
|
||||
@property (strong, readwrite) NSString *savedSessionID;
|
||||
@property (strong, readwrite) XMPPJID *savedSessionJID;
|
||||
@property (strong, readwrite) NSDate *savedSessionDate;
|
||||
|
||||
/**
|
||||
* Push Mode Configuration.
|
||||
* Options are detailed in the documentation from ejabberd.
|
||||
*
|
||||
* An example of a pushConfiguration element you would set:
|
||||
*
|
||||
* <push xmlns='p1:push'>
|
||||
* <keepalive max='30'/>
|
||||
* <session duration='60'/>
|
||||
* <body send='all' groupchat='true' from='jid'/>
|
||||
* <status type='xa'>Text Message when in push mode</status>
|
||||
* <offline>false</offline>
|
||||
* <notification>
|
||||
* <type>applepush</type>
|
||||
* <id>DeviceToken</id>
|
||||
* </notification>
|
||||
* <appid>application1</appid>
|
||||
* </push>
|
||||
*
|
||||
* To enable Apple Push on the ejabberd server, you must set the pushConfiguration element.
|
||||
*
|
||||
* You may set the pushConfiguration element at any time.
|
||||
* If you set it after the xmpp stream has already authenticated, then the push settings will be sent right away.
|
||||
* Otherwise, the push settings will be sent as soon as the stream is authenticated.
|
||||
*
|
||||
* After the pushConfiguration element has been set, you can change it at any time.
|
||||
* If you do, it will send the updated configuration options to the server.
|
||||
*
|
||||
* To disable push, you can simply set the pushConfiguration to nil.
|
||||
*
|
||||
* @see pushConfigurationContainer
|
||||
**/
|
||||
@property (readwrite, strong) NSXMLElement *pushConfiguration;
|
||||
|
||||
/**
|
||||
* Standby Mode.
|
||||
* The following methods allow you to switch on/off standby mode.
|
||||
*
|
||||
* Typical use case looks like this:
|
||||
*
|
||||
* - (void)applicationWillResignActive:(NSNotification *)notification
|
||||
* {
|
||||
* // Send standby element (via normal asynchronous mechanism)
|
||||
* XMPPElementReceipt *receipt = [xmppProcessOne goOnStandby];
|
||||
*
|
||||
* // Wait until standby element gets sent (pumped through dispatch queues and into OS socket buffer)
|
||||
* [receipt wait:-1.0];
|
||||
* }
|
||||
*
|
||||
* - (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
* {
|
||||
* [xmppProcessOne goOffStandby];
|
||||
* }
|
||||
**/
|
||||
- (XMPPElementReceipt *)goOnStandby;
|
||||
- (XMPPElementReceipt *)goOffStandby;
|
||||
|
||||
/**
|
||||
* Methods to help create the pushConfiguration required to enable anything on the server.
|
||||
**/
|
||||
|
||||
+ (NSXMLElement *)pushConfigurationContainer;
|
||||
|
||||
+ (NSXMLElement *)keepaliveWithMax:(NSTimeInterval)max;
|
||||
+ (NSXMLElement *)sessionWithDuration:(NSTimeInterval)duration;
|
||||
+ (NSXMLElement *)statusWithType:(NSString *)type message:(NSString *)message;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface XMPPRebindAuthentication : NSObject <XMPPSASLAuthentication>
|
||||
|
||||
- (id)initWithStream:(XMPPStream *)stream sessionID:(NSString *)sessionID sessionJID:(XMPPJID *)sessionJID;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface XMPPStream (XMPPProcessOne)
|
||||
|
||||
- (BOOL)supportsPush;
|
||||
- (BOOL)supportsRebind;
|
||||
|
||||
- (NSString *)rebindSessionID;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,418 @@
|
||||
#import "XMPPProcessOne.h"
|
||||
#import "XMPP.h"
|
||||
#import "XMPPInternal.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
|
||||
|
||||
NSString *const XMPPProcessOneSessionID = @"XMPPProcessOneSessionID";
|
||||
NSString *const XMPPProcessOneSessionJID = @"XMPPProcessOneSessionJID";
|
||||
NSString *const XMPPProcessOneSessionDate = @"XMPPProcessOneSessionDate";
|
||||
|
||||
@interface XMPPProcessOne ()
|
||||
{
|
||||
NSXMLElement *pushConfiguration;
|
||||
BOOL pushConfigurationSent;
|
||||
BOOL pushConfigurationConfirmed;
|
||||
NSString *pushIQID;
|
||||
}
|
||||
|
||||
- (void)sendPushConfiguration;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPProcessOne
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
if ((self = [super initWithDispatchQueue:NULL]))
|
||||
{
|
||||
pushConfiguration = nil;
|
||||
pushConfigurationSent = YES;
|
||||
pushConfigurationConfirmed = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)savedSessionID
|
||||
{
|
||||
return [[NSUserDefaults standardUserDefaults] stringForKey:XMPPProcessOneSessionID];
|
||||
}
|
||||
|
||||
- (void)setSavedSessionID:(NSString *)savedSessionID
|
||||
{
|
||||
if (savedSessionID)
|
||||
[[NSUserDefaults standardUserDefaults] setObject:savedSessionID forKey:XMPPProcessOneSessionID];
|
||||
else
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:XMPPProcessOneSessionID];
|
||||
}
|
||||
|
||||
- (XMPPJID *)savedSessionJID
|
||||
{
|
||||
NSString *sessionJidStr = [[NSUserDefaults standardUserDefaults] stringForKey:XMPPProcessOneSessionJID];
|
||||
|
||||
return [XMPPJID jidWithString:sessionJidStr];
|
||||
}
|
||||
|
||||
- (void)setSavedSessionJID:(XMPPJID *)savedSessionJID
|
||||
{
|
||||
NSString *sessionJidStr = [savedSessionJID full];
|
||||
|
||||
if (sessionJidStr)
|
||||
[[NSUserDefaults standardUserDefaults] setObject:sessionJidStr forKey:XMPPProcessOneSessionJID];
|
||||
else
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:XMPPProcessOneSessionJID];
|
||||
}
|
||||
|
||||
- (NSDate *)savedSessionDate
|
||||
{
|
||||
return [[NSUserDefaults standardUserDefaults] objectForKey:XMPPProcessOneSessionDate];
|
||||
}
|
||||
|
||||
- (void)setSavedSessionDate:(NSDate *)savedSessionDate
|
||||
{
|
||||
if (savedSessionDate)
|
||||
[[NSUserDefaults standardUserDefaults] setObject:savedSessionDate forKey:XMPPProcessOneSessionDate];
|
||||
else
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:XMPPProcessOneSessionDate];
|
||||
}
|
||||
|
||||
- (NSXMLElement *)pushConfiguration
|
||||
{
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
{
|
||||
return pushConfiguration;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSXMLElement *result = nil;
|
||||
|
||||
dispatch_sync(moduleQueue, ^{
|
||||
result = [pushConfiguration copy];
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPushConfiguration:(NSXMLElement *)pushConfig
|
||||
{
|
||||
NSXMLElement *newPushConfiguration = [pushConfig copy];
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
|
||||
if (pushConfiguration == nil && newPushConfiguration == nil)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pushConfiguration = newPushConfiguration;
|
||||
pushConfigurationSent = NO;
|
||||
pushConfigurationConfirmed = NO;
|
||||
|
||||
if ([xmppStream isAuthenticated])
|
||||
{
|
||||
[self sendPushConfiguration];
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (void)sendPushConfiguration
|
||||
{
|
||||
if (pushConfiguration)
|
||||
{
|
||||
pushIQID = [XMPPStream generateUUID];
|
||||
NSXMLElement *push = [pushConfiguration copy];
|
||||
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:pushIQID child:push];
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
pushConfigurationSent = YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
// <iq type='set'>
|
||||
// <disable xmlns='p1:push'>
|
||||
// </iq>
|
||||
|
||||
NSXMLElement *disable = [NSXMLElement elementWithName:@"disable" xmlns:@"p1:push"];
|
||||
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" child:disable];
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
pushConfigurationSent = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (XMPPElementReceipt *)goOnStandby
|
||||
{
|
||||
// <standby>true</standby>
|
||||
|
||||
NSXMLElement *standby = [NSXMLElement elementWithName:@"standby" stringValue:@"true"];
|
||||
|
||||
XMPPElementReceipt *receipt = nil;
|
||||
[xmppStream sendElement:standby andGetReceipt:&receipt];
|
||||
|
||||
return receipt;
|
||||
}
|
||||
|
||||
- (XMPPElementReceipt *)goOffStandby
|
||||
{
|
||||
// <standby>false</standby>
|
||||
|
||||
NSXMLElement *standby = [NSXMLElement elementWithName:@"standby" stringValue:@"false"];
|
||||
|
||||
XMPPElementReceipt *receipt = nil;
|
||||
[xmppStream sendElement:standby andGetReceipt:&receipt];
|
||||
|
||||
return receipt;
|
||||
}
|
||||
|
||||
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
|
||||
{
|
||||
if (!pushConfigurationSent)
|
||||
{
|
||||
[self sendPushConfiguration];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
|
||||
{
|
||||
if (pushConfigurationConfirmed)
|
||||
{
|
||||
self.savedSessionDate = [[NSDate alloc] init];
|
||||
}
|
||||
else
|
||||
{
|
||||
// The pushConfiguration was sent to the server, but we never received a confirmation.
|
||||
// So either the pushConfiguration never made it to the server,
|
||||
// or we got disconnected before we received the confirmation from the server.
|
||||
//
|
||||
// To be sure, we need to resent the pushConfiguration next time we authenticate.
|
||||
|
||||
pushConfigurationSent = NO;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Configuration Elements
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (NSXMLElement *)pushConfigurationContainer
|
||||
{
|
||||
return [NSXMLElement elementWithName:@"push" xmlns:@"p1:push"];
|
||||
}
|
||||
|
||||
+ (NSXMLElement *)keepaliveWithMax:(NSTimeInterval)max
|
||||
{
|
||||
// keepalive is the max interval between keepalive events received by server
|
||||
// before going out of reception (in seconds)
|
||||
|
||||
NSString *maxStr = [NSString stringWithFormat:@"%.0f", max];
|
||||
|
||||
NSXMLElement *keepalive = [NSXMLElement elementWithName:@"keepalive"];
|
||||
[keepalive addAttributeWithName:@"max" stringValue:maxStr];
|
||||
|
||||
return keepalive;
|
||||
}
|
||||
|
||||
+ (NSXMLElement *)sessionWithDuration:(NSTimeInterval)durationInSeconds
|
||||
{
|
||||
// session is the max time the session is kept before automatically
|
||||
// closing the session while in push mode (in minutes).
|
||||
//
|
||||
// Max session is 24 hours.
|
||||
// Server can decide to force the session close anyway, if the message queue is getting large.
|
||||
|
||||
double durationInMinutes = durationInSeconds / 60.0;
|
||||
|
||||
NSString *durationStr = [NSString stringWithFormat:@"%.0f", durationInMinutes];
|
||||
|
||||
NSXMLElement *session = [NSXMLElement elementWithName:@"session"];
|
||||
[session addAttributeWithName:@"duration" stringValue:durationStr];
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
+ (NSXMLElement *)statusWithType:(NSString *)type message:(NSString *)message
|
||||
{
|
||||
// status (optional) is the XMPP status the user should appear in,
|
||||
// and the message when the XMPP session is not linked to a TCP connection.
|
||||
//
|
||||
// If omittted, the presence and status is not change when going into disconnected opened session.
|
||||
|
||||
NSXMLElement *status = [NSXMLElement elementWithName:@"status"];
|
||||
|
||||
if (type)
|
||||
[status addAttributeWithName:@"type" stringValue:type];
|
||||
|
||||
if (message)
|
||||
[status setStringValue:message];
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRebindAuthentication
|
||||
{
|
||||
#if __has_feature(objc_arc_weak)
|
||||
__weak XMPPStream *xmppStream;
|
||||
#else
|
||||
__unsafe_unretained XMPPStream *xmppStream;
|
||||
#endif
|
||||
|
||||
NSString *sessionID;
|
||||
XMPPJID *sessionJID;
|
||||
}
|
||||
|
||||
+ (NSString *)mechanismName
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
xmppStream = stream;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithStream:(XMPPStream *)stream sessionID:(NSString *)aSessionID sessionJID:(XMPPJID *)aSessionJID
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
xmppStream = stream;
|
||||
sessionID = aSessionID;
|
||||
sessionJID = aSessionJID;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)start:(NSError **)errPtr
|
||||
{
|
||||
if (!sessionID || !sessionJID)
|
||||
{
|
||||
NSString *errMsg = @"Missing sessionID and/or sessionJID.";
|
||||
NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg};
|
||||
|
||||
NSError *err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info];
|
||||
|
||||
if (errPtr) *errPtr = err;
|
||||
return NO;
|
||||
}
|
||||
|
||||
// <rebind xmlns="p1:rebind">
|
||||
// <jid>user@domain/resource</jid>
|
||||
// <sid>123456789</sid>
|
||||
// </rebind>
|
||||
|
||||
NSXMLElement *jid = [NSXMLElement elementWithName:@"jid" stringValue:[sessionJID full]];
|
||||
NSXMLElement *sid = [NSXMLElement elementWithName:@"sid" stringValue:sessionID];
|
||||
|
||||
NSXMLElement *rebind = [NSXMLElement elementWithName:@"rebind" xmlns:@"p1:rebind"];
|
||||
[rebind addChild:jid];
|
||||
[rebind addChild:sid];
|
||||
|
||||
[xmppStream sendAuthElement:rebind];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)response
|
||||
{
|
||||
if ([[response name] isEqualToString:@"rebind"])
|
||||
{
|
||||
return XMPP_AUTH_SUCCESS;
|
||||
}
|
||||
else
|
||||
{
|
||||
return XMPP_AUTH_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPStream (XMPPProcessOne)
|
||||
|
||||
- (BOOL)supportsPush
|
||||
{
|
||||
__block BOOL result = NO;
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
|
||||
// The root element can be properly queried anytime after the
|
||||
// stream:features are received, and TLS has been setup (if required)
|
||||
if (self.state >= STATE_XMPP_POST_NEGOTIATION)
|
||||
{
|
||||
NSXMLElement *features = [self.rootElement elementForName:@"stream:features"];
|
||||
NSXMLElement *push = [features elementForName:@"push" xmlns:@"p1:push"];
|
||||
|
||||
result = (push != nil);
|
||||
}
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(self.xmppQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(self.xmppQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)supportsRebind
|
||||
{
|
||||
__block BOOL result = NO;
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
|
||||
// The root element can be properly queried anytime after the
|
||||
// stream:features are received, and TLS has been setup (if required)
|
||||
if (self.state >= STATE_XMPP_POST_NEGOTIATION)
|
||||
{
|
||||
NSXMLElement *features = [self.rootElement elementForName:@"stream:features"];
|
||||
NSXMLElement *rebind = [features elementForName:@"rebind" xmlns:@"p1:rebind"];
|
||||
|
||||
result = (rebind != nil);
|
||||
}
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(self.xmppQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(self.xmppQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)rebindSessionID
|
||||
{
|
||||
return [[self rootElement] attributeStringValueForName:@"id"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,183 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
#import "XMPPModule.h"
|
||||
|
||||
#define _XMPP_RECONNECT_H
|
||||
|
||||
#define DEFAULT_XMPP_RECONNECT_DELAY 2.0
|
||||
|
||||
#define DEFAULT_XMPP_RECONNECT_TIMER_INTERVAL 20.0
|
||||
|
||||
|
||||
@protocol XMPPReconnectDelegate;
|
||||
|
||||
/**
|
||||
* XMPPReconnect handles automatically reconnecting to the xmpp server due to accidental disconnections.
|
||||
* That is, a disconnection that is not the result of calling disconnect on the xmpp stream.
|
||||
*
|
||||
* Accidental disconnections may happen for a variety of reasons.
|
||||
* The most common are general connectivity issues such as disconnection from a WiFi access point.
|
||||
*
|
||||
* However, there are several of issues that occasionaly occur.
|
||||
* There are some routers on the market that disconnect TCP streams after a period of inactivity.
|
||||
* In addition to this, there have been iPhone revisions where the OS networking stack would pull the same crap.
|
||||
* These issue have been largely overcome due to the keepalive implementation in XMPPStream.
|
||||
*
|
||||
* Regardless of how the disconnect happens, the XMPPReconnect class can help to automatically re-establish
|
||||
* the xmpp stream so as to have minimum impact on the user (and hopefully they don't even notice).
|
||||
*
|
||||
* Once a stream has been opened and authenticated, this class will detect any accidental disconnections.
|
||||
* If one occurs, an attempt will be made to automatically reconnect after a short delay.
|
||||
* This delay is configurable via the reconnectDelay property.
|
||||
* At the same time the class will begin monitoring the network for reachability changes.
|
||||
* When the reachability of the xmpp host has changed, a reconnect may be tried again.
|
||||
* In addition to all this, a timer may optionally be used to attempt a reconnect periodically.
|
||||
* The timer is started if the initial reconnect fails.
|
||||
* This reconnect timer is fully configurable (may be enabled/disabled, and it's timeout may be changed).
|
||||
*
|
||||
* In all cases, prior to attempting a reconnect,
|
||||
* this class will invoke the shouldAttemptAutoReconnect delegate method.
|
||||
* The delegate may use this opportunity to optionally decline the auto reconnect attempt.
|
||||
*
|
||||
* Auto reconnect may be disabled at any time via the autoReconnect property.
|
||||
*
|
||||
* Note that auto reconnect will only occur for a stream that has been opened and authenticated.
|
||||
* So it will do nothing, for example, if there is no internet connectivity when your application
|
||||
* first launches, and the xmpp stream is unable to connect to the host.
|
||||
* In cases such as this it may be desireable to start monitoring the network for reachability changes.
|
||||
* This way when internet connectivity is restored, one can immediately connect the xmpp stream.
|
||||
* This is possible via the manualStart method,
|
||||
* which will trigger the class into action just as if an accidental disconnect occurred.
|
||||
**/
|
||||
|
||||
@interface XMPPReconnect : XMPPModule
|
||||
{
|
||||
Byte flags;
|
||||
Byte config;
|
||||
NSTimeInterval reconnectDelay;
|
||||
|
||||
dispatch_source_t reconnectTimer;
|
||||
NSTimeInterval reconnectTimerInterval;
|
||||
|
||||
SCNetworkReachabilityRef reachability;
|
||||
|
||||
int reconnectTicket;
|
||||
|
||||
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_5
|
||||
SCNetworkConnectionFlags previousReachabilityFlags;
|
||||
#else
|
||||
SCNetworkReachabilityFlags previousReachabilityFlags;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether auto reconnect is enabled or disabled.
|
||||
*
|
||||
* The default value is YES (enabled).
|
||||
*
|
||||
* Note: Altering this property will only affect future accidental disconnections.
|
||||
* For example, if autoReconnect was true, and you disable this property after an accidental disconnection,
|
||||
* this will not stop the current reconnect process.
|
||||
* In order to stop a current reconnect process use the stop method.
|
||||
*
|
||||
* Similarly, if autoReconnect was false, and you enable this property after an accidental disconnection,
|
||||
* this will not start a reconnect process.
|
||||
* In order to start a reconnect process use the manualStart method.
|
||||
**/
|
||||
@property (nonatomic, assign) BOOL autoReconnect;
|
||||
|
||||
/**
|
||||
* When the accidental disconnection first happens,
|
||||
* a short delay may be used before attempting the reconnection.
|
||||
*
|
||||
* The default value is DEFAULT_XMPP_RECONNECT_DELAY (defined at the top of this file).
|
||||
*
|
||||
* To disable this feature, set the value to zero.
|
||||
*
|
||||
* Note: NSTimeInterval is a double that specifies the time in seconds.
|
||||
**/
|
||||
@property (nonatomic, assign) NSTimeInterval reconnectDelay;
|
||||
|
||||
/**
|
||||
* A reconnect timer may optionally be used to attempt a reconnect periodically.
|
||||
* The timer will be started after the initial reconnect delay.
|
||||
*
|
||||
* The default value is DEFAULT_XMPP_RECONNECT_TIMER_INTERVAL (defined at the top of this file).
|
||||
*
|
||||
* To disable this feature, set the value to zero.
|
||||
*
|
||||
* Note: NSTimeInterval is a double that specifies the time in seconds.
|
||||
**/
|
||||
@property (nonatomic, assign) NSTimeInterval reconnectTimerInterval;
|
||||
|
||||
/**
|
||||
* Whether you want to reconnect using the legacy method -[XMPPStream oldSchoolSecureConnectWithTimeout:error:]
|
||||
* instead of the standard -[XMPPStream connect:].
|
||||
*
|
||||
* If you initially connect using -oldSchoolSecureConnectWithTimeout:error:, set this to YES to reconnect the same way.
|
||||
*
|
||||
* The default value is NO (disabled).
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL usesOldSchoolSecureConnect;
|
||||
|
||||
/**
|
||||
* As opposed to using autoReconnect, this method may be used to manually start the reconnect process.
|
||||
* This may be useful, for example, if one needs network monitoring in order to setup the inital xmpp connection.
|
||||
* Or if one wants autoReconnect but only in very limited situations which they prefer to control manually.
|
||||
*
|
||||
* After invoking this method one can expect the class to act as if an accidental disconnect just occurred.
|
||||
* That is, a reconnect attempt will be tried after reconnectDelay seconds,
|
||||
* and the class will begin monitoring the network for changes in reachability to the xmpp host.
|
||||
*
|
||||
* A manual start of the reconnect process will effectively end once the xmpp stream has been opened.
|
||||
* That is, if you invoke manualStart and the xmpp stream is later opened,
|
||||
* then future disconnections will not result in an auto reconnect process (unless the autoReconnect property applies).
|
||||
*
|
||||
* This method does nothing if the xmpp stream is not disconnected.
|
||||
**/
|
||||
- (void)manualStart;
|
||||
|
||||
/**
|
||||
* Stops the current reconnect process.
|
||||
*
|
||||
* This method will stop the current reconnect process regardless of whether the
|
||||
* reconnect process was started due to the autoReconnect property or due to a call to manualStart.
|
||||
*
|
||||
* Stopping the reconnect process does NOT prevent future auto reconnects if the property is enabled.
|
||||
* That is, if the autoReconnect property is still enabled, and the xmpp stream is later opened, authenticated and
|
||||
* accidentally disconnected, this class will still attempt an automatic reconnect.
|
||||
*
|
||||
* Stopping the reconnect process does NOT prevent future calls to manualStart from working.
|
||||
*
|
||||
* It only stops the CURRENT reconnect process.
|
||||
**/
|
||||
- (void)stop;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPReconnectDelegate
|
||||
@optional
|
||||
|
||||
/**
|
||||
* This method may be used to fine tune when we
|
||||
* should and should not attempt an auto reconnect.
|
||||
*
|
||||
* For example, if on the iPhone, one may want to prevent auto reconnect when WiFi is not available.
|
||||
**/
|
||||
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_5
|
||||
|
||||
- (void)xmppReconnect:(XMPPReconnect *)sender didDetectAccidentalDisconnect:(SCNetworkConnectionFlags)connectionFlags;
|
||||
- (BOOL)xmppReconnect:(XMPPReconnect *)sender shouldAttemptAutoReconnect:(SCNetworkConnectionFlags)connectionFlags;
|
||||
|
||||
#else
|
||||
|
||||
- (void)xmppReconnect:(XMPPReconnect *)sender didDetectAccidentalDisconnect:(SCNetworkReachabilityFlags)connectionFlags;
|
||||
- (BOOL)xmppReconnect:(XMPPReconnect *)sender shouldAttemptAutoReconnect:(SCNetworkReachabilityFlags)reachabilityFlags;
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,651 @@
|
||||
#import "XMPPReconnect.h"
|
||||
#import "XMPPStream.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "NSXMLElement+XMPP.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#define IMPOSSIBLE_REACHABILITY_FLAGS 0xFFFFFFFF
|
||||
|
||||
// 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
|
||||
|
||||
enum XMPPReconnectFlags
|
||||
{
|
||||
kShouldReconnect = 1 << 0, // If set, disconnection was accidental, and autoReconnect may be used
|
||||
kMultipleChanges = 1 << 1, // If set, there have been reachability changes during a connection attempt
|
||||
kManuallyStarted = 1 << 2, // If set, we were started manually via manualStart method
|
||||
kQueryingDelegates = 1 << 3, // If set, we are awaiting response(s) from the delegate(s)
|
||||
};
|
||||
|
||||
enum XMPPReconnectConfig
|
||||
{
|
||||
kAutoReconnect = 1 << 0, // If set, automatically attempts to reconnect after a disconnection
|
||||
};
|
||||
|
||||
#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_5 && !TARGET_OS_IPHONE
|
||||
// SCNetworkConnectionFlags was renamed to SCNetworkReachabilityFlags in 10.6
|
||||
typedef SCNetworkConnectionFlags SCNetworkReachabilityFlags;
|
||||
#endif
|
||||
|
||||
@interface XMPPReconnect (PrivateAPI)
|
||||
|
||||
- (void)setupReconnectTimer;
|
||||
- (void)teardownReconnectTimer;
|
||||
|
||||
- (void)setupNetworkMonitoring;
|
||||
- (void)teardownNetworkMonitoring;
|
||||
|
||||
- (void)maybeAttemptReconnect;
|
||||
- (void)maybeAttemptReconnectWithTicket:(int)ticket;
|
||||
- (void)maybeAttemptReconnectWithReachabilityFlags:(SCNetworkReachabilityFlags)reachabilityFlags;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPReconnect
|
||||
|
||||
@dynamic autoReconnect;
|
||||
@synthesize reconnectDelay;
|
||||
@synthesize reconnectTimerInterval;
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithDispatchQueue:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
if ((self = [super initWithDispatchQueue:queue]))
|
||||
{
|
||||
flags = 0;
|
||||
config = kAutoReconnect;
|
||||
|
||||
reconnectDelay = DEFAULT_XMPP_RECONNECT_DELAY;
|
||||
reconnectTimerInterval = DEFAULT_XMPP_RECONNECT_TIMER_INTERVAL;
|
||||
|
||||
reconnectTicket = 0;
|
||||
|
||||
previousReachabilityFlags = IMPOSSIBLE_REACHABILITY_FLAGS;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
[self teardownReconnectTimer];
|
||||
[self teardownNetworkMonitoring];
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Configuration and Flags
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)autoReconnect
|
||||
{
|
||||
__block BOOL result = NO;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = (config & kAutoReconnect) ? YES : NO;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setAutoReconnect:(BOOL)flag
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
if (flag)
|
||||
config |= kAutoReconnect;
|
||||
else
|
||||
config &= ~kAutoReconnect;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (BOOL)shouldReconnect
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
return (flags & kShouldReconnect) ? YES : NO;
|
||||
}
|
||||
|
||||
- (void)setShouldReconnect:(BOOL)flag
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
if (flag)
|
||||
flags |= kShouldReconnect;
|
||||
else
|
||||
flags &= ~kShouldReconnect;
|
||||
}
|
||||
|
||||
- (BOOL)multipleReachabilityChanges
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
return (flags & kMultipleChanges) ? YES : NO;
|
||||
}
|
||||
|
||||
- (void)setMultipleReachabilityChanges:(BOOL)flag
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
if (flag)
|
||||
flags |= kMultipleChanges;
|
||||
else
|
||||
flags &= ~kMultipleChanges;
|
||||
}
|
||||
|
||||
- (BOOL)manuallyStarted
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
return (flags & kManuallyStarted) ? YES : NO;
|
||||
}
|
||||
|
||||
- (void)setManuallyStarted:(BOOL)flag
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
if (flag)
|
||||
flags |= kManuallyStarted;
|
||||
else
|
||||
flags &= ~kManuallyStarted;
|
||||
}
|
||||
|
||||
- (BOOL)queryingDelegates
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
return (flags & kQueryingDelegates) ? YES : NO;
|
||||
}
|
||||
|
||||
- (void)setQueryingDelegates:(BOOL)flag
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked private method outside moduleQueue");
|
||||
|
||||
if (flag)
|
||||
flags |= kQueryingDelegates;
|
||||
else
|
||||
flags &= ~kQueryingDelegates;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Manual Manipulation
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)manualStart
|
||||
{
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
|
||||
if ([xmppStream isDisconnected] && [self manuallyStarted] == NO)
|
||||
{
|
||||
[self setManuallyStarted:YES];
|
||||
|
||||
[self setupReconnectTimer];
|
||||
[self setupNetworkMonitoring];
|
||||
}
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
|
||||
// Clear all flags to disable any further reconnect attemts regardless of the state we're in.
|
||||
|
||||
flags = 0;
|
||||
|
||||
// Stop any planned reconnect attempts and stop monitoring the network.
|
||||
|
||||
reconnectTicket++;
|
||||
|
||||
[self teardownReconnectTimer];
|
||||
[self teardownNetworkMonitoring];
|
||||
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPStream Delegate
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)xmppStreamDidConnect:(XMPPStream *)sender
|
||||
{
|
||||
// This method is executed on our moduleQueue.
|
||||
|
||||
// The stream is up so we can stop our reconnect attempts now.
|
||||
//
|
||||
// We essentially want to do the same thing as the stop method with one exception:
|
||||
// We do not want to clear the shouldReconnect flag.
|
||||
//
|
||||
// Remember the shouldReconnect flag gets set upon authentication.
|
||||
// A combination of this flag and the autoReconnect flag controls the auto reconnect mechanism.
|
||||
//
|
||||
// It is possible for us to get accidentally disconnected after
|
||||
// the stream opens but prior to authentication completing.
|
||||
// If this happens we still want to abide by the previous shouldReconnect setting.
|
||||
|
||||
[self setMultipleReachabilityChanges:NO];
|
||||
[self setManuallyStarted:NO];
|
||||
|
||||
reconnectTicket++;
|
||||
|
||||
[self teardownReconnectTimer];
|
||||
[self teardownNetworkMonitoring];
|
||||
}
|
||||
|
||||
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
|
||||
{
|
||||
// This method is executed on our moduleQueue.
|
||||
|
||||
// We're now connected and properly authenticated.
|
||||
// Should we get accidentally disconnected we should automatically reconnect (if autoReconnect is set).
|
||||
[self setShouldReconnect:YES];
|
||||
}
|
||||
|
||||
- (void)xmppStream:(XMPPStream *)sender didReceiveError:(NSXMLElement *)element
|
||||
{
|
||||
// This method is executed on our moduleQueue.
|
||||
|
||||
// <stream:error>
|
||||
// <conflict xmlns="urn:ietf:params:xml:ns:xmpp-streams"/>
|
||||
// <text xmlns="urn:ietf:params:xml:ns:xmpp-streams" xml:lang="">Replaced by new connection</text>
|
||||
// </stream:error>
|
||||
//
|
||||
// If our connection ever gets replaced, we shouldn't attempt a reconnect,
|
||||
// because the user has logged in on another device.
|
||||
// If we still applied the reconnect logic,
|
||||
// the two devices may get into an infinite loop of kicking each other off the system.
|
||||
|
||||
NSString *elementName = [element name];
|
||||
|
||||
if ([elementName isEqualToString:@"stream:error"] || [elementName isEqualToString:@"error"])
|
||||
{
|
||||
NSXMLElement *conflict = [element elementForName:@"conflict" xmlns:@"urn:ietf:params:xml:ns:xmpp-streams"];
|
||||
if (conflict)
|
||||
{
|
||||
[self setShouldReconnect:NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)xmppStreamWasToldToDisconnect:(XMPPStream *)sender
|
||||
{
|
||||
// This method is executed on our moduleQueue.
|
||||
|
||||
// We should not automatically attempt to reconnect when the connection closes.
|
||||
[self stop];
|
||||
}
|
||||
|
||||
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
|
||||
{
|
||||
// This method is executed on our moduleQueue.
|
||||
|
||||
if ([self autoReconnect] && [self shouldReconnect])
|
||||
{
|
||||
[self setupReconnectTimer];
|
||||
[self setupNetworkMonitoring];
|
||||
|
||||
SCNetworkReachabilityFlags reachabilityFlags = 0;
|
||||
SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags);
|
||||
|
||||
[multicastDelegate xmppReconnect:self didDetectAccidentalDisconnect:reachabilityFlags];
|
||||
}
|
||||
|
||||
if ([self multipleReachabilityChanges])
|
||||
{
|
||||
// While the previous connection attempt was in progress, the reachability of the xmpp host changed.
|
||||
// This means that while the previous attempt failed, an attempt now might succeed.
|
||||
|
||||
int ticket = ++reconnectTicket;
|
||||
|
||||
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (0.1 * NSEC_PER_SEC));
|
||||
dispatch_after(tt, moduleQueue, ^{ @autoreleasepool {
|
||||
|
||||
[self maybeAttemptReconnectWithTicket:ticket];
|
||||
|
||||
}});
|
||||
|
||||
// Note: We delay the method call.
|
||||
// This allows the other delegates to be notified of the closed stream prior to our reconnect attempt.
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Reachability
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void XMPPReconnectReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info)
|
||||
{
|
||||
@autoreleasepool {
|
||||
|
||||
XMPPReconnect *instance = (__bridge XMPPReconnect *)info;
|
||||
[instance maybeAttemptReconnectWithReachabilityFlags:flags];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Logic
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)setupReconnectTimer
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
|
||||
|
||||
if (reconnectTimer == NULL)
|
||||
{
|
||||
if ((reconnectDelay <= 0.0) && (reconnectTimerInterval <= 0.0))
|
||||
{
|
||||
// All timed reconnect attempts are disabled
|
||||
return;
|
||||
}
|
||||
|
||||
reconnectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
|
||||
|
||||
dispatch_source_set_event_handler(reconnectTimer, ^{ @autoreleasepool {
|
||||
|
||||
[self maybeAttemptReconnect];
|
||||
|
||||
}});
|
||||
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_source_t theReconnectTimer = reconnectTimer;
|
||||
|
||||
dispatch_source_set_cancel_handler(reconnectTimer, ^{
|
||||
XMPPLogVerbose(@"dispatch_release(reconnectTimer)");
|
||||
dispatch_release(theReconnectTimer);
|
||||
});
|
||||
#endif
|
||||
|
||||
dispatch_time_t startTime;
|
||||
if (reconnectDelay > 0.0)
|
||||
startTime = dispatch_time(DISPATCH_TIME_NOW, (reconnectDelay * NSEC_PER_SEC));
|
||||
else
|
||||
startTime = dispatch_time(DISPATCH_TIME_NOW, (reconnectTimerInterval * NSEC_PER_SEC));
|
||||
|
||||
uint64_t intervalTime;
|
||||
if (reconnectTimerInterval > 0.0)
|
||||
intervalTime = reconnectTimerInterval * NSEC_PER_SEC;
|
||||
else
|
||||
intervalTime = 0.0;
|
||||
|
||||
dispatch_source_set_timer(reconnectTimer, startTime, intervalTime, 0.25);
|
||||
dispatch_resume(reconnectTimer);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)teardownReconnectTimer
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
|
||||
|
||||
if (reconnectTimer)
|
||||
{
|
||||
dispatch_source_cancel(reconnectTimer);
|
||||
reconnectTimer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupNetworkMonitoring
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
|
||||
|
||||
if (reachability == NULL)
|
||||
{
|
||||
NSString *domain = xmppStream.hostName;
|
||||
if (domain == nil)
|
||||
{
|
||||
domain = @"apple.com";
|
||||
}
|
||||
|
||||
reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [domain UTF8String]);
|
||||
|
||||
if (reachability)
|
||||
{
|
||||
SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
|
||||
SCNetworkReachabilitySetCallback(reachability, XMPPReconnectReachabilityCallback, &context);
|
||||
|
||||
if (moduleQueue)
|
||||
{
|
||||
SCNetworkReachabilitySetDispatchQueue(reachability,moduleQueue);
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPLogWarn(@"%@: %@ - No xmpp moduleQueue!", THIS_FILE, THIS_METHOD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)teardownNetworkMonitoring
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
|
||||
|
||||
if (reachability)
|
||||
{
|
||||
if (moduleQueue)
|
||||
{
|
||||
SCNetworkReachabilitySetDispatchQueue(reachability,NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPLogWarn(@"%@: %@ - No xmpp moduleQueue!", THIS_FILE, THIS_METHOD);
|
||||
}
|
||||
|
||||
SCNetworkReachabilitySetCallback(reachability, NULL, NULL);
|
||||
CFRelease(reachability);
|
||||
reachability = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method may be invoked by the reconnectTimer.
|
||||
*
|
||||
* During auto reconnection it is invoked reconnectDelay seconds after an accidental disconnection.
|
||||
* After that, it is then invoked every reconnectTimerInterval seconds.
|
||||
*
|
||||
* This handles disconnections that were not the result of an internet connectivity issue.
|
||||
**/
|
||||
- (void)maybeAttemptReconnect
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
|
||||
|
||||
if (reachability)
|
||||
{
|
||||
SCNetworkReachabilityFlags reachabilityFlags;
|
||||
if (SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags))
|
||||
{
|
||||
[self maybeAttemptReconnectWithReachabilityFlags:reachabilityFlags];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked (after a short delay) if the reachability changed while
|
||||
* a reconnection attempt was in progress.
|
||||
**/
|
||||
- (void)maybeAttemptReconnectWithTicket:(int)ticket
|
||||
{
|
||||
NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue");
|
||||
|
||||
if (ticket != reconnectTicket)
|
||||
{
|
||||
// The dispatched task was cancelled.
|
||||
return;
|
||||
}
|
||||
|
||||
if (reachability)
|
||||
{
|
||||
SCNetworkReachabilityFlags reachabilityFlags;
|
||||
if (SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags))
|
||||
{
|
||||
[self maybeAttemptReconnectWithReachabilityFlags:reachabilityFlags];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)maybeAttemptReconnectWithReachabilityFlags:(SCNetworkReachabilityFlags)reachabilityFlags
|
||||
{
|
||||
if (!dispatch_get_specific(moduleQueueTag))
|
||||
{
|
||||
dispatch_async(moduleQueue, ^{ @autoreleasepool {
|
||||
|
||||
[self maybeAttemptReconnectWithReachabilityFlags:reachabilityFlags];
|
||||
|
||||
}});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (([self manuallyStarted]) || ([self autoReconnect] && [self shouldReconnect]))
|
||||
{
|
||||
if ([xmppStream isDisconnected] && ([self queryingDelegates] == NO))
|
||||
{
|
||||
// The xmpp stream is disconnected, and is not attempting reconnection
|
||||
|
||||
// Delegate rules:
|
||||
//
|
||||
// If ALL of the delegates return YES, then the result is YES.
|
||||
// If ANY of the delegates return NO, then the result is NO.
|
||||
// If there are no delegates, the default answer is YES.
|
||||
|
||||
GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator];
|
||||
|
||||
id del;
|
||||
dispatch_queue_t dq;
|
||||
|
||||
SEL selector = @selector(xmppReconnect:shouldAttemptAutoReconnect:);
|
||||
|
||||
NSUInteger delegateCount = [delegateEnumerator countForSelector:selector];
|
||||
|
||||
dispatch_semaphore_t delSemaphore = dispatch_semaphore_create(0);
|
||||
dispatch_group_t delGroup = dispatch_group_create();
|
||||
|
||||
while ([delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector])
|
||||
{
|
||||
dispatch_group_async(delGroup, dq, ^{ @autoreleasepool {
|
||||
|
||||
if (![del xmppReconnect:self shouldAttemptAutoReconnect:reachabilityFlags])
|
||||
{
|
||||
dispatch_semaphore_signal(delSemaphore);
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
[self setQueryingDelegates:YES];
|
||||
|
||||
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
dispatch_async(concurrentQueue, ^{ @autoreleasepool {
|
||||
|
||||
dispatch_group_wait(delGroup, DISPATCH_TIME_FOREVER);
|
||||
|
||||
// What was the delegate response?
|
||||
|
||||
BOOL shouldAttemptReconnect;
|
||||
if (delegateCount == 0)
|
||||
{
|
||||
shouldAttemptReconnect = YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldAttemptReconnect = (dispatch_semaphore_wait(delSemaphore, DISPATCH_TIME_NOW) != 0);
|
||||
}
|
||||
|
||||
dispatch_async(moduleQueue, ^{ @autoreleasepool {
|
||||
|
||||
[self setQueryingDelegates:NO];
|
||||
|
||||
if (shouldAttemptReconnect)
|
||||
{
|
||||
[self setMultipleReachabilityChanges:NO];
|
||||
previousReachabilityFlags = reachabilityFlags;
|
||||
|
||||
if (self.usesOldSchoolSecureConnect)
|
||||
{
|
||||
[xmppStream oldSchoolSecureConnectWithTimeout:XMPPStreamTimeoutNone error:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
[xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:nil];
|
||||
}
|
||||
}
|
||||
else if ([self multipleReachabilityChanges])
|
||||
{
|
||||
[self setMultipleReachabilityChanges:NO];
|
||||
previousReachabilityFlags = IMPOSSIBLE_REACHABILITY_FLAGS;
|
||||
|
||||
[self maybeAttemptReconnect];
|
||||
}
|
||||
else
|
||||
{
|
||||
previousReachabilityFlags = IMPOSSIBLE_REACHABILITY_FLAGS;
|
||||
}
|
||||
|
||||
}});
|
||||
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_release(delSemaphore);
|
||||
dispatch_release(delGroup);
|
||||
#endif
|
||||
}});
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// The xmpp stream is already attempting a connection.
|
||||
|
||||
if (reachabilityFlags != previousReachabilityFlags)
|
||||
{
|
||||
// It seems that the reachability of our xmpp host has changed in the middle of either
|
||||
// a reconnection attempt or while querying our delegates for permission to attempt reconnect.
|
||||
//
|
||||
// This may mean that the current attempt will fail,
|
||||
// but an another attempt after the failure will succeed.
|
||||
//
|
||||
// We make a note of the multiple changes,
|
||||
// and if the current attempt fails, we'll try again after a short delay.
|
||||
|
||||
[self setMultipleReachabilityChanges:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// XMPPGroupCoreDataStorageObject.h
|
||||
//
|
||||
// Created by Eric Chamberlain on 3/20/11.
|
||||
// Copyright (c) 2011 RF.com. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
|
||||
@class XMPPUserCoreDataStorageObject;
|
||||
|
||||
|
||||
@interface XMPPGroupCoreDataStorageObject : NSManagedObject {
|
||||
@private
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSString * name;
|
||||
@property (nonatomic, strong) NSSet* users;
|
||||
|
||||
+ (void)clearEmptyGroupsInManagedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
+ (id)fetchOrInsertGroupName:(NSString *)groupName inManagedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
+ (id)insertGroupName:(NSString *)groupName inManagedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// XMPPGroupCoreDataStorageObject.m
|
||||
//
|
||||
// Created by Eric Chamberlain on 3/20/11.
|
||||
// Copyright (c) 2011 RF.com. All rights reserved.
|
||||
//
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#import "XMPPGroupCoreDataStorageObject.h"
|
||||
#import "XMPPUserCoreDataStorageObject.h"
|
||||
|
||||
|
||||
@implementation XMPPGroupCoreDataStorageObject
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Public
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (void)clearEmptyGroupsInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:NSStringFromClass([self class])
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"users.@count == 0"];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setPredicate:predicate];
|
||||
[fetchRequest setIncludesPendingChanges:YES];
|
||||
|
||||
NSArray *results = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
|
||||
for (NSManagedObject *group in results) {
|
||||
[moc deleteObject:group];
|
||||
}
|
||||
}
|
||||
|
||||
+ (id)insertGroupName:(NSString *)groupName inManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
if (groupName == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
XMPPGroupCoreDataStorageObject *newGroup = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([self class])
|
||||
inManagedObjectContext:moc];
|
||||
newGroup.name = groupName;
|
||||
|
||||
return newGroup;
|
||||
}
|
||||
|
||||
|
||||
+ (id)fetchOrInsertGroupName:(NSString *)groupName inManagedObjectContext:(NSManagedObjectContext *)moc {
|
||||
if (groupName == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:NSStringFromClass([self class])
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", groupName];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setPredicate:predicate];
|
||||
[fetchRequest setIncludesPendingChanges:YES];
|
||||
[fetchRequest setFetchLimit:1];
|
||||
|
||||
NSArray *results = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
|
||||
if ([results count] > 0) {
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return [self insertGroupName:groupName inManagedObjectContext:moc];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Accessors
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@dynamic name;
|
||||
@dynamic users;
|
||||
|
||||
- (void)addUsersObject:(XMPPUserCoreDataStorageObject *)value {
|
||||
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
|
||||
[self willChangeValueForKey:@"users" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
|
||||
[[self primitiveValueForKey:@"users"] addObject:value];
|
||||
[self didChangeValueForKey:@"users" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
|
||||
}
|
||||
|
||||
- (void)removeUsersObject:(XMPPUserCoreDataStorageObject *)value {
|
||||
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
|
||||
[self willChangeValueForKey:@"users" withSetMutation:NSKeyValueMinusSetMutation usingObjects:changedObjects];
|
||||
[[self primitiveValueForKey:@"users"] removeObject:value];
|
||||
[self didChangeValueForKey:@"users" withSetMutation:NSKeyValueMinusSetMutation usingObjects:changedObjects];
|
||||
}
|
||||
|
||||
- (void)addUsers:(NSSet *)value {
|
||||
[self willChangeValueForKey:@"users" withSetMutation:NSKeyValueUnionSetMutation usingObjects:value];
|
||||
[[self primitiveValueForKey:@"users"] unionSet:value];
|
||||
[self didChangeValueForKey:@"users" withSetMutation:NSKeyValueUnionSetMutation usingObjects:value];
|
||||
}
|
||||
|
||||
- (void)removeUsers:(NSSet *)value {
|
||||
[self willChangeValueForKey:@"users" withSetMutation:NSKeyValueMinusSetMutation usingObjects:value];
|
||||
[[self primitiveValueForKey:@"users"] minusSet:value];
|
||||
[self didChangeValueForKey:@"users" withSetMutation:NSKeyValueMinusSetMutation usingObjects:value];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#import "XMPPResource.h"
|
||||
|
||||
@class XMPPStream;
|
||||
@class XMPPPresence;
|
||||
@class XMPPUserCoreDataStorageObject;
|
||||
|
||||
|
||||
@interface XMPPResourceCoreDataStorageObject : NSManagedObject <XMPPResource>
|
||||
|
||||
@property (nonatomic, strong) XMPPJID *jid;
|
||||
@property (nonatomic, strong) XMPPPresence *presence;
|
||||
|
||||
@property (nonatomic, assign) int priority;
|
||||
@property (nonatomic, assign) int intShow;
|
||||
|
||||
@property (nonatomic, strong) NSString * jidStr;
|
||||
@property (nonatomic, strong) NSString * presenceStr;
|
||||
|
||||
@property (nonatomic, strong) NSString * streamBareJidStr;
|
||||
|
||||
@property (nonatomic, strong) NSString * type;
|
||||
@property (nonatomic, strong) NSString * show;
|
||||
@property (nonatomic, strong) NSString * status;
|
||||
|
||||
@property (nonatomic, strong) NSDate * presenceDate;
|
||||
|
||||
@property (nonatomic, strong) NSNumber * priorityNum;
|
||||
@property (nonatomic, strong) NSNumber * showNum;
|
||||
|
||||
@property (nonatomic, strong) XMPPUserCoreDataStorageObject * user;
|
||||
|
||||
+ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
withPresence:(XMPPPresence *)presence
|
||||
streamBareJidStr:(NSString *)streamBareJidStr;
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence;
|
||||
|
||||
- (NSComparisonResult)compare:(id <XMPPResource>)another;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,217 @@
|
||||
#import "XMPP.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "XMPPRosterCoreDataStorage.h"
|
||||
#import "XMPPUserCoreDataStorageObject.h"
|
||||
#import "XMPPResourceCoreDataStorageObject.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
|
||||
|
||||
@interface XMPPResourceCoreDataStorageObject (CoreDataGeneratedPrimitiveAccessors)
|
||||
- (NSDate *)primitivePresenceDate;
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPResourceCoreDataStorageObject
|
||||
|
||||
@dynamic jid; // Implementation below
|
||||
@dynamic presence; // Implementation below
|
||||
@dynamic priority; // Implementation below
|
||||
@dynamic intShow; // Implementation below
|
||||
|
||||
@dynamic jidStr;
|
||||
@dynamic presenceStr;
|
||||
|
||||
@dynamic streamBareJidStr;
|
||||
|
||||
@dynamic type;
|
||||
@dynamic show;
|
||||
@dynamic status;
|
||||
|
||||
@dynamic presenceDate;
|
||||
|
||||
@dynamic priorityNum;
|
||||
@dynamic showNum;
|
||||
|
||||
@dynamic user;
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
return [XMPPJID jidWithString:[self jidStr]];
|
||||
}
|
||||
|
||||
- (void)setJid:(XMPPJID *)jid
|
||||
{
|
||||
self.jidStr = [jid full];
|
||||
}
|
||||
|
||||
- (XMPPPresence *)presence
|
||||
{
|
||||
[self willAccessValueForKey:@"presence"];
|
||||
XMPPPresence *presence = [self primitiveValueForKey:@"presence"];
|
||||
[self didAccessValueForKey:@"presence"];
|
||||
|
||||
if (presence == nil)
|
||||
{
|
||||
NSString *presenceStr = self.presenceStr;
|
||||
if (presenceStr != nil)
|
||||
{
|
||||
NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil];
|
||||
|
||||
presence = [XMPPPresence presenceFromElement:element];
|
||||
[self setPrimitiveValue:presence forKey:@"presence"];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return presence;
|
||||
}
|
||||
|
||||
- (void)setPresence:(XMPPPresence *)newPresence
|
||||
{
|
||||
[self willChangeValueForKey:@"presence"];
|
||||
[self setPrimitiveValue:newPresence forKey:@"presence"];
|
||||
[self didChangeValueForKey:@"presence"];
|
||||
|
||||
self.presenceStr = [newPresence compactXMLString];
|
||||
}
|
||||
|
||||
- (int)priority
|
||||
{
|
||||
return [[self priorityNum] intValue];
|
||||
}
|
||||
|
||||
- (void)setPriority:(int)priority
|
||||
{
|
||||
self.priorityNum = @(priority);
|
||||
}
|
||||
|
||||
- (int)intShow
|
||||
{
|
||||
return [[self showNum] intValue];
|
||||
}
|
||||
|
||||
- (void)setIntShow:(int)intShow
|
||||
{
|
||||
self.showNum = @(intShow);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Compiler Workarounds
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR
|
||||
|
||||
/**
|
||||
* This method is here to quiet the compiler.
|
||||
* Without it the compiler complains that this method is not implemented as required by the protocol.
|
||||
* This only seems to be a problem when compiling for the device.
|
||||
**/
|
||||
- (NSDate *)presenceDate
|
||||
{
|
||||
NSDate * tmpValue;
|
||||
|
||||
[self willAccessValueForKey:@"presenceDate"];
|
||||
tmpValue = [self primitivePresenceDate];
|
||||
[self didAccessValueForKey:@"presenceDate"];
|
||||
|
||||
return tmpValue;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Creation & Updates
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
withPresence:(XMPPPresence *)presence
|
||||
streamBareJidStr:(NSString *)streamBareJidStr
|
||||
{
|
||||
XMPPJID *jid = [presence from];
|
||||
|
||||
if (jid == nil)
|
||||
{
|
||||
XMPPLogWarn(@"%@: %@ - Invalid presence (missing or invalid jid): %@", [self class], THIS_METHOD, presence);
|
||||
return nil;
|
||||
}
|
||||
|
||||
XMPPResourceCoreDataStorageObject *newResource;
|
||||
newResource = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPResourceCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
newResource.streamBareJidStr = streamBareJidStr;
|
||||
|
||||
[newResource updateWithPresence:presence];
|
||||
|
||||
return newResource;
|
||||
}
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence
|
||||
{
|
||||
XMPPJID *jid = [presence from];
|
||||
|
||||
if (jid == nil)
|
||||
{
|
||||
XMPPLogWarn(@"%@: %@ - Invalid presence (missing or invalid jid): %@", [self class], THIS_METHOD, presence);
|
||||
return;
|
||||
}
|
||||
|
||||
self.jid = jid;
|
||||
self.presence = presence;
|
||||
|
||||
self.priority = [presence priority];
|
||||
self.intShow = [presence intShow];
|
||||
|
||||
self.type = [presence type];
|
||||
self.show = [presence show];
|
||||
self.status = [presence status];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Comparing
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSComparisonResult)compare:(id <XMPPResource>)another
|
||||
{
|
||||
XMPPPresence *mp = [self presence];
|
||||
XMPPPresence *ap = [another presence];
|
||||
|
||||
int mpp = [mp priority];
|
||||
int app = [ap priority];
|
||||
|
||||
if(mpp < app)
|
||||
return NSOrderedDescending;
|
||||
if(mpp > app)
|
||||
return NSOrderedAscending;
|
||||
|
||||
// Priority is the same.
|
||||
// Determine who is more available based on their show.
|
||||
int mps = [mp intShow];
|
||||
int aps = [ap intShow];
|
||||
|
||||
if(mps < aps)
|
||||
return NSOrderedDescending;
|
||||
if(mps > aps)
|
||||
return NSOrderedAscending;
|
||||
|
||||
// Priority and Show are the same.
|
||||
// Determine based on who was the last to receive a presence element.
|
||||
NSDate *mpd = [self presenceDate];
|
||||
NSDate *apd = [another presenceDate];
|
||||
|
||||
return [mpd compare:apd];
|
||||
}
|
||||
|
||||
@end
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,75 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPPRoster.h"
|
||||
#import "XMPPCoreDataStorage.h"
|
||||
#import "XMPPUserCoreDataStorageObject.h"
|
||||
#import "XMPPGroupCoreDataStorageObject.h"
|
||||
#import "XMPPResourceCoreDataStorageObject.h"
|
||||
|
||||
/**
|
||||
* This class is an example implementation of XMPPRosterStorage using core data.
|
||||
* You are free to substitute your own roster storage class.
|
||||
**/
|
||||
|
||||
@interface XMPPRosterCoreDataStorage : XMPPCoreDataStorage <XMPPRosterStorage>
|
||||
{
|
||||
// Inherits protected variables from XMPPCoreDataStorage
|
||||
|
||||
#if __has_feature(objc_arc_weak)
|
||||
__weak XMPPRoster *parent;
|
||||
#else
|
||||
__unsafe_unretained XMPPRoster *parent;
|
||||
#endif
|
||||
dispatch_queue_t parentQueue;
|
||||
void *parentQueueTag;
|
||||
|
||||
NSMutableSet *rosterPopulationSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to get an instance with the default database name.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* You are NOT required to use the sharedInstance.
|
||||
*
|
||||
* If your application uses multiple xmppStreams, and you use a sharedInstance of this class,
|
||||
* then all of your streams 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;
|
||||
|
||||
@property (readonly) NSManagedObjectContext *mainThreadManagedObjectContext;
|
||||
|
||||
*/
|
||||
|
||||
- (XMPPUserCoreDataStorageObject *)myUserForXMPPStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
- (XMPPResourceCoreDataStorageObject *)myResourceForXMPPStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
- (XMPPUserCoreDataStorageObject *)userForJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
- (XMPPResourceCoreDataStorageObject *)resourceForJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,575 @@
|
||||
#import "XMPPRosterCoreDataStorage.h"
|
||||
#import "XMPPGroupCoreDataStorageObject.h"
|
||||
#import "XMPPUserCoreDataStorageObject.h"
|
||||
#import "XMPPResourceCoreDataStorageObject.h"
|
||||
#import "XMPPRosterPrivate.h"
|
||||
#import "XMPPCoreDataStorageProtected.h"
|
||||
#import "XMPP.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "NSNumber+XMPP.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_INFO; // | 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");
|
||||
|
||||
|
||||
@implementation XMPPRosterCoreDataStorage
|
||||
|
||||
static XMPPRosterCoreDataStorage *sharedInstance;
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
sharedInstance = [[XMPPRosterCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil];
|
||||
});
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Setup
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
XMPPLogTrace();
|
||||
[super commonInit];
|
||||
|
||||
// This method is invoked by all public init methods of the superclass
|
||||
autoRemovePreviousDatabaseFile = YES;
|
||||
autoRecreateDatabaseFile = YES;
|
||||
|
||||
rosterPopulationSet = [[NSMutableSet alloc] init];
|
||||
}
|
||||
|
||||
- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue
|
||||
{
|
||||
NSParameterAssert(aParent != nil);
|
||||
NSParameterAssert(queue != NULL);
|
||||
|
||||
@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
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
if (parentQueue)
|
||||
dispatch_release(parentQueue);
|
||||
#endif
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Utilities
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)_clearAllResourcesForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertPrivateQueue();
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPResourceCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setFetchBatchSize:saveThreshold];
|
||||
|
||||
if (stream)
|
||||
{
|
||||
NSPredicate *predicate;
|
||||
predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@",
|
||||
[[self myJIDForXMPPStream:stream] bare]];
|
||||
|
||||
[fetchRequest setPredicate:predicate];
|
||||
}
|
||||
|
||||
NSArray *allResources = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
NSUInteger unsavedCount = [self numberOfUnsavedChanges];
|
||||
|
||||
for (XMPPResourceCoreDataStorageObject *resource in allResources)
|
||||
{
|
||||
XMPPUserCoreDataStorageObject *user = resource.user;
|
||||
[moc deleteObject:resource];
|
||||
[user recalculatePrimaryResource];
|
||||
|
||||
if (++unsavedCount >= saveThreshold)
|
||||
{
|
||||
[self save];
|
||||
unsavedCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Overrides
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)didCreateManagedObjectContext
|
||||
{
|
||||
// This method is overriden from the XMPPCoreDataStore superclass.
|
||||
// From the documentation:
|
||||
//
|
||||
// Override me, if needed, to provide customized behavior.
|
||||
//
|
||||
// For example, you may want to perform cleanup of any non-persistent data before you start using the database.
|
||||
//
|
||||
// The default implementation does nothing.
|
||||
|
||||
|
||||
// Reserved for future use (directory versioning).
|
||||
// Perhaps invoke [self _clearAllResourcesForXMPPStream:nil] ?
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Public API
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPUserCoreDataStorageObject *)myUserForXMPPStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
XMPPJID *myJID = stream.myJID;
|
||||
if (myJID == nil)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [self userForJID:myJID xmppStream:stream managedObjectContext:moc];
|
||||
}
|
||||
|
||||
- (XMPPResourceCoreDataStorageObject *)myResourceForXMPPStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
XMPPJID *myJID = stream.myJID;
|
||||
if (myJID == nil)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [self resourceForJID:myJID xmppStream:stream managedObjectContext:moc];
|
||||
}
|
||||
|
||||
- (XMPPUserCoreDataStorageObject *)userForJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
if (jid == nil) return nil;
|
||||
if (moc == nil) return nil;
|
||||
|
||||
NSString *bareJIDStr = [jid bare];
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSPredicate *predicate;
|
||||
if (stream == nil)
|
||||
predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", bareJIDStr];
|
||||
else
|
||||
predicate = [NSPredicate predicateWithFormat:@"jidStr == %@ AND streamBareJidStr == %@",
|
||||
bareJIDStr, [[self myJIDForXMPPStream:stream] bare]];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setPredicate:predicate];
|
||||
[fetchRequest setIncludesPendingChanges:YES];
|
||||
[fetchRequest setFetchLimit:1];
|
||||
|
||||
NSArray *results = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
return (XMPPUserCoreDataStorageObject *)[results lastObject];
|
||||
}
|
||||
|
||||
- (XMPPResourceCoreDataStorageObject *)resourceForJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream
|
||||
managedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
if (jid == nil) return nil;
|
||||
if (moc == nil) return nil;
|
||||
|
||||
NSString *fullJIDStr = [jid full];
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPResourceCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSPredicate *predicate;
|
||||
if (stream == nil)
|
||||
predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", fullJIDStr];
|
||||
else
|
||||
predicate = [NSPredicate predicateWithFormat:@"jidStr == %@ AND streamBareJidStr == %@",
|
||||
fullJIDStr, [[self myJIDForXMPPStream:stream] bare]];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setPredicate:predicate];
|
||||
[fetchRequest setIncludesPendingChanges:YES];
|
||||
[fetchRequest setFetchLimit:1];
|
||||
|
||||
NSArray *results = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
return (XMPPResourceCoreDataStorageObject *)[results lastObject];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Protocol Private API
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
[rosterPopulationSet addObject:[NSNumber xmpp_numberWithPtr:(__bridge void *)stream]];
|
||||
|
||||
// Clear anything already in the roster core data store.
|
||||
//
|
||||
// Note: Deleting a user will delete all associated resources
|
||||
// because of the cascade rule in our core data model.
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setFetchBatchSize:saveThreshold];
|
||||
|
||||
if (stream)
|
||||
{
|
||||
NSPredicate *predicate;
|
||||
predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@",
|
||||
[[self myJIDForXMPPStream:stream] bare]];
|
||||
|
||||
[fetchRequest setPredicate:predicate];
|
||||
}
|
||||
|
||||
NSArray *allUsers = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
for (XMPPUserCoreDataStorageObject *user in allUsers)
|
||||
{
|
||||
[moc deleteObject:user];
|
||||
}
|
||||
|
||||
[XMPPGroupCoreDataStorageObject clearEmptyGroupsInManagedObjectContext:moc];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
[rosterPopulationSet removeObject:[NSNumber xmpp_numberWithPtr:(__bridge void *)stream]];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleRosterItem:(NSXMLElement *)itemSubElement xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
// Remember XML heirarchy memory management rules.
|
||||
// The passed parameter is a subnode of the IQ, and we need to pass it to an asynchronous operation.
|
||||
NSXMLElement *item = [itemSubElement copy];
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
if ([rosterPopulationSet containsObject:[NSNumber xmpp_numberWithPtr:(__bridge void *)stream]])
|
||||
{
|
||||
NSString *streamBareJidStr = [[self myJIDForXMPPStream:stream] bare];
|
||||
|
||||
[XMPPUserCoreDataStorageObject insertInManagedObjectContext:moc
|
||||
withItem:item
|
||||
streamBareJidStr:streamBareJidStr];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString *jidStr = [item attributeStringValueForName:@"jid"];
|
||||
XMPPJID *jid = [[XMPPJID jidWithString:jidStr] bareJID];
|
||||
|
||||
XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc];
|
||||
|
||||
NSString *subscription = [item attributeStringValueForName:@"subscription"];
|
||||
if ([subscription isEqualToString:@"remove"])
|
||||
{
|
||||
if (user)
|
||||
{
|
||||
[moc deleteObject:user];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user)
|
||||
{
|
||||
[user updateWithItem:item];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString *streamBareJidStr = [[self myJIDForXMPPStream:stream] bare];
|
||||
|
||||
[XMPPUserCoreDataStorageObject insertInManagedObjectContext:moc
|
||||
withItem:item
|
||||
streamBareJidStr:streamBareJidStr];
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
BOOL allowRosterlessOperation = [parent allowRosterlessOperation];
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
XMPPJID *jid = [presence from];
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
NSString *streamBareJidStr = [[self myJIDForXMPPStream:stream] bare];
|
||||
|
||||
XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc];
|
||||
|
||||
if (user == nil && allowRosterlessOperation)
|
||||
{
|
||||
// This may happen if the roster is in rosterlessOperation mode.
|
||||
|
||||
user = [XMPPUserCoreDataStorageObject insertInManagedObjectContext:moc
|
||||
withJID:[presence from]
|
||||
streamBareJidStr:streamBareJidStr];
|
||||
}
|
||||
|
||||
[user updateWithPresence:presence streamBareJidStr:streamBareJidStr];
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
__block BOOL result = NO;
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc];
|
||||
|
||||
result = (user != nil);
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (void)setPhoto:(UIImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
#else
|
||||
- (void)setPhoto:(NSImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
#endif
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc];
|
||||
|
||||
if (user)
|
||||
{
|
||||
user.photo = photo;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
[self _clearAllResourcesForXMPPStream:stream];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
// Note: Deleting a user will delete all associated resources
|
||||
// because of the cascade rule in our core data model.
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setFetchBatchSize:saveThreshold];
|
||||
|
||||
if (stream)
|
||||
{
|
||||
NSPredicate *predicate;
|
||||
predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@",
|
||||
[[self myJIDForXMPPStream:stream] bare]];
|
||||
|
||||
[fetchRequest setPredicate:predicate];
|
||||
}
|
||||
|
||||
NSArray *allUsers = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
NSUInteger unsavedCount = [self numberOfUnsavedChanges];
|
||||
|
||||
for (XMPPUserCoreDataStorageObject *user in allUsers)
|
||||
{
|
||||
[moc deleteObject:user];
|
||||
|
||||
if (++unsavedCount >= saveThreshold)
|
||||
{
|
||||
[self save];
|
||||
unsavedCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[XMPPGroupCoreDataStorageObject clearEmptyGroupsInManagedObjectContext:moc];
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream{
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
__block NSMutableArray *results = [NSMutableArray array];
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setFetchBatchSize:saveThreshold];
|
||||
|
||||
if (stream)
|
||||
{
|
||||
NSPredicate *predicate;
|
||||
predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@",
|
||||
[[self myJIDForXMPPStream:stream] bare]];
|
||||
|
||||
[fetchRequest setPredicate:predicate];
|
||||
}
|
||||
|
||||
NSArray *allUsers = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
for(XMPPUserCoreDataStorageObject *user in allUsers){
|
||||
[results addObject:[user.jid bareJID]];
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
- (void)getSubscription:(NSString **)subscription
|
||||
ask:(NSString **)ask
|
||||
nickname:(NSString **)nickname
|
||||
groups:(NSArray **)groups
|
||||
forJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
NSManagedObjectContext *moc = [self managedObjectContext];
|
||||
XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc];
|
||||
|
||||
if(user)
|
||||
{
|
||||
if(subscription)
|
||||
{
|
||||
*subscription = user.subscription;
|
||||
}
|
||||
|
||||
if(ask)
|
||||
{
|
||||
*ask = user.ask;
|
||||
}
|
||||
|
||||
if(nickname)
|
||||
{
|
||||
*nickname = user.nickname;
|
||||
}
|
||||
|
||||
if(groups)
|
||||
{
|
||||
if([user.groups count])
|
||||
{
|
||||
NSMutableArray *groupNames = [NSMutableArray array];
|
||||
|
||||
for(XMPPGroupCoreDataStorageObject *group in user.groups){
|
||||
[groupNames addObject:group.name];
|
||||
}
|
||||
|
||||
*groups = groupNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,79 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#endif
|
||||
|
||||
#import "XMPPUser.h"
|
||||
#import "XMPP.h"
|
||||
|
||||
@class XMPPGroupCoreDataStorageObject;
|
||||
@class XMPPResourceCoreDataStorageObject;
|
||||
|
||||
|
||||
@interface XMPPUserCoreDataStorageObject : NSManagedObject <XMPPUser>
|
||||
{
|
||||
NSInteger section;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) XMPPJID *jid;
|
||||
@property (nonatomic, strong) NSString * jidStr;
|
||||
@property (nonatomic, strong) NSString * streamBareJidStr;
|
||||
|
||||
@property (nonatomic, strong) NSString * nickname;
|
||||
@property (nonatomic, strong) NSString * displayName;
|
||||
@property (nonatomic, strong) NSString * subscription;
|
||||
@property (nonatomic, strong) NSString * ask;
|
||||
@property (nonatomic, strong) NSNumber * unreadMessages;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@property (nonatomic, strong) UIImage *photo;
|
||||
#else
|
||||
@property (nonatomic, strong) NSImage *photo;
|
||||
#endif
|
||||
|
||||
@property (nonatomic, assign) NSInteger section;
|
||||
@property (nonatomic, strong) NSString * sectionName;
|
||||
@property (nonatomic, strong) NSNumber * sectionNum;
|
||||
|
||||
@property (nonatomic, strong) NSSet * groups;
|
||||
@property (nonatomic, strong) XMPPResourceCoreDataStorageObject * primaryResource;
|
||||
@property (nonatomic, strong) NSSet * resources;
|
||||
|
||||
+ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
withJID:(XMPPJID *)jid
|
||||
streamBareJidStr:(NSString *)streamBareJidStr;
|
||||
|
||||
+ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
withItem:(NSXMLElement *)item
|
||||
streamBareJidStr:(NSString *)streamBareJidStr;
|
||||
|
||||
- (void)updateWithItem:(NSXMLElement *)item;
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence streamBareJidStr:(NSString *)streamBareJidStr;
|
||||
- (void)recalculatePrimaryResource;
|
||||
|
||||
- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another;
|
||||
- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another options:(NSStringCompareOptions)mask;
|
||||
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another;
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another
|
||||
options:(NSStringCompareOptions)mask;
|
||||
|
||||
@end
|
||||
|
||||
@interface XMPPUserCoreDataStorageObject (CoreDataGeneratedAccessors)
|
||||
|
||||
- (void)addResourcesObject:(XMPPResourceCoreDataStorageObject *)value;
|
||||
- (void)removeResourcesObject:(XMPPResourceCoreDataStorageObject *)value;
|
||||
- (void)addResources:(NSSet *)value;
|
||||
- (void)removeResources:(NSSet *)value;
|
||||
|
||||
- (void)addGroupsObject:(XMPPGroupCoreDataStorageObject *)value;
|
||||
- (void)removeGroupsObject:(XMPPGroupCoreDataStorageObject *)value;
|
||||
- (void)addGroups:(NSSet *)value;
|
||||
- (void)removeGroups:(NSSet *)value;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,493 @@
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRosterCoreDataStorage.h"
|
||||
#import "XMPPUserCoreDataStorageObject.h"
|
||||
#import "XMPPResourceCoreDataStorageObject.h"
|
||||
#import "XMPPGroupCoreDataStorageObject.h"
|
||||
#import "NSNumber+XMPP.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_INFO; // | XMPP_LOG_FLAG_TRACE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
@interface XMPPUserCoreDataStorageObject ()
|
||||
|
||||
@property(nonatomic,strong) XMPPJID *primitiveJid;
|
||||
@property(nonatomic,strong) NSString *primitiveJidStr;
|
||||
|
||||
@property(nonatomic,strong) NSString *primitiveDisplayName;
|
||||
@property(nonatomic,assign) NSInteger primitiveSection;
|
||||
@property(nonatomic,strong) NSString *primitiveSectionName;
|
||||
@property(nonatomic,strong) NSNumber *primitiveSectionNum;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPUserCoreDataStorageObject
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Accessors
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@dynamic jid, primitiveJid;
|
||||
@dynamic jidStr, primitiveJidStr;
|
||||
@dynamic streamBareJidStr;
|
||||
|
||||
@dynamic nickname;
|
||||
@dynamic displayName, primitiveDisplayName;
|
||||
@dynamic subscription;
|
||||
@dynamic ask;
|
||||
@dynamic unreadMessages;
|
||||
@dynamic photo;
|
||||
|
||||
@dynamic section, primitiveSection;
|
||||
@dynamic sectionName, primitiveSectionName;
|
||||
@dynamic sectionNum, primitiveSectionNum;
|
||||
|
||||
@dynamic groups;
|
||||
@dynamic primaryResource;
|
||||
@dynamic resources;
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
// Create and cache the jid on demand
|
||||
|
||||
[self willAccessValueForKey:@"jid"];
|
||||
XMPPJID *tmp = [self primitiveJid];
|
||||
[self didAccessValueForKey:@"jid"];
|
||||
|
||||
if (tmp == nil) {
|
||||
tmp = [XMPPJID jidWithString:[self jidStr]];
|
||||
|
||||
[self setPrimitiveJid:tmp];
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setJid:(XMPPJID *)jid
|
||||
{
|
||||
self.jidStr = [jid bare];
|
||||
}
|
||||
|
||||
- (void)setJidStr:(NSString *)jidStr
|
||||
{
|
||||
[self willChangeValueForKey:@"jidStr"];
|
||||
[self setPrimitiveJidStr:jidStr];
|
||||
[self didChangeValueForKey:@"jidStr"];
|
||||
|
||||
// If the jidStr changes, the jid becomes invalid.
|
||||
[self setPrimitiveJid:nil];
|
||||
}
|
||||
|
||||
- (NSInteger)section
|
||||
{
|
||||
// Create and cache the section on demand
|
||||
[self willAccessValueForKey:@"section"];
|
||||
NSInteger tmp = [self primitiveSection];
|
||||
[self didAccessValueForKey:@"section"];
|
||||
|
||||
// section uses zero, so to distinguish unset values, use NSNotFound
|
||||
if (tmp == NSNotFound) {
|
||||
tmp = [[self sectionNum] integerValue];
|
||||
|
||||
[self setPrimitiveSection:tmp];
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setSection:(NSInteger)value
|
||||
{
|
||||
self.sectionNum = @(value);
|
||||
}
|
||||
|
||||
- (NSInteger)primitiveSection
|
||||
{
|
||||
return section;
|
||||
}
|
||||
|
||||
- (void)setPrimitiveSection:(NSInteger)primitiveSection
|
||||
{
|
||||
section = primitiveSection;
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)setSectionNum:(NSNumber *)sectionNum
|
||||
{
|
||||
[self willChangeValueForKey:@"sectionNum"];
|
||||
[self setPrimitiveSectionNum:sectionNum];
|
||||
[self didChangeValueForKey:@"sectionNum"];
|
||||
|
||||
// If the sectionNum changes, the section becomes invalid.
|
||||
// section uses zero, so to distinguish unset values, use NSNotFound
|
||||
[self setPrimitiveSection:NSNotFound];
|
||||
}
|
||||
|
||||
- (NSString *)sectionName
|
||||
{
|
||||
// Create and cache the sectionName on demand
|
||||
|
||||
[self willAccessValueForKey:@"sectionName"];
|
||||
NSString *tmp = [self primitiveSectionName];
|
||||
[self didAccessValueForKey:@"sectionName"];
|
||||
|
||||
if (tmp == nil) {
|
||||
// Section names are organized by capitalizing the first letter of the displayName
|
||||
|
||||
NSString *upperCase = [self.displayName uppercaseString];
|
||||
|
||||
// return the first character with support UTF-16:
|
||||
tmp = [upperCase substringWithRange:[upperCase rangeOfComposedCharacterSequenceAtIndex:0]];
|
||||
|
||||
[self setPrimitiveSectionName:tmp];
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)setDisplayName:(NSString *)displayName
|
||||
{
|
||||
[self willChangeValueForKey:@"displayName"];
|
||||
[self setPrimitiveDisplayName:displayName];
|
||||
[self didChangeValueForKey:@"displayName"];
|
||||
|
||||
// If the displayName changes, the sectionName becomes invalid.
|
||||
[self setPrimitiveSectionName:nil];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark NSManagedObject
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)awakeFromInsert
|
||||
{
|
||||
// Section uses zero, so to distinguish unset values, use NSNotFound.
|
||||
|
||||
self.primitiveSection = NSNotFound;
|
||||
}
|
||||
|
||||
- (void)awakeFromFetch
|
||||
{
|
||||
// Section uses zero, so to distinguish unset values, use NSNotFound.
|
||||
//
|
||||
// Note: Do NOT use "self.section = NSNotFound" as this will in turn set the sectionNum.
|
||||
|
||||
self.primitiveSection = NSNotFound;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Creation & Updates
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
withJID:(XMPPJID *)jid
|
||||
streamBareJidStr:(NSString *)streamBareJidStr
|
||||
{
|
||||
if (jid == nil)
|
||||
{
|
||||
XMPPLogVerbose(@"XMPPUserCoreDataStorageObject: invalid jid (nil)");
|
||||
return nil;
|
||||
}
|
||||
|
||||
XMPPUserCoreDataStorageObject *newUser;
|
||||
newUser = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPUserCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
newUser.streamBareJidStr = streamBareJidStr;
|
||||
|
||||
newUser.jid = jid;
|
||||
newUser.nickname = nil;
|
||||
|
||||
newUser.displayName = [jid bare];
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
+ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
withItem:(NSXMLElement *)item
|
||||
streamBareJidStr:(NSString *)streamBareJidStr
|
||||
{
|
||||
NSString *jidStr = [item attributeStringValueForName:@"jid"];
|
||||
XMPPJID *jid = [XMPPJID jidWithString:jidStr];
|
||||
|
||||
if (jid == nil)
|
||||
{
|
||||
XMPPLogVerbose(@"XMPPUserCoreDataStorageObject: invalid item (missing or invalid jid): %@", item);
|
||||
return nil;
|
||||
}
|
||||
|
||||
XMPPUserCoreDataStorageObject *newUser;
|
||||
newUser = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPUserCoreDataStorageObject"
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
newUser.streamBareJidStr = streamBareJidStr;
|
||||
|
||||
[newUser updateWithItem:item];
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
- (void)updateGroupsWithItem:(NSXMLElement *)item
|
||||
{
|
||||
XMPPGroupCoreDataStorageObject *group = nil;
|
||||
|
||||
// clear existing group memberships first
|
||||
if ([self.groups count] > 0) {
|
||||
[self removeGroups:self.groups];
|
||||
}
|
||||
|
||||
NSArray *groupItems = [item elementsForName:@"group"];
|
||||
NSString *groupName = nil;
|
||||
|
||||
for (NSXMLElement *groupElement in groupItems) {
|
||||
groupName = [groupElement stringValue];
|
||||
|
||||
group = [XMPPGroupCoreDataStorageObject fetchOrInsertGroupName:groupName
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
if (group != nil) {
|
||||
[self addGroupsObject:group];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateWithItem:(NSXMLElement *)item
|
||||
{
|
||||
NSString *jidStr = [item attributeStringValueForName:@"jid"];
|
||||
XMPPJID *jid = [XMPPJID jidWithString:jidStr];
|
||||
|
||||
if (jid == nil)
|
||||
{
|
||||
XMPPLogVerbose(@"XMPPUserCoreDataStorageObject: invalid item (missing or invalid jid): %@", item);
|
||||
return;
|
||||
}
|
||||
|
||||
self.jid = jid;
|
||||
self.nickname = [item attributeStringValueForName:@"name"];
|
||||
|
||||
self.displayName = self.nickname ? : jidStr;
|
||||
|
||||
self.subscription = [item attributeStringValueForName:@"subscription"];
|
||||
self.ask = [item attributeStringValueForName:@"ask"];
|
||||
|
||||
[self updateGroupsWithItem:item];
|
||||
}
|
||||
|
||||
- (void)recalculatePrimaryResource
|
||||
{
|
||||
self.primaryResource = nil;
|
||||
|
||||
NSArray *sortedResources = [[self allResources] sortedArrayUsingSelector:@selector(compare:)];
|
||||
if ([sortedResources count] > 0)
|
||||
{
|
||||
XMPPResourceCoreDataStorageObject *resource = sortedResources[0];
|
||||
|
||||
// Primary resource must have a non-negative priority
|
||||
if ([resource priority] >= 0)
|
||||
{
|
||||
self.primaryResource = resource;
|
||||
|
||||
if (resource.intShow >= 3)
|
||||
self.section = 0;
|
||||
else
|
||||
self.section = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.primaryResource == nil)
|
||||
{
|
||||
self.section = 2;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence streamBareJidStr:(NSString *)streamBareJidStr
|
||||
{
|
||||
XMPPResourceCoreDataStorageObject *resource =
|
||||
(XMPPResourceCoreDataStorageObject *)[self resourceForJID:[presence from]];
|
||||
|
||||
if ([[presence type] isEqualToString:@"unavailable"] || [presence isErrorPresence])
|
||||
{
|
||||
if (resource)
|
||||
{
|
||||
[self removeResourcesObject:resource];
|
||||
[[self managedObjectContext] deleteObject:resource];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resource)
|
||||
{
|
||||
[resource updateWithPresence:presence];
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPResourceCoreDataStorageObject *newResource;
|
||||
newResource = [XMPPResourceCoreDataStorageObject insertInManagedObjectContext:[self managedObjectContext]
|
||||
withPresence:presence
|
||||
streamBareJidStr:streamBareJidStr];
|
||||
|
||||
[self addResourcesObject:newResource];
|
||||
}
|
||||
}
|
||||
|
||||
[self recalculatePrimaryResource];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPUser Protocol
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)isOnline
|
||||
{
|
||||
return (self.primaryResource != nil);
|
||||
}
|
||||
|
||||
- (BOOL)isPendingApproval
|
||||
{
|
||||
// Either of the following mean we're waiting to have our presence subscription approved:
|
||||
// <item ask='subscribe' subscription='none' jid='robbiehanson@deusty.com'/>
|
||||
// <item ask='subscribe' subscription='from' jid='robbiehanson@deusty.com'/>
|
||||
|
||||
NSString *subscription = self.subscription;
|
||||
NSString *ask = self.ask;
|
||||
|
||||
if ([subscription isEqualToString:@"none"] || [subscription isEqualToString:@"from"])
|
||||
{
|
||||
if ([ask isEqualToString:@"subscribe"])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (id <XMPPResource>)resourceForJID:(XMPPJID *)jid
|
||||
{
|
||||
NSString *jidStr = [jid full];
|
||||
|
||||
for (XMPPResourceCoreDataStorageObject *resource in [self resources])
|
||||
{
|
||||
if ([jidStr isEqualToString:[resource jidStr]])
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray *)allResources
|
||||
{
|
||||
NSMutableArray *allResources = [NSMutableArray array];
|
||||
|
||||
for (XMPPResourceCoreDataStorageObject *resource in [[self resources] allObjects]) {
|
||||
|
||||
if(![resource isDeleted])
|
||||
{
|
||||
[allResources addObject:resource];
|
||||
}
|
||||
}
|
||||
|
||||
return allResources;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Comparisons
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Returns the result of invoking compareByName:options: with no options.
|
||||
**/
|
||||
- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another
|
||||
{
|
||||
return [self compareByName:another options:0];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compares the two users according to their display name.
|
||||
*
|
||||
* Options for the search — you can combine any of the following using a C bitwise OR operator:
|
||||
* NSCaseInsensitiveSearch, NSLiteralSearch, NSNumericSearch.
|
||||
* See "String Programming Guide for Cocoa" for details on these options.
|
||||
**/
|
||||
- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another options:(NSStringCompareOptions)mask
|
||||
{
|
||||
NSString *myName = [self displayName];
|
||||
NSString *theirName = [another displayName];
|
||||
|
||||
return [myName compare:theirName options:mask];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of invoking compareByAvailabilityName:options: with no options.
|
||||
**/
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another
|
||||
{
|
||||
return [self compareByAvailabilityName:another options:0];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compares the two users according to availability first, and then display name.
|
||||
* Thus available users come before unavailable users.
|
||||
* If both users are available, or both users are not available,
|
||||
* this method follows the same functionality as the compareByName:options: as documented above.
|
||||
**/
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another
|
||||
options:(NSStringCompareOptions)mask
|
||||
{
|
||||
if ([self isOnline])
|
||||
{
|
||||
if ([another isOnline])
|
||||
return [self compareByName:another options:mask];
|
||||
else
|
||||
return NSOrderedAscending;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ([another isOnline])
|
||||
return NSOrderedDescending;
|
||||
else
|
||||
return [self compareByName:another options:mask];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark KVO compliance methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingJid {
|
||||
// If the jidStr changes, the jid may change as well.
|
||||
return [NSSet setWithObject:@"jidStr"];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingIsOnline {
|
||||
return [NSSet setWithObject:@"primaryResource"];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingSection {
|
||||
// If the value of sectionNum changes, the section may change as well.
|
||||
return [NSSet setWithObject:@"sectionNum"];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingSectionName {
|
||||
// If the value of displayName changes, the sectionName may change as well.
|
||||
return [NSSet setWithObject:@"displayName"];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingAllResources {
|
||||
return [NSSet setWithObject:@"resources"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPResource.h"
|
||||
|
||||
@class XMPPJID;
|
||||
@class XMPPIQ;
|
||||
@class XMPPPresence;
|
||||
|
||||
|
||||
@interface XMPPResourceMemoryStorageObject : NSObject <XMPPResource, NSCopying, NSSecureCoding>
|
||||
{
|
||||
XMPPJID *jid;
|
||||
XMPPPresence *presence;
|
||||
|
||||
NSDate *presenceDate;
|
||||
}
|
||||
|
||||
/* From the XMPPResource protocol
|
||||
|
||||
- (XMPPJID *)jid;
|
||||
- (XMPPPresence *)presence;
|
||||
|
||||
- (NSDate *)presenceDate;
|
||||
|
||||
- (NSComparisonResult)compare:(id <XMPPResource>)another;
|
||||
|
||||
*/
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,206 @@
|
||||
#import "XMPP.h"
|
||||
#import "NSXMLElement+XEP_0203.h"
|
||||
#import "XMPPRosterMemoryStoragePrivate.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
|
||||
@implementation XMPPResourceMemoryStorageObject
|
||||
|
||||
- (id)initWithPresence:(XMPPPresence *)aPresence
|
||||
{
|
||||
if((self = [super init]))
|
||||
{
|
||||
jid = [aPresence from];
|
||||
presence = aPresence;
|
||||
|
||||
presenceDate = [presence delayedDeliveryDate];
|
||||
if (presenceDate == nil)
|
||||
{
|
||||
presenceDate = [[NSDate alloc] init];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Copying
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
// We use [self class] to support subclassing
|
||||
|
||||
XMPPResourceMemoryStorageObject *deepCopy = (XMPPResourceMemoryStorageObject *)[[[self class] alloc] init];
|
||||
|
||||
deepCopy->jid = [jid copy];
|
||||
deepCopy->presence = presence; // No need to bother with a copy sicne we don't alter presence
|
||||
deepCopy->presenceDate = [presenceDate copy];
|
||||
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#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])
|
||||
{
|
||||
jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"];
|
||||
presence = [coder decodeObjectOfClass:[XMPPPresence class] forKey:@"presence"];
|
||||
presenceDate = [coder decodeObjectOfClass:[NSDate class] forKey:@"presenceDate"];
|
||||
}
|
||||
else
|
||||
{
|
||||
jid = [coder decodeObjectForKey:@"jid"];
|
||||
presence = [coder decodeObjectForKey:@"presence"];
|
||||
presenceDate = [coder decodeObjectForKey:@"presenceDate"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
jid = [coder decodeObject];
|
||||
presence = [coder decodeObject];
|
||||
presenceDate = [coder decodeObject];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
[coder encodeObject:jid forKey:@"jid"];
|
||||
[coder encodeObject:presence forKey:@"presence"];
|
||||
[coder encodeObject:presenceDate forKey:@"presenceDate"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[coder encodeObject:jid];
|
||||
[coder encodeObject:presence];
|
||||
[coder encodeObject:presenceDate];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Standard Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
return jid;
|
||||
}
|
||||
|
||||
- (XMPPPresence *)presence
|
||||
{
|
||||
return presence;
|
||||
}
|
||||
|
||||
- (NSDate *)presenceDate
|
||||
{
|
||||
return presenceDate;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Update Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)aPresence
|
||||
{
|
||||
presence = aPresence;
|
||||
|
||||
presenceDate = [presence delayedDeliveryDate];
|
||||
if (presenceDate == nil)
|
||||
{
|
||||
presenceDate = [[NSDate alloc] init];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Comparison Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSComparisonResult)compare:(id <XMPPResource>)another
|
||||
{
|
||||
XMPPPresence *mp = [self presence];
|
||||
XMPPPresence *ap = [another presence];
|
||||
|
||||
int mpp = [mp priority];
|
||||
int app = [ap priority];
|
||||
|
||||
if(mpp < app)
|
||||
return NSOrderedDescending;
|
||||
if(mpp > app)
|
||||
return NSOrderedAscending;
|
||||
|
||||
// Priority is the same.
|
||||
// Determine who is more available based on their show.
|
||||
int mps = [mp intShow];
|
||||
int aps = [ap intShow];
|
||||
|
||||
if(mps < aps)
|
||||
return NSOrderedDescending;
|
||||
if(mps > aps)
|
||||
return NSOrderedAscending;
|
||||
|
||||
// Priority and Show are the same.
|
||||
// Determine based on who was the last to receive a presence element.
|
||||
NSDate *mpd = [self presenceDate];
|
||||
NSDate *apd = [another presenceDate];
|
||||
|
||||
return [mpd compare:apd];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark NSObject Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [jid hash];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
if([anObject isMemberOfClass:[self class]])
|
||||
{
|
||||
XMPPResourceMemoryStorageObject *another = (XMPPResourceMemoryStorageObject *)anObject;
|
||||
|
||||
return [jid isEqualToJID:[another jid]];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<XMPPResource[%p]: %@>", self, [jid full]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,128 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPRoster.h"
|
||||
#import "XMPPUserMemoryStorageObject.h"
|
||||
#import "XMPPResourceMemoryStorageObject.h"
|
||||
|
||||
|
||||
/**
|
||||
* This class is an example implementation of the XMPPRosterStorage protocol.
|
||||
* It simply keeps all roster informatin in memory.
|
||||
*
|
||||
* You are free to substitute your own storage class.
|
||||
**/
|
||||
|
||||
@interface XMPPRosterMemoryStorage : NSObject <XMPPRosterStorage>
|
||||
{
|
||||
#if __has_feature(objc_arc_weak)
|
||||
__weak XMPPRoster *parent;
|
||||
#else
|
||||
__unsafe_unretained XMPPRoster *parent;
|
||||
#endif
|
||||
dispatch_queue_t parentQueue;
|
||||
void *parentQueueTag;
|
||||
|
||||
Class userClass;
|
||||
Class resourceClass;
|
||||
|
||||
BOOL isRosterPopulation;
|
||||
NSMutableDictionary *roster;
|
||||
|
||||
XMPPJID *myJID;
|
||||
XMPPUserMemoryStorageObject *myUser;
|
||||
}
|
||||
|
||||
- (id)init;
|
||||
|
||||
@property (readonly) XMPPRoster *parent;
|
||||
|
||||
/**
|
||||
* You can optionally extend the XMPPUserMemoryStorage and XMPPResourceMemoryStorage classes.
|
||||
* Then just set the classes here, and your subclasses will automatically get used.
|
||||
**/
|
||||
@property (readwrite, assign) Class userClass;
|
||||
@property (readwrite, assign) Class resourceClass;
|
||||
|
||||
/**
|
||||
* The methods below provide access to the roster data.
|
||||
*
|
||||
* If invoked from a dispatch queue other than the roster's queue,
|
||||
* the methods return snapshots (copies) of the roster data.
|
||||
* These snapshots provide a thread-safe version of the roster data.
|
||||
* The thread-safety comes from the fact that the copied data will not be altered,
|
||||
* so it can therefore be used from multiple threads/queues if needed.
|
||||
**/
|
||||
|
||||
- (XMPPUserMemoryStorageObject *)myUser;
|
||||
- (XMPPResourceMemoryStorageObject *)myResource;
|
||||
|
||||
- (XMPPUserMemoryStorageObject *)userForJID:(XMPPJID *)jid;
|
||||
- (XMPPResourceMemoryStorageObject *)resourceForJID:(XMPPJID *)jid;
|
||||
|
||||
- (NSArray *)sortedUsersByName;
|
||||
- (NSArray *)sortedUsersByAvailabilityName;
|
||||
|
||||
- (NSArray *)sortedAvailableUsersByName;
|
||||
- (NSArray *)sortedUnavailableUsersByName;
|
||||
|
||||
- (NSArray *)unsortedUsers;
|
||||
- (NSArray *)unsortedAvailableUsers;
|
||||
- (NSArray *)unsortedUnavailableUsers;
|
||||
|
||||
- (NSArray *)sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRosterMemoryStorageDelegate
|
||||
@optional
|
||||
|
||||
/**
|
||||
* The XMPPRosterStorage classes use the same delegate as their parent XMPPRoster.
|
||||
**/
|
||||
|
||||
/**
|
||||
* Catch-all change notification.
|
||||
*
|
||||
* When the roster changes, for any of the reasons listed below, this delegate method fires.
|
||||
* This method always fires after the more precise delegate methods listed below.
|
||||
**/
|
||||
- (void)xmppRosterDidChange:(XMPPRosterMemoryStorage *)sender;
|
||||
|
||||
/**
|
||||
* Notification that the roster has received the roster from the server.
|
||||
*
|
||||
* If parent.autoFetchRoster is YES, the roster will automatically be fetched once the user authenticates.
|
||||
**/
|
||||
- (void)xmppRosterDidPopulate:(XMPPRosterMemoryStorage *)sender;
|
||||
|
||||
/**
|
||||
* Notifications that the roster has changed.
|
||||
*
|
||||
* This includes times when users are added or removed from our roster, or when a nickname is changed,
|
||||
* including when other resources logged in under the same user account as us make changes to our roster.
|
||||
*
|
||||
* This does not include when resources simply go online / offline.
|
||||
**/
|
||||
- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender didAddUser:(XMPPUserMemoryStorageObject *)user;
|
||||
- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender didUpdateUser:(XMPPUserMemoryStorageObject *)user;
|
||||
- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender didRemoveUser:(XMPPUserMemoryStorageObject *)user;
|
||||
|
||||
/**
|
||||
* Notifications when resources go online / offline.
|
||||
**/
|
||||
- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender
|
||||
didAddResource:(XMPPResourceMemoryStorageObject *)resource
|
||||
withUser:(XMPPUserMemoryStorageObject *)user;
|
||||
|
||||
- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender
|
||||
didUpdateResource:(XMPPResourceMemoryStorageObject *)resource
|
||||
withUser:(XMPPUserMemoryStorageObject *)user;
|
||||
|
||||
- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender
|
||||
didRemoveResource:(XMPPResourceMemoryStorageObject *)resource
|
||||
withUser:(XMPPUserMemoryStorageObject *)user;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,860 @@
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRosterPrivate.h"
|
||||
#import "XMPPRosterMemoryStorage.h"
|
||||
#import "XMPPRosterMemoryStoragePrivate.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 XMPPRosterMemoryStorage ()
|
||||
|
||||
@property (readonly) dispatch_queue_t parentQueue;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPRosterMemoryStorage
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
userClass = [XMPPUserMemoryStorageObject class];
|
||||
resourceClass = [XMPPResourceMemoryStorageObject class];
|
||||
|
||||
roster = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue
|
||||
{
|
||||
NSParameterAssert(aParent != nil);
|
||||
NSParameterAssert(queue != NULL);
|
||||
|
||||
@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
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
if (parentQueue)
|
||||
dispatch_release(parentQueue);
|
||||
#endif
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Properties
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@synthesize userClass;
|
||||
@synthesize resourceClass;
|
||||
|
||||
- (XMPPRoster *)parent
|
||||
{
|
||||
XMPPRoster *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
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (GCDMulticastDelegate <XMPPRosterMemoryStorageDelegate> *)multicastDelegate
|
||||
{
|
||||
return (GCDMulticastDelegate <XMPPRosterMemoryStorageDelegate> *)[parent multicastDelegate];
|
||||
}
|
||||
|
||||
- (XMPPUserMemoryStorageObject *)_userForJID:(XMPPJID *)jid
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
XMPPUserMemoryStorageObject *result = roster[[jid bareJID]];
|
||||
|
||||
if (result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
XMPPJID *myBareJID = [myJID bareJID];
|
||||
XMPPJID *bareJID = [jid bareJID];
|
||||
|
||||
if ([bareJID isEqualToJID:myBareJID])
|
||||
{
|
||||
return myUser;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (XMPPResourceMemoryStorageObject *)_resourceForJID:(XMPPJID *)jid
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
XMPPUserMemoryStorageObject *user = [self _userForJID:jid];
|
||||
return (XMPPResourceMemoryStorageObject *)[user resourceForJID:jid];
|
||||
}
|
||||
|
||||
- (NSArray *)_unsortedUsers
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
return [roster allValues];
|
||||
}
|
||||
|
||||
- (NSArray *)_unsortedAvailableUsers
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
NSArray *allUsers = [roster allValues];
|
||||
|
||||
NSMutableArray *result = [NSMutableArray arrayWithCapacity:[allUsers count]];
|
||||
|
||||
for (id <XMPPUser> user in allUsers)
|
||||
{
|
||||
if ([user isOnline])
|
||||
{
|
||||
[result addObject:user];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray *)_unsortedUnavailableUsers
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
NSArray *allUsers = [roster allValues];
|
||||
|
||||
NSMutableArray *result = [NSMutableArray arrayWithCapacity:[allUsers count]];
|
||||
|
||||
for (id <XMPPUser> user in allUsers)
|
||||
{
|
||||
if (![user isOnline])
|
||||
{
|
||||
[result addObject:user];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray *)_sortedUsersByName
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
return [[roster allValues] sortedArrayUsingSelector:@selector(compareByName:)];
|
||||
}
|
||||
|
||||
- (NSArray *)_sortedUsersByAvailabilityName
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
return [[roster allValues] sortedArrayUsingSelector:@selector(compareByAvailabilityName:)];
|
||||
}
|
||||
|
||||
- (NSArray *)_sortedAvailableUsersByName
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
return [[self _unsortedAvailableUsers] sortedArrayUsingSelector:@selector(compareByName:)];
|
||||
}
|
||||
|
||||
- (NSArray *)_sortedUnavailableUsersByName
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
return [[self _unsortedUnavailableUsers] sortedArrayUsingSelector:@selector(compareByName:)];
|
||||
}
|
||||
|
||||
- (NSArray *)_sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself
|
||||
{
|
||||
AssertPrivateQueue();
|
||||
|
||||
// Add all the resouces from all the available users in the roster
|
||||
//
|
||||
// Remember: There may be multiple resources per user
|
||||
|
||||
NSArray *availableUsers = [self unsortedAvailableUsers];
|
||||
|
||||
NSMutableArray *result = [NSMutableArray arrayWithCapacity:[availableUsers count]];
|
||||
|
||||
for (id<XMPPUser> user in availableUsers)
|
||||
{
|
||||
[result addObjectsFromArray:[user allResources]];
|
||||
}
|
||||
|
||||
if (includeResourcesForMyUserExcludingMyself)
|
||||
{
|
||||
// Now add all the available resources from our own user account (excluding ourselves)
|
||||
|
||||
NSArray *myResources = [myUser allResources];
|
||||
|
||||
for (id<XMPPResource> resource in myResources)
|
||||
{
|
||||
if (![myJID isEqualToJID:[resource jid]])
|
||||
{
|
||||
[result addObject:resource];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [result sortedArrayUsingSelector:@selector(compare:)];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Roster Management
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPUserMemoryStorageObject *)myUser
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return myUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block XMPPUserMemoryStorageObject *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{
|
||||
result = [myUser copy];
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (XMPPResourceMemoryStorageObject *)myResource
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return (XMPPResourceMemoryStorageObject *)[myUser resourceForJID:myJID];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block XMPPResourceMemoryStorageObject *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{
|
||||
XMPPResourceMemoryStorageObject *resource =
|
||||
(XMPPResourceMemoryStorageObject *)[myUser resourceForJID:myJID];
|
||||
result = [resource copy];
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (XMPPUserMemoryStorageObject *)userForJID:(XMPPJID *)jid
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _userForJID:jid];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block XMPPUserMemoryStorageObject *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
XMPPUserMemoryStorageObject *user = [self _userForJID:jid];
|
||||
result = [user copy];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (XMPPResourceMemoryStorageObject *)resourceForJID:(XMPPJID *)jid
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _resourceForJID:jid];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block XMPPResourceMemoryStorageObject *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
XMPPResourceMemoryStorageObject *resource = [self _resourceForJID:jid];
|
||||
result = [resource copy];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)sortedUsersByName
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _sortedUsersByName];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _sortedUsersByName];
|
||||
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)sortedUsersByAvailabilityName
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _sortedUsersByAvailabilityName];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _sortedUsersByAvailabilityName];
|
||||
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)sortedAvailableUsersByName
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _sortedAvailableUsersByName];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _sortedAvailableUsersByName];
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)sortedUnavailableUsersByName
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _sortedUnavailableUsersByName];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _sortedUnavailableUsersByName];
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)unsortedUsers
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _unsortedUsers];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _unsortedUsers];
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)unsortedAvailableUsers
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _unsortedAvailableUsers];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _unsortedAvailableUsers];
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)unsortedUnavailableUsers
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _unsortedUnavailableUsers];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _unsortedUnavailableUsers];
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself
|
||||
{
|
||||
// This is a public method, so it may be invoked on any thread/queue.
|
||||
|
||||
if (self.parentQueue == NULL)
|
||||
{
|
||||
// Haven't been attached to parent yet
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (dispatch_get_specific(parentQueueTag))
|
||||
{
|
||||
return [self _sortedResources:includeResourcesForMyUserExcludingMyself];
|
||||
}
|
||||
else
|
||||
{
|
||||
__block NSArray *result;
|
||||
|
||||
dispatch_sync(parentQueue, ^{ @autoreleasepool {
|
||||
|
||||
NSArray *temp = [self _sortedResources:includeResourcesForMyUserExcludingMyself];
|
||||
result = [[NSArray alloc] initWithArray:temp copyItems:YES];
|
||||
|
||||
}});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPRosterStorage Protocol
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
isRosterPopulation = YES;
|
||||
|
||||
myJID = self.parent.xmppStream.myJID;
|
||||
|
||||
myUser = [[self.userClass alloc] initWithJID:myJID];
|
||||
}
|
||||
|
||||
- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
isRosterPopulation = NO;
|
||||
|
||||
[[self multicastDelegate] xmppRosterDidPopulate:self];
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
|
||||
- (void)handleRosterItem:(NSXMLElement *)item xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
NSString *jidStr = [item attributeStringValueForName:@"jid"];
|
||||
XMPPJID *jid = [[XMPPJID jidWithString:jidStr] bareJID];
|
||||
|
||||
if (isRosterPopulation)
|
||||
{
|
||||
XMPPUserMemoryStorageObject *newUser =
|
||||
(XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithItem:item];
|
||||
|
||||
roster[jid] = newUser;
|
||||
|
||||
XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString *subscription = [item attributeStringValueForName:@"subscription"];
|
||||
|
||||
if ([subscription isEqualToString:@"remove"])
|
||||
{
|
||||
XMPPUserMemoryStorageObject *user = roster[jid];
|
||||
if (user)
|
||||
{
|
||||
[roster removeObjectForKey:jid];
|
||||
|
||||
XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
|
||||
|
||||
[[self multicastDelegate] xmppRoster:self didRemoveUser:user];
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPUserMemoryStorageObject *user = roster[jid];
|
||||
if (user)
|
||||
{
|
||||
[user updateWithItem:item];
|
||||
|
||||
XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
|
||||
|
||||
[[self multicastDelegate] xmppRoster:self didUpdateUser:user];
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
else
|
||||
{
|
||||
XMPPUserMemoryStorageObject *newUser =
|
||||
(XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithItem:item];
|
||||
|
||||
roster[jid] = newUser;
|
||||
|
||||
XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
|
||||
|
||||
[[self multicastDelegate] xmppRoster:self didAddUser:newUser];
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
int change = XMPP_USER_NO_CHANGE;
|
||||
|
||||
XMPPUserMemoryStorageObject *user = nil;
|
||||
XMPPResourceMemoryStorageObject *resource = nil;
|
||||
|
||||
XMPPJID *jidKey = [[presence from] bareJID];
|
||||
|
||||
user = roster[jidKey];
|
||||
if (user == nil)
|
||||
{
|
||||
// Not a presence element from anyone in our roster (that we know of).
|
||||
|
||||
if ([[myJID bareJID] isEqualToJID:jidKey])
|
||||
{
|
||||
// It's a presence element for our user, either our resource or another resource.
|
||||
|
||||
user = myUser;
|
||||
}
|
||||
else if([parent allowRosterlessOperation])
|
||||
{
|
||||
// Unknown user (this is the first time we've encountered them).
|
||||
// This happens if the roster is in rosterlessOperation mode.
|
||||
|
||||
user = (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithJID:jidKey];
|
||||
|
||||
roster[jidKey] = user;
|
||||
|
||||
[[self multicastDelegate] xmppRoster:self didAddUser:user];
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
}
|
||||
|
||||
change = [user updateWithPresence:presence resourceClass:self.resourceClass andGetResource:&resource];
|
||||
|
||||
XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster);
|
||||
|
||||
if (change == XMPP_USER_ADDED_RESOURCE)
|
||||
[[self multicastDelegate] xmppRoster:self didAddResource:resource withUser:user];
|
||||
|
||||
if (change == XMPP_USER_UPDATED_RESOURCE)
|
||||
[[self multicastDelegate] xmppRoster:self didUpdateResource:resource withUser:user];
|
||||
|
||||
if (change == XMPP_USER_REMOVED_RESOURCE)
|
||||
[[self multicastDelegate] xmppRoster:self didRemoveResource:resource withUser:user];
|
||||
|
||||
if (change != XMPP_USER_NO_CHANGE)
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
|
||||
- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
XMPPJID *jidKey = [jid bareJID];
|
||||
XMPPUserMemoryStorageObject *rosterUser = roster[jidKey];
|
||||
|
||||
return (rosterUser != nil);
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (void)setPhoto:(UIImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
#else
|
||||
- (void)setPhoto:(NSImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
#endif
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
XMPPJID *jidKey = [jid bareJID];
|
||||
XMPPUserMemoryStorageObject *rosterUser = roster[jidKey];
|
||||
|
||||
if (rosterUser)
|
||||
{
|
||||
[rosterUser setPhoto:photo];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
for (XMPPUserMemoryStorageObject *user in [roster objectEnumerator])
|
||||
{
|
||||
[user clearAllResources];
|
||||
}
|
||||
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
|
||||
- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
[roster removeAllObjects];
|
||||
|
||||
myUser = nil;
|
||||
|
||||
[[self multicastDelegate] xmppRosterDidChange:self];
|
||||
}
|
||||
|
||||
- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
NSMutableArray *results = [NSMutableArray array];
|
||||
|
||||
for (XMPPUserMemoryStorageObject *user in [roster objectEnumerator])
|
||||
{
|
||||
[results addObject:[user.jid bareJID]];
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
- (void)getSubscription:(NSString **)subscription
|
||||
ask:(NSString **)ask
|
||||
nickname:(NSString **)nickname
|
||||
groups:(NSArray **)groups
|
||||
forJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
|
||||
XMPPLogTrace();
|
||||
AssertParentQueue();
|
||||
|
||||
XMPPJID *jidKey = [jid bareJID];
|
||||
XMPPUserMemoryStorageObject *rosterUser = roster[jidKey];
|
||||
|
||||
if(rosterUser)
|
||||
{
|
||||
if(subscription)
|
||||
{
|
||||
*subscription = rosterUser.subscription;
|
||||
}
|
||||
|
||||
if(ask)
|
||||
{
|
||||
*ask = rosterUser.ask;
|
||||
}
|
||||
|
||||
if(nickname)
|
||||
{
|
||||
*nickname = rosterUser.nickname;
|
||||
}
|
||||
|
||||
if(groups)
|
||||
{
|
||||
*groups = rosterUser.groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
#import "XMPPUserMemoryStorageObject.h"
|
||||
#import "XMPPResourceMemoryStorageObject.h"
|
||||
|
||||
/**
|
||||
* The following methods are designed to be invoked ONLY from
|
||||
* within the XMPPRosterMemoryStorage implementation.
|
||||
*
|
||||
* Warning: XMPPUserMemoryStorage and XMPPResourceMemoryStorage are not explicitly thread-safe.
|
||||
* Only copies that are no longer being actively
|
||||
* altered by the XMPPRosterMemoryStorage class should be considered read-only and therefore thread-safe.
|
||||
**/
|
||||
|
||||
#define XMPP_USER_NO_CHANGE 0
|
||||
#define XMPP_USER_ADDED_RESOURCE 1
|
||||
#define XMPP_USER_UPDATED_RESOURCE 2
|
||||
#define XMPP_USER_REMOVED_RESOURCE 3
|
||||
|
||||
|
||||
@interface XMPPUserMemoryStorageObject ()
|
||||
|
||||
- (void)commonInit;
|
||||
|
||||
- (id)initWithJID:(XMPPJID *)aJid;
|
||||
- (id)initWithItem:(NSXMLElement *)item;
|
||||
|
||||
- (void)updateWithItem:(NSXMLElement *)item;
|
||||
|
||||
- (int)updateWithPresence:(XMPPPresence *)presence
|
||||
resourceClass:(Class)resourceClass
|
||||
andGetResource:(XMPPResourceMemoryStorageObject **)resourcePtr;
|
||||
|
||||
- (void)clearAllResources;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@property (nonatomic, strong, readwrite) UIImage *photo;
|
||||
#else
|
||||
@property (nonatomic, strong, readwrite) NSImage *photo;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@interface XMPPResourceMemoryStorageObject ()
|
||||
|
||||
- (id)initWithPresence:(XMPPPresence *)aPresence;
|
||||
|
||||
- (void)updateWithPresence:(XMPPPresence *)presence;
|
||||
|
||||
- (XMPPPresence *)presence;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,82 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPUser.h"
|
||||
#import "XMPP.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#endif
|
||||
|
||||
@class XMPPResourceMemoryStorageObject;
|
||||
|
||||
|
||||
@interface XMPPUserMemoryStorageObject : NSObject <XMPPUser, NSCopying, NSSecureCoding>
|
||||
{
|
||||
XMPPJID *jid;
|
||||
NSMutableDictionary *itemAttributes;
|
||||
NSMutableArray *groups;
|
||||
|
||||
NSMutableDictionary *resources;
|
||||
XMPPResourceMemoryStorageObject *primaryResource;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
UIImage *photo;
|
||||
#else
|
||||
NSImage *photo;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* From the XMPPUser protocol
|
||||
|
||||
- (XMPPJID *)jid;
|
||||
- (NSString *)nickname;
|
||||
|
||||
- (BOOL)isOnline;
|
||||
- (BOOL)isPendingApproval;
|
||||
|
||||
- (id <XMPPResource>)primaryResource;
|
||||
- (id <XMPPResource>)resourceForJID:(XMPPJID *)jid;
|
||||
|
||||
- (NSArray *)allResources;
|
||||
|
||||
*/
|
||||
|
||||
- (NSString *)subscription;
|
||||
|
||||
- (NSString *)ask;
|
||||
|
||||
/**
|
||||
* Simple convenience method.
|
||||
* If a nickname exists for the user, the nickname is returned.
|
||||
* Otherwise the jid is returned (as a string).
|
||||
**/
|
||||
- (NSString *)displayName;
|
||||
|
||||
/**
|
||||
* An array of Group Names.
|
||||
**/
|
||||
- (NSArray *)groups;
|
||||
|
||||
/**
|
||||
* If XMPPvCardAvatarModule is included in the framework, the XMPPRoster will automatically integrate with it,
|
||||
* and we'll save the the user photos after they've been downloaded.
|
||||
**/
|
||||
#if TARGET_OS_IPHONE
|
||||
@property (nonatomic, strong, readonly) UIImage *photo;
|
||||
#else
|
||||
@property (nonatomic, strong, readonly) NSImage *photo;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Simple comparison methods.
|
||||
**/
|
||||
|
||||
- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another;
|
||||
- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another options:(NSStringCompareOptions)mask;
|
||||
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another;
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another
|
||||
options:(NSStringCompareOptions)mask;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,512 @@
|
||||
#import "XMPP.h"
|
||||
#import "XMPPRosterMemoryStoragePrivate.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 XMPPUserMemoryStorageObject (PrivateAPI)
|
||||
- (void)recalculatePrimaryResource;
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPUserMemoryStorageObject
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
// This method is here to more easily support subclassing.
|
||||
// That way subclasses can optionally override just commonInit, instead of each individual init method.
|
||||
//
|
||||
// If you override this method, don't forget to invoke [super commonInit];
|
||||
|
||||
resources = [[NSMutableDictionary alloc] initWithCapacity:1];
|
||||
}
|
||||
|
||||
- (id)initWithJID:(XMPPJID *)aJid
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
jid = [aJid bareJID];
|
||||
|
||||
itemAttributes = [[NSMutableDictionary alloc] initWithCapacity:0];
|
||||
|
||||
groups = [[NSMutableArray alloc] initWithCapacity:0];
|
||||
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithItem:(NSXMLElement *)item
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
NSString *jidStr = [item attributeStringValueForName:@"jid"];
|
||||
jid = [[XMPPJID jidWithString:jidStr] bareJID];
|
||||
|
||||
itemAttributes = [item attributesAsDictionary];
|
||||
|
||||
groups = [[NSMutableArray alloc] initWithCapacity:0];
|
||||
|
||||
NSArray *groupElements = [item elementsForName:@"group"];
|
||||
|
||||
for (NSXMLElement *groupElement in groupElements) {
|
||||
NSString *groupName = [groupElement stringValue];
|
||||
|
||||
if ([groupName length])
|
||||
{
|
||||
[groups addObject:groupName];
|
||||
}
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Copying
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
// We use [self class] to support subclassing
|
||||
|
||||
XMPPUserMemoryStorageObject *deepCopy = (XMPPUserMemoryStorageObject *)[[[self class] alloc] init];
|
||||
|
||||
deepCopy->jid = [jid copy];
|
||||
deepCopy->itemAttributes = [itemAttributes mutableCopy];
|
||||
deepCopy->groups = [groups mutableCopy];
|
||||
|
||||
deepCopy->resources = [[NSMutableDictionary alloc] initWithCapacity:[resources count]];
|
||||
|
||||
for (XMPPJID *key in resources)
|
||||
{
|
||||
XMPPResourceMemoryStorageObject *resourceCopy = [resources[key] copy];
|
||||
|
||||
deepCopy->resources[key] = resourceCopy;
|
||||
}
|
||||
|
||||
[deepCopy recalculatePrimaryResource];
|
||||
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#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])
|
||||
{
|
||||
jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"];
|
||||
itemAttributes = [[coder decodeObjectOfClass:[NSDictionary class] forKey:@"itemAttributes"] mutableCopy];
|
||||
groups = [[coder decodeObjectOfClass:[NSArray class] forKey:@"groups"] mutableCopy];
|
||||
#if TARGET_OS_IPHONE
|
||||
photo = [[UIImage alloc] initWithData:[coder decodeObjectOfClass:[NSData class] forKey:@"photo"]];
|
||||
#else
|
||||
photo = [[NSImage alloc] initWithData:[coder decodeObjectOfClass:[NSData class] forKey:@"photo"]];
|
||||
#endif
|
||||
resources = [[coder decodeObjectOfClass:[NSDictionary class] forKey:@"resources"] mutableCopy];
|
||||
primaryResource = [coder decodeObjectOfClass:[XMPPResourceMemoryStorageObject class] forKey:@"primaryResource"];
|
||||
}
|
||||
else
|
||||
{
|
||||
jid = [coder decodeObjectForKey:@"jid"];
|
||||
itemAttributes = [[coder decodeObjectForKey:@"itemAttributes"] mutableCopy];
|
||||
groups = [[coder decodeObjectForKey:@"groups"] mutableCopy];
|
||||
#if TARGET_OS_IPHONE
|
||||
photo = [[UIImage alloc] initWithData:[coder decodeObjectForKey:@"photo"]];
|
||||
#else
|
||||
photo = [[NSImage alloc] initWithData:[coder decodeObjectForKey:@"photo"]];
|
||||
#endif
|
||||
resources = [[coder decodeObjectForKey:@"resources"] mutableCopy];
|
||||
primaryResource = [coder decodeObjectForKey:@"primaryResource"];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
jid = [coder decodeObject];
|
||||
itemAttributes = [[coder decodeObject] mutableCopy];
|
||||
groups = [[coder decodeObject] mutableCopy];
|
||||
#if TARGET_OS_IPHONE
|
||||
photo = [[UIImage alloc] initWithData:[coder decodeObject]];
|
||||
#else
|
||||
photo = [[NSImage alloc] initWithData:[coder decodeObject]];
|
||||
#endif
|
||||
resources = [[coder decodeObject] mutableCopy];
|
||||
primaryResource = [coder decodeObject];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
if ([coder allowsKeyedCoding])
|
||||
{
|
||||
[coder encodeObject:jid forKey:@"jid"];
|
||||
[coder encodeObject:itemAttributes forKey:@"itemAttributes"];
|
||||
[coder encodeObject:groups forKey:@"groups"];
|
||||
#if TARGET_OS_IPHONE
|
||||
[coder encodeObject:UIImagePNGRepresentation(photo) forKey:@"photo"];
|
||||
#else
|
||||
[coder encodeObject:[photo TIFFRepresentation] forKey:@"photo"];
|
||||
#endif
|
||||
[coder encodeObject:resources forKey:@"resources"];
|
||||
[coder encodeObject:primaryResource forKey:@"primaryResource"];
|
||||
}
|
||||
else
|
||||
{
|
||||
[coder encodeObject:jid];
|
||||
[coder encodeObject:itemAttributes];
|
||||
[coder encodeObject:groups];
|
||||
#if TARGET_OS_IPHONE
|
||||
[coder encodeObject:UIImagePNGRepresentation(photo)];
|
||||
#else
|
||||
[coder encodeObject:[photo TIFFRepresentation]];
|
||||
#endif
|
||||
[coder encodeObject:resources];
|
||||
[coder encodeObject:primaryResource];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Standard Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@synthesize photo;
|
||||
|
||||
- (XMPPJID *)jid
|
||||
{
|
||||
return jid;
|
||||
}
|
||||
|
||||
- (NSString *)nickname
|
||||
{
|
||||
return (NSString *) itemAttributes[@"name"];
|
||||
}
|
||||
|
||||
- (NSString *)subscription
|
||||
{
|
||||
return (NSString *) itemAttributes[@"subscription"];
|
||||
}
|
||||
|
||||
- (NSString *)ask
|
||||
{
|
||||
return (NSString *) itemAttributes[@"ask"];
|
||||
}
|
||||
|
||||
- (NSString *)displayName
|
||||
{
|
||||
NSString *nickname = [self nickname];
|
||||
if (nickname)
|
||||
return nickname;
|
||||
else
|
||||
return [jid bare];
|
||||
}
|
||||
|
||||
- (NSArray *)groups
|
||||
{
|
||||
return [groups copy];
|
||||
}
|
||||
|
||||
- (BOOL)isOnline
|
||||
{
|
||||
return (primaryResource != nil);
|
||||
}
|
||||
|
||||
- (BOOL)isPendingApproval
|
||||
{
|
||||
// Either of the following mean we're waiting to have our presence subscription approved:
|
||||
// <item ask='subscribe' subscription='none' jid='robbiehanson@deusty.com'/>
|
||||
// <item ask='subscribe' subscription='from' jid='robbiehanson@deusty.com'/>
|
||||
|
||||
NSString *subscription = itemAttributes[@"subscription"];
|
||||
NSString *ask = itemAttributes[@"ask"];
|
||||
|
||||
if ([subscription isEqualToString:@"none"] || [subscription isEqualToString:@"from"])
|
||||
{
|
||||
if([ask isEqualToString:@"subscribe"])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (id <XMPPResource>)primaryResource
|
||||
{
|
||||
return primaryResource;
|
||||
}
|
||||
|
||||
- (id <XMPPResource>)resourceForJID:(XMPPJID *)aJid
|
||||
{
|
||||
return resources[aJid];
|
||||
}
|
||||
|
||||
- (NSArray *)allResources
|
||||
{
|
||||
return [resources allValues];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Hooks
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)didAddResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence
|
||||
{
|
||||
// Override / customization hook
|
||||
}
|
||||
|
||||
- (void)willUpdateResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence
|
||||
{
|
||||
// Override / customization hook
|
||||
}
|
||||
|
||||
- (void)didUpdateResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence
|
||||
{
|
||||
// Override / customization hook
|
||||
}
|
||||
|
||||
- (void)didRemoveResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence
|
||||
{
|
||||
// Override / customization hook
|
||||
}
|
||||
|
||||
- (void)recalculatePrimaryResource
|
||||
{
|
||||
// Override me to customize how the primary resource is chosen.
|
||||
//
|
||||
// This method uses the [XMPPResourceMemoryStorage compare:] method to sort the resources,
|
||||
// and properly supports negative (bot) priorities.
|
||||
|
||||
primaryResource = nil;
|
||||
|
||||
NSArray *sortedResources = [[self allResources] sortedArrayUsingSelector:@selector(compare:)];
|
||||
if ([sortedResources count] > 0)
|
||||
{
|
||||
XMPPResourceMemoryStorageObject *possiblePrimary = sortedResources[0];
|
||||
|
||||
// Primary resource must have a non-negative priority
|
||||
if ([[possiblePrimary presence] priority] >= 0)
|
||||
{
|
||||
primaryResource = possiblePrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Update Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)clearAllResources
|
||||
{
|
||||
[resources removeAllObjects];
|
||||
|
||||
primaryResource = nil;
|
||||
}
|
||||
|
||||
- (void)updateWithItem:(NSXMLElement *)item
|
||||
{
|
||||
[itemAttributes removeAllObjects];
|
||||
|
||||
for (NSXMLNode *node in [item attributes])
|
||||
{
|
||||
NSString *key = [node name];
|
||||
NSString *value = [node stringValue];
|
||||
|
||||
itemAttributes[key] = value;
|
||||
}
|
||||
|
||||
[groups removeAllObjects];
|
||||
|
||||
NSArray *groupElements = [item elementsForName:@"group"];
|
||||
|
||||
for (NSXMLElement *groupElement in groupElements) {
|
||||
NSString *groupName = [groupElement stringValue];
|
||||
|
||||
if ([groupName length])
|
||||
{
|
||||
[groups addObject:groupName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (int)updateWithPresence:(XMPPPresence *)presence
|
||||
resourceClass:(Class)resourceClass
|
||||
andGetResource:(XMPPResourceMemoryStorageObject **)resourcePtr
|
||||
{
|
||||
int result = XMPP_USER_NO_CHANGE;
|
||||
XMPPResourceMemoryStorageObject *resource;
|
||||
|
||||
XMPPJID *key = [presence from];
|
||||
NSString *presenceType = [presence type];
|
||||
|
||||
if ([presenceType isEqualToString:@"unavailable"] || [presenceType isEqualToString:@"error"])
|
||||
{
|
||||
resource = resources[key];
|
||||
if (resource)
|
||||
{
|
||||
[resources removeObjectForKey:key];
|
||||
[self didRemoveResource:resource withPresence:presence];
|
||||
|
||||
result = XMPP_USER_REMOVED_RESOURCE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resource = resources[key];
|
||||
if (resource)
|
||||
{
|
||||
[self willUpdateResource:resource withPresence:presence];
|
||||
[resource updateWithPresence:presence];
|
||||
[self didUpdateResource:resource withPresence:presence];
|
||||
|
||||
result = XMPP_USER_UPDATED_RESOURCE;
|
||||
}
|
||||
else
|
||||
{
|
||||
resource = (XMPPResourceMemoryStorageObject *)[[resourceClass alloc] initWithPresence:presence];
|
||||
|
||||
resources[key] = resource;
|
||||
[self didAddResource:resource withPresence:presence];
|
||||
|
||||
result = XMPP_USER_ADDED_RESOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
[self recalculatePrimaryResource];
|
||||
|
||||
if (resourcePtr)
|
||||
*resourcePtr = resource;
|
||||
return result;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Comparisons
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Returns the result of invoking compareByName:options: with no options.
|
||||
**/
|
||||
- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another
|
||||
{
|
||||
return [self compareByName:another options:0];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compares the two users according to their display name.
|
||||
*
|
||||
* Options for the search — you can combine any of the following using a C bitwise OR operator:
|
||||
* NSCaseInsensitiveSearch, NSLiteralSearch, NSNumericSearch.
|
||||
* See "String Programming Guide for Cocoa" for details on these options.
|
||||
**/
|
||||
- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another options:(NSStringCompareOptions)mask
|
||||
{
|
||||
NSString *myName = [self displayName];
|
||||
NSString *theirName = [another displayName];
|
||||
|
||||
return [myName compare:theirName options:mask];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of invoking compareByAvailabilityName:options: with no options.
|
||||
**/
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another
|
||||
{
|
||||
return [self compareByAvailabilityName:another options:0];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method compares the two users according to availability first, and then display name.
|
||||
* Thus available users come before unavailable users.
|
||||
* If both users are available, or both users are not available,
|
||||
* this method follows the same functionality as the compareByName:options: as documented above.
|
||||
**/
|
||||
- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another
|
||||
options:(NSStringCompareOptions)mask
|
||||
{
|
||||
if ([self isOnline])
|
||||
{
|
||||
if ([another isOnline])
|
||||
return [self compareByName:another options:mask];
|
||||
else
|
||||
return NSOrderedAscending;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ([another isOnline])
|
||||
return NSOrderedDescending;
|
||||
else
|
||||
return [self compareByName:another options:mask];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark NSObject Methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [jid hash];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
if ([anObject isMemberOfClass:[self class]])
|
||||
{
|
||||
XMPPUserMemoryStorageObject *another = (XMPPUserMemoryStorageObject *)anObject;
|
||||
|
||||
return [jid isEqualToJID:[another jid]];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<XMPPUser[%p]: %@>", self, [jid bare]];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark KVO Compliance methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingIsOnline
|
||||
{
|
||||
return [NSSet setWithObject:@"primaryResource"];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingAllResources {
|
||||
return [NSSet setWithObject:@"resources"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,15 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPP.h"
|
||||
|
||||
|
||||
@protocol XMPPResource <NSObject>
|
||||
@required
|
||||
|
||||
- (XMPPJID *)jid;
|
||||
- (XMPPPresence *)presence;
|
||||
|
||||
- (NSDate *)presenceDate;
|
||||
|
||||
- (NSComparisonResult)compare:(id <XMPPResource>)another;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,406 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#endif
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPUser.h"
|
||||
#import "XMPPResource.h"
|
||||
|
||||
#define _XMPP_ROSTER_H
|
||||
|
||||
@protocol XMPPRosterStorage;
|
||||
@class DDList;
|
||||
@class XMPPIDTracker;
|
||||
|
||||
/**
|
||||
* The XMPPRoster provides the scaffolding for a roster solution.
|
||||
* It handles the basics of the xmpp roster communication,
|
||||
* and leaves the rest up to the xmppRosterStorage classes.
|
||||
*
|
||||
* You are free to extend, change, customize, or tweak the sample storage classes that come with the framework:
|
||||
* XMPPRosterMemoryStorage
|
||||
* XMPPRosterCoreDataStorage
|
||||
*
|
||||
* You can also completely implment your own roster storage class if you'd like.
|
||||
* The point of all this customizability is simple:
|
||||
* The roster is the component of the xmpp stack that is most often customized for a particular application.
|
||||
*
|
||||
*
|
||||
* Inter-Module Interaction:
|
||||
*
|
||||
* If you use XMPPvCardAvatarModule, the roster will automatically support user photos.
|
||||
**/
|
||||
@interface XMPPRoster : XMPPModule
|
||||
{
|
||||
/* Inherited from XMPPModule:
|
||||
|
||||
XMPPStream *xmppStream;
|
||||
|
||||
dispatch_queue_t moduleQueue;
|
||||
id multicastDelegate;
|
||||
*/
|
||||
__strong id <XMPPRosterStorage> xmppRosterStorage;
|
||||
|
||||
XMPPIDTracker *xmppIDTracker;
|
||||
|
||||
Byte config;
|
||||
Byte flags;
|
||||
|
||||
NSMutableArray *earlyPresenceElements;
|
||||
|
||||
DDList *mucModules;
|
||||
}
|
||||
|
||||
- (id)initWithRosterStorage:(id <XMPPRosterStorage>)storage;
|
||||
- (id)initWithRosterStorage:(id <XMPPRosterStorage>)storage 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;
|
||||
|
||||
*/
|
||||
|
||||
@property (strong, readonly) id <XMPPRosterStorage> xmppRosterStorage;
|
||||
|
||||
/**
|
||||
* Whether or not to automatically fetch the roster from the server.
|
||||
*
|
||||
* The default value is YES.
|
||||
**/
|
||||
@property (assign) BOOL autoFetchRoster;
|
||||
|
||||
/**
|
||||
* Whether or not to automatically clear all Users and Resources when the stream disconnects.
|
||||
* If you are using XMPPRosterCoreDataStorage you may want to set autoRemovePreviousDatabaseFile to NO.
|
||||
*
|
||||
* All Users and Resources will be cleared when the roster is next populated regardless of this property.
|
||||
*
|
||||
* The default value is YES.
|
||||
**/
|
||||
@property (assign) BOOL autoClearAllUsersAndResources;
|
||||
|
||||
/**
|
||||
* In traditional IM applications, the "buddy" system is rather straightforward.
|
||||
* User A sends a request to become "friends" with user B.
|
||||
* User B accepts the friend request.
|
||||
* And now user A and B appear in each other's roster.
|
||||
*
|
||||
* But, internally, XMPP presence actually works more like twitter.
|
||||
* User A sends a presence subscription request to user B.
|
||||
* Think about this more like following on twitter.
|
||||
* User A is requesting permission to receive presence information from user B (following their presence).
|
||||
* User B can accept this request, but may not be interested in receiving presence info from user B.
|
||||
* Again, this is more like twitter following, but with the addition of required permission.
|
||||
* So when user B accepts the request, this *only* means that user A will receive presence from user B.
|
||||
* It does *not* mean that user B will receive presence from user A.
|
||||
* Again, twitter-style, user A is now "following" user B.
|
||||
* In diagram form, presence is flowing like this:
|
||||
*
|
||||
* A <-- B
|
||||
*
|
||||
* Now, if B also wants to receive presence from user A, then user B must request this permission.
|
||||
* And furthermore, user A must accept the request.
|
||||
* Just because B has granted A permission to receive presence,
|
||||
* doesn't mean that B gets a free pass to receive presence from A.
|
||||
* Presence always requires permission.
|
||||
*
|
||||
* This property does the following:
|
||||
* If a presence subscription request is received from user X,
|
||||
* and user X has already been added to our roster (is a "known" user),
|
||||
* then the presence subscription request is automatically accepted.
|
||||
*
|
||||
* Continuing the example from above, if we were user A, and we've already added B to our roster,
|
||||
* then if B sends a presence susbcription request, we'll auto accept it.
|
||||
*
|
||||
* This makes the roster act more like traditional IM applications.
|
||||
*
|
||||
* The default value is YES.
|
||||
**/
|
||||
@property (assign) BOOL autoAcceptKnownPresenceSubscriptionRequests;
|
||||
|
||||
/**
|
||||
* Allows the roster module to function without ever fetching the full roster.
|
||||
* This is helpful for situations in which the roster is very big, yet the application only cares about online users.
|
||||
*
|
||||
* Typically, the roster module creates users based on the fetched full roster,
|
||||
* and then creates resources based on received presence.
|
||||
*
|
||||
* In this mode, the roster module will automatically create a user once a presence is received,
|
||||
* if the user has never been seen before.
|
||||
*
|
||||
* If allowRosterlessOperation is enabled, and autoFetchRoster is disabled (and roster is never manually fetched),
|
||||
* then XMPPUser's will be missing certain information that is only available via a roster fetch
|
||||
* (such as nickname, group, and subscription information).
|
||||
*
|
||||
* The default value is NO.
|
||||
**/
|
||||
@property (assign) BOOL allowRosterlessOperation;
|
||||
|
||||
/**
|
||||
* The roster has either been requested manually (fetchRoster:)
|
||||
* or automatically (autoFetchRoster) but has yet to be populated.
|
||||
**/
|
||||
@property (assign, getter = hasRequestedRoster, readonly) BOOL requestedRoster;
|
||||
|
||||
/**
|
||||
* The initial roster has been received by client and is currently being populated.
|
||||
* @see xmppRosterDidBeginPopulating:withVersion:
|
||||
* @see xmppRosterDidEndPopulating:
|
||||
**/
|
||||
@property (assign, getter = isPopulating, readonly) BOOL populating;
|
||||
|
||||
/**
|
||||
* The initial roster has been received by client and populated.
|
||||
**/
|
||||
@property (assign, readonly) BOOL hasRoster;
|
||||
|
||||
/**
|
||||
* Manually fetch the roster from the server.
|
||||
* Useful if you disable autoFetchRoster.
|
||||
**/
|
||||
- (void)fetchRoster;
|
||||
- (void)fetchRosterVersion:(NSString *)version;
|
||||
|
||||
/**
|
||||
* Adds the given user to the roster with an optional nickname
|
||||
* and requests permission to receive presence information from them.
|
||||
**/
|
||||
- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName;
|
||||
|
||||
/**
|
||||
* Adds the given user to the roster with an optional nickname,
|
||||
* adds the given user to groups
|
||||
* and requests permission to receive presence information from them.
|
||||
**/
|
||||
- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups;
|
||||
|
||||
/**
|
||||
* Adds the given user to the roster with an optional nickname,
|
||||
* adds the given user to groups
|
||||
* and optionally requests permission to receive presence information from them.
|
||||
**/
|
||||
- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups subscribeToPresence:(BOOL)subscribe;
|
||||
|
||||
/**
|
||||
* Sets/modifies the nickname for the given user.
|
||||
**/
|
||||
- (void)setNickname:(NSString *)nickname forUser:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* Remove the user from the roster, unsubscribe from their presence, AND
|
||||
* revoke given user's permission to receive our presence (if they have such permission).
|
||||
*
|
||||
* This is similar to removing a buddy in a traditional IM model.
|
||||
*
|
||||
* @see unsubscribePresenceFromUser:
|
||||
* @see revokePresencePermissionFromUser:
|
||||
**/
|
||||
- (void)removeUser:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* If we don't currently receive presence from the given user,
|
||||
* this method requests a subscription to start receiving presence updates from the given user.
|
||||
*
|
||||
* This is similar to following in the twitter model.
|
||||
*
|
||||
* Note: If the given user isn't already in the roster, it is recommended to instead use addUser:withNickname:.
|
||||
*
|
||||
* @see addUser:withNickname:
|
||||
**/
|
||||
- (void)subscribePresenceToUser:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* If we currently have a presence subscription to the given user,
|
||||
* this method then removes the subscription.
|
||||
*
|
||||
* This is similar to unfollowing in the twitter model.
|
||||
*
|
||||
* If the given user has a presence subscription to us (they're following our presence),
|
||||
* then their presence subscription to us is left intact.
|
||||
*
|
||||
* @see removeUser:
|
||||
* @see revokePresencePermissionFromUser:
|
||||
**/
|
||||
- (void)unsubscribePresenceFromUser:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* If we have previously accepted a presence subscription request from the given user,
|
||||
* this method revokes the previously granted permission.
|
||||
*
|
||||
* This is similar to forcing the given user to unfollow us in the twitter model.
|
||||
*
|
||||
* If we have a presence subscription to the given user (we're following their presence),
|
||||
* then our presence subscription to them is left intact.
|
||||
*
|
||||
* @see removeUser:
|
||||
* @see unsubscribePresenceFromUser:
|
||||
**/
|
||||
- (void)revokePresencePermissionFromUser:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* Accepts the presence subscription request the given user.
|
||||
*
|
||||
* If you also choose, you can add the user to your roster.
|
||||
* Doing so is similar to the traditional IM model.
|
||||
**/
|
||||
- (void)acceptPresenceSubscriptionRequestFrom:(XMPPJID *)jid andAddToRoster:(BOOL)flag;
|
||||
|
||||
/**
|
||||
* Rejects the presence subscription request from the given user.
|
||||
*
|
||||
* If you are already subscribed to the given user's presence,
|
||||
* rejecting they subscription request will not affect your subscription to their presence.
|
||||
**/
|
||||
- (void)rejectPresenceSubscriptionRequestFrom:(XMPPJID *)jid;
|
||||
|
||||
//
|
||||
//
|
||||
// You can access/enumerate the users & resources via the roster storage class (xmppRosterStorage property).
|
||||
//
|
||||
// Rember, XMPPRoster is just the scaffolding for a complete and customizable roster solution.
|
||||
// The roster storage classes hold the majority of the magic.
|
||||
//
|
||||
// And since you're free to plug-n-play storage classes, and customize them as much as you want.
|
||||
// This is where you can really tailor the xmpp stack to meet the needs of your application.
|
||||
//
|
||||
//
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRosterStorage <NSObject>
|
||||
@required
|
||||
|
||||
//
|
||||
//
|
||||
// -- PUBLIC METHODS --
|
||||
//
|
||||
// There are no public methods required by this protocol.
|
||||
//
|
||||
// Each individual roster storage class will provide a proper way to access/enumerate the
|
||||
// users/resources according to the underlying storage mechanism.
|
||||
//
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// -- PRIVATE METHODS --
|
||||
//
|
||||
// These methods are designed to be used ONLY by the XMPPRoster class.
|
||||
//
|
||||
//
|
||||
|
||||
/**
|
||||
* Configures the storage class, passing its parent and parent's dispatch queue.
|
||||
*
|
||||
* This method is called by the init method of the XMPPRoster class.
|
||||
* This method is designed to inform the storage class of its parent
|
||||
* and of the dispatch queue the parent will be operating on.
|
||||
*
|
||||
* The storage class may choose to operate on the same queue as its parent,
|
||||
* or it may operate on its own internal dispatch queue.
|
||||
*
|
||||
* This method should return YES if it was configured properly.
|
||||
* If a storage class is designed to be used with a single parent at a time, this method may return NO.
|
||||
* The XMPPRoster class is configured to ignore the passed
|
||||
* storage class in its init method if this method returns NO.
|
||||
**/
|
||||
- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue;
|
||||
|
||||
- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version;
|
||||
- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream;
|
||||
|
||||
- (void)handleRosterItem:(NSXMLElement *)item xmppStream:(XMPPStream *)stream;
|
||||
- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream;
|
||||
|
||||
- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream;
|
||||
|
||||
- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream;
|
||||
- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream;
|
||||
|
||||
- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream;
|
||||
|
||||
- (void)getSubscription:(NSString **)subscription
|
||||
ask:(NSString **)ask
|
||||
nickname:(NSString **)nickname
|
||||
groups:(NSArray **)groups
|
||||
forJID:(XMPPJID *)jid
|
||||
xmppStream:(XMPPStream *)stream;
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
* When XMPPvCardAvatarModule is included in the framework, the roster will integrate with it.
|
||||
* Implement this method to provide support for storing the downloaded user photos.
|
||||
**/
|
||||
#if TARGET_OS_IPHONE
|
||||
- (void)setPhoto:(UIImage *)image forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream;
|
||||
#else
|
||||
- (void)setPhoto:(NSImage *)image forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPRosterDelegate
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Sent when a presence subscription request is received.
|
||||
* That is, another user has added you to their roster,
|
||||
* and is requesting permission to receive presence broadcasts that you send.
|
||||
*
|
||||
* The entire presence packet is provided for proper extensibility.
|
||||
* You can use [presence from] to get the JID of the user who sent the request.
|
||||
*
|
||||
* The methods acceptPresenceSubscriptionRequestFrom: and rejectPresenceSubscriptionRequestFrom: can
|
||||
* be used to respond to the request.
|
||||
**/
|
||||
- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence;
|
||||
|
||||
/**
|
||||
* Sent when a Roster Push is received as specified in Section 2.1.6 of RFC 6121.
|
||||
**/
|
||||
- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterPush:(XMPPIQ *)iq;
|
||||
|
||||
/**
|
||||
* Sent when the initial roster is received.
|
||||
**/
|
||||
- (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender withVersion:(NSString *)version;
|
||||
|
||||
/**
|
||||
* Sent when the initial roster has been populated into storage.
|
||||
**/
|
||||
- (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender;
|
||||
|
||||
/**
|
||||
* Sent when the roster receives a roster item.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <item jid='romeo@example.net' name='Romeo' subscription='both'>
|
||||
* <group>Friends</group>
|
||||
* </item>
|
||||
**/
|
||||
- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(NSXMLElement *)item;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPRoster.h"
|
||||
|
||||
@interface XMPPRoster (PrivateInternalAPI)
|
||||
|
||||
/**
|
||||
* XMPPRosterStorage classes may optionally use the same delegate(s) as their parent XMPPRoster.
|
||||
* 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.
|
||||
**/
|
||||
- (GCDMulticastDelegate *)multicastDelegate;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,21 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPP.h"
|
||||
|
||||
@protocol XMPPResource;
|
||||
|
||||
|
||||
@protocol XMPPUser <NSObject>
|
||||
@required
|
||||
|
||||
- (XMPPJID *)jid;
|
||||
- (NSString *)nickname;
|
||||
|
||||
- (BOOL)isOnline;
|
||||
- (BOOL)isPendingApproval;
|
||||
|
||||
- (id <XMPPResource>)primaryResource;
|
||||
- (id <XMPPResource>)resourceForJID:(XMPPJID *)jid;
|
||||
|
||||
- (NSArray *)allResources;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,56 @@
|
||||
#import "XMPPModule.h"
|
||||
|
||||
extern const NSTimeInterval XMPPSystemInputActivityMonitorInactivityTimeIntervalNone;
|
||||
|
||||
#define _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
||||
|
||||
/**
|
||||
* XMPPSystemInputActivityMonitor is used to keep track of system input activity.
|
||||
* This module could be used to add features such as "auto away" when the user is inactive for 5 mins.
|
||||
**/
|
||||
|
||||
@interface XMPPSystemInputActivityMonitor : XMPPModule
|
||||
{
|
||||
dispatch_source_t timer;
|
||||
|
||||
BOOL active;
|
||||
NSDate *lastActivityDate;
|
||||
NSTimeInterval inactivityTimeInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns wether the system is active based on the lastActivityDate and inactivityTimeInterval.
|
||||
**/
|
||||
@property (assign, getter = isActive, readonly) BOOL active;
|
||||
|
||||
/**
|
||||
* The last time any input activity was detected.
|
||||
**/
|
||||
@property (assign, readonly) NSDate *lastActivityDate;
|
||||
|
||||
/**
|
||||
* The minimum time interval after the last input activity, that assumes the system is idle.
|
||||
*
|
||||
* To disable activity checking set this value to XMPPSystemInputActivityMonitorInactivityTimeIntervalNone
|
||||
* If set to XMPPSystemInputActivityMonitorInactivityTimeIntervalNone none of delegate methods will be called.
|
||||
* Setting this value doesn't cause the delegate methods to be called immediately.
|
||||
*
|
||||
* Default 300 Seconds
|
||||
**/
|
||||
@property (assign) NSTimeInterval inactivityTimeInterval;
|
||||
|
||||
@end
|
||||
|
||||
@protocol XMPPSystemInputActivityMonitorDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* The system did become active after being inactive.
|
||||
**/
|
||||
- (void)xmppSystemInputActivityMonitorDidBecomeActive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor;
|
||||
|
||||
/**
|
||||
* The system did become inactive after being active.
|
||||
**/
|
||||
- (void)xmppSystemInputActivityMonitorDidBecomeInactive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,202 @@
|
||||
#import "XMPPSystemInputActivityMonitor.h"
|
||||
#import "XMPP.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "XMPPFramework.h"
|
||||
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#warning This file does not work on TARGET_OS_IPHONE.
|
||||
#endif
|
||||
|
||||
// Log levels: off, error, warn, info, verbose
|
||||
#if DEBUG
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
const NSTimeInterval XMPPSystemInputActivityMonitorInactivityTimeIntervalNone = -1;
|
||||
|
||||
#define INACTIVITY_TIME_INTERVAL 300.0
|
||||
#define TIMER_TIME_INTERVAL 1.0
|
||||
|
||||
@implementation XMPPSystemInputActivityMonitor
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithDispatchQueue:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
if ((self = [super initWithDispatchQueue:queue]))
|
||||
{
|
||||
inactivityTimeInterval = INACTIVITY_TIME_INTERVAL;
|
||||
active = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)aXmppStream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if ([super activate:aXmppStream])
|
||||
{
|
||||
XMPPLogVerbose(@"%@: Activated", THIS_FILE);
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
|
||||
CFTimeInterval secondsSinceLastUserInteraction = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateHIDSystemState, kCGAnyInputEventType);
|
||||
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:(secondsSinceLastUserInteraction * -1)];
|
||||
[self _setLastActivityDate:date];
|
||||
|
||||
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
|
||||
|
||||
if (timer)
|
||||
{
|
||||
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (TIMER_TIME_INTERVAL * NSEC_PER_SEC), 0);
|
||||
dispatch_source_set_event_handler(timer, ^{
|
||||
CFTimeInterval secondsSinceLastUserInteraction = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateHIDSystemState, kCGAnyInputEventType);
|
||||
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:(secondsSinceLastUserInteraction * -1)];
|
||||
[self _setLastActivityDate:date];
|
||||
});
|
||||
dispatch_resume(timer);
|
||||
}
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if(timer)
|
||||
{
|
||||
dispatch_source_cancel(timer);
|
||||
timer = NULL;
|
||||
}
|
||||
|
||||
[super deactivate];
|
||||
}
|
||||
|
||||
- (void)dealloc{
|
||||
|
||||
if(timer)
|
||||
{
|
||||
dispatch_source_cancel(timer);
|
||||
timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma Internal
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)_setLastActivityDate:(NSDate *)flag{
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
lastActivityDate = flag;
|
||||
|
||||
if(inactivityTimeInterval > XMPPSystemInputActivityMonitorInactivityTimeIntervalNone)
|
||||
{
|
||||
if(active && -[lastActivityDate timeIntervalSinceNow] > inactivityTimeInterval)
|
||||
{
|
||||
active = NO;
|
||||
[multicastDelegate xmppSystemInputActivityMonitorDidBecomeInactive:self];
|
||||
}
|
||||
else if(!active && -[lastActivityDate timeIntervalSinceNow] < inactivityTimeInterval)
|
||||
{
|
||||
active = YES;
|
||||
[multicastDelegate xmppSystemInputActivityMonitorDidBecomeActive:self];
|
||||
}
|
||||
|
||||
}else{
|
||||
active = YES;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma Properties
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)isActive
|
||||
{
|
||||
__block BOOL result = YES;;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = active;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSDate *)lastActivityDate
|
||||
{
|
||||
__block NSDate *result = nil;;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = lastActivityDate;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)inactivityTimeInterval
|
||||
{
|
||||
__block NSTimeInterval result = XMPPSystemInputActivityMonitorInactivityTimeIntervalNone;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = inactivityTimeInterval;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setInactivityTimeInterval:(NSTimeInterval)flag
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
inactivityTimeInterval = flag;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// XMPPIQ+JabberRPC.h
|
||||
// XEP-0009
|
||||
//
|
||||
// Created by Eric Chamberlain on 5/16/10.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "XMPPIQ.h"
|
||||
|
||||
|
||||
@interface XMPPIQ(JabberRPC)
|
||||
|
||||
/**
|
||||
* Creates and returns a new autoreleased XMPPIQ.
|
||||
* This is the only method you normally need to call.
|
||||
**/
|
||||
+ (XMPPIQ *)rpcTo:(XMPPJID *)jid methodName:(NSString *)method parameters:(NSArray *)parameters;
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Element helper methods
|
||||
|
||||
// returns a Jabber-RPC query elelement
|
||||
// <query xmlns='jabber:iq:rpc'>
|
||||
+(NSXMLElement *)elementRpcQuery;
|
||||
|
||||
// returns a Jabber-RPC methodCall element
|
||||
// <methodCall>
|
||||
+(NSXMLElement *)elementMethodCall;
|
||||
|
||||
// returns a Jabber-RPC methodName element
|
||||
// <methodName>method</methodName>
|
||||
+(NSXMLElement *)elementMethodName:(NSString *)method;
|
||||
|
||||
// returns a Jabber-RPC params element
|
||||
// <params>
|
||||
+(NSXMLElement *)elementParams;
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Disco elements
|
||||
|
||||
// returns the Disco query identity element
|
||||
// <identity category='automation' type='rpc'/>
|
||||
+(NSXMLElement *)elementRpcIdentity;
|
||||
|
||||
// returns the Disco query feature element
|
||||
// <feature var='jabber:iq:rpc'/>
|
||||
+(NSXMLElement *)elementRpcFeature;
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Conversion methods
|
||||
|
||||
// encode any object into JabberRPC formatted element
|
||||
// this method calls the others
|
||||
+(NSXMLElement *)paramElementFromObject:(id)object;
|
||||
|
||||
+(NSXMLElement *)valueElementFromObject:(id)object;
|
||||
|
||||
+(NSXMLElement *)valueElementFromArray:(NSArray *)array;
|
||||
+(NSXMLElement *)valueElementFromDictionary:(NSDictionary *)dictionary;
|
||||
|
||||
+(NSXMLElement *)valueElementFromBoolean:(CFBooleanRef)boolean;
|
||||
+(NSXMLElement *)valueElementFromNumber:(NSNumber *)number;
|
||||
+(NSXMLElement *)valueElementFromString:(NSString *)string;
|
||||
+(NSXMLElement *)valueElementFromDate:(NSDate *)date;
|
||||
+(NSXMLElement *)valueElementFromData:(NSData *)data;
|
||||
|
||||
+(NSXMLElement *)valueElementFromElementWithName:(NSString *)elementName value:(NSString *)value;
|
||||
|
||||
|
||||
#pragma mark Wrapper methods
|
||||
|
||||
+(NSXMLElement *)wrapElement:(NSString *)elementName aroundElement:(NSXMLElement *)element;
|
||||
+(NSXMLElement *)wrapValueElementAroundElement:(NSXMLElement *)element;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// XMPPIQ+JabberRPC.m
|
||||
// XEP-0009
|
||||
//
|
||||
// Created by Eric Chamberlain on 5/16/10.
|
||||
//
|
||||
|
||||
|
||||
#import "XMPPIQ+JabberRPC.h"
|
||||
#import "XMPP.h"
|
||||
#import "NSData+XMPP.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
|
||||
@implementation XMPPIQ(JabberRPC)
|
||||
|
||||
+(XMPPIQ *)rpcTo:(XMPPJID *)jid methodName:(NSString *)method parameters:(NSArray *)parameters {
|
||||
// Send JabberRPC element
|
||||
//
|
||||
// <iq to="fullJID" type="set" id="elementID">
|
||||
// <query xmlns='jabber:iq:rpc'>
|
||||
// <methodCall>
|
||||
// <methodName>method</methodName>
|
||||
// <params>
|
||||
// <param>
|
||||
// <value><string>example</string></value>
|
||||
// </param>
|
||||
// ...
|
||||
// </params>
|
||||
// </methodCall>
|
||||
// </query>
|
||||
// </iq>
|
||||
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:jid elementID:[XMPPStream generateUUID]];
|
||||
|
||||
NSXMLElement *jabberRPC = [self elementRpcQuery];
|
||||
NSXMLElement *methodCall = [self elementMethodCall];
|
||||
NSXMLElement *methodName = [self elementMethodName:method];
|
||||
NSXMLElement *params = [self elementParams];
|
||||
|
||||
for (id parameter in parameters) {
|
||||
[params addChild:[self paramElementFromObject:parameter]];
|
||||
}
|
||||
|
||||
[methodCall addChild:methodName];
|
||||
[methodCall addChild:params];
|
||||
[jabberRPC addChild:methodCall];
|
||||
[iq addChild:jabberRPC];
|
||||
return iq;
|
||||
}
|
||||
|
||||
#pragma mark Element helper methods
|
||||
|
||||
+(NSXMLElement *)elementRpcQuery {
|
||||
return [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:rpc"];
|
||||
}
|
||||
|
||||
+(NSXMLElement *)elementMethodCall {
|
||||
return [NSXMLElement elementWithName:@"methodCall"];
|
||||
}
|
||||
|
||||
+(NSXMLElement *)elementMethodName:(NSString *)method {
|
||||
return [NSXMLElement elementWithName:@"methodName" stringValue:method];
|
||||
}
|
||||
|
||||
+(NSXMLElement *)elementParams {
|
||||
return [NSXMLElement elementWithName:@"params"];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Disco elements
|
||||
|
||||
+(NSXMLElement *)elementRpcIdentity {
|
||||
NSXMLElement *identity = [NSXMLElement elementWithName:@"identity"];
|
||||
[identity addAttributeWithName:@"category" stringValue:@"automation"];
|
||||
[identity addAttributeWithName:@"type" stringValue:@"rpc"];
|
||||
return identity;
|
||||
}
|
||||
|
||||
// returns the Disco query feature element
|
||||
// <feature var='jabber:iq:rpc'/>
|
||||
+(NSXMLElement *)elementRpcFeature {
|
||||
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature addAttributeWithName:@"var" stringValue:@"jabber:iq:rpc"];
|
||||
return feature;
|
||||
}
|
||||
#pragma mark Conversion methods
|
||||
|
||||
+(NSXMLElement *)paramElementFromObject:(id)object {
|
||||
if (!object) {
|
||||
return nil;
|
||||
}
|
||||
return [self wrapElement:@"param" aroundElement:[self valueElementFromObject:object]];
|
||||
}
|
||||
|
||||
+(NSXMLElement *)valueElementFromObject:(id)object {
|
||||
if (!object) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([object isKindOfClass: [NSArray class]]) {
|
||||
return [self valueElementFromArray: object];
|
||||
} else if ([object isKindOfClass: [NSDictionary class]]) {
|
||||
return [self valueElementFromDictionary: object];
|
||||
} else if (((__bridge CFBooleanRef)object == kCFBooleanTrue) || ((__bridge CFBooleanRef)object == kCFBooleanFalse)) {
|
||||
return [self valueElementFromBoolean: (__bridge CFBooleanRef)object];
|
||||
} else if ([object isKindOfClass: [NSNumber class]]) {
|
||||
return [self valueElementFromNumber: object];
|
||||
} else if ([object isKindOfClass: [NSString class]]) {
|
||||
return [self valueElementFromString: object];
|
||||
} else if ([object isKindOfClass: [NSDate class]]) {
|
||||
return [self valueElementFromDate: object];
|
||||
} else if ([object isKindOfClass: [NSData class]]) {
|
||||
return [self valueElementFromData: object];
|
||||
} else {
|
||||
return [self valueElementFromString: object];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromArray:(NSArray *)array {
|
||||
NSXMLElement *data = [NSXMLElement elementWithName:@"data"];
|
||||
|
||||
for (id object in array) {
|
||||
[data addChild:[self valueElementFromObject:object]];
|
||||
}
|
||||
return [self wrapValueElementAroundElement:data];
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromDictionary:(NSDictionary *)dictionary {
|
||||
NSXMLElement *structElement = [NSXMLElement elementWithName:@"struct"];
|
||||
|
||||
NSXMLElement *member;
|
||||
NSXMLElement *name;
|
||||
|
||||
for (NSString *key in dictionary) {
|
||||
member = [NSXMLElement elementWithName:@"member"];
|
||||
name = [NSXMLElement elementWithName:@"name" stringValue:key];
|
||||
[member addChild:name];
|
||||
[member addChild:[self valueElementFromObject:dictionary[key]]];
|
||||
}
|
||||
|
||||
return [self wrapValueElementAroundElement:structElement];
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromBoolean:(CFBooleanRef)boolean {
|
||||
if (boolean == kCFBooleanTrue) {
|
||||
return [self valueElementFromElementWithName:@"boolean" value:@"1"];
|
||||
} else {
|
||||
return [self valueElementFromElementWithName:@"boolean" value:@"0"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromNumber:(NSNumber *)number {
|
||||
// what type of NSNumber is this?
|
||||
if ([[NSString stringWithCString: [number objCType]
|
||||
encoding: NSUTF8StringEncoding] isEqualToString: @"d"]) {
|
||||
return [self valueElementFromElementWithName:@"double" value:[number stringValue]];
|
||||
} else {
|
||||
return [self valueElementFromElementWithName:@"i4" value:[number stringValue]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromString:(NSString *)string {
|
||||
return [self valueElementFromElementWithName:@"string" value:string];
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromDate:(NSDate *)date {
|
||||
unsigned calendarComponents = kCFCalendarUnitYear |
|
||||
kCFCalendarUnitMonth |
|
||||
kCFCalendarUnitDay |
|
||||
kCFCalendarUnitHour |
|
||||
kCFCalendarUnitMinute |
|
||||
kCFCalendarUnitSecond;
|
||||
NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:calendarComponents fromDate:date];
|
||||
NSString *dateString = [NSString stringWithFormat: @"%.4ld%.2ld%.2ldT%.2ld:%.2ld:%.2ld",
|
||||
(long)[dateComponents year],
|
||||
(long)[dateComponents month],
|
||||
(long)[dateComponents day],
|
||||
(long)[dateComponents hour],
|
||||
(long)[dateComponents minute],
|
||||
(long)[dateComponents second], nil];
|
||||
|
||||
return [self valueElementFromElementWithName:@"dateTime.iso8601" value: dateString];
|
||||
}
|
||||
|
||||
|
||||
+(NSXMLElement *)valueElementFromData:(NSData *)data {
|
||||
return [self valueElementFromElementWithName:@"base64" value:[data xmpp_base64Encoded]];
|
||||
}
|
||||
|
||||
+(NSXMLElement *)valueElementFromElementWithName:(NSString *)elementName value:(NSString *)value {
|
||||
return [self wrapValueElementAroundElement:[NSXMLElement elementWithName:elementName stringValue:value]];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Wrapper methods
|
||||
|
||||
+(NSXMLElement *)wrapElement:(NSString *)elementName aroundElement:(NSXMLElement *)element {
|
||||
NSXMLElement *wrapper = [NSXMLElement elementWithName:elementName];
|
||||
[wrapper addChild:element];
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
+(NSXMLElement *)wrapValueElementAroundElement:(NSXMLElement *)element {
|
||||
return [self wrapElement:@"value" aroundElement:element];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// XMPPIQ+JabberRPCResonse.h
|
||||
// XEP-0009
|
||||
//
|
||||
// Created by Eric Chamberlain on 5/25/10.
|
||||
//
|
||||
|
||||
#import "XMPPIQ.h"
|
||||
|
||||
typedef enum {
|
||||
JabberRPCElementTypeArray,
|
||||
JabberRPCElementTypeDictionary,
|
||||
JabberRPCElementTypeMember,
|
||||
JabberRPCElementTypeName,
|
||||
JabberRPCElementTypeInteger,
|
||||
JabberRPCElementTypeDouble,
|
||||
JabberRPCElementTypeBoolean,
|
||||
JabberRPCElementTypeString,
|
||||
JabberRPCElementTypeDate,
|
||||
JabberRPCElementTypeData
|
||||
} JabberRPCElementType;
|
||||
|
||||
|
||||
@interface XMPPIQ(JabberRPCResonse)
|
||||
|
||||
-(NSXMLElement *)methodResponseElement;
|
||||
|
||||
// is this a Jabber RPC method response
|
||||
-(BOOL)isMethodResponse;
|
||||
|
||||
-(BOOL)isFault;
|
||||
|
||||
-(BOOL)isJabberRPC;
|
||||
|
||||
-(id)methodResponse:(NSError **)error;
|
||||
|
||||
-(id)objectFromElement:(NSXMLElement *)param;
|
||||
|
||||
|
||||
#pragma mark -
|
||||
|
||||
-(NSArray *)parseArray:(NSXMLElement *)arrayElement;
|
||||
|
||||
-(NSDictionary *)parseStruct:(NSXMLElement *)structElement;
|
||||
|
||||
-(NSDictionary *)parseMember:(NSXMLElement *)memberElement;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSDate *)parseDateString: (NSString *)dateString withFormat: (NSString *)format;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSNumber *)parseInteger: (NSString *)value;
|
||||
|
||||
- (NSNumber *)parseDouble: (NSString *)value;
|
||||
|
||||
- (NSNumber *)parseBoolean: (NSString *)value;
|
||||
|
||||
- (NSString *)parseString: (NSString *)value;
|
||||
|
||||
- (NSDate *)parseDate: (NSString *)value;
|
||||
|
||||
- (NSData *)parseData: (NSString *)value;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// XMPPIQ+JabberRPCResonse.m
|
||||
// XEP-0009
|
||||
//
|
||||
// Created by Eric Chamberlain on 5/25/10.
|
||||
//
|
||||
|
||||
#import "XMPPIQ+JabberRPCResonse.h"
|
||||
#import "XMPPJabberRPCModule.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "NSData+XMPP.h"
|
||||
#import "NSXMLElement+XMPP.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
|
||||
|
||||
@implementation XMPPIQ(JabberRPCResonse)
|
||||
|
||||
-(NSXMLElement *)methodResponseElement {
|
||||
NSXMLElement *query = [self elementForName:@"query"];
|
||||
return [query elementForName:@"methodResponse"];
|
||||
}
|
||||
|
||||
// is this a Jabber RPC method response
|
||||
-(BOOL)isMethodResponse {
|
||||
/*
|
||||
<methodResponse>
|
||||
<params>
|
||||
<param>
|
||||
<value><string>South Dakota</string></value>
|
||||
</param>
|
||||
</params>
|
||||
</methodResponse>
|
||||
*/
|
||||
NSXMLElement *methodResponse = [self methodResponseElement];
|
||||
return methodResponse != nil;
|
||||
}
|
||||
|
||||
-(BOOL)isFault {
|
||||
/*
|
||||
<methodResponse>
|
||||
<fault>
|
||||
<value>
|
||||
<struct>
|
||||
<member>
|
||||
<name>faultCode</name>
|
||||
<value><int>4</int></value>
|
||||
</member>
|
||||
<member>
|
||||
<name>faultString</name>
|
||||
<value><string>Too many parameters.</string></value>
|
||||
</member>
|
||||
</struct>
|
||||
</value>
|
||||
</fault>
|
||||
</methodResponse>
|
||||
*/
|
||||
NSXMLElement *methodResponse = [self methodResponseElement];
|
||||
|
||||
return [methodResponse elementForName:@"fault"] != nil;
|
||||
}
|
||||
|
||||
-(BOOL)isJabberRPC {
|
||||
/*
|
||||
<query xmlns="jabber:iq:rpc">
|
||||
...
|
||||
</query>
|
||||
*/
|
||||
NSXMLElement *rpcQuery = [self elementForName:@"query" xmlns:@"jabber:iq:rpc"];
|
||||
return rpcQuery != nil;
|
||||
}
|
||||
|
||||
-(id)methodResponse:(NSError **)error {
|
||||
id response = nil;
|
||||
|
||||
/*
|
||||
<methodResponse>
|
||||
<params>
|
||||
<param>
|
||||
<value><string>South Dakota</string></value>
|
||||
</param>
|
||||
</params>
|
||||
</methodResponse>
|
||||
*/
|
||||
|
||||
// or
|
||||
|
||||
/*
|
||||
<methodResponse>
|
||||
<fault>
|
||||
<value>
|
||||
<struct>
|
||||
<member>
|
||||
<name>faultCode</name>
|
||||
<value><int>4</int></value>
|
||||
</member>
|
||||
<member>
|
||||
<name>faultString</name>
|
||||
<value><string>Too many parameters.</string></value>
|
||||
</member>
|
||||
</struct>
|
||||
</value>
|
||||
</fault>
|
||||
</methodResponse>
|
||||
*/
|
||||
NSXMLElement *methodResponse = [self methodResponseElement];
|
||||
|
||||
// parse the methodResponse
|
||||
|
||||
response = [self objectFromElement:(NSXMLElement *)[methodResponse childAtIndex:0]];
|
||||
|
||||
if ([self isFault]) {
|
||||
// we should produce an error
|
||||
// response should be a dict
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:XMPPJabberRPCErrorDomain
|
||||
code:[[response objectForKey:@"faultCode"] intValue]
|
||||
userInfo:(NSDictionary *)response];
|
||||
}
|
||||
response = nil;
|
||||
} else {
|
||||
if (error) {
|
||||
*error = nil;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
-(id)objectFromElement:(NSXMLElement *)param {
|
||||
NSString *element = [param name];
|
||||
|
||||
if ([element isEqualToString:@"params"] ||
|
||||
[element isEqualToString:@"param"] ||
|
||||
[element isEqualToString:@"fault"] ||
|
||||
[element isEqualToString:@"value"]) {
|
||||
|
||||
NSXMLElement *firstChild = (NSXMLElement *)[param childAtIndex:0];
|
||||
if (firstChild) {
|
||||
return [self objectFromElement:firstChild];
|
||||
} else {
|
||||
// no child element, treat it like a string
|
||||
return [self parseString:[param stringValue]];
|
||||
}
|
||||
} else if ([element isEqualToString:@"string"] ||
|
||||
[element isEqualToString:@"name"]) {
|
||||
return [self parseString:[param stringValue]];
|
||||
} else if ([element isEqualToString:@"member"]) {
|
||||
return [self parseMember:param];
|
||||
} else if ([element isEqualToString:@"array"]) {
|
||||
return [self parseArray:param];
|
||||
} else if ([element isEqualToString:@"struct"]) {
|
||||
return [self parseStruct:param];
|
||||
} else if ([element isEqualToString:@"int"]) {
|
||||
return [self parseInteger:[param stringValue]];
|
||||
} else if ([element isEqualToString:@"double"]) {
|
||||
return [self parseDouble:[param stringValue]];
|
||||
} else if ([element isEqualToString:@"boolean"]) {
|
||||
return [self parseBoolean:[param stringValue]];
|
||||
} else if ([element isEqualToString:@"dateTime.iso8601"]) {
|
||||
return [self parseDate:[param stringValue]];
|
||||
} else if ([element isEqualToString:@"base64"]) {
|
||||
return [self parseData:[param stringValue]];
|
||||
} else {
|
||||
// bad element
|
||||
XMPPLogWarn(@"%@: %@ - bad element: %@", THIS_FILE, THIS_METHOD, [param stringValue]);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
|
||||
-(NSArray *)parseArray:(NSXMLElement *)arrayElement {
|
||||
/*
|
||||
<array>
|
||||
<data>
|
||||
<value><i4>12</i4></value>
|
||||
<value><string>Egypt</string></value>
|
||||
<value><boolean>0</boolean></value>
|
||||
<value><i4>-31</i4></value>
|
||||
</data>
|
||||
</array>
|
||||
*/
|
||||
NSXMLElement *data = (NSXMLElement *)[arrayElement childAtIndex:0];
|
||||
NSArray *children = [data children];
|
||||
|
||||
NSMutableArray *array = [NSMutableArray arrayWithCapacity:[children count]];
|
||||
|
||||
for (NSXMLElement *child in children) {
|
||||
[array addObject:[self objectFromElement:child]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
-(NSDictionary *)parseStruct:(NSXMLElement *)structElement {
|
||||
/*
|
||||
<struct>
|
||||
<member>
|
||||
<name>lowerBound</name>
|
||||
<value><i4>18</i4></value>
|
||||
</member>
|
||||
<member>
|
||||
<name>upperBound</name>
|
||||
<value><i4>139</i4></value>
|
||||
</member>
|
||||
</struct>
|
||||
*/
|
||||
NSArray *children = [structElement children];
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:[children count]];
|
||||
|
||||
for (NSXMLElement *child in children) {
|
||||
[dict addEntriesFromDictionary:[self parseMember:child]];
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
-(NSDictionary *)parseMember:(NSXMLElement *)memberElement {
|
||||
NSString *key = [self objectFromElement:[memberElement elementForName:@"name"]];
|
||||
id value = [self objectFromElement:[memberElement elementForName:@"value"]];
|
||||
|
||||
return @{key : value};
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSDate *)parseDateString: (NSString *)dateString withFormat: (NSString *)format {
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
[dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
||||
[dateFormatter setDateFormat: format];
|
||||
|
||||
NSDate *result = [dateFormatter dateFromString: dateString];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSNumber *)parseInteger: (NSString *)value {
|
||||
return @([value integerValue]);
|
||||
}
|
||||
|
||||
- (NSNumber *)parseDouble: (NSString *)value {
|
||||
return @([value doubleValue]);
|
||||
}
|
||||
|
||||
- (NSNumber *)parseBoolean: (NSString *)value {
|
||||
if ([value isEqualToString: @"1"]) {
|
||||
return @YES;
|
||||
}
|
||||
|
||||
return @NO;
|
||||
}
|
||||
|
||||
- (NSString *)parseString: (NSString *)value {
|
||||
return [value stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
}
|
||||
|
||||
- (NSDate *)parseDate: (NSString *)value {
|
||||
NSDate *result = nil;
|
||||
|
||||
result = [self parseDateString: value withFormat: @"yyyyMMdd'T'HH:mm:ss"];
|
||||
|
||||
if (!result) {
|
||||
result = [self parseDateString: value withFormat: @"yyyy'-'MM'-'dd'T'HH:mm:ss"];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSData *)parseData: (NSString *)value {
|
||||
// Convert the base 64 encoded data into a string
|
||||
NSData *base64Data = [value dataUsingEncoding:NSASCIIStringEncoding];
|
||||
NSData *decodedData = [base64Data xmpp_base64Decoded];
|
||||
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// XMPPJabberRPCModule.h
|
||||
// XEP-0009
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 5/16/10.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPModule.h"
|
||||
|
||||
#define _XMPP_JABBER_RPC_MODULE_H
|
||||
|
||||
extern NSString *const XMPPJabberRPCErrorDomain;
|
||||
|
||||
@class XMPPJID;
|
||||
@class XMPPStream;
|
||||
@class XMPPIQ;
|
||||
@protocol XMPPJabberRPCModuleDelegate;
|
||||
|
||||
|
||||
@interface XMPPJabberRPCModule : XMPPModule
|
||||
{
|
||||
NSMutableDictionary *rpcIDs;
|
||||
NSTimeInterval defaultTimeout;
|
||||
}
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval defaultTimeout;
|
||||
|
||||
- (NSString *)sendRpcIQ:(XMPPIQ *)iq;
|
||||
- (NSString *)sendRpcIQ:(XMPPIQ *)iq withTimeout:(NSTimeInterval)timeout;
|
||||
|
||||
// caller knows best when a request has timed out
|
||||
// it should remove the rpcID, on timeout.
|
||||
- (void)timeoutRemoveRpcID:(NSString *)rpcID;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol XMPPJabberRPCModuleDelegate
|
||||
@optional
|
||||
|
||||
// sent when transport error is received
|
||||
-(void)jabberRPC:(XMPPJabberRPCModule *)sender elementID:(NSString *)elementID didReceiveError:(NSError *)error;
|
||||
|
||||
// sent when a methodResponse comes back
|
||||
-(void)jabberRPC:(XMPPJabberRPCModule *)sender elementID:(NSString *)elementID didReceiveMethodResponse:(id)response;
|
||||
|
||||
// sent when a Jabber-RPC server request is received
|
||||
-(void)jabberRPC:(XMPPJabberRPCModule *)sender didReceiveSetIQ:(XMPPIQ *)iq;
|
||||
@end
|
||||
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// XMPPJabberRPCModule.m
|
||||
// XEP-0009
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 5/16/10.
|
||||
//
|
||||
|
||||
#import "XMPPJabberRPCModule.h"
|
||||
#import "XMPP.h"
|
||||
#import "XMPPIQ+JabberRPC.h"
|
||||
#import "XMPPIQ+JabberRPCResonse.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "XMPPFramework.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
|
||||
// Log flags: trace
|
||||
#if DEBUG
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE;
|
||||
#else
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
||||
#endif
|
||||
|
||||
NSString *const XMPPJabberRPCErrorDomain = @"XMPPJabberRPCErrorDomain";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@interface RPCID : NSObject
|
||||
{
|
||||
NSString *rpcID;
|
||||
dispatch_source_t timer;
|
||||
}
|
||||
|
||||
@property (nonatomic, readonly) NSString *rpcID;
|
||||
@property (nonatomic, readonly) dispatch_source_t timer;
|
||||
|
||||
- (id)initWithRpcID:(NSString *)rpcID timer:(dispatch_source_t)timer;
|
||||
|
||||
- (void)cancelTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RPCID
|
||||
|
||||
@synthesize rpcID;
|
||||
@synthesize timer;
|
||||
|
||||
- (id)initWithRpcID:(NSString *)aRpcID timer:(dispatch_source_t)aTimer
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
rpcID = [aRpcID copy];
|
||||
|
||||
timer = aTimer;
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_retain(timer);
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)anObject
|
||||
{
|
||||
return [rpcID isEqual:anObject];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return [rpcID hash];
|
||||
}
|
||||
|
||||
- (void)cancelTimer
|
||||
{
|
||||
if (timer)
|
||||
{
|
||||
dispatch_source_cancel(timer);
|
||||
#if !OS_OBJECT_USE_OBJC
|
||||
dispatch_release(timer);
|
||||
#endif
|
||||
timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self cancelTimer];
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPJabberRPCModule
|
||||
|
||||
@dynamic defaultTimeout;
|
||||
|
||||
- (NSTimeInterval)defaultTimeout
|
||||
{
|
||||
__block NSTimeInterval result;
|
||||
|
||||
dispatch_block_t block = ^{
|
||||
result = defaultTimeout;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setDefaultTimeout:(NSTimeInterval)newDefaultTimeout
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
XMPPLogTrace();
|
||||
defaultTimeout = newDefaultTimeout;
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_async(moduleQueue, block);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Module Management
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithDispatchQueue:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
if ((self = [super initWithDispatchQueue:queue]))
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
rpcIDs = [[NSMutableDictionary alloc] initWithCapacity:5];
|
||||
defaultTimeout = 5.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)aXmppStream
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if ([super activate:aXmppStream])
|
||||
{
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
[xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]];
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
[xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]];
|
||||
#endif
|
||||
|
||||
[super deactivate];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Send RPC
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSString *)sendRpcIQ:(XMPPIQ *)iq
|
||||
{
|
||||
return [self sendRpcIQ:iq withTimeout:[self defaultTimeout]];
|
||||
}
|
||||
|
||||
- (NSString *)sendRpcIQ:(XMPPIQ *)iq withTimeout:(NSTimeInterval)timeout
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
NSString *elementID = [iq elementID];
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
|
||||
|
||||
dispatch_source_set_event_handler(timer, ^{ @autoreleasepool {
|
||||
|
||||
[self timeoutRemoveRpcID:elementID];
|
||||
}});
|
||||
|
||||
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
|
||||
|
||||
dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0);
|
||||
dispatch_resume(timer);
|
||||
|
||||
RPCID *rpcID = [[RPCID alloc] initWithRpcID:elementID timer:timer];
|
||||
|
||||
rpcIDs[elementID] = rpcID;
|
||||
|
||||
[xmppStream sendElement:iq];
|
||||
|
||||
return elementID;
|
||||
}
|
||||
|
||||
- (void)timeoutRemoveRpcID:(NSString *)elementID
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
RPCID *rpcID = rpcIDs[elementID];
|
||||
if (rpcID)
|
||||
{
|
||||
[rpcID cancelTimer];
|
||||
[rpcIDs removeObjectForKey:elementID];
|
||||
|
||||
NSError *error = [NSError errorWithDomain:XMPPJabberRPCErrorDomain
|
||||
code:1400
|
||||
userInfo:@{@"error" : @"Request timed out"}];
|
||||
|
||||
[multicastDelegate jabberRPC:self elementID:elementID didReceiveError:error];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPStream delegate
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
if ([iq isJabberRPC])
|
||||
{
|
||||
if ([iq isResultIQ] || [iq isErrorIQ])
|
||||
{
|
||||
// Example:
|
||||
//
|
||||
// <iq from="deusty.com" to="robbiehanson@deusty.com/work" id="abc123" type="result"/>
|
||||
|
||||
NSString *elementID = [iq elementID];
|
||||
|
||||
// check if this is a JabberRPC query
|
||||
// we could check the query element, but we should be able to do a lookup based on the unique elementID
|
||||
// because we send an ID, we should get one back
|
||||
|
||||
RPCID *rpcID = rpcIDs[elementID];
|
||||
if (rpcID == nil)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
XMPPLogVerbose(@"%@: Received RPC response!", THIS_FILE);
|
||||
|
||||
if ([iq isResultIQ])
|
||||
{
|
||||
id response;
|
||||
NSError *error = nil;
|
||||
|
||||
//TODO: parse iq and generate response.
|
||||
response = [iq methodResponse:&error];
|
||||
|
||||
if (error == nil) {
|
||||
[multicastDelegate jabberRPC:self elementID:elementID didReceiveMethodResponse:response];
|
||||
} else {
|
||||
[multicastDelegate jabberRPC:self elementID:elementID didReceiveError:error];
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: implement error parsing
|
||||
// not much specified in XEP, only 403 forbidden error
|
||||
NSXMLElement *errorElement = [iq childErrorElement];
|
||||
NSError *error = [NSError errorWithDomain:XMPPJabberRPCErrorDomain
|
||||
code:[errorElement attributeIntValueForName:@"code"]
|
||||
userInfo:@{
|
||||
@"error" : [errorElement attributesAsDictionary],
|
||||
@"condition" : [[errorElement childAtIndex:0] name],
|
||||
@"iq" : iq}];
|
||||
|
||||
[multicastDelegate jabberRPC:self elementID:elementID didReceiveError:error];
|
||||
}
|
||||
|
||||
[rpcID cancelTimer];
|
||||
[rpcIDs removeObjectForKey:elementID];
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
} else if ([iq isSetIQ]) {
|
||||
// we would receive set when implementing Jabber-RPC server
|
||||
|
||||
[multicastDelegate jabberRPC:self didReceiveSetIQ:iq];
|
||||
#endif
|
||||
}
|
||||
|
||||
// Jabber-RPC doesn't use get iq type
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPCapabilities delegate
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
/**
|
||||
* If an XMPPCapabilites instance is used we want to advertise our support for JabberRPC.
|
||||
**/
|
||||
- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query
|
||||
{
|
||||
XMPPLogTrace();
|
||||
|
||||
// <query xmlns="http://jabber.org/protocol/disco#info">
|
||||
// ...
|
||||
// <identity category='automation' type='rpc'/>
|
||||
// <feature var='jabber:iq:rpc'/>
|
||||
// ...
|
||||
// </query>
|
||||
[query addChild:[XMPPIQ elementRpcIdentity]];
|
||||
[query addChild:[XMPPIQ elementRpcFeature]];
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// XMPPIQ+LastActivity.h
|
||||
// XEP-0012
|
||||
//
|
||||
// Created by Daniel Rodríguez Troitiño on 1/26/2013.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "XMPPIQ.h"
|
||||
|
||||
@class XMPPJID;
|
||||
|
||||
/**
|
||||
* Last Activity XEP-0012 namespace: "jabber:iq:last".
|
||||
*/
|
||||
extern NSString *const XMPPLastActivityNamespace;
|
||||
|
||||
/**
|
||||
* Helper methods to create last activity queries inside IQ stanzas or to
|
||||
* inspect result IQ answering last activity queries.
|
||||
*/
|
||||
@interface XMPPIQ (LastActivity)
|
||||
|
||||
/**
|
||||
* Returns an get XMPPIQ with a last activity query child element addressed to
|
||||
* the given JID.
|
||||
*/
|
||||
+ (XMPPIQ *)lastActivityQueryTo:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* Returns a result XMPPIQ answering the given XMPPIQ request with the given
|
||||
* number of seconds.
|
||||
*
|
||||
* The from and to attributes of the returned XMPPIQ will be switched from the
|
||||
* from and to attributes of the given request. The element ID will be the same
|
||||
* of the given request. No status message will be included.
|
||||
*/
|
||||
+ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds;
|
||||
|
||||
/**
|
||||
* Returns a result XMPPIQ answering the given XMPPIQ request with the given
|
||||
* number of seconds and the given unavailable status.
|
||||
*
|
||||
* The from and to attributes of the returned XMPPIQ will be switched from the
|
||||
* from and to attributes of the given request. The element ID will be the same
|
||||
* of the given request.
|
||||
*/
|
||||
+ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds status:(NSString *)status;
|
||||
|
||||
/**
|
||||
* Returns an error XMPPIQ answering the given XMPPIQ request.
|
||||
*
|
||||
* The from and to attributes of the returned XMPPIQ will be switched from the
|
||||
* from and to attributes of the given request. The element ID will be the same
|
||||
* of the given request.
|
||||
*
|
||||
* An error element with a forbidden element will be included as child element:
|
||||
*
|
||||
* <iq from='juliet@capulet.com'
|
||||
* id='last1'
|
||||
* to='romeo@montague.net/orchard'
|
||||
* type='error'>
|
||||
* <error type='auth'>
|
||||
* <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
* </error>
|
||||
* </iq>
|
||||
*/
|
||||
+ (XMPPIQ *)lastActivityResponseForbiddenToIQ:(XMPPIQ *)request;
|
||||
|
||||
/**
|
||||
* Returns YES if the child element of this XMPPIQ is a query element from the
|
||||
* XMPPLastActivityNamespace. Otherwise returns NO.
|
||||
*/
|
||||
- (BOOL)isLastActivityQuery;
|
||||
|
||||
/**
|
||||
* Returns the parsed number from the seconds attribute of the query subelement.
|
||||
*
|
||||
* Returns NSNotFound in case this XMPPIQ has no query element, or that query
|
||||
* element does not have a seconds attribute. Returns 0 in case the seconds
|
||||
* attribute cannot be parsed as a integer.
|
||||
*/
|
||||
- (NSUInteger)lastActivitySeconds;
|
||||
|
||||
/**
|
||||
* Returns the contents of the query subelement.
|
||||
*
|
||||
* Returns nil in case this XMPPIQ has no query element, or the query element
|
||||
* does not have contents.
|
||||
*/
|
||||
- (NSString *)lastActivityUnavailableStatus;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// XMPPIQ+LastActivity.m
|
||||
// XEP-0012
|
||||
//
|
||||
// Created by Daniel Rodríguez Troitiño on 1/26/2013.
|
||||
//
|
||||
|
||||
#import "XMPPIQ+LastActivity.h"
|
||||
|
||||
#import "XMPP.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
NSString *const XMPPLastActivityNamespace = @"jabber:iq:last";
|
||||
|
||||
@implementation XMPPIQ (LastActivity)
|
||||
|
||||
+ (XMPPIQ *)lastActivityQueryTo:(XMPPJID *)jid
|
||||
{
|
||||
NSXMLElement *query = [[NSXMLElement alloc] initWithName:@"query" xmlns:XMPPLastActivityNamespace];
|
||||
return [[self alloc] initWithType:@"get" to:jid elementID:[XMPPStream generateUUID] child:query];
|
||||
}
|
||||
|
||||
+ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds
|
||||
{
|
||||
return [self lastActivityResponseToIQ:request withSeconds:seconds status:nil];
|
||||
}
|
||||
|
||||
+ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds status:(NSString *)status
|
||||
{
|
||||
NSXMLElement *query = [[NSXMLElement alloc] initWithName:@"query" xmlns:XMPPLastActivityNamespace];
|
||||
[query addAttributeWithName:@"seconds" stringValue:[NSString stringWithFormat:@"%lu", (unsigned long)seconds]];
|
||||
if (status && [status length] > 0)
|
||||
{
|
||||
[query setStringValue:status];
|
||||
}
|
||||
|
||||
return [[self alloc] initWithType:@"result" to:request.from elementID:request.elementID child:query];
|
||||
}
|
||||
|
||||
+ (XMPPIQ *)lastActivityResponseForbiddenToIQ:(XMPPIQ *)request
|
||||
{
|
||||
NSXMLElement *reason = [[NSXMLElement alloc] initWithName:@"forbidden" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"];
|
||||
NSXMLElement *error = [[NSXMLElement alloc] initWithName:@"error"];
|
||||
[error addAttributeWithName:@"type" stringValue:@"auth"];
|
||||
[error addChild:reason];
|
||||
|
||||
return [[self alloc] initWithType:@"error" to:request.from elementID:request.elementID child:error];
|
||||
}
|
||||
|
||||
- (BOOL)isLastActivityQuery
|
||||
{
|
||||
return nil != [self lastActivityQueryElement];
|
||||
}
|
||||
|
||||
- (NSUInteger)lastActivitySeconds
|
||||
{
|
||||
NSUInteger seconds = NSNotFound;
|
||||
NSXMLElement *query = [self lastActivityQueryElement];
|
||||
if (query)
|
||||
{
|
||||
NSXMLNode *attribute = [query attributeForName:@"seconds"];
|
||||
if (attribute)
|
||||
{
|
||||
// Some Servers Return -1 to indicate no activity, so we need to ignore these
|
||||
if([query attributeIntegerValueForName:@"seconds"] >= 0)
|
||||
{
|
||||
seconds = [query attributeUnsignedIntegerValueForName:@"seconds"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
- (NSString *)lastActivityUnavailableStatus
|
||||
{
|
||||
NSXMLElement *query = [self lastActivityQueryElement];
|
||||
if (query)
|
||||
{
|
||||
return [query stringValue];
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSXMLElement *)lastActivityQueryElement
|
||||
{
|
||||
return [self elementForName:@"query" xmlns:XMPPLastActivityNamespace];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// XMPPLastActivity.h
|
||||
// XEP-0012
|
||||
//
|
||||
// Created by Daniel Rodríguez Troitiño on 1/26/2013.
|
||||
//
|
||||
//
|
||||
|
||||
#import "XMPP.h"
|
||||
#import "XMPPIQ+LastActivity.h"
|
||||
|
||||
#define _XMPP_LAST_ACTIVITY_H
|
||||
|
||||
/**
|
||||
* Provides support to both sending last activity queries and answering those
|
||||
* last activity queries done by other entities, as documented in the XEP-0012.
|
||||
*
|
||||
* The automatic responses can be disabled setting respondsToQueries to NO.
|
||||
*
|
||||
* If no delegate of this class responds to
|
||||
* numberOfIdleTimeSecondsForXMPPLastActivity:currentIdleTimeSeconds: a default
|
||||
* value of 0 will be send to the other entities. You are encouraged to provide
|
||||
* at least one delegate with this method implemented.
|
||||
*/
|
||||
@interface XMPPLastActivity : XMPPModule
|
||||
|
||||
/**
|
||||
* Whether or not the module should respond to incoming last activity queries.
|
||||
*
|
||||
* If you create multiple instances of this module, only one instance should
|
||||
* respond to queries.
|
||||
*
|
||||
* It is recommended you set this (if needed) before you activate the module.
|
||||
*
|
||||
* The default value is YES.
|
||||
*/
|
||||
@property (atomic, assign) BOOL respondsToQueries;
|
||||
|
||||
/**
|
||||
* Send a last activity query to an specific JID.
|
||||
*
|
||||
* XEP-0012 specifies that last activity queries can be send to offline
|
||||
* entities (bare JID), to online resources (full JID) or servers (domain only
|
||||
* JID). The answers of each of those kind of JIDs are a little different and
|
||||
* has different meaning. It is responsability of the consumer to interpretate
|
||||
* those answers.
|
||||
*
|
||||
* The module will wait for an answer from the server or the entity for at most
|
||||
* 30 seconds before considering this request a timeout failure.
|
||||
*
|
||||
* The delegates of the module should implement
|
||||
* xmppLastActivity:didReceiveResponse: and
|
||||
* xmppLastActivity:didNotReceiveResponse:dueToTimeout: to be informed when the
|
||||
* result of the IQ arrives (or the timeout happens).
|
||||
*
|
||||
* The returning string is the element ID of the IQ that was sent including the
|
||||
* last activity query.
|
||||
*/
|
||||
- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid;
|
||||
|
||||
/**
|
||||
* Send a last activity query to an specific JID with an specific timeout.
|
||||
*
|
||||
* XEP-0012 specifies that last activity queries can be send to offline
|
||||
* entities (bare JID), to online resources (full JID) or servers (domain only
|
||||
* JID). The answers of each of those kind of JIDs are a little different and
|
||||
* has different meaning. It is responsability of the consumer to interpretate
|
||||
* those answers.
|
||||
*
|
||||
* The module will wait for an answer from the server or the entity for at most
|
||||
* the number seconds given by tiemout before considering this request a timeout
|
||||
* failure.
|
||||
*
|
||||
* The delegates of the module should implement
|
||||
* xmppLastActivity:didReceiveResponse: and
|
||||
* xmppLastActivity:didNotReceiveResponse:dueToTimeout: to be informed when the
|
||||
* result of the IQ arrives (or the timeout happens).
|
||||
*
|
||||
* The returning string is the element ID of the IQ that was sent including the
|
||||
* last activity query.
|
||||
*/
|
||||
- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout;
|
||||
|
||||
@end
|
||||
|
||||
@protocol XMPPLastActivityDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Callback when the XMPPLastActivity sender receives a result IQ or an error IQ
|
||||
* for a previous get IQ.
|
||||
*
|
||||
* The XMPPIQ response can be queried using the methods in the category
|
||||
* XMPPIQ (LastActivity) for the seconds and status value.
|
||||
*/
|
||||
- (void)xmppLastActivity:(XMPPLastActivity *)sender didReceiveResponse:(XMPPIQ *)response;
|
||||
|
||||
/**
|
||||
* Callback when the XMPPLastActivity sender does not receive neither a result
|
||||
* IQ nor a error IQ after timeout seconds have passed.
|
||||
*
|
||||
* The queryID is the element ID used to send the query, so the consumer can
|
||||
* compare it with the element ID returned by the sendLastActivityQueryToJID:
|
||||
* methods and act accordingly.
|
||||
*/
|
||||
- (void)xmppLastActivity:(XMPPLastActivity *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout;
|
||||
|
||||
/**
|
||||
* Callback to obtain the number of idle seconds that the XMPPLastActivity
|
||||
* sender should use as answer to a last activity query iq.
|
||||
*
|
||||
* Each delegate will be asked in turn (the order is not guaranteed) and each of
|
||||
* then should decide to return the given idleSeconds value, or a new value as
|
||||
* the number of idleSeconds. The first delegate will receive NSNotFound as the
|
||||
* value of idleSecond. If the last delegate returns NSNotFound as result, the
|
||||
* answer will be 0 seconds.
|
||||
*/
|
||||
- (NSUInteger)numberOfIdleTimeSecondsForXMPPLastActivity:(XMPPLastActivity *)sender queryIQ:(XMPPIQ *)iq currentIdleTimeSeconds:(NSUInteger)idleSeconds;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// XMPPLastActivity.m
|
||||
// XEP-0012
|
||||
//
|
||||
// Created by Daniel Rodríguez Troitiño on 1/26/2013.
|
||||
//
|
||||
//
|
||||
|
||||
#import "XMPPLastActivity.h"
|
||||
#import "XMPPIDTracker.h"
|
||||
#import "XMPPIQ+LastActivity.h"
|
||||
#import "XMPPFramework.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
static const NSTimeInterval XMPPLastActivityDefaultTimeout = 30.0;
|
||||
|
||||
|
||||
@interface XMPPLastActivity ()
|
||||
|
||||
@property (atomic, strong) XMPPIDTracker *queryTracker;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation XMPPLastActivity
|
||||
|
||||
@synthesize respondsToQueries = _respondsToQueries;
|
||||
@synthesize queryTracker = _queryTracker;
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithDispatchQueue:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
||||
{
|
||||
if ((self = [super initWithDispatchQueue:queue]))
|
||||
{
|
||||
_respondsToQueries = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)activate:(XMPPStream *)aXmppStream
|
||||
{
|
||||
if ([super activate:aXmppStream])
|
||||
{
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
[xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]];
|
||||
#endif
|
||||
|
||||
_queryTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)deactivate
|
||||
{
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
[xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]];
|
||||
#endif
|
||||
|
||||
dispatch_block_t block = ^{ @autoreleasepool {
|
||||
[_queryTracker removeAllIDs];
|
||||
_queryTracker = nil;
|
||||
}};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
|
||||
[super deactivate];
|
||||
}
|
||||
|
||||
- (BOOL)respondsToQueries
|
||||
{
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
{
|
||||
return _respondsToQueries;
|
||||
}
|
||||
else
|
||||
{
|
||||
__block BOOL result;
|
||||
|
||||
dispatch_sync(moduleQueue, ^{
|
||||
result = _respondsToQueries;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRespondsToQueries:(BOOL)respondsToQueries
|
||||
{
|
||||
dispatch_block_t block = ^{
|
||||
if (_respondsToQueries != respondsToQueries)
|
||||
{
|
||||
_respondsToQueries = respondsToQueries;
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
@autoreleasepool {
|
||||
// Capabilities may have changed, need to notify others
|
||||
[xmppStream resendMyPresence];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
if (dispatch_get_specific(moduleQueueTag))
|
||||
block();
|
||||
else
|
||||
dispatch_sync(moduleQueue, block);
|
||||
}
|
||||
|
||||
- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid
|
||||
{
|
||||
return [self sendLastActivityQueryToJID:jid withTimeout:XMPPLastActivityDefaultTimeout];
|
||||
}
|
||||
|
||||
- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout
|
||||
{
|
||||
XMPPIQ *query = [XMPPIQ lastActivityQueryTo:jid];
|
||||
NSString *queryID = query.elementID;
|
||||
|
||||
dispatch_async(moduleQueue, ^{
|
||||
__weak __typeof__(self) self_weak_ = self;
|
||||
[_queryTracker addID:queryID block:^(XMPPIQ *iq, id<XMPPTrackingInfo> info) {
|
||||
__strong __typeof__(self) self = self_weak_;
|
||||
if (iq)
|
||||
{
|
||||
[self delegateDidReceiveResponse:iq];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self delegateDidNotReceiveResponse:info.elementID dueToTimeout:info.timeout];
|
||||
}
|
||||
} timeout:timeout];
|
||||
|
||||
[xmppStream sendElement:query];
|
||||
});
|
||||
|
||||
return queryID;
|
||||
}
|
||||
|
||||
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
|
||||
{
|
||||
NSString *type = [iq type];
|
||||
|
||||
if (([type isEqualToString:@"result"] || [type isEqualToString:@"error"]))
|
||||
{
|
||||
return [_queryTracker invokeForID:iq.elementID withObject:iq];
|
||||
}
|
||||
else if (_respondsToQueries && [type isEqualToString:@"get"] && [iq isLastActivityQuery])
|
||||
{
|
||||
NSUInteger seconds = [self delegateNumberOfIdleTimeSecondsWithIQ:iq];
|
||||
XMPPIQ *response = [XMPPIQ lastActivityResponseToIQ:iq withSeconds:seconds];
|
||||
[sender sendElement:response];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
|
||||
{
|
||||
[_queryTracker removeAllIDs];
|
||||
}
|
||||
|
||||
#ifdef _XMPP_CAPABILITIES_H
|
||||
// If an XMPPCapabilities instance is used we want to advertise our support for last activity.
|
||||
- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query
|
||||
{
|
||||
if (_respondsToQueries)
|
||||
{
|
||||
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
|
||||
[feature addAttributeWithName:@"var" stringValue:XMPPLastActivityNamespace];
|
||||
|
||||
[query addChild:feature];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)delegateDidReceiveResponse:(XMPPIQ *)response
|
||||
{
|
||||
[multicastDelegate xmppLastActivity:self didReceiveResponse:response];
|
||||
}
|
||||
|
||||
- (void)delegateDidNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout
|
||||
{
|
||||
[multicastDelegate xmppLastActivity:self didNotReceiveResponse:queryID dueToTimeout:timeout];
|
||||
}
|
||||
|
||||
- (NSUInteger)delegateNumberOfIdleTimeSecondsWithIQ:(XMPPIQ *)iq
|
||||
{
|
||||
__block NSUInteger idleSeconds = NSNotFound;
|
||||
|
||||
GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator];
|
||||
|
||||
id delegate;
|
||||
dispatch_queue_t delegateQueue;
|
||||
SEL selector = @selector(numberOfIdleTimeSecondsForXMPPLastActivity:queryIQ:currentIdleTimeSeconds:);
|
||||
|
||||
while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue forSelector:selector])
|
||||
{
|
||||
dispatch_sync(delegateQueue, ^{ @autoreleasepool {
|
||||
idleSeconds = [delegate numberOfIdleTimeSecondsForXMPPLastActivity:self queryIQ:iq currentIdleTimeSeconds:idleSeconds];
|
||||
}});
|
||||
}
|
||||
|
||||
return idleSeconds == NSNotFound ? 0 : idleSeconds;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,231 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "XMPPModule.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import "DDXML.h"
|
||||
#endif
|
||||
|
||||
#define _XMPP_PRIVACY_H
|
||||
|
||||
@class XMPPIQ;
|
||||
|
||||
extern NSString *const XMPPPrivacyErrorDomain;
|
||||
|
||||
typedef enum XMPPPrivacyErrorCode
|
||||
{
|
||||
XMPPPrivacyQueryTimeout, // No response from server
|
||||
XMPPPrivacyDisconnect, // XMPP disconnection
|
||||
|
||||
} XMPPPrivacyErrorCode;
|
||||
|
||||
|
||||
|
||||
@interface XMPPPrivacy : XMPPModule
|
||||
{
|
||||
BOOL autoRetrievePrivacyListNames;
|
||||
BOOL autoRetrievePrivacyListItems;
|
||||
BOOL autoClearPrivacyListInfo;
|
||||
|
||||
NSMutableDictionary *privacyDict;
|
||||
NSString *activeListName;
|
||||
NSString *defaultListName;
|
||||
|
||||
NSMutableDictionary *pendingQueries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the module should automatically retrieve the privacy list names.
|
||||
* If this property is enabled, then the list is automatically fetched when the client signs in.
|
||||
*
|
||||
* The default value is YES.
|
||||
**/
|
||||
@property (readwrite, assign) BOOL autoRetrievePrivacyListNames;
|
||||
|
||||
/**
|
||||
* Whether or not the module should automatically retrieve the privacy list rules.
|
||||
* If this property is enabled, then the rules for each privacy list are automatically fetched.
|
||||
*
|
||||
* In other words, if the privacy list names are fetched (either automatically, or via retrieveListNames),
|
||||
* then the module will automatically fetch the associated rules.
|
||||
* It will also update the set of rules if we receive a "privacy list push"
|
||||
* from the server that another resource has changed one of the privacy lists.
|
||||
*
|
||||
* The default value is YES.
|
||||
**/
|
||||
@property (readwrite, assign) BOOL autoRetrievePrivacyListItems;
|
||||
|
||||
/**
|
||||
* Whether the module should automatically clear the privacy list info when the client disconnects.
|
||||
*
|
||||
* As per the XEP, if there are multiple resources signed in for the user,
|
||||
* and one resource makes changes to a privacy list, all other resources are "pushed" a notification.
|
||||
* However, if our client is disconnected when another resource makes the changes,
|
||||
* then the only way we can find out about the changes are to redownload the privacy lists.
|
||||
*
|
||||
* It is recommended to clear the privacy list to assure we have the correct info.
|
||||
* However, there may be specific situations in which an xmpp client can be sure the privacy list won't change.
|
||||
*
|
||||
* The default value is YES.
|
||||
**/
|
||||
@property (readwrite, assign) BOOL autoClearPrivacyListInfo;
|
||||
|
||||
/**
|
||||
* Manual fetch of list names and rules, and manual control over when to clear stored info.
|
||||
**/
|
||||
- (void)retrieveListNames;
|
||||
- (void)retrieveListWithName:(NSString *)privacyListName;
|
||||
- (void)clearPrivacyListInfo;
|
||||
|
||||
/**
|
||||
* Returns the privacy list names.
|
||||
* This is an array of strings.
|
||||
**/
|
||||
- (NSArray *)listNames;
|
||||
|
||||
/**
|
||||
* Returns the privacy list rules/items for the given list name.
|
||||
*
|
||||
* The result is an array or privacy items (NSXMLElement's).
|
||||
* The array is sorted according to order, where the item with the smallest 'order' is first in the array.
|
||||
**/
|
||||
- (NSArray *)listWithName:(NSString *)privacyListName;
|
||||
|
||||
/**
|
||||
* Returns information about the active list.
|
||||
* If there is no active list, the methods return nil.
|
||||
*
|
||||
* The activeList method is a convenience method for [modPriv listWithName:[modPriv activeListName]]
|
||||
**/
|
||||
- (NSString *)activeListName;
|
||||
- (NSArray *)activeList;
|
||||
|
||||
/**
|
||||
* Returns information about the default list.
|
||||
* If there is no default list, the methods return nil.
|
||||
*
|
||||
* The defaultList method is a convenience method for [modPriv listWithName:[modPriv defaultListName]]
|
||||
**/
|
||||
- (NSString *)defaultListName;
|
||||
- (NSArray *)defaultList;
|
||||
|
||||
/**
|
||||
* Changes the client's active privacy list to the list with the given name.
|
||||
* The privacy list name must match the name of an existing privacy list.
|
||||
*
|
||||
* To decline the use of an active list simply pass nil to this method.
|
||||
*
|
||||
* Once the server has acknowledged the change,
|
||||
* the delegate method xmppPrivacy:didSetActiveListName: will be invoked.
|
||||
* If the server is unable to process the change (e.g. invalid list name),
|
||||
* the delegate method xmppPrivacy:didNotSetActiveListName:error: will be invoked.
|
||||
*
|
||||
* The methods activeListName and activeList will update after the server acknowledges the change.
|
||||
**/
|
||||
- (void)setActiveListName:(NSString *)privacyListName;
|
||||
|
||||
/**
|
||||
* Changes the client's default privacy list to the list with the given name.
|
||||
* The privacy list name must match the name of an existing privacy list.
|
||||
*
|
||||
* To decline the use of a default list simply pass nil to this method.
|
||||
*
|
||||
* Once the server has acknowledged the change,
|
||||
* the delegate method xmppPrivacy:didSetDefaultListName: will be invoked.
|
||||
* If the server is unable to process the change (e.g. invalid list name, in use by another resource),
|
||||
* the delegate method xmppPrivacy:didNotSetDefaultListName:error: will be invoked.
|
||||
*
|
||||
* The methods defaultListName and defaultList will update after the server acknowledges the change.
|
||||
**/
|
||||
- (void)setDefaultListName:(NSString *)privacyListName;
|
||||
|
||||
/**
|
||||
* Adds/Edits/Removes a privacy list with the given name.
|
||||
* The given array should contain only privacy items (NSXMLElement's).
|
||||
*
|
||||
* To remove a privacy list, invoke this method will a nil or empty items parameter.
|
||||
**/
|
||||
- (void)setListWithName:(NSString *)privacyListName items:(NSArray *)items;
|
||||
|
||||
/**
|
||||
* The following are convenience methods to create privacy item rules.
|
||||
* A quick refresher from the XEP-0016 documentation is provided below.
|
||||
*
|
||||
* The 'type' attribute is OPTIONAL, and must be one of: jid|group|subscription
|
||||
*
|
||||
* If the 'type' is 'jid', then the 'value' must contain a valid JID.
|
||||
* JIDs are to be matched in the following order:
|
||||
*
|
||||
* - <user@domain/resource> (only that resource matches)
|
||||
* - <user@domain> (any resource matches)
|
||||
* - <domain/resource> (only that resource matches)
|
||||
* - <domain> (the domain itself matches, as does any user@domain or domain/resource)
|
||||
*
|
||||
* If the 'type' is 'group', then the 'value' should contain the name of a group in the user's roster.
|
||||
*
|
||||
* If the 'type' is 'subscription', then the 'value' must be one of: both|to|from|none
|
||||
*
|
||||
* The 'action' attribute is MANDATORY and must be one of: allow|deny
|
||||
*
|
||||
* The 'order' attribute is MANDATORY and must be a non-negative integer that is unique among all items in the list.
|
||||
* List items are processed by the server according to the 'order' attribute in ascending order. (0 before 1, etc)
|
||||
* Once the server matches a privacy item in the list, it obeys the item and ceases processing.
|
||||
*
|
||||
* The privacy item may contain one or more child elements that specify more granular blocking control:
|
||||
*
|
||||
* - <message/> (blocks incoming message stanzas)
|
||||
* - <iq/> (blocks incoming IQ stanzas)
|
||||
* - <presence-in/> (blocks incoming presence notifications)
|
||||
* - <presence-out/> (blocks outgoing presence notifications)
|
||||
**/
|
||||
+ (NSXMLElement *)privacyItemWithAction:(NSString *)action order:(NSUInteger)order;
|
||||
+ (NSXMLElement *)privacyItemWithType:(NSString *)type
|
||||
value:(NSString *)value
|
||||
action:(NSString *)action
|
||||
order:(NSUInteger)order;
|
||||
|
||||
+ (void)blockIQs:(NSXMLElement *)privacyItem;
|
||||
+ (void)blockMessages:(NSXMLElement *)privacyItem;
|
||||
+ (void)blockPresenceIn:(NSXMLElement *)privacyItem;
|
||||
+ (void)blockPresenceOut:(NSXMLElement *)privacyItem;
|
||||
|
||||
@end
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@protocol XMPPPrivacyDelegate
|
||||
@optional
|
||||
|
||||
/**
|
||||
* The following delegate methods correspond almost exactly with the action methods of the class.
|
||||
* There are a few possible ways in which an action could fail:
|
||||
*
|
||||
* 1. We receive an error response from the server.
|
||||
* 2. We receive no response from the server, and the query times out.
|
||||
* 3. We get disconnected before we receive the response.
|
||||
*
|
||||
* In case number 1, the error will be an XMPPIQ of type='error'.
|
||||
*
|
||||
* In case number 2 or 3, the error will be an NSError
|
||||
* with domain=XMPPPrivacyErrorDomain and code from the XMPPPrivacyErrorCode enumeration.
|
||||
**/
|
||||
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didReceiveListNames:(NSArray *)listNames;
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotReceiveListNamesDueToError:(id)error;
|
||||
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didReceiveListWithName:(NSString *)name items:(NSArray *)items;
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotReceiveListWithName:(NSString *)name error:(id)error;
|
||||
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didReceivePushWithListName:(NSString *)name;
|
||||
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didSetActiveListName:(NSString *)name;
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotSetActiveListName:(NSString *)name error:(id)error;
|
||||
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didSetDefaultListName:(NSString *)name;
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotSetDefaultListName:(NSString *)name error:(id)error;
|
||||
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didSetListWithName:(NSString *)name;
|
||||
- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotSetListWithName:(NSString *)name error:(id)error;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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>XMPPvCard.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// XMPPvCardAvatarCoreDataStorageObject.h
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
// This class is so that we don't load the photoData each time we need to touch the XMPPvCardCoreDataStorageObject.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class XMPPvCardCoreDataStorageObject;
|
||||
|
||||
|
||||
@interface XMPPvCardAvatarCoreDataStorageObject : NSManagedObject
|
||||
|
||||
@property (nonatomic, strong) NSData * photoData;
|
||||
@property (nonatomic, strong) XMPPvCardCoreDataStorageObject * vCard;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// XMPPvCardAvatarCoreDataStorageObject.m
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
|
||||
#import "XMPPvCardAvatarCoreDataStorageObject.h"
|
||||
#import "XMPPvCardCoreDataStorageObject.h"
|
||||
|
||||
|
||||
@implementation XMPPvCardAvatarCoreDataStorageObject
|
||||
|
||||
@dynamic photoData;
|
||||
@dynamic vCard;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// XMPPvCardCoreDataStorage.h
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPPCoreDataStorage.h"
|
||||
#import "XMPPvCardTempModule.h"
|
||||
#import "XMPPvCardAvatarModule.h"
|
||||
|
||||
/**
|
||||
* This class is an example implementation of XMPPCapabilitiesStorage using core data.
|
||||
* You are free to substitute your own storage class.
|
||||
**/
|
||||
|
||||
@interface XMPPvCardCoreDataStorage : XMPPCoreDataStorage <
|
||||
XMPPvCardAvatarStorage,
|
||||
XMPPvCardTempModuleStorage
|
||||
> {
|
||||
// Inherits protected variables from XMPPCoreDataStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* XEP-0054 provides a mechanism for transmitting vCards via XMPP.
|
||||
* Because the JID doesn't change very often and can be large with image data,
|
||||
* it is safe to persistently store the JID and wait for a user to explicity ask for an update,
|
||||
* or use XEP-0153 to monitor for JID changes.
|
||||
*
|
||||
* For this reason, it is recommended you use this sharedInstance across all your xmppStreams.
|
||||
* This way all streams can shared a knowledgebase concerning known JIDs and Avatar photos.
|
||||
*
|
||||
* All other aspects of vCard handling (such as lookup failures, etc) are kept separate between streams.
|
||||
**/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
//
|
||||
// This class inherits from XMPPCoreDataStorage.
|
||||
//
|
||||
// Please see the XMPPCoreDataStorage header file for more information.
|
||||
//
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// XMPPvCardCoreDataStorage.m
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
|
||||
#import "XMPPvCardCoreDataStorage.h"
|
||||
#import "XMPPvCardCoreDataStorageObject.h"
|
||||
#import "XMPPvCardTempCoreDataStorageObject.h"
|
||||
#import "XMPPvCardAvatarCoreDataStorageObject.h"
|
||||
#import "XMPP.h"
|
||||
#import "XMPPCoreDataStorageProtected.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
|
||||
|
||||
enum {
|
||||
kXMPPvCardTempNetworkFetchTimeout = 10,
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark -
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@implementation XMPPvCardCoreDataStorage
|
||||
|
||||
static XMPPvCardCoreDataStorage *sharedInstance;
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
sharedInstance = [[XMPPvCardCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil];
|
||||
});
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Setup
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (BOOL)configureWithParent:(XMPPvCardTempModule *)aParent queue:(dispatch_queue_t)queue
|
||||
{
|
||||
return [super configureWithParent:aParent queue:queue];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Overrides
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
autoAllowExternalBinaryDataStorage = YES;
|
||||
autoRecreateDatabaseFile = YES;
|
||||
[super commonInit];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPvCardAvatarStorage protocol
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (NSData *)photoDataForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
// This is a public method.
|
||||
// It may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
__block NSData *result;
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard;
|
||||
vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
result = vCard.photoData;
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)photoHashForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
// This is a public method.
|
||||
// It may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
__block NSString *result;
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard;
|
||||
vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
result = vCard.photoHash;
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)clearvCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
// This is a public method.
|
||||
// It may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard;
|
||||
vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
vCard.vCardTemp = nil;
|
||||
vCard.lastUpdated = [NSDate date];
|
||||
}];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark XMPPvCardTempModuleStorage protocol
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (XMPPvCardTemp *)vCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
// This is a public method.
|
||||
// It may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
__block XMPPvCardTemp *result;
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard;
|
||||
vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
result = vCard.vCardTemp;
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)setvCardTemp:(XMPPvCardTemp *)vCardTemp forJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
// This is a public method.
|
||||
// It may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
[self scheduleBlock:^{
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard;
|
||||
vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
vCard.waitingForFetch = @NO;
|
||||
vCard.vCardTemp = vCardTemp;
|
||||
|
||||
// Update photo and photo hash
|
||||
vCard.photoData = vCardTemp.photo;
|
||||
|
||||
vCard.lastUpdated = [NSDate date];
|
||||
}];
|
||||
}
|
||||
|
||||
- (XMPPvCardTemp *)myvCardTempForXMPPStream:(XMPPStream *)stream
|
||||
{
|
||||
if(!stream) return nil;
|
||||
|
||||
return [self vCardTempForJID:[[stream myJID] bareJID] xmppStream:stream];
|
||||
}
|
||||
|
||||
- (BOOL)shouldFetchvCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream
|
||||
{
|
||||
// This is a public method.
|
||||
// It may be invoked on any thread/queue.
|
||||
|
||||
XMPPLogTrace();
|
||||
|
||||
__block BOOL result;
|
||||
|
||||
[self executeBlock:^{
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard;
|
||||
vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
|
||||
BOOL waitingForFetch = [vCard.waitingForFetch boolValue];
|
||||
|
||||
if(![stream isAuthenticated])
|
||||
{
|
||||
result = NO;
|
||||
}
|
||||
else if (!waitingForFetch)
|
||||
{
|
||||
vCard.waitingForFetch = @YES;
|
||||
vCard.lastUpdated = [NSDate date];
|
||||
|
||||
result = YES;
|
||||
}
|
||||
else if ([vCard.lastUpdated timeIntervalSinceNow] < -kXMPPvCardTempNetworkFetchTimeout)
|
||||
{
|
||||
// Our last request exceeded the timeout, send a new one
|
||||
vCard.lastUpdated = [NSDate date];
|
||||
|
||||
result = YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We already have an outstanding request, no need to send another one.
|
||||
result = NO;
|
||||
}
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// XMPPvCardCoreDataStorageObject.h
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
|
||||
@class XMPPJID;
|
||||
@class XMPPvCardTemp;
|
||||
@class XMPPvCardTempCoreDataStorageObject;
|
||||
@class XMPPvCardAvatarCoreDataStorageObject;
|
||||
|
||||
|
||||
@interface XMPPvCardCoreDataStorageObject : NSManagedObject
|
||||
|
||||
|
||||
/*
|
||||
* User's JID, indexed for lookups
|
||||
*/
|
||||
@property (nonatomic, strong) NSString * jidStr;
|
||||
|
||||
/*
|
||||
* User's photoHash used by XEP-0153
|
||||
*/
|
||||
@property (nonatomic, strong, readonly) NSString * photoHash;
|
||||
|
||||
/*
|
||||
* The last time the record was modified, also used to determine if we need to fetch again
|
||||
*/
|
||||
@property (nonatomic, strong) NSDate * lastUpdated;
|
||||
|
||||
|
||||
/*
|
||||
* Flag indicating whether a get request is already pending, used in conjunction with lastUpdated
|
||||
*/
|
||||
@property (nonatomic, strong) NSNumber * waitingForFetch;
|
||||
|
||||
|
||||
/*
|
||||
* Relationship to the vCardTemp record.
|
||||
* We use a relationship, so the vCardTemp stays faulted until we really need it.
|
||||
*/
|
||||
@property (nonatomic, strong) XMPPvCardTempCoreDataStorageObject * vCardTempRel;
|
||||
|
||||
|
||||
/*
|
||||
* Relationship to the vCardAvatar record.
|
||||
* We use a relationship, so the vCardAvatar stays faulted until we really need it.
|
||||
*/
|
||||
@property (nonatomic, strong) XMPPvCardAvatarCoreDataStorageObject * vCardAvatarRel;
|
||||
|
||||
|
||||
/*
|
||||
* Accessor to retrieve photoData, so we can hide the underlying relationship implementation.
|
||||
*/
|
||||
@property (nonatomic, strong) NSData *photoData;
|
||||
|
||||
|
||||
/*
|
||||
* Accessor to retrieve vCardTemp, so we can hide the underlying relationship implementation.
|
||||
*/
|
||||
@property (nonatomic, strong) XMPPvCardTemp *vCardTemp;
|
||||
|
||||
|
||||
+ (XMPPvCardCoreDataStorageObject *)fetchOrInsertvCardForJID:(XMPPJID *)jid
|
||||
inManagedObjectContext:(NSManagedObjectContext *)moc;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// XMPPvCardCoreDataStorageObject.m
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
|
||||
#import "XMPPvCardCoreDataStorageObject.h"
|
||||
#import "XMPPvCardTempCoreDataStorageObject.h"
|
||||
#import "XMPPvCardAvatarCoreDataStorageObject.h"
|
||||
|
||||
#import "XMPPJID.h"
|
||||
#import "XMPPStream.h"
|
||||
#import "NSNumber+XMPP.h"
|
||||
#import "NSData+XMPP.h"
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
|
||||
@implementation XMPPvCardCoreDataStorageObject
|
||||
|
||||
+ (XMPPvCardCoreDataStorageObject *)fetchvCardForJID:(XMPPJID *)jid
|
||||
inManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
NSString *entityName = NSStringFromClass([XMPPvCardCoreDataStorageObject class]);
|
||||
|
||||
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", [jid bare]];
|
||||
|
||||
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
|
||||
[fetchRequest setEntity:entity];
|
||||
[fetchRequest setPredicate:predicate];
|
||||
[fetchRequest setIncludesPendingChanges:YES];
|
||||
[fetchRequest setFetchLimit:1];
|
||||
|
||||
NSArray *results = [moc executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
|
||||
return (XMPPvCardCoreDataStorageObject *)[results lastObject];
|
||||
}
|
||||
|
||||
|
||||
+ (XMPPvCardCoreDataStorageObject *)insertEmptyvCardForJID:(XMPPJID *)jid
|
||||
inManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
NSString *entityName = NSStringFromClass([XMPPvCardCoreDataStorageObject class]);
|
||||
|
||||
XMPPvCardCoreDataStorageObject *vCard = [NSEntityDescription insertNewObjectForEntityForName:entityName
|
||||
inManagedObjectContext:moc];
|
||||
|
||||
vCard.jidStr = [jid bare];
|
||||
return vCard;
|
||||
}
|
||||
|
||||
+ (XMPPvCardCoreDataStorageObject *)fetchOrInsertvCardForJID:(XMPPJID *)jid
|
||||
inManagedObjectContext:(NSManagedObjectContext *)moc
|
||||
{
|
||||
XMPPvCardCoreDataStorageObject *vCard = [self fetchvCardForJID:jid inManagedObjectContext:moc];
|
||||
if (vCard == nil)
|
||||
{
|
||||
vCard = [self insertEmptyvCardForJID:jid inManagedObjectContext:moc];
|
||||
}
|
||||
|
||||
return vCard;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark NSManagedObject methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
- (void)awakeFromInsert
|
||||
{
|
||||
[super awakeFromInsert];
|
||||
[self setPrimitiveValue:[NSDate date] forKey:@"lastUpdated"];
|
||||
}
|
||||
|
||||
|
||||
- (void)willSave
|
||||
{
|
||||
/*
|
||||
if (![self isDeleted] && [self isUpdated]) {
|
||||
[self setPrimitiveValue:[NSDate date] forKey:@"lastUpdated"];
|
||||
}
|
||||
*/
|
||||
|
||||
[super willSave];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark Getter/setter methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@dynamic jidStr;
|
||||
@dynamic photoHash;
|
||||
@dynamic lastUpdated;
|
||||
@dynamic waitingForFetch;
|
||||
@dynamic vCardTempRel;
|
||||
@dynamic vCardAvatarRel;
|
||||
|
||||
|
||||
- (NSData *)photoData {
|
||||
return self.vCardAvatarRel.photoData;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPhotoData:(NSData *)photoData
|
||||
{
|
||||
if ([photoData length] == 0)
|
||||
{
|
||||
if (self.vCardAvatarRel != nil)
|
||||
{
|
||||
[[self managedObjectContext] deleteObject:self.vCardAvatarRel];
|
||||
[self setPrimitiveValue:nil forKey:@"photoHash"];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.vCardAvatarRel == nil)
|
||||
{
|
||||
NSString *entityName = NSStringFromClass([XMPPvCardAvatarCoreDataStorageObject class]);
|
||||
|
||||
self.vCardAvatarRel = [NSEntityDescription insertNewObjectForEntityForName:entityName
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
}
|
||||
|
||||
[self willChangeValueForKey:@"photoData"];
|
||||
self.vCardAvatarRel.photoData = photoData;
|
||||
[self didChangeValueForKey:@"photoData"];
|
||||
|
||||
[self setPrimitiveValue:[[photoData xmpp_sha1Digest] xmpp_hexStringValue] forKey:@"photoHash"];
|
||||
}
|
||||
|
||||
|
||||
- (XMPPvCardTemp *)vCardTemp {
|
||||
return self.vCardTempRel.vCardTemp;
|
||||
}
|
||||
|
||||
|
||||
- (void)setVCardTemp:(XMPPvCardTemp *)vCardTemp
|
||||
{
|
||||
if (vCardTemp == nil && self.vCardTempRel != nil)
|
||||
{
|
||||
[[self managedObjectContext] deleteObject:self.vCardTempRel];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.vCardTempRel == nil)
|
||||
{
|
||||
NSString *entityName = NSStringFromClass([XMPPvCardTempCoreDataStorageObject class]);
|
||||
|
||||
self.vCardTempRel = [NSEntityDescription insertNewObjectForEntityForName:entityName
|
||||
inManagedObjectContext:[self managedObjectContext]];
|
||||
}
|
||||
|
||||
[self willChangeValueForKey:@"vCardTemp"];
|
||||
self.vCardTempRel.vCardTemp = vCardTemp;
|
||||
[self didChangeValueForKey:@"vCardTemp"];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma mark KVO methods
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingPhotoHash
|
||||
{
|
||||
return [NSSet setWithObjects:@"vCardAvatarRel", @"photoData", nil];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingVCardTemp
|
||||
{
|
||||
return [NSSet setWithObject:@"vCardTempRel"];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// XMPPvCardTempCoreDataStorageObject.h
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Oringally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
// This class is so that we don't load the vCardTemp each time we need to touch the XMPPvCardCoreDataStorageObject.
|
||||
// The vCardTemp abstraction also makes it easier to eventually add support for vCard4 over XMPP (XEP-0292).
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
#import "XMPPvcardTemp.h"
|
||||
|
||||
@class XMPPvCardCoreDataStorageObject;
|
||||
|
||||
|
||||
@interface XMPPvCardTempCoreDataStorageObject : NSManagedObject
|
||||
|
||||
@property (nonatomic, strong) XMPPvCardTemp * vCardTemp;
|
||||
@property (nonatomic, strong) XMPPvCardCoreDataStorageObject * vCard;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// XMPPvCardTempCoreDataStorageObject.m
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Originally created by Eric Chamberlain on 3/18/11.
|
||||
//
|
||||
|
||||
#import "XMPPvCardTempCoreDataStorageObject.h"
|
||||
#import "XMPPvCardCoreDataStorageObject.h"
|
||||
|
||||
|
||||
@implementation XMPPvCardTempCoreDataStorageObject
|
||||
|
||||
@dynamic vCardTemp;
|
||||
@dynamic vCard;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// XMPPvCardTemp.h
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Created by Eric Chamberlain on 3/9/11.
|
||||
// Copyright 2011 RF.com. All rights reserved.
|
||||
// Copyright 2010 Martin Morrison. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
#import "XMPPIQ.h"
|
||||
#import "XMPPJID.h"
|
||||
#import "XMPPUser.h"
|
||||
#import "XMPPvCardTempAdr.h"
|
||||
#import "XMPPvCardTempBase.h"
|
||||
#import "XMPPvCardTempEmail.h"
|
||||
#import "XMPPvCardTempLabel.h"
|
||||
#import "XMPPvCardTempTel.h"
|
||||
|
||||
|
||||
typedef enum _XMPPvCardTempClass {
|
||||
XMPPvCardTempClassNone,
|
||||
XMPPvCardTempClassPublic,
|
||||
XMPPvCardTempClassPrivate,
|
||||
XMPPvCardTempClassConfidential,
|
||||
} XMPPvCardTempClass;
|
||||
|
||||
|
||||
extern NSString *const kXMPPNSvCardTemp;
|
||||
extern NSString *const kXMPPvCardTempElement;
|
||||
|
||||
|
||||
/*
|
||||
* Note: according to the DTD, every fields bar N and FN can appear multiple times.
|
||||
* The provided accessors only support this for the field types where multiple
|
||||
* entries make sense - for the others, if required, the NSXMLElement accessors
|
||||
* must be used.
|
||||
*/
|
||||
@interface XMPPvCardTemp : XMPPvCardTempBase
|
||||
|
||||
|
||||
@property (nonatomic, strong) NSDate *bday;
|
||||
@property (nonatomic, strong) NSData *photo;
|
||||
@property (nonatomic, strong) NSString *nickname;
|
||||
@property (nonatomic, strong) NSString *formattedName;
|
||||
@property (nonatomic, strong) NSString *familyName;
|
||||
@property (nonatomic, strong) NSString *givenName;
|
||||
@property (nonatomic, strong) NSString *middleName;
|
||||
@property (nonatomic, strong) NSString *prefix;
|
||||
@property (nonatomic, strong) NSString *suffix;
|
||||
|
||||
@property (nonatomic, strong) NSArray *addresses;
|
||||
@property (nonatomic, strong) NSArray *labels;
|
||||
@property (nonatomic, strong) NSArray *telecomsAddresses;
|
||||
@property (nonatomic, strong) NSArray *emailAddresses;
|
||||
|
||||
@property (nonatomic, strong) XMPPJID *jid;
|
||||
@property (nonatomic, strong) NSString *mailer;
|
||||
|
||||
@property (nonatomic, strong) NSTimeZone *timeZone;
|
||||
@property (nonatomic, strong) CLLocation *location;
|
||||
|
||||
@property (nonatomic, strong) NSString *title;
|
||||
@property (nonatomic, strong) NSString *role;
|
||||
@property (nonatomic, strong) NSData *logo;
|
||||
@property (nonatomic, strong) XMPPvCardTemp *agent;
|
||||
@property (nonatomic, strong) NSString *orgName;
|
||||
|
||||
/*
|
||||
* ORGUNITs can only be set if there is already an ORGNAME. Otherwise, changes are ignored.
|
||||
*/
|
||||
@property (nonatomic, strong) NSArray *orgUnits;
|
||||
|
||||
@property (nonatomic, strong) NSArray *categories;
|
||||
@property (nonatomic, strong) NSString *note;
|
||||
@property (nonatomic, strong) NSString *prodid;
|
||||
@property (nonatomic, strong) NSDate *revision;
|
||||
@property (nonatomic, strong) NSString *sortString;
|
||||
@property (nonatomic, strong) NSString *phoneticSound;
|
||||
@property (nonatomic, strong) NSData *sound;
|
||||
@property (nonatomic, strong) NSString *uid;
|
||||
@property (nonatomic, strong) NSString *url;
|
||||
@property (nonatomic, strong) NSString *version;
|
||||
@property (nonatomic, strong) NSString *desc;
|
||||
|
||||
@property (nonatomic, assign) XMPPvCardTempClass privacyClass;
|
||||
@property (nonatomic, strong) NSData *key;
|
||||
@property (nonatomic, strong) NSString *keyType;
|
||||
|
||||
+ (XMPPvCardTemp *)vCardTempFromElement:(NSXMLElement *)element;
|
||||
+ (XMPPvCardTemp *)vCardTemp;
|
||||
+ (XMPPvCardTemp *)vCardTempSubElementFromIQ:(XMPPIQ *)iq;
|
||||
+ (XMPPvCardTemp *)vCardTempCopyFromIQ:(XMPPIQ *)iq;
|
||||
+ (XMPPIQ *)iqvCardRequestForJID:(XMPPJID *)jid;
|
||||
|
||||
|
||||
- (void)addAddress:(XMPPvCardTempAdr *)adr;
|
||||
- (void)removeAddress:(XMPPvCardTempAdr *)adr;
|
||||
- (void)clearAddresses;
|
||||
|
||||
|
||||
- (void)addLabel:(XMPPvCardTempLabel *)label;
|
||||
- (void)removeLabel:(XMPPvCardTempLabel *)label;
|
||||
- (void)clearLabels;
|
||||
|
||||
|
||||
- (void)addTelecomsAddress:(XMPPvCardTempTel *)tel;
|
||||
- (void)removeTelecomsAddress:(XMPPvCardTempTel *)tel;
|
||||
- (void)clearTelecomsAddresses;
|
||||
|
||||
|
||||
- (void)addEmailAddress:(XMPPvCardTempEmail *)email;
|
||||
- (void)removeEmailAddress:(XMPPvCardTempEmail *)email;
|
||||
- (void)clearEmailAddresses;
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,932 @@
|
||||
//
|
||||
// XMPPvCardTemp.m
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Created by Eric Chamberlain on 3/9/11.
|
||||
// Copyright 2011 RF.com. All rights reserved.
|
||||
// Copyright 2010 Martin Morrison. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "XMPPvCardTemp.h"
|
||||
#import "XMPPLogging.h"
|
||||
#import "XMPPDateTimeProfiles.h"
|
||||
#import "NSData+XMPP.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR;
|
||||
#endif
|
||||
|
||||
NSString *const kXMPPNSvCardTemp = @"vcard-temp";
|
||||
NSString *const kXMPPvCardTempElement = @"vCard";
|
||||
|
||||
|
||||
@implementation XMPPvCardTemp
|
||||
|
||||
#if DEBUG
|
||||
|
||||
+ (void)initialize {
|
||||
// We use the object_setClass method below to dynamically change the class from a standard NSXMLElement.
|
||||
// The size of the two classes is expected to be the same.
|
||||
//
|
||||
// If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug.
|
||||
// This check is here to aid future developers who may make this mistake.
|
||||
//
|
||||
// For Fearless And Experienced Objective-C Developers:
|
||||
// It may be possible to support adding instance variables to this class if you seriously need it.
|
||||
// To do so, try realloc'ing self after altering the class, and then initialize your variables.
|
||||
|
||||
size_t superSize = class_getInstanceSize([NSXMLElement class]);
|
||||
size_t ourSize = class_getInstanceSize([XMPPvCardTemp class]);
|
||||
|
||||
if (superSize != ourSize)
|
||||
{
|
||||
XMPPLogError(@"Adding instance variables to XMPPvCardTemp is not currently supported!");
|
||||
|
||||
[DDLog flushLog];
|
||||
exit(15);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+ (XMPPvCardTemp *)vCardTempFromElement:(NSXMLElement *)elem {
|
||||
object_setClass(elem, [XMPPvCardTemp class]);
|
||||
|
||||
return (XMPPvCardTemp *)elem;
|
||||
}
|
||||
|
||||
+ (XMPPvCardTemp *)vCardTemp{
|
||||
NSXMLElement *vCardTempElement = [NSXMLElement elementWithName:kXMPPvCardTempElement xmlns:kXMPPNSvCardTemp];
|
||||
return [XMPPvCardTemp vCardTempFromElement:vCardTempElement];
|
||||
}
|
||||
|
||||
+ (XMPPvCardTemp *)vCardTempSubElementFromIQ:(XMPPIQ *)iq
|
||||
{
|
||||
if ([iq isResultIQ])
|
||||
{
|
||||
NSXMLElement *query = [iq elementForName:kXMPPvCardTempElement xmlns:kXMPPNSvCardTemp];
|
||||
if (query)
|
||||
{
|
||||
return [self vCardTempFromElement:query];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (XMPPvCardTemp *)vCardTempCopyFromIQ:(XMPPIQ *)iq
|
||||
{
|
||||
return [[self vCardTempSubElementFromIQ:iq] copy];
|
||||
}
|
||||
|
||||
|
||||
+ (XMPPIQ *)iqvCardRequestForJID:(XMPPJID *)jid {
|
||||
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:[jid bareJID] elementID:[XMPPStream generateUUID]];
|
||||
NSXMLElement *vCardElem = [NSXMLElement elementWithName:kXMPPvCardTempElement xmlns:kXMPPNSvCardTemp];
|
||||
|
||||
[iq addChild:vCardElem];
|
||||
return iq;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Identification Types
|
||||
|
||||
|
||||
- (NSDate *)bday {
|
||||
NSDate *bday = nil;
|
||||
NSXMLElement *elem = [self elementForName:@"BDAY"];
|
||||
|
||||
if (elem != nil) {
|
||||
bday = [NSDate dateWithXmppDateString:[elem stringValue]];
|
||||
}
|
||||
|
||||
return bday;
|
||||
}
|
||||
|
||||
|
||||
- (void)setBday:(NSDate *)bday {
|
||||
NSXMLElement *elem = [self elementForName:@"BDAY"];
|
||||
|
||||
if (elem == nil) {
|
||||
elem = [NSXMLElement elementWithName:@"BDAY"];
|
||||
[self addChild:elem];
|
||||
}
|
||||
|
||||
[elem setStringValue:[bday xmppDateString]];
|
||||
}
|
||||
|
||||
|
||||
- (NSData *)photo {
|
||||
NSData *decodedData = nil;
|
||||
NSXMLElement *photo = [self elementForName:@"PHOTO"];
|
||||
|
||||
if (photo != nil) {
|
||||
// There is a PHOTO element. It should have a TYPE and a BINVAL
|
||||
//NSXMLElement *fileType = [photo elementForName:@"TYPE"];
|
||||
NSXMLElement *binval = [photo elementForName:@"BINVAL"];
|
||||
|
||||
if (binval) {
|
||||
NSData *base64Data = [[binval stringValue] dataUsingEncoding:NSASCIIStringEncoding];
|
||||
decodedData = [base64Data xmpp_base64Decoded];
|
||||
}
|
||||
}
|
||||
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPhoto:(NSData *)data {
|
||||
|
||||
NSXMLElement *photo = [self elementForName:@"PHOTO"];
|
||||
|
||||
if(photo)
|
||||
{
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:photo]];
|
||||
}
|
||||
|
||||
if([data length])
|
||||
{
|
||||
NSXMLElement *photo = [NSXMLElement elementWithName:@"PHOTO"];
|
||||
[self addChild:photo];
|
||||
|
||||
NSString *imageType = [data xmpp_imageType];
|
||||
|
||||
if([imageType length])
|
||||
{
|
||||
NSXMLElement *type = [NSXMLElement elementWithName:@"TYPE"];
|
||||
[photo addChild:type];
|
||||
[type setStringValue:imageType];
|
||||
}
|
||||
|
||||
NSXMLElement *binval = [NSXMLElement elementWithName:@"BINVAL"];
|
||||
[photo addChild:binval];
|
||||
[binval setStringValue:[data xmpp_base64Encoded]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)nickname {
|
||||
return [[self elementForName:@"NICKNAME"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setNickname:(NSString *)nick {
|
||||
XMPP_VCARD_SET_STRING_CHILD(nick, @"NICKNAME");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)formattedName {
|
||||
return [[self elementForName:@"FN"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setFormattedName:(NSString *)fn {
|
||||
XMPP_VCARD_SET_STRING_CHILD(fn, @"FN");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)familyName {
|
||||
NSString *result = nil;
|
||||
NSXMLElement *name = [self elementForName:@"N"];
|
||||
|
||||
if (name != nil) {
|
||||
NSXMLElement *part = [name elementForName:@"FAMILY"];
|
||||
|
||||
if (part != nil) {
|
||||
result = [part stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setFamilyName:(NSString *)family {
|
||||
XMPP_VCARD_SET_N_CHILD(family, @"FAMILY");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)givenName {
|
||||
NSString *result = nil;
|
||||
NSXMLElement *name = [self elementForName:@"N"];
|
||||
|
||||
if (name != nil) {
|
||||
NSXMLElement *part = [name elementForName:@"GIVEN"];
|
||||
|
||||
if (part != nil) {
|
||||
result = [part stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setGivenName:(NSString *)given {
|
||||
XMPP_VCARD_SET_N_CHILD(given, @"GIVEN");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)middleName {
|
||||
NSString *result = nil;
|
||||
NSXMLElement *name = [self elementForName:@"N"];
|
||||
|
||||
if (name != nil) {
|
||||
NSXMLElement *part = [name elementForName:@"MIDDLE"];
|
||||
|
||||
if (part != nil) {
|
||||
result = [part stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setMiddleName:(NSString *)middle {
|
||||
XMPP_VCARD_SET_N_CHILD(middle, @"MIDDLE");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)prefix {
|
||||
NSString *result = nil;
|
||||
NSXMLElement *name = [self elementForName:@"N"];
|
||||
|
||||
if (name != nil) {
|
||||
NSXMLElement *part = [name elementForName:@"PREFIX"];
|
||||
|
||||
if (part != nil) {
|
||||
result = [part stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPrefix:(NSString *)prefix {
|
||||
XMPP_VCARD_SET_N_CHILD(prefix, @"PREFIX");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)suffix {
|
||||
NSString *result = nil;
|
||||
NSXMLElement *name = [self elementForName:@"N"];
|
||||
|
||||
if (name != nil) {
|
||||
NSXMLElement *part = [name elementForName:@"SUFFIX"];
|
||||
|
||||
if (part != nil) {
|
||||
result = [part stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setSuffix:(NSString *)suffix {
|
||||
XMPP_VCARD_SET_N_CHILD(suffix, @"SUFFIX");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Delivery Addressing Types
|
||||
|
||||
|
||||
- (NSArray *)addresses { return nil; }
|
||||
- (void)addAddress:(XMPPvCardTempAdr *)adr { }
|
||||
- (void)removeAddress:(XMPPvCardTempAdr *)adr { }
|
||||
- (void)setAddresses:(NSArray *)adrs { }
|
||||
- (void)clearAddresses { }
|
||||
|
||||
|
||||
- (NSArray *)labels { return nil; }
|
||||
- (void)addLabel:(XMPPvCardTempLabel *)label { }
|
||||
- (void)removeLabel:(XMPPvCardTempLabel *)label { }
|
||||
- (void)setLabels:(NSArray *)labels { }
|
||||
- (void)clearLabels { }
|
||||
|
||||
|
||||
- (NSArray *)telecomsAddresses { return nil; }
|
||||
- (void)addTelecomsAddress:(XMPPvCardTempTel *)tel { }
|
||||
- (void)removeTelecomsAddress:(XMPPvCardTempTel *)tel { }
|
||||
- (void)setTelecomsAddresses:(NSArray *)tels { }
|
||||
- (void)clearTelecomsAddresses { }
|
||||
|
||||
|
||||
- (NSArray *)emailAddresses { return nil; }
|
||||
- (void)addEmailAddress:(XMPPvCardTempEmail *)email { }
|
||||
- (void)removeEmailAddress:(XMPPvCardTempEmail *)email { }
|
||||
- (void)setEmailAddresses:(NSArray *)emails { }
|
||||
- (void)clearEmailAddresses { }
|
||||
|
||||
|
||||
- (XMPPJID *)jid {
|
||||
XMPPJID *jid = nil;
|
||||
NSXMLElement *elem = [self elementForName:@"JABBERID"];
|
||||
|
||||
if (elem != nil) {
|
||||
jid = [XMPPJID jidWithString:[elem stringValue]];
|
||||
}
|
||||
|
||||
return jid;
|
||||
}
|
||||
|
||||
|
||||
- (void)setJid:(XMPPJID *)jid {
|
||||
NSXMLElement *elem = [self elementForName:@"JABBERID"];
|
||||
|
||||
if (elem == nil && jid != nil) {
|
||||
elem = [NSXMLElement elementWithName:@"JABBERID"];
|
||||
[self addChild:elem];
|
||||
}
|
||||
|
||||
if (jid != nil) {
|
||||
[elem setStringValue:[jid full]];
|
||||
} else if (elem != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:elem]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)mailer {
|
||||
return [[self elementForName:@"MAILER"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setMailer:(NSString *)mailer {
|
||||
XMPP_VCARD_SET_STRING_CHILD(mailer, @"MAILER");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Geographical Types
|
||||
|
||||
|
||||
- (NSTimeZone *)timeZone {
|
||||
// Turns out this is hard. Being lazy for now (not like anyone actually uses this, right?)
|
||||
NSXMLElement *tz = [self elementForName:@"TZ"];
|
||||
if (tz != nil) {
|
||||
// This is unlikely to work. :-(
|
||||
return [NSTimeZone timeZoneWithName:[tz stringValue]];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)setTimeZone:(NSTimeZone *)tz {
|
||||
NSXMLElement *elem = [self elementForName:@"TZ"];
|
||||
|
||||
if (elem == nil && tz != nil) {
|
||||
elem = [NSXMLElement elementWithName:@"TZ"];
|
||||
[self addChild:elem];
|
||||
}
|
||||
|
||||
if (tz != nil) {
|
||||
NSInteger offset = [tz secondsFromGMT];
|
||||
[elem setStringValue:[NSString stringWithFormat:@"%02ld:%02ld",
|
||||
(long)(offset / 3600), (long)((offset % 3600) / 60)]];
|
||||
} else if (elem != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:elem]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (CLLocation *)location {
|
||||
CLLocation *loc = nil;
|
||||
NSXMLElement *geo = [self elementForName:@"GEO"];
|
||||
|
||||
if (geo != nil) {
|
||||
NSXMLElement *lat = [geo elementForName:@"LAT"];
|
||||
NSXMLElement *lon = [geo elementForName:@"LON"];
|
||||
|
||||
loc = [[CLLocation alloc] initWithLatitude:[[lat stringValue] doubleValue] longitude:[[lon stringValue] doubleValue]];
|
||||
}
|
||||
|
||||
return loc;
|
||||
}
|
||||
|
||||
|
||||
- (void)setLocation:(CLLocation *)geo {
|
||||
NSXMLElement *elem = [self elementForName:@"GEO"];
|
||||
NSXMLElement *lat;
|
||||
NSXMLElement *lon;
|
||||
|
||||
if (geo != nil) {
|
||||
CLLocationCoordinate2D coord = [geo coordinate];
|
||||
if (elem == nil) {
|
||||
elem = [NSXMLElement elementWithName:@"GEO"];
|
||||
[self addChild:elem];
|
||||
|
||||
lat = [NSXMLElement elementWithName:@"LAT"];
|
||||
[elem addChild:lat];
|
||||
lon = [NSXMLElement elementWithName:@"LON"];
|
||||
[elem addChild:lon];
|
||||
} else {
|
||||
lat = [elem elementForName:@"LAT"];
|
||||
lon = [elem elementForName:@"LON"];
|
||||
}
|
||||
|
||||
[lat setStringValue:[NSString stringWithFormat:@"%.6f", coord.latitude]];
|
||||
[lon setStringValue:[NSString stringWithFormat:@"%.6f", coord.longitude]];
|
||||
} else if (elem != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:elem]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Organizational Types
|
||||
|
||||
|
||||
- (NSString *)title {
|
||||
return [[self elementForName:@"TITLE"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setTitle:(NSString *)title {
|
||||
XMPP_VCARD_SET_STRING_CHILD(title, @"TITLE");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)role {
|
||||
return [[self elementForName:@"ROLE"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setRole:(NSString *)role {
|
||||
XMPP_VCARD_SET_STRING_CHILD(role, @"ROLE");
|
||||
}
|
||||
|
||||
|
||||
- (NSData *)logo {
|
||||
NSData *decodedData = nil;
|
||||
NSXMLElement *logo = [self elementForName:@"LOGO"];
|
||||
|
||||
if (logo != nil) {
|
||||
// There is a LOGO element. It should have a TYPE and a BINVAL
|
||||
//NSXMLElement *fileType = [photo elementForName:@"TYPE"];
|
||||
NSXMLElement *binval = [logo elementForName:@"BINVAL"];
|
||||
|
||||
if (binval) {
|
||||
NSData *base64Data = [[binval stringValue] dataUsingEncoding:NSASCIIStringEncoding];
|
||||
decodedData = [base64Data xmpp_base64Decoded];
|
||||
}
|
||||
}
|
||||
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
|
||||
- (void)setLogo:(NSData *)data {
|
||||
NSXMLElement *logo = [self elementForName:@"LOGO"];
|
||||
|
||||
if (logo == nil) {
|
||||
logo = [NSXMLElement elementWithName:@"LOGO"];
|
||||
[self addChild:logo];
|
||||
}
|
||||
|
||||
NSXMLElement *binval = [logo elementForName:@"BINVAL"];
|
||||
|
||||
if (binval == nil) {
|
||||
binval = [NSXMLElement elementWithName:@"BINVAL"];
|
||||
[logo addChild:binval];
|
||||
}
|
||||
|
||||
[binval setStringValue:[data xmpp_base64Encoded]];
|
||||
}
|
||||
|
||||
|
||||
- (XMPPvCardTemp *)agent {
|
||||
XMPPvCardTemp *agent = nil;
|
||||
NSXMLElement *elem = [self elementForName:@"AGENT"];
|
||||
|
||||
if (elem != nil) {
|
||||
agent = [XMPPvCardTemp vCardTempFromElement:elem];
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
|
||||
- (void)setAgent:(XMPPvCardTemp *)agent {
|
||||
NSXMLElement *elem = [self elementForName:@"AGENT"];
|
||||
|
||||
if (elem != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:elem]];
|
||||
}
|
||||
|
||||
if (agent != nil) {
|
||||
[self addChild:agent];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)orgName {
|
||||
NSString *result = nil;
|
||||
NSXMLElement *org = [self elementForName:@"ORG"];
|
||||
|
||||
if (org != nil) {
|
||||
NSXMLElement *orgname = [org elementForName:@"ORGNAME"];
|
||||
|
||||
if (orgname != nil) {
|
||||
result = [orgname stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setOrgName:(NSString *)orgname {
|
||||
NSXMLElement *org = [self elementForName:@"ORG"];
|
||||
NSXMLElement *elem = nil;
|
||||
|
||||
if (orgname != nil) {
|
||||
if (org == nil) {
|
||||
org = [NSXMLElement elementWithName:@"ORG"];
|
||||
[self addChild:org];
|
||||
} else {
|
||||
elem = [org elementForName:@"ORGNAME"];
|
||||
}
|
||||
|
||||
if (elem == nil) {
|
||||
elem = [NSXMLElement elementWithName:@"ORGNAME"];
|
||||
[org addChild:elem];
|
||||
}
|
||||
|
||||
[elem setStringValue:orgname];
|
||||
} else if (org != nil) {
|
||||
// This implicitly removes all orgunits too, as per the spec
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:org]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSArray *)orgUnits {
|
||||
NSArray *result = nil;
|
||||
NSXMLElement *org = [self elementForName:@"ORG"];
|
||||
|
||||
if (org != nil) {
|
||||
NSArray *elems = [org elementsForName:@"ORGUNIT"];
|
||||
NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:[elems count]];
|
||||
|
||||
for (NSXMLElement *elem in elems) {
|
||||
[arr addObject:[elem stringValue]];
|
||||
}
|
||||
|
||||
result = [NSArray arrayWithArray:arr];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setOrgUnits:(NSArray *)orgunits {
|
||||
NSXMLElement *org = [self elementForName:@"ORG"];
|
||||
|
||||
// If there is no org, then there is nothing to do (need ORGNAME first)
|
||||
if (org != nil) {
|
||||
NSArray *elems = [org elementsForName:@"ORGUNIT"];
|
||||
for (NSXMLElement *elem in elems) {
|
||||
[org removeChildAtIndex:[[org children] indexOfObject:elem]];
|
||||
}
|
||||
|
||||
for (NSString *unit in orgunits) {
|
||||
NSXMLElement *elem = [NSXMLElement elementWithName:@"ORGUNIT"];
|
||||
[elem setStringValue:unit];
|
||||
|
||||
[org addChild:elem];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Explanatory Types
|
||||
|
||||
|
||||
- (NSArray *)categories {
|
||||
NSArray *result = nil;
|
||||
NSXMLElement *categories = [self elementForName:@"CATEGORIES"];
|
||||
|
||||
if (categories != nil) {
|
||||
NSArray *elems = [categories elementsForName:@"KEYWORD"];
|
||||
NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:[elems count]];
|
||||
|
||||
for (NSXMLElement *elem in elems) {
|
||||
[arr addObject:[elem stringValue]];
|
||||
}
|
||||
|
||||
result = [NSArray arrayWithArray:arr];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
- (void)setCategories:(NSArray *)categories {
|
||||
NSXMLElement *cat = [self elementForName:@"CATEGORIES"];
|
||||
|
||||
if (categories != nil) {
|
||||
if (cat == nil) {
|
||||
cat = [NSXMLElement elementWithName:@"CATEGORIES"];
|
||||
[self addChild:cat];
|
||||
}
|
||||
|
||||
NSArray *elems = [cat elementsForName:@"KEYWORD"];
|
||||
for (NSXMLElement *elem in elems) {
|
||||
[cat removeChildAtIndex:[[cat children] indexOfObject:elem]];
|
||||
}
|
||||
|
||||
for (NSString *kw in categories) {
|
||||
NSXMLElement *elem = [NSXMLElement elementWithName:@"KEYWORD"];
|
||||
[elem setStringValue:kw];
|
||||
|
||||
[cat addChild:elem];
|
||||
}
|
||||
} else if (cat != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:cat]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)note {
|
||||
return [[self elementForName:@"NOTE"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setNote:(NSString *)note {
|
||||
XMPP_VCARD_SET_STRING_CHILD(note, @"NOTE");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)prodid {
|
||||
return [[self elementForName:@"PRODID"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setProdid:(NSString *)prodid {
|
||||
XMPP_VCARD_SET_STRING_CHILD(prodid, @"PRODID");
|
||||
}
|
||||
|
||||
|
||||
- (NSDate *)revision {
|
||||
NSDate *rev = nil;
|
||||
NSXMLElement *elem = [self elementForName:@"REV"];
|
||||
|
||||
if (elem != nil) {
|
||||
rev = [NSDate dateWithXmppDateTimeString:[elem stringValue]];
|
||||
}
|
||||
|
||||
return rev;
|
||||
}
|
||||
|
||||
|
||||
- (void)setRevision:(NSDate *)rev {
|
||||
NSXMLElement *elem = [self elementForName:@"REV"];
|
||||
|
||||
if (elem == nil) {
|
||||
elem = [NSXMLElement elementWithName:@"REV"];
|
||||
[self addChild:elem];
|
||||
}
|
||||
|
||||
[elem setStringValue:[rev xmppDateTimeString]];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)sortString {
|
||||
return [[self elementForName:@"SORT-STRING"] stringValue];
|
||||
}
|
||||
- (void)setSortString:(NSString *)sortString {
|
||||
XMPP_VCARD_SET_STRING_CHILD(sortString, @"SORT-STRING");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)phoneticSound {
|
||||
NSString *phon = nil;
|
||||
NSXMLElement *sound = [self elementForName:@"SOUND"];
|
||||
|
||||
if (sound != nil) {
|
||||
NSXMLElement *elem = [sound elementForName:@"PHONETIC"];
|
||||
|
||||
if (elem != nil) {
|
||||
phon = [elem stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return phon;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPhoneticSound:(NSString *)phonetic {
|
||||
NSXMLElement *sound = [self elementForName:@"SOUND"];
|
||||
NSXMLElement *elem = nil;
|
||||
|
||||
if (sound == nil && phonetic != nil) {
|
||||
sound = [NSXMLElement elementWithName:@"SOUND"];
|
||||
[self addChild:sound];
|
||||
}
|
||||
|
||||
if (sound != nil) {
|
||||
elem = [sound elementForName:@"PHONETIC"];
|
||||
|
||||
if (elem != nil && phonetic != nil) {
|
||||
elem = [NSXMLElement elementWithName:@"PHONETIC"];
|
||||
[sound addChild:elem];
|
||||
}
|
||||
}
|
||||
|
||||
if (phonetic != nil) {
|
||||
[elem setStringValue:phonetic];
|
||||
} else if (sound != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:phonetic]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSData *)sound {
|
||||
NSData *decodedData = nil;
|
||||
NSXMLElement *sound = [self elementForName:@"SOUND"];
|
||||
|
||||
if (sound != nil) {
|
||||
NSXMLElement *binval = [sound elementForName:@"BINVAL"];
|
||||
|
||||
if (binval) {
|
||||
NSData *base64Data = [[binval stringValue] dataUsingEncoding:NSASCIIStringEncoding];
|
||||
decodedData = [base64Data xmpp_base64Decoded];
|
||||
}
|
||||
}
|
||||
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
|
||||
- (void)setSound:(NSData *)data {
|
||||
NSXMLElement *sound = [self elementForName:@"SOUND"];
|
||||
|
||||
if (sound == nil) {
|
||||
sound = [NSXMLElement elementWithName:@"SOUND"];
|
||||
[self addChild:sound];
|
||||
}
|
||||
|
||||
NSXMLElement *binval = [sound elementForName:@"BINVAL"];
|
||||
|
||||
if (binval == nil) {
|
||||
binval = [NSXMLElement elementWithName:@"BINVAL"];
|
||||
[sound addChild:binval];
|
||||
}
|
||||
|
||||
[binval setStringValue:[data xmpp_base64Encoded]];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)uid {
|
||||
return [[self elementForName:@"UID"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setUid:(NSString *)uid {
|
||||
XMPP_VCARD_SET_STRING_CHILD(uid, @"UID");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)url {
|
||||
return [[self elementForName:@"URL"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setUrl:(NSString *)url {
|
||||
XMPP_VCARD_SET_STRING_CHILD(url, @"URL");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)version {
|
||||
return [self attributeStringValueForName:@"version"];
|
||||
}
|
||||
|
||||
|
||||
- (void)setVersion:(NSString *)version {
|
||||
[self addAttributeWithName:@"version" stringValue:version];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)desc {
|
||||
return [[self elementForName:@"DESC"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setDesc:(NSString *)desc {
|
||||
XMPP_VCARD_SET_STRING_CHILD(desc, @"DESC");
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Security Types
|
||||
|
||||
|
||||
- (XMPPvCardTempClass)privacyClass {
|
||||
XMPPvCardTempClass priv = XMPPvCardTempClassNone;
|
||||
NSXMLElement *elem = [self elementForName:@"CLASS"];
|
||||
|
||||
if (elem != nil) {
|
||||
if ([elem elementForName:@"PUBLIC"] != nil) {
|
||||
priv = XMPPvCardTempClassPublic;
|
||||
} else if ([elem elementForName:@"PRIVATE"] != nil) {
|
||||
priv = XMPPvCardTempClassPrivate;
|
||||
} else if ([elem elementForName:@"CONFIDENTIAL"] != nil) {
|
||||
priv = XMPPvCardTempClassConfidential;
|
||||
}
|
||||
}
|
||||
|
||||
return priv;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPrivacyClass:(XMPPvCardTempClass)privacyClass {
|
||||
NSXMLElement *elem = [self elementForName:@"CLASS"];
|
||||
|
||||
if (elem == nil && privacyClass != XMPPvCardTempClassNone) {
|
||||
elem = [NSXMLElement elementWithName:@"CLASS"];
|
||||
}
|
||||
|
||||
if (elem != nil) {
|
||||
for (NSString *cls in @[@"PUBLIC", @"PRIVATE", @"CONFIDENTIAL"]) {
|
||||
NSXMLElement *priv = [elem elementForName:cls];
|
||||
if (priv != nil) {
|
||||
[elem removeChildAtIndex:[[elem children] indexOfObject:priv]];
|
||||
}
|
||||
}
|
||||
|
||||
switch (privacyClass) {
|
||||
case XMPPvCardTempClassPublic:
|
||||
[elem addChild:[NSXMLElement elementWithName:@"PUBLIC"]];
|
||||
break;
|
||||
case XMPPvCardTempClassPrivate:
|
||||
[elem addChild:[NSXMLElement elementWithName:@"PRIVATE"]];
|
||||
break;
|
||||
case XMPPvCardTempClassConfidential:
|
||||
[elem addChild:[NSXMLElement elementWithName:@"CONFIDENTIAL"]];
|
||||
break;
|
||||
default:
|
||||
case XMPPvCardTempClassNone:
|
||||
// Remove the whole element
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:elem]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSData *)key { return nil; }
|
||||
- (void)setKey:(NSData *)key { }
|
||||
|
||||
|
||||
- (NSString *)keyType {
|
||||
NSString *typ = nil;
|
||||
NSXMLElement *key = [self elementForName:@"KEY"];
|
||||
|
||||
if (key != nil) {
|
||||
NSXMLElement *elem = [key elementForName:@"TYPE"];
|
||||
|
||||
if (elem != nil) {
|
||||
typ = [elem stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
return typ;
|
||||
}
|
||||
|
||||
|
||||
- (void)setKeyType:(NSString *)type {
|
||||
NSXMLElement *key = [self elementForName:@"KEY"];
|
||||
NSXMLElement *elem = nil;
|
||||
|
||||
if (key == nil && type != nil) {
|
||||
key = [NSXMLElement elementWithName:@"KEY"];
|
||||
[self addChild:key];
|
||||
}
|
||||
|
||||
if (key != nil) {
|
||||
elem = [key elementForName:@"TYPE"];
|
||||
|
||||
if (elem != nil && type != nil) {
|
||||
elem = [NSXMLElement elementWithName:@"TYPE"];
|
||||
[key addChild:elem];
|
||||
}
|
||||
}
|
||||
|
||||
if (type != nil) {
|
||||
[elem setStringValue:type];
|
||||
} else if (key != nil) {
|
||||
[self removeChildAtIndex:[[self children] indexOfObject:key]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// XMPPvCardTempAdr.h
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Created by Eric Chamberlain on 3/9/11.
|
||||
// Copyright 2011 RF.com. All rights reserved.
|
||||
// Copyright 2010 Martin Morrison. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "XMPPvCardTempAdrTypes.h"
|
||||
|
||||
|
||||
@interface XMPPvCardTempAdr : XMPPvCardTempAdrTypes
|
||||
|
||||
+ (XMPPvCardTempAdr *)vCardAdrFromElement:(NSXMLElement *)elem;
|
||||
|
||||
@property (nonatomic, weak) NSString *pobox;
|
||||
@property (nonatomic, weak) NSString *extendedAddress;
|
||||
@property (nonatomic, weak) NSString *street;
|
||||
@property (nonatomic, weak) NSString *locality;
|
||||
@property (nonatomic, weak) NSString *region;
|
||||
@property (nonatomic, weak) NSString *postalCode;
|
||||
@property (nonatomic, weak) NSString *country;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// XMPPvCardTempAdr.m
|
||||
// XEP-0054 vCard-temp
|
||||
//
|
||||
// Created by Eric Chamberlain on 3/9/11.
|
||||
// Copyright 2011 RF.com. All rights reserved.
|
||||
// Copyright 2010 Martin Morrison. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "XMPPvCardTempAdr.h"
|
||||
#import "XMPPLogging.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR;
|
||||
#endif
|
||||
|
||||
|
||||
@implementation XMPPvCardTempAdr
|
||||
|
||||
#if DEBUG
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
// We use the object_setClass method below to dynamically change the class from a standard NSXMLElement.
|
||||
// The size of the two classes is expected to be the same.
|
||||
//
|
||||
// If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug.
|
||||
// This check is here to aid future developers who may make this mistake.
|
||||
//
|
||||
// For Fearless And Experienced Objective-C Developers:
|
||||
// It may be possible to support adding instance variables to this class if you seriously need it.
|
||||
// To do so, try realloc'ing self after altering the class, and then initialize your variables.
|
||||
|
||||
size_t superSize = class_getInstanceSize([NSXMLElement class]);
|
||||
size_t ourSize = class_getInstanceSize([XMPPvCardTempAdr class]);
|
||||
|
||||
if (superSize != ourSize)
|
||||
{
|
||||
XMPPLogError(@"Adding instance variables to XMPPvCardTempAdr is not currently supported!");
|
||||
|
||||
[DDLog flushLog];
|
||||
exit(15);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+ (XMPPvCardTempAdr *)vCardAdrFromElement:(NSXMLElement *)elem {
|
||||
object_setClass(elem, [XMPPvCardTempAdr class]);
|
||||
|
||||
return (XMPPvCardTempAdr *)elem;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Getter/setter methods
|
||||
|
||||
|
||||
- (NSString *)pobox {
|
||||
return [[self elementForName:@"POBOX"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setPobox:(NSString *)pobox {
|
||||
XMPP_VCARD_SET_STRING_CHILD(pobox, @"POBOX");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)extendedAddress {
|
||||
return [[self elementForName:@"EXTADD"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setExtendedAddress:(NSString *)extadd {
|
||||
XMPP_VCARD_SET_STRING_CHILD(extadd, @"EXTADD");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)street {
|
||||
return [[self elementForName:@"STREET"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setStreet:(NSString *)street {
|
||||
XMPP_VCARD_SET_STRING_CHILD(street, @"STREET");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)locality {
|
||||
return [[self elementForName:@"LOCALITY"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setLocality:(NSString *)locality {
|
||||
XMPP_VCARD_SET_STRING_CHILD(locality, @"LOCALITY");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)region {
|
||||
return [[self elementForName:@"REGION"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setRegion:(NSString *)region {
|
||||
XMPP_VCARD_SET_STRING_CHILD(region, @"REGION");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)postalCode {
|
||||
return [[self elementForName:@"PCODE"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setPostalCode:(NSString *)pcode {
|
||||
XMPP_VCARD_SET_STRING_CHILD(pcode, @"PCODE");
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)country {
|
||||
return [[self elementForName:@"CTRY"] stringValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)setCountry:(NSString *)ctry {
|
||||
XMPP_VCARD_SET_STRING_CHILD(ctry, @"CTRY");
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
Loaded 100 of 1261 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user