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 "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;
40 -(void)importStatuses;
41 -(void)addStatusFromString:(NSString *)statusString isAway:(BOOL)shouldBeAway withGroup:(AIStatusGroup *)parentGroup;
44 @implementation BGICImportController (PRIVATE)
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];
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];
67 if (accountsArray == nil)
68 accountsArray = [[NSMutableArray alloc] init];
70 [accountsArray removeAllObjects];
72 NSArray *accountsAvailable = [[adium accountController] accounts];
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]]];
84 // no accounts are present so we'll error this phase out
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];
93 // loop through the iChat log paths and move them all to the Trash
94 -(void)deleteAllFromiChat
96 for (int deleteLoop = 0; deleteLoop < [fullDump count]; deleteLoop++) {
97 NSString *logPath = [fullDump objectAtIndex:deleteLoop];
99 if ([logPath rangeOfString:@"DS_Store"].length == 0)
100 [[NSFileManager defaultManager] trashFileAtPath:[[ICHAT_LOCATION stringByAppendingPathComponent:logPath] stringByExpandingTildeInPath]];
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];
113 NSEnumerator *serviceEnum = [[[adium accountController] services] objectEnumerator];
114 AIService *service = nil;
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;
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;
132 for (int accountLoop = 0; accountLoop < [accountsFromRaw count]; accountLoop++) {
133 if (![serviceName isEqual:@"SubNet"]) {
134 NSDictionary *currentAccount = [accountsFromRaw objectAtIndex:accountLoop];
136 NSString *accountName = [currentAccount objectForKey:@"LoginAs"];
138 AIAccount *newAcct = [[adium accountController] createAccountWithService:
139 ([serviceName isEqual:@"Jabber"] ? (AIService *)jabberService : ([accountName rangeOfString:@"mac.com"].length > 0 ? (AIService *)macService : (AIService *)aimService))
144 NSNumber *autoLogin = [currentAccount objectForKey:@"AutoLogin"];
145 [newAcct setPreference:autoLogin
147 group:GROUP_ACCOUNT_STATUS];
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];
155 NSNumber *serverPort = [currentAccount objectForKey:@"ServerPort"];
157 [newAcct setPreference:serverPort
158 forKey:KEY_CONNECT_PORT
159 group:GROUP_ACCOUNT_STATUS];
161 [[adium accountController] addAccount:newAcct];
164 blockForBonjour = YES;
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];
180 [importProgress setIndeterminate:NO];
181 [importProgress setMaxValue:dumpCount];
182 [importProgress setMinValue:0];
183 [deleteLogsButton setHidden:YES];
184 [cancelImportButton setHidden:NO];
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];
198 NSString *logPath = [fullDump objectAtIndex:dumpLoop];
200 if (!logImporter) logImporter = [[BGICLogImportController alloc] initWithDestination:destinationAccount];
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]]];
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]];
212 if (dumpLoop < dumpCount && cancelImport == NO) {
213 [self performSelector:@selector(importLogs) withObject:nil afterDelay:0.10];
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];
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]];
235 // loop through the availables and add them
236 NSArray *customAvailable = [ichatPrefs objectForKey:@"CustomAvailableMessages"];
238 [importStatusDetails setStringValue:[NSString stringWithFormat:[AILocalizedString(@"Now importing %i 'Available' messages", nil) stringByAppendingEllipsis],
239 [customAvailable count]]];
241 AIStatusGroup *availableGroup = nil;
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];
249 [[[adium statusController] rootStateGroup] addStatusItem:availableGroup atIndex:-1];
252 for (int availableLoop = 0; availableLoop < [customAvailable count]; availableLoop++) {
253 [self addStatusFromString:[customAvailable objectAtIndex:availableLoop] isAway:NO withGroup:availableGroup];
256 AIStatusGroup *awayGroup = nil;
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];
264 [[[adium statusController] rootStateGroup] addStatusItem:awayGroup atIndex:-1];
267 // loop through the aways and add them
268 NSArray *customAways = [ichatPrefs objectForKey:@"CustomAwayMessages"];
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];
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];
291 // optionally add to a status group
292 if (parentGroup == nil) {
293 [[adium statusController] addStatusState:newStatus];
295 [parentGroup addStatusItem:newStatus atIndex:-1];
301 @implementation BGICImportController
303 + (void)importIChatConfiguration
306 BGICImportController *ichatCon = [[BGICImportController alloc] initWithWindowNibName:@"ICImport"];
307 [ichatCon showWindow:ichatCon];
310 -(void)awakeFromNib {
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];
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
344 [backButton setEnabled:NO];
345 [proceedButton setEnabled:YES];
346 [proceedButton setTitle:AILocalizedStringFromTable(@"Continue", @"Buttons", nil)]; // in case we are on the last step
348 [importAccountsButton setState:NSOffState];
349 [importStatusButton setState:NSOffState];
350 [importLogsButton setState:NSOffState];
351 [createStatusGroupsButton setState:NSOffState];
353 [assistantPanes selectTabViewItemWithIdentifier:@"start"];
356 -(IBAction)proceed:(id)sender
358 BOOL doneSomething = NO;
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)
365 if ([importStatusButton state] == NSOnState)
367 if ([importLogsButton state] == NSOnState)
371 if (currentStep == -1) {
373 currentStep++; // return to 0
374 [self closeWindow:self];
377 if ([importAccountsButton state] == NSOnState && currentStep > 0 && !doneSomething) {
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
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];
397 if ([importStatusButton state] == NSOnState && currentStep > 0 && !doneSomething) {
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
406 [self performSelector:@selector(importStatuses) withObject:nil afterDelay:0.3];
409 if ([importLogsButton state] == NSOnState && currentStep > 0 && !doneSomething) {
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) {
419 [backButton setEnabled:YES];
420 [titleField setStringValue:AILocalizedString(@"Import Finished", nil)];
421 [assistantPanes selectTabViewItemWithIdentifier:@"end"];
422 [proceedButton setTitle:AILocalizedString(@"Done", nil)];
427 -(IBAction)completeBonjourCreation:(id)sender
429 AIAccount *newAcct = [[adium accountController] createAccountWithService:bonjourService
430 UID:[bonjourAccountNameField stringValue]];
432 [newAcct setPreference:[NSNumber numberWithBool:bonjourAutoLogin]
434 group:GROUP_ACCOUNT_STATUS];
436 [[adium accountController] addAccount:newAcct];
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]];
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)
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]
469 didEndSelector:@selector(deleteAlertDidEnd:returnCode:contextInfo:)
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];
486 [logImporter release];