Allow localization of the Alert Text label in the Display an Alert action
[adiumx.git] / Source / ESAddressBookIntegrationPlugin.m
blob4962a0502ee3fd820f29ad7d58f6b129fadecbe1
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 "ESAddressBookIntegrationPlugin.h"
18 #import <Adium/AIAccountControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIPreferenceControllerProtocol.h>
21 #import <Adium/AIMenuControllerProtocol.h>
22 #import <Adium/AIAccount.h>
23 #import <Adium/AIListObject.h>
24 #import <Adium/AIMetaContact.h>
25 #import <Adium/AIService.h>
26 #import <AIUtilities/AIAttributedStringAdditions.h>
27 #import <AIUtilities/AIDictionaryAdditions.h>
28 #import <AIUtilities/AIMutableOwnerArray.h>
29 #import <AIUtilities/AIStringAdditions.h>
30 #import <AIUtilities/OWAddressBookAdditions.h>
31 #import <AIUtilities/AIFileManagerAdditions.h>
33 #define IMAGE_LOOKUP_INTERVAL   0.01
34 #define SHOW_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Show In Address Book", "Show In Address Book Contextual Menu")
35 #define EDIT_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Edit In Address Book", "Edit In Address Book Contextual Menu")
36 #define ADD_TO_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Add To Address Book", "Add To Address Book Contextual Menu")
38 #define CONTACT_ADDED_SUCCESS_TITLE             AILocalizedString(@"Success", "Title of a panel shown after adding successfully adding a contact to the address book.")
39 #define CONTACT_ADDED_SUCCESS_Message   AILocalizedString(@"%@ had been successfully added to the Address Book.\nWould you like to edit the card now?", nil)
40 #define CONTACT_ADDED_ERROR_TITLE               AILocalizedString(@"Error", nil)
41 #define CONTACT_ADDED_ERROR_Message             AILocalizedString(@"An error had occurred while adding %@ to the Address Book.", nil)
43 @interface ESAddressBookIntegrationPlugin(PRIVATE)
44 - (void)updateAllContacts;
45 - (void)updateSelfIncludingIcon:(BOOL)includeIcon;
46 - (void)preferencesChanged:(NSNotification *)notification;
47 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic;
48 - (ABPerson *)personForListObject:(AIListObject *)inObject;
49 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID;
50 - (void)rebuildAddressBookDict;
51 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject;
52 - (void)showInAddressBook;
53 - (void)editInAddressBook;
54 - (void)addToAddressBookDict:(NSArray *)people;
55 - (void)removeFromAddressBookDict:(NSArray *)UIDs;
56 - (void)installAddressBookActions;
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:AIApplicationDidFinishLoadingNotification
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                                 AILogWithSignature(@"Warning: 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                                 [[sharedAddressBook class] cancelPreviousPerformRequestsWithTarget:sharedAddressBook
353                                                                                                                                                   selector:@selector(save)
354                                                                                                                                                         object:nil];
355                                 [sharedAddressBook performSelector:@selector(save)
356                                                                                 withObject:nil
357                                                                                 afterDelay:5.0];
358                         }
359                 }
360     }
361     
362     return modifiedAttributes;
366  * @brief Return the name of an ABPerson in the way Adium should display it
368  * @param person An <tt>ABPerson</tt>
369  * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
370  * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
371  */
372 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
374         NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;     
375         NSString *nickName;
376         NSString *displayName = nil;
377         NSNumber *flags;
378         
379         // If the record is for a company, return the company name if present
380         if ((flags = [person valueForProperty:kABPersonFlags])) {
381                 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
382                         NSString *companyName = [person valueForProperty:kABOrganizationProperty];
383                         if (companyName && [companyName length]) {
384                                 return companyName;
385                         }
386                 }
387         }
388                 
389         firstName = [person valueForProperty:kABFirstNameProperty];
390         middleName = [person valueForProperty:kABMiddleNameProperty];
391         lastName = [person valueForProperty:kABLastNameProperty];
392         phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
393         phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
394         
395         //
396         if (useMiddleName && middleName)
397                 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
399         if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
400                 displayName = nickName;
402         } else if (!lastName || (displayFormat == First)) {  
403                 /* If no last name is available, use the first name */
404                 displayName = firstName;
405                 if (phonetic != NULL) *phonetic = phoneticFirstName;
407         } else if (!firstName) {
408                 /* If no first name is available, use the last name */
409                 displayName = lastName;
410                 if (phonetic != NULL) *phonetic = phoneticLastName;
412         } else {
413                 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
415                 /* Look to the preference setting */
416                 switch (displayFormat) {
417                         case FirstLast:
418                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
419                                 if (havePhonetic) {
420                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
421                                                 (phoneticFirstName ? phoneticFirstName : firstName),
422                                                 (phoneticLastName ? phoneticLastName : lastName)];
423                                 }
424                                 break;
425                         case LastFirst:
426                                 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName]; 
427                                 if (havePhonetic) {
428                                         *phonetic = [NSString stringWithFormat:@"%@, %@",
429                                                 (phoneticLastName ? phoneticLastName : lastName),
430                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
431                                 }
432                                 break;
433                         case LastFirstNoComma:
434                                 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName]; 
435                                 if (havePhonetic) {
436                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
437                                                 (phoneticLastName ? phoneticLastName : lastName),
438                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
439                                 }                                       
440                                 break;
441                         case FirstLastInitial:
442                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,[lastName substringToIndex:1]]; 
443                                 if (havePhonetic) {
444                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
445                                                                  (phoneticFirstName ? phoneticFirstName : firstName),
446                                                                  [lastName substringToIndex:1]];
447                                 }
448                         case First:
449                                 //No action; handled before we reach the switch statement
450                                 break;
451                 }
452         }
454         return displayName;
458  * @brief Observe preference changes
460  * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
461  * option is toggled, as metaContacts are created while building the dict.
463  * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
464  */
465 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
466                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
468     if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
469                 BOOL                    oldCreateMetaContacts = createMetaContacts;
470                 
471         //load new displayFormat
472                 enableImport = [[prefDict objectForKey:KEY_AB_ENABLE_IMPORT] boolValue];
473                 displayFormat = [[prefDict objectForKey:KEY_AB_DISPLAYFORMAT] intValue];
474         automaticSync = [[prefDict objectForKey:KEY_AB_IMAGE_SYNC] boolValue];
475         useNickName = [[prefDict objectForKey:KEY_AB_USE_NICKNAME] boolValue];
476                 useMiddleName = [[prefDict objectForKey:KEY_AB_USE_MIDDLE] boolValue];
477                 preferAddressBookImages = [[prefDict objectForKey:KEY_AB_PREFER_ADDRESS_BOOK_IMAGES] boolValue];
478                 useABImages = [[prefDict objectForKey:KEY_AB_USE_IMAGES] boolValue];
480                 createMetaContacts = [[prefDict objectForKey:KEY_AB_CREATE_METACONTACTS] boolValue];
481                 
482                 if (firstTime) {
483                         //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
484                         [self rebuildAddressBookDict];
485                         
486                         //Register ourself as a listObject observer, which will update all objects
487                         [[adium contactController] registerListObjectObserver:self];
488                         
489                         //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin                     
490                 } else {
491                         //This isn't the first time through
493                         //If we weren't creating meta contacts before but we are now
494                         if (!oldCreateMetaContacts && createMetaContacts) {
495                                 /*
496                                  Build the address book dictionary, which will also trigger metacontact grouping as appropriate
497                                  Delay to the next run loop to give better UI responsiveness
498                                  */
499                                 [self performSelector:@selector(rebuildAddressBookDict)
500                                                    withObject:nil
501                                                    afterDelay:0.0001];
502                         }
503                         
504                         //Update all contacts, which will update objects and then our "me" card information
505                         [self updateAllContacts];
506                 }
508     } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
509                 //Find the person
510                 ABPerson *person = [self personForListObject:object];
511                 
512                 if (person) {
513                         //Set the person's image to the inObject's serverside User Icon.
514                         NSData  *imageData = [object preferenceForKey:KEY_USER_ICON
515                                                                                                         group:PREF_GROUP_USERICONS
516                                                                         ignoreInheritedValues:YES];
517                         
518                         //If the pref is now nil, we should restore the address book back to the serverside icon if possible
519                         if (!imageData) {
520                                 imageData = [[object statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
521                         }
522                         
523                         [person setImageData:imageData];
524                 }
525         }
529  * @brief Returns the appropriate service for the property.
531  * @param property - an ABPerson property.
532  */
533 + (AIService *)serviceFromProperty:(NSString *)property
535         NSString        *serviceID = nil;
536         
537         if ([property isEqualToString:kABAIMInstantProperty])
538                 serviceID = @"AIM";
539         
540         else if ([property isEqualToString:kABICQInstantProperty])
541                 serviceID = @"ICQ";
542         
543         else if ([property isEqualToString:kABMSNInstantProperty])
544                 serviceID = @"MSN";
545         
546         else if ([property isEqualToString:kABJabberInstantProperty])
547                 serviceID = @"Jabber";
548         
549         else if ([property isEqualToString:kABYahooInstantProperty])
550                 serviceID = @"Yahoo!";
552         return (serviceID ? [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:serviceID] : nil);
556  * @brief Returns the appropriate property for the service.
557  */
558 + (NSString *)propertyFromService:(AIService *)inService
560         NSString *result;
561         NSString *serviceID = [inService serviceID];
563         result = [serviceDict objectForKey:serviceID];
565         //Check for some special cases
566         if (!result) {
567                 if ([serviceID isEqualToString:@"GTalk"]) {
568                         result = kABJabberInstantProperty;
569                 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
570                         result = kABJabberInstantProperty;
571                 } else if ([serviceID isEqualToString:@"Mac"]) {
572                         result = kABAIMInstantProperty;
573                 }
574         }
575         
576         return result;
579 #pragma mark Image data
582  * @brief Called when the address book completes an asynchronous image lookup
584  * @param inData NSData representing an NSImage
585  * @param tag A tag indicating the lookup with which this call is associated. We use a tracking dictionary, trackingDict, to associate this int back to a usable object.
586  */
587 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
589         if (tag == meTag) {
590                 [[adium preferenceController] setPreference:inData
591                                                                                          forKey:KEY_DEFAULT_USER_ICON 
592                                                                                           group:GROUP_ACCOUNT_STATUS];
593                 meTag = -1;
594                 
595         } else if (useABImages) {
596                 NSNumber                *tagNumber;
597                 NSImage                 *image;
598                 AIListObject    *listObject;
599 //              AIListContact   *parentContact;
600                 NSString                *uniqueID;
601                 id                              setOrObject;
602                 
603                 tagNumber = [NSNumber numberWithInt:tag];
604                 
605                 //Apply the image to the appropriate listObject
606                 image = (inData ? [[[NSImage alloc] initWithData:inData] autorelease] : nil);
607                 [image setDataRetained:YES];
609                 //Get the object from our tracking dictionary
610                 setOrObject = [trackingDict objectForKey:tagNumber];
611                 
612                 if ([setOrObject isKindOfClass:[AIListObject class]]) {
613                         listObject = (AIListObject *)setOrObject;
614                         
615                         //Apply the image at the appropriate priority
616                         [listObject setDisplayUserIcon:image
617                                                                  withOwner:self
618                                                          priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
620                         /*
621                         parentContact = [listObject parentContact];
622                          */
623                         
624                 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
625                         NSEnumerator    *enumerator;
626 //                      BOOL                    checkedForMetaContact = NO;
628                         //Apply the image to each listObject at the appropriate priority
629                         enumerator = [(NSSet *)setOrObject objectEnumerator];
630                         while ((listObject = [enumerator nextObject])) {
631                                 
632                                 /*
633                                 //These objects all have the same unique ID so will all also have the same meta contact; just check once
634                                 if (!checkedForMetaContact) {
635                                         parentContact = [listObject parentContact];
636                                         if (parentContact == listObject) parentContact = nil;
637                                         checkedForMetaContact = YES;
638                                 }
639                                 */
640                                 
641                                 [listObject setDisplayUserIcon:image
642                                                                          withOwner:self
643                                                                  priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
644                         }
645                 }
646                 
647                 /*
648                 if (parentContact) {
649                         [parentContact setDisplayUserIcon:image
650                                                                         withOwner:self
651                                                                 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];                        
652                 }
653                 */
654                 
655                 //No further need for the dictionary entries
656                 [trackingDict removeObjectForKey:tagNumber];
657                 
658                 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
659                         [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
660                         [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
661                 }
662         }
666  * @brief Queue an asynchronous image fetch for person associated with inObject
668  * Image lookups are done asynchronously.  This allows other processing to be done between image calls, improving the perceived
669  * speed.  [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
670  *                      and the asynchronous lookup therefore hung the problem.  Submitted as radar 3977541.]
672  * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
673  * the number of accounts on that service.  We therefore aggregate the lookups to lower the address book search
674  * and image/data creation overhead.
676  * @param person The ABPerson to fetch the image from
677  * @param inObject The AIListObject with which to ultimately associate the image
678  */
679 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
681         int                             tag;
682         NSNumber                *tagNumber;
683         NSString                *uniqueId;
684         
685         uniqueId = [person uniqueId];
686         
687         //Check if we already have a tag for the loading of another object with the same
688         //internalObjectID
689         if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
690                 id                              previousValue;
691                 NSMutableSet    *objectSet;
692                 
693                 previousValue = [trackingDict objectForKey:tagNumber];
694                 
695                 if ([previousValue isKindOfClass:[AIListObject class]]) {
696                         //If the old value is just a listObject, create an array with the old object
697                         //and the new object
698                         objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
699                         
700                         //Store the array in the tracking dict
701                         [trackingDict setObject:objectSet forKey:tagNumber];
702                         
703                 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
704                         //Add the new object to the previously-created array
705                         [(NSMutableSet *)previousValue addObject:inObject];
706                 }
707                 
708         } else {
709                 //Begin the image load
710                 tag = [person beginLoadingImageDataForClient:self];
711                 tagNumber = [NSNumber numberWithInt:tag];
712                 
713                 //We need to be able to take a tagNumber and retrieve the object
714                 [trackingDict setObject:inObject forKey:tagNumber];
715                 
716                 //We also want to take a person's uniqueID and potentially find an existing tag number
717                 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
718                 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];
719         }
722 #pragma mark Searching
724  * @brief Find an ABPerson corresponding to an AIListObject
726  * @param inObject The object for which it search
727  * @result An ABPerson is one is found, or nil if none is found
728  */
729 - (ABPerson *)personForListObject:(AIListObject *)inObject
731         ABPerson        *person = nil;
732         NSString        *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
733         ABRecord        *record = nil;
734         
735         if (uniqueID)
736                 record = [sharedAddressBook recordForUniqueId:uniqueID];
737         
738         if (record && [record isKindOfClass:[ABPerson class]]) {
739                 person = (ABPerson *)record;
740         } else {
741                 if ([inObject isKindOfClass:[AIMetaContact class]]) {
742                         NSEnumerator    *enumerator;
743                         AIListContact   *listContact;
744                         
745                         //Search for an ABPerson for each listContact within the metaContact; first one we find is
746                         //the lucky winner.
747                         enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
748                         while ((listContact = [enumerator nextObject]) && (person == nil)) {
749                                 person = [self personForListObject:listContact];
750                         }
751                         
752                 } else {
753                         NSString                *UID = [inObject UID];
754                         NSString                *serviceID = [[inObject service] serviceID];
755                         
756                         person = [self _searchForUID:UID serviceID:serviceID];
757                         
758                         /* If we don't find anything yet, look at alternative service possibilities:
759                          *    AIM <--> ICQ
760                          */
761                         if (!person) {
762                                 if ([serviceID isEqualToString:@"AIM"]) {
763                                         person = [self _searchForUID:UID serviceID:@"ICQ"];
764                                 } else if ([serviceID isEqualToString:@"ICQ"]) {
765                                         person = [self _searchForUID:UID serviceID:@"AIM"];
766                                 }
767                         }
768                 }
769         }
770         return person;
774  * @brief Find an ABPerson for a given UID and serviceID combination
775  * 
776  * Uses our addressBookDict cache created in rebuildAddressBook.
778  * @param UID The UID for the contact
779  * @param serviceID The serviceID for the contact
780  * @result A corresponding <tt>ABPerson</tt>
781  */
782 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
784         ABPerson                *person = nil;
785         NSDictionary    *dict;
786         
787         if ([serviceID isEqualToString:@"Mac"]) {
788                 dict = [addressBookDict objectForKey:@"AIM"];
790         } else if ([serviceID isEqualToString:@"GTalk"]) {
791                 dict = [addressBookDict objectForKey:@"Jabber"];
793         } else if ([serviceID isEqualToString:@"LiveJournal"]) {
794                 dict = [addressBookDict objectForKey:@"Jabber"];
795                 
796         } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
797                 dict = [addressBookDict objectForKey:@"Yahoo!"];
798                 
799         } else {
800                 dict = [addressBookDict objectForKey:serviceID];
801         } 
802         
803         if (dict) {
804                 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
805                 if (uniqueID) {
806                         person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
807                 }
808         }
809         
810         return person;
813 - (NSSet *)contactsForPerson:(ABPerson *)person
815         NSArray                 *allServiceKeys = [serviceDict allKeys];
816         NSString                *serviceID;
817         NSMutableSet    *contactSet = [NSMutableSet set];
818         NSEnumerator    *servicesEnumerator;
819         ABMultiValue    *emails;
820         int                             i, emailsCount;
822         //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
823         {
824                 emails = [person valueForProperty:kABEmailProperty];
825                 emailsCount = [emails count];
826                 
827                 for (i = 0; i < emailsCount ; i++) {
828                         NSString        *email;
829                         
830                         email = [emails valueAtIndex:i];
831                         if ([email hasSuffix:@"@mac.com"]) {
832                                 //Retrieve all appropriate contacts
833                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
834                                                                                                                                                                   UID:email
835                                                                                                                                                  existingOnly:YES];
837                                 //Add them to our set
838                                 [contactSet unionSet:contacts];
839                         }
840                 }
841         }
842         
843         //Now go through the instant messaging keys
844         servicesEnumerator = [allServiceKeys objectEnumerator];
845         while ((serviceID = [servicesEnumerator nextObject])) {
846                 NSString                *addressBookKey = [serviceDict objectForKey:serviceID];
847                 ABMultiValue    *names;
848                 int                             nameCount;
850                 //An ABPerson may have multiple names; iterate through them
851                 names = [person valueForProperty:addressBookKey];
852                 nameCount = [names count];
853                 
854                 //Continue to the next serviceID immediately if no names are found
855                 if (nameCount == 0) continue;
856                 
857                 BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
858                                                                                    [serviceID isEqualToString:@"ICQ"]);
859                 BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"] ||
860                                            [serviceID isEqualToString:@"XMPP"];
861                 
862                 for (i = 0 ; i < nameCount ; i++) {
863                         NSString        *UID = [[names valueAtIndex:i] compactedString];
864                         if ([UID length]) {
865                                 if (isOSCAR) {
866                                         serviceID = serviceIDForOscarUID(UID);
867                                         
868                                 } else if (isJabber) {
869                                         serviceID = serviceIDForJabberUID(UID);
870                                 }
871                                 
872                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
873                                                                                                                                                                   UID:UID
874                                                                                                                                                  existingOnly:YES];
875                                 
876                                 //Add them to our set
877                                 [contactSet unionSet:contacts];
878                         }
879                 }
880         }
882         return contactSet;
885 #pragma mark Address book changed
887  * @brief Address book changed externally
889  * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
890  */
891 - (void)addressBookChanged:(NSNotification *)notification
893         /* In case of a single person, these will be NSStrings.
894          * In case of more then one, they are will be NSArrays containing NSStrings.
895          */     
896         id                              addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
897         NSMutableSet    *allModifiedPeople = [[NSMutableSet alloc] init];
898         ABPerson                *me = [sharedAddressBook me];
899         BOOL                    modifiedMe = NO;;
901         //Delay listObjectNotifications to speed up metaContact creation
902         [[adium contactController] delayListObjectNotifications];
904         //Addition of new records
905         if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
906                 NSArray *peopleToAdd;
908                 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
909                         //We are dealing with multiple records
910                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
911                 } else {
912                         //We have only one record
913                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
914                 }
916                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
917                 [self addToAddressBookDict:peopleToAdd];
918         }
919         
920         //Modification of existing records
921         if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
922                 NSArray *peopleToAdd;
924                 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
925                         //We are dealing with multiple records
926                         [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
927                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
928                 } else {
929                         //We have only one record
930                         [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
931                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
932                 }
933                 
934                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
935                 [self addToAddressBookDict:peopleToAdd];
936         }
937         
938         //Deletion of existing records
939         if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
940                 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
941                         //We are dealing with multiple records
942                         [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
943                 } else {
944                         //We have only one record
945                         [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
946                 }
947                 
948                 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
949         }
950         
951         NSEnumerator    *peopleEnumerator;
952         ABPerson                *person;
953         
954         //Do appropriate updates for each updated ABPerson
955         peopleEnumerator = [allModifiedPeople objectEnumerator];
956         while ((person = [peopleEnumerator nextObject])) {
957                 if (person == me) {
958                         modifiedMe = YES;
959                 }
961                 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
962                 [[adium contactController] updateContacts:[self contactsForPerson:person]
963                                                                           forObserver:self];
964         }
966         //Update us if appropriate
967         if (modifiedMe) {
968                 [self updateSelfIncludingIcon:YES];
969         }
970         
971         //Stop delaying list object notifications since we are done
972         [[adium contactController] endListObjectNotificationsDelay];
973         [allModifiedPeople release];
977  * @brief Update all existing contacts and accounts
978  */
979 - (void)updateAllContacts
981         [[adium contactController] updateAllListObjectsForObserver:self];
982     [self updateSelfIncludingIcon:YES];
986  * @brief Account list changed: Update all existing accounts
987  */
988 - (void)accountListChanged:(NSNotification *)notification
990         [self updateSelfIncludingIcon:NO];
994  * @brief Update all existing accounts
996  * We use the "me" card to determine the default icon and account display name
997  */
998 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
1000         @try
1001         {
1002         //Begin loading image data for the "me" address book entry, if one exists
1003         ABPerson *me;
1004         if ((me = [sharedAddressBook me])) {
1005                         
1006                         //Default buddy icon
1007                         if (includeIcon) {
1008                                 //Begin the image load
1009                                 meTag = [me beginLoadingImageDataForClient:self];
1010                         }
1011                         
1012                         //Set account display names
1013                         if (enableImport) {
1014                                 NSString                *myDisplayName, *myPhonetic = nil;
1015                                 
1016                                 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
1017                                 
1018                                 NSEnumerator    *accountsArray = [[[adium accountController] accounts] objectEnumerator];
1019                                 AIAccount               *account;
1020                                 
1021                                 while ((account = [accountsArray nextObject])) {
1022                                         if (![account isTemporary]) {
1023                                                 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
1024                                                                                                                                           withOwner:self
1025                                                                                                                                   priorityLevel:Low_Priority];
1026                                                 
1027                                                 if (myPhonetic) {
1028                                                         [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
1029                                                                                                                                                    withOwner:self
1030                                                                                                                                            priorityLevel:Low_Priority];                                                                         
1031                                                 }
1032                                         }
1033                                 }
1035                                 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
1036                                                                                                                                                                                    forKey:KEY_ACCOUNT_DISPLAY_NAME]
1037                                                                                                           forGroup:GROUP_ACCOUNT_STATUS];
1038                         }
1039         }
1040         }
1041         @catch(id exc)
1042         {
1043                 NSLog(@"ABIntegration: Caught %@", exc);
1044         }
1047 #pragma mark Address book caching
1049  * @brief rebuild our address book lookup dictionary
1050  */
1051 - (void)rebuildAddressBookDict
1053         //Delay listObjectNotifications to speed up metaContact creation
1054         [[adium contactController] delayListObjectNotifications];
1055         
1056         [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
1057         
1058         [self addToAddressBookDict:[sharedAddressBook people]];
1060         //Stop delaying list object notifications since we are done
1061         [[adium contactController] endListObjectNotificationsDelay];
1066  * @brief Service ID for an OSCAR UID
1068  * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
1069  * because we may have a .Mac, an ICQ, or an AIM name in the field
1070  */
1071 NSString* serviceIDForOscarUID(NSString *UID)
1073         NSString        *serviceID;
1075         const char      firstCharacter = [UID characterAtIndex:0];
1076         
1077         //Determine service based on UID
1078         if ([UID hasSuffix:@"@mac.com"]) {
1079                 serviceID = @"Mac";
1080         } else if (firstCharacter >= '0' && firstCharacter <= '9') {
1081                 serviceID = @"ICQ";
1082         } else {
1083                 serviceID = @"AIM";
1084         }
1085         
1086         return serviceID;
1090  * @brief Service ID for a Jabber UID
1092  * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
1093  * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
1094  */
1095 NSString* serviceIDForJabberUID(NSString *UID)
1097         NSString        *serviceID;
1099         if ([UID hasSuffix:@"@gmail.com"] ||
1100                 [UID hasSuffix:@"@googlemail.com"]) {
1101                 serviceID = @"GTalk";
1102         } else if ([UID hasSuffix:@"@livejournal.com"]) {
1103                 serviceID = @"LiveJournal";
1104         } else {
1105                 serviceID = @"Jabber";
1106         }
1107         
1108         return serviceID;
1113  * @brief add people to our address book lookup dictionary
1115  * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
1116  * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
1117  * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
1118  * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
1119  * does not allow.
1121  * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
1122  * create metaContacts baesd on this information.
1123  */
1124 - (void)addToAddressBookDict:(NSArray *)people
1126         NSEnumerator            *peopleEnumerator;
1127         NSArray                         *allServiceKeys = [serviceDict allKeys];
1128         ABPerson                        *person;
1129         
1130         peopleEnumerator = [people objectEnumerator];
1131         while ((person = [peopleEnumerator nextObject])) {
1132                 NSEnumerator            *servicesEnumerator;
1133                 NSString                        *serviceID;
1134                 
1135                 NSMutableArray          *UIDsArray = [NSMutableArray array];
1136                 NSMutableArray          *servicesArray = [NSMutableArray array];
1137                 
1138                 NSMutableDictionary     *dict;
1139                 ABMultiValue            *emails;
1140                 int                                     i, emailsCount;
1141                 
1142                 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1143                 {
1144                         emails = [person valueForProperty:kABEmailProperty];
1145                         emailsCount = [emails count];
1146                         
1147                         for (i = 0; i < emailsCount ; i++) {
1148                                 NSString        *email;
1149                                 
1150                                 email = [emails valueAtIndex:i];
1151                                 if ([email hasSuffix:@"@mac.com"]) {
1152                                         
1153                                         //@mac.com UIDs go into the AIM dictionary
1154                                         if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1155                                                 dict = [[[NSMutableDictionary alloc] init] autorelease];
1156                                                 [addressBookDict setObject:dict forKey:@"AIM"];
1157                                         }
1158                                         
1159                                         [dict setObject:[person uniqueId] forKey:email];
1160                                         
1161                                         //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1162                                         [UIDsArray addObject:email];
1163                                         [servicesArray addObject:@"Mac"];
1164                                 }
1165                         }
1166                 }
1167                 
1168                 //Now go through the instant messaging keys
1169                 servicesEnumerator = [allServiceKeys objectEnumerator];
1170                 while ((serviceID = [servicesEnumerator nextObject])) {
1171                         NSString                        *addressBookKey = [serviceDict objectForKey:serviceID];
1172                         ABMultiValue            *names;
1173                         int                                     nameCount;
1175                         //An ABPerson may have multiple names; iterate through them
1176                         names = [person valueForProperty:addressBookKey];
1177                         nameCount = [names count];
1178                         
1179                         //Continue to the next serviceID immediately if no names are found
1180                         if (nameCount == 0) continue;
1181                         
1182                         //One or more names were found, so we'll need a dictionary
1183                         if (!(dict = [addressBookDict objectForKey:serviceID])) {
1184                                 dict = [[NSMutableDictionary alloc] init];
1185                                 [addressBookDict setObject:dict forKey:serviceID];
1186                                 [dict release];
1187                         }
1189                         BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
1190                                                                                            [serviceID isEqualToString:@"ICQ"]);
1191                         BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"] ||
1192                                                [serviceID isEqualToString:@"XMPP"];
1194                         for (i = 0 ; i < nameCount ; i++) {
1195                                 NSString        *UID = [[names valueAtIndex:i] compactedString];
1196                                 if ([UID length]) {
1197                                         [dict setObject:[person uniqueId] forKey:UID];
1198                                         
1199                                         [UIDsArray addObject:UID];
1200                                         
1201                                         if (isOSCAR) {
1202                                                 serviceID = serviceIDForOscarUID(UID);
1203                                                 
1204                                         } else if (isJabber) {
1205                                                 serviceID = serviceIDForJabberUID(UID);
1206                                         }
1207                                         
1208                                         [servicesArray addObject:serviceID];
1209                                 }
1210                         }
1211                 }
1212                 
1213                 if (([UIDsArray count] > 1) && createMetaContacts) {
1214                         /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1215                         [[adium contactController] groupUIDs:UIDsArray 
1216                                                                          forServices:servicesArray];
1217                 }
1218         }
1222  * @brief remove people from our address book lookup dictionary
1223  */
1224 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1226         NSEnumerator            *enumerator;
1227         NSArray                         *allServiceKeys = [serviceDict allKeys];
1228         NSString                        *uniqueID;
1229         
1230         enumerator = [uniqueIDs objectEnumerator];
1231         while ((uniqueID = [enumerator nextObject])) {
1232                 NSEnumerator            *servicesEnumerator;
1233                 NSString                        *serviceID;
1234                 NSMutableDictionary     *dict;
1235                 
1236                 //The same person may have multiple services; iterate through them and remove each one.
1237                 servicesEnumerator = [allServiceKeys objectEnumerator];
1238                 while ((serviceID = [servicesEnumerator nextObject])) {
1239                         NSEnumerator *keysEnumerator;
1240                         NSString *key;
1241                         
1242                         dict = [addressBookDict objectForKey:serviceID];
1243                         
1244                         keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1245                         
1246                         //The same person may have multiple accounts from the same service; we should remove them all.
1247                         while ((key = [keysEnumerator nextObject])) {
1248                                 [dict removeObjectForKey:key];
1249                         }
1250                 }
1251         }       
1254 #pragma mark AB contextual menu
1256  * @brief Validate menu item
1257  */
1258 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1260         BOOL    hasABEntry = ([self personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1261         BOOL    result = NO;
1262         
1263         if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1264                 result = hasABEntry;
1265         else if ([menuItem isEqual:addToABContexualMenuItem])
1266                 result = !hasABEntry;
1267         
1268         return result;
1272  * @brief Shows the selected contact in Address Book
1273  */
1274 - (void)showInAddressBook
1276         ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1277         NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1278         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1282  * @brief Edits the selected contact in Address Book
1283  */
1284 - (void)editInAddressBook
1286         ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1287         NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1288         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1291 - (void)addToAddressBook
1293         AIListObject                    *contact = [[adium menuController] currentContextMenuObject];
1294         NSString                                *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1295         
1296         if (serviceProperty) {
1297                 ABPerson                                *person = [[ABPerson alloc] init];
1298                 
1299                 //Set the name
1300                 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1301                 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1302                 
1303                 NSString                                *UID = [contact formattedUID];
1304         
1305                 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1306                 AIListObject *c;
1307                 ABMutableMultiValue             *multiValue;
1308                 
1309                 while((c = [containedContactEnu nextObject]))
1310                 {
1311                         multiValue = [[ABMutableMultiValue alloc] init];
1312                         UID = [c formattedUID];
1313                         serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1314                         
1315                         //Set the IM property
1316                         [multiValue addValue:UID withLabel:serviceProperty];
1317                         [person setValue:multiValue forKey:serviceProperty];
1318                         
1319                         [multiValue release];
1320                 }
1321                 
1322                 
1323                 //Set the image
1324                 [person setImageData:[contact userIconData]];
1325                 
1326                 //Set the notes
1327                 [person setValue:[contact notes] forKey:kABNoteProperty];
1328                 
1329                 //Add our newly created person to the AB database
1330                 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1331                         //Save the uid of the new person
1332                         [contact setPreference:[person uniqueId]
1333                                                         forKey:KEY_AB_UNIQUE_ID
1334                                                          group:PREF_GROUP_ADDRESSBOOK];
1335                         
1336                         //Ask the user whether it would like to edit the new contact
1337                         int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1338                                                                                  CONTACT_ADDED_SUCCESS_Message,
1339                                                                                  AILocalizedString(@"Yes", nil),
1340                                                                                  AILocalizedString(@"No", nil), nil, UID);
1341                         
1342                         if (result == NSOKButton) {
1343                                 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1344                                 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1345                                 [url release];
1346                         }
1347                 } else {
1348                         NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);
1349                 }
1350                 
1351                 //Clean up
1352                 [person release];
1353         }
1356 @end