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 <Adium/AIUserIcons.h>
27 #import <AIUtilities/AIAttributedStringAdditions.h>
28 #import <AIUtilities/AIDictionaryAdditions.h>
29 #import <AIUtilities/AIMutableOwnerArray.h>
30 #import <AIUtilities/AIStringAdditions.h>
31 #import <AIUtilities/OWAddressBookAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
34 #import "AIAddressBookUserIconSource.h"
36 #define IMAGE_LOOKUP_INTERVAL 0.01
37 #define SHOW_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Show In Address Book", "Show In Address Book Contextual Menu")
38 #define EDIT_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Edit In Address Book", "Edit In Address Book Contextual Menu")
39 #define ADD_TO_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Add To Address Book", "Add To Address Book Contextual Menu")
41 #define CONTACT_ADDED_SUCCESS_TITLE AILocalizedString(@"Success", "Title of a panel shown after adding successfully adding a contact to the address book.")
42 #define CONTACT_ADDED_SUCCESS_Message AILocalizedString(@"%@ had been successfully added to the Address Book.\nWould you like to edit the card now?", nil)
43 #define CONTACT_ADDED_ERROR_TITLE AILocalizedString(@"Error", nil)
44 #define CONTACT_ADDED_ERROR_Message AILocalizedString(@"An error had occurred while adding %@ to the Address Book.", nil)
46 #define KEY_ADDRESS_BOOK_ACTIONS_INSTALLED @"Adium:Installed Adress Book Actions 1.2"
48 @interface ESAddressBookIntegrationPlugin(PRIVATE)
49 + (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID;
51 - (void)updateAllContacts;
52 - (void)updateSelfIncludingIcon:(BOOL)includeIcon;
53 - (void)preferencesChanged:(NSNotification *)notification;
54 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic;
55 - (void)rebuildAddressBookDict;
56 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject;
57 - (void)showInAddressBook;
58 - (void)editInAddressBook;
59 - (void)addToAddressBookDict:(NSArray *)people;
60 - (void)removeFromAddressBookDict:(NSArray *)UIDs;
61 - (void)installAddressBookActions;
65 * @class ESAddressBookIntegrationPlugin
66 * @brief Provides Apple Address Book integration
68 * This class allows Adium to seamlessly interact with the Apple Address Book, pulling names and icons, storing icons
69 * if desired, and generating metaContacts based on screen name grouping. It relies upon cards having screen names listed
70 * in the appropriate service fields in the address book.
72 @implementation ESAddressBookIntegrationPlugin
74 static ABAddressBook *sharedAddressBook = nil;
75 static NSMutableDictionary *addressBookDict = nil;
77 static NSDictionary *serviceDict = nil;
79 NSString* serviceIDForOscarUID(NSString *UID);
80 NSString* serviceIDForJabberUID(NSString *UID);
83 * @brief Install plugin
85 * This plugin finishes installing in adiumFinishedLaunching:
90 addressBookDict = nil;
91 createMetaContacts = NO;
93 //Configure our preferences
94 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:AB_DISPLAYFORMAT_DEFAULT_PREFS
95 forClass:[self class]]
96 forGroup:PREF_GROUP_ADDRESSBOOK];
98 //We want the enableImport preference immediately (without waiting for the preferences observer to be registered in adiumFinishedLaunching:)
99 enableImport = [[[adium preferenceController] preferenceForKey:KEY_AB_ENABLE_IMPORT
100 group:PREF_GROUP_ADDRESSBOOK] boolValue];
101 advancedPreferences = [[ESAddressBookIntegrationAdvancedPreferences preferencePane] retain];
103 //Services dictionary
104 serviceDict = [[NSDictionary dictionaryWithObjectsAndKeys:kABAIMInstantProperty,@"AIM",
105 kABJabberInstantProperty,@"Jabber",
106 kABMSNInstantProperty,@"MSN",
107 kABYahooInstantProperty,@"Yahoo!",
108 kABICQInstantProperty,@"ICQ",nil] retain];
110 //Shared Address Book
111 [sharedAddressBook release]; sharedAddressBook = [[ABAddressBook sharedAddressBook] retain];
113 //Create our contextual menus
114 showInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_IN_AB_CONTEXTUAL_MENU_TITLE
115 action:@selector(showInAddressBook)
116 keyEquivalent:@""] autorelease];
117 [showInABContextualMenuItem setTarget:self];
119 editInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:EDIT_IN_AB_CONTEXTUAL_MENU_TITLE
120 action:@selector(editInAddressBook)
121 keyEquivalent:@""] autorelease];
122 [editInABContextualMenuItem setTarget:self];
123 [editInABContextualMenuItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
124 [editInABContextualMenuItem setAlternate:YES];
126 addToABContexualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:ADD_TO_AB_CONTEXTUAL_MENU_TITLE
127 action:@selector(addToAddressBook)
128 keyEquivalent:@""] autorelease];
129 [addToABContexualMenuItem setTarget:self];
132 [[adium menuController] addContextualMenuItem:addToABContexualMenuItem toLocation:Context_Contact_Action];
133 [[adium menuController] addContextualMenuItem:showInABContextualMenuItem toLocation:Context_Contact_Action];
134 [[adium menuController] addContextualMenuItem:editInABContextualMenuItem toLocation:Context_Contact_Action];
136 [self installAddressBookActions];
138 //Wait for Adium to finish launching before we build the address book so the contact list will be ready
139 [[adium notificationCenter] addObserver:self
140 selector:@selector(adiumFinishedLaunching:)
141 name:AIApplicationDidFinishLoadingNotification
144 //Update self immediately so the information is available to plugins and interface elements as they load
145 [self updateSelfIncludingIcon:YES];
148 - (void)installAddressBookActions
150 NSNumber *installedActions = [[NSUserDefaults standardUserDefaults] objectForKey:KEY_ADDRESS_BOOK_ACTIONS_INSTALLED];
152 if (!installedActions || ![installedActions boolValue]) {
153 NSEnumerator *enumerator = [[NSArray arrayWithObjects:@"AIM", @"MSN", @"Yahoo", @"ICQ", @"Jabber", @"SMS", nil] objectEnumerator];
155 NSFileManager *fileManager = [NSFileManager defaultManager];
156 NSArray *libraryDirectoryArray;
157 NSString *libraryDirectory, *pluginDirectory;
159 libraryDirectoryArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
160 if ([libraryDirectoryArray count]) {
161 libraryDirectory = [libraryDirectoryArray objectAtIndex:0];
164 //Ridiculous safety since everyone should have a Library folder...
165 libraryDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
166 [fileManager createDirectoryAtPath:libraryDirectory attributes:nil];
169 pluginDirectory = [[libraryDirectory stringByAppendingPathComponent:@"Address Book Plug-Ins"] stringByAppendingPathComponent:@"/"];
170 [fileManager createDirectoryAtPath:pluginDirectory attributes:nil];
172 while ((name = [enumerator nextObject])) {
173 NSString *fullName = [NSString stringWithFormat:@"AdiumAddressBookAction_%@",name];
174 NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:fullName ofType:@"scpt"];
177 NSString *destination = [pluginDirectory stringByAppendingPathComponent:[fullName stringByAppendingPathExtension:@"scpt"]];
178 [fileManager trashFileAtPath:destination];
179 [fileManager copyPath:path
183 //Remove the old xtra if installed
184 [fileManager trashFileAtPath:[pluginDirectory stringByAppendingPathComponent:
185 [NSString stringWithFormat:@"%@-Adium.scpt",name]]];
187 AILogWithSignature(@"Warning: Could not find %@",self, fullName);
191 [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES]
192 forKey:KEY_ADDRESS_BOOK_ACTIONS_INSTALLED];
197 * @brief Uninstall plugin
199 - (void)uninstallPlugin
201 [[adium contactController] unregisterListObjectObserver:self];
202 [[adium notificationCenter] removeObserver:self];
210 [serviceDict release]; serviceDict = nil;
212 [sharedAddressBook release]; sharedAddressBook = nil;
213 [addressBookUserIconSource release]; addressBookUserIconSource = nil;
215 [[adium preferenceController] unregisterPreferenceObserver:self];
216 [[adium notificationCenter] removeObserver:self];
222 * @brief Adium finished launching
224 * Register our observers for the address book changing externally and for the account list changing.
225 * Register our preference observers. This will trigger initial building of the address book dictionary.
227 - (void)adiumFinishedLaunching:(NSNotification *)notification
229 //Observe external address book changes
230 [[NSNotificationCenter defaultCenter] addObserver:self
231 selector:@selector(addressBookChanged:)
232 name:kABDatabaseChangedExternallyNotification
235 //Observe account changes
236 [[adium notificationCenter] addObserver:self
237 selector:@selector(accountListChanged:)
238 name:Account_ListChanged
241 //Observe preferences changes
242 id<AIPreferenceController> preferenceController = [adium preferenceController];
243 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_ADDRESSBOOK];
244 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_USERICONS];
246 addressBookUserIconSource = [[AIAddressBookUserIconSource alloc] init];
247 [AIUserIcons registerUserIconSource:addressBookUserIconSource];
251 * @brief Used as contacts are created and icons are changed.
253 * When first created, load a contact's address book information from our dict.
254 * When an icon as a status object changes, if desired, write the changed icon out to the appropriate AB card.
256 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
258 //Just stop here if we don't have an address book dict to work with
259 if (!addressBookDict) return nil;
261 //We handle accounts separately; doing updates here causes chaos in addition to being inefficient.
262 if ([inObject isKindOfClass:[AIAccount class]]) return nil;
264 NSSet *modifiedAttributes = nil;
266 if (inModifiedKeys == nil) { //Only perform this when updating for all list objects
267 ABPerson *person = [[self class] personForListObject:inObject];
269 if ([AIUserIcons userIconSource:addressBookUserIconSource changeWouldBeRelevantForObject:inObject]) {
270 //This object's user icon would be changed if we have a new icon Update!
271 [addressBookUserIconSource queueDelayedFetchOfImageForPerson:person object:inObject];
276 //Load the name if appropriate
277 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
278 NSString *displayName, *phoneticName = nil;
280 displayNameArray = [inObject displayArrayForKey:@"Display Name"];
282 displayName = [self nameForPerson:person phonetic:&phoneticName];
285 NSString *oldValue = [displayNameArray objectWithOwner:self];
286 if (!oldValue || ![oldValue isEqualToString:displayName]) {
287 [displayNameArray setObject:displayName withOwner:self];
288 modifiedAttributes = [NSSet setWithObject:@"Display Name"];
292 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"];
295 oldValue = [phoneticNameArray objectWithOwner:self];
296 if (!oldValue || ![oldValue isEqualToString:phoneticName]) {
297 [phoneticNameArray setObject:phoneticName withOwner:self];
298 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
301 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
303 //Clear any stored value
304 if ([phoneticNameArray objectWithOwner:self]) {
305 [displayNameArray setObject:nil withOwner:self];
306 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
311 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
313 displayNameArray = [inObject displayArrayForKey:@"Display Name"
316 //Clear any stored value
317 if ([displayNameArray objectWithOwner:self]) {
318 [displayNameArray setObject:nil withOwner:self];
319 modifiedAttributes = [NSSet setWithObject:@"Display Name"];
322 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
324 //Clear any stored value
325 if ([phoneticNameArray objectWithOwner:self]) {
326 [displayNameArray setObject:nil withOwner:self];
327 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
332 //If we changed anything, request an update of the alias / long display name
333 if (modifiedAttributes) {
334 [[adium notificationCenter] postNotificationName:Contact_ApplyDisplayName
336 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:silent]
341 } else if (automaticSync && [inModifiedKeys containsObject:KEY_USER_ICON]) {
343 //Only update when the serverside icon changes if there is no Adium preference overriding it
344 if (![AIUserIcons manuallySetUserIconDataForObject:inObject]) {
346 ABPerson *person = [[self class] personForListObject:inObject];
348 if (person && (person != [sharedAddressBook me])) {
349 [person setImageData:[inObject userIconData]];
351 [[sharedAddressBook class] cancelPreviousPerformRequestsWithTarget:sharedAddressBook
352 selector:@selector(save)
354 [sharedAddressBook performSelector:@selector(save)
361 return modifiedAttributes;
365 * @brief Return the name of an ABPerson in the way Adium should display it
367 * @param person An <tt>ABPerson</tt>
368 * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
369 * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
371 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
373 NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;
375 NSString *displayName = nil;
378 // If the record is for a company, return the company name if present
379 if ((flags = [person valueForProperty:kABPersonFlags])) {
380 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
381 NSString *companyName = [person valueForProperty:kABOrganizationProperty];
382 if (companyName && [companyName length]) {
388 firstName = [person valueForProperty:kABFirstNameProperty];
389 middleName = [person valueForProperty:kABMiddleNameProperty];
390 lastName = [person valueForProperty:kABLastNameProperty];
391 phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
392 phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
395 if (useMiddleName && middleName)
396 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
398 if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
399 displayName = nickName;
401 } else if (!lastName || (displayFormat == First)) {
402 /* If no last name is available, use the first name */
403 displayName = firstName;
404 if (phonetic != NULL) *phonetic = phoneticFirstName;
406 } else if (!firstName) {
407 /* If no first name is available, use the last name */
408 displayName = lastName;
409 if (phonetic != NULL) *phonetic = phoneticLastName;
412 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
414 /* Look to the preference setting */
415 switch (displayFormat) {
417 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
419 *phonetic = [NSString stringWithFormat:@"%@ %@",
420 (phoneticFirstName ? phoneticFirstName : firstName),
421 (phoneticLastName ? phoneticLastName : lastName)];
425 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName];
427 *phonetic = [NSString stringWithFormat:@"%@, %@",
428 (phoneticLastName ? phoneticLastName : lastName),
429 (phoneticFirstName ? phoneticFirstName : firstName)];
432 case LastFirstNoComma:
433 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName];
435 *phonetic = [NSString stringWithFormat:@"%@ %@",
436 (phoneticLastName ? phoneticLastName : lastName),
437 (phoneticFirstName ? phoneticFirstName : firstName)];
440 case FirstLastInitial:
441 displayName = [NSString stringWithFormat:@"%@ %@",firstName,[lastName substringToIndex:1]];
443 *phonetic = [NSString stringWithFormat:@"%@ %@",
444 (phoneticFirstName ? phoneticFirstName : firstName),
445 [lastName substringToIndex:1]];
448 //No action; handled before we reach the switch statement
457 * @brief Observe preference changes
459 * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
460 * option is toggled, as metaContacts are created while building the dict.
462 * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
464 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
465 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
467 if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
468 BOOL oldCreateMetaContacts = createMetaContacts;
470 //load new displayFormat
471 enableImport = [[prefDict objectForKey:KEY_AB_ENABLE_IMPORT] boolValue];
472 displayFormat = [[prefDict objectForKey:KEY_AB_DISPLAYFORMAT] intValue];
473 automaticSync = [[prefDict objectForKey:KEY_AB_IMAGE_SYNC] boolValue];
474 useNickName = [[prefDict objectForKey:KEY_AB_USE_NICKNAME] boolValue];
475 useMiddleName = [[prefDict objectForKey:KEY_AB_USE_MIDDLE] boolValue];
477 createMetaContacts = [[prefDict objectForKey:KEY_AB_CREATE_METACONTACTS] boolValue];
480 //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
481 [self rebuildAddressBookDict];
483 //Register ourself as a listObject observer, which will update all objects
484 [[adium contactController] registerListObjectObserver:self];
486 //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin
488 //This isn't the first time through
490 //If we weren't creating meta contacts before but we are now
491 if (!oldCreateMetaContacts && createMetaContacts) {
493 Build the address book dictionary, which will also trigger metacontact grouping as appropriate
494 Delay to the next run loop to give better UI responsiveness
496 [self performSelector:@selector(rebuildAddressBookDict)
501 //Update all contacts, which will update objects and then our "me" card information
502 [self updateAllContacts];
505 } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
506 //Set an icon to the address book
507 ABPerson *person = [[self class] personForListObject:object];
510 [person setImageData:[object userIconData]];
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;
567 * @brief Called when the address book completes an asynchronous image lookup
569 * @param inData NSData representing an NSImage
570 * @param tag A tag indicating the lookup with which this call is associated.
572 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
575 [[adium preferenceController] setPreference:inData
576 forKey:KEY_DEFAULT_USER_ICON
577 group:GROUP_ACCOUNT_STATUS];
582 #pragma mark Searching
584 * @brief Find an ABPerson corresponding to an AIListObject
586 * @param inObject The object for which it search
587 * @result An ABPerson is one is found, or nil if none is found
589 + (ABPerson *)personForListObject:(AIListObject *)inObject
591 ABPerson *person = nil;
592 NSString *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
593 ABRecord *record = nil;
596 record = [sharedAddressBook recordForUniqueId:uniqueID];
598 if (record && [record isKindOfClass:[ABPerson class]]) {
599 person = (ABPerson *)record;
601 if ([inObject isKindOfClass:[AIMetaContact class]]) {
602 NSEnumerator *enumerator;
603 AIListContact *listContact;
605 //Search for an ABPerson for each listContact within the metaContact; first one we find is
607 enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
608 while ((listContact = [enumerator nextObject]) && (person == nil)) {
609 person = [self personForListObject:listContact];
613 NSString *UID = [inObject UID];
614 NSString *serviceID = [[inObject service] serviceID];
616 person = [self _searchForUID:UID serviceID:serviceID];
618 /* If we don't find anything yet, look at alternative service possibilities:
622 if ([serviceID isEqualToString:@"AIM"]) {
623 person = [self _searchForUID:UID serviceID:@"ICQ"];
624 } else if ([serviceID isEqualToString:@"ICQ"]) {
625 person = [self _searchForUID:UID serviceID:@"AIM"];
634 * @brief Find an ABPerson for a given UID and serviceID combination
636 * Uses our addressBookDict cache created in rebuildAddressBook.
638 * @param UID The UID for the contact
639 * @param serviceID The serviceID for the contact
640 * @result A corresponding <tt>ABPerson</tt>
642 + (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
644 ABPerson *person = nil;
647 if ([serviceID isEqualToString:@"Mac"]) {
648 dict = [addressBookDict objectForKey:@"AIM"];
650 } else if ([serviceID isEqualToString:@"GTalk"]) {
651 dict = [addressBookDict objectForKey:@"Jabber"];
653 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
654 dict = [addressBookDict objectForKey:@"Jabber"];
656 } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
657 dict = [addressBookDict objectForKey:@"Yahoo!"];
660 dict = [addressBookDict objectForKey:serviceID];
664 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
666 person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
675 - (NSSet *)contactsForPerson:(ABPerson *)person
677 NSArray *allServiceKeys = [serviceDict allKeys];
679 NSMutableSet *contactSet = [NSMutableSet set];
680 NSEnumerator *servicesEnumerator;
681 ABMultiValue *emails;
684 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
686 emails = [person valueForProperty:kABEmailProperty];
687 emailsCount = [emails count];
689 for (i = 0; i < emailsCount ; i++) {
692 email = [emails valueAtIndex:i];
693 if ([email hasSuffix:@"@mac.com"]) {
694 //Retrieve all appropriate contacts
695 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
699 //Add them to our set
700 [contactSet unionSet:contacts];
705 //Now go through the instant messaging keys
706 servicesEnumerator = [allServiceKeys objectEnumerator];
707 while ((serviceID = [servicesEnumerator nextObject])) {
708 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
712 //An ABPerson may have multiple names; iterate through them
713 names = [person valueForProperty:addressBookKey];
714 nameCount = [names count];
716 //Continue to the next serviceID immediately if no names are found
717 if (nameCount == 0) continue;
719 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
720 [serviceID isEqualToString:@"ICQ"]);
721 BOOL isJabber = [serviceID isEqualToString:@"Jabber"] ||
722 [serviceID isEqualToString:@"XMPP"];
724 for (i = 0 ; i < nameCount ; i++) {
725 NSString *UID = [[names valueAtIndex:i] compactedString];
728 serviceID = serviceIDForOscarUID(UID);
730 } else if (isJabber) {
731 serviceID = serviceIDForJabberUID(UID);
734 NSSet *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
738 //Add them to our set
739 [contactSet unionSet:contacts];
747 #pragma mark Address book changed
749 * @brief Address book changed externally
751 * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
753 - (void)addressBookChanged:(NSNotification *)notification
755 /* In case of a single person, these will be NSStrings.
756 * In case of more then one, they are will be NSArrays containing NSStrings.
758 id addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
759 NSMutableSet *allModifiedPeople = [[NSMutableSet alloc] init];
760 ABPerson *me = [sharedAddressBook me];
761 BOOL modifiedMe = NO;;
763 //Delay listObjectNotifications to speed up metaContact creation
764 [[adium contactController] delayListObjectNotifications];
766 //Addition of new records
767 if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
768 NSArray *peopleToAdd;
770 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
771 //We are dealing with multiple records
772 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
774 //We have only one record
775 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
778 [allModifiedPeople addObjectsFromArray:peopleToAdd];
779 [self addToAddressBookDict:peopleToAdd];
782 //Modification of existing records
783 if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
784 NSArray *peopleToAdd;
786 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
787 //We are dealing with multiple records
788 [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
789 peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
791 //We have only one record
792 [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
793 peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
796 [allModifiedPeople addObjectsFromArray:peopleToAdd];
797 [self addToAddressBookDict:peopleToAdd];
800 //Deletion of existing records
801 if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
802 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
803 //We are dealing with multiple records
804 [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
806 //We have only one record
807 [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
810 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
813 NSEnumerator *peopleEnumerator;
816 //Do appropriate updates for each updated ABPerson
817 peopleEnumerator = [allModifiedPeople objectEnumerator];
818 while ((person = [peopleEnumerator nextObject])) {
823 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
824 [[adium contactController] updateContacts:[self contactsForPerson:person]
828 //Update us if appropriate
830 [self updateSelfIncludingIcon:YES];
833 //Stop delaying list object notifications since we are done
834 [[adium contactController] endListObjectNotificationsDelay];
835 [allModifiedPeople release];
839 * @brief Update all existing contacts and accounts
841 - (void)updateAllContacts
843 [[adium contactController] updateAllListObjectsForObserver:self];
844 [self updateSelfIncludingIcon:YES];
848 * @brief Account list changed: Update all existing accounts
850 - (void)accountListChanged:(NSNotification *)notification
852 [self updateSelfIncludingIcon:NO];
856 * @brief Update all existing accounts
858 * We use the "me" card to determine the default icon and account display name
860 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
864 //Begin loading image data for the "me" address book entry, if one exists
866 if ((me = [sharedAddressBook me])) {
870 //Begin the image load
871 meTag = [me beginLoadingImageDataForClient:self];
874 //Set account display names
876 NSString *myDisplayName, *myPhonetic = nil;
878 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
880 NSEnumerator *accountsArray = [[[adium accountController] accounts] objectEnumerator];
883 while ((account = [accountsArray nextObject])) {
884 if (![account isTemporary]) {
885 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
887 priorityLevel:Low_Priority];
890 [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
892 priorityLevel:Low_Priority];
897 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
898 forKey:KEY_ACCOUNT_DISPLAY_NAME]
899 forGroup:GROUP_ACCOUNT_STATUS];
905 NSLog(@"ABIntegration: Caught %@", exc);
909 #pragma mark Address book caching
911 * @brief rebuild our address book lookup dictionary
913 - (void)rebuildAddressBookDict
915 //Delay listObjectNotifications to speed up metaContact creation
916 [[adium contactController] delayListObjectNotifications];
918 [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
920 [self addToAddressBookDict:[sharedAddressBook people]];
922 //Stop delaying list object notifications since we are done
923 [[adium contactController] endListObjectNotificationsDelay];
928 * @brief Service ID for an OSCAR UID
930 * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
931 * because we may have a .Mac, an ICQ, or an AIM name in the field
933 NSString* serviceIDForOscarUID(NSString *UID)
937 const char firstCharacter = [UID characterAtIndex:0];
939 //Determine service based on UID
940 if ([UID hasSuffix:@"@mac.com"]) {
942 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
952 * @brief Service ID for a Jabber UID
954 * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
955 * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
957 NSString* serviceIDForJabberUID(NSString *UID)
961 if ([UID hasSuffix:@"@gmail.com"] ||
962 [UID hasSuffix:@"@googlemail.com"]) {
963 serviceID = @"GTalk";
964 } else if ([UID hasSuffix:@"@livejournal.com"]) {
965 serviceID = @"LiveJournal";
967 serviceID = @"Jabber";
975 * @brief add people to our address book lookup dictionary
977 * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
978 * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
979 * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
980 * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
983 * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
984 * create metaContacts baesd on this information.
986 - (void)addToAddressBookDict:(NSArray *)people
988 NSEnumerator *peopleEnumerator;
989 NSArray *allServiceKeys = [serviceDict allKeys];
992 peopleEnumerator = [people objectEnumerator];
993 while ((person = [peopleEnumerator nextObject])) {
994 NSEnumerator *servicesEnumerator;
997 NSMutableArray *UIDsArray = [NSMutableArray array];
998 NSMutableArray *servicesArray = [NSMutableArray array];
1000 NSMutableDictionary *dict;
1001 ABMultiValue *emails;
1004 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1006 emails = [person valueForProperty:kABEmailProperty];
1007 emailsCount = [emails count];
1009 for (i = 0; i < emailsCount ; i++) {
1012 email = [emails valueAtIndex:i];
1013 if ([email hasSuffix:@"@mac.com"]) {
1015 //@mac.com UIDs go into the AIM dictionary
1016 if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1017 dict = [[[NSMutableDictionary alloc] init] autorelease];
1018 [addressBookDict setObject:dict forKey:@"AIM"];
1021 [dict setObject:[person uniqueId] forKey:email];
1023 //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1024 [UIDsArray addObject:email];
1025 [servicesArray addObject:@"Mac"];
1030 //Now go through the instant messaging keys
1031 servicesEnumerator = [allServiceKeys objectEnumerator];
1032 while ((serviceID = [servicesEnumerator nextObject])) {
1033 NSString *addressBookKey = [serviceDict objectForKey:serviceID];
1034 ABMultiValue *names;
1037 //An ABPerson may have multiple names; iterate through them
1038 names = [person valueForProperty:addressBookKey];
1039 nameCount = [names count];
1041 //Continue to the next serviceID immediately if no names are found
1042 if (nameCount == 0) continue;
1044 //One or more names were found, so we'll need a dictionary
1045 if (!(dict = [addressBookDict objectForKey:serviceID])) {
1046 dict = [[NSMutableDictionary alloc] init];
1047 [addressBookDict setObject:dict forKey:serviceID];
1051 BOOL isOSCAR = ([serviceID isEqualToString:@"AIM"] ||
1052 [serviceID isEqualToString:@"ICQ"]);
1053 BOOL isJabber = [serviceID isEqualToString:@"Jabber"] ||
1054 [serviceID isEqualToString:@"XMPP"];
1056 for (i = 0 ; i < nameCount ; i++) {
1057 NSString *UID = [[names valueAtIndex:i] compactedString];
1059 [dict setObject:[person uniqueId] forKey:UID];
1061 [UIDsArray addObject:UID];
1064 serviceID = serviceIDForOscarUID(UID);
1066 } else if (isJabber) {
1067 serviceID = serviceIDForJabberUID(UID);
1070 [servicesArray addObject:serviceID];
1075 if (([UIDsArray count] > 1) && createMetaContacts) {
1076 /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1077 [[adium contactController] groupUIDs:UIDsArray
1078 forServices:servicesArray];
1084 * @brief remove people from our address book lookup dictionary
1086 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1088 NSEnumerator *enumerator;
1089 NSArray *allServiceKeys = [serviceDict allKeys];
1092 enumerator = [uniqueIDs objectEnumerator];
1093 while ((uniqueID = [enumerator nextObject])) {
1094 NSEnumerator *servicesEnumerator;
1095 NSString *serviceID;
1096 NSMutableDictionary *dict;
1098 //The same person may have multiple services; iterate through them and remove each one.
1099 servicesEnumerator = [allServiceKeys objectEnumerator];
1100 while ((serviceID = [servicesEnumerator nextObject])) {
1101 NSEnumerator *keysEnumerator;
1104 dict = [addressBookDict objectForKey:serviceID];
1106 keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1108 //The same person may have multiple accounts from the same service; we should remove them all.
1109 while ((key = [keysEnumerator nextObject])) {
1110 [dict removeObjectForKey:key];
1116 #pragma mark AB contextual menu
1118 * @brief Validate menu item
1120 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1122 BOOL hasABEntry = ([[self class] personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1125 if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1126 result = hasABEntry;
1127 else if ([menuItem isEqual:addToABContexualMenuItem])
1128 result = !hasABEntry;
1134 * @brief Shows the selected contact in Address Book
1136 - (void)showInAddressBook
1138 ABPerson *selectedPerson = [[self class] personForListObject:[[adium menuController] currentContextMenuObject]];
1139 NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1140 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1144 * @brief Edits the selected contact in Address Book
1146 - (void)editInAddressBook
1148 ABPerson *selectedPerson = [[self class] personForListObject:[[adium menuController] currentContextMenuObject]];
1149 NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1150 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1153 - (void)addToAddressBook
1155 AIListObject *contact = [[adium menuController] currentContextMenuObject];
1156 NSString *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1158 if (serviceProperty) {
1159 ABPerson *person = [[ABPerson alloc] init];
1162 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1163 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1165 NSString *UID = [contact formattedUID];
1167 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1169 ABMutableMultiValue *multiValue;
1171 while((c = [containedContactEnu nextObject]))
1173 multiValue = [[ABMutableMultiValue alloc] init];
1174 UID = [c formattedUID];
1175 serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1177 //Set the IM property
1178 [multiValue addValue:UID withLabel:serviceProperty];
1179 [person setValue:multiValue forKey:serviceProperty];
1181 [multiValue release];
1186 [person setImageData:[contact userIconData]];
1189 [person setValue:[contact notes] forKey:kABNoteProperty];
1191 //Add our newly created person to the AB database
1192 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1193 //Save the uid of the new person
1194 [contact setPreference:[person uniqueId]
1195 forKey:KEY_AB_UNIQUE_ID
1196 group:PREF_GROUP_ADDRESSBOOK];
1198 //Ask the user whether it would like to edit the new contact
1199 int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1200 CONTACT_ADDED_SUCCESS_Message,
1201 AILocalizedString(@"Yes", nil),
1202 AILocalizedString(@"No", nil), nil, UID);
1204 if (result == NSOKButton) {
1205 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1206 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1210 NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);