This commit is contained in:
giuseppenuc committed 2016-02-24 16:56:39 +01:00
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:&currentNumberOfBytesSent numberOfBytesReceived:&currentNumberOfBytesReceived];
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
+117
View File
@@ -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
+418
View File
@@ -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
+183
View File
@@ -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
+651
View File
@@ -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
@@ -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
+15
View File
@@ -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
+406
View File
@@ -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
+16
View File
@@ -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
+21
View File
@@ -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
+77
View File
@@ -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
+219
View File
@@ -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
+50
View File
@@ -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
+341
View File
@@ -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
+94
View File
@@ -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
+97
View File
@@ -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
+119
View File
@@ -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
+222
View File
@@ -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
+231
View File
@@ -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>
@@ -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>
@@ -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
+131
View File
@@ -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
+423
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
#import "XMPPMessage.h"
@interface XMPPMessage(XEP0045)
- (BOOL)isGroupChatMessage;
- (BOOL)isGroupChatMessageWithBody;
- (BOOL)isGroupChatMessageWithSubject;
@end
+36
View File
@@ -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
+314
View File
@@ -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
+58
View File
@@ -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
+57
View File
@@ -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
+19
View File
@@ -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>
@@ -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
+120
View File
@@ -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
+932
View File
@@ -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
+28
View File
@@ -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
+136
View File
@@ -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