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 "BGICImportController.h"
18 #import "BGICLogImportController.h"
20 #import <AIUtilities/AIFileManagerAdditions.h>
22 #import "AIStatusController.h"
23 #import "AIStatusGroup.h"
24 #import "AIAccountController.h"
26 #import "ESAIMService.h"
27 #import "ESDotMacService.h"
28 #import "ESJabberService.h"
29 #import "AWBonjourService.h"
31 #define ICHAT_LOCATION [@"~/Documents/iChats/" stringByExpandingTildeInPath]
33 @interface BGICImportController (PRIVATE)
34 -(void)startLogImport;
35 -(void)populateAccountPicker;
36 -(void)deleteAllFromiChat;
37 -(void)importAccountsForService:(NSString *)serviceName;
39 -(void)importStatuses;
40 -(void)addStatusFromString:(NSString *)statusString isAway:(BOOL)shouldBeAway withGroup:(AIStatusGroup *)parentGroup;
43 @implementation BGICImportController (PRIVATE)
47 [backButton setEnabled:NO];
48 [importProgress startAnimation:importProgress];
49 [importDetails setStringValue:@"Gathering list of transcripts..."];
50 [loggingPanes selectTabViewItemWithIdentifier:@"import"];
51 [cancelImportButton setHidden:NO];
52 fullDump = [[[NSFileManager defaultManager] subpathsAtPath:ICHAT_LOCATION] retain];
53 dumpCount = [fullDump count];
57 [self performSelector:@selector(importLogs) withObject:nil afterDelay:0.15];
60 // loop the accounts in Adium and add them to the popup.
61 // This selection will be used for the log folder target (service.account in PATH_LOGS to be exact)
62 -(void)populateAccountPicker
64 [accountSelectionPopup removeAllItems];
66 if(accountsArray == nil)
67 accountsArray = [[NSMutableArray alloc] init];
69 [accountsArray removeAllObjects];
71 NSArray *accountsAvailable = [[adium accountController] accounts];
73 if([accountsAvailable count] > 0)
75 [accountSelectionPopup setHidden:NO];
77 for(int accountLoop = 0; accountLoop < [accountsAvailable count]; accountLoop++)
79 AIAccount *currentAccount = [accountsAvailable objectAtIndex:accountLoop];
80 [accountSelectionLabel setStringValue:@"Please select an account to import your transcripts into:"];
81 [accountSelectionPopup addItemWithTitle:[NSString stringWithFormat:@"%@ (%@)", [currentAccount formattedUID], [currentAccount serviceID]]];
82 [accountsArray addObject:[NSString stringWithFormat:@"%@.%@", [currentAccount serviceID], [currentAccount formattedUID]]];
87 // no accounts are present so we'll error this phase out
89 [accountSelectionLabel setStringValue:@"Importing transcripts requires at least 1 account to be present."];
90 [accountSelectionPopup setHidden:YES];
91 [backButton setEnabled:YES];
92 [proceedButton setEnabled:YES];
96 // loop through the iChat log paths and move them all to the Trash
97 -(void)deleteAllFromiChat
99 for(int deleteLoop = 0; deleteLoop < [fullDump count]; deleteLoop++)
101 NSString *logPath = [fullDump objectAtIndex:deleteLoop];
103 if([logPath rangeOfString:@"DS_Store"].length == 0)
105 [[NSFileManager defaultManager] trashFileAtPath:[[ICHAT_LOCATION stringByAppendingPathComponent:logPath] stringByExpandingTildeInPath]];
109 [importDetails setStringValue:@"Your iChat transcripts have been removed."];
110 [proceedButton setEnabled:YES];
113 -(void)importAccountsForService:(NSString *)serviceName
115 // com.apple.iChat.AIM.plist -> accounts on AIM
116 NSDictionary *rawPrefsFile = [NSDictionary dictionaryWithContentsOfFile:[[NSString stringWithFormat:@"~/Library/Preferences/com.apple.iChat.%@.plist", serviceName] stringByExpandingTildeInPath]];
117 NSArray *accountsFromRaw = [[rawPrefsFile valueForKey:@"Accounts"] allValues];
119 NSEnumerator *serviceEnum = [[[adium accountController] services] objectEnumerator];
120 AIService *service = nil;
122 // we'll grab these momentarily and use judiciously afterwards, Bonjour is external to this to method, unlike the others
123 ESAIMService *aimService = nil;
124 ESDotMacService *macService = nil;
125 ESJabberService *jabberService = nil;
127 while ((service = [serviceEnum nextObject]) != nil)
129 if([[service serviceID] isEqual:@"AIM"])
130 aimService = (ESAIMService *)service;
131 if([[service serviceID] isEqual:@"Mac"])
132 macService = (ESDotMacService *)service;
133 if([[service serviceID] isEqual:@"Jabber"])
134 jabberService = (ESJabberService *)service;
135 if([[service serviceID] isEqual:@"Bonjour"])
136 bonjourService = (AWBonjourService *)service;
139 for(int accountLoop = 0; accountLoop < [accountsFromRaw count]; accountLoop++)
141 if(![serviceName isEqual:@"SubNet"])
143 NSDictionary *currentAccount = [accountsFromRaw objectAtIndex:accountLoop];
145 NSString *accountName = [currentAccount objectForKey:@"LoginAs"];
147 AIAccount *newAcct = [[adium accountController] createAccountWithService:
148 ([serviceName isEqual:@"Jabber"] ? (AIService *)jabberService : ([accountName rangeOfString:@"mac.com"].length > 0 ? (AIService *)macService : (AIService *)aimService))
153 NSNumber *autoLogin = [currentAccount objectForKey:@"AutoLogin"];
154 [newAcct setPreference:autoLogin
156 group:GROUP_ACCOUNT_STATUS];
158 NSString *serverHost = [currentAccount objectForKey:@"ServerHost"];
159 if([serverHost length] > 0)
160 [newAcct setPreference:serverHost
161 forKey:KEY_CONNECT_HOST
162 group:GROUP_ACCOUNT_STATUS];
164 NSNumber *serverPort = [currentAccount objectForKey:@"ServerPort"];
166 [newAcct setPreference:serverPort
167 forKey:KEY_CONNECT_PORT
168 group:GROUP_ACCOUNT_STATUS];
170 [[adium accountController] addAccount:newAcct];
174 blockForBonjour = YES;
176 // iChat stores only the fact that the Default (username) account should be used and whether to auto-login
177 NSDictionary *currentAccount = [accountsFromRaw objectAtIndex:accountLoop];
178 bonjourAutoLogin = [[currentAccount objectForKey:@"AutoLogin"] boolValue];
180 // Adium, however, has a more flexible Bonjour account configuration and we have to take this into account.
181 [NSApp beginSheet:bonjourNamePromptWindow modalForWindow:[self window] modalDelegate:nil didEndSelector:nil contextInfo:nil];
191 [importProgress setIndeterminate:NO];
192 [importProgress setMaxValue:dumpCount];
193 [importProgress setMinValue:0];
194 [deleteLogsButton setHidden:YES];
195 [cancelImportButton setHidden:NO];
198 if(dumpLoop >= dumpCount)
200 [importProgress setIndeterminate:YES];
201 [importProgress stopAnimation:importProgress];
202 [importDetails setStringValue:@"Transcript importing complete."];
203 [importProgress setHidden:YES];
204 [proceedButton setEnabled:YES];
205 [deleteLogsButton setHidden:NO];
206 [backButton setEnabled:YES];
207 [cancelImportButton setHidden:YES];
211 NSString *logPath = [fullDump objectAtIndex:dumpLoop];
213 if(logImporter == nil)
214 logImporter = [[BGICLogImportController alloc] initWithDestination:destinationAccount];
216 [importProgress setDoubleValue:dumpLoop];
217 [importDetails setStringValue:[NSString stringWithFormat:@"Now importing transcript %i of %i - %@...", dumpLoop, dumpCount, [logPath stringByDeletingPathExtension]]];
219 if([logPath rangeOfString:@"DS_Store"].length == 0)
221 // pass the current log's path over and let the log conversion class do it's work
222 [logImporter createNewLogForPath:[[ICHAT_LOCATION stringByAppendingPathComponent:logPath] stringByExpandingTildeInPath]];
226 if(dumpLoop < dumpCount && cancelImport == NO) {
227 [self performSelector:@selector(importLogs) withObject:nil afterDelay:0.10];
232 [importDetails setStringValue:[NSString stringWithFormat:@"Transcript importing cancelled. %i of %i transcripts already imported.", dumpLoop, dumpCount]];
233 [importProgress setIndeterminate:YES];
234 [importProgress stopAnimation:importProgress];
235 [importProgress setHidden:YES];
236 [cancelImportButton setHidden:YES];
237 [backButton setEnabled:YES];
238 [proceedButton setEnabled:YES];
244 -(void)importStatuses
246 // iChat (on 10.4 at least) stores custom statuses in a couple of arrays in it's plist
247 NSDictionary *ichatPrefs = [NSDictionary dictionaryWithContentsOfFile:[@"~/Library/Preferences/com.apple.iChat.plist" stringByExpandingTildeInPath]];
249 // loop through the availables and add them
250 NSArray *customAvailable = [ichatPrefs objectForKey:@"CustomAvailableMessages"];
252 [importStatusDetails setStringValue:[NSString stringWithFormat:@"Now importing %i Available messages...", [customAvailable count]]];
254 AIStatusGroup *availableGroup = nil;
256 // optionally create a status group for collecting them together
257 if([createStatusGroupsButton state] == NSOnState)
259 availableGroup = [AIStatusGroup statusGroup];
260 [availableGroup setTitle:@"iChat Available Messages"];
261 [availableGroup setStatusType:AIAvailableStatusType];
263 [[[adium statusController] rootStateGroup] addStatusItem:availableGroup atIndex:-1];
266 for(int availableLoop = 0; availableLoop < [customAvailable count]; availableLoop++)
268 [self addStatusFromString:[customAvailable objectAtIndex:availableLoop] isAway:NO withGroup:availableGroup];
271 AIStatusGroup *awayGroup = nil;
273 // optionally create a status group for collecting them together
274 if([createStatusGroupsButton state] == NSOnState)
276 awayGroup = [AIStatusGroup statusGroup];
277 [awayGroup setTitle:@"iChat Away Messages"];
278 [awayGroup setStatusType:AIAwayStatusType];
280 [[[adium statusController] rootStateGroup] addStatusItem:awayGroup atIndex:-1];
283 // loop through the aways and add them
284 NSArray *customAways = [ichatPrefs objectForKey:@"CustomAwayMessages"];
286 [importStatusDetails setStringValue:[NSString stringWithFormat:@"Now importing %i Away messages...", [customAways count]]];
288 for(int awayLoop = 0; awayLoop < [customAways count]; awayLoop++)
290 [self addStatusFromString:[customAways objectAtIndex:awayLoop] isAway:YES withGroup:awayGroup];
293 [importStatusDetails setStringValue:@"Status importing is now complete."];
294 [importStatusProgress stopAnimation:importStatusProgress];
295 [backButton setEnabled:YES];
298 // the only difference between imported statuses is their type and reply behavior (optionally can be added to a group)
299 -(void)addStatusFromString:(NSString *)statusString isAway:(BOOL)shouldBeAway withGroup:(AIStatusGroup *)parentGroup
301 AIStatus *newStatus = [AIStatus statusOfType:(shouldBeAway ? AIAwayStatusType : AIAvailableStatusType)];
302 [newStatus setTitle:statusString];
303 [newStatus setStatusMessage:[[[NSAttributedString alloc] initWithString:statusString] autorelease]];
304 [newStatus setAutoReplyIsStatusMessage:(shouldBeAway ? YES : NO)];
305 [newStatus setShouldForceInitialIdleTime:NO];
307 // optionally add to a status group
308 if(parentGroup == nil) {
309 [[adium statusController] addStatusState:newStatus];
312 [parentGroup addStatusItem:newStatus atIndex:-1];
318 @implementation BGICImportController
320 -(void)awakeFromNib {
323 //Configure our background view; it should display the image transparently where our tabView overlaps it
324 [backgroundView setBackgroundImage:[NSImage imageNamed:@"AdiumyButler"]];
325 NSRect tabViewFrame = [assistantPanes frame];
326 NSRect backgroundViewFrame = [backgroundView frame];
327 tabViewFrame.origin.x -= backgroundViewFrame.origin.x;
328 tabViewFrame.origin.y -= backgroundViewFrame.origin.y;
329 [backgroundView setTransparentRect:tabViewFrame];
331 [[self window] center];
333 [assistantPanes selectTabViewItemWithIdentifier:@"start"];
334 [backButton setEnabled:NO];
337 -(IBAction)openHelp:(id)sender
339 #warning This help anchor is necessary and needs a corresponding page in the book + the index needs regenerated.
340 NSString *locBookName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleHelpBookName"];
341 [[NSHelpManager sharedHelpManager] openHelpAnchor:@"ichatImport" inBook:locBookName];
344 // this action is currently defined as returning to the start of the assistant, unchecking all and noting completed actions
345 -(IBAction)goBack:(id)sender
349 [backButton setEnabled:NO];
350 [proceedButton setEnabled:YES];
351 [proceedButton setTitle:@"Continue"]; // in case we are on the last step
353 [importAccountsButton setState:NSOffState];
354 [importStatusButton setState:NSOffState];
355 [importLogsButton setState:NSOffState];
356 [createStatusGroupsButton setState:NSOffState];
358 [assistantPanes selectTabViewItemWithIdentifier:@"start"];
361 -(IBAction)proceed:(id)sender
363 BOOL doneSomething = NO;
365 // when first clicked we'll determine how the workflow proceeds
366 if([[[assistantPanes selectedTabViewItem] identifier] isEqual:@"start"])
368 // we want to increase the number of steps for each option selected
369 if([importAccountsButton state] == NSOnState)
371 if([importStatusButton state] == NSOnState)
373 if([importLogsButton state] == NSOnState)
377 if(currentStep == -1)
380 currentStep++; // return to 0
381 [self closeWindow:self];
384 if([importAccountsButton state] == NSOnState && currentStep > 0 && !doneSomething) {
386 [backButton setEnabled:NO];
387 [importAccountsProgress startAnimation:importAccountsProgress];
388 [importAccountsDetails setStringValue:@"Now importing all your accounts from iChat..."];
389 [titleField setStringValue:@"Importing Accounts and Settings..."];
390 [assistantPanes selectTabViewItemWithIdentifier:@"accounts"];
391 [importAccountsButton setState:NSOffState]; // reset so we don't do this again
393 // do what's necessary to import here
394 [self importAccountsForService:@"AIM"];
395 [self importAccountsForService:@"Jabber"];
396 [self importAccountsForService:@"SubNet"]; // SubNet is where iChat stores Bonjour accounts
397 if(!blockForBonjour) {
398 [importAccountsDetails setStringValue:@"Your accounts have been successfully imported."];
399 [importAccountsProgress stopAnimation:importAccountsProgress];
400 [backButton setEnabled:YES];
403 if([importStatusButton state] == NSOnState && currentStep > 0 && !doneSomething) {
405 [backButton setEnabled:NO];
406 [importStatusProgress startAnimation:importStatusProgress];
407 [importStatusDetails setStringValue:@"Preparing to import your custom Available and Away messages..."];
408 [titleField setStringValue:@"Importing Statuses..."];
409 [assistantPanes selectTabViewItemWithIdentifier:@"statuses"];
410 [importStatusButton setState:NSOffState]; // reset so we don't do this again
412 [self performSelector:@selector(importStatuses) withObject:nil afterDelay:0.3];
414 if([importLogsButton state] == NSOnState && currentStep > 0 && !doneSomething) {
416 [proceedButton setEnabled:NO];
417 [self populateAccountPicker];
418 [titleField setStringValue:@"Importing iChat Transcripts..."];
419 [loggingPanes selectTabViewItemWithIdentifier:@"select"];
420 [assistantPanes selectTabViewItemWithIdentifier:@"logs"];
421 [importLogsButton setState:NSOffState]; // reset so we don't do this again
423 else if(currentStep == 0 && !doneSomething) {
425 [backButton setEnabled:YES];
426 [titleField setStringValue:@"Import Finished"];
427 [assistantPanes selectTabViewItemWithIdentifier:@"end"];
428 [proceedButton setTitle:@"Done"];
433 -(IBAction)completeBonjourCreation:(id)sender
435 AIAccount *newAcct = [[adium accountController] createAccountWithService:bonjourService
436 UID:[bonjourAccountNameField stringValue]];
439 [newAcct setPreference:[NSNumber numberWithBool:bonjourAutoLogin]
441 group:GROUP_ACCOUNT_STATUS];
443 [[adium accountController] addAccount:newAcct];
446 [NSApp endSheet:bonjourNamePromptWindow];
447 [bonjourNamePromptWindow orderOut:bonjourNamePromptWindow];
448 [backButton setEnabled:YES];
449 [importAccountsDetails setStringValue:@"Your accounts have been successfully imported."];
450 [importAccountsProgress stopAnimation:importAccountsProgress];
451 blockForBonjour = NO;
454 -(IBAction)selectLogAccountDestination:(id)sender
456 destinationAccount = [accountsArray objectAtIndex:[sender indexOfSelectedItem]];
457 [self performSelector:@selector(startLogImport) withObject:nil afterDelay:0.7]; // immediate == scary :)
460 // we need only set the cancel flag appropriately and the recursive importLogs will handle on its next pass
461 -(IBAction)cancelLogImport:(id)sender
463 [importDetails setStringValue:@"Cancelling transcript import... this may take a moment."];
467 -(IBAction)deleteLogs:(id)sender
469 NSAlert *warningBeforehand = [NSAlert alertWithMessageText:@"Are you sure you want to delete all of your iChat Transcripts?"
470 defaultButton:@"Delete"
471 alternateButton:@"Cancel"
473 informativeTextWithFormat:@"All of the iChat transcripts that were imported into Adium will be moved to the Trash."];
474 [warningBeforehand beginSheetModalForWindow:[self window]
476 didEndSelector:@selector(deleteAlertDidEnd:returnCode:contextInfo:)
480 - (void)deleteAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
482 if(returnCode == NSAlertDefaultReturn)
484 [importDetails setStringValue:@"Deleting iChat Transcripts, this may take a few moments."];
485 [deleteLogsButton setHidden:YES];
486 [proceedButton setEnabled:NO];
487 [self performSelector:@selector(deleteAllFromiChat) withObject:nil afterDelay:0.3];
496 [logImporter release];