#import "XMPPRoster.h" #import "XMPP.h" #import "XMPPIDTracker.h" #import "XMPPLogging.h" #import "XMPPFramework.h" #import "DDList.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 enum XMPPRosterConfig { kAutoFetchRoster = 1 << 0, // If set, we automatically fetch roster after authentication kAutoAcceptKnownPresenceSubscriptionRequests = 1 << 1, // See big description in header file... :D kRosterlessOperation = 1 << 2, kAutoClearAllUsersAndResources = 1 << 3, }; enum XMPPRosterFlags { kRequestedRoster = 1 << 0, // If set, we have requested the roster kHasRoster = 1 << 1, // If set, we have received the roster kPopulatingRoster = 1 << 2, // If set, we are populating the roster }; @interface XMPPRoster (PrivateAPI) - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence; @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation XMPPRoster - (id)init { return [self initWithRosterStorage:nil dispatchQueue:NULL]; } - (id)initWithDispatchQueue:(dispatch_queue_t)queue { return [self initWithRosterStorage:nil dispatchQueue:queue]; } - (id)initWithRosterStorage:(id )storage { return [self initWithRosterStorage:storage dispatchQueue:NULL]; } - (id)initWithRosterStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue { NSParameterAssert(storage != nil); if ((self = [super initWithDispatchQueue:queue])) { if ([storage configureWithParent:self queue:moduleQueue]) { xmppRosterStorage = storage; } else { XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); } config = kAutoFetchRoster | kAutoAcceptKnownPresenceSubscriptionRequests | kAutoClearAllUsersAndResources; flags = 0; earlyPresenceElements = [[NSMutableArray alloc] initWithCapacity:2]; mucModules = [[DDList alloc] init]; } return self; } - (BOOL)activate:(XMPPStream *)aXmppStream { XMPPLogTrace(); if ([super activate:aXmppStream]) { XMPPLogVerbose(@"%@: Activated", THIS_FILE); xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream dispatchQueue:moduleQueue]; #ifdef _XMPP_VCARD_AVATAR_MODULE_H { // Automatically tie into the vCard system so we can store user photos. [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPvCardAvatarModule class]]; } #endif #ifdef _XMPP_MUC_H { // Automatically tie into the MUC system so we can ignore non-roster presence stanzas. [xmppStream enumerateModulesWithBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { if ([module isKindOfClass:[XMPPMUC class]]) { [mucModules add:(__bridge void *)module]; } }]; } #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_VCARD_AVATAR_MODULE_H { [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPvCardAvatarModule class]]; } #endif [super deactivate]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Internal //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * This method may optionally be used by XMPPRosterStorage classes (declared in XMPPRosterPrivate.h). **/ - (GCDMulticastDelegate *)multicastDelegate { return multicastDelegate; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (id )xmppRosterStorage { // Note: The xmppRosterStorage variable is read-only (set in the init method) return xmppRosterStorage; } - (BOOL)autoFetchRoster { __block BOOL result = NO; dispatch_block_t block = ^{ result = (config & kAutoFetchRoster) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } - (void)setAutoFetchRoster:(BOOL)flag { dispatch_block_t block = ^{ if (flag) config |= kAutoFetchRoster; else config &= ~kAutoFetchRoster; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (BOOL)autoClearAllUsersAndResources { __block BOOL result = NO; dispatch_block_t block = ^{ result = (config & kAutoClearAllUsersAndResources) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } - (void)setAutoClearAllUsersAndResources:(BOOL)flag { dispatch_block_t block = ^{ if (flag) config |= kAutoClearAllUsersAndResources; else config &= ~kAutoClearAllUsersAndResources; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (BOOL)autoAcceptKnownPresenceSubscriptionRequests { __block BOOL result = NO; dispatch_block_t block = ^{ result = (config & kAutoAcceptKnownPresenceSubscriptionRequests) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } - (void)setAutoAcceptKnownPresenceSubscriptionRequests:(BOOL)flag { dispatch_block_t block = ^{ if (flag) config |= kAutoAcceptKnownPresenceSubscriptionRequests; else config &= ~kAutoAcceptKnownPresenceSubscriptionRequests; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (BOOL)allowRosterlessOperation { __block BOOL result = NO; dispatch_block_t block = ^{ result = (config & kRosterlessOperation) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } - (void)setAllowRosterlessOperation:(BOOL)flag { dispatch_block_t block = ^{ if (flag) config |= kRosterlessOperation; else config &= ~kRosterlessOperation; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (BOOL)hasRequestedRoster { __block BOOL result = NO; dispatch_block_t block = ^{ result = (flags & kRequestedRoster) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } - (BOOL)isPopulating{ __block BOOL result = NO; dispatch_block_t block = ^{ result = (flags & kPopulatingRoster) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } - (BOOL)hasRoster { __block BOOL result = NO; dispatch_block_t block = ^{ result = (flags & kHasRoster) ? YES : NO; }; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_sync(moduleQueue, block); return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)_requestedRoster { NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); return (flags & kRequestedRoster) ? YES : NO; } - (void)_setRequestedRoster:(BOOL)flag { NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); if (flag) flags |= kRequestedRoster; else flags &= ~kRequestedRoster; } - (void)_setHasRoster:(BOOL)flag { NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); if (flag) flags |= kHasRoster; else flags &= ~kHasRoster; } - (BOOL)_populatingRoster { NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); return (flags & kPopulatingRoster) ? YES : NO; } - (void)_setPopulatingRoster:(BOOL)flag { NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); if (flag) flags |= kPopulatingRoster; else flags &= ~kPopulatingRoster; } - (void)_addRosterItems:(NSArray *)rosterItems { NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); BOOL hasRoster = [self hasRoster]; for (NSXMLElement *item in rosterItems) { // During roster population, we need to filter out items for users who aren't actually in our roster. // That is, those users who have requested to be our buddy, but we haven't approved yet. // This is described in more detail in the method isRosterItem above. [multicastDelegate xmppRoster:self didReceiveRosterItem:item]; if (hasRoster || [self isRosterItem:item]) { [xmppRosterStorage handleRosterItem:item xmppStream:xmppStream]; } } } /** * Some server's include in our roster the JID's of user's NOT in our roster. * This happens when another user adds us to their roster, and requests permission to receive our presence. * * As discussed in RFC 3921, the state of the other user is "None + Pending In", * and the server "SHOULD NOT" include these JID's in the roster it sends us. * * Nonetheless, some servers do anyway. * This method filters out such rogue entries in our roster. * * Note that the server will automatically send us the proper presence subscription request, * and it will continue to do so everytime we sign in. * From the RFC: * the user's server MUST keep a record of the subscription request and deliver the request when the * user next creates an available resource, until the user either approves or denies the request. * * So there is absolutely NO reason to process these entries, or include them in the roster's storage. * Furthermore, it isn't reliable to depend on these entires being there. * The RFC has clearly defined recommendations on the matter, and servers that currently send these rogue items * may very likely stop doing so in future versions. **/ - (BOOL)isRosterItem:(NSXMLElement *)item { NSString *subscription = [item attributeStringValueForName:@"subscription"]; if ([subscription isEqualToString:@"none"]) { NSString *ask = [item attributeStringValueForName:@"ask"]; if ([ask isEqualToString:@"subscribe"]) { return YES; } else { return NO; } } return YES; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Roster Management //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName{ [self addUser:jid withNickname:optionalName groups:nil subscribeToPresence:YES]; } - (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups{ [self addUser:jid withNickname:optionalName groups:groups subscribeToPresence:YES]; } - (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups subscribeToPresence:(BOOL)subscribe{ if (jid == nil) return; XMPPJID *myJID = xmppStream.myJID; if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) { // You don't need to add yourself to the roster. // XMPP will automatically send you presence from all resources signed in under your username. // // E.g. If you sign in with robbiehanson@deusty.com/home you'll automatically // receive presence from robbiehanson@deusty.com/work XMPPLogInfo(@"%@: %@ - Ignoring request to add myself to my own roster", [self class], THIS_METHOD); return; } // Add the buddy to our roster // // // // // family // // // NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; [item addAttributeWithName:@"jid" stringValue:[jid bare]]; if(optionalName) { [item addAttributeWithName:@"name" stringValue:optionalName]; } for (NSString *group in groups) { NSXMLElement *groupElement = [NSXMLElement elementWithName:@"group"]; [groupElement setStringValue:group]; [item addChild:groupElement]; } NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; [query addChild:item]; NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; [iq addAttributeWithName:@"type" stringValue:@"set"]; [iq addChild:query]; [xmppStream sendElement:iq]; if(subscribe) { [self subscribePresenceToUser:jid]; } } - (void)setNickname:(NSString *)nickname forUser:(XMPPJID *)jid { // This is a public method, so it may be invoked on any thread/queue. if (jid == nil) return; // // // // // NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; [item addAttributeWithName:@"jid" stringValue:[jid bare]]; [item addAttributeWithName:@"name" stringValue:nickname]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; [query addChild:item]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; [iq addChild:query]; [xmppStream sendElement:iq]; } - (void)subscribePresenceToUser:(XMPPJID *)jid { // This is a public method, so it may be invoked on any thread/queue. if (jid == nil) return; XMPPJID *myJID = xmppStream.myJID; if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) { XMPPLogInfo(@"%@: %@ - Ignoring request to subscribe presence to myself", [self class], THIS_METHOD); return; } // XMPPPresence *presence = [XMPPPresence presenceWithType:@"subscribe" to:[jid bareJID]]; [xmppStream sendElement:presence]; } - (void)unsubscribePresenceFromUser:(XMPPJID *)jid { // This is a public method, so it may be invoked on any thread/queue. if (jid == nil) return; XMPPJID *myJID = xmppStream.myJID; if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) { XMPPLogInfo(@"%@: %@ - Ignoring request to unsubscribe presence from myself", [self class], THIS_METHOD); return; } // XMPPPresence *presence = [XMPPPresence presenceWithType:@"unsubscribe" to:[jid bareJID]]; [xmppStream sendElement:presence]; } - (void)revokePresencePermissionFromUser:(XMPPJID *)jid { // This is a public method, so it may be invoked on any thread/queue. if (jid == nil) return; XMPPJID *myJID = xmppStream.myJID; if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) { XMPPLogInfo(@"%@: %@ - Ignoring request to revoke presence from myself", [self class], THIS_METHOD); return; } // XMPPPresence *presence = [XMPPPresence presenceWithType:@"unsubscribed" to:[jid bareJID]]; [xmppStream sendElement:presence]; } - (void)removeUser:(XMPPJID *)jid { // This is a public method, so it may be invoked on any thread/queue. if (jid == nil) return; XMPPJID *myJID = xmppStream.myJID; if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) { XMPPLogInfo(@"%@: %@ - Ignoring request to remove myself from my own roster", [self class], THIS_METHOD); return; } // Remove the user from our roster. // And unsubscribe from presence. // And revoke contact's subscription to our presence. // ...all in one step // // // // // NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; [item addAttributeWithName:@"jid" stringValue:[jid bare]]; [item addAttributeWithName:@"subscription" stringValue:@"remove"]; NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; [query addChild:item]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; [iq addChild:query]; [xmppStream sendElement:iq]; } - (void)acceptPresenceSubscriptionRequestFrom:(XMPPJID *)jid andAddToRoster:(BOOL)flag { // This is a public method, so it may be invoked on any thread/queue. // Send presence response // // XMPPPresence *presence = [XMPPPresence presenceWithType:@"subscribed" to:[jid bareJID]]; [xmppStream sendElement:presence]; // Add optionally add user to our roster if (flag) { [self addUser:jid withNickname:nil]; } } - (void)rejectPresenceSubscriptionRequestFrom:(XMPPJID *)jid { // This is a public method, so it may be invoked on any thread/queue. // Send presence response // // XMPPPresence *presence = [XMPPPresence presenceWithType:@"unsubscribed" to:[jid bareJID]]; [xmppStream sendElement:presence]; } - (void)fetchRoster { // This is a public method, so it may be invoked on any thread/queue. dispatch_block_t block = ^{ @autoreleasepool { [self fetchRosterVersion:nil]; }}; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } - (void)fetchRosterVersion:(NSString *)version { // This is a public method, so it may be invoked on any thread/queue. dispatch_block_t block = ^{ @autoreleasepool { if ([self _requestedRoster]) { // We've already requested the roster from the server. return; } // // // NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; if (version) [query addAttributeWithName:@"ver" stringValue:version]; XMPPIQ *iq = [XMPPIQ iqWithType:@"get" elementID:[xmppStream generateUUID]]; [iq addChild:query]; [xmppIDTracker addElement:iq target:self selector:@selector(handleFetchRosterQueryIQ:withInfo:) timeout:60]; [xmppStream sendElement:iq]; [self _setRequestedRoster:YES]; }}; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark XMPPIDTracker //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)handleFetchRosterQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)basicTrackingInfo{ dispatch_block_t block = ^{ @autoreleasepool { NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:roster"]; NSString * version = [query attributeStringValueForName:@"ver"]; BOOL hasRoster = [self hasRoster]; if (!hasRoster) { [xmppRosterStorage clearAllUsersAndResourcesForXMPPStream:xmppStream]; [self _setPopulatingRoster:YES]; [multicastDelegate xmppRosterDidBeginPopulating:self withVersion:version]; [xmppRosterStorage beginRosterPopulationForXMPPStream:xmppStream withVersion:version]; } NSArray *items = [query elementsForName:@"item"]; [self _addRosterItems:items]; if (!hasRoster) { // We should have our roster now [self _setHasRoster:YES]; [self _setPopulatingRoster:NO]; [multicastDelegate xmppRosterDidEndPopulating:self]; [xmppRosterStorage endRosterPopulationForXMPPStream:xmppStream]; // Process any premature presence elements we received. for (XMPPPresence *presence in earlyPresenceElements) { [self xmppStream:xmppStream didReceivePresence:presence]; } [earlyPresenceElements removeAllObjects]; } }}; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark XMPPStream Delegate //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { // This method is invoked on the moduleQueue. XMPPLogTrace(); if ([self autoFetchRoster]) { [self fetchRoster]; } } - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq { // This method is invoked on the moduleQueue. XMPPLogTrace(); // Note: Some jabber servers send an iq element with an xmlns. // Because of the bug in Apple's NSXML (documented in our elementForName method), // it is important we specify the xmlns for the query. NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:roster"]; if (query) { if([iq isSetIQ]) { [multicastDelegate xmppRoster:self didReceiveRosterPush:iq]; NSArray *items = [query elementsForName:@"item"]; [self _addRosterItems:items]; } else if([iq isResultIQ]) { [xmppIDTracker invokeForElement:iq withObject:iq]; } return YES; } return NO; } - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence { // This method is invoked on the moduleQueue. XMPPLogTrace(); if (![self hasRoster] && ![self allowRosterlessOperation]) { // We received a presence notification, // but we don't have a roster to apply it to yet. // // This is possible if we send our presence before we've received our roster. // It's even possible if we send our presence after we've requested our roster. // There is no guarantee the server will process our requests serially, // and the server may start sending presence elements before it sends our roster. // // However, if we've requested the roster, // then it shouldn't be too long before we receive it. // So we should be able to simply queue the presence elements for later processing. if ([self _requestedRoster]) { // We store the presence element until we get our roster. [earlyPresenceElements addObject:presence]; } else { // The user has not requested the roster. // This is a rogue presence element, or the user is simply not using our roster management. } return; } if ([[presence type] isEqualToString:@"subscribe"]) { XMPPJID *userJID = [[presence from] bareJID]; BOOL knownUser = [xmppRosterStorage userExistsWithJID:userJID xmppStream:xmppStream]; if (knownUser && [self autoAcceptKnownPresenceSubscriptionRequests]) { // Presence subscription request from someone who's already in our roster. // Automatically approve. // // XMPPPresence *response = [XMPPPresence presenceWithType:@"subscribed" to:userJID]; [xmppStream sendElement:response]; } else { // Presence subscription request from someone who's NOT in our roster [multicastDelegate xmppRoster:self didReceivePresenceSubscriptionRequest:presence]; } } else { #ifdef _XMPP_MUC_H // Ignore MUC related presence items for (XMPPMUC *muc in mucModules) { if ([muc isMUCRoomPresence:presence]) { return; } } #endif [xmppRosterStorage handlePresence:presence xmppStream:xmppStream]; } } - (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence { // This method is invoked on the moduleQueue. XMPPLogTrace(); // We check the toStr, so we don't dump the resources when a user leaves a MUC room. if ([[presence type] isEqualToString:@"unavailable"] && [presence toStr] == nil) { // We don't receive presence notifications when we're offline. // So we need to remove all resources from our roster when we're offline. // When we become available again, we'll automatically receive the // presence from every available user in our roster. // // We will receive general roster updates as long as we're still connected though. // So there's no need to refetch the roster. [xmppRosterStorage clearAllResourcesForXMPPStream:xmppStream]; [earlyPresenceElements removeAllObjects]; } } - (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error { // This method is invoked on the moduleQueue. XMPPLogTrace(); if([self autoClearAllUsersAndResources]) { [xmppRosterStorage clearAllUsersAndResourcesForXMPPStream:xmppStream]; } else { [xmppRosterStorage clearAllResourcesForXMPPStream:xmppStream]; } [self _setRequestedRoster:NO]; [self _setHasRoster:NO]; [earlyPresenceElements removeAllObjects]; } #ifdef _XMPP_MUC_H - (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module { if ([module isKindOfClass:[XMPPMUC class]]) { if (![mucModules contains:(__bridge void *)module]) { [mucModules add:(__bridge void *)module]; } } } - (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module { if ([module isKindOfClass:[XMPPMUC class]]) { [mucModules remove:(__bridge void *)module]; } } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark XMPPvCardAvatarDelegate //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #ifdef _XMPP_VCARD_AVATAR_MODULE_H #if TARGET_OS_IPHONE - (void)xmppvCardAvatarModule:(XMPPvCardAvatarModule *)vCardTempModule didReceivePhoto:(UIImage *)photo forJID:(XMPPJID *)jid #else - (void)xmppvCardAvatarModule:(XMPPvCardAvatarModule *)vCardTempModule didReceivePhoto:(NSImage *)photo forJID:(XMPPJID *)jid #endif { if ([xmppRosterStorage respondsToSelector:@selector(setPhoto:forUserWithJID:xmppStream:)]) { [xmppRosterStorage setPhoto:photo forUserWithJID:[jid bareJID] xmppStream:xmppStream]; } } #endif @end