We need a b8 before we can release since we've had some significant changes since...
[adiumx.git] / Source / AdiumAccounts.m
blobc2bea6ddde0f16609754e2f04f9e5de3e02f96bd
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 ServiceID upgrade code (v0.63 -> v0.70 for libpurple, v0.70 -> v0.80 for bonjour, v1.0 -> v1.1 for libpurple)
265  * The changed name will only be saved if some other account change, such as adding an account, occurs,
266  * so this code should remain indefinitely to provide an upgrade path to people whose service IDs are in an
267  * old style.
269  * @param serviceID NSString service ID (old or new)
270  * @param accountDict Dictionary of the saved account
271  * @return NSString service ID (new), or nil if unable to upgrade
272  */
273 - (NSString *)_upgradeServiceID:(NSString *)serviceID forAccountDict:(NSDictionary *)accountDict
275         //Libgaim
276         if ([serviceID rangeOfString:@"libgaim" options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
277                 NSMutableString *newServiceID = [serviceID mutableCopy];
278                 [newServiceID replaceOccurrencesOfString:@"libgaim"
279                                                                           withString:@"libpurple"
280                                                                                  options:(NSLiteralSearch | NSAnchoredSearch)
281                                                                                    range:NSMakeRange(0, [newServiceID length])];
282                 serviceID = [newServiceID autorelease];
284         } else if ([serviceID rangeOfString:@"LIBGAIM" options:(NSLiteralSearch | NSAnchoredSearch | NSBackwardsSearch)].location != NSNotFound) {
285                 if ([serviceID isEqualToString:@"AIM-LIBGAIM"]) {
286                         NSString        *uid = [accountDict objectForKey:ACCOUNT_UID];
287                         if (uid && [uid length]) {
288                                 const char      firstCharacter = [uid characterAtIndex:0];
289                                 
290                                 if ([uid hasSuffix:@"@mac.com"]) {
291                                         serviceID = @"libpurple-oscar-Mac";
292                                 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
293                                         serviceID = @"libpurple-oscar-ICQ";
294                                 } else {
295                                         serviceID = @"libpurple-oscar-AIM";
296                                 }
297                         }
298                 } else if ([serviceID isEqualToString:@"GaduGadu-LIBGAIM"]) {
299                         serviceID = @"libpurple-Gadu-Gadu";
300                 } else if ([serviceID isEqualToString:@"Jabber-LIBGAIM"]) {
301                         serviceID = @"libpurple-Jabber";
302                 } else if ([serviceID isEqualToString:@"MSN-LIBGAIM"]) {
303                         serviceID = @"libpurple-MSN";
304                 } else if ([serviceID isEqualToString:@"Napster-LIBGAIM"]) {
305                         serviceID = @"libpurple-Napster";
306                 } else if ([serviceID isEqualToString:@"Novell-LIBGAIM"]) {
307                         serviceID = @"libpurple-GroupWise";
308                 } else if ([serviceID isEqualToString:@"Sametime-LIBGAIM"]) {
309                         serviceID = @"libpurple-Sametime";
310                 } else if ([serviceID isEqualToString:@"Yahoo-LIBGAIM"]) {
311                         serviceID = @"libpurple-Yahoo!";
312                 } else if ([serviceID isEqualToString:@"Yahoo-Japan-LIBGAIM"]) {
313                         serviceID = @"libpurple-Yahoo!-Japan";
314                 }
315         } else if ([serviceID isEqualToString:@"rvous-libezv"])
316                 serviceID = @"bonjour-libezv";
317         else if ([serviceID isEqualToString:@"joscar-OSCAR-AIM"])
318                 serviceID = @"libpurple-oscar-AIM";
319         else if ([serviceID isEqualToString:@"joscar-OSCAR-dotMac"])
320                 serviceID = @"libpurple-oscar-Mac";
321         
322         return serviceID;
326  * @brief Save accounts to disk
327  */
328 - (void)_saveAccounts
330         NSMutableArray  *flatAccounts = [NSMutableArray array];
331         NSEnumerator    *enumerator;
332         AIAccount               *account;
333         
334         //Build a flattened array of the accounts
335         enumerator = [accounts objectEnumerator];
336         while ((account = [enumerator nextObject])) {
337                 if (![account isTemporary]) {
338                         NSMutableDictionary             *flatAccount = [NSMutableDictionary dictionary];
339                         AIService                               *service = [account service];
340                         [flatAccount setObject:[service serviceCodeUniqueID] forKey:ACCOUNT_TYPE];      //Unique plugin ID
341                         [flatAccount setObject:[service serviceID] forKey:ACCOUNT_SERVICE];             //Shared service ID
342                         [flatAccount setObject:[account UID] forKey:ACCOUNT_UID];                                                       //Account UID
343                         [flatAccount setObject:[account internalObjectID] forKey:ACCOUNT_OBJECT_ID];                    //Account Object ID
344                         
345                         [flatAccounts addObject:flatAccount];
346                 }
347         }
348         
349         //Add any unloadable accounts so they're not lost
350         [flatAccounts addObjectsFromArray:unloadableAccounts];
352         //Save and broadcast an account list changed notification
353         [[adium preferenceController] setPreference:flatAccounts forKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
354         [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
358  * @brief Perform upgrades for a new version
360  * 1.0: KEY_ACCOUNT_DISPLAY_NAME and @"TextProfile" cleared if @"" and moved to global if identical on all accounts
361  */
362 - (void)upgradeAccounts
364         NSUserDefaults  *userDefaults = [NSUserDefaults standardUserDefaults];
365         NSNumber                *upgradedAccounts = [userDefaults objectForKey:@"Adium:Account Prefs Upgraded for 1.0"];
366         
367         if (!upgradedAccounts || ![upgradedAccounts boolValue]) {
368                 [userDefaults setObject:[NSNumber numberWithBool:YES] forKey:@"Adium:Account Prefs Upgraded for 1.0"];
370                 AIAccount               *account;
371                 NSEnumerator    *enumerator, *keyEnumerator;
372                 NSString                *key;
374                 //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.
375                 NSSet   *keysWeNowUseGlobally = [NSSet setWithObjects:KEY_ACCOUNT_DISPLAY_NAME, @"TextProfile", nil];
377                 NSCharacterSet  *whitespaceAndNewlineCharacterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
379                 keyEnumerator = [keysWeNowUseGlobally objectEnumerator];
380                 while ((key = [keyEnumerator nextObject])) {
381                         NSAttributedString      *firstAttributedString = nil;
382                         BOOL                            allOnThisKeyAreTheSame = YES;
384                         enumerator = [[self accounts] objectEnumerator];
385                         while ((account = [enumerator nextObject])) {
386                                 NSAttributedString *attributedString = [[account preferenceForKey:key
387                                                                                                                                                         group:GROUP_ACCOUNT_STATUS
388                                                                                                                         ignoreInheritedValues:YES] attributedString];
389                                 if (attributedString && ![attributedString length]) {
390                                         [account setPreference:nil
391                                                                         forKey:key
392                                                                          group:GROUP_ACCOUNT_STATUS];
393                                         attributedString = nil;
394                                 }
395                                 
396                                 if (attributedString) {
397                                         if (firstAttributedString) {
398                                                 /* If this string is not the same as the first one we found, all are not the same.
399                                                  * Only need to check if thus far they all have been the same
400                                                  */
401                                                 if (allOnThisKeyAreTheSame &&
402                                                         ![[[attributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet] isEqualToString:
403                                                                 [[firstAttributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet]]) {
404                                                         allOnThisKeyAreTheSame = NO;
405                                                 }
406                                         } else {
407                                                 //Note the first one we find, which will be our reference
408                                                 firstAttributedString = attributedString;
409                                         }
410                                 }
411                         }
412                         
413                         if (allOnThisKeyAreTheSame && firstAttributedString) {
414                                 //All strings on this key are the same. Set the preference globally...
415                                 [[adium preferenceController] setPreference:[firstAttributedString dataRepresentation]
416                                                                                                          forKey:key
417                                                                                                           group:GROUP_ACCOUNT_STATUS];
418                                 
419                                 //And remove it from all accounts
420                                 enumerator = [[self accounts] objectEnumerator];
421                                 while ((account = [enumerator nextObject])) {
422                                         [account setPreference:nil
423                                                                         forKey:key
424                                                                          group:GROUP_ACCOUNT_STATUS];
425                                 }
426                         }
427                 }
429                 [userDefaults synchronize];
430         }
433 @end