XMPPGoogleSharedStatus.m
13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#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