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);
608 //Address book can feed us giant images, which we really don't want to keep around
609 NSSize size = [image size];
610 if (size.width > 96 || size.height > 96)
611 image = [image imageByScalingToSize:NSMakeSize(96, 96)];
612 [image setDataRetained:YES];
614 //Get the object from our tracking dictionary
615 setOrObject = [trackingDict objectForKey:tagNumber];
617 if ([setOrObject isKindOfClass:[AIListObject class]]) {
618 listObject = (AIListObject *)setOrObject;
620 //Apply the image at the appropriate priority
621 [listObject setDisplayUserIcon:image
623 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
626 parentContact = [listObject parentContact];
629 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
630 NSEnumerator *enumerator;
631 // BOOL checkedForMetaContact = NO;
633 //Apply the image to each listObject at the appropriate priority
634 enumerator = [(NSSet *)setOrObject objectEnumerator];
635 while ((listObject = [enumerator nextObject])) {
638 //These objects all have the same unique ID so will all also have the same meta contact; just check once
639 if (!checkedForMetaContact) {
640 parentContact = [listObject parentContact];
641 if (parentContact == listObject) parentContact = nil;
642 checkedForMetaContact = YES;
646 [listObject setDisplayUserIcon:image
648 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
654 [parentContact setDisplayUserIcon:image
656 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
660 //No further need for the dictionary entries
661 [trackingDict removeObjectForKey:tagNumber];
663 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
664 [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
665 [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
671 * @brief Queue an asynchronous image fetch for person associated with inObject
673 * Image lookups are done asynchronously. This allows other processing to be done between image calls, improving the perceived
674 * speed. [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
675 * and the asynchronous lookup therefore hung the problem. Submitted as radar 3977541.]
677 * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
678 * the number of accounts on that service. We therefore aggregate the lookups to lower the address book search
679 * and image/data creation overhead.
681 * @param person The ABPerson to fetch the image from
682 * @param inObject The AIListObject with which to ultimately associate the image
684 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
690 uniqueId = [person uniqueId];
692 //Check if we already have a tag for the loading of another object with the same
694 if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
696 NSMutableSet *objectSet;
698 previousValue = [trackingDict objectForKey:tagNumber];
700 if ([previousValue isKindOfClass:[AIListObject class]]) {
701 //If the old value is just a listObject, create an array with the old object
703 objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
705 //Store the array in the tracking dict
706 [trackingDict setObject:objectSet forKey:tagNumber];
708 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
709 //Add the new object to the previously-created array
710 [(NSMutableSet *)previousValue addObject:inObject];
714 //Begin the image load
715 tag = [person beginLoadingImageDataForClient:self];
716 tagNumber = [NSNumber numberWithInt:tag];
718 //We need to be able to take a tagNumber and retrieve the object
719 [trackingDict setObject:inObject forKey:tagNumber];
721 //We also want to take a person's uniqueID and potentially find an existing tag number
722 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
723 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];
727 #pragma mark Searching
729 * @brief Find an ABPerson corresponding to an AIListObject
731 * @param inObject The object for which it search
732 * @result An ABPerson is one is found, or nil if none is found
734 - (ABPerson *)personForListObject:(AIListObject *)inObject
736 ABPerson *person = nil;
737 NSString *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
738 ABRecord *record = nil;
741 record = [sharedAddressBook recordForUniqueId:uniqueID];
743 if (record && [record isKindOfClass:[ABPerson class]]) {
744 person = (ABPerson *)record;
746 if ([inObject isKindOfClass:[AIMetaContact class]]) {
747 NSEnumerator *enumerator;
748 AIListContact *listContact;
750 //Search for an ABPerson for each listContact within the metaContact; first one we find is
752 enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
753 while ((listContact = [enumerator nextObject]) && (person == nil)) {
754 person = [self personForListObject:listContact];
758 NSString *UID = [inObject UID];
759 NSString *serviceID = [[inObject service] serviceID];
761 person = [self _searchForUID:UID serviceID:serviceID];
763 /* If we don't find anything yet, look at alternative service possibilities:
767 if ([serviceID isEqualToString:@"AIM"]) {
768 person = [self _searchForUID:UID serviceID:@"ICQ"];
769 } else if ([serviceID isEqualToString:@"ICQ"]) {
770 person = [self _searchForUID:UID serviceID:@"AIM"];
779 * @brief Find an ABPerson for a given UID and serviceID combination
781 * Uses our addressBookDict cache created in rebuildAddressBook.
783 * @param UID The UID for the contact
784 * @param serviceID The serviceID for the contact
785 * @result A corresponding <tt>ABPerson</tt>
787 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
789 ABPerson *person = nil;
792 if ([serviceID isEqualToString:@"Mac"]) {
793 dict = [addressBookDict objectForKey:@"AIM"];
795 } else if ([serviceID isEqualToString:@"GTalk"]) {
796 dict = [addressBookDict objectForKey:@"Jabber"];
798 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
799 dict = [addressBookDict objectForKey:@"Jabber"];
801 } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
802 dict = [addressBookDict objectForKey:@"Yahoo!"];
805 dict = [addressBookDict objectForKey:serviceID];
809 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
811 person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
818 - (NSSet *)contactsForPerson:(ABPerson *)person
820 NSArray *allServiceKeys = [serviceDict allKeys];
822 NSMutableSet *contactSet = [NSMutableSet set];
823 NSEnumerator *servicesEnumerator;
824 ABMultiValue *emails;
827 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
829 emails = [person valueForProperty:kABEmailProperty];
830 emailsCount = [emails count];
832 for (i = 0; i < emailsCount ; i++) {
835 email = [emails valueAtIndex:i];
836 if ([email hasSuffix:@"@mac.com"]) {
837 //Retrieve all appropriate contacts
838 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
842 //Add them to our set
843 [contactSet unionSet:contacts];
848 //Now go through the instant messaging keys
849 servicesEnumerator = [allServiceKeys objectEnumerator];
850 while ((serviceID = [servicesEnumerator nextObject])) {
851 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
855 //An ABPerson may have multiple names; iterate through them
856 names = [person valueForProperty:addressBookKey];
857 nameCount = [names count];
859 //Continue to the next serviceID immediately if no names are found
860 if (nameCount == 0) continue;
862 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
863 [serviceID isEqualToString:@"ICQ"]);
864 BOOL isJabber = [serviceID isEqualToString:@"Jabber"] ||
865 [serviceID isEqualToString:@"XMPP"];
867 for (i = 0 ; i < nameCount ; i++) {
868 NSString *UID = [[names valueAtIndex:i] compactedString];
871 serviceID = serviceIDForOscarUID(UID);
873 } else if (isJabber) {
874 serviceID = serviceIDForJabberUID(UID);
877 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
881 //Add them to our set
882 [contactSet unionSet:contacts];
890 #pragma mark Address book changed
892 * @brief Address book changed externally
894 * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
896 - (void)addressBookChanged:(NSNotification *)notification
898 /* In case of a single person, these will be NSStrings.
899 * In case of more then one, they are will be NSArrays containing NSStrings.
901 id addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
902 NSMutableSet *allModifiedPeople = [[NSMutableSet alloc] init];
903 ABPerson *me = [sharedAddressBook me];
904 BOOL modifiedMe = NO;;
906 //Delay listObjectNotifications to speed up metaContact creation
907 [[adium contactController] delayListObjectNotifications];
909 //Addition of new records
910 if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
911 NSArray *peopleToAdd;
913 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
914 //We are dealing with multiple records
915 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
917 //We have only one record
918 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
921 [allModifiedPeople addObjectsFromArray:peopleToAdd];
922 [self addToAddressBookDict:peopleToAdd];
925 //Modification of existing records
926 if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
927 NSArray *peopleToAdd;
929 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
930 //We are dealing with multiple records
931 [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
932 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
934 //We have only one record
935 [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
936 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
939 [allModifiedPeople addObjectsFromArray:peopleToAdd];
940 [self addToAddressBookDict:peopleToAdd];
943 //Deletion of existing records
944 if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
945 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
946 //We are dealing with multiple records
947 [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
949 //We have only one record
950 [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
953 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
956 NSEnumerator *peopleEnumerator;
959 //Do appropriate updates for each updated ABPerson
960 peopleEnumerator = [allModifiedPeople objectEnumerator];
961 while ((person = [peopleEnumerator nextObject])) {
966 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
967 [[adium contactController] updateContacts:[self contactsForPerson:person]
971 //Update us if appropriate
973 [self updateSelfIncludingIcon:YES];
976 //Stop delaying list object notifications since we are done
977 [[adium contactController] endListObjectNotificationsDelay];
978 [allModifiedPeople release];
982 * @brief Update all existing contacts and accounts
984 - (void)updateAllContacts
986 [[adium contactController] updateAllListObjectsForObserver:self];
987 [self updateSelfIncludingIcon:YES];
991 * @brief Account list changed: Update all existing accounts
993 - (void)accountListChanged:(NSNotification *)notification
995 [self updateSelfIncludingIcon:NO];
999 * @brief Update all existing accounts
1001 * We use the "me" card to determine the default icon and account display name
1003 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
1007 //Begin loading image data for the "me" address book entry, if one exists
1009 if ((me = [sharedAddressBook me])) {
1011 //Default buddy icon
1013 //Begin the image load
1014 meTag = [me beginLoadingImageDataForClient:self];
1017 //Set account display names
1019 NSString *myDisplayName, *myPhonetic = nil;
1021 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
1023 NSEnumerator *accountsArray = [[[adium accountController] accounts] objectEnumerator];
1026 while ((account = [accountsArray nextObject])) {
1027 if (![account isTemporary]) {
1028 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
1030 priorityLevel:Low_Priority];
1033 [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
1035 priorityLevel:Low_Priority];
1040 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
1041 forKey:KEY_ACCOUNT_DISPLAY_NAME]
1042 forGroup:GROUP_ACCOUNT_STATUS];
1048 NSLog(@"ABIntegration: Caught %@", exc);
1052 #pragma mark Address book caching
1054 * @brief rebuild our address book lookup dictionary
1056 - (void)rebuildAddressBookDict
1058 //Delay listObjectNotifications to speed up metaContact creation
1059 [[adium contactController] delayListObjectNotifications];
1061 [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
1063 [self addToAddressBookDict:[sharedAddressBook people]];
1065 //Stop delaying list object notifications since we are done
1066 [[adium contactController] endListObjectNotificationsDelay];
1071 * @brief Service ID for an OSCAR UID
1073 * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
1074 * because we may have a .Mac, an ICQ, or an AIM name in the field
1076 NSString* serviceIDForOscarUID(NSString *UID)
1078 NSString *serviceID;
1080 const char firstCharacter = [UID characterAtIndex:0];
1082 //Determine service based on UID
1083 if ([UID hasSuffix:@"@mac.com"]) {
1085 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
1095 * @brief Service ID for a Jabber UID
1097 * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
1098 * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
1100 NSString* serviceIDForJabberUID(NSString *UID)
1102 NSString *serviceID;
1104 if ([UID hasSuffix:@"@gmail.com"] ||
1105 [UID hasSuffix:@"@googlemail.com"]) {
1106 serviceID = @"GTalk";
1107 } else if ([UID hasSuffix:@"@livejournal.com"]) {
1108 serviceID = @"LiveJournal";
1110 serviceID = @"Jabber";
1118 * @brief add people to our address book lookup dictionary
1120 * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
1121 * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
1122 * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
1123 * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
1126 * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
1127 * create metaContacts baesd on this information.
1129 - (void)addToAddressBookDict:(NSArray *)people
1131 NSEnumerator *peopleEnumerator;
1132 NSArray *allServiceKeys = [serviceDict allKeys];
1135 peopleEnumerator = [people objectEnumerator];
1136 while ((person = [peopleEnumerator nextObject])) {
1137 NSEnumerator *servicesEnumerator;
1138 NSString *serviceID;
1140 NSMutableArray *UIDsArray = [NSMutableArray array];
1141 NSMutableArray *servicesArray = [NSMutableArray array];
1143 NSMutableDictionary *dict;
1144 ABMultiValue *emails;
1147 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1149 emails = [person valueForProperty:kABEmailProperty];
1150 emailsCount = [emails count];
1152 for (i = 0; i < emailsCount ; i++) {
1155 email = [emails valueAtIndex:i];
1156 if ([email hasSuffix:@"@mac.com"]) {
1158 //@mac.com UIDs go into the AIM dictionary
1159 if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1160 dict = [[[NSMutableDictionary alloc] init] autorelease];
1161 [addressBookDict setObject:dict forKey:@"AIM"];
1164 [dict setObject:[person uniqueId] forKey:email];
1166 //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1167 [UIDsArray addObject:email];
1168 [servicesArray addObject:@"Mac"];
1173 //Now go through the instant messaging keys
1174 servicesEnumerator = [allServiceKeys objectEnumerator];
1175 while ((serviceID = [servicesEnumerator nextObject])) {
1176 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
1177 ABMultiValue *names;
1180 //An ABPerson may have multiple names; iterate through them
1181 names = [person valueForProperty:addressBookKey];
1182 nameCount = [names count];
1184 //Continue to the next serviceID immediately if no names are found
1185 if (nameCount == 0) continue;
1187 //One or more names were found, so we'll need a dictionary
1188 if (!(dict = [addressBookDict objectForKey:serviceID])) {
1189 dict = [[NSMutableDictionary alloc] init];
1190 [addressBookDict setObject:dict forKey:serviceID];
1194 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
1195 [serviceID isEqualToString:@"ICQ"]);
1196 BOOL isJabber = [serviceID isEqualToString:@"Jabber"] ||
1197 [serviceID isEqualToString:@"XMPP"];
1199 for (i = 0 ; i < nameCount ; i++) {
1200 NSString *UID = [[names valueAtIndex:i] compactedString];
1202 [dict setObject:[person uniqueId] forKey:UID];
1204 [UIDsArray addObject:UID];
1207 serviceID = serviceIDForOscarUID(UID);
1209 } else if (isJabber) {
1210 serviceID = serviceIDForJabberUID(UID);
1213 [servicesArray addObject:serviceID];
1218 if (([UIDsArray count] > 1) && createMetaContacts) {
1219 /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1220 [[adium contactController] groupUIDs:UIDsArray
1221 forServices:servicesArray];
1227 * @brief remove people from our address book lookup dictionary
1229 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1231 NSEnumerator *enumerator;
1232 NSArray *allServiceKeys = [serviceDict allKeys];
1235 enumerator = [uniqueIDs objectEnumerator];
1236 while ((uniqueID = [enumerator nextObject])) {
1237 NSEnumerator *servicesEnumerator;
1238 NSString *serviceID;
1239 NSMutableDictionary *dict;
1241 //The same person may have multiple services; iterate through them and remove each one.
1242 servicesEnumerator = [allServiceKeys objectEnumerator];
1243 while ((serviceID = [servicesEnumerator nextObject])) {
1244 NSEnumerator *keysEnumerator;
1247 dict = [addressBookDict objectForKey:serviceID];
1249 keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1251 //The same person may have multiple accounts from the same service; we should remove them all.
1252 while ((key = [keysEnumerator nextObject])) {
1253 [dict removeObjectForKey:key];
1259 #pragma mark AB contextual menu
1261 * @brief Validate menu item
1263 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1265 BOOL hasABEntry = ([self personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1268 if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1269 result = hasABEntry;
1270 else if ([menuItem isEqual:addToABContexualMenuItem])
1271 result = !hasABEntry;
1277 * @brief Shows the selected contact in Address Book
1279 - (void)showInAddressBook
1281 ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1282 NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1283 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1287 * @brief Edits the selected contact in Address Book
1289 - (void)editInAddressBook
1291 ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1292 NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1293 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1296 - (void)addToAddressBook
1298 AIListObject *contact = [[adium menuController] currentContextMenuObject];
1299 NSString *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1301 if (serviceProperty) {
1302 ABPerson *person = [[ABPerson alloc] init];
1305 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1306 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1308 NSString *UID = [contact formattedUID];
1310 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1312 ABMutableMultiValue *multiValue;
1314 while((c = [containedContactEnu nextObject]))
1316 multiValue = [[ABMutableMultiValue alloc] init];
1317 UID = [c formattedUID];
1318 serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1320 //Set the IM property
1321 [multiValue addValue:UID withLabel:serviceProperty];
1322 [person setValue:multiValue forKey:serviceProperty];
1324 [multiValue release];
1329 [person setImageData:[contact userIconData]];
1332 [person setValue:[contact notes] forKey:kABNoteProperty];
1334 //Add our newly created person to the AB database
1335 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1336 //Save the uid of the new person
1337 [contact setPreference:[person uniqueId]
1338 forKey:KEY_AB_UNIQUE_ID
1339 group:PREF_GROUP_ADDRESSBOOK];
1341 //Ask the user whether it would like to edit the new contact
1342 int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1343 CONTACT_ADDED_SUCCESS_Message,
1344 AILocalizedString(@"Yes", nil),
1345 AILocalizedString(@"No", nil), nil, UID);
1347 if (result == NSOKButton) {
1348 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1349 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1353 NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);