Removed references to AIDockingPanel which I accidentally committed. Fixes #9102
[adiumx.git] / Source / GBFireLogImporter.m
blob109dc6047cbc383cb125b89c4be80ef1c0fb576b
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 "GBFireLogImporter.h"
18 #import <Adium/AIAccount.h>
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AILoginControllerProtocol.h>
22 #import <Adium/ESTextAndButtonsWindowController.h>
23 #import <AIUtilities/AIFileManagerAdditions.h>
24 #import <AIUtilities/NSCalendarDate+ISO8601Unparsing.h>
26 #define PATH_LOGS                       @"/Logs"
27 #define XML_MARKER @"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
29 @interface GBFireLogImporter (private)
30 - (void)askBeforeImport;
31 - (void)importFireLogs;
32 @end
34 @implementation GBFireLogImporter
36 + (void)importLogs
38         GBFireLogImporter *importer = [[GBFireLogImporter alloc] init];
39         [importer askBeforeImport];
40         [importer release];
43 - (id)init
45         self = [super init];
46         if(self == nil)
47                 return nil;
48         
49         [NSBundle loadNibNamed:@"FireLogImporter" owner:self];
50         
51         return self;
54 - (void)askBeforeImport
56         [[adium interfaceController] displayQuestion:AILocalizedString(@"Import Fire Logs?",nil)
57                                                                  withDescription:AILocalizedString(@"For some older log formats, the importer cannot properly determine which account was used for conversation.  In such cases, the importer will guess which account to use based upon the order of the accounts.  Before importing Fire's logs, arrange your account order within the Preferences.",nil)
58                                                                  withWindowTitle:nil
59                                                                    defaultButton:AILocalizedString(@"Import", nil)
60                                                                  alternateButton:AILocalizedString(@"Cancel", nil)
61                                                                          otherButton:nil
62                                                                                   target:self
63                                                                                 selector:@selector(importQuestionResponse:userInfo:)
64                                                                                 userInfo:nil];
67 - (void)importQuestionResponse:(NSNumber *)response userInfo:(id)info
69         if([response intValue] == AITextAndButtonsDefaultReturn)
70                 [NSThread detachNewThreadSelector:@selector(importFireLogs) toTarget:self withObject:nil];
73 NSString *quotes[] = {
74         @"\"I have gotten into the habit of recording important meetings\"",
75         @"\"One never knows when an inconvenient truth will fall between the cracks and vanish\"",
76         @"- Londo Mollari"
79 - (void)importFireLogs
81         NSAutoreleasePool *outerPool = [[NSAutoreleasePool alloc] init];
82         [window orderFront:self];
83         [progressIndicator startAnimation:nil];
84         [textField_quote setStringValue:quotes[0]];
85         NSFileManager *fm = [NSFileManager defaultManager];
86         NSString *inputLogDir = [[[fm userApplicationSupportFolder] stringByAppendingPathComponent:@"Fire"] stringByAppendingPathComponent:@"Sessions"];
87         BOOL isDir = NO;
88         
89         if(![fm fileExistsAtPath:inputLogDir isDirectory:&isDir] || !isDir)
90                 //Nothing to read
91                 return;
92         
93         NSArray *subPaths = [fm subpathsAtPath:inputLogDir];
94         NSString *outputBasePath = [[[[adium loginController] userDirectory] stringByAppendingPathComponent:PATH_LOGS] stringByExpandingTildeInPath];
95         
96         NSArray *accounts = [[adium accountController] accounts];
97         int current;
98         NSMutableDictionary *defaultScreenname = [NSMutableDictionary dictionary];
99         for(current = [accounts count] - 1; current >= 0; current--)
100         {
101                 AIAccount *acct = [accounts objectAtIndex:current];
102                 [defaultScreenname setObject:[acct UID] forKey:[acct serviceID]];
103         }
104         
105         [progressIndicator setDoubleValue:0.0];
106         [progressIndicator setIndeterminate:NO];
107         int total = [subPaths count], currentQuote = 0;
108         for(current = 0; current < total; current++)
109         {
110                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  //A lot of temporary memory is used here
111                 [progressIndicator setDoubleValue:(double)current/(double)total];
112                 int nextQuote = current*sizeof(quotes)/sizeof(NSString *)/total;
113                 if(nextQuote != currentQuote)
114                 {
115                         currentQuote = nextQuote;
116                         [textField_quote setStringValue:quotes[currentQuote]];
117                 }
118                 NSString *logPath = [subPaths objectAtIndex:current];
119                 NSString *fullInputPath = [inputLogDir stringByAppendingPathComponent:logPath];
120                 if(![fm fileExistsAtPath:fullInputPath isDirectory:&isDir] || isDir)
121                 {
122                         //ignore directories
123                         [pool release];
124                         continue;
125                 }
126                 NSString *extension = [logPath pathExtension];
127                 NSArray *pathComponents = [logPath pathComponents];
128                 if([pathComponents count] != 2)
129                 {
130                         //Incorrect directory structure, likely a .DS_Store or something like that
131                         [pool release];
132                         continue;
133                 }
134                 
135                 NSString *userAndService = [pathComponents objectAtIndex:[pathComponents count] - 2];
136                 NSRange range = [userAndService rangeOfString:@"-" options:NSBackwardsSearch];
137                 if (range.location == NSNotFound) {
138                         NSLog(@"Warning: [%@ importFireLogs] could not find '-'.", self);
139                         //Incorrect directory structure
140                         [pool release];
141                         continue;                       
142                 }
143                 NSString *user = [userAndService substringToIndex:range.location];
144                 NSString *service = [userAndService substringFromIndex:range.location + 1];
145                 NSDate *date = [NSDate dateWithNaturalLanguageString:[[pathComponents lastObject] stringByDeletingPathExtension]];
146                                 
147                 if([extension isEqualToString:@"session"])
148                 {
149                         NSString *account = [defaultScreenname objectForKey:service];
150                         if(account == nil)
151                                 account = @"Fire";
152                         NSString *outputFileDir = [[outputBasePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", service, account]] stringByAppendingPathComponent:user];
153                         NSString *outputFile = [outputFileDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@).adiumLog", user, [date descriptionWithCalendarFormat:@"%Y-%m-%dT%H.%M.%S%z" timeZone:nil locale:nil]]];
154                         [fm createDirectoriesForPath:outputFileDir];
155                         [fm copyPath:fullInputPath toPath:outputFile handler:self];
156                 }
157                 else if([extension isEqualToString:@"session2"])
158                 {
159                         NSString *account = [defaultScreenname objectForKey:service];
160                         if(account == nil)
161                                 account = @"Fire";
162                         NSString *outputFileDir = [[outputBasePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", service, account]] stringByAppendingPathComponent:user];
163                         NSString *outputFile = [outputFileDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@).AdiumHTMLLog", user, [date descriptionWithCalendarFormat:@"%Y-%m-%dT%H.%M.%S%z" timeZone:nil locale:nil]]];
164                         [fm createDirectoriesForPath:outputFileDir];
165                         [fm copyPath:fullInputPath toPath:outputFile handler:self];
166                 }
167                 else if([extension isEqualToString:@"xhtml"])
168                 {
169                         NSString *outputFile = [outputBasePath stringByAppendingPathComponent:@"tempLogImport"];
170                         [fm createDirectoriesForPath:outputBasePath];
171                         GBFireXMLLogImporter *xmlLog = [[GBFireXMLLogImporter alloc] init];
172                         NSString *account = nil;
173                         if([xmlLog readFile:fullInputPath toFile:outputFile account:&account])
174                         {
175                                 if(account == nil)
176                                         account = [defaultScreenname objectForKey:service];
177                                 if(account == nil)
178                                         account = @"Fire";
180                                 NSString *realOutputFileDir = [[outputBasePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", service, account]] stringByAppendingPathComponent:user];
181                                 NSString *realOutputFile = [realOutputFileDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@).chatlog", user, [date descriptionWithCalendarFormat:@"%Y-%m-%dT%H.%M.%S%z" timeZone:nil locale:nil]]];
182                                 [fm createDirectoriesForPath:realOutputFileDir];
183                                 [fm movePath:outputFile toPath:realOutputFile handler:self];
184                         }
185                         [xmlLog release];
186                 }
187                 [pool release];
188         }
189         [window close];
190         [outerPool release];
193 @end
195 static void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context);
196 static void addChild(CFXMLParserRef parser, void *parent, void *child, void *context);
197 static void endStructure(CFXMLParserRef parser, void *xmlType, void *context);
198 static Boolean errorStructure (CFXMLParserRef parser, CFXMLParserStatusCode error, void *info);
200 @implementation GBFireXMLLogImporter
202 - (id)init
204         self = [super init];
205         if(self == nil)
206                 return nil;
207         
208         state = XML_STATE_NONE;
209         
210         inputFileString = nil;
211         outputFileHandle = nil;
212         sender = nil;
213         mySN = nil;
214         date = nil;
215         parser = NULL;
216         encryption = nil;
217         
218         eventTranslate = [[NSDictionary alloc] initWithObjectsAndKeys:
219                 @"userJoined", @"memberJoined",
220                 @"userParted", @"memberParted",
221                 @"userFormattedIdChanged", @"newNickname",
222                 @"channelTopicChanged", @"topicChanged",
223                 @"userPermissions/Promoted", @"memberPromoted",
224                 @"userPermissions/Demoted", @"memberDemoted",
225                 @"userPermissions/Voiced", @"memberVoiced",
226                 @"userPermissions/Devoiced", @"memberDevoiced",
227                 @"userKicked", @"memberKicked",
228                 nil];
229                 
230         return self;
233 - (BOOL)readFile:(NSString *)inFile toFile:(NSString *)outFile account:(NSString * *)account;
235         AILog(@"%@: readFile:%@ toFile:%@",NSStringFromClass([self class]), inFile, outFile);
237         BOOL success = YES;
238         NSData *inputData = [NSData dataWithContentsOfFile:inFile];
239         inputFileString = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding];
240         int outfd = open([outFile fileSystemRepresentation], O_CREAT | O_WRONLY, 0644);
241         outputFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:outfd closeOnDealloc:YES];
242         
243         CFXMLParserCallBacks callbacks = {
244                 0,
245                 createStructure,
246                 addChild,
247                 endStructure,
248                 NULL,
249                 errorStructure
250         };
251         CFXMLParserContext context = {
252                 0,
253                 self,
254                 CFRetain,
255                 CFRelease,
256                 NULL
257         };
258         NSMutableString *newStr = [NSMutableString stringWithContentsOfFile:inFile];
259         int endOffset = [newStr rangeOfString:@">" options:NSBackwardsSearch].location;
260         int startOffset = [newStr rangeOfString:@"<" options:NSBackwardsSearch].location;
261         if((endOffset == NSNotFound || endOffset < startOffset) && startOffset != NSNotFound)
262         {
263                 //Broken XML can crash the importer, attempt a repair, but most likely the parse will fail
264                 [newStr appendString:@">"];
265                 AILog(@"Fire log import: %@ has broken XML, you should fix this and re-import it", inFile);
266                 NSLog(@"Fire log import: %@ has broken XML, you should fix this and re-import it", inFile);
267 #warning Surely we shouldn't log both, but this should be rare.  Maybe put up a dialog?, thoughts?  Same for below too
268         }
269         NSData *data = [newStr dataUsingEncoding:NSUTF8StringEncoding];
270         parser = CFXMLParserCreate(NULL, (CFDataRef)data, NULL,kCFXMLParserSkipMetaData | kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion, &callbacks, &context);
271         if (!CFXMLParserParse(parser)) {
272                 NSLog(@"Fire log import: Parse of %@ failed", inFile);
273                 AILog(@"Fire log import: Parse of %@ failed", inFile);
274                 success = NO;
275         }
276         CFRelease(parser);
277         parser = nil;
278         [outputFileHandle closeFile];
279         
280         *account = [[mySN retain] autorelease];
281         return success;
284 - (void)dealloc
286         [inputFileString release];
287         [outputFileHandle release];
288         [eventTranslate release];
289         [sender release];
290         [htmlMessage release];
291         [mySN release];
292         [date release];
293         [encryption release];
294         [super dealloc];
297 - (void)startedElement:(NSString *)name info:(const CFXMLElementInfo *)info
299         NSDictionary *attributes = (NSDictionary *)info->attributes;
300         
301         switch(state){
302                 case XML_STATE_NONE:
303                         if([name isEqualToString:@"envelope"])
304                         {
305                                 [sender release];
306                                 sender = nil;
307                                 state = XML_STATE_ENVELOPE;
308                         }
309                         else if([name isEqualToString:@"event"])
310                         {
311                                 [date release];
312                                 
313                                 NSString *dateStr = [attributes objectForKey:@"occurred"];
314                                 if(dateStr != nil)
315                                         date = [[NSCalendarDate dateWithString:dateStr] retain];
316                                 else
317                                         date = nil;
318                                 
319                                 [eventName release];
320                                 NSString *eventStr = [attributes objectForKey:@"name"];
321                                 if(eventStr != nil)
322                                         eventName = [[NSString alloc] initWithString:eventStr];
323                                 else
324                                         eventName = nil;
325                                 [sender release];
326                                 sender = nil;
327                                 [htmlMessage release];
328                                 htmlMessage = nil;
329                                 state = XML_STATE_EVENT;
330                         }
331                         else if([name isEqualToString:@"log"])
332                         {
333                                 NSString *service = [attributes objectForKey:@"service"];
334                                 NSString *account = [attributes objectForKey:@"accountName"];
335                                 if(account != nil)
336                                 {
337                                         NSRange range = [account rangeOfString:@"-"];
338                                         if(range.location != NSNotFound)
339                                         {
340                                                 mySN = [[account substringFromIndex:range.location + 1] retain];
341                                                 range = [mySN rangeOfString:@"@"];
342                                                 NSRange revRange = [mySN rangeOfString:@"@" options:NSBackwardsSearch];
343                                                 if ((revRange.location != range.location) && (revRange.location != NSNotFound))
344                                                 {
345                                                         NSString *oldMySN = mySN;
346                                                         mySN = [[mySN substringToIndex:revRange.location] retain];
347                                                         [oldMySN release];
348                                                 }
349                                         }
350                                 }
351                                 NSMutableString *chatTag = [NSMutableString stringWithFormat:@"%@\n<chat", XML_MARKER];
352                                 [chatTag appendString:@" xmlns=\"http://purl.org/net/ulf/ns/0.4-02\""];
353                                 if(mySN != nil)
354                                         [chatTag appendFormat:@" account=\"%@\"", mySN];
355                                 if(service != nil)
356                                         [chatTag appendFormat:@" service=\"%@\"", service];
357                                 [chatTag appendString:@">\n"];
358                                 [outputFileHandle writeData:[chatTag dataUsingEncoding:NSUTF8StringEncoding]];
359                         }
360                         break;
361                 case XML_STATE_ENVELOPE:
362                         if ([name isEqualToString:@"message"])
363                         {
364                                 [date release];
365                                 
366                                 NSString *dateStr = [attributes objectForKey:@"received"];
367                                 if(dateStr != nil)
368                                         date = [[NSCalendarDate dateWithString:dateStr] retain];
369                                 else
370                                         date = nil;
371                                 
372                                 //Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
373                                 messageStart = CFXMLParserGetLocation(parser) - 1;
374                                 
375                                 if([attributes objectForKey:@"action"] != nil)
376                                         actionMessage = YES;
377                                 else
378                                         actionMessage = NO;
379                                 
380                                 if([attributes objectForKey:@"away"] != nil)
381                                         autoResponse = YES;
382                                 else
383                                         autoResponse = NO;
384                                 [encryption release];
385                                 encryption = [[attributes objectForKey:@"security"] retain];
386                                 
387                                 state = XML_STATE_MESSAGE;
388                         }
389                         else if([name isEqualToString:@"sender"])
390                         {
391                                 [sender release];
392                                 
393                                 NSString *nickname = [attributes objectForKey:@"nickname"];
394                                 NSString *selfSender = [attributes objectForKey:@"self"];
395                                 if(nickname != nil)
396                                         sender = [nickname retain];
397                                 else if ([selfSender isEqualToString:@"yes"])
398                                         sender = [mySN retain];
399                                 else
400                                         sender = nil;
401                                 state = XML_STATE_SENDER;
402                         }
403                         break;
404                 case XML_STATE_SENDER:
405                 case XML_STATE_MESSAGE:
406                 case XML_STATE_EVENT:
407                         if([name isEqualToString:@"Message"])
408                         {
409                                 state = XML_STATE_EVENT_ATTRIBUTED_MESSAGE;
410                         }
411                         if([name isEqualToString:@"message"])
412                         {
413                                 //Mark the location of the message...  same as above
414                                 messageStart = CFXMLParserGetLocation(parser) - 1;
415                                 state = XML_STATE_EVENT_MESSAGE;
416                         }
417                         if([name isEqualToString:@"nickname"])
418                         {
419                                 state = XML_STATE_EVENT_NICKNAME;
420                         }
421                 case XML_STATE_EVENT_ATTRIBUTED_MESSAGE:
422                 case XML_STATE_EVENT_MESSAGE:
423                 case XML_STATE_EVENT_NICKNAME:
424                         break;
425         }
428 - (void)endedElement:(NSString *)name empty:(BOOL)empty
430         switch(state)
431         {
432                 case XML_STATE_ENVELOPE:
433                         if([name isEqualToString:@"envelope"])
434                                 state = XML_STATE_NONE;
435                         break;
436                 case XML_STATE_SENDER:
437                         if([name isEqualToString:@"sender"])
438                                 state = XML_STATE_ENVELOPE;
439                         break;
440                 case XML_STATE_MESSAGE:
441                         if([name isEqualToString:@"message"])
442                         {
443                                 CFIndex end = CFXMLParserGetLocation(parser);
444                                 NSString *message = nil;
445                                 if(!empty)
446                                         message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)];  // 10 for </message> and 1 for the index being off 
448                                 //Common logging format
449                                 NSMutableString *outMessage = [NSMutableString stringWithString:@"<message"];
450                                 if(actionMessage)
451                                 {
452                                         [outMessage appendString:@" type=\"action\""];
453                                         
454                                         //Hmm...  there is a bug in Fire's logging format that logs action messages like <span>username </span>message
455                                         int cutIndex = 0;
456                                         int index = [message rangeOfString:@"<span>"].location;
457                                         if(index == 0)
458                                         {
459                                                 int endIndex = [message rangeOfString:@"</span>"].location;
460                                                 if(sender == nil)
461                                                         sender = [[message substringWithRange:NSMakeRange(6, endIndex-7)] retain];  //6 is length of <span>.  7 is length of <span> plus trailing space
462                                                 index = cutIndex = endIndex + 7;  //7 is length of </span>
463                                         }
464                                         else
465                                                 index = 0;
466                                         while([message characterAtIndex:index] == '<')
467                                         {
468                                                 NSRange searchRange = NSMakeRange(index, [message length] - index);
469                                                 NSRange range = [message rangeOfString:@">" options:0 range:searchRange];
470                                                 index = range.location + 1;
471                                         }
472                                         NSString *newMessage = nil;
473                                         if(message)
474                                                 newMessage = [[NSString alloc] initWithFormat:@"%@/me %@", [message substringWithRange:NSMakeRange(cutIndex, index-cutIndex)], [message substringFromIndex:index]];
475                                         else
476                                                 newMessage = [[NSString alloc] initWithString:@"/me "];
477                                         message = [newMessage autorelease];
478                                 }
479                                 if(autoResponse)
480                                         [outMessage appendString:@" auto=\"yes\""];
481                                 if(sender != nil)
482                                         [outMessage appendFormat:@" sender=\"%@\"", sender];
483                                 if(date != nil)
484                                         [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
485                                 if([encryption length])
486                                         [outMessage appendFormat:@" encryption=\"%@\"", encryption];
487                                 if([message length])
488                                         [outMessage appendFormat:@">%@</message>\n", message];
489                                 else
490                                         [outMessage appendString:@"/>\n"];
491                                 [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
492                                 state = XML_STATE_ENVELOPE;
493                         }
494                         break;
495                 case XML_STATE_EVENT:
496                         if([name isEqualToString:@"event"])
497                                 state = XML_STATE_NONE;
498                         break;
499                 case XML_STATE_EVENT_ATTRIBUTED_MESSAGE:
500                         if([name isEqualToString:@"Message"])
501                                 state = XML_STATE_EVENT;
502                         break;
503                 case XML_STATE_EVENT_MESSAGE:
504                         if([name isEqualToString:@"message"])
505                         {
506                                 CFIndex end = CFXMLParserGetLocation(parser);
507                                 NSString *message = nil;
508                                 if(!empty)
509                                         message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)];  // 10 for </message> and 1 for the index being off 
511                                 if([eventName isEqualToString:@"loggedOff"] || [eventName isEqualToString:@"memberParted"])
512                                 {
513                                         NSMutableString *outMessage = [NSMutableString stringWithString:@"<status type=\"offline\""];
514                                         if(sender != nil)
515                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
516                                         if(date != nil)
517                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
518                                         [outMessage appendString:@"/>"];
519                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
520                                 }
521                                 else if([eventName isEqualToString:@"loggedOn"] || [eventName isEqualToString:@"memberJoined"])
522                                 {
523                                         NSMutableString *outMessage = [NSMutableString stringWithString:@"<status type=\"online\""];
524                                         if(sender != nil)
525                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
526                                         if(date != nil)
527                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
528                                         [outMessage appendString:@"/>"];
529                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
530                                 }
531                                 else if([eventName isEqualToString:@"StatusChanged"])
532                                 {
533                                         //now we have to parse all these
534                                         NSString *type = nil;
535                                         int parseMessageIndex = 0;
536                                         BOOL idle = NO;
537                                         if(message != nil)
538                                         {
539                                                 if((parseMessageIndex = [message rangeOfString:@"changed status to Idle"].location) != NSNotFound)
540                                                 {
541                                                         type = @"online";
542                                                         idle = YES;
543                                                 }
544                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Available"].location) != NSNotFound)
545                                                         type = @"online";
546                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Away"].location) != NSNotFound)
547                                                         type = @"away";
548                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Busy"].location) != NSNotFound)
549                                                         type = @"busy";
550                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Not at Home"].location) != NSNotFound)
551                                                         type = @"notAtHome";
552                                                 else if((parseMessageIndex = [message rangeOfString:@"status to On the Phone"].location) != NSNotFound)
553                                                         type = @"onThePhone";
554                                                 else if((parseMessageIndex = [message rangeOfString:@"status to On Vacation"].location) != NSNotFound)
555                                                         type = @"onVacation";
556                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Do Not Disturb"].location) != NSNotFound)
557                                                         type = @"doNotDisturb";
558                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Extended Away"].location) != NSNotFound)
559                                                         type = @"extendedAway";
560                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Be Right Back"].location) != NSNotFound)
561                                                         type = @"beRightBack";
562                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Be NA"].location) != NSNotFound)
563                                                         type = @"notAvailable";
564                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Be Not at Home"].location) != NSNotFound)
565                                                         type = @"notAtHome";
566                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Not at my Desk"].location) != NSNotFound)
567                                                         type = @"notAtMyDesk";
568                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Not in the Office"].location) != NSNotFound)
569                                                         type = @"notInTheOffice";
570                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Stepped Out"].location) != NSNotFound)
571                                                         type = @"steppedOut";
572                                                 else
573                                                         NSLog(@"Unknown type %@", message);
574                                         }
575                                         //if the type is unknown, we can't do anything, drop it!
576                                         if(type != nil)
577                                         {
578                                                 int colonIndex = [message rangeOfString:@":" options:0 range:NSMakeRange(parseMessageIndex, [message length] - parseMessageIndex)].location;
579                                                 
580                                                 NSMutableString *outMessage = [NSMutableString stringWithFormat:@"<status type=\"%@\"", type];
581                                                 if(sender != nil)
582                                                         [outMessage appendFormat:@" sender=\"%@\"", sender];
583                                                 if(date != nil)
584                                                         [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
585                                                 if(idle)
586                                                         [outMessage appendString:@" idleTime=\"10\""];
588                                                 NSString *subStr = nil;
589                                                 if(colonIndex != NSNotFound && [message length] > colonIndex + 2)
590                                                         //Eliminate the "has changed status to: " from the string
591                                                         subStr = [message substringFromIndex:colonIndex + 2];
592                                                 if(![subStr hasPrefix:@"<span"] && [subStr hasSuffix:@"</span>"])
593                                                         //Eliminate the "</span>" at the end if it doesn't start with "<span"
594                                                         subStr = [subStr substringToIndex:[subStr length] - 7];
595                                                 if([htmlMessage length])
596                                                         //Prefer the attributed message
597                                                         [outMessage appendFormat:@">%@</status>\n", htmlMessage];
598                                                 else if([subStr length])
599                                                         [outMessage appendFormat:@">%@</status>\n", subStr];
600                                                 else
601                                                         [outMessage appendString:@"/>\n"];
602                                                 [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
603                                         }
604                                 }
605                                 else if([eventName isEqualToString:@"topicChanged"] ||
606                                                 [eventName isEqualToString:@"memberJoined"] ||
607                                                 [eventName isEqualToString:@"memberParted"] ||
608                                                 [eventName isEqualToString:@"memberPromoted"] ||
609                                                 [eventName isEqualToString:@"memberDemoted"] ||
610                                                 [eventName isEqualToString:@"memberVoiced"] ||
611                                                 [eventName isEqualToString:@"memberDevoiced"] ||
612                                                 [eventName isEqualToString:@"memberKicked"] ||
613                                                 [eventName isEqualToString:@"newNickname"])
614                                 {
615                                         NSMutableString *outMessage = [NSMutableString stringWithFormat:@"<status type=\"%@\"", [eventTranslate objectForKey:eventName]];
616                                         if(sender != nil)
617                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
618                                         if(date != nil)
619                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
620                                         
621                                         if([message length])
622                                                 [outMessage appendFormat:@">%@</status>\n", message];
623                                         else
624                                                 [outMessage appendString:@"/>\n"];
625                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
626                                 }
627                                 else if(eventName == nil)
628                                 {
629                                         //Generic message
630                                         NSMutableString *outMessage = [NSMutableString stringWithFormat:@"<event type=\"service\"", eventName];
631                                         if(sender != nil)
632                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
633                                         if(date != nil)
634                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
635                                         
636                                         if([message length])
637                                                 [outMessage appendFormat:@">%@</event>\n", message];
638                                         else
639                                                 [outMessage appendString:@"/>\n"];
640                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
641                                 }
642                                 else
643                                 {
644                                         //Need to translate these
645                                         NSLog(@"Got an Event %@ at %@: %@",
646                                                   eventName,
647                                                   date,
648                                                   message);
649                                 }
650                                 state = XML_STATE_EVENT;
651                         }
652                         break;
653                 case XML_STATE_EVENT_NICKNAME:
654                         state = XML_STATE_EVENT;
655                         break;
656                 case XML_STATE_NONE:
657                         if([name isEqualToString:@"log"])
658                                 [outputFileHandle writeData:[[NSString stringWithString:@"\n</chat>"] dataUsingEncoding:NSUTF8StringEncoding]];
659                         break;
660         }
663 typedef struct{
664         NSString        *name;
665         BOOL            empty;
666 } element;
668 - (void)text:(NSString *)text
670         switch(state)
671         {
672                 case XML_STATE_SENDER:
673                         if(sender == nil)
674                                 sender = [text retain];
675                         break;
676                 case XML_STATE_EVENT_NICKNAME:
677                         if(sender == nil)
678                                 sender = [text retain];
679                         break;
680                 case XML_STATE_EVENT_ATTRIBUTED_MESSAGE:
681                         if(htmlMessage == nil)
682                                 htmlMessage = [text mutableCopy];
683                         else
684                                 [htmlMessage appendString:text];
685                         break;
686                 case XML_STATE_NONE:
687                 case XML_STATE_ENVELOPE:
688                 case XML_STATE_MESSAGE:
689                 case XML_STATE_EVENT:
690                 case XML_STATE_EVENT_MESSAGE:
691                         break;
692         }
695 @end
697 static void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
699         element *ret = nil;
700         
701     // Use the dataTypeID to determine what to print.
702     switch (CFXMLNodeGetTypeCode(node)) {
703         case kCFXMLNodeTypeDocument:
704             break;
705         case kCFXMLNodeTypeElement:
706                 {
707                         NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
708                         const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
709                         [(GBFireXMLLogImporter *)context startedElement:name info:info];
710                         ret = (element *)malloc(sizeof(element));
711                         ret->name = [name retain];
712                         ret->empty = info->isEmpty;
713                         break;
714                 }
715         case kCFXMLNodeTypeProcessingInstruction:
716         case kCFXMLNodeTypeComment:
717                         break;
718         case kCFXMLNodeTypeEntityReference:
719                 {
720                         unichar entity = 0;
721                         CFXMLEntityReferenceInfo *entityInfo = (CFXMLEntityReferenceInfo *)CFXMLNodeGetInfoPtr(node);
722                         NSString *dataString = (NSString *)CFXMLNodeGetString(node);
723                         if(entityInfo->entityType == kCFXMLEntityTypeCharacter)
724                         {
725                                 if([dataString characterAtIndex:0] == '#')
726                                 {
727                                         BOOL hex = 0;
728                                         if([dataString characterAtIndex:1] == 'x')
729                                                 hex = 1;
730                                         
731                                         int i;
732                                         for(i = hex + 1; i < [dataString length]; i++)
733                                         {
734                                                 if(hex)
735                                                 {
736                                                         unichar encodedDigit = [dataString characterAtIndex:i];
737                                                         if(encodedDigit <= '9' && encodedDigit >= '0')
738                                                                 entity = entity * 16 + (encodedDigit - '0');
739                                                         else if(encodedDigit <= 'F' && encodedDigit >= 'A')
740                                                                 entity = entity * 16 + (encodedDigit - 'A' + 10);
741                                                         else if(encodedDigit <= 'f' && encodedDigit >= 'a')
742                                                                 entity = entity * 16 + (encodedDigit - 'a' + 10);
743                                                 }
744                                                 else
745                                                 {
746                                                         entity = entity * 10 + ([dataString characterAtIndex:i] - '0');
747                                                 }
748                                         }
749                                 }
750                         }
751                         else if(entityInfo->entityType == kCFXMLEntityTypeParsedInternal)
752                         {
753                                 if ([dataString isEqualToString:@"lt"])
754                                         entity = 0x3C;
755                                 else if ([dataString isEqualToString:@"gt"])
756                                         entity = 0x3E;
757                                 else if ([dataString isEqualToString:@"quot"])
758                                         entity = 0x22;
759                                 else if ([dataString isEqualToString:@"amp"])
760                                         entity = 0x26;
761                                 else if ([dataString isEqualToString:@"apos"])
762                                         entity = 0x27;
763                                 else if ([dataString isEqualToString:@"ldquo"])
764                                         entity = 0x201c;
765                                 else if ([dataString isEqualToString:@"rdquo"])
766                                         entity = 0x201d;
767                         }
768                         [(GBFireXMLLogImporter *)context text:[[[NSString alloc] initWithCharacters:&entity length:1] autorelease]];
769             break;
770                 }
771         case kCFXMLNodeTypeText:
772                         [(GBFireXMLLogImporter *)context text:[NSString stringWithString:(NSString *)CFXMLNodeGetString(node)]];
773             break;
774         case kCFXMLNodeTypeCDATASection:
775         case kCFXMLNodeTypeDocumentType:
776         case kCFXMLNodeTypeWhitespace:
777         default:
778                         break;
779         }
780         
781     // Return the data string for use by the addChild and 
782     // endStructure callbacks.
783     return (void *) ret;
786 static void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
790 static void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
792         NSString *name = nil;
793         BOOL empty = NO;
794         if(xmlType != NULL)
795         {
796                 name = [NSString stringWithString:((element *)xmlType)->name];
797                 empty = ((element *)xmlType)->empty;
798         }
799         [(GBFireXMLLogImporter *)context endedElement:name empty:empty];
800         if(xmlType != NULL)
801         {
802                 [((element *)xmlType)->name release];
803                 free(xmlType);
804         }
807 static Boolean errorStructure (CFXMLParserRef parser, CFXMLParserStatusCode error, void *info)
809         return NO;