Fixed a bunch of unit tests to restore state after they complete.
[adiumx.git] / Source / GBFireLogImporter.m
blob3f1d58595ed1349051ff25edf26a1ac713fe3447
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 mettings\"",
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);
199 @implementation GBFireXMLLogImporter
201 - (id)init
203         self = [super init];
204         if(self == nil)
205                 return nil;
206         
207         state = XML_STATE_NONE;
208         
209         inputFileString = nil;
210         outputFileHandle = nil;
211         sender = nil;
212         mySN = nil;
213         date = nil;
214         parser = NULL;
215         encryption = nil;
216         
217         eventTranslate = [[NSDictionary alloc] initWithObjectsAndKeys:
218                 @"userJoined", @"memberJoined",
219                 @"userParted", @"memberParted",
220                 @"userFormattedIdChanged", @"newNickname",
221                 @"channelTopicChanged", @"topicChanged",
222                 @"userPermissions/Promoted", @"memberPromoted",
223                 @"userPermissions/Demoted", @"memberDemoted",
224                 @"userPermissions/Voiced", @"memberVoiced",
225                 @"userPermissions/Devoiced", @"memberDevoiced",
226                 @"userKicked", @"memberKicked",
227                 nil];
228                 
229         return self;
232 - (BOOL)readFile:(NSString *)inFile toFile:(NSString *)outFile account:(NSString * *)account;
234         AILog(@"%@: readFile:%@ toFile:%@",NSStringFromClass([self class]), inFile, outFile);
236         BOOL success = YES;
237         NSData *inputData = [NSData dataWithContentsOfFile:inFile];
238         inputFileString = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding];
239         int outfd = open([outFile fileSystemRepresentation], O_CREAT | O_WRONLY, 0644);
240         outputFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:outfd closeOnDealloc:YES];
241         NSURL *url = [[NSURL alloc] initFileURLWithPath:inFile];
242         
243         CFXMLParserCallBacks callbacks = {
244                 0,
245                 createStructure,
246                 addChild,
247                 endStructure,
248                 NULL,
249                 NULL
250         };
251         CFXMLParserContext context = {
252                 0,
253                 self,
254                 CFRetain,
255                 CFRelease,
256                 NULL
257         };
258         parser = CFXMLParserCreateWithDataFromURL(NULL, (CFURLRef)url, kCFXMLParserSkipMetaData | kCFXMLParserSkipWhitespace, kCFXMLNodeCurrentVersion, &callbacks, &context);
259         if (!CFXMLParserParse(parser)) {
260                 NSLog(@"Fire log import: Parse of %@ failed", inFile);
261                 AILog(@"Fire log import: Parse of %@ failed", inFile);
262                 success = NO;
263         }
264         CFRelease(parser);
265         parser = nil;
266         [url release];
267         [outputFileHandle closeFile];
268         
269         *account = [[mySN retain] autorelease];
270         return success;
273 - (void)dealloc
275         [inputFileString release];
276         [outputFileHandle release];
277         [eventTranslate release];
278         [sender release];
279         [htmlMessage release];
280         [mySN release];
281         [date release];
282         [encryption release];
283         [super dealloc];
286 - (void)startedElement:(NSString *)name info:(const CFXMLElementInfo *)info
288         NSDictionary *attributes = (NSDictionary *)info->attributes;
289         
290         switch(state){
291                 case XML_STATE_NONE:
292                         if([name isEqualToString:@"envelope"])
293                         {
294                                 [sender release];
295                                 sender = nil;
296                                 state = XML_STATE_ENVELOPE;
297                         }
298                         else if([name isEqualToString:@"event"])
299                         {
300                                 [date release];
301                                 
302                                 NSString *dateStr = [attributes objectForKey:@"occurred"];
303                                 if(dateStr != nil)
304                                         date = [[NSCalendarDate dateWithString:dateStr] retain];
305                                 else
306                                         date = nil;
307                                 
308                                 [eventName release];
309                                 NSString *eventStr = [attributes objectForKey:@"name"];
310                                 if(eventStr != nil)
311                                         eventName = [[NSString alloc] initWithString:eventStr];
312                                 else
313                                         eventName = nil;
314                                 [sender release];
315                                 sender = nil;
316                                 [htmlMessage release];
317                                 htmlMessage = nil;
318                                 state = XML_STATE_EVENT;
319                         }
320                         else if([name isEqualToString:@"log"])
321                         {
322                                 NSString *service = [attributes objectForKey:@"service"];
323                                 NSString *account = [attributes objectForKey:@"accountName"];
324                                 if(account != nil)
325                                 {
326                                         NSRange range = [account rangeOfString:@"-"];
327                                         if(range.location != NSNotFound)
328                                         {
329                                                 mySN = [[account substringFromIndex:range.location + 1] retain];
330                                                 range = [mySN rangeOfString:@"@"];
331                                                 NSRange revRange = [mySN rangeOfString:@"@" options:NSBackwardsSearch];
332                                                 if ((revRange.location != range.location) && (revRange.location != NSNotFound))
333                                                 {
334                                                         NSString *oldMySN = mySN;
335                                                         mySN = [[mySN substringToIndex:revRange.location] retain];
336                                                         [oldMySN release];
337                                                 }
338                                         }
339                                 }
340                                 NSMutableString *chatTag = [NSMutableString stringWithFormat:@"%@\n<chat", XML_MARKER];
341                                 [chatTag appendString:@" xmlns=\"http://purl.org/net/ulf/ns/0.4-02\""];
342                                 if(mySN != nil)
343                                         [chatTag appendFormat:@" account=\"%@\"", mySN];
344                                 if(service != nil)
345                                         [chatTag appendFormat:@" service=\"%@\"", service];
346                                 [chatTag appendString:@">\n"];
347                                 [outputFileHandle writeData:[chatTag dataUsingEncoding:NSUTF8StringEncoding]];
348                         }
349                         break;
350                 case XML_STATE_ENVELOPE:
351                         if ([name isEqualToString:@"message"])
352                         {
353                                 [date release];
354                                 
355                                 NSString *dateStr = [attributes objectForKey:@"received"];
356                                 if(dateStr != nil)
357                                         date = [[NSCalendarDate dateWithString:dateStr] retain];
358                                 else
359                                         date = nil;
360                                 
361                                 //Mark the location of the message...  We can copy it directly.  Anyone know why it is off by 1?
362                                 messageStart = CFXMLParserGetLocation(parser) - 1;
363                                 
364                                 if([attributes objectForKey:@"action"] != nil)
365                                         actionMessage = YES;
366                                 else
367                                         actionMessage = NO;
368                                 
369                                 if([attributes objectForKey:@"away"] != nil)
370                                         autoResponse = YES;
371                                 else
372                                         autoResponse = NO;
373                                 [encryption release];
374                                 encryption = [[attributes objectForKey:@"security"] retain];
375                                 
376                                 state = XML_STATE_MESSAGE;
377                         }
378                         else if([name isEqualToString:@"sender"])
379                         {
380                                 [sender release];
381                                 
382                                 NSString *nickname = [attributes objectForKey:@"nickname"];
383                                 NSString *selfSender = [attributes objectForKey:@"self"];
384                                 if(nickname != nil)
385                                         sender = [nickname retain];
386                                 else if ([selfSender isEqualToString:@"yes"])
387                                         sender = [mySN retain];
388                                 else
389                                         sender = nil;
390                                 state = XML_STATE_SENDER;
391                         }
392                         break;
393                 case XML_STATE_SENDER:
394                 case XML_STATE_MESSAGE:
395                 case XML_STATE_EVENT:
396                         if([name isEqualToString:@"Message"])
397                         {
398                                 state = XML_STATE_EVENT_ATTRIBUTED_MESSAGE;
399                         }
400                         if([name isEqualToString:@"message"])
401                         {
402                                 //Mark the location of the message...  same as above
403                                 messageStart = CFXMLParserGetLocation(parser) - 1;
404                                 state = XML_STATE_EVENT_MESSAGE;
405                         }
406                         if([name isEqualToString:@"nickname"])
407                         {
408                                 state = XML_STATE_EVENT_NICKNAME;
409                         }
410                 case XML_STATE_EVENT_ATTRIBUTED_MESSAGE:
411                 case XML_STATE_EVENT_MESSAGE:
412                 case XML_STATE_EVENT_NICKNAME:
413                         break;
414         }
417 - (void)endedElement:(NSString *)name empty:(BOOL)empty
419         switch(state)
420         {
421                 case XML_STATE_ENVELOPE:
422                         if([name isEqualToString:@"envelope"])
423                                 state = XML_STATE_NONE;
424                         break;
425                 case XML_STATE_SENDER:
426                         if([name isEqualToString:@"sender"])
427                                 state = XML_STATE_ENVELOPE;
428                         break;
429                 case XML_STATE_MESSAGE:
430                         if([name isEqualToString:@"message"])
431                         {
432                                 CFIndex end = CFXMLParserGetLocation(parser);
433                                 NSString *message = nil;
434                                 if(!empty)
435                                         message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)];  // 10 for </message> and 1 for the index being off 
437                                 //Common logging format
438                                 NSMutableString *outMessage = [NSMutableString stringWithString:@"<message"];
439                                 if(actionMessage)
440                                 {
441                                         [outMessage appendString:@" type=\"action\""];
442                                         
443                                         //Hmm...  there is a bug in Fire's logging format that logs action messages like <span>username </span>message
444                                         int cutIndex = 0;
445                                         int index = [message rangeOfString:@"<span>"].location;
446                                         if(index == 0)
447                                         {
448                                                 int endIndex = [message rangeOfString:@"</span>"].location;
449                                                 if(sender == nil)
450                                                         sender = [[message substringWithRange:NSMakeRange(6, endIndex-7)] retain];  //6 is length of <span>.  7 is length of <span> plus trailing space
451                                                 index = cutIndex = endIndex + 7;  //7 is length of </span>
452                                         }
453                                         else
454                                                 index = 0;
455                                         while([message characterAtIndex:index] == '<')
456                                         {
457                                                 NSRange searchRange = NSMakeRange(index, [message length] - index);
458                                                 NSRange range = [message rangeOfString:@">" options:0 range:searchRange];
459                                                 index = range.location + 1;
460                                         }
461                                         NSString *newMessage = nil;
462                                         if(message)
463                                                 newMessage = [[NSString alloc] initWithFormat:@"%@/me %@", [message substringWithRange:NSMakeRange(cutIndex, index-cutIndex)], [message substringFromIndex:index]];
464                                         else
465                                                 newMessage = [[NSString alloc] initWithString:@"/me "];
466                                         message = [newMessage autorelease];
467                                 }
468                                 if(autoResponse)
469                                         [outMessage appendString:@" auto=\"yes\""];
470                                 if(sender != nil)
471                                         [outMessage appendFormat:@" sender=\"%@\"", sender];
472                                 if(date != nil)
473                                         [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
474                                 if([encryption length])
475                                         [outMessage appendFormat:@" encryption=\"%@\"", encryption];
476                                 if([message length])
477                                         [outMessage appendFormat:@">%@</message>\n", message];
478                                 else
479                                         [outMessage appendString:@"/>\n"];
480                                 [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
481                                 state = XML_STATE_ENVELOPE;
482                         }
483                         break;
484                 case XML_STATE_EVENT:
485                         if([name isEqualToString:@"event"])
486                                 state = XML_STATE_NONE;
487                         break;
488                 case XML_STATE_EVENT_ATTRIBUTED_MESSAGE:
489                         if([name isEqualToString:@"Message"])
490                                 state = XML_STATE_EVENT;
491                         break;
492                 case XML_STATE_EVENT_MESSAGE:
493                         if([name isEqualToString:@"message"])
494                         {
495                                 CFIndex end = CFXMLParserGetLocation(parser);
496                                 NSString *message = nil;
497                                 if(!empty)
498                                         message = [inputFileString substringWithRange:NSMakeRange(messageStart, end - messageStart - 11)];  // 10 for </message> and 1 for the index being off 
500                                 if([eventName isEqualToString:@"loggedOff"] || [eventName isEqualToString:@"memberParted"])
501                                 {
502                                         NSMutableString *outMessage = [NSMutableString stringWithString:@"<status type=\"offline\""];
503                                         if(sender != nil)
504                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
505                                         if(date != nil)
506                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
507                                         [outMessage appendString:@"/>"];
508                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
509                                 }
510                                 else if([eventName isEqualToString:@"loggedOn"] || [eventName isEqualToString:@"memberJoined"])
511                                 {
512                                         NSMutableString *outMessage = [NSMutableString stringWithString:@"<status type=\"online\""];
513                                         if(sender != nil)
514                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
515                                         if(date != nil)
516                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
517                                         [outMessage appendString:@"/>"];
518                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
519                                 }
520                                 else if([eventName isEqualToString:@"StatusChanged"])
521                                 {
522                                         //now we have to parse all these
523                                         NSString *type = nil;
524                                         int parseMessageIndex = 0;
525                                         BOOL idle = NO;
526                                         if(message != nil)
527                                         {
528                                                 if((parseMessageIndex = [message rangeOfString:@"changed status to Idle"].location) != NSNotFound)
529                                                 {
530                                                         type = @"online";
531                                                         idle = YES;
532                                                 }
533                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Available"].location) != NSNotFound)
534                                                         type = @"online";
535                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Away"].location) != NSNotFound)
536                                                         type = @"away";
537                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Busy"].location) != NSNotFound)
538                                                         type = @"busy";
539                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Not at Home"].location) != NSNotFound)
540                                                         type = @"notAtHome";
541                                                 else if((parseMessageIndex = [message rangeOfString:@"status to On the Phone"].location) != NSNotFound)
542                                                         type = @"onThePhone";
543                                                 else if((parseMessageIndex = [message rangeOfString:@"status to On Vacation"].location) != NSNotFound)
544                                                         type = @"onVacation";
545                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Do Not Disturb"].location) != NSNotFound)
546                                                         type = @"doNotDisturb";
547                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Extended Away"].location) != NSNotFound)
548                                                         type = @"extendedAway";
549                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Be Right Back"].location) != NSNotFound)
550                                                         type = @"beRightBack";
551                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Be NA"].location) != NSNotFound)
552                                                         type = @"notAvailable";
553                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Be Not at Home"].location) != NSNotFound)
554                                                         type = @"notAtHome";
555                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Not at my Desk"].location) != NSNotFound)
556                                                         type = @"notAtMyDesk";
557                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Not in the Office"].location) != NSNotFound)
558                                                         type = @"notInTheOffice";
559                                                 else if((parseMessageIndex = [message rangeOfString:@"status to Stepped Out"].location) != NSNotFound)
560                                                         type = @"steppedOut";
561                                                 else
562                                                         NSLog(@"Unknown type %@", message);
563                                         }
564                                         //if the type is unknown, we can't do anything, drop it!
565                                         if(type != nil)
566                                         {
567                                                 int colonIndex = [message rangeOfString:@":" options:0 range:NSMakeRange(parseMessageIndex, [message length] - parseMessageIndex)].location;
568                                                 
569                                                 NSMutableString *outMessage = [NSMutableString stringWithFormat:@"<status type=\"%@\"", type];
570                                                 if(sender != nil)
571                                                         [outMessage appendFormat:@" sender=\"%@\"", sender];
572                                                 if(date != nil)
573                                                         [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
574                                                 if(idle)
575                                                         [outMessage appendString:@" idleTime=\"10\""];
577                                                 NSString *subStr = nil;
578                                                 if(colonIndex != NSNotFound && [message length] > colonIndex + 2)
579                                                         //Eliminate the "has changed status to: " from the string
580                                                         subStr = [message substringFromIndex:colonIndex + 2];
581                                                 if(![subStr hasPrefix:@"<span"] && [subStr hasSuffix:@"</span>"])
582                                                         //Eliminate the "</span>" at the end if it doesn't start with "<span"
583                                                         subStr = [subStr substringToIndex:[subStr length] - 7];
584                                                 if([htmlMessage length])
585                                                         //Prefer the attributed message
586                                                         [outMessage appendFormat:@">%@</status>\n", htmlMessage];
587                                                 else if([subStr length])
588                                                         [outMessage appendFormat:@">%@</status>\n", subStr];
589                                                 else
590                                                         [outMessage appendString:@"/>\n"];
591                                                 [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
592                                         }
593                                 }
594                                 else if([eventName isEqualToString:@"topicChanged"] ||
595                                                 [eventName isEqualToString:@"memberJoined"] ||
596                                                 [eventName isEqualToString:@"memberParted"] ||
597                                                 [eventName isEqualToString:@"memberPromoted"] ||
598                                                 [eventName isEqualToString:@"memberDemoted"] ||
599                                                 [eventName isEqualToString:@"memberVoiced"] ||
600                                                 [eventName isEqualToString:@"memberDevoiced"] ||
601                                                 [eventName isEqualToString:@"memberKicked"] ||
602                                                 [eventName isEqualToString:@"newNickname"])
603                                 {
604                                         NSMutableString *outMessage = [NSMutableString stringWithFormat:@"<status type=\"%@\"", [eventTranslate objectForKey:eventName]];
605                                         if(sender != nil)
606                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
607                                         if(date != nil)
608                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
609                                         
610                                         if([message length])
611                                                 [outMessage appendFormat:@">%@</status>\n", message];
612                                         else
613                                                 [outMessage appendString:@"/>\n"];
614                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
615                                 }
616                                 else if(eventName == nil)
617                                 {
618                                         //Generic message
619                                         NSMutableString *outMessage = [NSMutableString stringWithFormat:@"<event type=\"service\"", eventName];
620                                         if(sender != nil)
621                                                 [outMessage appendFormat:@" sender=\"%@\"", sender];
622                                         if(date != nil)
623                                                 [outMessage appendFormat:@" time=\"%@\"", [date ISO8601DateString]];
624                                         
625                                         if([message length])
626                                                 [outMessage appendFormat:@">%@</event>\n", message];
627                                         else
628                                                 [outMessage appendString:@"/>\n"];
629                                         [outputFileHandle writeData:[outMessage dataUsingEncoding:NSUTF8StringEncoding]];
630                                 }
631                                 else
632                                 {
633                                         //Need to translate these
634                                         NSLog(@"Got an Event %@ at %@: %@",
635                                                   eventName,
636                                                   date,
637                                                   message);
638                                 }
639                                 state = XML_STATE_EVENT;
640                         }
641                         break;
642                 case XML_STATE_EVENT_NICKNAME:
643                         state = XML_STATE_EVENT;
644                         break;
645                 case XML_STATE_NONE:
646                         if([name isEqualToString:@"log"])
647                                 [outputFileHandle writeData:[[NSString stringWithString:@"\n</chat>"] dataUsingEncoding:NSUTF8StringEncoding]];
648                         break;
649         }
652 typedef struct{
653         NSString        *name;
654         BOOL            empty;
655 } element;
657 - (void)text:(NSString *)text
659         switch(state)
660         {
661                 case XML_STATE_SENDER:
662                         if(sender == nil)
663                                 sender = [text retain];
664                         break;
665                 case XML_STATE_EVENT_NICKNAME:
666                         if(sender == nil)
667                                 sender = [text retain];
668                         break;
669                 case XML_STATE_EVENT_ATTRIBUTED_MESSAGE:
670                         if(htmlMessage == nil)
671                                 htmlMessage = [text mutableCopy];
672                         else
673                                 [htmlMessage appendString:text];
674                         break;
675                 case XML_STATE_NONE:
676                 case XML_STATE_ENVELOPE:
677                 case XML_STATE_MESSAGE:
678                 case XML_STATE_EVENT:
679                 case XML_STATE_EVENT_MESSAGE:
680                         break;
681         }
684 @end
686 void *createStructure(CFXMLParserRef parser, CFXMLNodeRef node, void *context)
688         element *ret = nil;
689         
690     // Use the dataTypeID to determine what to print.
691     switch (CFXMLNodeGetTypeCode(node)) {
692         case kCFXMLNodeTypeDocument:
693             break;
694         case kCFXMLNodeTypeElement:
695                 {
696                         NSString *name = [NSString stringWithString:(NSString *)CFXMLNodeGetString(node)];
697                         const CFXMLElementInfo *info = CFXMLNodeGetInfoPtr(node);
698                         [(GBFireXMLLogImporter *)context startedElement:name info:info];
699                         ret = (element *)malloc(sizeof(element));
700                         ret->name = [name retain];
701                         ret->empty = info->isEmpty;
702                         break;
703                 }
704         case kCFXMLNodeTypeProcessingInstruction:
705         case kCFXMLNodeTypeComment:
706                         break;
707         case kCFXMLNodeTypeEntityReference:
708                 {
709                         unichar entity = 0;
710                         CFXMLEntityReferenceInfo *entityInfo = (CFXMLEntityReferenceInfo *)CFXMLNodeGetInfoPtr(node);
711                         NSString *dataString = (NSString *)CFXMLNodeGetString(node);
712                         if(entityInfo->entityType == kCFXMLEntityTypeCharacter)
713                         {
714                                 if([dataString characterAtIndex:0] == '#')
715                                 {
716                                         BOOL hex = 0;
717                                         if([dataString characterAtIndex:1] == 'x')
718                                                 hex = 1;
719                                         
720                                         int i;
721                                         for(i = hex + 1; i < [dataString length]; i++)
722                                         {
723                                                 if(hex)
724                                                 {
725                                                         unichar encodedDigit = [dataString characterAtIndex:i];
726                                                         if(encodedDigit <= '9' && encodedDigit >= '0')
727                                                                 entity = entity * 16 + (encodedDigit - '0');
728                                                         else if(encodedDigit <= 'F' && encodedDigit >= 'A')
729                                                                 entity = entity * 16 + (encodedDigit - 'A' + 10);
730                                                         else if(encodedDigit <= 'f' && encodedDigit >= 'a')
731                                                                 entity = entity * 16 + (encodedDigit - 'a' + 10);
732                                                 }
733                                                 else
734                                                 {
735                                                         entity = entity * 10 + ([dataString characterAtIndex:i] - '0');
736                                                 }
737                                         }
738                                 }
739                         }
740                         else if(entityInfo->entityType == kCFXMLEntityTypeParsedInternal)
741                         {
742                                 if ([dataString isEqualToString:@"lt"])
743                                         entity = 0x3C;
744                                 else if ([dataString isEqualToString:@"gt"])
745                                         entity = 0x3E;
746                                 else if ([dataString isEqualToString:@"quot"])
747                                         entity = 0x22;
748                                 else if ([dataString isEqualToString:@"amp"])
749                                         entity = 0x26;
750                                 else if ([dataString isEqualToString:@"apos"])
751                                         entity = 0x27;
752                                 else if ([dataString isEqualToString:@"ldquo"])
753                                         entity = 0x201c;
754                                 else if ([dataString isEqualToString:@"rdquo"])
755                                         entity = 0x201d;
756                         }
757                         [(GBFireXMLLogImporter *)context text:[[[NSString alloc] initWithCharacters:&entity length:1] autorelease]];
758             break;
759                 }
760         case kCFXMLNodeTypeText:
761                         [(GBFireXMLLogImporter *)context text:[NSString stringWithString:(NSString *)CFXMLNodeGetString(node)]];
762             break;
763         case kCFXMLNodeTypeCDATASection:
764         case kCFXMLNodeTypeDocumentType:
765         case kCFXMLNodeTypeWhitespace:
766         default:
767                         break;
768         }
769         
770     // Return the data string for use by the addChild and 
771     // endStructure callbacks.
772     return (void *) ret;
775 void addChild(CFXMLParserRef parser, void *parent, void *child, void *context)
779 void endStructure(CFXMLParserRef parser, void *xmlType, void *context)
781         NSString *name = nil;
782         BOOL empty = NO;
783         if(xmlType != NULL)
784         {
785                 name = [NSString stringWithString:((element *)xmlType)->name];
786                 empty = ((element *)xmlType)->empty;
787         }
788         [(GBFireXMLLogImporter *)context endedElement:name empty:empty];
789         if(xmlType != NULL)
790         {
791                 [((element *)xmlType)->name release];
792                 free(xmlType);
793         }