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 "ESAddressBookIntegrationPlugin.h"
20 #import <Adium/AIMenuControllerProtocol.h>
21 #import <AIUtilities/AIAttributedStringAdditions.h>
22 #import <AIUtilities/AIDictionaryAdditions.h>
23 #import <AIUtilities/AIMutableOwnerArray.h>
24 #import <AIUtilities/AIStringAdditions.h>
25 #import <AIUtilities/OWAddressBookAdditions.h>
26 #import <AIUtilities/AIExceptionHandlingUtilities.h>
27 #import <AIUtilities/AIFileManagerAdditions.h>
28 #import <Adium/AIAccount.h>
29 #import <Adium/AIListObject.h>
30 #import <Adium/AIMetaContact.h>
31 #import <Adium/AIService.h>
33 #define IMAGE_LOOKUP_INTERVAL 0.01
34 #define SHOW_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Show In Address Book", "Show In Address Book Contextual Menu")
35 #define EDIT_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Edit In Address Book", "Edit In Address Book Contextual Menu")
36 #define ADD_TO_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Add To Address Book", "Add To Address Book Contextual Menu")
38 #define CONTACT_ADDED_SUCCESS_TITLE AILocalizedString(@"Success", "Title of a panel shown after adding successfully adding a contact to the address book.")
39 #define CONTACT_ADDED_SUCCESS_Message AILocalizedString(@"%@ had been successfully added to the Address Book.\nWould you like to edit the card now?", nil)
40 #define CONTACT_ADDED_ERROR_TITLE AILocalizedString(@"Error", nil)
41 #define CONTACT_ADDED_ERROR_Message AILocalizedString(@"An error had occurred while adding %@ to the Address Book.", nil)
43 @interface ESAddressBookIntegrationPlugin(PRIVATE)
44 - (void)updateAllContacts;
45 - (void)updateSelfIncludingIcon:(BOOL)includeIcon;
46 - (void)preferencesChanged:(NSNotification *)notification;
47 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic;
48 - (ABPerson *)personForListObject:(AIListObject *)inObject;
49 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID;
50 - (void)rebuildAddressBookDict;
51 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject;
52 - (void)showInAddressBook;
53 - (void)editInAddressBook;
54 - (void)addToAddressBookDict:(NSArray *)people;
55 - (void)removeFromAddressBookDict:(NSArray *)UIDs;
56 - (void)installAddressBookActions;
60 * @class ESAddressBookIntegrationPlugin
61 * @brief Provides Apple Address Book integration
63 * This class allows Adium to seamlessly interact with the Apple Address Book, pulling names and icons, storing icons
64 * if desired, and generating metaContacts based on screen name grouping. It relies upon cards having screen names listed
65 * in the appropriate service fields in the address book.
67 @implementation ESAddressBookIntegrationPlugin
69 static ABAddressBook *sharedAddressBook = nil;
70 static NSDictionary *serviceDict = nil;
72 NSString* serviceIDForOscarUID(NSString *UID);
73 NSString* serviceIDForJabberUID(NSString *UID);
76 * @brief Install plugin
78 * This plugin finishes installing in adiumFinishedLaunching:
83 addressBookDict = nil;
84 listObjectArrayForImageData = nil;
85 personArrayForImageData = nil;
86 imageLookupTimer = nil;
87 createMetaContacts = NO;
89 //Tracking dictionary for asynchronous image loads
90 trackingDict = [[NSMutableDictionary alloc] init];
91 trackingDictPersonToTagNumber = [[NSMutableDictionary alloc] init];
92 trackingDictTagNumberToPerson = [[NSMutableDictionary alloc] init];
94 //Configure our preferences
95 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:AB_DISPLAYFORMAT_DEFAULT_PREFS
96 forClass:[self class]]
97 forGroup:PREF_GROUP_ADDRESSBOOK];
99 //We want the enableImport preference immediately (without waiting for the preferences observer to be registered in adiumFinishedLaunching:)
100 enableImport = [[[adium preferenceController] preferenceForKey:KEY_AB_ENABLE_IMPORT
101 group:PREF_GROUP_ADDRESSBOOK] boolValue];
102 advancedPreferences = [[ESAddressBookIntegrationAdvancedPreferences preferencePane] retain];
104 //Services dictionary
105 serviceDict = [[NSDictionary dictionaryWithObjectsAndKeys:kABAIMInstantProperty,@"AIM",
106 kABJabberInstantProperty,@"Jabber",
107 kABMSNInstantProperty,@"MSN",
108 kABYahooInstantProperty,@"Yahoo!",
109 kABICQInstantProperty,@"ICQ",nil] retain];
111 //Shared Address Book
112 [sharedAddressBook release]; sharedAddressBook = [[ABAddressBook sharedAddressBook] retain];
114 //Create our contextual menus
115 showInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_IN_AB_CONTEXTUAL_MENU_TITLE
116 action:@selector(showInAddressBook)
117 keyEquivalent:@""] autorelease];
118 [showInABContextualMenuItem setTarget:self];
120 editInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:EDIT_IN_AB_CONTEXTUAL_MENU_TITLE
121 action:@selector(editInAddressBook)
122 keyEquivalent:@""] autorelease];
123 [editInABContextualMenuItem setTarget:self];
124 [editInABContextualMenuItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
125 [editInABContextualMenuItem setAlternate:YES];
127 addToABContexualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:ADD_TO_AB_CONTEXTUAL_MENU_TITLE
128 action:@selector(addToAddressBook)
129 keyEquivalent:@""] autorelease];
130 [addToABContexualMenuItem setTarget:self];
133 [[adium menuController] addContextualMenuItem:addToABContexualMenuItem toLocation:Context_Contact_Action];
134 [[adium menuController] addContextualMenuItem:showInABContextualMenuItem toLocation:Context_Contact_Action];
135 [[adium menuController] addContextualMenuItem:editInABContextualMenuItem toLocation:Context_Contact_Action];
137 [self installAddressBookActions];
139 //Wait for Adium to finish launching before we build the address book so the contact list will be ready
140 [[adium notificationCenter] addObserver:self
141 selector:@selector(adiumFinishedLaunching:)
142 name:Adium_CompletedApplicationLoad
145 //Update self immediately so the information is available to plugins and interface elements as they load
146 [self updateSelfIncludingIcon:YES];
149 - (void)installAddressBookActions
151 NSNumber *installedActions = [[NSUserDefaults standardUserDefaults] objectForKey:@"Adium:Installed Adress Book Actions"];
153 if (!installedActions || ![installedActions boolValue]) {
154 NSEnumerator *enumerator = [[NSArray arrayWithObjects:@"AIM", @"MSN", @"Yahoo", @"ICQ", @"Jabber", @"SMS", nil] objectEnumerator];
156 NSFileManager *fileManager = [NSFileManager defaultManager];
157 NSArray *libraryDirectoryArray;
158 NSString *libraryDirectory, *pluginDirectory;
160 libraryDirectoryArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
161 if ([libraryDirectoryArray count]) {
162 libraryDirectory = [libraryDirectoryArray objectAtIndex:0];
165 //Ridiculous safety since everyone should have a Library folder...
166 libraryDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
167 [fileManager createDirectoryAtPath:libraryDirectory attributes:nil];
170 pluginDirectory = [[libraryDirectory stringByAppendingPathComponent:@"Address Book Plug-Ins"] stringByAppendingPathComponent:@"/"];
171 [fileManager createDirectoryAtPath:pluginDirectory attributes:nil];
173 while ((name = [enumerator nextObject])) {
174 NSString *fullName = [NSString stringWithFormat:@"AdiumAddressBookAction_%@",name];
175 NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:fullName ofType:@"scpt"];
178 [fileManager copyPath:path
179 toPath:[pluginDirectory stringByAppendingPathComponent:[fullName stringByAppendingPathExtension:@"scpt"]]
182 //Remove the old xtra if installed
183 [fileManager trashFileAtPath:[pluginDirectory stringByAppendingPathComponent:
184 [NSString stringWithFormat:@"%@-Adium.scpt",name]]];
186 NSLog(@"%@: Could not find %@",self, fullName);
190 [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES]
191 forKey:@"Adium:Installed Adress Book Actions"];
196 * @brief Uninstall plugin
198 - (void)uninstallPlugin
200 [[adium contactController] unregisterListObjectObserver:self];
201 [[adium notificationCenter] removeObserver:self];
209 [serviceDict release]; serviceDict = nil;
210 [trackingDict release]; trackingDict = nil;
211 [trackingDictPersonToTagNumber release]; trackingDictPersonToTagNumber = nil;
212 [trackingDictTagNumberToPerson release]; trackingDictTagNumberToPerson = nil;
214 [sharedAddressBook release]; sharedAddressBook = nil;
216 [[adium preferenceController] unregisterPreferenceObserver:self];
217 [[adium notificationCenter] removeObserver:self];
223 * @brief Adium finished launching
225 * Register our observers for the address book changing externally and for the account list changing.
226 * Register our preference observers. This will trigger initial building of the address book dictionary.
228 - (void)adiumFinishedLaunching:(NSNotification *)notification
230 //Observe external address book changes
231 [[NSNotificationCenter defaultCenter] addObserver:self
232 selector:@selector(addressBookChanged:)
233 name:kABDatabaseChangedExternallyNotification
236 //Observe account changes
237 [[adium notificationCenter] addObserver:self
238 selector:@selector(accountListChanged:)
239 name:Account_ListChanged
242 //Observe preferences changes
243 id<AIPreferenceController> preferenceController = [adium preferenceController];
244 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_ADDRESSBOOK];
245 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_USERICONS];
249 * @brief Used as contacts are created and icons are changed.
251 * When first created, load a contact's address book information from our dict.
252 * When an icon as a status object changes, if desired, write the changed icon out to the appropriate AB card.
254 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
256 //Just stop here if we don't have an address book dict to work with
257 if (!addressBookDict) return nil;
259 //We handle accounts separately; doing updates here causes chaos in addition to being inefficient.
260 if ([inObject isKindOfClass:[AIAccount class]]) return nil;
262 NSSet *modifiedAttributes = nil;
264 if (inModifiedKeys == nil) { //Only perform this when updating for all list objects
265 ABPerson *person = [self personForListObject:inObject];
268 [self queueDelayedFetchOfImageForPerson:person object:inObject];
271 //Load the name if appropriate
272 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
273 NSString *displayName, *phoneticName = nil;
275 displayNameArray = [inObject displayArrayForKey:@"Display Name"];
277 displayName = [self nameForPerson:person phonetic:&phoneticName];
280 NSString *oldValue = [displayNameArray objectWithOwner:self];
281 if (!oldValue || ![oldValue isEqualToString:displayName]) {
282 [displayNameArray setObject:displayName withOwner:self];
283 modifiedAttributes = [NSSet setWithObject:@"Display Name"];
287 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"];
290 oldValue = [phoneticNameArray objectWithOwner:self];
291 if (!oldValue || ![oldValue isEqualToString:phoneticName]) {
292 [phoneticNameArray setObject:phoneticName withOwner:self];
293 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
296 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
298 //Clear any stored value
299 if ([phoneticNameArray objectWithOwner:self]) {
300 [displayNameArray setObject:nil withOwner:self];
301 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
306 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
308 displayNameArray = [inObject displayArrayForKey:@"Display Name"
311 //Clear any stored value
312 if ([displayNameArray objectWithOwner:self]) {
313 [displayNameArray setObject:nil withOwner:self];
314 modifiedAttributes = [NSSet setWithObject:@"Display Name"];
317 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
319 //Clear any stored value
320 if ([phoneticNameArray objectWithOwner:self]) {
321 [displayNameArray setObject:nil withOwner:self];
322 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
327 //If we changed anything, request an update of the alias / long display name
328 if (modifiedAttributes) {
329 [[adium notificationCenter] postNotificationName:Contact_ApplyDisplayName
331 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:silent]
336 } else if (automaticSync && [inModifiedKeys containsObject:KEY_USER_ICON]) {
338 //Only update when the serverside icon changes if there is no Adium preference overriding it
339 if (![inObject preferenceForKey:KEY_USER_ICON group:PREF_GROUP_USERICONS ignoreInheritedValues:YES]) {
341 ABPerson *person = [self personForListObject:inObject];
343 if (person && (person != [sharedAddressBook me])) {
344 //Set the person's image to the inObject's serverside User Icon.
345 NSData *userIconData = [inObject statusObjectForKey:@"UserIconData"];
347 userIconData = [[inObject statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
350 [person setImageData:userIconData];
355 return modifiedAttributes;
359 * @brief Return the name of an ABPerson in the way Adium should display it
361 * @param person An <tt>ABPerson</tt>
362 * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
363 * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
365 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
367 NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;
369 NSString *displayName = nil;
372 // If the record is for a company, return the company name if present
373 if ((flags = [person valueForProperty:kABPersonFlags])) {
374 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
375 NSString *companyName = [person valueForProperty:kABOrganizationProperty];
376 if (companyName && [companyName length]) {
382 firstName = [person valueForProperty:kABFirstNameProperty];
383 middleName = [person valueForProperty:kABMiddleNameProperty];
384 lastName = [person valueForProperty:kABLastNameProperty];
385 phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
386 phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
389 if (useMiddleName && middleName)
390 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
392 if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
393 displayName = nickName;
395 } else if (!lastName || (displayFormat == First)) {
396 /* If no last name is available, use the first name */
397 displayName = firstName;
398 if (phonetic != NULL) *phonetic = phoneticFirstName;
400 } else if (!firstName) {
401 /* If no first name is available, use the last name */
402 displayName = lastName;
403 if (phonetic != NULL) *phonetic = phoneticLastName;
406 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
408 /* Look to the preference setting */
409 switch (displayFormat) {
411 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
413 *phonetic = [NSString stringWithFormat:@"%@ %@",
414 (phoneticFirstName ? phoneticFirstName : firstName),
415 (phoneticLastName ? phoneticLastName : lastName)];
419 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName];
421 *phonetic = [NSString stringWithFormat:@"%@, %@",
422 (phoneticLastName ? phoneticLastName : lastName),
423 (phoneticFirstName ? phoneticFirstName : firstName)];
426 case LastFirstNoComma:
427 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName];
429 *phonetic = [NSString stringWithFormat:@"%@ %@",
430 (phoneticLastName ? phoneticLastName : lastName),
431 (phoneticFirstName ? phoneticFirstName : firstName)];
435 //No action; handled before we reach the switch statement
444 * @brief Observe preference changes
446 * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
447 * option is toggled, as metaContacts are created while building the dict.
449 * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
451 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
452 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
454 if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
455 BOOL oldCreateMetaContacts = createMetaContacts;
458 //load new displayFormat
459 if ((value = [prefDict objectForKey:KEY_AB_ENABLE_IMPORT])) enableImport = [value boolValue];
460 if ((value = [prefDict objectForKey:KEY_AB_DISPLAYFORMAT])) displayFormat = [value intValue];
461 if ((value = [prefDict objectForKey:KEY_AB_IMAGE_SYNC])) automaticSync = [value boolValue];
462 if ((value = [prefDict objectForKey:KEY_AB_USE_NICKNAME])) useNickName = [value boolValue];
463 if ((value = [prefDict objectForKey:KEY_AB_USE_MIDDLE])) useMiddleName = [value boolValue];
464 if ((value = [prefDict objectForKey:KEY_AB_PREFER_ADDRESS_BOOK_IMAGES])) preferAddressBookImages = [value boolValue];
465 if ((value = [prefDict objectForKey:KEY_AB_USE_IMAGES])) useABImages = [value boolValue];
467 if ((value = [prefDict objectForKey:KEY_AB_CREATE_METACONTACTS])) createMetaContacts = [value boolValue];
470 //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
471 [self rebuildAddressBookDict];
473 //Register ourself as a listObject observer, which will update all objects
474 [[adium contactController] registerListObjectObserver:self];
476 //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin
478 //This isn't the first time through
480 //If we weren't creating meta contacts before but we are now
481 if (!oldCreateMetaContacts && createMetaContacts) {
483 Build the address book dictionary, which will also trigger metacontact grouping as appropriate
484 Delay to the next run loop to give better UI responsiveness
486 [self performSelector:@selector(rebuildAddressBookDict)
491 //Update all contacts, which will update objects and then our "me" card information
492 [self updateAllContacts];
495 } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
497 ABPerson *person = [self personForListObject:object];
500 //Set the person's image to the inObject's serverside User Icon.
501 NSData *imageData = [object preferenceForKey:KEY_USER_ICON
502 group:PREF_GROUP_USERICONS
503 ignoreInheritedValues:YES];
505 //If the pref is now nil, we should restore the address book back to the serverside icon if possible
507 imageData = [[object statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
510 [person setImageData:imageData];
516 * @brief Returns the appropriate service for the property.
518 * @param property - an ABPerson property.
520 + (AIService *)serviceFromProperty:(NSString *)property
522 NSString *serviceID = nil;
524 if ([property isEqualToString:kABAIMInstantProperty])
527 else if ([property isEqualToString:kABICQInstantProperty])
530 else if ([property isEqualToString:kABMSNInstantProperty])
533 else if ([property isEqualToString:kABJabberInstantProperty])
534 serviceID = @"Jabber";
536 else if ([property isEqualToString:kABYahooInstantProperty])
537 serviceID = @"Yahoo!";
539 return (serviceID ? [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:serviceID] : nil);
543 * @brief Returns the appropriate property for the service.
545 + (NSString *)propertyFromService:(AIService *)inService
548 NSString *serviceID = [inService serviceID];
550 result = [serviceDict objectForKey:serviceID];
552 //Check for some special cases
554 if ([serviceID isEqualToString:@"GTalk"]) {
555 result = kABJabberInstantProperty;
556 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
557 result = kABJabberInstantProperty;
558 } else if ([serviceID isEqualToString:@"Mac"]) {
559 result = kABAIMInstantProperty;
566 #pragma mark Image data
569 * @brief Called when the address book completes an asynchronous image lookup
571 * @param inData NSData representing an NSImage
572 * @param tag A tag indicating the lookup with which this call is associated. We use a tracking dictionary, trackingDict, to associate this int back to a usable object.
574 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
577 [[adium preferenceController] setPreference:inData
578 forKey:KEY_DEFAULT_USER_ICON
579 group:GROUP_ACCOUNT_STATUS];
582 } else if (useABImages) {
585 AIListObject *listObject;
586 // AIListContact *parentContact;
590 tagNumber = [NSNumber numberWithInt:tag];
592 //Apply the image to the appropriate listObject
593 image = (inData ? [[[NSImage alloc] initWithData:inData] autorelease] : nil);
595 //Get the object from our tracking dictionary
596 setOrObject = [trackingDict objectForKey:tagNumber];
598 if ([setOrObject isKindOfClass:[AIListObject class]]) {
599 listObject = (AIListObject *)setOrObject;
601 //Apply the image at the appropriate priority
602 [listObject setDisplayUserIcon:image
604 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
607 parentContact = [listObject parentContact];
610 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
611 NSEnumerator *enumerator;
612 // BOOL checkedForMetaContact = NO;
614 //Apply the image to each listObject at the appropriate priority
615 enumerator = [(NSSet *)setOrObject objectEnumerator];
616 while ((listObject = [enumerator nextObject])) {
619 //These objects all have the same unique ID so will all also have the same meta contact; just check once
620 if (!checkedForMetaContact) {
621 parentContact = [listObject parentContact];
622 if (parentContact == listObject) parentContact = nil;
623 checkedForMetaContact = YES;
627 [listObject setDisplayUserIcon:image
629 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
635 [parentContact setDisplayUserIcon:image
637 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
641 //No further need for the dictionary entries
642 [trackingDict removeObjectForKey:tagNumber];
644 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
645 [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
646 [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
652 * @brief Queue an asynchronous image fetch for person associated with inObject
654 * Image lookups are done asynchronously. This allows other processing to be done between image calls, improving the perceived
655 * speed. [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
656 * and the asynchronous lookup therefore hung the problem. Submitted as radar 3977541.]
658 * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
659 * the number of accounts on that service. We therefore aggregate the lookups to lower the address book search
660 * and image/data creation overhead.
662 * @param person The ABPerson to fetch the image from
663 * @pram inObject The AIListObject with which to ultimately associate the image
665 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
671 uniqueId = [person uniqueId];
673 //Check if we already have a tag for the loading of another object with the same
675 if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
677 NSMutableSet *objectSet;
679 previousValue = [trackingDict objectForKey:tagNumber];
681 if ([previousValue isKindOfClass:[AIListObject class]]) {
682 //If the old value is just a listObject, create an array with the old object
684 objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
686 //Store the array in the tracking dict
687 [trackingDict setObject:objectSet forKey:tagNumber];
689 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
690 //Add the new object to the previously-created array
691 [(NSMutableSet *)previousValue addObject:inObject];
695 //Begin the image load
696 tag = [person beginLoadingImageDataForClient:self];
697 tagNumber = [NSNumber numberWithInt:tag];
699 //We need to be able to take a tagNumber and retrieve the object
700 [trackingDict setObject:inObject forKey:tagNumber];
702 //We also want to take a person's uniqueID and potentially find an existing tag number
703 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
704 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];
708 #pragma mark Searching
710 * @brief Find an ABPerson corresponding to an AIListObject
712 * @param inObject The object for which it search
713 * @result An ABPerson is one is found, or nil if none is found
715 - (ABPerson *)personForListObject:(AIListObject *)inObject
717 ABPerson *person = nil;
718 NSString *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
719 ABRecord *record = nil;
722 record = [sharedAddressBook recordForUniqueId:uniqueID];
724 if (record && [record isKindOfClass:[ABPerson class]]) {
725 person = (ABPerson *)record;
727 if ([inObject isKindOfClass:[AIMetaContact class]]) {
728 NSEnumerator *enumerator;
729 AIListContact *listContact;
731 //Search for an ABPerson for each listContact within the metaContact; first one we find is
733 enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
734 while ((listContact = [enumerator nextObject]) && (person == nil)) {
735 person = [self personForListObject:listContact];
739 NSString *UID = [inObject UID];
740 NSString *serviceID = [[inObject service] serviceID];
742 person = [self _searchForUID:UID serviceID:serviceID];
744 /* If we don't find anything yet, look at alternative service possibilities:
748 if ([serviceID isEqualToString:@"AIM"]) {
749 person = [self _searchForUID:UID serviceID:@"ICQ"];
750 } else if ([serviceID isEqualToString:@"ICQ"]) {
751 person = [self _searchForUID:UID serviceID:@"AIM"];
760 * @brief Find an ABPerson for a given UID and serviceID combination
762 * Uses our addressBookDict cache created in rebuildAddressBook.
764 * @param UID The UID for the contact
765 * @param serviceID The serviceID for the contact
766 * @result A corresponding <tt>ABPerson</tt>
768 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
770 ABPerson *person = nil;
773 if ([serviceID isEqualToString:@"Mac"]) {
774 dict = [addressBookDict objectForKey:@"AIM"];
776 } else if ([serviceID isEqualToString:@"GTalk"]) {
777 dict = [addressBookDict objectForKey:@"Jabber"];
779 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
780 dict = [addressBookDict objectForKey:@"Jabber"];
782 } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
783 dict = [addressBookDict objectForKey:@"Yahoo!"];
786 dict = [addressBookDict objectForKey:serviceID];
790 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
792 person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
799 - (NSSet *)contactsForPerson:(ABPerson *)person
801 NSArray *allServiceKeys = [serviceDict allKeys];
803 NSMutableSet *contactSet = [NSMutableSet set];
804 NSEnumerator *servicesEnumerator;
805 ABMultiValue *emails;
808 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
810 emails = [person valueForProperty:kABEmailProperty];
811 emailsCount = [emails count];
813 for (i = 0; i < emailsCount ; i++) {
816 email = [emails valueAtIndex:i];
817 if ([email hasSuffix:@"@mac.com"]) {
818 //Retrieve all appropriate contacts
819 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
823 //Add them to our set
824 [contactSet unionSet:contacts];
829 //Now go through the instant messaging keys
830 servicesEnumerator = [allServiceKeys objectEnumerator];
831 while ((serviceID = [servicesEnumerator nextObject])) {
832 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
836 //An ABPerson may have multiple names; iterate through them
837 names = [person valueForProperty:addressBookKey];
838 nameCount = [names count];
840 //Continue to the next serviceID immediately if no names are found
841 if (nameCount == 0) continue;
843 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
844 [serviceID isEqualToString:@"ICQ"]);
845 BOOL isJabber = [serviceID isEqualToString:@"Jabber"];
847 for (i = 0 ; i < nameCount ; i++) {
848 NSString *UID = [[names valueAtIndex:i] compactedString];
851 serviceID = serviceIDForOscarUID(UID);
853 } else if (isJabber) {
854 serviceID = serviceIDForJabberUID(UID);
857 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
861 //Add them to our set
862 [contactSet unionSet:contacts];
870 #pragma mark Address book changed
872 * @brief Address book changed externally
874 * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
876 - (void)addressBookChanged:(NSNotification *)notification
878 /* In case of a single person, these will be NSStrings.
879 * In case of more then one, they are will be NSArrays containing NSStrings.
881 id addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
882 NSMutableSet *allModifiedPeople = [[NSMutableSet alloc] init];
883 ABPerson *me = [sharedAddressBook me];
884 BOOL modifiedMe = NO;;
886 //Delay listObjectNotifications to speed up metaContact creation
887 [[adium contactController] delayListObjectNotifications];
889 //Addition of new records
890 if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
891 NSArray *peopleToAdd;
893 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
894 //We are dealing with multiple records
895 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
897 //We have only one record
898 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
901 [allModifiedPeople addObjectsFromArray:peopleToAdd];
902 [self addToAddressBookDict:peopleToAdd];
905 //Modification of existing records
906 if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
907 NSArray *peopleToAdd;
909 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
910 //We are dealing with multiple records
911 [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
912 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
914 //We have only one record
915 [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
916 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
919 [allModifiedPeople addObjectsFromArray:peopleToAdd];
920 [self addToAddressBookDict:peopleToAdd];
923 //Deletion of existing records
924 if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
925 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
926 //We are dealing with multiple records
927 [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
929 //We have only one record
930 [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
933 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
936 NSEnumerator *peopleEnumerator;
939 //Do appropriate updates for each updated ABPerson
940 peopleEnumerator = [allModifiedPeople objectEnumerator];
941 while ((person = [peopleEnumerator nextObject])) {
946 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
947 [[adium contactController] updateContacts:[self contactsForPerson:person]
951 //Update us if appropriate
953 [self updateSelfIncludingIcon:YES];
956 //Stop delaying list object notifications since we are done
957 [[adium contactController] endListObjectNotificationsDelay];
961 * @brief Update all existing contacts and accounts
963 - (void)updateAllContacts
965 [[adium contactController] updateAllListObjectsForObserver:self];
966 [self updateSelfIncludingIcon:YES];
970 * @brief Account list changed: Update all existing accounts
972 - (void)accountListChanged:(NSNotification *)notification
974 [self updateSelfIncludingIcon:NO];
978 * @brief Update all existing accounts
980 * We use the "me" card to determine the default icon and account display name
982 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
985 //Begin loading image data for the "me" address book entry, if one exists
987 if ((me = [sharedAddressBook me])) {
991 //Begin the image load
992 meTag = [me beginLoadingImageDataForClient:self];
995 //Set account display names
997 NSString *myDisplayName, *myPhonetic = nil;
999 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
1001 NSEnumerator *accountsArray = [[[adium accountController] accounts] objectEnumerator];
1004 while ((account = [accountsArray nextObject])) {
1005 if (![account isTemporary]) {
1006 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
1008 priorityLevel:Low_Priority];
1011 [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
1013 priorityLevel:Low_Priority];
1018 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
1019 forKey:KEY_ACCOUNT_DISPLAY_NAME]
1020 forGroup:GROUP_ACCOUNT_STATUS];
1024 NSLog(@"ABIntegration: Caught %@: %@", [localException name], [localException reason]);
1028 #pragma mark Address book caching
1030 * @brief rebuild our address book lookup dictionary
1032 - (void)rebuildAddressBookDict
1034 //Delay listObjectNotifications to speed up metaContact creation
1035 [[adium contactController] delayListObjectNotifications];
1037 [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
1039 [self addToAddressBookDict:[sharedAddressBook people]];
1041 //Stop delaying list object notifications since we are done
1042 [[adium contactController] endListObjectNotificationsDelay];
1047 * @brief Service ID for an OSCAR UID
1049 * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
1050 * because we may have a .Mac, an ICQ, or an AIM name in the field
1052 NSString* serviceIDForOscarUID(NSString *UID)
1054 NSString *serviceID;
1056 const char firstCharacter = [UID characterAtIndex:0];
1058 //Determine service based on UID
1059 if ([UID hasSuffix:@"@mac.com"]) {
1061 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
1071 * @brief Service ID for a Jabber UID
1073 * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
1074 * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
1076 NSString* serviceIDForJabberUID(NSString *UID)
1078 NSString *serviceID;
1080 if ([UID hasSuffix:@"@gmail.com"] ||
1081 [UID hasSuffix:@"@googlemail.com"]) {
1082 serviceID = @"GTalk";
1083 } else if ([UID hasSuffix:@"@livejournal.com"]) {
1084 serviceID = @"LiveJournal";
1086 serviceID = @"Jabber";
1094 * @brief add people to our address book lookup dictionary
1096 * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
1097 * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
1098 * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
1099 * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
1102 * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
1103 * create metaContacts baesd on this information.
1105 - (void)addToAddressBookDict:(NSArray *)people
1107 NSEnumerator *peopleEnumerator;
1108 NSArray *allServiceKeys = [serviceDict allKeys];
1111 peopleEnumerator = [people objectEnumerator];
1112 while ((person = [peopleEnumerator nextObject])) {
1113 NSEnumerator *servicesEnumerator;
1114 NSString *serviceID;
1116 NSMutableArray *UIDsArray = [NSMutableArray array];
1117 NSMutableArray *servicesArray = [NSMutableArray array];
1119 NSMutableDictionary *dict;
1120 ABMultiValue *emails;
1123 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1125 emails = [person valueForProperty:kABEmailProperty];
1126 emailsCount = [emails count];
1128 for (i = 0; i < emailsCount ; i++) {
1131 email = [emails valueAtIndex:i];
1132 if ([email hasSuffix:@"@mac.com"]) {
1134 //@mac.com UIDs go into the AIM dictionary
1135 if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1136 dict = [[[NSMutableDictionary alloc] init] autorelease];
1137 [addressBookDict setObject:dict forKey:@"AIM"];
1140 [dict setObject:[person uniqueId] forKey:email];
1142 //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1143 [UIDsArray addObject:email];
1144 [servicesArray addObject:@"Mac"];
1149 //Now go through the instant messaging keys
1150 servicesEnumerator = [allServiceKeys objectEnumerator];
1151 while ((serviceID = [servicesEnumerator nextObject])) {
1152 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
1153 ABMultiValue *names;
1156 //An ABPerson may have multiple names; iterate through them
1157 names = [person valueForProperty:addressBookKey];
1158 nameCount = [names count];
1160 //Continue to the next serviceID immediately if no names are found
1161 if (nameCount == 0) continue;
1163 //One or more names were found, so we'll need a dictionary
1164 if (!(dict = [addressBookDict objectForKey:serviceID])) {
1165 dict = [[NSMutableDictionary alloc] init];
1166 [addressBookDict setObject:dict forKey:serviceID];
1170 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
1171 [serviceID isEqualToString:@"ICQ"]);
1172 BOOL isJabber = [serviceID isEqualToString:@"Jabber"];
1174 for (i = 0 ; i < nameCount ; i++) {
1175 NSString *UID = [[names valueAtIndex:i] compactedString];
1177 [dict setObject:[person uniqueId] forKey:UID];
1179 [UIDsArray addObject:UID];
1182 serviceID = serviceIDForOscarUID(UID);
1184 } else if (isJabber) {
1185 serviceID = serviceIDForJabberUID(UID);
1188 [servicesArray addObject:serviceID];
1193 if (([UIDsArray count] > 1) && createMetaContacts) {
1194 /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1195 [[adium contactController] groupUIDs:UIDsArray
1196 forServices:servicesArray];
1202 * @brief remove people from our address book lookup dictionary
1204 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1206 NSEnumerator *enumerator;
1207 NSArray *allServiceKeys = [serviceDict allKeys];
1210 enumerator = [uniqueIDs objectEnumerator];
1211 while ((uniqueID = [enumerator nextObject])) {
1212 NSEnumerator *servicesEnumerator;
1213 NSString *serviceID;
1214 NSMutableDictionary *dict;
1216 //The same person may have multiple services; iterate through them and remove each one.
1217 servicesEnumerator = [allServiceKeys objectEnumerator];
1218 while ((serviceID = [servicesEnumerator nextObject])) {
1219 NSEnumerator *keysEnumerator;
1222 dict = [addressBookDict objectForKey:serviceID];
1224 keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1226 //The same person may have multiple accounts from the same service; we should remove them all.
1227 while ((key = [keysEnumerator nextObject])) {
1228 [dict removeObjectForKey:key];
1234 #pragma mark AB contextual menu
1236 * @brief Validate menu item
1238 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1240 BOOL hasABEntry = ([self personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1243 if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1244 result = hasABEntry;
1245 else if ([menuItem isEqual:addToABContexualMenuItem])
1246 result = !hasABEntry;
1252 * @brief Shows the selected contact in Address Book
1254 - (void)showInAddressBook
1256 ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1257 NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1258 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1262 * @brief Edits the selected contact in Address Book
1264 - (void)editInAddressBook
1266 ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1267 NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1268 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1271 - (void)addToAddressBook
1273 AIListObject *contact = [[adium menuController] currentContextMenuObject];
1274 NSString *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1276 if (serviceProperty) {
1277 ABPerson *person = [[ABPerson alloc] init];
1280 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1281 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1283 NSString *UID = [contact formattedUID];
1285 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1287 ABMutableMultiValue *multiValue;
1289 while((c = [containedContactEnu nextObject]))
1291 multiValue = [[ABMutableMultiValue alloc] init];
1292 UID = [c formattedUID];
1293 serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1295 //Set the IM property
1296 [multiValue addValue:UID withLabel:serviceProperty];
1297 [person setValue:multiValue forKey:serviceProperty];
1299 [multiValue release];
1304 [person setImageData:[contact userIconData]];
1307 [person setValue:[contact notes] forKey:kABNoteProperty];
1309 //Add our newly created person to the AB database
1310 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1311 //Save the uid of the new person
1312 [contact setPreference:[person uniqueId]
1313 forKey:KEY_AB_UNIQUE_ID
1314 group:PREF_GROUP_ADDRESSBOOK];
1316 //Ask the user whether it would like to edit the new contact
1317 int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1318 CONTACT_ADDED_SUCCESS_Message,
1319 AILocalizedString(@"Yes", nil),
1320 AILocalizedString(@"No", nil), nil, UID);
1322 if (result == NSOKButton) {
1323 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1324 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1328 NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);