Use initialize, not load, wherever possible.
[adiumx.git] / Source / AdiumAccounts.m
blob4b6a2d1f8b01efbb134166cd50d788073815567b
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 "AdiumAccounts.h"
18 #import <Adium/AIAccountControllerProtocol.h>
19 #import <Adium/AIPreferenceControllerProtocol.h>
20 #import <Adium/AIAccount.h>
21 #import <Adium/AIService.h>
22 #import <AIUtilities/AIArrayAdditions.h>
23 #import <AIUtilities/AIAttributedStringAdditions.h>
25 //Preference keys
26 #define TOP_ACCOUNT_ID                                  @"TopAccountID"         //Highest account object ID
27 #define ACCOUNT_LIST                                    @"Accounts"             //Array of accounts
28 #define ACCOUNT_TYPE                                    @"Type"                         //Account type
29 #define ACCOUNT_SERVICE                                 @"Service"                      //Account service
30 #define ACCOUNT_UID                                             @"UID"                          //Account UID
31 #define ACCOUNT_OBJECT_ID                               @"ObjectID"             //Account object ID
33 @interface AdiumAccounts (PRIVATE)
34 - (void)_loadAccounts;
35 - (void)_saveAccounts;
36 - (NSString *)_generateUniqueInternalObjectID;
37 - (NSString *)_upgradeServiceID:(NSString *)serviceID forAccountDict:(NSDictionary *)accountDict;
38 - (void)upgradeAccounts;
39 @end
41 /*!
42  * @class AdiumAccounts
43  * @brief Class to handle AIAccount access and creation
44  *
45  * This is a private class used by AIAccountController, its public interface.
46  */
47 @implementation AdiumAccounts
49 - (id)init {
50         if ((self = [super init])) {
51                 accounts = [[NSMutableArray alloc] init];
52                 unloadableAccounts = [[NSMutableArray alloc] init];     
53         }
54         
55         return self;
58 - (void)dealloc {
59     [accounts release];
60         [unloadableAccounts release];
62         [super dealloc];
65 /*!
66  * @brief Finish Initing
67  *
68  * Requires the all AIServices have registered
69  */
70 - (void)controllerDidLoad
72         [self _loadAccounts];
73         
74         [self upgradeAccounts];
78 //Accounts -------------------------------------------------------------------------------------------------------
79 #pragma mark Accounts
80 /*!
81  * @brief Returns an array of all available accounts
82  *
83  * @return NSArray of AIAccount instances
84  */
85 - (NSArray *)accounts
87     return accounts;
90 /*!
91  * @brief Returns an array of accounts compatible with a service
92  *
93  * @param service AIService for compatible accounts
94  * @param NSArray of AIAccount instances
95  */
96 - (NSArray *)accountsCompatibleWithService:(AIService *)service
98         NSMutableArray  *matchingAccounts = [NSMutableArray array];
99         NSEnumerator    *enumerator = [accounts objectEnumerator];
100         AIAccount               *account;
101         NSString                *serviceClass = [service serviceClass];
102         
103         while ((account = [enumerator nextObject])) {
104                 if ([account enabled] &&
105                         [[[account service] serviceClass] isEqualToString:serviceClass]) {
106                         [matchingAccounts addObject:account];
107                 }
108         }
109         
110         return matchingAccounts;        
113 - (AIAccount *)accountWithInternalObjectID:(NSString *)objectID
115     NSEnumerator        *enumerator = [accounts objectEnumerator];
116     AIAccount           *account = nil;
118         //Some ancient preferences have NSNumbers instead of NSStrings. Work properly, silently.
119         if ([objectID isKindOfClass:[NSNumber class]]) objectID = [(NSNumber *)objectID stringValue];
121     while (objectID && (account = [enumerator nextObject])) {
122         if ([objectID isEqualToString:[account internalObjectID]]) break;
123     }
124     
125     return account;
129 //Editing --------------------------------------------------------------------------------------------------------------
130 #pragma mark Editing
132  * @brief Create an account
134  * The account is not added to Adium's list of accounts, this must be done separately with addAccount:
135  * @param service AIService for the account
136  * @param inUID NSString userID for the account
137  * @return AIAccount instance that was created
138  */
139 - (AIAccount *)createAccountWithService:(AIService *)service UID:(NSString *)inUID
140 {       
141         return [service accountWithUID:inUID internalObjectID:[self _generateUniqueInternalObjectID]];
145  * @brief Add an account
147  * @param inAccount AIAccount to add
148  */
149 - (void)addAccount:(AIAccount *)inAccount
151         [accounts addObject:inAccount];
152         [self _saveAccounts];
156  * @brief Delete an account
158  * @param inAccount AIAccount to delete
159  */
160 - (void)deleteAccount:(AIAccount *)inAccount
162         //Shut down the account in preparation for release
163         //XXX - Is this sufficient?  Don't some accounts take a while to disconnect and all? -ai
164         [inAccount willBeDeleted];
165         [[adium accountController] forgetPasswordForAccount:inAccount];
167         //Remove from our array
168         [accounts removeObject:inAccount];
169         [self _saveAccounts];
173  * @brief Move an account
175  * @param inAccount AIAccount to move
176  * @param destIndex Index to place the account
177  * @return new index of the account
178  */
179 - (int)moveAccount:(AIAccount *)account toIndex:(int)destIndex
181     [accounts moveObject:account toIndex:destIndex];
182     [self _saveAccounts];
183         return [accounts indexOfObject:account];
187  * @brief An account's UID changed
189  * Save our account array, which stores the account's UID permanently
190  */
191 - (void)accountDidChangeUID:(AIAccount *)account
193         [self _saveAccounts];
197  * @brief Generate a unique account InternalObjectID
199  * @return NSString unique InternalObjectID
200  */
201 //XXX - This setup leaves the possibility that mangled preferences files would create multiple accounts with the same ID -ai
202 - (NSString *)_generateUniqueInternalObjectID
204         int                     topAccountID = [[[adium preferenceController] preferenceForKey:TOP_ACCOUNT_ID group:PREF_GROUP_ACCOUNTS] intValue];
205         NSString        *internalObjectID = [NSString stringWithFormat:@"%i",topAccountID];
206         
207         [[adium preferenceController] setPreference:[NSNumber numberWithInt:topAccountID + 1]
208                                                                                  forKey:TOP_ACCOUNT_ID
209                                                                                   group:PREF_GROUP_ACCOUNTS];
211         return internalObjectID;
215 //Storage --------------------------------------------------------------------------------------------------------------
216 #pragma mark Storage
218  * @brief Load accounts from disk
219  */
220 - (void)_loadAccounts
222     NSArray              *accountList = [[adium preferenceController] preferenceForKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
223         NSEnumerator *enumerator;
224         NSDictionary *accountDict;
226     //Create an instance of every saved account
227         enumerator = [accountList objectEnumerator];
228         while ((accountDict = [enumerator nextObject])) {
229                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
230                 NSString                *serviceID = [self _upgradeServiceID:[accountDict objectForKey:ACCOUNT_TYPE] forAccountDict:accountDict];
231         AIAccount               *newAccount;
233                 //Fetch the account service, UID, and ID
234                 AIService       *service = [[adium accountController] serviceWithUniqueID:serviceID];
235                 NSString        *accountUID = [accountDict objectForKey:ACCOUNT_UID];
236                 NSString        *internalObjectID = [accountDict objectForKey:ACCOUNT_OBJECT_ID];
238         //Create the account and add it to our array
239         if (service && accountUID && [accountUID length]) {
240                         if ((newAccount = [service accountWithUID:accountUID internalObjectID:internalObjectID])) {
241                 [accounts addObject:newAccount];
242             } else {
243                                 NSLog(@"Could not load account %@",accountDict);
244                                 [unloadableAccounts addObject:accountDict];
245                         }
246         } else {
247                         if ([accountUID length]) {
248                                 NSLog(@"Available services are %@: could not load account %@ on service %@ (service %@)",
249                                           [[adium accountController] services], accountDict, serviceID, service);
250                                 [unloadableAccounts addObject:accountDict];
251                         } else {
252                                 AILog(@"Ignored an account with a 0 length accountUID: %@", accountDict);
253                         }
254                 }
255                 [pool release];
256     }
258         //Broadcast an account list changed notification
259     [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
263  * @brief Temporary serviceID upgrade code (v0.63 -> v0.70 for libpurple, v0.70 -> v0.80 for bonjour)
265  * @param serviceID NSString service ID (old or new)
266  * @param accountDict Dictionary of the saved account
267  * @return NSString service ID (new), or nil if unable to upgrade
268  */
269 - (NSString *)_upgradeServiceID:(NSString *)serviceID forAccountDict:(NSDictionary *)accountDict
271         //Libgaim
272         if ([serviceID rangeOfString:@"libgaim" options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
273                 NSMutableString *newServiceID = [serviceID mutableCopy];
274                 [newServiceID replaceOccurrencesOfString:@"libgaim"
275                                                                           withString:@"libpurple"
276                                                                                  options:(NSLiteralSearch | NSAnchoredSearch)
277                                                                                    range:NSMakeRange(0, [newServiceID length])];
278                 serviceID = [newServiceID autorelease];
280         } else if ([serviceID rangeOfString:@"LIBGAIM" options:(NSLiteralSearch | NSAnchoredSearch | NSBackwardsSearch)].location != NSNotFound) {
281                 if ([serviceID isEqualToString:@"AIM-LIBGAIM"]) {
282                         NSString        *uid = [accountDict objectForKey:ACCOUNT_UID];
283                         if (uid && [uid length]) {
284                                 const char      firstCharacter = [uid characterAtIndex:0];
285                                 
286                                 if ([uid hasSuffix:@"@mac.com"]) {
287                                         serviceID = @"libpurple-oscar-Mac";
288                                 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
289                                         serviceID = @"libpurple-oscar-ICQ";
290                                 } else {
291                                         serviceID = @"libpurple-oscar-AIM";
292                                 }
293                         }
294                 } else if ([serviceID isEqualToString:@"GaduGadu-LIBGAIM"]) {
295                         serviceID = @"libpurple-Gadu-Gadu";
296                 } else if ([serviceID isEqualToString:@"Jabber-LIBGAIM"]) {
297                         serviceID = @"libpurple-Jabber";
298                 } else if ([serviceID isEqualToString:@"MSN-LIBGAIM"]) {
299                         serviceID = @"libpurple-MSN";
300                 } else if ([serviceID isEqualToString:@"Napster-LIBGAIM"]) {
301                         serviceID = @"libpurple-Napster";
302                 } else if ([serviceID isEqualToString:@"Novell-LIBGAIM"]) {
303                         serviceID = @"libpurple-GroupWise";
304                 } else if ([serviceID isEqualToString:@"Sametime-LIBGAIM"]) {
305                         serviceID = @"libpurple-Sametime";
306                 } else if ([serviceID isEqualToString:@"Yahoo-LIBGAIM"]) {
307                         serviceID = @"libpurple-Yahoo!";
308                 } else if ([serviceID isEqualToString:@"Yahoo-Japan-LIBGAIM"]) {
309                         serviceID = @"libpurple-Yahoo!-Japan";
310                 }
311         } else if ([serviceID isEqualToString:@"rvous-libezv"])
312                 serviceID = @"bonjour-libezv";
313         else if ([serviceID isEqualToString:@"joscar-OSCAR-AIM"])
314                 serviceID = @"libpurple-oscar-AIM";
315         else if ([serviceID isEqualToString:@"joscar-OSCAR-dotMac"])
316                 serviceID = @"libpurple-oscar-Mac";
317         
318         return serviceID;
322  * @brief Save accounts to disk
323  */
324 - (void)_saveAccounts
326         NSMutableArray  *flatAccounts = [NSMutableArray array];
327         NSEnumerator    *enumerator;
328         AIAccount               *account;
329         
330         //Build a flattened array of the accounts
331         enumerator = [accounts objectEnumerator];
332         while ((account = [enumerator nextObject])) {
333                 if (![account isTemporary]) {
334                         NSMutableDictionary             *flatAccount = [NSMutableDictionary dictionary];
335                         AIService                               *service = [account service];
336                         [flatAccount setObject:[service serviceCodeUniqueID] forKey:ACCOUNT_TYPE];      //Unique plugin ID
337                         [flatAccount setObject:[service serviceID] forKey:ACCOUNT_SERVICE];             //Shared service ID
338                         [flatAccount setObject:[account UID] forKey:ACCOUNT_UID];                                                       //Account UID
339                         [flatAccount setObject:[account internalObjectID] forKey:ACCOUNT_OBJECT_ID];                    //Account Object ID
340                         
341                         [flatAccounts addObject:flatAccount];
342                 }
343         }
344         
345         //Add any unloadable accounts so they're not lost
346         [flatAccounts addObjectsFromArray:unloadableAccounts];
348         //Save and broadcast an account list changed notification
349         [[adium preferenceController] setPreference:flatAccounts forKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
350         [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
354  * @brief Perform upgrades for a new version
356  * 1.0: KEY_ACCOUNT_DISPLAY_NAME and @"TextProfile" cleared if @"" and moved to global if identical on all accounts
357  */
358 - (void)upgradeAccounts
360         NSUserDefaults  *userDefaults = [NSUserDefaults standardUserDefaults];
361         NSNumber                *upgradedAccounts = [userDefaults objectForKey:@"Adium:Account Prefs Upgraded for 1.0"];
362         
363         if (!upgradedAccounts || ![upgradedAccounts boolValue]) {
364                 [userDefaults setObject:[NSNumber numberWithBool:YES] forKey:@"Adium:Account Prefs Upgraded for 1.0"];
366                 AIAccount               *account;
367                 NSEnumerator    *enumerator, *keyEnumerator;
368                 NSString                *key;
370                 //Adium 0.8x would store @"" in preferences which we now want to be able to inherit global values if they don't have a value.
371                 NSSet   *keysWeNowUseGlobally = [NSSet setWithObjects:KEY_ACCOUNT_DISPLAY_NAME, @"TextProfile", nil];
373                 NSCharacterSet  *whitespaceAndNewlineCharacterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
375                 keyEnumerator = [keysWeNowUseGlobally objectEnumerator];
376                 while ((key = [keyEnumerator nextObject])) {
377                         NSAttributedString      *firstAttributedString = nil;
378                         BOOL                            allOnThisKeyAreTheSame = YES;
380                         enumerator = [[self accounts] objectEnumerator];
381                         while ((account = [enumerator nextObject])) {
382                                 NSAttributedString *attributedString = [[account preferenceForKey:key
383                                                                                                                                                         group:GROUP_ACCOUNT_STATUS
384                                                                                                                         ignoreInheritedValues:YES] attributedString];
385                                 if (attributedString && ![attributedString length]) {
386                                         [account setPreference:nil
387                                                                         forKey:key
388                                                                          group:GROUP_ACCOUNT_STATUS];
389                                         attributedString = nil;
390                                 }
391                                 
392                                 if (attributedString) {
393                                         if (firstAttributedString) {
394                                                 /* If this string is not the same as the first one we found, all are not the same.
395                                                  * Only need to check if thus far they all have been the same
396                                                  */
397                                                 if (allOnThisKeyAreTheSame &&
398                                                         ![[[attributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet] isEqualToString:
399                                                                 [[firstAttributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet]]) {
400                                                         allOnThisKeyAreTheSame = NO;
401                                                 }
402                                         } else {
403                                                 //Note the first one we find, which will be our reference
404                                                 firstAttributedString = attributedString;
405                                         }
406                                 }
407                         }
408                         
409                         if (allOnThisKeyAreTheSame && firstAttributedString) {
410                                 //All strings on this key are the same. Set the preference globally...
411                                 [[adium preferenceController] setPreference:[firstAttributedString dataRepresentation]
412                                                                                                          forKey:key
413                                                                                                           group:GROUP_ACCOUNT_STATUS];
414                                 
415                                 //And remove it from all accounts
416                                 enumerator = [[self accounts] objectEnumerator];
417                                 while ((account = [enumerator nextObject])) {
418                                         [account setPreference:nil
419                                                                         forKey:key
420                                                                          group:GROUP_ACCOUNT_STATUS];
421                                 }
422                         }
423                 }
425                 [userDefaults synchronize];
426         }
429 @end