#import "XMPPPubSub.h" #import "XMPPIQ+XEP_0060.h" #import "XMPPInternal.h" #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif // Defined in XMPPIQ+XEP_0060.h // // #define XMLNS_PUBSUB @"http://jabber.org/protocol/pubsub" // #define XMLNS_PUBSUB_OWNER @"http://jabber.org/protocol/pubsub#owner" // #define XMLNS_PUBSUB_EVENT @"http://jabber.org/protocol/pubsub#event" // #define XMLNS_PUBSUB_NODE_CONFIG @"http://jabber.org/protocol/pubsub#node_config" @implementation XMPPPubSub { XMPPJID *serviceJID; XMPPJID *myJID; NSMutableDictionary *subscribeDict; NSMutableDictionary *unsubscribeDict; NSMutableDictionary *retrieveSubsDict; NSMutableDictionary *configSubDict; NSMutableDictionary *createDict; NSMutableDictionary *deleteDict; NSMutableDictionary *configNodeDict; NSMutableDictionary *publishDict; NSMutableDictionary *retrieveItemsDict; } + (BOOL)isPubSubMessage:(XMPPMessage *)message { NSXMLElement *event = [message elementForName:@"event" xmlns:XMLNS_PUBSUB_EVENT]; return (event != nil); } @synthesize serviceJID; - (id)init { // This will cause a crash - it's designed to. // Only the init methods listed in XMPPPubSub.h are supported. return [self initWithServiceJID:nil dispatchQueue:NULL]; } - (id)initWithDispatchQueue:(dispatch_queue_t)queue { // This will cause a crash - it's designed to. // Only the init methods listed in XMPPPubSub.h are supported. return [self initWithServiceJID:nil dispatchQueue:NULL]; } - (id)initWithServiceJID:(XMPPJID *)aServiceJID { return [self initWithServiceJID:aServiceJID dispatchQueue:NULL]; } - (id)initWithServiceJID:(XMPPJID *)aServiceJID dispatchQueue:(dispatch_queue_t)queue { // If aServiceJID is nil, we won't include a 'to' attribute in the element(s) we send. // This is the proper configuration for PEP, as it uses the bare JID as the pubsub node. if ((self = [super initWithDispatchQueue:queue])) { serviceJID = [aServiceJID copy]; subscribeDict = [[NSMutableDictionary alloc] init]; unsubscribeDict = [[NSMutableDictionary alloc] init]; retrieveSubsDict = [[NSMutableDictionary alloc] init]; configSubDict = [[NSMutableDictionary alloc] init]; createDict = [[NSMutableDictionary alloc] init]; deleteDict = [[NSMutableDictionary alloc] init]; configNodeDict = [[NSMutableDictionary alloc] init]; publishDict = [[NSMutableDictionary alloc] init]; retrieveItemsDict = [[NSMutableDictionary alloc] init]; } return self; } - (BOOL)activate:(XMPPStream *)aXmppStream { if ([super activate:aXmppStream]) { if (serviceJID == nil) { myJID = xmppStream.myJID; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(myJIDDidChange:) name:XMPPStreamDidChangeMyJIDNotification object:nil]; } return YES; } return NO; } - (void)deactivate { [subscribeDict removeAllObjects]; [unsubscribeDict removeAllObjects]; [retrieveSubsDict removeAllObjects]; [configSubDict removeAllObjects]; [createDict removeAllObjects]; [deleteDict removeAllObjects]; [configNodeDict removeAllObjects]; [publishDict removeAllObjects]; [retrieveItemsDict removeAllObjects]; if (serviceJID == nil) { [[NSNotificationCenter defaultCenter] removeObserver:self name:XMPPStreamDidChangeMyJIDNotification object:nil]; } [super deactivate]; } - (void)myJIDDidChange:(NSNotification *)notification { // Notifications are delivered on the thread/queue that posted them. // In this case, they are delivered on xmppStream's internal processing queue. XMPPStream *stream = (XMPPStream *)[notification object]; dispatch_block_t block = ^{ @autoreleasepool { if (xmppStream == stream) { myJID = xmppStream.myJID; } }}; if (dispatch_get_specific(moduleQueueTag)) block(); else dispatch_async(moduleQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark XMPPStream Delegate //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Delegate method to receive incoming IQ stanzas. **/ - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq { // Check to see if IQ is from our PubSub/PEP service if (serviceJID) { if (![serviceJID isEqualToJID:[iq from]]) return NO; } else { if (![myJID isEqualToJID:[iq from] options:XMPPJIDCompareBare]) return NO; } NSString *elementID = [iq elementID]; NSString *node = nil; if ((node = subscribeDict[elementID])) { // Example subscription success response: // // // // // // // // Example subscription error response: // // // // // // // // // XEP-0060 provides many other example responses, but // not all of them fit perfectly into the subscribed/not-subscribed categories. // For example, the subscription could be: // // - pending, approval required // - unconfigured, configuration required // - unconfigured, configuration supported // // However, in the general sense, the subscription request was accepted. // So these special cases will still be broadcast as "subscibed", // and it is the delegates responsibility to handle these special cases if the server is configured as such. if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didSubscribeToNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotSubscribeToNode:node withError:iq]; [subscribeDict removeObjectForKey:elementID]; return YES; } else if ((node = unsubscribeDict[elementID])) { // Example unsubscribe success response: // // // // Example unsubscribe error response: // // // // // // // // // XEP-0060 provides many other example responses, but // not all of them fit perfectly into the unsubscribed/not-unsubscribed categories. // // For example, there's an error that gets returned if the client wasn't subscribed. // Depending on the client, this could possibly get treated as a successful unsubscribe action. // // It is the delegates responsibility to handle these special cases. if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didUnsubscribeFromNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotUnsubscribeFromNode:node withError:iq]; [unsubscribeDict removeObjectForKey:elementID]; return YES; } else if ((node = retrieveSubsDict[elementID])) { // Example retrieve success response: // // // // // // // // // // // // // // Example retrieve error response: // // // // // // // if ([[iq type] isEqualToString:@"result"]) { if ([node isKindOfClass:[NSNull class]]) [multicastDelegate xmppPubSub:self didRetrieveSubscriptions:iq]; else [multicastDelegate xmppPubSub:self didRetrieveSubscriptions:iq forNode:node]; } else { if ([node isKindOfClass:[NSNull class]]) [multicastDelegate xmppPubSub:self didNotRetrieveSubscriptions:iq]; else [multicastDelegate xmppPubSub:self didNotRetrieveSubscriptions:iq forNode:node]; } [retrieveSubsDict removeObjectForKey:elementID]; return YES; } else if ((node = configSubDict[elementID])) { // Example configure subscription success response: // // // // Example configure subscription error response: // // // // // // // if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didConfigureSubscriptionToNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotConfigureSubscriptionToNode:node withError:iq]; [configSubDict removeObjectForKey:elementID]; return YES; } else if ((node = publishDict[elementID])) { // Example publish success response: // // // // // // // // // // Example publish error response: // // // // // // if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didPublishToNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotPublishToNode:node withError:iq]; [publishDict removeObjectForKey:elementID]; return YES; } else if ((node = createDict[elementID])) { // Example create success response: // // // // Example create error response: // // // // // // // if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didCreateNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotCreateNode:node withError:iq]; [createDict removeObjectForKey:elementID]; return YES; } else if ((node = deleteDict[elementID])) { // Example delete success response: // // // // Example delete error response: // // // // // // if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didDeleteNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotDeleteNode:node withError:iq]; [deleteDict removeObjectForKey:elementID]; return YES; } else if ((node = configNodeDict[elementID])) { // Example configure node success response: // // // // Example configure node error response: // // // // // // if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didConfigureNode:node withResult:iq]; else [multicastDelegate xmppPubSub:self didNotConfigureNode:node withError:iq]; [configNodeDict removeObjectForKey:elementID]; return YES; } else if ((node = retrieveItemsDict[elementID])) { // Example retrieve from node success response: // // // // // // ... // // // // // // Example delete error response: // // // // // // // if ([[iq type] isEqualToString:@"result"]) [multicastDelegate xmppPubSub:self didRetrieveItems:iq fromNode:node]; else [multicastDelegate xmppPubSub:self didNotRetrieveItems:iq fromNode:node]; [retrieveItemsDict removeObjectForKey:elementID]; return YES; } return NO; } /** * Delegate method to receive incoming message stanzas. **/ - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message { // Check to see if message is from our PubSub/PEP service if (serviceJID) { if (![serviceJID isEqualToJID:[message from]]) return; } else { if ([myJID isEqualToJID:[message from] options:XMPPJIDCompareBare]) return; } // // // // // [... entry ...] // // // // NSXMLElement *event = [message elementForName:@"event" xmlns:XMLNS_PUBSUB_EVENT]; if (event) { [multicastDelegate xmppPubSub:self didReceiveMessage:message]; } } - (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error { [subscribeDict removeAllObjects]; [unsubscribeDict removeAllObjects]; [retrieveSubsDict removeAllObjects]; [configSubDict removeAllObjects]; [createDict removeAllObjects]; [deleteDict removeAllObjects]; [configNodeDict removeAllObjects]; [publishDict removeAllObjects]; [retrieveItemsDict removeAllObjects]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utility Methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSXMLElement *)formForOptions:(NSDictionary *)options withFromType:(NSString *)formTypeValue { // // // http://jabber.org/protocol/pubsub#subscribe_options // // 1 // 0 // false // // chat // online // away // // NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; [x addAttributeWithName:@"type" stringValue:@"submit"]; NSXMLElement *formTypeField = [NSXMLElement elementWithName:@"field"]; [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"]; [formTypeField addAttributeWithName:@"type" stringValue:@"hidden"]; [formTypeField addChild:[NSXMLElement elementWithName:@"value" stringValue:formTypeValue]]; [x addChild:formTypeField]; [options enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){ NSAssert([key isKindOfClass:[NSString class]], @"The keys within an options dictionary must be strings"); NSXMLElement *field = [NSXMLElement elementWithName:@"field"]; NSString *var = (NSString *)key; [field addAttributeWithName:@"var" stringValue:var]; if ([obj isKindOfClass:[NSArray class]]) { NSArray *values = (NSArray *)obj; for (id value in values) { [field addChild:[NSXMLElement elementWithName:@"value" objectValue:value]]; } } else { [field addChild:[NSXMLElement elementWithName:@"value" objectValue:obj]]; } [x addChild:field]; }]; return x; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Subscription Methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSString *)subscribeToNode:(NSString *)node { return [self subscribeToNode:node withJID:nil options:nil]; } - (NSString *)subscribeToNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid { return [self subscribeToNode:node withJID:myBareOrFullJid options:nil]; } - (NSString *)subscribeToNode:(NSString *)aNode withJID:(XMPPJID *)myBareOrFullJid options:(NSDictionary *)options { if (aNode == nil) return nil; // In-case aNode is mutable NSString *node = [aNode copy]; // We default to using the full JID NSString *jidStr = myBareOrFullJid ? [myBareOrFullJid full] : [xmppStream.myJID full]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ subscribeDict[uuid] = node; }); // Example from XEP-0060 section 6.1.1: // // // // // // NSXMLElement *subscribe = [NSXMLElement elementWithName:@"subscribe"]; [subscribe addAttributeWithName:@"node" stringValue:node]; [subscribe addAttributeWithName:@"jid" stringValue:jidStr]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:subscribe]; if (options) { // Example from XEP-0060 section 6.3.7: // // // // // http://jabber.org/protocol/pubsub#subscribe_options // // 1 // 0 // false // // chat // online // away // // // NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_SUBSCRIBE_OPTIONS]; NSXMLElement *optionsStanza = [NSXMLElement elementWithName:@"options"]; [optionsStanza addChild:x]; [pubsub addChild:optionsStanza]; } XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } - (NSString *)unsubscribeFromNode:(NSString *)node { return [self unsubscribeFromNode:node withJID:nil subid:nil]; } - (NSString *)unsubscribeFromNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid { return [self unsubscribeFromNode:node withJID:myBareOrFullJid subid:nil]; } - (NSString *)unsubscribeFromNode:(NSString *)aNode withJID:(XMPPJID *)myBareOrFullJid subid:(NSString *)subid { if (aNode == nil) return nil; // In-case aNode is mutable NSString *node = [aNode copy]; // We default to using the full JID NSString *jidStr = myBareOrFullJid ? [myBareOrFullJid full] : [xmppStream.myJID full]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ unsubscribeDict[uuid] = node; }); // Example from XEP-0060 section 6.2.1: // // // // // // NSXMLElement *unsubscribe = [NSXMLElement elementWithName:@"unsubscribe"]; [unsubscribe addAttributeWithName:@"node" stringValue:node]; [unsubscribe addAttributeWithName:@"jid" stringValue:jidStr]; if (subid) [unsubscribe addAttributeWithName:@"subid" stringValue:subid]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:unsubscribe]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } - (NSString *)retrieveSubscriptions { return [self retrieveSubscriptionsForNode:nil]; } - (NSString *)retrieveSubscriptionsForNode:(NSString *)aNode { // Parameter aNode is optional // In-case aNode is mutable NSString *node = [aNode copy]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ if (node) retrieveSubsDict[uuid] = node; else retrieveSubsDict[uuid] = [NSNull null]; }); // Get subscriptions for all nodes: // // // // // // // // // Get subscriptions for a specific node: // // // // // // NSXMLElement *subscriptions = [NSXMLElement elementWithName:@"subscriptions"]; if (node) { [subscriptions addAttributeWithName:@"node" stringValue:node]; } NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:subscriptions]; XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } - (NSString *)configureSubscriptionToNode:(NSString *)aNode withJID:(XMPPJID *)myBareOrFullJid subid:(NSString *)subid options:(NSDictionary *)options { if (aNode == nil) return nil; // In-case aNode is mutable NSString *node = [aNode copy]; // We default to using the full JID NSString *jidStr = myBareOrFullJid ? [myBareOrFullJid full] : [xmppStream.myJID full]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ configSubDict[uuid] = node; }); // Example from XEP-0060 section 6.3.5: // // // // // // // http://jabber.org/protocol/pubsub#subscribe_options // // 1 // 0 // false // // chat // online // away // // // // // NSXMLElement *optionsStanza = [NSXMLElement elementWithName:@"options"]; [optionsStanza addAttributeWithName:@"node" stringValue:node]; [optionsStanza addAttributeWithName:@"jid" stringValue:jidStr]; if (subid) { [optionsStanza addAttributeWithName:@"subid" stringValue:subid]; } if (options) { NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_NODE_CONFIG]; [optionsStanza addChild:x]; } NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB_OWNER]; [pubsub addChild:optionsStanza]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Node Admin //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSString *)createNode:(NSString *)node { return [self createNode:node withOptions:nil]; } - (NSString *)createNode:(NSString *)aNode withOptions:(NSDictionary *)options { if (aNode == nil) return nil; // In-case aNode is mutable NSString *node = [aNode copy]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ createDict[uuid] = node; }); // // // // // // // http://jabber.org/protocol/pubsub#node_config // // Princely Musings (Atom) // 1 // 1 // 1 // 10 // ... // // // // NSXMLElement *create = [NSXMLElement elementWithName:@"create"]; [create addAttributeWithName:@"node" stringValue:node]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:create]; if (options) { // Example from XEP-0060 section 8.1.3 show above NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_NODE_CONFIG]; NSXMLElement *configure = [NSXMLElement elementWithName:@"configure"]; [configure addChild:x]; [pubsub addChild:configure]; } XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } /** * This method currently does not support redirection **/ - (NSString *)deleteNode:(NSString *)aNode { if (aNode == nil) return nil; // In-case aNode is mutable NSString *node = [aNode copy]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ deleteDict[uuid] = node; }); // Example XEP-0060 section 8.4.1: // // // // // // NSXMLElement *delete = [NSXMLElement elementWithName:@"delete"]; [delete addAttributeWithName:@"node" stringValue:node]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB_OWNER]; [pubsub addChild:delete]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } - (NSString *)configureNode:(NSString *)node { return [self configureNode:node withOptions:nil]; } - (NSString *)configureNode:(NSString *)aNode withOptions:(NSDictionary *)options { if (aNode == nil) return nil; // In-case aNode is mutable NSString *node = [aNode copy]; // Generate uuid and add to dict NSString *uuid = [xmppStream generateUUID]; dispatch_async(moduleQueue, ^{ configNodeDict[uuid] = node; }); // // // // // NSXMLElement *configure = [NSXMLElement elementWithName:@"configure"]; [configure addAttributeWithName:@"node" stringValue:node]; if (options) { NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_NODE_CONFIG]; [configure addChild:x]; } NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB_OWNER]; [pubsub addChild:configure]; XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; return uuid; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Publication methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry { return [self publishToNode:node entry:entry withItemID:nil options:nil]; } - (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry withItemID:(NSString *)itemId { return [self publishToNode:node entry:entry withItemID:itemId options:nil]; } - (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry withItemID:(NSString *)itemId options:(NSDictionary *)options { if (node == nil) return nil; if (entry == nil) return nil; // // // // // Some content // // // // [... FORM ... ] // // // NSString *uuid = [xmppStream generateUUID]; NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; if (itemId) [item addAttributeWithName:@"id" stringValue:itemId]; [item addChild:entry]; NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; [publish addAttributeWithName:@"node" stringValue:node]; [publish addChild:item]; NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:publish]; if (options) { // Example from XEP-0060 section 7.1.5: // // // // // http://jabber.org/protocol/pubsub#publish-options // // // presence // // // NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_PUBLISH_OPTIONS]; NSXMLElement *publishOptions = [NSXMLElement elementWithName:@"publish-options"]; [publishOptions addChild:x]; [pubsub addChild:publishOptions]; } XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; dispatch_async(moduleQueue, ^{ publishDict[uuid] = node; }); return uuid; } - (NSString *)retrieveItemsFromNode:(NSString *)node { return [self retrieveItemsFromNode:node withItemIDs:nil]; } - (NSString *)retrieveItemsFromNode:(NSString *)node withItemIDs:(NSArray *)itemIds { if (node == nil) return nil; // //   //     //       //       //     //   // NSString *uuid = [xmppStream generateUUID]; NSXMLElement *items = [NSXMLElement elementWithName:@"items"]; [items addAttributeWithName:@"node" stringValue:node]; if (itemIds) { for (id itemId in itemIds) { NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; [item addAttributeWithName:@"id" stringValue:itemId]; [items addChild:item]; } } NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; [pubsub addChild:items]; XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:serviceJID elementID:uuid]; [iq addChild:pubsub]; [xmppStream sendElement:iq]; dispatch_async(moduleQueue, ^{ retrieveItemsDict[uuid] = node; }); return uuid; } @end