* Don't try to cache the blocked / permitted contacts in CBGaimAccount; just query...
[adiumx.git] / Source / ESBlockingPlugin.m
blob435b8c206bb5a86e502313e128596c1a3709c8fd
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:(id <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 - (void)accountConnected:(NSNotification *)notification
357         AIAccount               *account = [notification object];
359         if ([account conformsToProtocol:@protocol(AIAccount_Privacy)]) {
360                 NSEnumerator    *contactEnumerator;
361                 AIListContact   *currentContact;
362                 NSArray                 *blockedContacts = [(AIAccount <AIAccount_Privacy> *)account listObjectsOnPrivacyList:AIPrivacyTypeDeny];
363                 
364                 //check if each contact is on the account's deny list
365                 contactEnumerator = [[account contacts] objectEnumerator];
366                 while ((currentContact = [contactEnumerator nextObject])) {
367                         if ([blockedContacts containsObject:[currentContact UID]]) {
368                                 //inform the contact that they're blocked
369                                 [currentContact setIsBlocked:YES updateList:NO];
370                         } else {
371                                 [currentContact setIsBlocked:NO updateList:NO];
372                         }
373                 }
374         }
378  * @brief Determine if all the referenced contacts are blocked or unblocked
380  * @param contacts The contacts to query
381  * @result A flag indicating if all the contacts are blocked or not
382  */
383 - (BOOL)areAllGivenContactsBlocked:(NSArray *)contacts
385         NSEnumerator    *contactEnumerator = [contacts objectEnumerator];
386         AIListContact   *currentContact = nil;
387         BOOL                    areAllGivenContactsBlocked = YES;
388         
389         //for each contact in the array
390         while ((currentContact = [contactEnumerator nextObject])) {
391                 
392                 //if the contact is unblocked, then all the contacts in the array aren't blocked
393                 if (![currentContact isBlocked]) {
394                         areAllGivenContactsBlocked = NO;
395                         break;
396                 }
397         }
398         
399         return areAllGivenContactsBlocked;
403  * @brief Block or unblock participants of the active chat in a chat window
405  * If all the participants of the chat are blocked, attempt to unblock each
406  * Else, attempt to block those that are not already blocked.
407  * Then, Update the item for the chat.
409  * We have to do it this way because a user can (un)block participants of 
410  * a chat window in the background by command-clicking the toolbar item.
412  * @param senderItem The toolbar item that received the event
413  */
414 - (IBAction)blockOrUnblockParticipants:(NSToolbarItem *)senderItem
416         NSEnumerator    *windowEnumerator = [[NSApp windows] objectEnumerator];
417         NSWindow                *currentWindow = nil;
418         NSToolbar               *windowToolbar = nil;
419         NSToolbar               *senderToolbar = [senderItem toolbar];
420         AIChat                  *activeChatInWindow = nil;
421         NSArray                 *participants = nil;
422         
423         //for each open window
424         while ((currentWindow = [windowEnumerator nextObject])) {
426                 //if it has a toolbar
427                 if ((windowToolbar = [currentWindow toolbar])) {
429                         //do the toolbars match?
430                         if (windowToolbar == senderToolbar) {
431                                 activeChatInWindow = [[adium interfaceController] activeChatInWindow:currentWindow];
432                                 participants = [activeChatInWindow participatingListObjects];
433                                 
434                                 //do the deed
435                                 [self setPrivacy:(![self areAllGivenContactsBlocked:participants]) forContacts:participants];
436                                 [self updateToolbarItem:senderItem forChat:activeChatInWindow];
437                                 break;
438                         }
439                 }
440         }
443 #pragma mark -
444 #pragma mark Protocols
447  * @brief Update any chat with the list object
449  * If the list object is (un)blocked, update any chats that we my have open with it.
450  */
451 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
453         if ([inModifiedKeys containsObject:@"isBlocked"]) {
454                 [self updateToolbarItemForObject:inObject];
455         }
456         
457         return nil;
460 #pragma mark -
461 #pragma mark Notifications
464  * @brief Toolbar has added an instance of the chat block toolbar item
465  */
466 - (void)toolbarWillAddItem:(NSNotification *)notification
468         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
469         
470         if ([[item itemIdentifier] isEqualToString:TOOLBAR_ITEM_IDENTIFIER]) {
471                 
472                 //If this is the first item added, start observing for chats becoming visible so we can update the item
473                 if ([chatToolbarItems count] == 0) {
474                         [[adium notificationCenter] addObserver:self
475                                                                                    selector:@selector(chatDidBecomeVisible:)
476                                                                                            name:@"AIChatDidBecomeVisible"
477                                                                                          object:nil];
478                 }
479                 
480                 [self updateToolbarItem:item forChat:[[adium interfaceController] activeChat]];
481                 [chatToolbarItems addObject:item];
482         }
486  * @brief A toolbar item was removed
487  */
488 - (void)toolbarDidRemoveItem:(NSNotification *)notification
490         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
491         [chatToolbarItems removeObject:item];
492         
493         if ([chatToolbarItems count] == 0) {
494                 [[adium notificationCenter] removeObserver:self
495                                                                                           name:@"AIChatDidBecomeVisible"
496                                                                                         object:nil];
497         }
501  * @brief A chat became visible in a window.
503  * Update the window's (un)block toolbar item to reflect the block state of a list object
505  * @param notification Notification with an AIChat object and an @"NSWindow" userInfo key
506  */
507 - (void)chatDidBecomeVisible:(NSNotification *)notification
509         [self updateToolbarIconOfChat:[notification object]
510                                                   inWindow:[[notification userInfo] objectForKey:@"NSWindow"]];
513 #pragma mark -
514 #pragma mark Toolbar Item Update Methods
517  * @brief Update the toolbar icon in a chat for a particular contact
519  * @param inObject The list object we want to update the toolbar item for
520  */
521 - (void)updateToolbarItemForObject:(AIListObject *)inObject
523         AIChat          *chat = nil;
524         NSWindow        *window = nil;
525         
526         //Update the icon in the toolbar for this contact if a chat is open and we have any toolbar items
527         if (([chatToolbarItems count] > 0) &&
528                 [inObject isKindOfClass:[AIListContact class]] &&
529                 (chat = [[adium chatController] existingChatWithContact:(AIListContact *)inObject]) &&
530                 (window = [[adium interfaceController] windowForChat:chat])) {
531                 [self updateToolbarIconOfChat:chat
532                                                          inWindow:window];
533         }
537  * @brief Update the toolbar item for the particpants of a particular chat
539  * @param item The toolbar item to modify
540  * @param chat The chat for which the participants are participating in
541  */
542 - (void)updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat
544         if ([self areAllGivenContactsBlocked:[chat participatingListObjects]]) {
545                 //assume unblock appearance
546                 [item setLabel:UNBLOCK];
547                 [item setPaletteLabel:UNBLOCK];
548                 [item setImage:[blockedToolbarIcons valueForKey:TOOLBAR_UNBLOCK_ICON_KEY]];
549         } else {
550                 //assume block appearance
551                 [item setLabel:BLOCK];
552                 [item setPaletteLabel:BLOCK];
553                 [item setImage:[blockedToolbarIcons valueForKey:TOOLBAR_BLOCK_ICON_KEY]];
554         }
558  * @brief Update the (un)block toolbar icon in a chat
560  * @param chat The chat with the participants
561  * @param window The window in which the chat resides
562  */
563 - (void)updateToolbarIconOfChat:(AIChat *)chat inWindow:(NSWindow *)window
565         NSToolbar               *toolbar = [window toolbar];
566         NSEnumerator    *enumerator = [[toolbar items] objectEnumerator];
567         NSToolbarItem   *item;
568         
569         while ((item = [enumerator nextObject])) {
570                 if ([[item itemIdentifier] isEqualToString:TOOLBAR_ITEM_IDENTIFIER]) {
571                         [self updateToolbarItem:item forChat:chat];
572                         break;
573                 }
574         }
577 @end