{{{
[adiumx.git] / Source / AdiumAccounts.m
blobc9b1991dd6860a089b555e66c317a9f91f50ca8f
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                 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];
236                 
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];
241             } else {
242                                 [unloadableAccounts addObject:accountDict];
243                         }
244         }
245     }
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
257  */
258 - (NSString *)_upgradeServiceID:(NSString *)serviceID forAccountDict:(NSDictionary *)accountDict
260         //Libgaim
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];
266                                 
267                                 if ([uid hasSuffix:@"@mac.com"]) {
268                                         serviceID = @"libgaim-oscar-Mac";
269                                 } else if (firstCharacter >= '0' && firstCharacter <= '9') {
270                                         serviceID = @"libgaim-oscar-ICQ";
271                                 } else {
272                                         serviceID = @"libgaim-oscar-AIM";
273                                 }
274                         }
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";
291                 }
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";
297         /*
298          //Reenable if joscar takes over ICQ again
299         else if ([serviceID isEqualToString:@"libgaim-oscar-ICQ"])
300                 serviceID = @"joscar-OSCAR-ICQ";
301         */
302         else if ([serviceID isEqualToString:@"libgaim-oscar-Mac"])
303                 serviceID = @"joscar-OSCAR-dotMac";
304 #endif
305         
306         return serviceID;
310  * @brief Save accounts to disk
311  */
312 - (void)_saveAccounts
314         NSMutableArray  *flatAccounts = [NSMutableArray array];
315         NSEnumerator    *enumerator;
316         AIAccount               *account;
317         
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
328                         
329                         [flatAccounts addObject:flatAccount];
330                 }
331         }
332         
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
345  */
346 - (void)upgradeAccounts
348         NSUserDefaults  *userDefaults = [NSUserDefaults standardUserDefaults];
349         NSNumber                *upgradedAccounts = [userDefaults objectForKey:@"Adium:Account Prefs Upgraded for 1.0"];
350         
351         if (!upgradedAccounts || ![upgradedAccounts boolValue]) {
352                 [userDefaults setObject:[NSNumber numberWithBool:YES] forKey:@"Adium:Account Prefs Upgraded for 1.0"];
354                 AIAccount               *account;
355                 NSEnumerator    *enumerator, *keyEnumerator;
356                 NSString                *key;
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
375                                                                         forKey:key
376                                                                          group:GROUP_ACCOUNT_STATUS];
377                                         attributedString = nil;
378                                 }
379                                 
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
384                                                  */
385                                                 if (allOnThisKeyAreTheSame &&
386                                                         ![[[attributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet] isEqualToString:
387                                                                 [[firstAttributedString string] stringByTrimmingCharactersInSet:whitespaceAndNewlineCharacterSet]]) {
388                                                         allOnThisKeyAreTheSame = NO;
389                                                 }
390                                         } else {
391                                                 //Note the first one we find, which will be our reference
392                                                 firstAttributedString = attributedString;
393                                         }
394                                 }
395                         }
396                         
397                         if (allOnThisKeyAreTheSame && firstAttributedString) {
398                                 //All strings on this key are the same. Set the preference globally...
399                                 [[adium preferenceController] setPreference:[firstAttributedString dataRepresentation]
400                                                                                                          forKey:key
401                                                                                                           group:GROUP_ACCOUNT_STATUS];
402                                 
403                                 //And remove it from all accounts
404                                 enumerator = [[self accounts] objectEnumerator];
405                                 while ((account = [enumerator nextObject])) {
406                                         [account setPreference:nil
407                                                                         forKey:key
408                                                                          group:GROUP_ACCOUNT_STATUS];
409                                 }
410                         }
411                 }
413                 [userDefaults synchronize];
414         }
417 @end