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 NSString *serviceID = [self _upgradeServiceID:[accountDict objectForKey:ACCOUNT_TYPE] forAccountDict:accountDict];
230 AIAccount *newAccount;
232 //Fetch the account service, UID, and ID
233 AIService *service = [[adium accountController] serviceWithUniqueID:serviceID];
234 NSString *accountUID = [accountDict objectForKey:ACCOUNT_UID];
235 NSString *internalObjectID = [accountDict objectForKey:ACCOUNT_OBJECT_ID];
237 //Create the account and add it to our array
238 if (service && accountUID && [accountUID length]) {
239 if ((newAccount = [service accountWithUID:accountUID internalObjectID:internalObjectID])) {
240 [accounts addObject:newAccount];
242 [unloadableAccounts addObject:accountDict];
247 //Broadcast an account list changed notification
248 [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
252 * @brief Temporary serviceID upgrade code (v0.63 -> v0.70 for libgaim, v0.70 -> v0.80 for bonjour)
254 * @param serviceID NSString service ID (old or new)
255 * @param accountDict Dictionary of the saved account
256 * @return NSString service ID (new), or nil if unable to upgrade
258 - (NSString *)_upgradeServiceID:(NSString *)serviceID forAccountDict:(NSDictionary *)accountDict
261 if ([serviceID rangeOfString:@"LIBGAIM" options:(NSLiteralSearch | NSAnchoredSearch | NSBackwardsSearch)].location != NSNotFound) {
262 if ([serviceID isEqualToString:@"AIM-LIBGAIM"]) {
263 NSString *uid = [accountDict objectForKey:ACCOUNT_UID];
264 if (uid && [uid length]) {
265 const char firstCharacter = [uid characterAtIndex:0];
267 if ([uid hasSuffix:@"@mac.com"]) {
268 serviceID = @"libgaim-oscar-Mac";
269 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
270 serviceID = @"libgaim-oscar-ICQ";
272 serviceID = @"libgaim-oscar-AIM";
275 } else if ([serviceID isEqualToString:@"GaduGadu-LIBGAIM"]) {
276 serviceID = @"libgaim-Gadu-Gadu";
277 } else if ([serviceID isEqualToString:@"Jabber-LIBGAIM"]) {
278 serviceID = @"libgaim-Jabber";
279 } else if ([serviceID isEqualToString:@"MSN-LIBGAIM"]) {
280 serviceID = @"libgaim-MSN";
281 } else if ([serviceID isEqualToString:@"Napster-LIBGAIM"]) {
282 serviceID = @"libgaim-Napster";
283 } else if ([serviceID isEqualToString:@"Novell-LIBGAIM"]) {
284 serviceID = @"libgaim-GroupWise";
285 } else if ([serviceID isEqualToString:@"Sametime-LIBGAIM"]) {
286 serviceID = @"libgaim-Sametime";
287 } else if ([serviceID isEqualToString:@"Yahoo-LIBGAIM"]) {
288 serviceID = @"libgaim-Yahoo!";
289 } else if ([serviceID isEqualToString:@"Yahoo-Japan-LIBGAIM"]) {
290 serviceID = @"libgaim-Yahoo!-Japan";
292 } else if ([serviceID isEqualToString:@"rvous-libezv"])
293 serviceID = @"bonjour-libezv";
294 #ifdef JOSCAR_SUPERCEDE_LIBGAIM
295 else if ([serviceID isEqualToString:@"libgaim-oscar-AIM"])
296 serviceID = @"joscar-OSCAR-AIM";
298 //Reenable if joscar takes over ICQ again
299 else if ([serviceID isEqualToString:@"libgaim-oscar-ICQ"])
300 serviceID = @"joscar-OSCAR-ICQ";
302 else if ([serviceID isEqualToString:@"libgaim-oscar-Mac"])
303 serviceID = @"joscar-OSCAR-dotMac";
310 * @brief Save accounts to disk
312 - (void)_saveAccounts
314 NSMutableArray *flatAccounts = [NSMutableArray array];
315 NSEnumerator *enumerator;
318 //Build a flattened array of the accounts
319 enumerator = [accounts objectEnumerator];
320 while ((account = [enumerator nextObject])) {
321 if (![account isTemporary]) {
322 NSMutableDictionary *flatAccount = [NSMutableDictionary dictionary];
323 AIService *service = [account service];
324 [flatAccount setObject:[service serviceCodeUniqueID] forKey:ACCOUNT_TYPE]; //Unique plugin ID
325 [flatAccount setObject:[service serviceID] forKey:ACCOUNT_SERVICE]; //Shared service ID
326 [flatAccount setObject:[account UID] forKey:ACCOUNT_UID]; //Account UID
327 [flatAccount setObject:[account internalObjectID] forKey:ACCOUNT_OBJECT_ID]; //Account Object ID
329 [flatAccounts addObject:flatAccount];
333 //Add any unloadable accounts so they're not lost
334 [flatAccounts addObjectsFromArray:unloadableAccounts];
336 //Save and broadcast an account list changed notification
337 [[adium preferenceController] setPreference:flatAccounts forKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
338 [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
342 * @brief Perform upgrades for a new version
344 * 1.0: KEY_ACCOUNT_DISPLAY_NAME and @"TextProfile" cleared if @"" and moved to global if identical on all accounts
346 - (void)upgradeAccounts
348 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
349 NSNumber *upgradedAccounts = [userDefaults objectForKey:@"Adium:Account Prefs Upgraded for 1.0"];
351 if (!upgradedAccounts || ![upgradedAccounts boolValue]) {
352 [userDefaults setObject:[NSNumber numberWithBool:YES] forKey:@"Adium:Account Prefs Upgraded for 1.0"];
355 NSEnumerator *enumerator, *keyEnumerator;
358 //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.
359 NSSet *keysWeNowUseGlobally = [NSSet setWithObjects:KEY_ACCOUNT_DISPLAY_NAME, @"TextProfile", nil];
361 NSCharacterSet *whitespaceAndNewlineCharacterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
363 keyEnumerator = [keysWeNowUseGlobally objectEnumerator];
364 while ((key = [keyEnumerator nextObject])) {
365 NSAttributedString *firstAttributedString = nil;
366 BOOL allOnThisKeyAreTheSame = YES;
368 enumerator = [[self accounts] objectEnumerator];
369 while ((account = [enumerator nextObject])) {
370 NSAttributedString *attributedString = [[account preferenceForKey:key
371 group:GROUP_ACCOUNT_STATUS
372 ignoreInheritedValues:YES] attributedString];
373 if (attributedString && ![attributedString length]) {
374 [account setPreference:nil
376 group:GROUP_ACCOUNT_STATUS];
377 attributedString = nil;
380 if (attributedString) {
381 if (firstAttributedString) {
382 /* If this string is not the same as the first one we found, all are not the same.
383 * Only need to check if thus far they all have been the same
385 if (allOnThisKeyAreTheSame &&
386 ![[[attributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet] isEqualToString:
387 [[firstAttributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet]]) {
388 allOnThisKeyAreTheSame = NO;
391 //Note the first one we find, which will be our reference
392 firstAttributedString = attributedString;
397 if (allOnThisKeyAreTheSame && firstAttributedString) {
398 //All strings on this key are the same. Set the preference globally...
399 [[adium preferenceController] setPreference:[firstAttributedString dataRepresentation]
401 group:GROUP_ACCOUNT_STATUS];
403 //And remove it from all accounts
404 enumerator = [[self accounts] objectEnumerator];
405 while ((account = [enumerator nextObject])) {
406 [account setPreference:nil
408 group:GROUP_ACCOUNT_STATUS];
413 [userDefaults synchronize];