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 "ESAddressBookIntegrationPlugin.h"
18 #import <Adium/AIAccountControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIPreferenceControllerProtocol.h>
21 #import <Adium/AIMenuControllerProtocol.h>
22 #import <Adium/AIAccount.h>
23 #import <Adium/AIListObject.h>
24 #import <Adium/AIMetaContact.h>
25 #import <Adium/AIService.h>
26 #import <AIUtilities/AIAttributedStringAdditions.h>
27 #import <AIUtilities/AIDictionaryAdditions.h>
28 #import <AIUtilities/AIMutableOwnerArray.h>
29 #import <AIUtilities/AIStringAdditions.h>
30 #import <AIUtilities/OWAddressBookAdditions.h>
31 #import <AIUtilities/AIFileManagerAdditions.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:AIApplicationDidFinishLoadingNotification
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 AILogWithSignature(@"Warning: 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];
352 [[sharedAddressBook class] cancelPreviousPerformRequestsWithTarget:sharedAddressBook
353 selector:@selector(save)
355 [sharedAddressBook performSelector:@selector(save)
362 return modifiedAttributes;
366 * @brief Return the name of an ABPerson in the way Adium should display it
368 * @param person An <tt>ABPerson</tt>
369 * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
370 * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
372 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
374 NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;
376 NSString *displayName = nil;
379 // If the record is for a company, return the company name if present
380 if ((flags = [person valueForProperty:kABPersonFlags])) {
381 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
382 NSString *companyName = [person valueForProperty:kABOrganizationProperty];
383 if (companyName && [companyName length]) {
389 firstName = [person valueForProperty:kABFirstNameProperty];
390 middleName = [person valueForProperty:kABMiddleNameProperty];
391 lastName = [person valueForProperty:kABLastNameProperty];
392 phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
393 phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
396 if (useMiddleName && middleName)
397 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
399 if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
400 displayName = nickName;
402 } else if (!lastName || (displayFormat == First)) {
403 /* If no last name is available, use the first name */
404 displayName = firstName;
405 if (phonetic != NULL) *phonetic = phoneticFirstName;
407 } else if (!firstName) {
408 /* If no first name is available, use the last name */
409 displayName = lastName;
410 if (phonetic != NULL) *phonetic = phoneticLastName;
413 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
415 /* Look to the preference setting */
416 switch (displayFormat) {
418 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
420 *phonetic = [NSString stringWithFormat:@"%@ %@",
421 (phoneticFirstName ? phoneticFirstName : firstName),
422 (phoneticLastName ? phoneticLastName : lastName)];
426 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName];
428 *phonetic = [NSString stringWithFormat:@"%@, %@",
429 (phoneticLastName ? phoneticLastName : lastName),
430 (phoneticFirstName ? phoneticFirstName : firstName)];
433 case LastFirstNoComma:
434 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName];
436 *phonetic = [NSString stringWithFormat:@"%@ %@",
437 (phoneticLastName ? phoneticLastName : lastName),
438 (phoneticFirstName ? phoneticFirstName : firstName)];
441 case FirstLastInitial:
442 displayName = [NSString stringWithFormat:@"%@ %@",firstName,[lastName substringToIndex:1]];
444 *phonetic = [NSString stringWithFormat:@"%@ %@",
445 (phoneticFirstName ? phoneticFirstName : firstName),
446 [lastName substringToIndex:1]];
449 //No action; handled before we reach the switch statement
458 * @brief Observe preference changes
460 * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
461 * option is toggled, as metaContacts are created while building the dict.
463 * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
465 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
466 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
468 if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
469 BOOL oldCreateMetaContacts = createMetaContacts;
471 //load new displayFormat
472 enableImport = [[prefDict objectForKey:KEY_AB_ENABLE_IMPORT] boolValue];
473 displayFormat = [[prefDict objectForKey:KEY_AB_DISPLAYFORMAT] intValue];
474 automaticSync = [[prefDict objectForKey:KEY_AB_IMAGE_SYNC] boolValue];
475 useNickName = [[prefDict objectForKey:KEY_AB_USE_NICKNAME] boolValue];
476 useMiddleName = [[prefDict objectForKey:KEY_AB_USE_MIDDLE] boolValue];
477 preferAddressBookImages = [[prefDict objectForKey:KEY_AB_PREFER_ADDRESS_BOOK_IMAGES] boolValue];
478 useABImages = [[prefDict objectForKey:KEY_AB_USE_IMAGES] boolValue];
480 createMetaContacts = [[prefDict objectForKey:KEY_AB_CREATE_METACONTACTS] boolValue];
483 //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
484 [self rebuildAddressBookDict];
486 //Register ourself as a listObject observer, which will update all objects
487 [[adium contactController] registerListObjectObserver:self];
489 //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin
491 //This isn't the first time through
493 //If we weren't creating meta contacts before but we are now
494 if (!oldCreateMetaContacts && createMetaContacts) {
496 Build the address book dictionary, which will also trigger metacontact grouping as appropriate
497 Delay to the next run loop to give better UI responsiveness
499 [self performSelector:@selector(rebuildAddressBookDict)
504 //Update all contacts, which will update objects and then our "me" card information
505 [self updateAllContacts];
508 } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
510 ABPerson *person = [self personForListObject:object];
513 //Set the person's image to the inObject's serverside User Icon.
514 NSData *imageData = [object preferenceForKey:KEY_USER_ICON
515 group:PREF_GROUP_USERICONS
516 ignoreInheritedValues:YES];
518 //If the pref is now nil, we should restore the address book back to the serverside icon if possible
520 imageData = [[object statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
523 [person setImageData:imageData];
529 * @brief Returns the appropriate service for the property.
531 * @param property - an ABPerson property.
533 + (AIService *)serviceFromProperty:(NSString *)property
535 NSString *serviceID = nil;
537 if ([property isEqualToString:kABAIMInstantProperty])
540 else if ([property isEqualToString:kABICQInstantProperty])
543 else if ([property isEqualToString:kABMSNInstantProperty])
546 else if ([property isEqualToString:kABJabberInstantProperty])
547 serviceID = @"Jabber";
549 else if ([property isEqualToString:kABYahooInstantProperty])
550 serviceID = @"Yahoo!";
552 return (serviceID ? [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:serviceID] : nil);
556 * @brief Returns the appropriate property for the service.
558 + (NSString *)propertyFromService:(AIService *)inService
561 NSString *serviceID = [inService serviceID];
563 result = [serviceDict objectForKey:serviceID];
565 //Check for some special cases
567 if ([serviceID isEqualToString:@"GTalk"]) {
568 result = kABJabberInstantProperty;
569 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
570 result = kABJabberInstantProperty;
571 } else if ([serviceID isEqualToString:@"Mac"]) {
572 result = kABAIMInstantProperty;
579 #pragma mark Image data
582 * @brief Called when the address book completes an asynchronous image lookup
584 * @param inData NSData representing an NSImage
585 * @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.
587 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
590 [[adium preferenceController] setPreference:inData
591 forKey:KEY_DEFAULT_USER_ICON
592 group:GROUP_ACCOUNT_STATUS];
595 } else if (useABImages) {
598 AIListObject *listObject;
599 // AIListContact *parentContact;
603 tagNumber = [NSNumber numberWithInt:tag];
605 //Apply the image to the appropriate listObject
606 image = (inData ? [[[NSImage alloc] initWithData:inData] autorelease] : nil);
607 [image setDataRetained:YES];
609 //Get the object from our tracking dictionary
610 setOrObject = [trackingDict objectForKey:tagNumber];
612 if ([setOrObject isKindOfClass:[AIListObject class]]) {
613 listObject = (AIListObject *)setOrObject;
615 //Apply the image at the appropriate priority
616 [listObject setDisplayUserIcon:image
618 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
621 parentContact = [listObject parentContact];
624 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
625 NSEnumerator *enumerator;
626 // BOOL checkedForMetaContact = NO;
628 //Apply the image to each listObject at the appropriate priority
629 enumerator = [(NSSet *)setOrObject objectEnumerator];
630 while ((listObject = [enumerator nextObject])) {
633 //These objects all have the same unique ID so will all also have the same meta contact; just check once
634 if (!checkedForMetaContact) {
635 parentContact = [listObject parentContact];
636 if (parentContact == listObject) parentContact = nil;
637 checkedForMetaContact = YES;
641 [listObject setDisplayUserIcon:image
643 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
649 [parentContact setDisplayUserIcon:image
651 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
655 //No further need for the dictionary entries
656 [trackingDict removeObjectForKey:tagNumber];
658 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
659 [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
660 [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
666 * @brief Queue an asynchronous image fetch for person associated with inObject
668 * Image lookups are done asynchronously. This allows other processing to be done between image calls, improving the perceived
669 * speed. [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
670 * and the asynchronous lookup therefore hung the problem. Submitted as radar 3977541.]
672 * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
673 * the number of accounts on that service. We therefore aggregate the lookups to lower the address book search
674 * and image/data creation overhead.
676 * @param person The ABPerson to fetch the image from
677 * @param inObject The AIListObject with which to ultimately associate the image
679 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
685 uniqueId = [person uniqueId];
687 //Check if we already have a tag for the loading of another object with the same
689 if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
691 NSMutableSet *objectSet;
693 previousValue = [trackingDict objectForKey:tagNumber];
695 if ([previousValue isKindOfClass:[AIListObject class]]) {
696 //If the old value is just a listObject, create an array with the old object
698 objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
700 //Store the array in the tracking dict
701 [trackingDict setObject:objectSet forKey:tagNumber];
703 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
704 //Add the new object to the previously-created array
705 [(NSMutableSet *)previousValue addObject:inObject];
709 //Begin the image load
710 tag = [person beginLoadingImageDataForClient:self];
711 tagNumber = [NSNumber numberWithInt:tag];
713 //We need to be able to take a tagNumber and retrieve the object
714 [trackingDict setObject:inObject forKey:tagNumber];
716 //We also want to take a person's uniqueID and potentially find an existing tag number
717 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
718 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];
722 #pragma mark Searching
724 * @brief Find an ABPerson corresponding to an AIListObject
726 * @param inObject The object for which it search
727 * @result An ABPerson is one is found, or nil if none is found
729 - (ABPerson *)personForListObject:(AIListObject *)inObject
731 ABPerson *person = nil;
732 NSString *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
733 ABRecord *record = nil;
736 record = [sharedAddressBook recordForUniqueId:uniqueID];
738 if (record && [record isKindOfClass:[ABPerson class]]) {
739 person = (ABPerson *)record;
741 if ([inObject isKindOfClass:[AIMetaContact class]]) {
742 NSEnumerator *enumerator;
743 AIListContact *listContact;
745 //Search for an ABPerson for each listContact within the metaContact; first one we find is
747 enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
748 while ((listContact = [enumerator nextObject]) && (person == nil)) {
749 person = [self personForListObject:listContact];
753 NSString *UID = [inObject UID];
754 NSString *serviceID = [[inObject service] serviceID];
756 person = [self _searchForUID:UID serviceID:serviceID];
758 /* If we don't find anything yet, look at alternative service possibilities:
762 if ([serviceID isEqualToString:@"AIM"]) {
763 person = [self _searchForUID:UID serviceID:@"ICQ"];
764 } else if ([serviceID isEqualToString:@"ICQ"]) {
765 person = [self _searchForUID:UID serviceID:@"AIM"];
774 * @brief Find an ABPerson for a given UID and serviceID combination
776 * Uses our addressBookDict cache created in rebuildAddressBook.
778 * @param UID The UID for the contact
779 * @param serviceID The serviceID for the contact
780 * @result A corresponding <tt>ABPerson</tt>
782 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
784 ABPerson *person = nil;
787 if ([serviceID isEqualToString:@"Mac"]) {
788 dict = [addressBookDict objectForKey:@"AIM"];
790 } else if ([serviceID isEqualToString:@"GTalk"]) {
791 dict = [addressBookDict objectForKey:@"Jabber"];
793 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
794 dict = [addressBookDict objectForKey:@"Jabber"];
796 } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
797 dict = [addressBookDict objectForKey:@"Yahoo!"];
800 dict = [addressBookDict objectForKey:serviceID];
804 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
806 person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
813 - (NSSet *)contactsForPerson:(ABPerson *)person
815 NSArray *allServiceKeys = [serviceDict allKeys];
817 NSMutableSet *contactSet = [NSMutableSet set];
818 NSEnumerator *servicesEnumerator;
819 ABMultiValue *emails;
822 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
824 emails = [person valueForProperty:kABEmailProperty];
825 emailsCount = [emails count];
827 for (i = 0; i < emailsCount ; i++) {
830 email = [emails valueAtIndex:i];
831 if ([email hasSuffix:@"@mac.com"]) {
832 //Retrieve all appropriate contacts
833 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
837 //Add them to our set
838 [contactSet unionSet:contacts];
843 //Now go through the instant messaging keys
844 servicesEnumerator = [allServiceKeys objectEnumerator];
845 while ((serviceID = [servicesEnumerator nextObject])) {
846 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
850 //An ABPerson may have multiple names; iterate through them
851 names = [person valueForProperty:addressBookKey];
852 nameCount = [names count];
854 //Continue to the next serviceID immediately if no names are found
855 if (nameCount == 0) continue;
857 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
858 [serviceID isEqualToString:@"ICQ"]);
859 BOOL isJabber = [serviceID isEqualToString:@"Jabber"] ||
860 [serviceID isEqualToString:@"XMPP"];
862 for (i = 0 ; i < nameCount ; i++) {
863 NSString *UID = [[names valueAtIndex:i] compactedString];
866 serviceID = serviceIDForOscarUID(UID);
868 } else if (isJabber) {
869 serviceID = serviceIDForJabberUID(UID);
872 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
876 //Add them to our set
877 [contactSet unionSet:contacts];
885 #pragma mark Address book changed
887 * @brief Address book changed externally
889 * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
891 - (void)addressBookChanged:(NSNotification *)notification
893 /* In case of a single person, these will be NSStrings.
894 * In case of more then one, they are will be NSArrays containing NSStrings.
896 id addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
897 NSMutableSet *allModifiedPeople = [[NSMutableSet alloc] init];
898 ABPerson *me = [sharedAddressBook me];
899 BOOL modifiedMe = NO;;
901 //Delay listObjectNotifications to speed up metaContact creation
902 [[adium contactController] delayListObjectNotifications];
904 //Addition of new records
905 if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
906 NSArray *peopleToAdd;
908 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
909 //We are dealing with multiple records
910 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
912 //We have only one record
913 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
916 [allModifiedPeople addObjectsFromArray:peopleToAdd];
917 [self addToAddressBookDict:peopleToAdd];
920 //Modification of existing records
921 if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
922 NSArray *peopleToAdd;
924 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
925 //We are dealing with multiple records
926 [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
927 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
929 //We have only one record
930 [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
931 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
934 [allModifiedPeople addObjectsFromArray:peopleToAdd];
935 [self addToAddressBookDict:peopleToAdd];
938 //Deletion of existing records
939 if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
940 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
941 //We are dealing with multiple records
942 [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
944 //We have only one record
945 [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
948 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
951 NSEnumerator *peopleEnumerator;
954 //Do appropriate updates for each updated ABPerson
955 peopleEnumerator = [allModifiedPeople objectEnumerator];
956 while ((person = [peopleEnumerator nextObject])) {
961 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
962 [[adium contactController] updateContacts:[self contactsForPerson:person]
966 //Update us if appropriate
968 [self updateSelfIncludingIcon:YES];
971 //Stop delaying list object notifications since we are done
972 [[adium contactController] endListObjectNotificationsDelay];
973 [allModifiedPeople release];
977 * @brief Update all existing contacts and accounts
979 - (void)updateAllContacts
981 [[adium contactController] updateAllListObjectsForObserver:self];
982 [self updateSelfIncludingIcon:YES];
986 * @brief Account list changed: Update all existing accounts
988 - (void)accountListChanged:(NSNotification *)notification
990 [self updateSelfIncludingIcon:NO];
994 * @brief Update all existing accounts
996 * We use the "me" card to determine the default icon and account display name
998 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
1002 //Begin loading image data for the "me" address book entry, if one exists
1004 if ((me = [sharedAddressBook me])) {
1006 //Default buddy icon
1008 //Begin the image load
1009 meTag = [me beginLoadingImageDataForClient:self];
1012 //Set account display names
1014 NSString *myDisplayName, *myPhonetic = nil;
1016 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
1018 NSEnumerator *accountsArray = [[[adium accountController] accounts] objectEnumerator];
1021 while ((account = [accountsArray nextObject])) {
1022 if (![account isTemporary]) {
1023 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
1025 priorityLevel:Low_Priority];
1028 [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
1030 priorityLevel:Low_Priority];
1035 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
1036 forKey:KEY_ACCOUNT_DISPLAY_NAME]
1037 forGroup:GROUP_ACCOUNT_STATUS];
1043 NSLog(@"ABIntegration: Caught %@", exc);
1047 #pragma mark Address book caching
1049 * @brief rebuild our address book lookup dictionary
1051 - (void)rebuildAddressBookDict
1053 //Delay listObjectNotifications to speed up metaContact creation
1054 [[adium contactController] delayListObjectNotifications];
1056 [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
1058 [self addToAddressBookDict:[sharedAddressBook people]];
1060 //Stop delaying list object notifications since we are done
1061 [[adium contactController] endListObjectNotificationsDelay];
1066 * @brief Service ID for an OSCAR UID
1068 * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
1069 * because we may have a .Mac, an ICQ, or an AIM name in the field
1071 NSString* serviceIDForOscarUID(NSString *UID)
1073 NSString *serviceID;
1075 const char firstCharacter = [UID characterAtIndex:0];
1077 //Determine service based on UID
1078 if ([UID hasSuffix:@"@mac.com"]) {
1080 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
1090 * @brief Service ID for a Jabber UID
1092 * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
1093 * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
1095 NSString* serviceIDForJabberUID(NSString *UID)
1097 NSString *serviceID;
1099 if ([UID hasSuffix:@"@gmail.com"] ||
1100 [UID hasSuffix:@"@googlemail.com"]) {
1101 serviceID = @"GTalk";
1102 } else if ([UID hasSuffix:@"@livejournal.com"]) {
1103 serviceID = @"LiveJournal";
1105 serviceID = @"Jabber";
1113 * @brief add people to our address book lookup dictionary
1115 * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
1116 * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
1117 * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
1118 * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
1121 * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
1122 * create metaContacts baesd on this information.
1124 - (void)addToAddressBookDict:(NSArray *)people
1126 NSEnumerator *peopleEnumerator;
1127 NSArray *allServiceKeys = [serviceDict allKeys];
1130 peopleEnumerator = [people objectEnumerator];
1131 while ((person = [peopleEnumerator nextObject])) {
1132 NSEnumerator *servicesEnumerator;
1133 NSString *serviceID;
1135 NSMutableArray *UIDsArray = [NSMutableArray array];
1136 NSMutableArray *servicesArray = [NSMutableArray array];
1138 NSMutableDictionary *dict;
1139 ABMultiValue *emails;
1142 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1144 emails = [person valueForProperty:kABEmailProperty];
1145 emailsCount = [emails count];
1147 for (i = 0; i < emailsCount ; i++) {
1150 email = [emails valueAtIndex:i];
1151 if ([email hasSuffix:@"@mac.com"]) {
1153 //@mac.com UIDs go into the AIM dictionary
1154 if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1155 dict = [[[NSMutableDictionary alloc] init] autorelease];
1156 [addressBookDict setObject:dict forKey:@"AIM"];
1159 [dict setObject:[person uniqueId] forKey:email];
1161 //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1162 [UIDsArray addObject:email];
1163 [servicesArray addObject:@"Mac"];
1168 //Now go through the instant messaging keys
1169 servicesEnumerator = [allServiceKeys objectEnumerator];
1170 while ((serviceID = [servicesEnumerator nextObject])) {
1171 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
1172 ABMultiValue *names;
1175 //An ABPerson may have multiple names; iterate through them
1176 names = [person valueForProperty:addressBookKey];
1177 nameCount = [names count];
1179 //Continue to the next serviceID immediately if no names are found
1180 if (nameCount == 0) continue;
1182 //One or more names were found, so we'll need a dictionary
1183 if (!(dict = [addressBookDict objectForKey:serviceID])) {
1184 dict = [[NSMutableDictionary alloc] init];
1185 [addressBookDict setObject:dict forKey:serviceID];
1189 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
1190 [serviceID isEqualToString:@"ICQ"]);
1191 BOOL isJabber = [serviceID isEqualToString:@"Jabber"] ||
1192 [serviceID isEqualToString:@"XMPP"];
1194 for (i = 0 ; i < nameCount ; i++) {
1195 NSString *UID = [[names valueAtIndex:i] compactedString];
1197 [dict setObject:[person uniqueId] forKey:UID];
1199 [UIDsArray addObject:UID];
1202 serviceID = serviceIDForOscarUID(UID);
1204 } else if (isJabber) {
1205 serviceID = serviceIDForJabberUID(UID);
1208 [servicesArray addObject:serviceID];
1213 if (([UIDsArray count] > 1) && createMetaContacts) {
1214 /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1215 [[adium contactController] groupUIDs:UIDsArray
1216 forServices:servicesArray];
1222 * @brief remove people from our address book lookup dictionary
1224 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1226 NSEnumerator *enumerator;
1227 NSArray *allServiceKeys = [serviceDict allKeys];
1230 enumerator = [uniqueIDs objectEnumerator];
1231 while ((uniqueID = [enumerator nextObject])) {
1232 NSEnumerator *servicesEnumerator;
1233 NSString *serviceID;
1234 NSMutableDictionary *dict;
1236 //The same person may have multiple services; iterate through them and remove each one.
1237 servicesEnumerator = [allServiceKeys objectEnumerator];
1238 while ((serviceID = [servicesEnumerator nextObject])) {
1239 NSEnumerator *keysEnumerator;
1242 dict = [addressBookDict objectForKey:serviceID];
1244 keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1246 //The same person may have multiple accounts from the same service; we should remove them all.
1247 while ((key = [keysEnumerator nextObject])) {
1248 [dict removeObjectForKey:key];
1254 #pragma mark AB contextual menu
1256 * @brief Validate menu item
1258 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1260 BOOL hasABEntry = ([self personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1263 if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1264 result = hasABEntry;
1265 else if ([menuItem isEqual:addToABContexualMenuItem])
1266 result = !hasABEntry;
1272 * @brief Shows the selected contact in Address Book
1274 - (void)showInAddressBook
1276 ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1277 NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1278 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1282 * @brief Edits the selected contact in Address Book
1284 - (void)editInAddressBook
1286 ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1287 NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1288 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1291 - (void)addToAddressBook
1293 AIListObject *contact = [[adium menuController] currentContextMenuObject];
1294 NSString *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1296 if (serviceProperty) {
1297 ABPerson *person = [[ABPerson alloc] init];
1300 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1301 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1303 NSString *UID = [contact formattedUID];
1305 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1307 ABMutableMultiValue *multiValue;
1309 while((c = [containedContactEnu nextObject]))
1311 multiValue = [[ABMutableMultiValue alloc] init];
1312 UID = [c formattedUID];
1313 serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1315 //Set the IM property
1316 [multiValue addValue:UID withLabel:serviceProperty];
1317 [person setValue:multiValue forKey:serviceProperty];
1319 [multiValue release];
1324 [person setImageData:[contact userIconData]];
1327 [person setValue:[contact notes] forKey:kABNoteProperty];
1329 //Add our newly created person to the AB database
1330 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1331 //Save the uid of the new person
1332 [contact setPreference:[person uniqueId]
1333 forKey:KEY_AB_UNIQUE_ID
1334 group:PREF_GROUP_ADDRESSBOOK];
1336 //Ask the user whether it would like to edit the new contact
1337 int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1338 CONTACT_ADDED_SUCCESS_Message,
1339 AILocalizedString(@"Yes", nil),
1340 AILocalizedString(@"No", nil), nil, UID);
1342 if (result == NSOKButton) {
1343 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1344 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1348 NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);