Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / BGICImportController.m
blob6ff78c024f5ea420cb11f934a77f31fed7e3feb8
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 "BGICImportController.h"
18 #import "BGICLogImportController.h"
20 #import "ESAIMService.h"
21 #import "ESDotMacService.h"
22 #import "ESJabberService.h"
23 #import "AWBonjourService.h"
25 #import <Adium/AIStatusControllerProtocol.h>
26 #import <Adium/AIStatusGroup.h>
27 #import <Adium/AIAccountControllerProtocol.h>
29 #import <AIUtilities/AIFileManagerAdditions.h>
30 #import <AIUtilities/AIStringAdditions.h>
32 #define ICHAT_LOCATION [@"~/Documents/iChats/" stringByExpandingTildeInPath]
34 @interface BGICImportController (PRIVATE)
35 -(void)startLogImport;
36 -(void)populateAccountPicker;
37 -(void)deleteAllFromiChat;
38 -(void)importAccountsForService:(NSString *)serviceName;
39 -(void)importLogs;
40 -(void)importStatuses;
41 -(void)addStatusFromString:(NSString *)statusString isAway:(BOOL)shouldBeAway withGroup:(AIStatusGroup *)parentGroup;
42 @end
44 @implementation BGICImportController (PRIVATE)
46 -(void)startLogImport
48         [backButton setEnabled:NO];
49         [importProgress startAnimation:importProgress];
50         [importDetails setStringValue:[AILocalizedString(@"Gathering list of transcripts", nil) stringByAppendingEllipsis]];
51         [loggingPanes selectTabViewItemWithIdentifier:@"import"];
52         [cancelImportButton setHidden:NO];
53         fullDump = [[[NSFileManager defaultManager] subpathsAtPath:ICHAT_LOCATION] retain];
54         dumpCount = [fullDump count];
55         dumpLoop = 0;
56         currentStep--;
57         cancelImport = NO;
58         [self performSelector:@selector(importLogs) withObject:nil afterDelay:0.15];    
61 // loop the accounts in Adium and add them to the popup.
62 // This selection will be used for the log folder target (service.account in PATH_LOGS to be exact)
63 -(void)populateAccountPicker
65         [accountSelectionPopup removeAllItems];
66         
67         if (accountsArray == nil)
68                 accountsArray = [[NSMutableArray alloc] init];
69         else
70                 [accountsArray removeAllObjects];
71         
72         NSArray *accountsAvailable = [[adium accountController] accounts];
73         
74         if ([accountsAvailable count] > 0) {
75                 [accountSelectionPopup setHidden:NO];
77                 for (int accountLoop = 0; accountLoop < [accountsAvailable count]; accountLoop++) {
78                         AIAccount *currentAccount = [accountsAvailable objectAtIndex:accountLoop];
79                         [accountSelectionLabel setStringValue:AILocalizedString(@"Please select an account into which to import your transcripts:", nil)];
80                         [accountSelectionPopup addItemWithTitle:[NSString stringWithFormat:@"%@ (%@)", [currentAccount formattedUID], [currentAccount serviceID]]];
81                         [accountsArray addObject:[NSString stringWithFormat:@"%@.%@", [currentAccount serviceID], [currentAccount formattedUID]]];
82                 }               
83         } else {
84                 // no accounts are present so we'll error this phase out
85                 currentStep--;
86                 [accountSelectionLabel setStringValue:AILocalizedString(@"Importing transcripts requires at least 1 account to be present.", nil)];
87                 [accountSelectionPopup setHidden:YES];
88                 [backButton setEnabled:YES];
89                 [proceedButton setEnabled:YES];
90         }
93 // loop through the iChat log paths and move them all to the Trash
94 -(void)deleteAllFromiChat
95 {       
96         for (int deleteLoop = 0; deleteLoop < [fullDump count]; deleteLoop++) {
97                 NSString *logPath = [fullDump objectAtIndex:deleteLoop];
98                 
99                 if ([logPath rangeOfString:@"DS_Store"].length == 0)
100                         [[NSFileManager defaultManager] trashFileAtPath:[[ICHAT_LOCATION stringByAppendingPathComponent:logPath] stringByExpandingTildeInPath]];
101         }       
102         
103         [importDetails setStringValue:AILocalizedString(@"Your iChat transcripts have been removed.", nil)];
104         [proceedButton setEnabled:YES];
107 -(void)importAccountsForService:(NSString *)serviceName
109         // com.apple.iChat.AIM.plist -> accounts on AIM
110         NSDictionary *rawPrefsFile = [NSDictionary dictionaryWithContentsOfFile:[[NSString stringWithFormat:@"~/Library/Preferences/com.apple.iChat.%@.plist", serviceName] stringByExpandingTildeInPath]];
111         NSArray *accountsFromRaw = [[rawPrefsFile valueForKey:@"Accounts"] allValues];
112                 
113         NSEnumerator *serviceEnum = [[[adium accountController] services] objectEnumerator];
114         AIService *service = nil;
115         
116         // we'll grab these momentarily and use judiciously afterwards, Bonjour is external to this to method, unlike the others
117         ESAIMService *aimService = nil;
118         ESDotMacService *macService = nil;
119         ESJabberService *jabberService = nil;   
120                                         
121         while ((service = [serviceEnum nextObject])) {
122                 if ([[service serviceID] isEqual:@"AIM"])
123                         aimService = (ESAIMService *)service;
124                 if ([[service serviceID] isEqual:@"Mac"])
125                         macService = (ESDotMacService *)service;
126                 if ([[service serviceID] isEqual:@"Jabber"])
127                         jabberService = (ESJabberService *)service;
128                 if ([[service serviceID] isEqual:@"Bonjour"])
129                         bonjourService = (AWBonjourService *)service;
130         }       
131         
132         for (int accountLoop = 0; accountLoop < [accountsFromRaw count]; accountLoop++) {
133                 if (![serviceName isEqual:@"SubNet"]) {
134                         NSDictionary *currentAccount = [accountsFromRaw objectAtIndex:accountLoop];
135                         
136                         NSString *accountName = [currentAccount objectForKey:@"LoginAs"];
137                         
138                         AIAccount *newAcct = [[adium accountController] createAccountWithService:
139                                 ([serviceName isEqual:@"Jabber"] ? (AIService *)jabberService : ([accountName rangeOfString:@"mac.com"].length > 0 ? (AIService *)macService : (AIService *)aimService))
140                                                                                                                                                                  UID:accountName];
141                         if (newAcct == nil)
142                                 continue;
143                         
144                         NSNumber *autoLogin = [currentAccount objectForKey:@"AutoLogin"];
145                         [newAcct setPreference:autoLogin
146                                                         forKey:@"Online"
147                                                          group:GROUP_ACCOUNT_STATUS];
148                         
149                         NSString *serverHost = [currentAccount objectForKey:@"ServerHost"];
150                         if ([serverHost length] > 0)
151                                 [newAcct setPreference:serverHost
152                                                                 forKey:KEY_CONNECT_HOST
153                                                                  group:GROUP_ACCOUNT_STATUS];   
154                         
155                         NSNumber *serverPort = [currentAccount objectForKey:@"ServerPort"];
156                         if (serverPort)
157                                 [newAcct setPreference:serverPort
158                                                                 forKey:KEY_CONNECT_PORT
159                                                                  group:GROUP_ACCOUNT_STATUS];
160                         
161                         [[adium accountController] addAccount:newAcct];
163                 } else {
164                         blockForBonjour = YES;                  
165                         
166                         // iChat stores only the fact that the Default (username) account should be used and whether to auto-login
167                         NSDictionary *currentAccount = [accountsFromRaw objectAtIndex:accountLoop];
168                         bonjourAutoLogin = [[currentAccount objectForKey:@"AutoLogin"] boolValue];
170                         // Adium, however, has a more flexible Bonjour account configuration and we have to take this into account.
171                         [NSApp beginSheet:bonjourNamePromptWindow modalForWindow:[self window] modalDelegate:nil didEndSelector:nil contextInfo:nil];
172                         
173                 }
174         }
177 -(void)importLogs 
179         if (dumpLoop == 0) {
180                 [importProgress setIndeterminate:NO];
181                 [importProgress setMaxValue:dumpCount];
182                 [importProgress setMinValue:0];
183                 [deleteLogsButton setHidden:YES];
184                 [cancelImportButton setHidden:NO];
185         }
186         
187         if (dumpLoop >= dumpCount) {
188                 [importProgress setIndeterminate:YES];
189                 [importProgress stopAnimation:importProgress];
190                 [importDetails setStringValue:AILocalizedString(@"Transcript importing complete.", nil)];
191                 [importProgress setHidden:YES];
192                 [proceedButton setEnabled:YES];
193                 [deleteLogsButton setHidden:NO];
194                 [backButton setEnabled:YES];
195                 [cancelImportButton setHidden:YES];
197         } else {
198                 NSString *logPath = [fullDump objectAtIndex:dumpLoop];
199                 
200                 if (!logImporter) logImporter = [[BGICLogImportController alloc] initWithDestination:destinationAccount];
201                 
202                 [importProgress setDoubleValue:dumpLoop];
203                 [importDetails setStringValue:[NSString stringWithFormat:[AILocalizedString(@"Now importing transcript %i of %i: %@", "%i will be a number; %@ is a name")  stringByAppendingEllipsis],
204                                                                            dumpLoop, dumpCount, [logPath stringByDeletingPathExtension]]];
205                 
206                 if ([logPath rangeOfString:@"DS_Store"].location == NSNotFound) {
207                         // pass the current log's path over and let the log conversion class do it's work
208                         [logImporter createNewLogForPath:[[ICHAT_LOCATION stringByAppendingPathComponent:logPath] stringByExpandingTildeInPath]];
209                 }
210         }
212         if (dumpLoop < dumpCount && cancelImport == NO) {
213                 [self performSelector:@selector(importLogs) withObject:nil afterDelay:0.10];
214         }
215                 
216         if (cancelImport) {
217                 [importDetails setStringValue:[NSString stringWithFormat:AILocalizedString(@"Transcript importing cancelled. %i of %i transcripts already imported.", nil),
218                                                                            dumpLoop, dumpCount]];
219                 [importProgress setIndeterminate:YES];
220                 [importProgress stopAnimation:importProgress];
221                 [importProgress setHidden:YES];
222                 [cancelImportButton setHidden:YES];
223                 [backButton setEnabled:YES];
224                 [proceedButton setEnabled:YES];
225         }
226         
227         dumpLoop++;
230 -(void)importStatuses
232         // iChat (on 10.4 at least) stores custom statuses in a couple of arrays in it's plist
233         NSDictionary *ichatPrefs = [NSDictionary dictionaryWithContentsOfFile:[@"~/Library/Preferences/com.apple.iChat.plist" stringByExpandingTildeInPath]];
234                 
235         // loop through the availables and add them
236         NSArray *customAvailable = [ichatPrefs objectForKey:@"CustomAvailableMessages"];
237         
238         [importStatusDetails setStringValue:[NSString stringWithFormat:[AILocalizedString(@"Now importing %i 'Available' messages", nil) stringByAppendingEllipsis],
239                                                                                  [customAvailable count]]];
240         
241         AIStatusGroup *availableGroup = nil;
242         
243         // optionally create a status group for collecting them together
244         if ([createStatusGroupsButton state] == NSOnState) {
245                 availableGroup = [AIStatusGroup statusGroup];
246                 [availableGroup setTitle:AILocalizedString(@"iChat Available Messages", nil)];
247                 [availableGroup setStatusType:AIAvailableStatusType];
248                 // add to the set
249                 [[[adium statusController] rootStateGroup] addStatusItem:availableGroup atIndex:-1];
250         }
252         for (int availableLoop = 0; availableLoop < [customAvailable count]; availableLoop++) {
253                 [self addStatusFromString:[customAvailable objectAtIndex:availableLoop] isAway:NO withGroup:availableGroup];
254         }
255         
256         AIStatusGroup *awayGroup = nil;
257         
258         // optionally create a status group for collecting them together
259         if ([createStatusGroupsButton state] == NSOnState) {
260                 awayGroup = [AIStatusGroup statusGroup];
261                 [awayGroup setTitle:AILocalizedString(@"iChat Away Messages", nil)];
262                 [awayGroup setStatusType:AIAwayStatusType];
263                 // add to the set
264                 [[[adium statusController] rootStateGroup] addStatusItem:awayGroup atIndex:-1];
265         }       
267         // loop through the aways and add them
268         NSArray *customAways = [ichatPrefs objectForKey:@"CustomAwayMessages"];
269         
270         [importStatusDetails setStringValue:[NSString stringWithFormat:[AILocalizedString(@"Now importing %i 'Away' messages", nil) stringByAppendingEllipsis],
271                                                                                  [customAways count]]];
273         for (int awayLoop = 0; awayLoop < [customAways count]; awayLoop++) {
274                 [self addStatusFromString:[customAways objectAtIndex:awayLoop] isAway:YES withGroup:awayGroup];
275         }
276                 
277         [importStatusDetails setStringValue:AILocalizedString(@"Status importing is now complete.", nil)];
278         [importStatusProgress stopAnimation:importStatusProgress];
279         [backButton setEnabled:YES];
282 // the only difference between imported statuses is their type and reply behavior (optionally can be added to a group)
283 -(void)addStatusFromString:(NSString *)statusString isAway:(BOOL)shouldBeAway withGroup:(AIStatusGroup *)parentGroup
285         AIStatus *newStatus = [AIStatus statusOfType:(shouldBeAway ? AIAwayStatusType : AIAvailableStatusType)];
286         [newStatus setTitle:statusString];
287         [newStatus setStatusMessage:[[[NSAttributedString alloc] initWithString:statusString] autorelease]];
288         [newStatus setAutoReplyIsStatusMessage:(shouldBeAway ? YES : NO)];
289         [newStatus setShouldForceInitialIdleTime:NO];
290         
291         // optionally add to a status group
292         if (parentGroup == nil) {
293                 [[adium statusController] addStatusState:newStatus];    
294         } else {
295                 [parentGroup addStatusItem:newStatus atIndex:-1];
296         }
299 @end
301 @implementation BGICImportController
303 + (void)importIChatConfiguration
305         //This is a leak.
306         BGICImportController *ichatCon = [[BGICImportController alloc] initWithWindowNibName:@"ICImport"];
307         [ichatCon showWindow:ichatCon]; 
310 -(void)awakeFromNib {
311         currentStep = 0;
312         
313         //Configure our background view; it should display the image transparently where our tabView overlaps it
314         [backgroundView setBackgroundImage:[NSImage imageNamed:@"AdiumyButler"]];
315         NSRect tabViewFrame = [assistantPanes frame];
316         NSRect backgroundViewFrame = [backgroundView frame];
317         tabViewFrame.origin.x -= backgroundViewFrame.origin.x;
318         tabViewFrame.origin.y -= backgroundViewFrame.origin.y;
319         [backgroundView setTransparentRect:tabViewFrame];       
321         [importAccountsButton setState:NSOnState];
322         [importStatusButton setState:NSOnState];
323         [createStatusGroupsButton setState:NSOnState];
324         [importLogsButton setState:NSOnState];
326         [[self window] center];
327         
328         [assistantPanes selectTabViewItemWithIdentifier:@"start"];
329         [backButton setEnabled:NO];
332 -(IBAction)openHelp:(id)sender
334 #warning This help anchor is necessary and needs a corresponding page in the book + the index needs regenerated.
335         NSString *locBookName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleHelpBookName"];
336         [[NSHelpManager sharedHelpManager] openHelpAnchor:@"ichatImport"  inBook:locBookName];
339 // this action is currently defined as returning to the start of the assistant, unchecking all and noting completed actions
340 -(IBAction)goBack:(id)sender
342         currentStep = 0;
343         
344         [backButton setEnabled:NO];
345         [proceedButton setEnabled:YES];
346         [proceedButton setTitle:AILocalizedStringFromTable(@"Continue", @"Buttons", nil)]; // in case we are on the last step
347         
348         [importAccountsButton setState:NSOffState];
349         [importStatusButton setState:NSOffState];
350         [importLogsButton setState:NSOffState];
351         [createStatusGroupsButton setState:NSOffState];
352         
353         [assistantPanes selectTabViewItemWithIdentifier:@"start"];
356 -(IBAction)proceed:(id)sender 
358         BOOL doneSomething = NO;
359         
360         // when first clicked we'll determine how the workflow proceeds
361         if ([[[assistantPanes selectedTabViewItem] identifier] isEqual:@"start"]) {
362                 // we want to increase the number of steps for each option selected
363                 if ([importAccountsButton state] == NSOnState)
364                         currentStep++;
365                 if ([importStatusButton state] == NSOnState)
366                         currentStep++;
367                 if ([importLogsButton state] == NSOnState)
368                         currentStep++;
369         }
370         
371         if (currentStep == -1) {
372                 doneSomething = YES;
373                 currentStep++; // return to 0
374                 [self closeWindow:self];
375         }
377         if ([importAccountsButton state] == NSOnState && currentStep > 0 && !doneSomething) {
378                 doneSomething = YES;
379                 [backButton setEnabled:NO];
380                 [importAccountsProgress startAnimation:importAccountsProgress];
381                 [importAccountsDetails setStringValue:[AILocalizedString(@"Now importing all your accounts from iChat", nil) stringByAppendingEllipsis]];
382                 [titleField setStringValue:[AILocalizedString(@"Importing Accounts and Settings",nil) stringByAppendingEllipsis]];
383                 [assistantPanes selectTabViewItemWithIdentifier:@"accounts"];
384                 [importAccountsButton setState:NSOffState]; // reset so we don't do this again
385                 currentStep--;
386                 // do what's necessary to import here
387                 [self importAccountsForService:@"AIM"];
388                 [self importAccountsForService:@"Jabber"];
389                 [self importAccountsForService:@"SubNet"]; // SubNet is where iChat stores Bonjour accounts
390                 if (!blockForBonjour) {
391                         [importAccountsDetails setStringValue:AILocalizedString(@"Your accounts have been successfully imported.", nil)];
392                         [importAccountsProgress stopAnimation:importAccountsProgress];
393                         [backButton setEnabled:YES];
394                 }
395         }
397         if ([importStatusButton state] == NSOnState && currentStep > 0 && !doneSomething) {
398                 doneSomething = YES;
399                 [backButton setEnabled:NO];
400                 [importStatusProgress startAnimation:importStatusProgress];
401                 [importStatusDetails setStringValue:[AILocalizedString(@"Preparing to import your custom status messages", nil)  stringByAppendingEllipsis]];
402                 [titleField setStringValue:[AILocalizedString(@"Importing Statuses", nil) stringByAppendingEllipsis]];
403                 [assistantPanes selectTabViewItemWithIdentifier:@"statuses"];
404                 [importStatusButton setState:NSOffState]; // reset so we don't do this again
405                 currentStep--;
406                 [self performSelector:@selector(importStatuses) withObject:nil afterDelay:0.3];
407         }
408         
409         if ([importLogsButton state] == NSOnState && currentStep > 0  && !doneSomething) {
410                 doneSomething = YES;
411                 [proceedButton setEnabled:NO];
412                 [self populateAccountPicker];
413                 [titleField     setStringValue:[AILocalizedString(@"Importing iChat Transcripts", nil) stringByAppendingEllipsis]];
414                 [loggingPanes selectTabViewItemWithIdentifier:@"select"];
415                 [assistantPanes selectTabViewItemWithIdentifier:@"logs"];
416                 [importLogsButton setState:NSOffState]; // reset so we don't do this again
417         } else if (currentStep == 0  && !doneSomething) {
418                 doneSomething = YES;
419                 [backButton setEnabled:YES];
420                 [titleField     setStringValue:AILocalizedString(@"Import Finished", nil)];
421                 [assistantPanes selectTabViewItemWithIdentifier:@"end"];
422                 [proceedButton setTitle:AILocalizedString(@"Done", nil)];
423                 currentStep--;
424         }
427 -(IBAction)completeBonjourCreation:(id)sender
429         AIAccount *newAcct = [[adium accountController] createAccountWithService:bonjourService
430                                                                                                                                                  UID:[bonjourAccountNameField stringValue]];
431         if (newAcct) {                                                          
432                 [newAcct setPreference:[NSNumber numberWithBool:bonjourAutoLogin]
433                                                 forKey:@"Online"
434                                                  group:GROUP_ACCOUNT_STATUS];
435                 
436                 [[adium accountController] addAccount:newAcct];         
437         }
438                                 
439         [NSApp endSheet:bonjourNamePromptWindow];
440         [bonjourNamePromptWindow orderOut:bonjourNamePromptWindow];
441         [backButton setEnabled:YES];
442         [importAccountsDetails setStringValue:AILocalizedString(@"Your accounts have been successfully imported.", nil)];
443         [importAccountsProgress stopAnimation:importAccountsProgress];
444         blockForBonjour = NO;
447 -(IBAction)selectLogAccountDestination:(id)sender
449         destinationAccount = [accountsArray objectAtIndex:[sender indexOfSelectedItem]];
450         [self performSelector:@selector(startLogImport) withObject:nil afterDelay:0.7]; // immediate == scary :)
453 // we need only set the cancel flag appropriately and the recursive importLogs will handle on its next pass
454 -(IBAction)cancelLogImport:(id)sender
456         [importDetails setStringValue:[AILocalizedString(@"Cancelling transcript import", nil) stringByAppendingEllipsis]];
457         cancelImport = YES;
460 -(IBAction)deleteLogs:(id)sender
462         NSAlert *warningBeforehand = [NSAlert alertWithMessageText:AILocalizedString(@"Are you sure you want to delete all of your iChat Transcripts?", nil)
463                                                                                                  defaultButton:AILocalizedStringFromTable(@"Delete", @"Buttons", nil)
464                                                                                            alternateButton:AILocalizedStringFromTable(@"Cancel", @"Buttons", nil)
465                                                                                                    otherButton:nil 
466                                                                          informativeTextWithFormat:AILocalizedString(@"All of the iChat transcripts that were imported into Adium will be moved to the Trash.", nil)];
467         [warningBeforehand beginSheetModalForWindow:[self window] 
468                                                                   modalDelegate:self
469                                                                  didEndSelector:@selector(deleteAlertDidEnd:returnCode:contextInfo:) 
470                                                                         contextInfo:nil];
473 - (void)deleteAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
475         if (returnCode == NSAlertDefaultReturn) {
476                 [importDetails setStringValue:[AILocalizedString(@"Deleting iChat Transcripts", nil) stringByAppendingEllipsis]];
477                 [deleteLogsButton setHidden:YES];
478                 [proceedButton setEnabled:NO];
479                 [self performSelector:@selector(deleteAllFromiChat) withObject:nil afterDelay:0.3];
480         }
483 -(void)dealloc
485         [fullDump release];
486         [logImporter release];
488         [super dealloc];
491 @end