Merged [15040]: Trying some magic: 5 seconds after the last unreachable host is repor...
[adiumx.git] / Source / AIContactStatusEventsPlugin.m
blob1304e9b19e8645925a7abc3020bf9655eb4ff44c
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIContactController.h"
18 #import "AIContactStatusEventsPlugin.h"
19 #import "ESContactAlertsController.h"
20 #import <AIUtilities/ESImageAdditions.h>
21 #import <Adium/AIAccount.h>
22 #import <Adium/AIListGroup.h>
23 #import <Adium/AIMetaContact.h>
25 @interface AIContactStatusEventsPlugin (PRIVATE)
26 - (BOOL)updateCache:(NSMutableDictionary *)cache forKey:(NSString *)key newValue:(id)newStatus listObject:(AIListObject *)inObject performCompare:(BOOL)performCompare;
27 @end
29 /*!
30  * @class AIContactStatusEventsPlugin
31  * @brief Component to provide events for contact status changes (online, offline, away, idle, etc.)
32  */
33 @implementation AIContactStatusEventsPlugin
35 /*!
36  * @brief Install
37  */
38 - (void)installPlugin
40         //
41     onlineCache = [[NSMutableDictionary alloc] init];
42     awayCache = [[NSMutableDictionary alloc] init];
43     idleCache = [[NSMutableDictionary alloc] init];
44         statusMessageCache = [[NSMutableDictionary alloc] init];
45         
46         //Register the events we generate
47         [[adium contactAlertsController] registerEventID:CONTACT_STATUS_ONLINE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
48         [[adium contactAlertsController] registerEventID:CONTACT_STATUS_ONLINE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
49         [[adium contactAlertsController] registerEventID:CONTACT_STATUS_AWAY_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
50         [[adium contactAlertsController] registerEventID:CONTACT_STATUS_AWAY_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
51         [[adium contactAlertsController] registerEventID:CONTACT_STATUS_IDLE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
52         [[adium contactAlertsController] registerEventID:CONTACT_STATUS_IDLE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
53         [[adium contactAlertsController] registerEventID:CONTACT_SEEN_ONLINE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
54         [[adium contactAlertsController] registerEventID:CONTACT_SEEN_ONLINE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
55         
56         //Observe status changes
57     [[adium contactController] registerListObjectObserver:self];
60 - (void)uninstallPlugin
62         [[adium contactController] unregisterListObjectObserver:self];
65 /*!
66  * @brief Short description
67  * @result A short localized description of the passed event
68  */
69 - (NSString *)shortDescriptionForEventID:(NSString *)eventID
71         NSString        *description;
72         
73         if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
74                 description = AILocalizedString(@"Signs on",nil);
75         }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
76                 description = AILocalizedString(@"Signs off",nil);
77         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
78                 description = AILocalizedString(@"Goes away",nil);
79         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
80                 description = AILocalizedString(@"Returns from away",nil);
81         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
82                 description = AILocalizedString(@"Becomes idle",nil);
83         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
84                 description = AILocalizedString(@"Returns from idle",nil);
85         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
86                 description = AILocalizedString(@"Is seen",nil);
87         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
88                 description = AILocalizedString(@"Is no longer seen",nil);
89         }else{
90                 description = @"";
91         }
92         
93         return(description);
96 /*!
97  * @brief Global short description for an event
98  */
99 - (NSString *)globalShortDescriptionForEventID:(NSString *)eventID
101         NSString        *description;
102         
103         if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
104                 description = AILocalizedString(@"Contact signs on",nil);
105         }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
106                 description = AILocalizedString(@"Contact signs off",nil);
107         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
108                 description = AILocalizedString(@"Contact goes away",nil);
109         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
110                 description = AILocalizedString(@"Contact returns from away",nil);
111         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
112                 description = AILocalizedString(@"Contact becomes idle",nil);
113         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
114                 description = AILocalizedString(@"Contact returns from idle",nil);
115         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
116                 description = AILocalizedString(@"Contact is seen",nil);
117         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
118                 description = AILocalizedString(@"Contact is no longer seen",nil);
119         }else{
120                 description = @"";
121         }
122         
123         return(description);
127  * @brief English, non-translated global short description for an event
129  * This exists because old X(tras) relied upon matching the description of event IDs, and I don't feel like making
130  * a converter for old packs.  If anyone wants to fix this situation, please feel free :)
132  * @result English global short description which should only be used internally
133  */
134 - (NSString *)englishGlobalShortDescriptionForEventID:(NSString *)eventID
136         NSString        *description;
137         
138         if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
139                 description = @"Contact Signed On";
140         }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
141                 description = @"Contact Signed Off";
142         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
143                 description = @"Contact Went Away";
144         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
145                 description = @"Contact Returned from Away";
146         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
147                 description = @"Contact Went Idle";
148         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
149                 description = @"Contact Returned from Idle";
150         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
151                 description = @"Contact is seen";
152         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
153                 description = @"Contact is no longer seen";
154         }else{
155                 description = @"";      
156         }
157         
158         return(description);
162  * @brief Long description for an event
163  */
164 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
166         NSString        *format;
167         NSString        *name;
168         
169         if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
170                 format = AILocalizedString(@"When %@ connects",nil);
171         }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
172                 format = AILocalizedString(@"When %@ disconnects",nil);
173         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
174                 format = AILocalizedString(@"When %@ goes away",nil);
175         }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
176                 format = AILocalizedString(@"When %@ returns from away",nil);
177         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
178                 format = AILocalizedString(@"When %@ goes idle",nil);
179         }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
180                 format = AILocalizedString(@"When %@ returns from idle",nil);
181         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
182                 format = AILocalizedString(@"When you see %@",nil);
183         }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
184                 format = AILocalizedString(@"When you no longer see %@",nil);
185         }else{
186                 format = @"";
187         }
188         
189         if(listObject){
190                 name = ([listObject isKindOfClass:[AIListGroup class]] ?
191                                 [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
192                                 [listObject displayName]);
193         }else{
194                 name = AILocalizedString(@"a contact",nil);
195         }
197         return([NSString stringWithFormat:format,name]);
201  * @brief Natural language description for an event
203  * @param eventID The event identifier
204  * @param listObject The listObject triggering the event
205  * @param userInfo Event-specific userInfo
206  * @param includeSubject If YES, return a full sentence.  If not, return a fragment.
207  * @result The natural language description.
208  */
209 - (NSString *)naturalLanguageDescriptionForEventID:(NSString *)eventID
210                                                                                 listObject:(AIListObject *)listObject
211                                                                                   userInfo:(id)userInfo
212                                                                         includeSubject:(BOOL)includeSubject
214         NSString        *description = nil;
215         
216         if(includeSubject){
217                 NSString        *format = nil;
218                 
219                 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
220                         format = AILocalizedString(@"%@ connected",nil);
221                 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
222                         format = AILocalizedString(@"%@ disconnected",nil);
223                 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
224                         format = AILocalizedString(@"%@ went away",nil);
225                 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
226                         format = AILocalizedString(@"%@ came back",nil);
227                 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
228                         format = AILocalizedString(@"%@ went idle",nil);
229                 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
230                         format = AILocalizedString(@"%@ became active",nil);
231                 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
232                         format = AILocalizedString(@"%@ is seen",nil);
233                 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
234                         format = AILocalizedString(@"%@ is no longer seen",nil);
235                 }
236                 
237                 if(format){
238                         description = [NSString stringWithFormat:format,[listObject displayName]];
239                 }
240         }else{
241                 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
242                         description = AILocalizedString(@"connected",nil);
243                 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
244                         description = AILocalizedString(@"disconnected",nil);
245                 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
246                         description = AILocalizedString(@"went away",nil);
247                 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
248                         description = AILocalizedString(@"came back",nil);
249                 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
250                         description = AILocalizedString(@"went idle",nil);
251                 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
252                         description = AILocalizedString(@"became active",nil);
253                 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
254                         description = AILocalizedString(@"is seen",nil);
255                 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
256                         description = AILocalizedString(@"is no longer seen",nil);
257                 }
258         }
259         
260         return(description);
263 - (NSImage *)imageForEventID:(NSString *)eventID
265         static NSImage  *eventImage = nil;
266         if(!eventImage) eventImage = [[NSImage imageNamed:@"DefaultIcon" forClass:[self class]] retain];
267         return eventImage;
270 #pragma mark Caching and event generation
272  * @brief Cache list object updates
274  * We cache list object updates so we can avoid generating the same event for the same contact on two accounts
275  * or for multiple identical changes within a metaContact.
276  */
277 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
279         /* Ignore accounts.
280          * Ignore meta contact children since the actual meta contact provides a better event. */
281         if((![inObject isKindOfClass:[AIAccount class]]) &&             //Ignore accounts
282            (![[inObject containingObject] isKindOfClass:[AIMetaContact class]])){       
283                 
284                 if([inModifiedKeys containsObject:@"Online"]){
285                         id newValue = [inObject numberStatusObjectForKey:@"Online" fromAnyContainedObject:NO];
286                         if([self updateCache:onlineCache
287                                                   forKey:@"Online"
288                                                 newValue:newValue
289                                           listObject:inObject
290                                   performCompare:YES]){
291                                 if(!silent){
292                                         NSString        *event = ([newValue boolValue] ? CONTACT_STATUS_ONLINE_YES : CONTACT_STATUS_ONLINE_NO);
293                                         [[adium contactAlertsController] generateEvent:event
294                                                                                                          forListObject:inObject
295                                                                                                                   userInfo:nil
296                                                                           previouslyPerformedActionIDs:nil];
297                                 }
298                                                                         
299                                 NSString        *event = ([newValue boolValue] ? CONTACT_SEEN_ONLINE_YES : CONTACT_SEEN_ONLINE_NO);
300                                 [[adium contactAlertsController] generateEvent:event
301                                                                                                  forListObject:inObject
302                                                                                                           userInfo:nil
303                                                                   previouslyPerformedActionIDs:nil];
304                         }
305                 }
306                 
307                 /* Events which are irrelevent if the contact is not online - these changes occur when we are
308                  * just doing bookkeeping e.g. an away contact signs off, we clear the away flag, but they didn't actually
309                  * come back from away. */
310                 if ([[inObject numberStatusObjectForKey:@"Online"] boolValue]){
311                         if([inModifiedKeys containsObject:@"StatusMessage"] || [inModifiedKeys containsObject:@"StatusType"]){
312                                 NSNumber        *newAwayNumber;
313                                 NSString        *newStatusMessage;
314                                 BOOL            awayChanged, statusMessageChanged;
315                                 NSSet           *previouslyPerformedActionIDs = nil;
317                                 //Update away/not-away
318                                 newAwayNumber = [NSNumber numberWithBool:([inObject statusType] == AIAwayStatusType)];
319                                 
320                                 awayChanged = [self updateCache:awayCache
321                                                                                  forKey:@"Away"
322                                                                            newValue:newAwayNumber
323                                                                          listObject:inObject
324                                                                  performCompare:YES];
325                                 
326                                 //Update status message
327                                 newStatusMessage = [[inObject statusMessage] string];
328                                 statusMessageChanged = [self updateCache:statusMessageCache 
329                                                                                                   forKey:@"StatusMessage"
330                                                                                                 newValue:newStatusMessage
331                                                                                           listObject:inObject
332                                                                                   performCompare:YES];
333                                 
334                                 if(statusMessageChanged && !silent){
335                                         if (newStatusMessage != nil){
336                                                 //Evan: Not yet a contact alert, but we use the notification - how could/should we use this?
337                                                 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTACT_STATUS_MESSAGE
338                                                                                                                                                                                 forListObject:inObject
339                                                                                                                                                                                          userInfo:nil
340                                                                                                                                                  previouslyPerformedActionIDs:nil];
341                                         }
342                                 }
343                                 
344                                 //Don't repeat notifications for the away change which the status message already covered
345                                 if(awayChanged && !silent){
346                                         NSString                *event = ([newAwayNumber boolValue] ? CONTACT_STATUS_AWAY_YES : CONTACT_STATUS_AWAY_NO);
347                                         NSDictionary    *userInfo;
348                                         
349                                         userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:(statusMessageChanged && (newStatusMessage != nil))]
350                                                                                                                    forKey:@"Already Posted StatusMessage"];
352                                         [[adium contactAlertsController] generateEvent:event
353                                                                                                          forListObject:inObject
354                                                                                                                   userInfo:userInfo
355                                                                           previouslyPerformedActionIDs:previouslyPerformedActionIDs];
356                                 }
357                         }
359                         if([inModifiedKeys containsObject:@"IsIdle"]){
360                                 id newValue = [inObject numberStatusObjectForKey:@"IsIdle" fromAnyContainedObject:NO];
361                                 if([self updateCache:idleCache
362                                                           forKey:@"IsIdle"
363                                                         newValue:newValue
364                                                   listObject:inObject
365                                           performCompare:YES] && !silent){
366                                         NSString        *event = ([newValue boolValue] ? CONTACT_STATUS_IDLE_YES : CONTACT_STATUS_IDLE_NO);
367                                         [[adium contactAlertsController] generateEvent:event
368                                                                                                          forListObject:inObject
369                                                                                                                   userInfo:nil
370                                                                           previouslyPerformedActionIDs:nil];
371                                 }
372                         }
373                 }
374         }
376         return(nil);    
380  * @brief Update the cache
382  * @param cache The cache
383  * @param key The key
384  * @param newStatus The new value
385  * @param inObject The list object
386  * @param performCompare If NO, we are only concerned about whether any object exists. If YES, a change from one value to another means we've updated.
388  * @result YES if the cache changed; NO if it remained the same (event has already occurred on another associated contact)
389  */
390 - (BOOL)updateCache:(NSMutableDictionary *)cache forKey:(NSString *)key newValue:(id)newStatus listObject:(AIListObject *)inObject performCompare:(BOOL)performCompare
392         id              oldStatus = [cache objectForKey:[inObject internalObjectID]];
393         if((newStatus && !oldStatus) ||
394            (oldStatus && !newStatus) ||
395            ((performCompare && newStatus && oldStatus && ![newStatus performSelector:@selector(compare:) withObject:oldStatus] == 0))){
396                 
397                 if (newStatus){
398                         [cache setObject:newStatus forKey:[inObject internalObjectID]];
399                 }else{
400                         [cache removeObjectForKey:[inObject internalObjectID]];
401                 }
402                 return(YES);
403         }else{
404                 return(NO);
405         }
408 @end