2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "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>
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;
42 * @class AdiumAccounts
43 * @brief Class to handle AIAccount access and creation
45 * This is a private class used by AIAccountController, its public interface.
47 @implementation AdiumAccounts
50 if ((self = [super init])) {
51 accounts = [[NSMutableArray alloc] init];
52 unloadableAccounts = [[NSMutableArray alloc] init];
60 [unloadableAccounts release];
66 * @brief Finish Initing
68 * Requires the all AIServices have registered
70 - (void)controllerDidLoad
74 [self upgradeAccounts];
78 //Accounts -------------------------------------------------------------------------------------------------------
81 * @brief Returns an array of all available accounts
83 * @return NSArray of AIAccount instances
91 * @brief Returns an array of accounts compatible with a service
93 * @param service AIService for compatible accounts
94 * @param NSArray of AIAccount instances
96 - (NSArray *)accountsCompatibleWithService:(AIService *)service
98 NSMutableArray *matchingAccounts = [NSMutableArray array];
99 NSEnumerator *enumerator = [accounts objectEnumerator];
101 NSString *serviceClass = [service serviceClass];
103 while ((account = [enumerator nextObject])) {
104 if ([account enabled] &&
105 [[[account service] serviceClass] isEqualToString:serviceClass]) {
106 [matchingAccounts addObject:account];
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;
129 //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
139 - (AIAccount *)createAccountWithService:(AIService *)service UID:(NSString *)inUID
141 return [service accountWithUID:inUID internalObjectID:[self _generateUniqueInternalObjectID]];
145 * @brief Add an account
147 * @param inAccount AIAccount to add
149 - (void)addAccount:(AIAccount *)inAccount
151 [accounts addObject:inAccount];
152 [self _saveAccounts];
156 * @brief Delete an account
158 * @param inAccount AIAccount to delete
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
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
191 - (void)accountDidChangeUID:(AIAccount *)account
193 [self _saveAccounts];
197 * @brief Generate a unique account InternalObjectID
199 * @return NSString unique InternalObjectID
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];
207 [[adium preferenceController] setPreference:[NSNumber numberWithInt:topAccountID + 1]
208 forKey:TOP_ACCOUNT_ID
209 group:PREF_GROUP_ACCOUNTS];
211 return internalObjectID;
215 //Storage --------------------------------------------------------------------------------------------------------------
218 * @brief Load accounts from disk
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];
243 NSLog(@"Could not load account %@",accountDict);
244 [unloadableAccounts addObject:accountDict];
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];
252 AILog(@"Ignored an account with a 0 length accountUID: %@", accountDict);
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 libgaim, 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
269 - (NSString *)_upgradeServiceID:(NSString *)serviceID forAccountDict:(NSDictionary *)accountDict
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];
286 if ([uid hasSuffix:@"@mac.com"]) {
287 serviceID = @"libgaim-oscar-Mac";
288 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
289 serviceID = @"libgaim-oscar-ICQ";
291 serviceID = @"libgaim-oscar-AIM";
294 } else if ([serviceID isEqualToString:@"GaduGadu-LIBGAIM"]) {
295 serviceID = @"libgaim-Gadu-Gadu";
296 } else if ([serviceID isEqualToString:@"Jabber-LIBGAIM"]) {
297 serviceID = @"libgaim-Jabber";
298 } else if ([serviceID isEqualToString:@"MSN-LIBGAIM"]) {
299 serviceID = @"libgaim-MSN";
300 } else if ([serviceID isEqualToString:@"Napster-LIBGAIM"]) {
301 serviceID = @"libgaim-Napster";
302 } else if ([serviceID isEqualToString:@"Novell-LIBGAIM"]) {
303 serviceID = @"libgaim-GroupWise";
304 } else if ([serviceID isEqualToString:@"Sametime-LIBGAIM"]) {
305 serviceID = @"libgaim-Sametime";
306 } else if ([serviceID isEqualToString:@"Yahoo-LIBGAIM"]) {
307 serviceID = @"libgaim-Yahoo!";
308 } else if ([serviceID isEqualToString:@"Yahoo-Japan-LIBGAIM"]) {
309 serviceID = @"libgaim-Yahoo!-Japan";
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";
322 * @brief Save accounts to disk
324 - (void)_saveAccounts
326 NSMutableArray *flatAccounts = [NSMutableArray array];
327 NSEnumerator *enumerator;
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
341 [flatAccounts addObject:flatAccount];
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
358 - (void)upgradeAccounts
360 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
361 NSNumber *upgradedAccounts = [userDefaults objectForKey:@"Adium:Account Prefs Upgraded for 1.0"];
363 if (!upgradedAccounts || ![upgradedAccounts boolValue]) {
364 [userDefaults setObject:[NSNumber numberWithBool:YES] forKey:@"Adium:Account Prefs Upgraded for 1.0"];
367 NSEnumerator *enumerator, *keyEnumerator;
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
388 group:GROUP_ACCOUNT_STATUS];
389 attributedString = nil;
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
397 if (allOnThisKeyAreTheSame &&
398 ![[[attributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet] isEqualToString:
399 [[firstAttributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet]]) {
400 allOnThisKeyAreTheSame = NO;
403 //Note the first one we find, which will be our reference
404 firstAttributedString = attributedString;
409 if (allOnThisKeyAreTheSame && firstAttributedString) {
410 //All strings on this key are the same. Set the preference globally...
411 [[adium preferenceController] setPreference:[firstAttributedString dataRepresentation]
413 group:GROUP_ACCOUNT_STATUS];
415 //And remove it from all accounts
416 enumerator = [[self accounts] objectEnumerator];
417 while ((account = [enumerator nextObject])) {
418 [account setPreference:nil
420 group:GROUP_ACCOUNT_STATUS];
425 [userDefaults synchronize];