header, schmeader
[adiumx.git] / Source / ESAddressBookIntegrationPlugin.m
blob3b34c3a023b316896b0c462c6d80e3bcece1f40f
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
17 #import <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIContactControllerProtocol.h>
19 #import "ESAddressBookIntegrationPlugin.h"
20 #import <Adium/AIMenuControllerProtocol.h>
21 #import <AIUtilities/AIAttributedStringAdditions.h>
22 #import <AIUtilities/AIDictionaryAdditions.h>
23 #import <AIUtilities/AIMutableOwnerArray.h>
24 #import <AIUtilities/AIStringAdditions.h>
25 #import <AIUtilities/OWAddressBookAdditions.h>
26 #import <AIUtilities/AIExceptionHandlingUtilities.h>
27 #import <AIUtilities/AIFileManagerAdditions.h>
28 #import <Adium/AIAccount.h>
29 #import <Adium/AIListObject.h>
30 #import <Adium/AIMetaContact.h>
31 #import <Adium/AIService.h>
33 #define IMAGE_LOOKUP_INTERVAL   0.01
34 #define SHOW_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Show In Address Book", "Show In Address Book Contextual Menu")
35 #define EDIT_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Edit In Address Book", "Edit In Address Book Contextual Menu")
36 #define ADD_TO_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Add To Address Book", "Add To Address Book Contextual Menu")
38 #define CONTACT_ADDED_SUCCESS_TITLE             AILocalizedString(@"Success", "Title of a panel shown after adding successfully adding a contact to the address book.")
39 #define CONTACT_ADDED_SUCCESS_Message   AILocalizedString(@"%@ had been successfully added to the Address Book.\nWould you like to edit the card now?", nil)
40 #define CONTACT_ADDED_ERROR_TITLE               AILocalizedString(@"Error", nil)
41 #define CONTACT_ADDED_ERROR_Message             AILocalizedString(@"An error had occurred while adding %@ to the Address Book.", nil)
43 @interface ESAddressBookIntegrationPlugin(PRIVATE)
44 - (void)updateAllContacts;
45 - (void)updateSelfIncludingIcon:(BOOL)includeIcon;
46 - (void)preferencesChanged:(NSNotification *)notification;
47 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic;
48 - (ABPerson *)personForListObject:(AIListObject *)inObject;
49 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID;
50 - (void)rebuildAddressBookDict;
51 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject;
52 - (void)showInAddressBook;
53 - (void)editInAddressBook;
54 - (void)addToAddressBookDict:(NSArray *)people;
55 - (void)removeFromAddressBookDict:(NSArray *)UIDs;
56 - (void)installAddressBookActions;
57 @end
59 /*!
60  * @class ESAddressBookIntegrationPlugin
61  * @brief Provides Apple Address Book integration
62  *
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.
66  */
67 @implementation ESAddressBookIntegrationPlugin
69 static  ABAddressBook   *sharedAddressBook = nil;
70 static  NSDictionary    *serviceDict = nil;
72 NSString* serviceIDForOscarUID(NSString *UID);
73 NSString* serviceIDForJabberUID(NSString *UID);
75 /*!
76  * @brief Install plugin
77  *
78  * This plugin finishes installing in adiumFinishedLaunching:
79  */
80 - (void)installPlugin
82     meTag = -1;
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];
93         
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];
103         
104     //Services dictionary
105     serviceDict = [[NSDictionary dictionaryWithObjectsAndKeys:kABAIMInstantProperty,@"AIM",
106                                                                 kABJabberInstantProperty,@"Jabber",
107                                                                 kABMSNInstantProperty,@"MSN",
108                                                                 kABYahooInstantProperty,@"Yahoo!",
109                                                                 kABICQInstantProperty,@"ICQ",nil] retain];
110         
111         //Shared Address Book
112         [sharedAddressBook release]; sharedAddressBook = [[ABAddressBook sharedAddressBook] retain];
113         
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];
119         
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];
126         
127         addToABContexualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:ADD_TO_AB_CONTEXTUAL_MENU_TITLE
128                                                                                                                                                                          action:@selector(addToAddressBook)
129                                                                                                                                                           keyEquivalent:@""] autorelease];
130         [addToABContexualMenuItem setTarget:self];
131         
132         //Install our menues
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];
136         
137         [self installAddressBookActions];
138         
139         //Wait for Adium to finish launching before we build the address book so the contact list will be ready
140         [[adium notificationCenter] addObserver:self
141                                                                    selector:@selector(adiumFinishedLaunching:)
142                                                                            name:Adium_CompletedApplicationLoad
143                                                                          object:nil];
144         
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"];
152         
153         if (!installedActions || ![installedActions boolValue]) {
154                 NSEnumerator  *enumerator = [[NSArray arrayWithObjects:@"AIM", @"MSN", @"Yahoo", @"ICQ", @"Jabber", @"SMS", nil] objectEnumerator];
155                 NSString          *name;
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];
164                 } else {
165                         //Ridiculous safety since everyone should have a Library folder...
166                         libraryDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
167                         [fileManager createDirectoryAtPath:libraryDirectory attributes:nil];
168                 }
170                 pluginDirectory = [[libraryDirectory stringByAppendingPathComponent:@"Address Book Plug-Ins"] stringByAppendingPathComponent:@"/"];
171                 [fileManager createDirectoryAtPath:pluginDirectory attributes:nil];
172                 
173                 while ((name = [enumerator nextObject])) {
174                         NSString *fullName = [NSString stringWithFormat:@"AdiumAddressBookAction_%@",name];
175                         NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:fullName ofType:@"scpt"];
177                         if (path) {
178                                 [fileManager copyPath:path
179                                                            toPath:[pluginDirectory stringByAppendingPathComponent:[fullName stringByAppendingPathExtension:@"scpt"]]
180                                                           handler:NULL];
181                                 
182                                 //Remove the old xtra if installed
183                                 [fileManager trashFileAtPath:[pluginDirectory stringByAppendingPathComponent:
184                                         [NSString stringWithFormat:@"%@-Adium.scpt",name]]];
185                         } else {
186                                 NSLog(@"%@: Could not find %@",self, fullName);
187                         }
188                 }
190                 [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES]
191                                                                                                   forKey:@"Adium:Installed Adress Book Actions"];
192         }
196  * @brief Uninstall plugin
197  */
198 - (void)uninstallPlugin
200     [[adium contactController] unregisterListObjectObserver:self];
201     [[adium notificationCenter] removeObserver:self];
205  * @brief Deallocate
206  */
207 - (void)dealloc
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];
219         [super dealloc];
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.
227  */
228 - (void)adiumFinishedLaunching:(NSNotification *)notification
229 {       
230     //Observe external address book changes
231     [[NSNotificationCenter defaultCenter] addObserver:self
232                                                                                          selector:@selector(addressBookChanged:)
233                                                                                                  name:kABDatabaseChangedExternallyNotification
234                                                                                            object:nil];
236     //Observe account changes
237     [[adium notificationCenter] addObserver:self
238                                                                    selector:@selector(accountListChanged:)
239                                                                            name:Account_ListChanged
240                                                                          object:nil];
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.
253  */
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;
258         
259         //We handle accounts separately; doing updates here causes chaos in addition to being inefficient.
260         if ([inObject isKindOfClass:[AIAccount class]]) return nil;
261         
262         NSSet           *modifiedAttributes = nil;
263         
264     if (inModifiedKeys == nil) { //Only perform this when updating for all list objects
265         ABPerson *person = [self personForListObject:inObject];
266                 
267                 if (person) {
268                         [self queueDelayedFetchOfImageForPerson:person object:inObject];
270                         if (enableImport) {
271                                 //Load the name if appropriate
272                                 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
273                                 NSString                        *displayName, *phoneticName = nil;
274                                 
275                                 displayNameArray = [inObject displayArrayForKey:@"Display Name"];
276                                 
277                                 displayName = [self nameForPerson:person phonetic:&phoneticName];
278                                 
279                                 //Apply the values 
280                                 NSString *oldValue = [displayNameArray objectWithOwner:self];
281                                 if (!oldValue || ![oldValue isEqualToString:displayName]) {
282                                         [displayNameArray setObject:displayName withOwner:self];
283                                         modifiedAttributes = [NSSet setWithObject:@"Display Name"];
284                                 }
285                                 
286                                 if (phoneticName) {
287                                         phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"];
289                                         //Apply the values 
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];
294                                         }
295                                 } else {
296                                         phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
297                                                                                                                           create:NO];
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];
302                                         }                                       
303                                 }
305                         } else {
306                                 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
307                                 
308                                 displayNameArray = [inObject displayArrayForKey:@"Display Name"
309                                                                                                                  create:NO];
311                                 //Clear any stored value
312                                 if ([displayNameArray objectWithOwner:self]) {
313                                         [displayNameArray setObject:nil withOwner:self];
314                                         modifiedAttributes = [NSSet setWithObject:@"Display Name"];
315                                 }
316                                 
317                                 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
318                                                                                                                   create:NO];
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];
323                                 }                                       
324                                 
325                         }
327                         //If we changed anything, request an update of the alias / long display name
328                         if (modifiedAttributes) {
329                                 [[adium notificationCenter] postNotificationName:Contact_ApplyDisplayName
330                                                                                                                   object:inObject
331                                                                                                                 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:silent]
332                                                                                                                                                                                          forKey:@"Notify"]];
333                         }
334                 }
335                 
336     } else if (automaticSync && [inModifiedKeys containsObject:KEY_USER_ICON]) {
337         
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]) {
340                         //Find the person
341                         ABPerson *person = [self personForListObject:inObject];
342                         
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"];
346                                 if (!userIconData) {
347                                         userIconData = [[inObject statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
348                                 }
349                                 
350                                 [person setImageData:userIconData];
351                         }
352                 }
353     }
354     
355     return modifiedAttributes;
359  * @brief Return the name of an ABPerson in the way Adium should display it
361  * @param person An <tt>ABPerson</tt>
362  * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
363  * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
364  */
365 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
367         NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;     
368         NSString *nickName;
369         NSString *displayName = nil;
370         NSNumber *flags;
371         
372         // If the record is for a company, return the company name if present
373         if ((flags = [person valueForProperty:kABPersonFlags])) {
374                 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
375                         NSString *companyName = [person valueForProperty:kABOrganizationProperty];
376                         if (companyName && [companyName length]) {
377                                 return companyName;
378                         }
379                 }
380         }
381                 
382         firstName = [person valueForProperty:kABFirstNameProperty];
383         middleName = [person valueForProperty:kABMiddleNameProperty];
384         lastName = [person valueForProperty:kABLastNameProperty];
385         phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
386         phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
387         
388         //
389         if (useMiddleName && middleName)
390                 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
392         if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
393                 displayName = nickName;
395         } else if (!lastName || (displayFormat == First)) {  
396                 /* If no last name is available, use the first name */
397                 displayName = firstName;
398                 if (phonetic != NULL) *phonetic = phoneticFirstName;
400         } else if (!firstName) {
401                 /* If no first name is available, use the last name */
402                 displayName = lastName;
403                 if (phonetic != NULL) *phonetic = phoneticLastName;
405         } else {
406                 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
408                 /* Look to the preference setting */
409                 switch (displayFormat) {
410                         case FirstLast:
411                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
412                                 if (havePhonetic) {
413                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
414                                                 (phoneticFirstName ? phoneticFirstName : firstName),
415                                                 (phoneticLastName ? phoneticLastName : lastName)];
416                                 }
417                                 break;
418                         case LastFirst:
419                                 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName]; 
420                                 if (havePhonetic) {
421                                         *phonetic = [NSString stringWithFormat:@"%@, %@",
422                                                 (phoneticLastName ? phoneticLastName : lastName),
423                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
424                                 }
425                                 break;
426                         case LastFirstNoComma:
427                                 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName]; 
428                                 if (havePhonetic) {
429                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
430                                                 (phoneticLastName ? phoneticLastName : lastName),
431                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
432                                 }                                       
433                                 break;
434                         case First:
435                                 //No action; handled before we reach the switch statement
436                                 break;
437                 }
438         }
440         return displayName;
444  * @brief Observe preference changes
446  * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
447  * option is toggled, as metaContacts are created while building the dict.
449  * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
450  */
451 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
452                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
454     if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
455                 BOOL                    oldCreateMetaContacts = createMetaContacts;
456                 NSNumber                *value;
457                 
458         //load new displayFormat
459                 if ((value = [prefDict objectForKey:KEY_AB_ENABLE_IMPORT])) enableImport = [value boolValue];
460         if ((value = [prefDict objectForKey:KEY_AB_DISPLAYFORMAT]))  displayFormat = [value intValue];
461         if ((value = [prefDict objectForKey:KEY_AB_IMAGE_SYNC])) automaticSync = [value boolValue];
462         if ((value = [prefDict objectForKey:KEY_AB_USE_NICKNAME])) useNickName = [value boolValue];
463                 if ((value = [prefDict objectForKey:KEY_AB_USE_MIDDLE])) useMiddleName = [value boolValue];
464                 if ((value = [prefDict objectForKey:KEY_AB_PREFER_ADDRESS_BOOK_IMAGES])) preferAddressBookImages = [value boolValue];
465                 if ((value = [prefDict objectForKey:KEY_AB_USE_IMAGES])) useABImages = [value boolValue];
467                 if ((value = [prefDict objectForKey:KEY_AB_CREATE_METACONTACTS])) createMetaContacts = [value boolValue];
468                 
469                 if (firstTime) {
470                         //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
471                         [self rebuildAddressBookDict];
472                         
473                         //Register ourself as a listObject observer, which will update all objects
474                         [[adium contactController] registerListObjectObserver:self];
475                         
476                         //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin                     
477                 } else {
478                         //This isn't the first time through
480                         //If we weren't creating meta contacts before but we are now
481                         if (!oldCreateMetaContacts && createMetaContacts) {
482                                 /*
483                                  Build the address book dictionary, which will also trigger metacontact grouping as appropriate
484                                  Delay to the next run loop to give better UI responsiveness
485                                  */
486                                 [self performSelector:@selector(rebuildAddressBookDict)
487                                                    withObject:nil
488                                                    afterDelay:0.0001];
489                         }
490                         
491                         //Update all contacts, which will update objects and then our "me" card information
492                         [self updateAllContacts];
493                 }
495     } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
496                 //Find the person
497                 ABPerson *person = [self personForListObject:object];
498                 
499                 if (person) {
500                         //Set the person's image to the inObject's serverside User Icon.
501                         NSData  *imageData = [object preferenceForKey:KEY_USER_ICON
502                                                                                                         group:PREF_GROUP_USERICONS
503                                                                         ignoreInheritedValues:YES];
504                         
505                         //If the pref is now nil, we should restore the address book back to the serverside icon if possible
506                         if (!imageData) {
507                                 imageData = [[object statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
508                         }
509                         
510                         [person setImageData:imageData];
511                 }
512         }
516  * @brief Returns the appropriate service for the property.
518  * @param property - an ABPerson property.
519  */
520 + (AIService *)serviceFromProperty:(NSString *)property
522         NSString        *serviceID = nil;
523         
524         if ([property isEqualToString:kABAIMInstantProperty])
525                 serviceID = @"AIM";
526         
527         else if ([property isEqualToString:kABICQInstantProperty])
528                 serviceID = @"ICQ";
529         
530         else if ([property isEqualToString:kABMSNInstantProperty])
531                 serviceID = @"MSN";
532         
533         else if ([property isEqualToString:kABJabberInstantProperty])
534                 serviceID = @"Jabber";
535         
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.
544  */
545 + (NSString *)propertyFromService:(AIService *)inService
547         NSString *result;
548         NSString *serviceID = [inService serviceID];
550         result = [serviceDict objectForKey:serviceID];
552         //Check for some special cases
553         if (!result) {
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;
560                 }
561         }
562         
563         return result;
566 #pragma mark Image data
569  * @brief Called when the address book completes an asynchronous image lookup
571  * @param inData NSData representing an NSImage
572  * @param tag A tag indicating the lookup with which this call is associated. We use a tracking dictionary, trackingDict, to associate this int back to a usable object.
573  */
574 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
576         if (tag == meTag) {
577                 [[adium preferenceController] setPreference:inData
578                                                                                          forKey:KEY_DEFAULT_USER_ICON 
579                                                                                           group:GROUP_ACCOUNT_STATUS];
580                 meTag = -1;
581                 
582         } else if (useABImages) {
583                 NSNumber                *tagNumber;
584                 NSImage                 *image;
585                 AIListObject    *listObject;
586 //              AIListContact   *parentContact;
587                 NSString                *uniqueID;
588                 id                              setOrObject;
589                 
590                 tagNumber = [NSNumber numberWithInt:tag];
591                 
592                 //Apply the image to the appropriate listObject
593                 image = (inData ? [[[NSImage alloc] initWithData:inData] autorelease] : nil);
594                 
595                 //Get the object from our tracking dictionary
596                 setOrObject = [trackingDict objectForKey:tagNumber];
597                 
598                 if ([setOrObject isKindOfClass:[AIListObject class]]) {
599                         listObject = (AIListObject *)setOrObject;
600                         
601                         //Apply the image at the appropriate priority
602                         [listObject setDisplayUserIcon:image
603                                                                  withOwner:self
604                                                          priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
606                         /*
607                         parentContact = [listObject parentContact];
608                          */
609                         
610                 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
611                         NSEnumerator    *enumerator;
612 //                      BOOL                    checkedForMetaContact = NO;
614                         //Apply the image to each listObject at the appropriate priority
615                         enumerator = [(NSSet *)setOrObject objectEnumerator];
616                         while ((listObject = [enumerator nextObject])) {
617                                 
618                                 /*
619                                 //These objects all have the same unique ID so will all also have the same meta contact; just check once
620                                 if (!checkedForMetaContact) {
621                                         parentContact = [listObject parentContact];
622                                         if (parentContact == listObject) parentContact = nil;
623                                         checkedForMetaContact = YES;
624                                 }
625                                 */
626                                 
627                                 [listObject setDisplayUserIcon:image
628                                                                          withOwner:self
629                                                                  priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
630                         }
631                 }
632                 
633                 /*
634                 if (parentContact) {
635                         [parentContact setDisplayUserIcon:image
636                                                                         withOwner:self
637                                                                 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];                        
638                 }
639                 */
640                 
641                 //No further need for the dictionary entries
642                 [trackingDict removeObjectForKey:tagNumber];
643                 
644                 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
645                         [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
646                         [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
647                 }
648         }
652  * @brief Queue an asynchronous image fetch for person associated with inObject
654  * Image lookups are done asynchronously.  This allows other processing to be done between image calls, improving the perceived
655  * speed.  [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
656  *                      and the asynchronous lookup therefore hung the problem.  Submitted as radar 3977541.]
658  * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
659  * the number of accounts on that service.  We therefore aggregate the lookups to lower the address book search
660  * and image/data creation overhead.
662  * @param person The ABPerson to fetch the image from
663  * @pram inObject The AIListObject with which to ultimately associate the image
664  */
665 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
667         int                             tag;
668         NSNumber                *tagNumber;
669         NSString                *uniqueId;
670         
671         uniqueId = [person uniqueId];
672         
673         //Check if we already have a tag for the loading of another object with the same
674         //internalObjectID
675         if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
676                 id                              previousValue;
677                 NSMutableSet    *objectSet;
678                 
679                 previousValue = [trackingDict objectForKey:tagNumber];
680                 
681                 if ([previousValue isKindOfClass:[AIListObject class]]) {
682                         //If the old value is just a listObject, create an array with the old object
683                         //and the new object
684                         objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
685                         
686                         //Store the array in the tracking dict
687                         [trackingDict setObject:objectSet forKey:tagNumber];
688                         
689                 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
690                         //Add the new object to the previously-created array
691                         [(NSMutableSet *)previousValue addObject:inObject];
692                 }
693                 
694         } else {
695                 //Begin the image load
696                 tag = [person beginLoadingImageDataForClient:self];
697                 tagNumber = [NSNumber numberWithInt:tag];
698                 
699                 //We need to be able to take a tagNumber and retrieve the object
700                 [trackingDict setObject:inObject forKey:tagNumber];
701                 
702                 //We also want to take a person's uniqueID and potentially find an existing tag number
703                 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
704                 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];
705         }
708 #pragma mark Searching
710  * @brief Find an ABPerson corresponding to an AIListObject
712  * @param inObject The object for which it search
713  * @result An ABPerson is one is found, or nil if none is found
714  */
715 - (ABPerson *)personForListObject:(AIListObject *)inObject
717         ABPerson        *person = nil;
718         NSString        *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
719         ABRecord        *record = nil;
720         
721         if (uniqueID)
722                 record = [sharedAddressBook recordForUniqueId:uniqueID];
723         
724         if (record && [record isKindOfClass:[ABPerson class]]) {
725                 person = (ABPerson *)record;
726         } else {
727                 if ([inObject isKindOfClass:[AIMetaContact class]]) {
728                         NSEnumerator    *enumerator;
729                         AIListContact   *listContact;
730                         
731                         //Search for an ABPerson for each listContact within the metaContact; first one we find is
732                         //the lucky winner.
733                         enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
734                         while ((listContact = [enumerator nextObject]) && (person == nil)) {
735                                 person = [self personForListObject:listContact];
736                         }
737                         
738                 } else {
739                         NSString                *UID = [inObject UID];
740                         NSString                *serviceID = [[inObject service] serviceID];
741                         
742                         person = [self _searchForUID:UID serviceID:serviceID];
743                         
744                         /* If we don't find anything yet, look at alternative service possibilities:
745                          *    AIM <--> ICQ
746                          */
747                         if (!person) {
748                                 if ([serviceID isEqualToString:@"AIM"]) {
749                                         person = [self _searchForUID:UID serviceID:@"ICQ"];
750                                 } else if ([serviceID isEqualToString:@"ICQ"]) {
751                                         person = [self _searchForUID:UID serviceID:@"AIM"];
752                                 }
753                         }
754                 }
755         }
756         return person;
760  * @brief Find an ABPerson for a given UID and serviceID combination
761  * 
762  * Uses our addressBookDict cache created in rebuildAddressBook.
764  * @param UID The UID for the contact
765  * @param serviceID The serviceID for the contact
766  * @result A corresponding <tt>ABPerson</tt>
767  */
768 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
770         ABPerson                *person = nil;
771         NSDictionary    *dict;
772         
773         if ([serviceID isEqualToString:@"Mac"]) {
774                 dict = [addressBookDict objectForKey:@"AIM"];
776         } else if ([serviceID isEqualToString:@"GTalk"]) {
777                 dict = [addressBookDict objectForKey:@"Jabber"];
779         } else if ([serviceID isEqualToString:@"LiveJournal"]) {
780                 dict = [addressBookDict objectForKey:@"Jabber"];
781                 
782         } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
783                 dict = [addressBookDict objectForKey:@"Yahoo!"];
784                 
785         } else {
786                 dict = [addressBookDict objectForKey:serviceID];
787         } 
788         
789         if (dict) {
790                 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
791                 if (uniqueID) {
792                         person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
793                 }
794         }
795         
796         return person;
799 - (NSSet *)contactsForPerson:(ABPerson *)person
801         NSArray                 *allServiceKeys = [serviceDict allKeys];
802         NSString                *serviceID;
803         NSMutableSet    *contactSet = [NSMutableSet set];
804         NSEnumerator    *servicesEnumerator;
805         ABMultiValue    *emails;
806         int                             i, emailsCount;
808         //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
809         {
810                 emails = [person valueForProperty:kABEmailProperty];
811                 emailsCount = [emails count];
812                 
813                 for (i = 0; i < emailsCount ; i++) {
814                         NSString        *email;
815                         
816                         email = [emails valueAtIndex:i];
817                         if ([email hasSuffix:@"@mac.com"]) {
818                                 //Retrieve all appropriate contacts
819                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
820                                                                                                                                                                   UID:email
821                                                                                                                                                  existingOnly:YES];
823                                 //Add them to our set
824                                 [contactSet unionSet:contacts];
825                         }
826                 }
827         }
828         
829         //Now go through the instant messaging keys
830         servicesEnumerator = [allServiceKeys objectEnumerator];
831         while ((serviceID = [servicesEnumerator nextObject])) {
832                 NSString                *addressBookKey = [serviceDict objectForKey:serviceID];
833                 ABMultiValue    *names;
834                 int                             nameCount;
836                 //An ABPerson may have multiple names; iterate through them
837                 names = [person valueForProperty:addressBookKey];
838                 nameCount = [names count];
839                 
840                 //Continue to the next serviceID immediately if no names are found
841                 if (nameCount == 0) continue;
842                 
843                 BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
844                                                                                    [serviceID isEqualToString:@"ICQ"]);
845                 BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"];
846                 
847                 for (i = 0 ; i < nameCount ; i++) {
848                         NSString        *UID = [[names valueAtIndex:i] compactedString];
849                         if ([UID length]) {
850                                 if (isOSCAR) {
851                                         serviceID = serviceIDForOscarUID(UID);
852                                         
853                                 } else if (isJabber) {
854                                         serviceID = serviceIDForJabberUID(UID);
855                                 }
856                                 
857                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
858                                                                                                                                                                   UID:UID
859                                                                                                                                                  existingOnly:YES];
860                                 
861                                 //Add them to our set
862                                 [contactSet unionSet:contacts];
863                         }
864                 }
865         }
867         return contactSet;
870 #pragma mark Address book changed
872  * @brief Address book changed externally
874  * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
875  */
876 - (void)addressBookChanged:(NSNotification *)notification
878         /* In case of a single person, these will be NSStrings.
879          * In case of more then one, they are will be NSArrays containing NSStrings.
880          */     
881         id                              addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
882         NSMutableSet    *allModifiedPeople = [[NSMutableSet alloc] init];
883         ABPerson                *me = [sharedAddressBook me];
884         BOOL                    modifiedMe = NO;;
886         //Delay listObjectNotifications to speed up metaContact creation
887         [[adium contactController] delayListObjectNotifications];
889         //Addition of new records
890         if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
891                 NSArray *peopleToAdd;
893                 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
894                         //We are dealing with multiple records
895                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
896                 } else {
897                         //We have only one record
898                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
899                 }
901                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
902                 [self addToAddressBookDict:peopleToAdd];
903         }
904         
905         //Modification of existing records
906         if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
907                 NSArray *peopleToAdd;
909                 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
910                         //We are dealing with multiple records
911                         [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
912                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
913                 } else {
914                         //We have only one record
915                         [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
916                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
917                 }
918                 
919                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
920                 [self addToAddressBookDict:peopleToAdd];
921         }
922         
923         //Deletion of existing records
924         if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
925                 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
926                         //We are dealing with multiple records
927                         [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
928                 } else {
929                         //We have only one record
930                         [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
931                 }
932                 
933                 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
934         }
935         
936         NSEnumerator    *peopleEnumerator;
937         ABPerson                *person;
938         
939         //Do appropriate updates for each updated ABPerson
940         peopleEnumerator = [allModifiedPeople objectEnumerator];
941         while ((person = [peopleEnumerator nextObject])) {
942                 if (person == me) {
943                         modifiedMe = YES;
944                 }
946                 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
947                 [[adium contactController] updateContacts:[self contactsForPerson:person]
948                                                                           forObserver:self];
949         }
951         //Update us if appropriate
952         if (modifiedMe) {
953                 [self updateSelfIncludingIcon:YES];
954         }
955         
956         //Stop delaying list object notifications since we are done
957         [[adium contactController] endListObjectNotificationsDelay];
961  * @brief Update all existing contacts and accounts
962  */
963 - (void)updateAllContacts
965         [[adium contactController] updateAllListObjectsForObserver:self];
966     [self updateSelfIncludingIcon:YES];
970  * @brief Account list changed: Update all existing accounts
971  */
972 - (void)accountListChanged:(NSNotification *)notification
974         [self updateSelfIncludingIcon:NO];
978  * @brief Update all existing accounts
980  * We use the "me" card to determine the default icon and account display name
981  */
982 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
984         AI_DURING 
985         //Begin loading image data for the "me" address book entry, if one exists
986         ABPerson *me;
987         if ((me = [sharedAddressBook me])) {
988                         
989                         //Default buddy icon
990                         if (includeIcon) {
991                                 //Begin the image load
992                                 meTag = [me beginLoadingImageDataForClient:self];
993                         }
994                         
995                         //Set account display names
996                         if (enableImport) {
997                                 NSString                *myDisplayName, *myPhonetic = nil;
998                                 
999                                 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
1000                                 
1001                                 NSEnumerator    *accountsArray = [[[adium accountController] accounts] objectEnumerator];
1002                                 AIAccount               *account;
1003                                 
1004                                 while ((account = [accountsArray nextObject])) {
1005                                         if (![account isTemporary]) {
1006                                                 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
1007                                                                                                                                           withOwner:self
1008                                                                                                                                   priorityLevel:Low_Priority];
1009                                                 
1010                                                 if (myPhonetic) {
1011                                                         [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
1012                                                                                                                                                    withOwner:self
1013                                                                                                                                            priorityLevel:Low_Priority];                                                                         
1014                                                 }
1015                                         }
1016                                 }
1018                                 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
1019                                                                                                                                                                                    forKey:KEY_ACCOUNT_DISPLAY_NAME]
1020                                                                                                           forGroup:GROUP_ACCOUNT_STATUS];
1021                         }
1022         }
1023         AI_HANDLER
1024                 NSLog(@"ABIntegration: Caught %@: %@", [localException name], [localException reason]);
1025         AI_ENDHANDLER
1028 #pragma mark Address book caching
1030  * @brief rebuild our address book lookup dictionary
1031  */
1032 - (void)rebuildAddressBookDict
1034         //Delay listObjectNotifications to speed up metaContact creation
1035         [[adium contactController] delayListObjectNotifications];
1036         
1037         [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
1038         
1039         [self addToAddressBookDict:[sharedAddressBook people]];
1041         //Stop delaying list object notifications since we are done
1042         [[adium contactController] endListObjectNotificationsDelay];
1047  * @brief Service ID for an OSCAR UID
1049  * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
1050  * because we may have a .Mac, an ICQ, or an AIM name in the field
1051  */
1052 NSString* serviceIDForOscarUID(NSString *UID)
1054         NSString        *serviceID;
1056         const char      firstCharacter = [UID characterAtIndex:0];
1057         
1058         //Determine service based on UID
1059         if ([UID hasSuffix:@"@mac.com"]) {
1060                 serviceID = @"Mac";
1061         } else if (firstCharacter >= '0' && firstCharacter <= '9') {
1062                 serviceID = @"ICQ";
1063         } else {
1064                 serviceID = @"AIM";
1065         }
1066         
1067         return serviceID;
1071  * @brief Service ID for a Jabber UID
1073  * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
1074  * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
1075  */
1076 NSString* serviceIDForJabberUID(NSString *UID)
1078         NSString        *serviceID;
1080         if ([UID hasSuffix:@"@gmail.com"] ||
1081                 [UID hasSuffix:@"@googlemail.com"]) {
1082                 serviceID = @"GTalk";
1083         } else if ([UID hasSuffix:@"@livejournal.com"]) {
1084                 serviceID = @"LiveJournal";
1085         } else {
1086                 serviceID = @"Jabber";
1087         }
1088         
1089         return serviceID;
1094  * @brief add people to our address book lookup dictionary
1096  * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
1097  * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
1098  * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
1099  * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
1100  * does not allow.
1102  * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
1103  * create metaContacts baesd on this information.
1104  */
1105 - (void)addToAddressBookDict:(NSArray *)people
1107         NSEnumerator            *peopleEnumerator;
1108         NSArray                         *allServiceKeys = [serviceDict allKeys];
1109         ABPerson                        *person;
1110         
1111         peopleEnumerator = [people objectEnumerator];
1112         while ((person = [peopleEnumerator nextObject])) {
1113                 NSEnumerator            *servicesEnumerator;
1114                 NSString                        *serviceID;
1115                 
1116                 NSMutableArray          *UIDsArray = [NSMutableArray array];
1117                 NSMutableArray          *servicesArray = [NSMutableArray array];
1118                 
1119                 NSMutableDictionary     *dict;
1120                 ABMultiValue            *emails;
1121                 int                                     i, emailsCount;
1122                 
1123                 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1124                 {
1125                         emails = [person valueForProperty:kABEmailProperty];
1126                         emailsCount = [emails count];
1127                         
1128                         for (i = 0; i < emailsCount ; i++) {
1129                                 NSString        *email;
1130                                 
1131                                 email = [emails valueAtIndex:i];
1132                                 if ([email hasSuffix:@"@mac.com"]) {
1133                                         
1134                                         //@mac.com UIDs go into the AIM dictionary
1135                                         if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1136                                                 dict = [[[NSMutableDictionary alloc] init] autorelease];
1137                                                 [addressBookDict setObject:dict forKey:@"AIM"];
1138                                         }
1139                                         
1140                                         [dict setObject:[person uniqueId] forKey:email];
1141                                         
1142                                         //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1143                                         [UIDsArray addObject:email];
1144                                         [servicesArray addObject:@"Mac"];
1145                                 }
1146                         }
1147                 }
1148                 
1149                 //Now go through the instant messaging keys
1150                 servicesEnumerator = [allServiceKeys objectEnumerator];
1151                 while ((serviceID = [servicesEnumerator nextObject])) {
1152                         NSString                        *addressBookKey = [serviceDict objectForKey:serviceID];
1153                         ABMultiValue            *names;
1154                         int                                     nameCount;
1156                         //An ABPerson may have multiple names; iterate through them
1157                         names = [person valueForProperty:addressBookKey];
1158                         nameCount = [names count];
1159                         
1160                         //Continue to the next serviceID immediately if no names are found
1161                         if (nameCount == 0) continue;
1162                         
1163                         //One or more names were found, so we'll need a dictionary
1164                         if (!(dict = [addressBookDict objectForKey:serviceID])) {
1165                                 dict = [[NSMutableDictionary alloc] init];
1166                                 [addressBookDict setObject:dict forKey:serviceID];
1167                                 [dict release];
1168                         }
1170                         BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
1171                                                                                            [serviceID isEqualToString:@"ICQ"]);
1172                         BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"];
1174                         for (i = 0 ; i < nameCount ; i++) {
1175                                 NSString        *UID = [[names valueAtIndex:i] compactedString];
1176                                 if ([UID length]) {
1177                                         [dict setObject:[person uniqueId] forKey:UID];
1178                                         
1179                                         [UIDsArray addObject:UID];
1180                                         
1181                                         if (isOSCAR) {
1182                                                 serviceID = serviceIDForOscarUID(UID);
1183                                                 
1184                                         } else if (isJabber) {
1185                                                 serviceID = serviceIDForJabberUID(UID);
1186                                         }
1187                                         
1188                                         [servicesArray addObject:serviceID];
1189                                 }
1190                         }
1191                 }
1192                 
1193                 if (([UIDsArray count] > 1) && createMetaContacts) {
1194                         /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1195                         [[adium contactController] groupUIDs:UIDsArray 
1196                                                                          forServices:servicesArray];
1197                 }
1198         }
1202  * @brief remove people from our address book lookup dictionary
1203  */
1204 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1206         NSEnumerator            *enumerator;
1207         NSArray                         *allServiceKeys = [serviceDict allKeys];
1208         NSString                        *uniqueID;
1209         
1210         enumerator = [uniqueIDs objectEnumerator];
1211         while ((uniqueID = [enumerator nextObject])) {
1212                 NSEnumerator            *servicesEnumerator;
1213                 NSString                        *serviceID;
1214                 NSMutableDictionary     *dict;
1215                 
1216                 //The same person may have multiple services; iterate through them and remove each one.
1217                 servicesEnumerator = [allServiceKeys objectEnumerator];
1218                 while ((serviceID = [servicesEnumerator nextObject])) {
1219                         NSEnumerator *keysEnumerator;
1220                         NSString *key;
1221                         
1222                         dict = [addressBookDict objectForKey:serviceID];
1223                         
1224                         keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1225                         
1226                         //The same person may have multiple accounts from the same service; we should remove them all.
1227                         while ((key = [keysEnumerator nextObject])) {
1228                                 [dict removeObjectForKey:key];
1229                         }
1230                 }
1231         }       
1234 #pragma mark AB contextual menu
1236  * @brief Validate menu item
1237  */
1238 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1240         BOOL    hasABEntry = ([self personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1241         BOOL    result = NO;
1242         
1243         if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1244                 result = hasABEntry;
1245         else if ([menuItem isEqual:addToABContexualMenuItem])
1246                 result = !hasABEntry;
1247         
1248         return result;
1252  * @brief Shows the selected contact in Address Book
1253  */
1254 - (void)showInAddressBook
1256         ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1257         NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1258         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1262  * @brief Edits the selected contact in Address Book
1263  */
1264 - (void)editInAddressBook
1266         ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1267         NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1268         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1271 - (void)addToAddressBook
1273         AIListObject                    *contact = [[adium menuController] currentContextMenuObject];
1274         NSString                                *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1275         
1276         if (serviceProperty) {
1277                 ABPerson                                *person = [[ABPerson alloc] init];
1278                 
1279                 //Set the name
1280                 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1281                 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1282                 
1283                 NSString                                *UID = [contact formattedUID];
1284         
1285                 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1286                 AIListObject *c;
1287                 ABMutableMultiValue             *multiValue;
1288                 
1289                 while((c = [containedContactEnu nextObject]))
1290                 {
1291                         multiValue = [[ABMutableMultiValue alloc] init];
1292                         UID = [c formattedUID];
1293                         serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1294                         
1295                         //Set the IM property
1296                         [multiValue addValue:UID withLabel:serviceProperty];
1297                         [person setValue:multiValue forKey:serviceProperty];
1298                         
1299                         [multiValue release];
1300                 }
1301                 
1302                 
1303                 //Set the image
1304                 [person setImageData:[contact userIconData]];
1305                 
1306                 //Set the notes
1307                 [person setValue:[contact notes] forKey:kABNoteProperty];
1308                 
1309                 //Add our newly created person to the AB database
1310                 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1311                         //Save the uid of the new person
1312                         [contact setPreference:[person uniqueId]
1313                                                         forKey:KEY_AB_UNIQUE_ID
1314                                                          group:PREF_GROUP_ADDRESSBOOK];
1315                         
1316                         //Ask the user whether it would like to edit the new contact
1317                         int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1318                                                                                  CONTACT_ADDED_SUCCESS_Message,
1319                                                                                  AILocalizedString(@"Yes", nil),
1320                                                                                  AILocalizedString(@"No", nil), nil, UID);
1321                         
1322                         if (result == NSOKButton) {
1323                                 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1324                                 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1325                                 [url release];
1326                         }
1327                 } else {
1328                         NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);
1329                 }
1330                 
1331                 //Clean up
1332                 [person release];
1333         }
1336 @end