Use initialize, not load, wherever possible.
[adiumx.git] / Source / ESBlockingPlugin.m
blobac4a0fea6033ce5cc4507b5701eaae52d3f32be6
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 <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIContactControllerProtocol.h>
19 #import <Adium/AIMenuControllerProtocol.h>
20 #import <Adium/AIToolbarControllerProtocol.h>
21 #import <Adium/AIInterfaceControllerProtocol.h>
22 #import <Adium/AIChatControllerProtocol.h>
23 #import "ESBlockingPlugin.h"
24 #import <AIUtilities/AIMenuAdditions.h>
25 #import <AIUtilities/AIStringAdditions.h>
26 #import <AIUtilities/AIToolbarUtilities.h>
27 #import <AIUtilities/AIImageAdditions.h>
28 #import <Adium/AIAccount.h>
29 #import <Adium/AIListContact.h>
30 #import <Adium/AIMetaContact.h>
31 #import <Adium/AIChat.h>
33 #define BLOCK                                           AILocalizedString(@"Block","Block Contact menu item")
34 #define UNBLOCK                                         AILocalizedString(@"Unblock","Unblock Contact menu item")
35 #define BLOCK_MENUITEM                          [BLOCK stringByAppendingEllipsis]
36 #define UNBLOCK_MENUITEM                        [UNBLOCK stringByAppendingEllipsis]
37 #define TOOLBAR_ITEM_IDENTIFIER         @"BlockParticipants"
38 #define TOOLBAR_BLOCK_ICON_KEY          @"Block"
39 #define TOOLBAR_UNBLOCK_ICON_KEY        @"Unblock"
41 @interface ESBlockingPlugin(PRIVATE)
42 - (void)_setContact:(AIListContact *)contact isBlocked:(BOOL)isBlocked;
43 - (void)accountConnected:(NSNotification *)notification;
44 - (BOOL)areAllGivenContactsBlocked:(NSArray *)contacts;
45 - (void)setPrivacy:(BOOL)block forContacts:(NSArray *)contacts;
46 - (IBAction)blockOrUnblockParticipants:(NSToolbarItem *)senderItem;
48 //protocols
49 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent;
51 //notifications
52 - (void)chatDidBecomeVisible:(NSNotification *)notification;
53 - (void)toolbarWillAddItem:(NSNotification *)notification;
54 - (void)toolbarDidRemoveItem:(NSNotification *)notification;
56 //toolbar item methods
57 - (void)updateToolbarIconOfChat:(AIChat *)inChat inWindow:(NSWindow *)window;
58 - (void)updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat;
59 - (void)updateToolbarItemForObject:(AIListObject *)inObject;
60 @end
62 #pragma mark -
63 @implementation ESBlockingPlugin
65 - (void)installPlugin
67         //Install the Block menu items
68         blockContactMenuItem = [[NSMenuItem alloc] initWithTitle:BLOCK_MENUITEM
69                                                                                                           target:self
70                                                                                                           action:@selector(blockContact:)
71                                                                                            keyEquivalent:@"b"];
72         
73         [blockContactMenuItem setKeyEquivalentModifierMask:(NSCommandKeyMask|NSAlternateKeyMask)];
74         
75         [[adium menuController] addMenuItem:blockContactMenuItem toLocation:LOC_Contact_NegativeAction];
77     //Add our get info contextual menu items
78     blockContactContextualMenuItem = [[NSMenuItem alloc] initWithTitle:BLOCK_MENUITEM
79                                                                                                                                 target:self
80                                                                                                                                 action:@selector(blockContact:)
81                                                                                                                  keyEquivalent:@""];
82     [[adium menuController] addContextualMenuItem:blockContactContextualMenuItem toLocation:Context_Contact_NegativeAction];
83         
84         //we want to know when an account connects
85         [[adium notificationCenter] addObserver:self
86                                                                    selector:@selector(accountConnected:)
87                                                                            name:ACCOUNT_CONNECTED
88                                                                          object:nil];
89         
90         //create the block toolbar item
91         chatToolbarItems = [[NSMutableSet alloc] init];
92         //cache toolbar icons
93         blockedToolbarIcons = [[NSDictionary alloc] initWithObjectsAndKeys:
94                                                                 [NSImage imageNamed:@"block.png" forClass:[self class]], TOOLBAR_BLOCK_ICON_KEY, 
95                                                                 [NSImage imageNamed:@"unblock.png" forClass:[self class]], TOOLBAR_UNBLOCK_ICON_KEY, 
96                                                                 nil];
97         NSToolbarItem   *chatItem = [AIToolbarUtilities toolbarItemWithIdentifier:TOOLBAR_ITEM_IDENTIFIER
98                                                                                                                                                 label:BLOCK
99                                                                                                                                  paletteLabel:BLOCK
100                                                                                                                                           toolTip:AILocalizedString(@"Blocking prevents a contact from contacting you or seeing your online status.", nil)
101                                                                                                                                            target:self
102                                                                                                                           settingSelector:@selector(setImage:)
103                                                                                                                                   itemContent:[blockedToolbarIcons valueForKey:TOOLBAR_BLOCK_ICON_KEY]
104                                                                                                                                            action:@selector(blockOrUnblockParticipants:)
105                                                                                                                                                  menu:nil];
106         
107         [[adium toolbarController] registerToolbarItem:chatItem forToolbarType:@"MessageWindow"];
108         
109         [[NSNotificationCenter defaultCenter] addObserver:self
110                                                                                          selector:@selector(toolbarWillAddItem:)
111                                                                                                  name:NSToolbarWillAddItemNotification
112                                                                                            object:nil];
113         [[NSNotificationCenter defaultCenter] addObserver:self
114                                                                                          selector:@selector(toolbarDidRemoveItem:)
115                                                                                                  name:NSToolbarDidRemoveItemNotification
116                                                                                            object:nil];
117         [[adium contactController] registerListObjectObserver:self];
120 - (void)uninstallPlugin
122         [[adium notificationCenter] removeObserver:self];
123         [[adium contactController] unregisterListObjectObserver:self];
124         [[NSNotificationCenter defaultCenter] removeObserver:self];
125         [chatToolbarItems release];
126         [blockedToolbarIcons release];
127         [blockContactMenuItem release];
128         [blockContactContextualMenuItem release];
132  * @brief Block or unblock contacts
134  * @param block Flag indicating what the operation should achieve: NO for unblock, YES for block.
135  * @param contacts The contacts to block or unblock
136  */
137 - (void)setPrivacy:(BOOL)block forContacts:(NSArray *)contacts
139         NSEnumerator    *contactEnumerator = [contacts objectEnumerator];
140         AIListContact   *currentContact = nil;
141         
142         while ((currentContact = [contactEnumerator nextObject])) {
143                 if ([currentContact isBlocked] != block) {
144                         [currentContact setIsBlocked:block updateList:YES];
145                 }
146         }
149 - (IBAction)blockContact:(id)sender
151         AIListObject    *object;
152         
153         object = ((sender == blockContactMenuItem) ?
154                           [[adium interfaceController] selectedListObject] :
155                           [[adium menuController] currentContextMenuObject]);
156         
157         //Don't do groups
158         if ([object isKindOfClass:[AIListContact class]]) {
159                 AIListContact   *contact = (AIListContact *)object;
160                 BOOL                    shouldBlock;
161                 NSString                *format;
163                 shouldBlock = [[sender title] isEqualToString:BLOCK_MENUITEM];
164                 format = (shouldBlock ? 
165                                   AILocalizedString(@"Are you sure you want to block %@?",nil) :
166                                   AILocalizedString(@"Are you sure you want to unblock %@?",nil));
168                 if (NSRunAlertPanel([NSString stringWithFormat:format, [contact displayName]],
169                                                         @"",
170                                                         (shouldBlock ? BLOCK : UNBLOCK),
171                                                         AILocalizedString(@"Cancel", nil),
172                                                         nil) == NSAlertDefaultReturn) {
173                         
174                         //Handle metas
175                         if ([object isKindOfClass:[AIMetaContact class]]) {
176                                 AIMetaContact *meta = (AIMetaContact *)object;
177                                                                         
178                                 //Enumerate over the various list contacts contained
179                                 NSEnumerator *enumerator = [[meta listContacts] objectEnumerator];
180                                 AIListContact *containedContact = nil;
181                                 
182                                 while ((containedContact = [enumerator nextObject])) {
183                                         AIAccount <AIAccount_Privacy> *acct = [containedContact account];
184                                         if ([acct conformsToProtocol:@protocol(AIAccount_Privacy)]) {
185                                                 [self _setContact:containedContact isBlocked:shouldBlock];
186                                         } else {
187                                                 NSLog(@"Account %@ does not support blocking (contact %@ not blocked on this account)", acct, containedContact);
188                                         }
189                                 }
190                         } else {
191                                 AIListContact *contact = (AIListContact *)object;
192                                 AIAccount <AIAccount_Privacy> *acct = [contact account];
193                                 if ([acct conformsToProtocol:@protocol(AIAccount_Privacy)]) {
194                                         [self _setContact:contact isBlocked:shouldBlock];
195                                 } else {
196                                         NSLog(@"Account %@ does not support blocking (contact %@ not blocked on this account)", acct, contact);
197                                 }
198                         }
199                         
200                         [[adium notificationCenter] postNotificationName:@"AIPrivacySettingsChangedOutsideOfPrivacyWindow"
201                                                                                                           object:nil];          
202                 }
203         }
206 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
208         AIListObject *object;
209         
210         if (menuItem == blockContactMenuItem) {
211                 object = [[adium interfaceController] selectedListObject];
212         } else {
213                 object = [[adium menuController] currentContextMenuObject];
214         }
215         
216         //Don't do groups
217         if ([object isKindOfClass:[AIListContact class]]) {
218                 //Handle metas
219                 if ([object isKindOfClass:[AIMetaContact class]]) {
220                         AIMetaContact *meta = (AIMetaContact *)object;
221                                                                 
222                         //Enumerate over the various list contacts contained
223                         NSEnumerator    *enumerator = [[meta listContacts] objectEnumerator];
224                         AIListContact   *contact = nil;
225                         int                             votesForBlock = 0;
226                         int                             votesForUnblock = 0;
228                         while ((contact = [enumerator nextObject])) {
229                                 AIAccount <AIAccount_Privacy> *acct = [contact account];
230                                 if ([acct conformsToProtocol:@protocol(AIAccount_Privacy)]) {
231                                         AIPrivacyType privType = (([acct privacyOptions] == AIPrivacyOptionAllowUsers) ? AIPrivacyTypePermit : AIPrivacyTypeDeny);
232                                         if ([[acct listObjectsOnPrivacyList:privType] containsObject:contact]) {
233                                                 switch (privType) {
234                                                         case AIPrivacyTypePermit:
235                                                                 /* He's on a permit list. The action would remove him, blocking him */
236                                                                 votesForBlock++;
237                                                                 break;
238                                                         case AIPrivacyTypeDeny:
239                                                                 /* He's on a deny list. The action would remove him, unblocking him */
240                                                                 votesForUnblock++;
241                                                                 break;
242                                                 }
243                                                 
244                                         } else {
245                                                 switch (privType) {
246                                                         case AIPrivacyTypePermit:
247                                                                 /* He's not on the permit list. The action would add him, unblocking him */
248                                                                 votesForUnblock++;
249                                                                 break;
250                                                         case AIPrivacyTypeDeny:
251                                                                 /* He's not on a deny list. The action would add him, blocking him */
252                                                                 votesForBlock++;
253                                                                 break;
254                                                 }                                               
255                                         }
256                                 }
257                         }
259                         if (votesForBlock || votesForUnblock) {
260                                 if (votesForBlock >= votesForUnblock) {
261                                         [menuItem setTitle:BLOCK_MENUITEM];
262                                 } else {
263                                         [menuItem setTitle:UNBLOCK_MENUITEM];   
264                                 }
265                                 
266                                 return YES;
268                         } else {
269                                 return NO;
270                         }
272                 } else {
273                         AIListContact *contact = (AIListContact *)object;
274                         AIAccount <AIAccount_Privacy> *acct = [contact account];
275                         if ([acct conformsToProtocol:@protocol(AIAccount_Privacy)]) {
276                                 AIPrivacyType privType = (([acct privacyOptions] == AIPrivacyOptionAllowUsers) ? AIPrivacyTypePermit : AIPrivacyTypeDeny);
277                                 if ([[acct listObjectsOnPrivacyList:privType] containsObject:contact]) {
278                                         switch (privType) {
279                                                 case AIPrivacyTypePermit:
280                                                         /* He's on a permit list. The action would remove him, blocking him */
281                                                         [menuItem setTitle:BLOCK_MENUITEM];
282                                                         break;
283                                                 case AIPrivacyTypeDeny:
284                                                         /* He's on a deny list. The action would remove him, unblocking him */
285                                                         [menuItem setTitle:UNBLOCK_MENUITEM];
286                                                         break;
287                                         }
288                                         
289                                 } else {
290                                         switch (privType) {
291                                                 case AIPrivacyTypePermit:
292                                                         /* He's not on the permit list. The action would add him, unblocking him */
293                                                         [menuItem setTitle:UNBLOCK_MENUITEM];
294                                                         break;
295                                                 case AIPrivacyTypeDeny:
296                                                         /* He's not on a deny list. The action would add him, blocking him */
297                                                         [menuItem setTitle:BLOCK_MENUITEM];
298                                                         break;
299                                         }                                               
300                                 }
301                                 
302                                 return YES;
304                         } else {
305                                 return NO;
306                         }
307                 }
308         }
309         return NO;
312 #pragma mark -
313 #pragma mark Private
314 //Private --------------------------------------------------------------------------------------------------------------
316 - (void)_setContact:(AIListContact *)contact isBlocked:(BOOL)isBlocked
318         //We want to block on all accounts with the same service class. If you want someone gone, you want 'em GONE.
319         NSEnumerator    *enumerator = [[[adium accountController] accountsCompatibleWithService:[contact service]] objectEnumerator];
320         AIAccount<AIAccount_Privacy>    *account = nil;
321         AIListContact   *sameContact = nil;
323         while ((account = [enumerator nextObject])) {
324                 sameContact = [account contactWithUID:[contact UID]];
325                 if ([account conformsToProtocol:@protocol(AIAccount_Privacy)]){
326                         
327                         if (sameContact){ 
328                                 /* If the account is in AIPrivacyOptionAllowUsers mode, blocking a contact means removing it from the allow list.
329                                  * Similarly, in allow mode, unblocking a contact means adding it to the allow list.
330                                  *
331                                  * In AIPrivacyOptionDenyUsers mode, blocking a contact means adding it to the block list.
332                                  *
333                                  * In all other modes, we can't block specific contacts... so we first switch to AIPrivacyOptionDenyUsers, the more lenient
334                                  * of the two possibilities, then add the contact to the block list.
335                                  */
336                                 AIPrivacyOption privacyOption = [account privacyOptions];
337                                 if (privacyOption == AIPrivacyOptionAllowUsers) {
338                                         [sameContact setIsAllowed:!isBlocked updateList:YES];
340                                 } else {
341                                         if (privacyOption != AIPrivacyOptionDenyUsers) {
342                                                 [account setPrivacyOptions:AIPrivacyOptionDenyUsers];
343                                         }
345                                         [sameContact setIsBlocked:isBlocked updateList:YES];
346                                 }
347                         }
348                 }
349         }
353  * @brief Inform AIListContact instances of the user's intended privacy towards the people they represent
354  */
355 #warning Something similar needs to happen to update when an account privacyOptions change
356 - (void)accountConnected:(NSNotification *)notification
358         AIAccount               *account = [notification object];
360         if ([account conformsToProtocol:@protocol(AIAccount_Privacy)] &&
361                 ([(AIAccount <AIAccount_Privacy> *)account privacyOptions] == AIPrivacyOptionDenyUsers)) {
362                 NSEnumerator    *contactEnumerator;
363                 AIListContact   *currentContact;
364                 NSArray                 *blockedContacts = [(AIAccount <AIAccount_Privacy> *)account listObjectsOnPrivacyList:AIPrivacyTypeDeny];
365                 
366                 contactEnumerator = [blockedContacts objectEnumerator];
367                 while ((currentContact = [contactEnumerator nextObject])) {
368                         [currentContact setIsBlocked:YES updateList:NO];
369                 }
370         }
374  * @brief Determine if all the referenced contacts are blocked or unblocked
376  * @param contacts The contacts to query
377  * @result A flag indicating if all the contacts are blocked or not
378  */
379 - (BOOL)areAllGivenContactsBlocked:(NSArray *)contacts
381         NSEnumerator    *contactEnumerator = [contacts objectEnumerator];
382         AIListContact   *currentContact = nil;
383         BOOL                    areAllGivenContactsBlocked = YES;
384         
385         //for each contact in the array
386         while ((currentContact = [contactEnumerator nextObject])) {
387                 
388                 //if the contact is unblocked, then all the contacts in the array aren't blocked
389                 if (![currentContact isBlocked]) {
390                         areAllGivenContactsBlocked = NO;
391                         break;
392                 }
393         }
394         
395         return areAllGivenContactsBlocked;
399  * @brief Block or unblock participants of the active chat in a chat window
401  * If all the participants of the chat are blocked, attempt to unblock each
402  * Else, attempt to block those that are not already blocked.
403  * Then, Update the item for the chat.
405  * We have to do it this way because a user can (un)block participants of 
406  * a chat window in the background by command-clicking the toolbar item.
408  * @param senderItem The toolbar item that received the event
409  */
410 - (IBAction)blockOrUnblockParticipants:(NSToolbarItem *)senderItem
412         NSEnumerator    *windowEnumerator = [[NSApp windows] objectEnumerator];
413         NSWindow                *currentWindow = nil;
414         NSToolbar               *windowToolbar = nil;
415         NSToolbar               *senderToolbar = [senderItem toolbar];
416         AIChat                  *activeChatInWindow = nil;
417         NSArray                 *participants = nil;
418         
419         //for each open window
420         while ((currentWindow = [windowEnumerator nextObject])) {
422                 //if it has a toolbar
423                 if ((windowToolbar = [currentWindow toolbar])) {
425                         //do the toolbars match?
426                         if (windowToolbar == senderToolbar) {
427                                 activeChatInWindow = [[adium interfaceController] activeChatInWindow:currentWindow];
428                                 participants = [activeChatInWindow participatingListObjects];
429                                 
430                                 //do the deed
431                                 [self setPrivacy:(![self areAllGivenContactsBlocked:participants]) forContacts:participants];
432                                 [self updateToolbarItem:senderItem forChat:activeChatInWindow];
433                                 break;
434                         }
435                 }
436         }
439 #pragma mark -
440 #pragma mark Protocols
443  * @brief Update any chat with the list object
445  * If the list object is (un)blocked, update any chats that we my have open with it.
446  */
447 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
449         if ([inModifiedKeys containsObject:@"isBlocked"]) {
450                 [self updateToolbarItemForObject:inObject];
451         }
452         
453         return nil;
456 #pragma mark -
457 #pragma mark Notifications
460  * @brief Toolbar has added an instance of the chat block toolbar item
461  */
462 - (void)toolbarWillAddItem:(NSNotification *)notification
464         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
465         
466         if ([[item itemIdentifier] isEqualToString:TOOLBAR_ITEM_IDENTIFIER]) {
467                 
468                 //If this is the first item added, start observing for chats becoming visible so we can update the item
469                 if ([chatToolbarItems count] == 0) {
470                         [[adium notificationCenter] addObserver:self
471                                                                                    selector:@selector(chatDidBecomeVisible:)
472                                                                                            name:@"AIChatDidBecomeVisible"
473                                                                                          object:nil];
474                 }
475                 
476                 [self updateToolbarItem:item forChat:[[adium interfaceController] activeChat]];
477                 [chatToolbarItems addObject:item];
478         }
482  * @brief A toolbar item was removed
483  */
484 - (void)toolbarDidRemoveItem:(NSNotification *)notification
486         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
487         [chatToolbarItems removeObject:item];
488         
489         if ([chatToolbarItems count] == 0) {
490                 [[adium notificationCenter] removeObserver:self
491                                                                                           name:@"AIChatDidBecomeVisible"
492                                                                                         object:nil];
493         }
497  * @brief A chat became visible in a window.
499  * Update the window's (un)block toolbar item to reflect the block state of a list object
501  * @param notification Notification with an AIChat object and an @"NSWindow" userInfo key
502  */
503 - (void)chatDidBecomeVisible:(NSNotification *)notification
505         [self updateToolbarIconOfChat:[notification object]
506                                                   inWindow:[[notification userInfo] objectForKey:@"NSWindow"]];
509 #pragma mark -
510 #pragma mark Toolbar Item Update Methods
513  * @brief Update the toolbar icon in a chat for a particular contact
515  * @param inObject The list object we want to update the toolbar item for
516  */
517 - (void)updateToolbarItemForObject:(AIListObject *)inObject
519         AIChat          *chat = nil;
520         NSWindow        *window = nil;
521         
522         //Update the icon in the toolbar for this contact if a chat is open and we have any toolbar items
523         if (([chatToolbarItems count] > 0) &&
524                 [inObject isKindOfClass:[AIListContact class]] &&
525                 (chat = [[adium chatController] existingChatWithContact:(AIListContact *)inObject]) &&
526                 (window = [[adium interfaceController] windowForChat:chat])) {
527                 [self updateToolbarIconOfChat:chat
528                                                          inWindow:window];
529         }
533  * @brief Update the toolbar item for the particpants of a particular chat
535  * @param item The toolbar item to modify
536  * @param chat The chat for which the participants are participating in
537  */
538 - (void)updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat
540         if ([self areAllGivenContactsBlocked:[chat participatingListObjects]]) {
541                 //assume unblock appearance
542                 [item setLabel:UNBLOCK];
543                 [item setPaletteLabel:UNBLOCK];
544                 [item setImage:[blockedToolbarIcons valueForKey:TOOLBAR_UNBLOCK_ICON_KEY]];
545         } else {
546                 //assume block appearance
547                 [item setLabel:BLOCK];
548                 [item setPaletteLabel:BLOCK];
549                 [item setImage:[blockedToolbarIcons valueForKey:TOOLBAR_BLOCK_ICON_KEY]];
550         }
554  * @brief Update the (un)block toolbar icon in a chat
556  * @param chat The chat with the participants
557  * @param window The window in which the chat resides
558  */
559 - (void)updateToolbarIconOfChat:(AIChat *)chat inWindow:(NSWindow *)window
561         NSToolbar               *toolbar = [window toolbar];
562         NSEnumerator    *enumerator = [[toolbar items] objectEnumerator];
563         NSToolbarItem   *item;
564         
565         while ((item = [enumerator nextObject])) {
566                 if ([[item itemIdentifier] isEqualToString:TOOLBAR_ITEM_IDENTIFIER]) {
567                         [self updateToolbarItem:item forChat:chat];
568                         break;
569                 }
570         }
573 @end