370 lines
13 KiB
Objective-C
370 lines
13 KiB
Objective-C
#import "XMPPGoogleSharedStatus.h"
|
|
#import "XMPP.h"
|
|
#import "XMPPFramework.h"
|
|
|
|
#if ! __has_feature(objc_arc)
|
|
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
|
|
#endif
|
|
|
|
#define GOOGLE_SHARED_STATUS @"google:shared-status"
|
|
#define GOOGLE_DISCO_INFO @"http://jabber.org/protocol/disco#info"
|
|
#define GOOGLE_PRESENCE_PRIORITY @"24"
|
|
|
|
// Dictionary keys to access shared status information.
|
|
NSString *const XMPPGoogleSharedStatusShow = @"show";
|
|
NSString *const XMPPGoogleSharedStatusInvisible = @"invisible";
|
|
NSString *const XMPPGoogleSharedStatusStatus = @"status";
|
|
|
|
// Shared status display values. Note that you cannot set
|
|
// a shared status for an idle show.
|
|
NSString *const XMPPGoogleSharedStatusShowAvailable = @"default";
|
|
NSString *const XMPPGoogleSharedStatusShowBusy = @"dnd";
|
|
NSString *const XMPPGoogleSharedStatusShowIdle = @"away";
|
|
|
|
@interface XMPPGoogleSharedStatus () {
|
|
|
|
// Server specified maximum values.
|
|
NSInteger _statusListMaxCount;
|
|
NSInteger _statusMessageMaxLength;
|
|
}
|
|
|
|
@property (nonatomic, copy) NSString *previousShow;
|
|
|
|
@end
|
|
|
|
@implementation XMPPGoogleSharedStatus
|
|
|
|
#pragma mark - Object Lifecycle
|
|
|
|
- (id)initWithDispatchQueue:(dispatch_queue_t)queue {
|
|
if((self = [super initWithDispatchQueue:queue])) {
|
|
self.sharedStatus = [NSDictionary dictionary];
|
|
_status = @"";
|
|
_show = XMPPGoogleSharedStatusShowAvailable;
|
|
_invisible = NO;
|
|
|
|
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
|
self.assumeIdleUpdateResponsibility = YES;
|
|
#endif
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
- (BOOL)activate:(XMPPStream *)aXmppStream
|
|
{
|
|
if ([super activate:aXmppStream])
|
|
{
|
|
|
|
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
|
[xmppStream autoAddDelegate:self
|
|
delegateQueue:moduleQueue
|
|
toModulesOfClass:[XMPPSystemInputActivityMonitor class]];
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)deactivate
|
|
{
|
|
|
|
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
|
[xmppStream removeAutoDelegate:self
|
|
delegateQueue:moduleQueue
|
|
fromModulesOfClass:[XMPPSystemInputActivityMonitor class]];
|
|
#endif
|
|
|
|
[super deactivate];
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Convenience Properties
|
|
|
|
// Convinience single-item setters which forward status setting
|
|
// to the more complex update method below. Use these sparingly,
|
|
// because they have heavy load when used in succession.
|
|
- (void)setStatus:(NSString *)status {
|
|
[self updateSharedStatus:status show:self.show invisible:self.invisible];
|
|
}
|
|
|
|
- (void)setShow:(NSString *)show {
|
|
[self updateSharedStatus:self.status show:show invisible:self.invisible];
|
|
}
|
|
|
|
- (void)setInvisible:(BOOL)invisible {
|
|
[self updateSharedStatus:self.status show:self.show invisible:invisible];
|
|
}
|
|
|
|
#pragma mark - Status Updates
|
|
|
|
// This is a heavyweight method used to update all three attributes,
|
|
// status, show, and invisible, at once. Use this method over the
|
|
// convinience accessors above. Wherever possible, lessen the status
|
|
// update load. This method refuses service if shared status is not
|
|
// supported, removes previous statuses if the list reaches max
|
|
// count, and cuts status strings to the max string length.
|
|
- (void)updateSharedStatus:(NSString *)status show:(NSString *)show invisible:(BOOL)invisible {
|
|
if(!self.sharedStatusSupported) {
|
|
NSLog(@"Google Shared Status Not Supported!");
|
|
return;
|
|
}
|
|
|
|
// Create a mutable copy of shared status information and chop the status.
|
|
NSMutableDictionary *sharedStatusUpdate = [self.sharedStatus mutableCopy];
|
|
if(status.length > _statusMessageMaxLength)
|
|
status = [status substringToIndex:_statusMessageMaxLength];
|
|
|
|
// If the show has changed update it.
|
|
if(![show isEqualToString:sharedStatusUpdate[XMPPGoogleSharedStatusShow]]) {
|
|
[sharedStatusUpdate removeObjectForKey:XMPPGoogleSharedStatusShow];
|
|
sharedStatusUpdate[XMPPGoogleSharedStatusShow] = show;
|
|
}
|
|
|
|
// Get the current show value, and whether the status should update for it.
|
|
BOOL displayShow = ([show isEqualToString:XMPPGoogleSharedStatusShowAvailable] ||
|
|
[show isEqualToString:XMPPGoogleSharedStatusShowBusy]);
|
|
NSString *currentShow = self.sharedStatus[XMPPGoogleSharedStatusShow];
|
|
|
|
// Since we can't show an away status, only change the status it has
|
|
// changed and is not showing idle.
|
|
if(![status isEqualToString:sharedStatusUpdate[XMPPGoogleSharedStatusStatus]] && displayShow) {
|
|
[sharedStatusUpdate removeObjectForKey:XMPPGoogleSharedStatusStatus];
|
|
sharedStatusUpdate[XMPPGoogleSharedStatusStatus] = status;
|
|
|
|
// Update the appropriate status list by adding the new status.
|
|
// If the list max count was reached, truncate its earliest status.
|
|
NSMutableArray *statusList = [sharedStatusUpdate[currentShow] mutableCopy];
|
|
[statusList insertObject:status atIndex:0];
|
|
if(statusList.count > _statusListMaxCount)
|
|
[statusList removeObjectAtIndex:statusList.count-1];
|
|
|
|
// Remove any blank statuses.
|
|
for(int i = 0; i < statusList.count; i++) {
|
|
NSString *status = statusList[i];
|
|
if([status isEqualToString:@""])
|
|
[statusList removeObjectAtIndex:i];
|
|
}
|
|
|
|
// Update the status list for this status.
|
|
[sharedStatusUpdate removeObjectForKey:currentShow];
|
|
sharedStatusUpdate[currentShow] = statusList;
|
|
}
|
|
|
|
// Update the invisibility for this shared status.
|
|
[sharedStatusUpdate removeObjectForKey:XMPPGoogleSharedStatusInvisible];
|
|
sharedStatusUpdate[XMPPGoogleSharedStatusInvisible] = @(invisible);
|
|
|
|
// Wrap it in an XMPPIQ "set" and send it to the server.
|
|
XMPPIQ *statusIQ = [XMPPIQ iqWithType:@"set" to:self.xmppStream.myJID.bareJID];
|
|
[statusIQ addAttributeWithName:@"id" stringValue:self.xmppStream.myJID.resource];
|
|
[statusIQ addChild:[self packSharedStatus:sharedStatusUpdate]];
|
|
[self.xmppStream sendElement:statusIQ];
|
|
|
|
// Now update our local values, toll-free.
|
|
_show = show;
|
|
_status = status;
|
|
_invisible = invisible;
|
|
}
|
|
|
|
#pragma mark - Capability Discovery
|
|
|
|
// Discover and refresh shared status as soon as authentication.
|
|
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
|
|
[self discoverCapabilities];
|
|
[self refreshSharedStatus];
|
|
}
|
|
|
|
// Disable support when the stream disconnects.
|
|
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error {
|
|
self.sharedStatus = [NSDictionary dictionary];
|
|
self.sharedStatusSupported = NO;
|
|
}
|
|
|
|
// First discover capabilities to find google:shared-status. If it exists,
|
|
// we can continue with the shared status support.
|
|
- (void)discoverCapabilities {
|
|
XMPPIQ *discoIQ = [XMPPIQ iqWithType:@"get" to:[XMPPJID jidWithString:@"gmail.com"]];
|
|
[discoIQ addChild:[XMPPElement elementWithName:@"query" xmlns:GOOGLE_DISCO_INFO]];
|
|
[self.xmppStream sendElement:discoIQ];
|
|
}
|
|
|
|
// Manually trigger a refresh cycle for getting shared statuses. This will
|
|
// return an XML stanza containing status-lists and current status info.
|
|
- (void)refreshSharedStatus {
|
|
XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:self.xmppStream.myJID.bareJID];
|
|
[iq addAttributeWithName:@"id" stringValue:self.xmppStream.myJID.resource];
|
|
|
|
XMPPElement *query = (XMPPElement *)[XMPPElement elementWithName:@"query" xmlns:GOOGLE_SHARED_STATUS];
|
|
[query addAttributeWithName:@"version" stringValue:@"2"];
|
|
|
|
[iq addChild:query];
|
|
[self.xmppStream sendElement:iq];
|
|
}
|
|
|
|
// If we received a shared-status IQ and we support it, unpack it into a
|
|
// dictionary and notify the delegates. If it is the first shared-status
|
|
// subscription IQ, it contains the maximum string length and list count
|
|
// information attributes as well, so save those.
|
|
// If we received a discovery IQ, determine shared status support.
|
|
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq {
|
|
XMPPElement *query = (XMPPElement *) [iq children][0];
|
|
if([query.xmlns isEqualToString:GOOGLE_SHARED_STATUS] && self.sharedStatusSupported) {
|
|
self.sharedStatus = [self unpackSharedStatus:query];
|
|
_show = self.sharedStatus[XMPPGoogleSharedStatusShow];
|
|
_status = self.sharedStatus[XMPPGoogleSharedStatusStatus];
|
|
_invisible = [self.sharedStatus[XMPPGoogleSharedStatusInvisible] boolValue];
|
|
[multicastDelegate xmppGoogleSharedStatus:self didReceiveUpdatedStatus:self.sharedStatus];
|
|
|
|
if([query attributeForName:@"status-max"])
|
|
_statusMessageMaxLength = [[query attributeForName:@"status-max"].stringValue integerValue];
|
|
if([query attributeForName:@"status-list-contents-max"])
|
|
_statusListMaxCount = [[query attributeForName:@"status-list-contents-max"].stringValue integerValue];
|
|
|
|
} else if([query.xmlns isEqualToString:GOOGLE_DISCO_INFO]) {
|
|
for(XMPPElement *feature in query.children)
|
|
if([[[feature attributeForName:@"var"] stringValue] isEqualToString:GOOGLE_SHARED_STATUS])
|
|
self.sharedStatusSupported = YES;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Packing & Unpacking
|
|
|
|
// Unpacks the XMPPElement "query" for a shared status and returns
|
|
// a formatted NSDictionary with corresponding arrays and strings.
|
|
// The XMPPElement "query" is a child of a shared status XMPPIQ.
|
|
- (NSDictionary *)unpackSharedStatus:(XMPPElement *)sharedStatus {
|
|
if([sharedStatus.xmlns isEqualToString:GOOGLE_SHARED_STATUS]) {
|
|
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
|
|
|
for(XMPPElement *element in sharedStatus.children) {
|
|
if([element.name isEqualToString:@"status-list"]) {
|
|
NSMutableArray *array = [NSMutableArray array];
|
|
for(XMPPElement *status in element.children)
|
|
[array addObject:[status stringValue]];
|
|
dict[[[element attributeForName:XMPPGoogleSharedStatusShow] stringValue]] = array;
|
|
} else if([element.name isEqualToString:XMPPGoogleSharedStatusStatus]) {
|
|
dict[element.name] = [element stringValue];
|
|
} else if([element.name isEqualToString:XMPPGoogleSharedStatusShow]) {
|
|
dict[element.name] = [element stringValue];
|
|
} else if([element.name isEqualToString:XMPPGoogleSharedStatusInvisible]) {
|
|
dict[element.name] = @([[[element attributeForName:@"value"] stringValue] boolValue]);
|
|
} else {
|
|
NSLog(@"Missed element: %@", element);
|
|
}
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
// Packs a formatted NSDictionary with corresponding arrays and strings
|
|
// back into an XMPPElement "query" for shared status. This must be
|
|
// further wrapped in an XMPPIQ to send a status update.
|
|
- (XMPPElement *)packSharedStatus:(NSDictionary *)sharedStatus {
|
|
XMPPElement *element = (XMPPElement *)[XMPPElement elementWithName:@"query" xmlns:GOOGLE_SHARED_STATUS];
|
|
[element addAttributeWithName:@"version" stringValue:@"2"];
|
|
|
|
[element addChild:[XMPPElement elementWithName:XMPPGoogleSharedStatusStatus
|
|
stringValue:sharedStatus[XMPPGoogleSharedStatusStatus]]];
|
|
[element addChild:[XMPPElement elementWithName:XMPPGoogleSharedStatusShow
|
|
stringValue:sharedStatus[XMPPGoogleSharedStatusShow]]];
|
|
|
|
for(NSString *key in sharedStatus.allKeys.reverseObjectEnumerator) {
|
|
if([key isEqualToString:XMPPGoogleSharedStatusShowAvailable] ||
|
|
[key isEqualToString:XMPPGoogleSharedStatusShowBusy]) {
|
|
|
|
NSArray *statusList = sharedStatus[key];
|
|
XMPPElement *statusElement = [XMPPElement elementWithName:@"status-list"];
|
|
[statusElement addAttributeWithName:@"show" stringValue:key];
|
|
|
|
for(NSString *status in statusList)
|
|
[statusElement addChild:[XMPPElement elementWithName:@"status" stringValue:status]];
|
|
[element addChild:statusElement];
|
|
} else if(!([key isEqualToString:XMPPGoogleSharedStatusInvisible] ||
|
|
[key isEqualToString:XMPPGoogleSharedStatusShow] ||
|
|
[key isEqualToString:XMPPGoogleSharedStatusStatus])) {
|
|
NSLog(@"Invalid element: %@", key);
|
|
}
|
|
}
|
|
|
|
XMPPElement *invisible = [XMPPElement elementWithName:XMPPGoogleSharedStatusInvisible];
|
|
[invisible addAttributeWithName:@"value"
|
|
stringValue:[sharedStatus[XMPPGoogleSharedStatusInvisible] boolValue] ? @"true" : @"false"];
|
|
[element addChild:invisible];
|
|
|
|
return element;
|
|
}
|
|
|
|
#pragma mark - Other Properties
|
|
|
|
- (void)setAssumeIdleUpdateResponsibility:(BOOL)flag {
|
|
_assumeIdleUpdateResponsibility = flag;
|
|
}
|
|
|
|
- (void)setStatusAvailability:(NSString *)statusAvailability {
|
|
_statusAvailability = statusAvailability;
|
|
|
|
XMPPPresence *presence = [XMPPPresence presence];
|
|
[presence addChild:[XMPPElement elementWithName:@"priority" stringValue:GOOGLE_PRESENCE_PRIORITY]];
|
|
XMPPElement *show = [XMPPElement elementWithName:@"show"];
|
|
XMPPElement *status = [XMPPElement elementWithName:@"status"];
|
|
|
|
if(self.statusAvailability) {
|
|
[show setStringValue:self.statusAvailability];
|
|
[presence addChild:show];
|
|
}
|
|
|
|
if(self.statusMessage) {
|
|
[status setStringValue:self.statusMessage];
|
|
[presence addChild:status];
|
|
}
|
|
|
|
[self.xmppStream sendElement:presence];
|
|
}
|
|
|
|
- (void)setStatusMessage:(NSString *)statusMessage {
|
|
_statusMessage = statusMessage;
|
|
|
|
XMPPPresence *presence = [XMPPPresence presence];
|
|
XMPPElement *show = [XMPPElement elementWithName:@"show"];
|
|
XMPPElement *status = [XMPPElement elementWithName:@"status"];
|
|
|
|
if(self.statusAvailability) {
|
|
[show setStringValue:self.statusAvailability];
|
|
[presence addChild:show];
|
|
}
|
|
|
|
if(self.statusMessage) {
|
|
[status setStringValue:self.statusMessage];
|
|
[presence addChild:status];
|
|
}
|
|
|
|
[self.xmppStream sendElement:presence];
|
|
}
|
|
|
|
#ifdef _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H
|
|
|
|
#pragma mark - XMPP System Input Activity Monitor Delegate
|
|
|
|
- (void)xmppSystemInputActivityMonitorDidBecomeActive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor
|
|
{
|
|
self.show = self.previousShow;
|
|
}
|
|
|
|
- (void)xmppSystemInputActivityMonitorDidBecomeInactive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor
|
|
{
|
|
self.previousShow = self.show;
|
|
self.show = XMPPGoogleSharedStatusShowIdle;
|
|
}
|
|
|
|
#endif
|
|
|
|
@end
|