2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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;
49 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent;
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;
63 @implementation ESBlockingPlugin
67 //Install the Block menu items
68 blockContactMenuItem = [[NSMenuItem alloc] initWithTitle:BLOCK_MENUITEM
70 action:@selector(blockContact:)
73 [blockContactMenuItem setKeyEquivalentModifierMask:(NSCommandKeyMask|NSAlternateKeyMask)];
75 [[adium menuController] addMenuItem:blockContactMenuItem toLocation:LOC_Contact_NegativeAction];
77 //Add our get info contextual menu items
78 blockContactContextualMenuItem = [[NSMenuItem alloc] initWithTitle:BLOCK_MENUITEM
80 action:@selector(blockContact:)
82 [[adium menuController] addContextualMenuItem:blockContactContextualMenuItem toLocation:Context_Contact_NegativeAction];
84 //we want to know when an account connects
85 [[adium notificationCenter] addObserver:self
86 selector:@selector(accountConnected:)
87 name:ACCOUNT_CONNECTED
90 //create the block toolbar item
91 chatToolbarItems = [[NSMutableSet alloc] init];
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,
97 NSToolbarItem *chatItem = [AIToolbarUtilities toolbarItemWithIdentifier:TOOLBAR_ITEM_IDENTIFIER
100 toolTip:AILocalizedString(@"Blocking prevents a contact from contacting you or seeing your online status.", nil)
102 settingSelector:@selector(setImage:)
103 itemContent:[blockedToolbarIcons valueForKey:TOOLBAR_BLOCK_ICON_KEY]
104 action:@selector(blockOrUnblockParticipants:)
107 [[adium toolbarController] registerToolbarItem:chatItem forToolbarType:@"MessageWindow"];
109 [[NSNotificationCenter defaultCenter] addObserver:self
110 selector:@selector(toolbarWillAddItem:)
111 name:NSToolbarWillAddItemNotification
113 [[NSNotificationCenter defaultCenter] addObserver:self
114 selector:@selector(toolbarDidRemoveItem:)
115 name:NSToolbarDidRemoveItemNotification
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
137 - (void)setPrivacy:(BOOL)block forContacts:(NSArray *)contacts
139 NSEnumerator *contactEnumerator = [contacts objectEnumerator];
140 AIListContact *currentContact = nil;
142 while ((currentContact = [contactEnumerator nextObject])) {
143 if ([currentContact isBlocked] != block) {
144 [currentContact setIsBlocked:block updateList:YES];
149 - (IBAction)blockContact:(id)sender
151 AIListObject *object;
153 object = ((sender == blockContactMenuItem) ?
154 [[adium interfaceController] selectedListObject] :
155 [[adium menuController] currentContextMenuObject]);
158 if ([object isKindOfClass:[AIListContact class]]) {
159 AIListContact *contact = (AIListContact *)object;
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]],
170 (shouldBlock ? BLOCK : UNBLOCK),
171 AILocalizedString(@"Cancel", nil),
172 nil) == NSAlertDefaultReturn) {
175 if ([object isKindOfClass:[AIMetaContact class]]) {
176 AIMetaContact *meta = (AIMetaContact *)object;
178 //Enumerate over the various list contacts contained
179 NSEnumerator *enumerator = [[meta listContacts] objectEnumerator];
180 AIListContact *containedContact = nil;
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];
187 NSLog(@"Account %@ does not support blocking (contact %@ not blocked on this account)", acct, containedContact);
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];
196 NSLog(@"Account %@ does not support blocking (contact %@ not blocked on this account)", acct, contact);
200 [[adium notificationCenter] postNotificationName:@"AIPrivacySettingsChangedOutsideOfPrivacyWindow"
206 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
208 AIListObject *object;
210 if (menuItem == blockContactMenuItem) {
211 object = [[adium interfaceController] selectedListObject];
213 object = [[adium menuController] currentContextMenuObject];
217 if ([object isKindOfClass:[AIListContact class]]) {
219 if ([object isKindOfClass:[AIMetaContact class]]) {
220 AIMetaContact *meta = (AIMetaContact *)object;
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]) {
234 case AIPrivacyTypePermit:
235 /* He's on a permit list. The action would remove him, blocking him */
238 case AIPrivacyTypeDeny:
239 /* He's on a deny list. The action would remove him, unblocking him */
246 case AIPrivacyTypePermit:
247 /* He's not on the permit list. The action would add him, unblocking him */
250 case AIPrivacyTypeDeny:
251 /* He's not on a deny list. The action would add him, blocking him */
259 if (votesForBlock || votesForUnblock) {
260 if (votesForBlock >= votesForUnblock) {
261 [menuItem setTitle:BLOCK_MENUITEM];
263 [menuItem setTitle:UNBLOCK_MENUITEM];
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]) {
279 case AIPrivacyTypePermit:
280 /* He's on a permit list. The action would remove him, blocking him */
281 [menuItem setTitle:BLOCK_MENUITEM];
283 case AIPrivacyTypeDeny:
284 /* He's on a deny list. The action would remove him, unblocking him */
285 [menuItem setTitle:UNBLOCK_MENUITEM];
291 case AIPrivacyTypePermit:
292 /* He's not on the permit list. The action would add him, unblocking him */
293 [menuItem setTitle:UNBLOCK_MENUITEM];
295 case AIPrivacyTypeDeny:
296 /* He's not on a deny list. The action would add him, blocking him */
297 [menuItem setTitle:BLOCK_MENUITEM];
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)]){
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.
331 * In AIPrivacyOptionDenyUsers mode, blocking a contact means adding it to the block list.
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.
336 AIPrivacyOption privacyOption = [account privacyOptions];
337 if (privacyOption == AIPrivacyOptionAllowUsers) {
338 [sameContact setIsAllowed:!isBlocked updateList:YES];
341 if (privacyOption != AIPrivacyOptionDenyUsers) {
342 [account setPrivacyOptions:AIPrivacyOptionDenyUsers];
345 [sameContact setIsBlocked:isBlocked updateList:YES];
353 * @brief Inform AIListContact instances of the user's intended privacy towards the people they represent
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];
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];
371 [currentContact setIsBlocked:NO updateList:NO];
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
383 - (BOOL)areAllGivenContactsBlocked:(NSArray *)contacts
385 NSEnumerator *contactEnumerator = [contacts objectEnumerator];
386 AIListContact *currentContact = nil;
387 BOOL areAllGivenContactsBlocked = YES;
389 //for each contact in the array
390 while ((currentContact = [contactEnumerator nextObject])) {
392 //if the contact is unblocked, then all the contacts in the array aren't blocked
393 if (![currentContact isBlocked]) {
394 areAllGivenContactsBlocked = NO;
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
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;
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];
435 [self setPrivacy:(![self areAllGivenContactsBlocked:participants]) forContacts:participants];
436 [self updateToolbarItem:senderItem forChat:activeChatInWindow];
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.
451 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
453 if ([inModifiedKeys containsObject:@"isBlocked"]) {
454 [self updateToolbarItemForObject:inObject];
461 #pragma mark Notifications
464 * @brief Toolbar has added an instance of the chat block toolbar item
466 - (void)toolbarWillAddItem:(NSNotification *)notification
468 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
470 if ([[item itemIdentifier] isEqualToString:TOOLBAR_ITEM_IDENTIFIER]) {
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"
480 [self updateToolbarItem:item forChat:[[adium interfaceController] activeChat]];
481 [chatToolbarItems addObject:item];
486 * @brief A toolbar item was removed
488 - (void)toolbarDidRemoveItem:(NSNotification *)notification
490 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
491 [chatToolbarItems removeObject:item];
493 if ([chatToolbarItems count] == 0) {
494 [[adium notificationCenter] removeObserver:self
495 name:@"AIChatDidBecomeVisible"
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
507 - (void)chatDidBecomeVisible:(NSNotification *)notification
509 [self updateToolbarIconOfChat:[notification object]
510 inWindow:[[notification userInfo] objectForKey:@"NSWindow"]];
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
521 - (void)updateToolbarItemForObject:(AIListObject *)inObject
524 NSWindow *window = nil;
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
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
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]];
550 //assume block appearance
551 [item setLabel:BLOCK];
552 [item setPaletteLabel:BLOCK];
553 [item setImage:[blockedToolbarIcons valueForKey:TOOLBAR_BLOCK_ICON_KEY]];
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
563 - (void)updateToolbarIconOfChat:(AIChat *)chat inWindow:(NSWindow *)window
565 NSToolbar *toolbar = [window toolbar];
566 NSEnumerator *enumerator = [[toolbar items] objectEnumerator];
569 while ((item = [enumerator nextObject])) {
570 if ([[item itemIdentifier] isEqualToString:TOOLBAR_ITEM_IDENTIFIER]) {
571 [self updateToolbarItem:item forChat:chat];