645 lines
17 KiB
Objective-C
645 lines
17 KiB
Objective-C
#import "XMPP.h"
|
|
#import "XMPPLogging.h"
|
|
#import "XMPPBlocking.h"
|
|
#import "NSNumber+XMPP.h"
|
|
|
|
// Log levels: off, error, warn, info, verbose
|
|
// Log flags: trace
|
|
#ifdef DEBUG
|
|
static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; // | XMPP_LOG_FLAG_TRACE;
|
|
#else
|
|
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
|
|
#endif
|
|
|
|
#define QUERY_TIMEOUT 30.0 // NSTimeInterval (double) = seconds
|
|
|
|
NSString *const XMPPBlockingErrorDomain = @"XMPPBlockingErrorDomain";
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
typedef enum XMPPBlockingQueryInfoType {
|
|
FetchBlockingList,
|
|
BlockUser,
|
|
UnblockUser,
|
|
UnblockAll,
|
|
|
|
} XMPPBlockingQueryInfoType;
|
|
|
|
@interface XMPPBlockingQueryInfo : NSObject
|
|
{
|
|
XMPPBlockingQueryInfoType type;
|
|
XMPPJID *blockingXMPPJID;
|
|
NSArray *blockingListItems;
|
|
|
|
dispatch_source_t timer;
|
|
}
|
|
|
|
@property (nonatomic, readonly) XMPPBlockingQueryInfoType type;
|
|
@property (nonatomic, readonly) NSArray *blockingListItems;
|
|
|
|
@property (nonatomic, readwrite) XMPPJID *blockingXMPPJID;
|
|
@property (nonatomic, readwrite) dispatch_source_t timer;
|
|
|
|
- (void)cancel;
|
|
|
|
+ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type;
|
|
+ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid;
|
|
+ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid items:(NSArray *)items;
|
|
|
|
@end
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@interface XMPPBlocking (/* Must be nameless for properties */)
|
|
|
|
- (void)addQueryInfo:(XMPPBlockingQueryInfo *)qi withKey:(NSString *)uuid;
|
|
- (void)queryTimeout:(NSString *)uuid;
|
|
|
|
@end
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@implementation XMPPBlocking
|
|
|
|
@synthesize blockingDict = _blockingDict;
|
|
|
|
- (id)init
|
|
{
|
|
return [self initWithDispatchQueue:NULL];
|
|
}
|
|
|
|
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
|
|
{
|
|
if ((self = [super initWithDispatchQueue:queue]))
|
|
{
|
|
autoRetrieveBlockingListItems = YES;
|
|
autoClearBlockingListInfo = YES;
|
|
|
|
blockingDict = [[NSMutableDictionary alloc] init];
|
|
|
|
pendingQueries = [[NSMutableDictionary alloc] init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)activate:(XMPPStream *)aXmppStream
|
|
{
|
|
if ([super activate:aXmppStream])
|
|
{
|
|
// Reserved for possible future use.
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)deactivate
|
|
{
|
|
// Reserved for possible future use.
|
|
|
|
[super deactivate];
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Properties
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (BOOL)autoRetrieveBlockingListItems
|
|
{
|
|
if (dispatch_get_specific(moduleQueueTag))
|
|
{
|
|
return autoRetrieveBlockingListItems;
|
|
}
|
|
else
|
|
{
|
|
__block BOOL result;
|
|
|
|
dispatch_sync(moduleQueue, ^{
|
|
result = autoRetrieveBlockingListItems;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
- (void)setAutoRetrieveBlockingListItems:(BOOL)flag
|
|
{
|
|
dispatch_block_t block = ^{
|
|
|
|
autoRetrieveBlockingListItems = flag;
|
|
};
|
|
|
|
if (dispatch_get_specific(moduleQueueTag))
|
|
block();
|
|
else
|
|
dispatch_async(moduleQueue, block);
|
|
}
|
|
|
|
- (BOOL)autoClearBlockingListInfo
|
|
{
|
|
if (dispatch_get_specific(moduleQueueTag))
|
|
{
|
|
return autoClearBlockingListInfo;
|
|
}
|
|
else
|
|
{
|
|
__block BOOL result;
|
|
|
|
dispatch_sync(moduleQueue, ^{
|
|
result = autoClearBlockingListInfo;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
- (void)setAutoClearBlockingListInfo:(BOOL)flag
|
|
{
|
|
dispatch_block_t block = ^{
|
|
|
|
autoClearBlockingListInfo = flag;
|
|
};
|
|
|
|
if (dispatch_get_specific(moduleQueueTag))
|
|
block();
|
|
else
|
|
dispatch_async(moduleQueue, block);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Public API
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (void)retrieveBlockingListItems
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
// <iq type='get' id='blocklist1'>
|
|
// <blocklist xmlns='urn:xmpp:blocking'/>
|
|
// </iq>
|
|
|
|
NSXMLElement *block = [NSXMLElement elementWithName:@"blocklist" xmlns:@"urn:xmpp:blocking"];
|
|
|
|
NSString *uuid = [xmppStream generateUUID];
|
|
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:uuid child:block];
|
|
|
|
[xmppStream sendElement:iq];
|
|
|
|
XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:FetchBlockingList];
|
|
[self addQueryInfo:qi withKey:uuid];
|
|
}
|
|
|
|
- (void)clearBlockingListInfo
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
if (dispatch_get_specific(moduleQueueTag))
|
|
{
|
|
[blockingDict removeAllObjects];
|
|
}
|
|
else
|
|
{
|
|
dispatch_async(moduleQueue, ^{ @autoreleasepool {
|
|
|
|
[blockingDict removeAllObjects];
|
|
}});
|
|
}
|
|
}
|
|
|
|
- (NSArray*)blockingList
|
|
{
|
|
if (dispatch_get_specific(moduleQueueTag))
|
|
{
|
|
return [blockingDict allKeys];
|
|
}
|
|
else
|
|
{
|
|
__block NSArray *result;
|
|
|
|
dispatch_sync(moduleQueue, ^{ @autoreleasepool {
|
|
|
|
result = [[blockingDict allKeys] copy];
|
|
}});
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
- (void)blockJID:(XMPPJID*)xmppJID
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
id value = blockingDict[[xmppJID full]];
|
|
if (value == nil)
|
|
{
|
|
blockingDict[[xmppJID full]] = [NSNull null];
|
|
}
|
|
|
|
// <iq from='juliet@capulet.com/chamber' type='set' id='block1'>
|
|
// <block xmlns='urn:xmpp:blocking'>
|
|
// <item jid='romeo@montague.net'/>
|
|
// </block>
|
|
// </iq>
|
|
|
|
NSXMLElement *block = [NSXMLElement elementWithName:@"block" xmlns:@"urn:xmpp:blocking"];
|
|
NSXMLElement *item = [NSXMLElement elementWithName:@"item"];
|
|
[item addAttributeWithName:@"jid" stringValue:[xmppJID full]];
|
|
[block addChild:item];
|
|
|
|
NSString *uuid = [xmppStream generateUUID];
|
|
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:block];
|
|
[iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.bare];
|
|
|
|
[xmppStream sendElement:iq];
|
|
|
|
XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:BlockUser];
|
|
qi.blockingXMPPJID = xmppJID;
|
|
[self addQueryInfo:qi withKey:uuid];
|
|
}
|
|
|
|
- (void)unblockJID:(XMPPJID*)xmppJID
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
id value = blockingDict[[xmppJID full]];
|
|
if (value != nil)
|
|
{
|
|
[blockingDict removeObjectForKey:[xmppJID full]];
|
|
}
|
|
|
|
// <iq type='set' id='unblock1'>
|
|
// <unblock xmlns='urn:xmpp:blocking'>
|
|
// <item jid='romeo@montague.net'/>
|
|
// </unblock>
|
|
// </iq>
|
|
|
|
NSXMLElement *block = [NSXMLElement elementWithName:@"unblock" xmlns:@"urn:xmpp:blocking"];
|
|
NSXMLElement *item = [NSXMLElement elementWithName:@"item"];
|
|
[item addAttributeWithName:@"jid" stringValue:[xmppJID full]];
|
|
[block addChild:item];
|
|
|
|
NSString *uuid = [xmppStream generateUUID];
|
|
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:block];
|
|
|
|
[xmppStream sendElement:iq];
|
|
|
|
XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:UnblockUser];
|
|
qi.blockingXMPPJID = xmppJID;
|
|
[self addQueryInfo:qi withKey:uuid];
|
|
}
|
|
|
|
- (BOOL)containsJID:(XMPPJID*)xmppJID
|
|
{
|
|
if (blockingDict[[xmppJID full]])
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Unblock all.
|
|
*/
|
|
- (void)unblockAll
|
|
{
|
|
XMPPLogTrace();
|
|
|
|
// <iq type='set' id='unblock2'>
|
|
// <unblock xmlns='urn:xmpp:blocking'/>
|
|
// </iq>
|
|
|
|
NSXMLElement *block = [NSXMLElement elementWithName:@"unblock" xmlns:@"urn:xmpp:blocking"];
|
|
|
|
NSString *uuid = [xmppStream generateUUID];
|
|
XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:block];
|
|
|
|
[xmppStream sendElement:iq];
|
|
|
|
XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:UnblockAll];
|
|
[self addQueryInfo:qi withKey:uuid];
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark Query Processing
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (void)addQueryInfo:(XMPPBlockingQueryInfo *)queryInfo withKey:(NSString *)uuid
|
|
{
|
|
// Setup timer
|
|
|
|
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
|
|
|
|
dispatch_source_set_event_handler(timer, ^{ @autoreleasepool {
|
|
|
|
[self queryTimeout:uuid];
|
|
}});
|
|
|
|
dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, (QUERY_TIMEOUT * NSEC_PER_SEC));
|
|
|
|
dispatch_source_set_timer(timer, fireTime, DISPATCH_TIME_FOREVER, 1.0);
|
|
dispatch_resume(timer);
|
|
|
|
queryInfo.timer = timer;
|
|
|
|
// Add to dictionary
|
|
pendingQueries[uuid] = queryInfo;
|
|
}
|
|
|
|
- (void)removeQueryInfo:(XMPPBlockingQueryInfo *)queryInfo withKey:(NSString *)uuid
|
|
{
|
|
// Invalidate timer
|
|
[queryInfo cancel];
|
|
|
|
// Remove from dictionary
|
|
[pendingQueries removeObjectForKey:uuid];
|
|
}
|
|
|
|
- (void)processQuery:(XMPPBlockingQueryInfo *)queryInfo withFailureCode:(XMPPBlockingErrorCode)errorCode
|
|
{
|
|
NSError *error = [NSError errorWithDomain:XMPPBlockingErrorDomain code:errorCode userInfo:nil];
|
|
|
|
if (queryInfo.type == FetchBlockingList)
|
|
{
|
|
[multicastDelegate xmppBlocking:self didNotReceivedBlockingListDueToError:error];
|
|
}
|
|
else if (queryInfo.type == BlockUser)
|
|
{
|
|
[multicastDelegate xmppBlocking:self didNotBlockJID:queryInfo.blockingXMPPJID error:error];
|
|
}
|
|
else if (queryInfo.type == UnblockUser)
|
|
{
|
|
[multicastDelegate xmppBlocking:self didNotUnblockJID:queryInfo.blockingXMPPJID error:error];
|
|
}
|
|
else if (queryInfo.type == UnblockAll)
|
|
{
|
|
[multicastDelegate xmppBlocking:self didNotUnblockAllDueToError:error];
|
|
}
|
|
}
|
|
|
|
- (void)queryTimeout:(NSString *)uuid
|
|
{
|
|
XMPPBlockingQueryInfo *queryInfo = pendingQueries[uuid];
|
|
if (queryInfo)
|
|
{
|
|
[self processQuery:queryInfo withFailureCode:XMPPBlockingQueryTimeout];
|
|
[self removeQueryInfo:queryInfo withKey:uuid];
|
|
}
|
|
}
|
|
|
|
- (void)processQueryResponse:(XMPPIQ *)iq withInfo:(XMPPBlockingQueryInfo *)queryInfo
|
|
{
|
|
if (queryInfo.type == FetchBlockingList)
|
|
{
|
|
// Blocking List Query Response:
|
|
//
|
|
// <iq type='result' id='blocklist1'>
|
|
// <blocklist xmlns='urn:xmpp:blocking'>
|
|
// <item jid='romeo@montague.net'/>
|
|
// <item jid='iago@shakespeare.lit'/>
|
|
// </blocklist>
|
|
// </iq>
|
|
|
|
if ([[iq type] isEqualToString:@"result"])
|
|
{
|
|
NSXMLElement *blocklist = [iq elementForName:@"blocklist" xmlns:@"urn:xmpp:blocking"];
|
|
if (blocklist == nil) return;
|
|
|
|
NSArray *listItems = [blocklist elementsForName:@"item"];
|
|
for (NSXMLElement *listItem in listItems)
|
|
{
|
|
NSString *name = [listItem attributeStringValueForName:@"jid"];
|
|
if (name)
|
|
{
|
|
id value = blockingDict[name];
|
|
if (value == nil)
|
|
{
|
|
blockingDict[name] = [NSNull null];
|
|
}
|
|
}
|
|
}
|
|
|
|
[multicastDelegate xmppBlocking:self didReceivedBlockingList:[self blockingList]];
|
|
[self removeQueryInfo:queryInfo withKey:[iq elementID]];
|
|
}
|
|
else
|
|
{
|
|
[multicastDelegate xmppBlocking:self didNotReceivedBlockingListDueToError:iq];
|
|
}
|
|
}
|
|
else if (queryInfo.type == BlockUser)
|
|
{
|
|
// <iq type='result' id='block1'/>
|
|
|
|
if ([[iq type] isEqualToString:@"result"])
|
|
{
|
|
[self removeQueryInfo:queryInfo withKey:[iq elementID]];
|
|
[multicastDelegate xmppBlocking:self didBlockJID:queryInfo.blockingXMPPJID];
|
|
}
|
|
else
|
|
{
|
|
[blockingDict removeObjectForKey:[queryInfo.blockingXMPPJID full]];
|
|
[multicastDelegate xmppBlocking:self didNotBlockJID:queryInfo.blockingXMPPJID error:iq];
|
|
}
|
|
}
|
|
else if (queryInfo.type == UnblockUser)
|
|
{
|
|
// <iq type='result' id='unblock1'/>
|
|
|
|
if ([[iq type] isEqualToString:@"result"])
|
|
{
|
|
[self removeQueryInfo:queryInfo withKey:[iq elementID]];
|
|
[multicastDelegate xmppBlocking:self didUnblockJID:queryInfo.blockingXMPPJID];
|
|
}
|
|
else
|
|
{
|
|
XMPPBlockingQueryInfo *queryInfo = pendingQueries[[iq elementID]];
|
|
|
|
id value = blockingDict[[queryInfo.blockingXMPPJID full]];
|
|
if (value == nil)
|
|
{
|
|
blockingDict[[queryInfo.blockingXMPPJID full]] = [NSNull null];
|
|
}
|
|
|
|
[multicastDelegate xmppBlocking:self didNotBlockJID:queryInfo.blockingXMPPJID error:iq];
|
|
}
|
|
}
|
|
else if (queryInfo.type == UnblockAll)
|
|
{
|
|
// <iq type='result' id='unblock2'/>
|
|
|
|
if ([[iq type] isEqualToString:@"result"])
|
|
{
|
|
[self removeQueryInfo:queryInfo withKey:[iq elementID]];
|
|
[multicastDelegate xmppBlocking:self didUnblockAllWithError:nil];
|
|
}
|
|
else
|
|
{
|
|
XMPPBlockingQueryInfo *queryInfo = pendingQueries[[iq elementID]];
|
|
|
|
id value = blockingDict[[queryInfo.blockingXMPPJID full]];
|
|
if (value == nil)
|
|
{
|
|
blockingDict[queryInfo.blockingXMPPJID] = [NSNull null];
|
|
}
|
|
|
|
[multicastDelegate xmppBlocking:self didNotUnblockAllDueToError:iq];
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark XMPPStream Delegate
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
|
|
{
|
|
if (self.autoRetrieveBlockingListItems)
|
|
{
|
|
[self retrieveBlockingListItems];
|
|
}
|
|
}
|
|
|
|
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
|
|
{
|
|
NSString *type = [iq type];
|
|
|
|
if ([type isEqualToString:@"set"])
|
|
{
|
|
NSXMLElement *block = [iq elementForName:@"block" xmlns:@"urn:xmpp:blocking"];
|
|
NSXMLElement *unblock = [iq elementForName:@"unblock" xmlns:@"urn:xmpp:blocking"];
|
|
|
|
if (block || unblock)
|
|
{
|
|
NSXMLElement *list = [block elementForName:@"item"];
|
|
|
|
if (!list)
|
|
{
|
|
list = [unblock elementForName:@"item"];
|
|
}
|
|
|
|
NSString *itemName = [list attributeStringValueForName:@"jid"];
|
|
if (itemName == nil)
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
[multicastDelegate xmppBlocking:self didReceivePushWithBlockingList:itemName];
|
|
|
|
XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]];
|
|
[xmppStream sendElement:iqResponse];
|
|
|
|
if (self.autoRetrieveBlockingListItems)
|
|
{
|
|
[self retrieveBlockingListItems];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This may be a response to a query we sent
|
|
|
|
XMPPBlockingQueryInfo *queryInfo = pendingQueries[[iq elementID]];
|
|
|
|
|
|
if (queryInfo)
|
|
{
|
|
[self processQueryResponse:iq withInfo:queryInfo];
|
|
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
-(void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
|
|
{
|
|
// If there are any pending queries,
|
|
// they just failed due to the disconnection.
|
|
|
|
for (NSString *uuid in pendingQueries)
|
|
{
|
|
XMPPBlockingQueryInfo *queryInfo = pendingQueries[uuid];
|
|
|
|
[self processQuery:queryInfo withFailureCode:XMPPBlockingDisconnect];
|
|
}
|
|
|
|
// Clear the list of pending queries
|
|
|
|
[pendingQueries removeAllObjects];
|
|
|
|
// Maybe clear all stored blocking info
|
|
|
|
if (self.autoClearBlockingListInfo)
|
|
{
|
|
[self clearBlockingListInfo];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#pragma mark -
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
@implementation XMPPBlockingQueryInfo
|
|
|
|
@synthesize type;
|
|
@synthesize blockingXMPPJID;
|
|
@synthesize blockingListItems;
|
|
@synthesize timer;
|
|
|
|
- (id)initWithType:(XMPPBlockingQueryInfoType)aType jid:(XMPPJID *)jid items:(NSArray *)items
|
|
{
|
|
if ((self = [super init]))
|
|
{
|
|
type = aType;
|
|
blockingXMPPJID = [jid copy];
|
|
blockingListItems = [items copy];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)cancel
|
|
{
|
|
if (timer)
|
|
{
|
|
dispatch_source_cancel(timer);
|
|
#if !OS_OBJECT_USE_OBJC
|
|
dispatch_release(timer);
|
|
#endif
|
|
timer = NULL;
|
|
}
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self cancel];
|
|
}
|
|
|
|
+ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type
|
|
{
|
|
return [self queryInfoWithType:type jid:nil items:nil];
|
|
}
|
|
|
|
+ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid
|
|
{
|
|
return [self queryInfoWithType:type jid:jid items:nil];
|
|
}
|
|
|
|
+ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid items:(NSArray *)items
|
|
{
|
|
return [[XMPPBlockingQueryInfo alloc] initWithType:type jid:jid items:items];
|
|
}
|
|
|
|
@end
|