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

+124
View File
@@ -0,0 +1,124 @@
#import "XMPP.h"
#import "XMPPTime.h"
#define _XMPP_AUTO_TIME_H
@class XMPPJID;
/**
* The XMPPAutoTime module monitors the time difference between our machine and the target.
* The target may simply be the server, or a specific resource.
*
* The module works by sending time queries to the target, and tracking the responses.
* The module will automatically send multiple queuries, and take into account the average RTT.
* It will also automatically update itself on a customizable interval, and whenever the machine's clock changes.
*
* This module is helpful when you are using timestamps from the target.
* For example, you may be receiving offline messages from your server.
* However, all these offline messages are timestamped from the server's clock.
* And the current machine's clock may vary considerably from the server's clock.
* Timezone differences don't matter as UTC is always used in XMPP, but clocks can easily differ.
* This may cause the user some confusion as server timestamps may reflect a time in the future,
* or much longer ago than in reality.
**/
@interface XMPPAutoTime : XMPPModule
{
NSTimeInterval recalibrationInterval;
XMPPJID *targetJID;
NSTimeInterval timeDifference;
dispatch_time_t lastCalibrationTime;
dispatch_source_t recalibrationTimer;
BOOL awaitingQueryResponse;
XMPPTime *xmppTime;
NSData *lastServerAddress;
NSDate *systemUptimeChecked;
NSTimeInterval systemUptime;
}
/**
* How often to recalibrate the time difference.
*
* The module will automatically calculate the time difference when it is activated,
* or when it first sees the xmppStream become authenticated (whichever occurs first).
* After that first calculation, it will update itself according to this interval.
*
* To temporarily disable recalibration, set the interval to zero.
*
* The default recalibrationInterval is 24 hours.
**/
@property (readwrite) NSTimeInterval recalibrationInterval;
/**
* The target to query.
*
* If the targetJID is nil, this implies the target is the xmpp server we're connected to.
* If the targetJID is non-nil, it must be a full JID (user@domain.tld/rsrc).
*
* The default targetJID is nil.
**/
@property (readwrite, strong) XMPPJID *targetJID;
/**
* Returns the calculated time difference between our machine and the target.
*
* This is NOT a reference to the difference in time zones.
* Time zone differences generally shouldn't matter as xmpp standards mandate the use of UTC.
*
* Rather this is the difference between our UTC time, and the remote party's UTC time.
* If the two clocks are not synchronized, then the result represents the approximate difference.
*
* If our clock is earlier than the remote clock, then the value will be negative.
* If our clock is ahead of the remote clock, then the value will be positive.
*
* If you later receive a timestamp from the remote party, you can simply add the diff.
* For example:
*
* myTime = [givenTimeFromRemoteParty dateByAddingTimeInterval:diff];
**/
@property (readonly) NSTimeInterval timeDifference;
/**
* Returns the date of the target based on the time difference.
**/
@property (readonly) NSDate *date;
/**
* The last time we've completed a calibration.
**/
@property (readonly) dispatch_time_t lastCalibrationTime;
/**
* XMPPAutoTime is used to automatically query a target for its time (and calculate the difference).
* Sometimes the target is also sending time requests to us as well.
* If so, you may optionally set respondsToQueries to YES to allow the module to respond to incoming time queries.
*
* If you create multiple instances of XMPPAutoTime or XMPPTime,
* then only one instance should respond to queries.
*
* The default value is NO.
**/
@property (readwrite) BOOL respondsToQueries;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@protocol XMPPAutoTimeDelegate
@optional
- (void)xmppAutoTime:(XMPPAutoTime *)sender didUpdateTimeDifference:(NSTimeInterval)timeDifference;
@end
@interface XMPPStream (XMPPAutoTime)
- (NSTimeInterval)xmppAutoTime_timeDifferenceForTargetJID:(XMPPJID *)targetJID;
- (NSDate *)xmppAutoTime_dateForTargetJID:(XMPPJID *)targetJID;
@end
+488
View File
@@ -0,0 +1,488 @@
#import "XMPPAutoTime.h"
#import "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
// 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
@interface XMPPAutoTime ()
@property (nonatomic, strong) NSData *lastServerAddress;
@property (nonatomic, strong) NSDate *systemUptimeChecked;
- (void)updateRecalibrationTimer;
- (void)startRecalibrationTimer;
- (void)stopRecalibrationTimer;
@end
#pragma mark -
@implementation XMPPAutoTime
- (id)initWithDispatchQueue:(dispatch_queue_t)queue
{
if ((self = [super initWithDispatchQueue:queue]))
{
recalibrationInterval = (60 * 60 * 24);
lastCalibrationTime = DISPATCH_TIME_FOREVER;
xmppTime = [[XMPPTime alloc] initWithDispatchQueue:queue];
xmppTime.respondsToQueries = NO;
[xmppTime addDelegate:self delegateQueue:moduleQueue];
}
return self;
}
- (BOOL)activate:(XMPPStream *)aXmppStream
{
if ([super activate:aXmppStream])
{
[xmppTime activate:aXmppStream];
self.systemUptimeChecked = [NSDate date];
systemUptime = [[NSProcessInfo processInfo] systemUptime];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(systemClockDidChange:)
name:NSSystemClockDidChangeNotification
object:nil];
return YES;
}
return NO;
}
- (void)deactivate
{
dispatch_block_t block = ^{ @autoreleasepool {
[self stopRecalibrationTimer];
[xmppTime deactivate];
awaitingQueryResponse = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super deactivate];
}};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_sync(moduleQueue, block);
}
- (void)dealloc
{
// recalibrationTimer released in [self deactivate]
[xmppTime removeDelegate:self];
xmppTime = nil; // Might be referenced via [super dealloc] -> [self deactivate]
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@synthesize lastServerAddress;
@synthesize systemUptimeChecked;
- (NSTimeInterval)recalibrationInterval
{
if (dispatch_get_specific(moduleQueueTag))
{
return recalibrationInterval;
}
else
{
__block NSTimeInterval result;
dispatch_sync(moduleQueue, ^{
result = recalibrationInterval;
});
return result;
}
}
- (void)setRecalibrationInterval:(NSTimeInterval)interval
{
dispatch_block_t block = ^{
if (recalibrationInterval != interval)
{
recalibrationInterval = interval;
// Update the recalibrationTimer.
//
// Depending on new value and current state of the recalibrationTimer,
// this may mean starting, stoping, or simply updating the timer.
if (recalibrationInterval > 0)
{
// Remember: Only start the timer after the xmpp stream is up and authenticated
if ([xmppStream isAuthenticated])
[self startRecalibrationTimer];
}
else
{
[self stopRecalibrationTimer];
}
}
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (XMPPJID *)targetJID
{
if (dispatch_get_specific(moduleQueueTag))
{
return targetJID;
}
else
{
__block XMPPJID *result;
dispatch_sync(moduleQueue, ^{
result = targetJID;
});
return result;
}
}
- (void)setTargetJID:(XMPPJID *)jid
{
dispatch_block_t block = ^{
if (![targetJID isEqualToJID:jid])
{
targetJID = jid;
}
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (NSTimeInterval)timeDifference
{
if (dispatch_get_specific(moduleQueueTag))
{
return timeDifference;
}
else
{
__block NSTimeInterval result;
dispatch_sync(moduleQueue, ^{
result = timeDifference;
});
return result;
}
}
- (NSDate *)date
{
if (dispatch_get_specific(moduleQueueTag))
{
return [[NSDate date] dateByAddingTimeInterval:-timeDifference];
}
else
{
__block NSDate *result;
dispatch_sync(moduleQueue, ^{
result = [[NSDate date] dateByAddingTimeInterval:-timeDifference];
});
return result;
}
}
- (dispatch_time_t)lastCalibrationTime
{
if (dispatch_get_specific(moduleQueueTag))
{
return lastCalibrationTime;
}
else
{
__block dispatch_time_t result;
dispatch_sync(moduleQueue, ^{
result = lastCalibrationTime;
});
return result;
}
}
- (BOOL)respondsToQueries
{
return xmppTime.respondsToQueries;
}
- (void)setRespondsToQueries:(BOOL)respondsToQueries
{
xmppTime.respondsToQueries = respondsToQueries;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Notifications
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)systemClockDidChange:(NSNotification *)notification
{
XMPPLogTrace();
XMPPLogVerbose(@"NSSystemClockDidChangeNotification: %@", notification);
if (lastCalibrationTime == DISPATCH_TIME_FOREVER)
{
// Doesn't matter, we haven't done a calibration yet.
return;
}
// When the system clock changes, this affects our timeDifference.
// However, the notification doesn't tell us by how much the system clock has changed.
// So here's how we figure it out:
//
// The systemUptime isn't affected by the system clock.
// We previously recorded the system uptime, and simultaneously recoded the system clock time.
// We can now grab the current system uptime and current system clock time.
// Using the four data points we can calculate how much the system clock has changed.
NSDate *now = [NSDate date];
NSTimeInterval sysUptime = [[NSProcessInfo processInfo] systemUptime];
dispatch_async(moduleQueue, ^{ @autoreleasepool {
// Calculate system clock change
NSDate *oldSysTime = systemUptimeChecked;
NSDate *newSysTime = now;
NSTimeInterval oldSysUptime = systemUptime;
NSTimeInterval newSysUptime = sysUptime;
NSTimeInterval sysTimeDiff = [newSysTime timeIntervalSinceDate:oldSysTime];
NSTimeInterval sysUptimeDiff = newSysUptime - oldSysUptime;
NSTimeInterval sysClockChange = sysTimeDiff - sysUptimeDiff;
// Modify timeDifference & notify delegate
timeDifference += sysClockChange;
[multicastDelegate xmppAutoTime:self didUpdateTimeDifference:timeDifference];
// Dont forget to update our variables
self.systemUptimeChecked = now;
systemUptime = sysUptime;
}});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Recalibration Timer
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)handleRecalibrationTimerFire
{
XMPPLogTrace();
if (awaitingQueryResponse) return;
awaitingQueryResponse = YES;
if (targetJID)
[xmppTime sendQueryToJID:targetJID];
else
[xmppTime sendQueryToServer];
}
- (void)updateRecalibrationTimer
{
XMPPLogTrace();
NSAssert(recalibrationTimer != NULL, @"Broken logic (1)");
NSAssert(recalibrationInterval > 0, @"Broken logic (2)");
uint64_t interval = (recalibrationInterval * NSEC_PER_SEC);
dispatch_time_t tt;
if (lastCalibrationTime == DISPATCH_TIME_FOREVER)
tt = dispatch_time(DISPATCH_TIME_NOW, 0); // First timer fire at (NOW)
else
tt = dispatch_time(lastCalibrationTime, interval); // First timer fire at (lastCalibrationTime + interval)
dispatch_source_set_timer(recalibrationTimer, tt, interval, 0);
}
- (void)startRecalibrationTimer
{
XMPPLogTrace();
if (recalibrationInterval <= 0)
{
// Timer is disabled
return;
}
BOOL newTimer = NO;
if (recalibrationTimer == NULL)
{
newTimer = YES;
recalibrationTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);
dispatch_source_set_event_handler(recalibrationTimer, ^{ @autoreleasepool {
[self handleRecalibrationTimerFire];
}});
}
[self updateRecalibrationTimer];
if (newTimer)
{
dispatch_resume(recalibrationTimer);
}
}
- (void)stopRecalibrationTimer
{
XMPPLogTrace();
if (recalibrationTimer)
{
#if !OS_OBJECT_USE_OBJC
dispatch_release(recalibrationTimer);
#endif
recalibrationTimer = NULL;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPTime Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)xmppTime:(XMPPTime *)sender didReceiveResponse:(XMPPIQ *)iq withRTT:(NSTimeInterval)rtt
{
XMPPLogTrace();
awaitingQueryResponse = NO;
lastCalibrationTime = dispatch_time(DISPATCH_TIME_NOW, 0);
timeDifference = [XMPPTime approximateTimeDifferenceFromResponse:iq andRTT:rtt];
[multicastDelegate xmppAutoTime:self didUpdateTimeDifference:timeDifference];
}
- (void)xmppTime:(XMPPTime *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout
{
XMPPLogTrace();
awaitingQueryResponse = NO;
// Nothing to do here really. Most likely the server doesn't support XEP-0202.
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark XMPPStream Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket
{
NSData *currentServerAddress = [socket connectedAddress];
if (lastServerAddress == nil)
{
self.lastServerAddress = currentServerAddress;
}
else if (![lastServerAddress isEqualToData:currentServerAddress])
{
XMPPLogInfo(@"%@: Connected to a different server. Resetting calibration info.", [self class]);
lastCalibrationTime = DISPATCH_TIME_FOREVER;
timeDifference = 0.0;
self.lastServerAddress = currentServerAddress;
}
}
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
[self startRecalibrationTimer];
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
[self stopRecalibrationTimer];
awaitingQueryResponse = NO;
// We do NOT reset the lastCalibrationTime here.
// If we reconnect to the same server, the lastCalibrationTime remains valid.
}
@end
@implementation XMPPStream (XMPPAutoTime)
- (NSTimeInterval)xmppAutoTime_timeDifferenceForTargetJID:(XMPPJID *)targetJID
{
__block NSTimeInterval timeDifference = 0.0;
[self enumerateModulesOfClass:[XMPPAutoTime class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) {
XMPPAutoTime *autoTime = (XMPPAutoTime *)module;
if([targetJID isEqualToJID:autoTime.targetJID] || (!targetJID && !autoTime.targetJID))
{
timeDifference = autoTime.timeDifference;
*stop = YES;
}
}];
return timeDifference;
}
- (NSDate *)xmppAutoTime_dateForTargetJID:(XMPPJID *)targetJID
{
__block NSDate *date = [NSDate date];
[self enumerateModulesOfClass:[XMPPAutoTime class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) {
XMPPAutoTime *autoTime = (XMPPAutoTime *)module;
if([targetJID isEqualToJID:autoTime.targetJID] || (!targetJID && !autoTime.targetJID))
{
date = autoTime.date;
*stop = YES;
}
}];
return date;
}
@end
+90
View File
@@ -0,0 +1,90 @@
#import <Foundation/Foundation.h>
#import "XMPP.h"
#define _XMPP_TIME_H
@class XMPPIDTracker;
@protocol XMPPTimeDelegate;
@interface XMPPTime : XMPPModule
{
BOOL respondsToQueries;
XMPPIDTracker *queryTracker;
}
/**
* Whether or not the module should respond to incoming time queries.
* It 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 (readwrite) BOOL respondsToQueries;
/**
* Send query to the server or a specific JID.
* The disco module may be used to detect if the target supports this XEP.
*
* The returned string is the queryID (the elementID of the query that was sent).
* In other words:
*
* SEND: <iq id="<returned_string>" type="get" .../>
* RECV: <iq id="<returned_string>" type="result" .../>
*
* This may be helpful if you are sending multiple simultaneous queries to the same target.
**/
- (NSString *)sendQueryToServer;
- (NSString *)sendQueryToServerWithTimeout:(NSTimeInterval)timeout;
- (NSString *)sendQueryToJID:(XMPPJID *)jid;
- (NSString *)sendQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout;
/**
* Extracts the utc date from the given response/time element,
* and returns an NSDate representation of the time in the local time zone.
* Since the returned date is in the local time zone, it is suitable for presentation.
**/
+ (NSDate *)dateFromResponse:(XMPPIQ *)iq;
/**
* Extracts the time zone offset from the given response/time element.
**/
+ (NSTimeZone *)timeZoneOffsetFromResponse:(XMPPIQ *)iq;
/**
* Given the returned time response from a remote party, and the approximate round trip time,
* calculates the difference between our clock and the remote party's clock.
*
* This is NOT a reference to the difference in time zones.
* Time zone differences generally shouldn't matter as xmpp standards mandate the use of UTC.
*
* Rather this is the difference between our UTC time, and the remote party's UTC time.
* If the two clocks are not synchronized, then the result represents the approximate difference.
*
* If our clock is earlier than the remote clock, then the result will be negative.
* If our clock is ahead of the remote clock, then the result will be positive.
*
* If you later receive a timestamp from the remote party, you could add the diff.
* For example:
*
* myTime = [givenTimeFromRemoteParty dateByAddingTimeInterval:diff];
**/
+ (NSTimeInterval)approximateTimeDifferenceFromResponse:(XMPPIQ *)iq andRTT:(NSTimeInterval)rtt;
/**
* Creates and returns a time element.
**/
+ (NSXMLElement *)timeElement;
+ (NSXMLElement *)timeElementFromDate:(NSDate *)date;
@end
@protocol XMPPTimeDelegate
@optional
- (void)xmppTime:(XMPPTime *)sender didReceiveResponse:(XMPPIQ *)iq withRTT:(NSTimeInterval)rtt;
- (void)xmppTime:(XMPPTime *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout;
// Note: If the xmpp stream is disconnected, no delegate methods will be called, and outstanding queries are forgotten.
@end
+505
View File
@@ -0,0 +1,505 @@
#import "XMPPTime.h"
#import "XMPPIDTracker.h"
#import "XMPPDateTimeProfiles.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 DEFAULT_TIMEOUT 30.0 // seconds
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@interface XMPPTimeQueryInfo : XMPPBasicTrackingInfo
{
NSDate *timeSent;
}
@property (nonatomic, readonly) NSDate *timeSent;
- (NSTimeInterval)rtt;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPTime
- (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)flag
{
dispatch_block_t block = ^{
if (respondsToQueries != flag)
{
respondsToQueries = flag;
#ifdef _XMPP_CAPABILITIES_H
@autoreleasepool {
// Capabilities may have changed, need to notify others.
[xmppStream resendMyPresence];
}
#endif
}
};
if (dispatch_get_specific(moduleQueueTag))
block();
else
dispatch_async(moduleQueue, block);
}
- (NSString *)generateQueryIDWithTimeout:(NSTimeInterval)timeout
{
// This method may be invoked on any thread/queue.
// Generate unique ID for query.
// It's important the ID be unique as the ID is the
// only thing that distinguishes multiple queries from each other.
NSString *queryID = [xmppStream generateUUID];
dispatch_async(moduleQueue, ^{ @autoreleasepool {
XMPPTimeQueryInfo *queryInfo = [[XMPPTimeQueryInfo alloc] initWithTarget:self
selector:@selector(handleResponse:withInfo:)
timeout:timeout];
[queryTracker addID:queryID trackingInfo:queryInfo];
}});
return queryID;
}
- (NSString *)sendQueryToServer
{
// This is a public method.
// It may be invoked on any thread/queue.
return [self sendQueryToServerWithTimeout:DEFAULT_TIMEOUT];
}
- (NSString *)sendQueryToServerWithTimeout:(NSTimeInterval)timeout
{
// This is a public method.
// It may be invoked on any thread/queue.
NSString *queryID = [self generateQueryIDWithTimeout:timeout];
// Send ping packet
//
// <iq type="get" to="domain" id="queryID">
// <time xmlns="urn:xmpp:time"/>
// </iq>
//
// Note: Sometimes the to attribute is required. (ejabberd)
NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"];
XMPPJID *domainJID = [[xmppStream myJID] domainJID];
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:domainJID elementID:queryID child:time];
[xmppStream sendElement:iq];
return queryID;
}
- (NSString *)sendQueryToJID:(XMPPJID *)jid
{
// This is a public method.
// It may be invoked on any thread/queue.
return [self sendQueryToJID:jid withTimeout:DEFAULT_TIMEOUT];
}
- (NSString *)sendQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout
{
// This is a public method.
// It may be invoked on any thread/queue.
NSString *queryID = [self generateQueryIDWithTimeout:timeout];
// Send ping element
//
// <iq type="get" to="fullJID" id="abc123">
// <time xmlns="urn:xmpp:time"/>
// </iq>
NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"];
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:queryID child:time];
[xmppStream sendElement:iq];
return queryID;
}
- (void)handleResponse:(XMPPIQ *)iq withInfo:(XMPPTimeQueryInfo *)queryInfo
{
if (iq)
{
if ([[iq type] isEqualToString:@"result"])
[multicastDelegate xmppTime:self didReceiveResponse:iq withRTT:[queryInfo rtt]];
else
[multicastDelegate xmppTime:self didNotReceiveResponse:[queryInfo elementID] dueToTimeout:-1.0];
}
else
{
[multicastDelegate xmppTime:self didNotReceiveResponse:[queryInfo elementID] dueToTimeout:[queryInfo timeout]];
}
}
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
{
// This method is invoked on the moduleQueue.
NSString *type = [iq type];
if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"])
{
// Examples:
//
// <iq type="result" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
// <time xmlns="urn:xmpp:time">
// <tzo>-06:00</tzo>
// <utc>2006-12-19T17:58:35Z</utc>
// </time>
// </iq>
//
// <iq type="error" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
// <time xmlns="urn:xmpp:time"/>
// <error code="501" type="cancel">
// <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
// </error>
// </iq>
return [queryTracker invokeForID:[iq elementID] withObject:iq];
}
else if (respondsToQueries && [type isEqualToString:@"get"])
{
// Example:
//
// <iq type="get" from="robbie@deusty.com/home" to="robbie@voalte.com/office" id="abc123">
// <time xmlns="urn:xmpp:time"/>
// </iq>
NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"];
if (time)
{
NSXMLElement *currentTime = [[self class] timeElement];
XMPPIQ *response = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]];
[response addChild:currentTime];
[sender sendElement:response];
return YES;
}
}
return NO;
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
[queryTracker removeAllIDs];
}
#ifdef _XMPP_CAPABILITIES_H
/**
* If an XMPPCapabilites instance is used we want to advertise our support for XEP-0202.
**/
- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query
{
// This method is invoked on the moduleQueue.
if (respondsToQueries)
{
// <query xmlns="http://jabber.org/protocol/disco#info">
// ...
// <feature var="urn:xmpp:time"/>
// ...
// </query>
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
[feature addAttributeWithName:@"var" stringValue:@"urn:xmpp:time"];
[query addChild:feature];
}
}
#endif
+ (NSDate *)dateFromResponse:(XMPPIQ *)iq
{
// <iq type="result" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
// <time xmlns="urn:xmpp:time">
// <tzo>-06:00</tzo>
// <utc>2006-12-19T17:58:35Z</utc>
// </time>
// </iq>
NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"];
if (time == nil) return nil;
NSString *utc = [[time elementForName:@"utc"] stringValue];
if (utc == nil) return nil;
// Note:
//
// NSDate is a very simple class, but can be confusing at times.
// NSDate simply stores an NSTimeInterval internally,
// which is just a double representing the number of seconds since the reference date.
// Since it's a double, it can yield sub-millisecond precision.
//
// In addition to this, it stores the values in UTC.
// However, if you print the value using NSLog via "%@",
// it will automatically print the date in the local timezone:
//
// NSDate *refDate = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0];
//
// NSLog(@"%f", [refDate timeIntervalSinceReferenceDate]); // Prints: 0.0
// NSLog(@"%@", refDate); // Prints: 2000-12-31 19:00:00 -05:00
// NSLog(@"%@", [utcDateFormatter stringFromDate:refDate]); // Prints: 2001-01-01 00:00:00 +00:00
//
// Now the value we've received from XMPPDateTimeProfiles is correct.
// If we print it out using a utcDateFormatter we would see it is correct.
// If we printed it out generically using NSLog, then we would see it converted into our local time zone.
return [XMPPDateTimeProfiles parseDateTime:utc];
}
+ (NSTimeZone *)timeZoneOffsetFromResponse:(XMPPIQ *)iq
{
// <iq type="result" from="robbie@voalte.com/office" to="robbie@deusty.com/home" id="abc123">
// <time xmlns="urn:xmpp:time">
// <tzo>-06:00</tzo>
// <utc>2006-12-19T17:58:35Z</utc>
// </time>
// </iq>
NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"];
if (time == nil) return 0;
NSString *tzo = [[time elementForName:@"tzo"] stringValue];
if (tzo == nil) return 0;
return [XMPPDateTimeProfiles parseTimeZoneOffset:tzo];
}
+ (NSTimeInterval)approximateTimeDifferenceFromResponse:(XMPPIQ *)iq andRTT:(NSTimeInterval)rtt
{
// First things first, get the current date and time
NSDate *localDate = [NSDate date];
// Then worry about the calculations
NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"];
if (time == nil) return 0.0;
NSString *utc = [[time elementForName:@"utc"] stringValue];
if (utc == nil) return 0.0;
NSDate *remoteDate = [XMPPDateTimeProfiles parseDateTime:utc];
if (remoteDate == nil) return 0.0;
NSTimeInterval localTI = [localDate timeIntervalSinceReferenceDate];
NSTimeInterval remoteTI = [remoteDate timeIntervalSinceReferenceDate] - (rtt / 2.0);
// Did the response contain millisecond precision?
// This is an important consideration.
// Imagine if both computers are perfectly synced,
// but the remote response doesn't contain milliseconds.
// This could possibly cause us to think the difference is close to a full second.
//
// DateTime examples (from XMPPDateTimeProfiles documentation):
//
// 1969-07-21T02:56:15
// 1969-07-21T02:56:15Z
// 1969-07-20T21:56:15-05:00
// 1969-07-21T02:56:15.123
// 1969-07-21T02:56:15.123Z
// 1969-07-20T21:56:15.123-05:00
BOOL hasMilliseconds = ([utc length] > 19) && ([utc characterAtIndex:19] == '.');
if (hasMilliseconds)
{
return remoteTI - localTI;
}
else
{
// No milliseconds. What to do?
//
// We could simply truncate the milliseconds from our time...
// But this could make things much worse.
// For example:
//
// local = 14:22:36.750
// remote = 14:22:37
//
// If we truncate the result now we calculate a diff of 1.000 (a full second).
// Considering the remote's milliseconds could have been anything from 000 to 999,
// this means our calculations are:
//
// perfect : 0.1% chance
// diff too big : 75.0% chance
// diff too small : 24.9% chance
//
// Perhaps a better solution would give us a more even spread.
// We can do this by calculating the range:
//
// 37.000 - 36.750 = 0.25
// 37.999 - 36.750 = 1.249
//
// So a better guess of the diff is 0.750 (3/4 of a second):
//
// perfect : 0.1% chance
// diff too big : 50.0% chance
// diff too small : 49.9% chance
NSTimeInterval diff1 = localTI - (remoteTI + 0.000);
NSTimeInterval diff2 = localTI - (remoteTI + 0.999);
return ((diff1 + diff2) / 2.0);
}
}
+ (NSXMLElement *)timeElement
{
return [self timeElementFromDate:[NSDate date]];
}
+ (NSXMLElement *)timeElementFromDate:(NSDate *)date
{
// <time xmlns="urn:xmpp:time">
// <tzo>-06:00</tzo>
// <utc>2006-12-19T17:58:35Z</utc>
// </time>
NSDateFormatter *df = [[NSDateFormatter alloc] init];
[df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3)
[df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
[df setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
[df setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
NSString *utcValue = [df stringFromDate:date];
NSInteger tzoInSeconds = [[NSTimeZone systemTimeZone] secondsFromGMTForDate:date];
NSInteger tzoH = tzoInSeconds / (60 * 60);
NSInteger tzoS = tzoInSeconds % (60 * 60);
NSString *tzoValue = [NSString stringWithFormat:@"%+03li:%02li", (long)tzoH, (long)tzoS];
NSXMLElement *tzo = [NSXMLElement elementWithName:@"tzo" stringValue:tzoValue];
NSXMLElement *utc = [NSXMLElement elementWithName:@"utc" stringValue:utcValue];
NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"];
[time addChild:tzo];
[time addChild:utc];
return time;
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation XMPPTimeQueryInfo
@synthesize timeSent;
- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout
{
if ((self = [super initWithTarget:aTarget selector:aSelector timeout:aTimeout]))
{
timeSent = [[NSDate alloc] init];
}
return self;
}
- (NSTimeInterval)rtt
{
return [timeSent timeIntervalSinceNow] * -1.0;
}
@end