root/trunk/Models/JVChatTranscript.m

Revision 3766, 45.9 kB (checked in by timothy, 11 months ago)

Remove more 10.3 workarounds.

Line 
1 #import "JVChatTranscript.h"
2 #import "JVChatSession.h"
3 #import "JVChatMessage.h"
4 #import "JVChatEvent.h"
5 #import "KAIgnoreRule.h"
6 #import "NSAttributedStringMoreAdditions.h"
7
8 #import <libxml/tree.h>
9
10 NSString *JVChatTranscriptUpdatedNotification = @"JVChatTranscriptUpdatedNotification";
11
12 #pragma mark -
13
14 /* Future method ideas (implement when needed):
15 - (void) prependMessage:(JVChatMessage *) message;
16 - (void) prependMessages:(NSArray *) messages;
17
18 - (void) prependChatTranscript:(JVChatTranscript *) transcript;
19
20 - (void) insertMessage:(JVChatMessage *) message atIndex:(unsigned) index;
21
22 - (void) replaceMessageAtIndex:(unsigned) index withMessage:(JVChatMessage *) message;
23 - (void) replaceMessagesInRange:(NSRange) range withMessages:(NSArray *) messages;
24
25 - (void) removeMessage:(JVChatMessage *) message;
26 - (void) removeMessageAtIndex:(unsigned) index;
27 - (void) removeMessageAtIndexes:(NSIndexSet *) indexes;
28 - (void) removeMessagesInRange:(NSRange) range;
29 - (void) removeMessagesInArray:(NSArray *) messages;
30 - (void) removeAllMessages;
31 */
32
33 @interface JVChatSession (JVChatSessionPrivate)
34 - (id) _initWithNode:(xmlNode *) node andTranscript:(JVChatTranscript *) transcript;
35 - (void) _setNode:(xmlNode *) node;
36 @end
37
38 #pragma mark -
39
40 @interface JVChatMessage (JVChatMessagePrivate)
41 - (id) _initWithNode:(xmlNode *) node andTranscript:(JVChatTranscript *) transcript;
42 - (void) _setNode:(xmlNode *) node;
43 - (void) _loadFromXML;
44 - (void) _loadSenderFromXML;
45 - (void) _loadBodyFromXML;
46 @end
47
48 #pragma mark -
49
50 @interface JVChatEvent (JVChatEventPrivate)
51 - (id) _initWithNode:(xmlNode *) node andTranscript:(JVChatTranscript *) transcript;
52 - (void) _setNode:(xmlNode *) node;
53 @end
54
55 #pragma mark -
56
57 @interface JVChatTranscript (JVChatTranscriptPrivate)
58 - (void) _enforceElementLimit;
59 - (void) _incrementalWriteToLog:(xmlNodePtr) node continuation:(BOOL) cont;
60 - (void) _changeFileAttributesAtPath:(NSString *) path;
61 @end
62
63 #pragma mark -
64
65 @implementation JVChatTranscript
66 + (id) chatTranscript {
67         return [[[self alloc] init] autorelease];
68 }
69
70 + (id) chatTranscriptWithChatTranscript:(JVChatTranscript *) transcript {
71         return [[[self alloc] initWithChatTranscript:transcript] autorelease];
72 }
73
74 + (id) chatTranscriptWithElements:(NSArray *) elements {
75         return [[[self alloc] initWithElements:elements] autorelease];
76 }
77
78 + (id) chatTranscriptWithContentsOfFile:(NSString *) path {
79         return [[[self alloc] initWithContentsOfFile:path] autorelease];
80 }
81
82 + (id) chatTranscriptWithContentsOfURL:(NSURL *) url {
83         return [[[self alloc] initWithContentsOfURL:url] autorelease];
84 }
85
86 #pragma mark -
87
88 - (id) init {
89         if( ( self = [super init] ) ) {
90                 _filePath = nil;
91                 _logFile = nil;
92                 _objectSpecifier = nil;
93                 _autoWriteChanges = NO;
94                 _requiresNewEnvelope = YES;
95                 _previousLogOffset = 0;
96                 _elementLimit = 0;
97
98                 @synchronized( self ) {
99                         _messages = [[NSMutableArray allocWithZone:[self zone]] initWithCapacity:100];
100
101                         _xmlLog = xmlNewDoc( (xmlChar *) "1.0" );
102                         xmlDocSetRootElement( _xmlLog, xmlNewNode( NULL, (xmlChar *) "log" ) );
103                         xmlSetProp( xmlDocGetRootElement( _xmlLog ), (xmlChar *) "began", (xmlChar *) [[[NSDate date] description] UTF8String] );
104                 }
105         }
106
107         return self;
108 }
109
110 - (id) initWithChatTranscript:(JVChatTranscript *) transcript {
111         if( ( self = [self init] ) )
112                 [self appendChatTranscript:transcript];
113
114         return self;
115 }
116
117 - (id) initWithElements:(NSArray *) elements {
118         if( ( self = [self init] ) )
119                 [self appendElements:elements];
120
121         return self;
122 }
123
124 - (id) initWithContentsOfFile:(NSString *) path {
125         if( ( self = [self init] ) ) {
126                 path = [path stringByStandardizingPath];
127
128                 @synchronized( self ) {
129                         xmlFreeDoc( _xmlLog ); // release the empty document we made in [self init]
130                         if( ! ( _xmlLog = xmlParseFile( [path fileSystemRepresentation] ) ) ) {
131                                 [self autorelease]; // file failed to parse, return nil
132                                 return nil;
133                         }
134                 }
135
136                 [self setAutomaticallyWritesChangesToFile:YES];
137                 [self setFilePath:path];
138         }
139
140         return self;
141 }
142
143 - (id) initWithContentsOfURL:(NSURL *) url {
144         if( ( self = [self init] ) ) {
145                 NSData *contents = [NSData dataWithContentsOfURL:url];
146                 if( ! contents || ! [contents length] ) {
147                         [self release]; // URL failed to return content, return nil
148                         return nil;
149                 }
150
151                 @synchronized( self ) {
152                         xmlFreeDoc( _xmlLog ); // release the empty document we made in [self init]
153                         if( ! ( _xmlLog = xmlParseMemory( [contents bytes], [contents length] ) ) ) {
154                                 [self autorelease]; // data failed to parse, return nil
155                                 return nil;
156                         }
157                 }
158         }
159
160         return self;
161 }
162
163 - (void) dealloc {
164         [_filePath release];
165         [_logFile release];
166         [_messages release];
167         [_objectSpecifier release];
168
169         xmlFreeDoc( _xmlLog );
170
171         _objectSpecifier = nil;
172         _filePath = nil;
173         _logFile = nil;
174         _messages = nil;
175         _xmlLog = NULL;
176
177         [super dealloc];
178 }
179
180 #pragma mark -
181
182 - (void *) document {
183         return _xmlLog;
184 }
185
186 #pragma mark -
187
188 - (BOOL) isEmpty {
189         @synchronized( self ) {
190                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
191                 do {
192                         if( node && node -> type == XML_ELEMENT_NODE )
193                                 return NO;
194                 } while( node && ( node = node -> next ) );
195         }
196
197         return YES;
198 }
199
200 - (unsigned long) elementCount {
201         unsigned long count = 0;
202
203         @synchronized( self ) {
204                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
205                 do {
206                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
207                                 xmlNode *subNode = node -> children;
208                                 do {
209                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) )
210                                                 count++;
211                                 } while( subNode && ( subNode = subNode -> next ) );
212                         } else if( node && node -> type == XML_ELEMENT_NODE ) count++;
213                 } while( node && ( node = node -> next ) );
214         }
215
216         return count;
217 }
218
219 - (unsigned long) sessionCount {
220         unsigned long count = 0;
221
222         @synchronized( self ) {
223                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
224                 do {
225                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "session", (char *) node -> name ) )
226                                 count++;
227                 } while( node && ( node = node -> next ) );
228         }
229
230         return count;
231 }
232
233 - (unsigned long) messageCount {
234         unsigned long count = 0;
235
236         @synchronized( self ) {
237                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
238                 do {
239                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
240                                 xmlNode *subNode = node -> children;
241                                 do {
242                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) )
243                                                 count++;
244                                 } while( subNode && ( subNode = subNode -> next ) );
245                         }
246                 } while( node && ( node = node -> next ) );
247         }
248
249         return count;
250 }
251
252 - (unsigned long) eventCount {
253         unsigned long count = 0;
254
255         @synchronized( self ) {
256                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
257                 do {
258                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "event", (char *) node -> name ) )
259                                 count++;
260                 } while( node && ( node = node -> next ) );
261         }
262
263         return count;
264 }
265
266 #pragma mark -
267
268 - (void) setElementLimit:(unsigned int) limit {
269         _elementLimit = limit;
270         [self _enforceElementLimit];
271 }
272
273 - (unsigned int) elementLimit {
274         return _elementLimit;
275 }
276
277 #pragma mark -
278
279 - (NSArray *) elements {
280         return [self elementsInRange:NSMakeRange( 0, -1 )]; // will stop at the total number of elements.
281 }
282
283 - (NSArray *) elementsInRange:(NSRange) range {
284         if( ! range.length ) return [NSArray array];
285
286         @synchronized( self ) {
287                 unsigned long i = 0;
288                 NSMutableArray *ret = [[NSMutableArray allocWithZone:nil] initWithCapacity:range.length];
289
290                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
291                 do {
292                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
293                                 xmlNode *subNode = node -> children;
294                                 do {
295                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) ) {
296                                                 if( NSLocationInRange( i, range ) ) {
297                                                         JVChatMessage *msg = [[JVChatMessage allocWithZone:nil] _initWithNode:subNode andTranscript:self];
298                                                         if( msg ) [ret addObject:msg];
299                                                         [msg release];
300                                                 }
301
302                                                 if( ++i > ( range.location + range.length ) ) goto done;
303                                         }
304                                 } while( subNode && ( subNode = subNode -> next ) );
305                         } else if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "session", (char *) node -> name ) ) {
306                                 if( NSLocationInRange( i, range ) ) {
307                                         JVChatSession *session = [[JVChatSession allocWithZone:nil] _initWithNode:node andTranscript:self];
308                                         if( session ) [ret addObject:session];
309                                         [session release];
310                                 }
311
312                                 if( ++i > ( range.location + range.length ) ) goto done;
313                         } else if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "event", (char *) node -> name ) ) {
314                                 if( NSLocationInRange( i, range ) ) {
315                                         JVChatEvent *event = [[JVChatEvent allocWithZone:nil] _initWithNode:node andTranscript:self];
316                                         if( event ) [ret addObject:event];
317                                         [event release];
318                                 }
319
320                                 if( ++i > ( range.location + range.length ) ) goto done;
321                         }
322                 } while( node && ( node = node -> next ) );
323
324         done:
325                 return [ret autorelease];
326         } return nil;
327 }
328
329 - (id) elementAtIndex:(unsigned long) index {
330         return [[self elementsInRange:NSMakeRange( index, 1 )] lastObject];
331 }
332
333 - (id) lastElement {
334         @synchronized( self ) {
335                 xmlNode *node = xmlGetLastChild( xmlDocGetRootElement( _xmlLog ) );
336                 do {
337                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
338                                 xmlNode *subNode = xmlGetLastChild( node );
339                                 do {
340                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) )
341                                                 return [[[JVChatMessage allocWithZone:nil] _initWithNode:subNode andTranscript:self] autorelease];
342                                 } while( subNode && ( subNode = subNode -> prev ) );
343                         } else if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "session", (char *) node -> name ) ) {
344                                 return [[[JVChatSession allocWithZone:nil] _initWithNode:node andTranscript:self] autorelease];
345                         } else if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "event", (char *) node -> name ) ) {
346                                 return [[[JVChatEvent allocWithZone:nil] _initWithNode:node andTranscript:self] autorelease];
347                         }
348                 } while( node && ( node = node -> prev ) );
349         }
350
351         return nil;
352 }
353
354 #pragma mark -
355
356 - (NSArray *) appendElements:(NSArray *) elements {
357         NSMutableArray *ret = [[NSMutableArray allocWithZone:nil] initWithCapacity:[elements count]];
358         NSEnumerator *enumerator = [elements objectEnumerator];
359         id element = nil;
360
361         while( ( element = [enumerator nextObject] ) ) {
362                 if( ! [element conformsToProtocol:@protocol( JVChatTranscriptElement )] ) continue;
363                 @synchronized( ( [element transcript] ? (id) [element transcript] : (id) element ) ) {
364                         id newElement = nil;
365                         if( [element isKindOfClass:[JVChatMessage class]] ) newElement = [self appendMessage:element];
366                         else if( [element isKindOfClass:[JVChatEvent class]] ) newElement = [self appendEvent:element];
367                         else if( [element isKindOfClass:[JVChatSession class]] ) newElement = [self appendSession:element];
368                         if( newElement ) [ret addObject:newElement];
369                 }
370         }
371
372         return [ret autorelease];
373 }
374
375 - (void) appendChatTranscript:(JVChatTranscript *) transcript {
376         [self appendElements:[transcript elements]];
377 }
378
379 #pragma mark -
380
381 - (NSArray *) messages {
382         return [self messagesInRange:NSMakeRange( 0, -1 )]; // will stop at the total number of messages.
383 }
384
385 - (NSArray *) messagesInRange:(NSRange) range {
386         if( ! range.length ) return [NSArray array];
387
388         @synchronized( self ) {
389                 if( [_messages count] >= ( range.location + range.length ) ) {
390                         NSArray *sub = [_messages subarrayWithRange:range];
391                         if( ! [sub containsObject:[NSNull null]] ) {
392                                 return sub;
393                         }
394                 }
395
396                 if( [_messages count] < range.location )
397                         for( unsigned long i = [_messages count]; i < range.location; i++ )
398                                 [_messages insertObject:[NSNull null] atIndex:i];
399
400                 NSMutableArray *ret = [[NSMutableArray allocWithZone:nil] initWithCapacity:range.length];
401                 JVChatMessage *msg = nil;
402
403                 unsigned long i = 0;
404
405                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
406                 do {
407                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
408                                 xmlNode *subNode = node -> children;
409                                 do {
410                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) ) {
411                                                 if( NSLocationInRange( i, range ) ) {
412                                                         if( [_messages count] > i && [[_messages objectAtIndex:i] isKindOfClass:[JVChatMessage class]] ) {
413                                                                 msg = [[_messages objectAtIndex:i] retain];
414                                                         } else if( [_messages count] > i && [[_messages objectAtIndex:i] isKindOfClass:[NSNull class]] ) {
415                                                                 msg = [[JVChatMessage allocWithZone:nil] _initWithNode:subNode andTranscript:self];
416                                                                 id classDesc = [NSClassDescription classDescriptionForClass:[self class]];
417                                                                 [msg setObjectSpecifier:[[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDesc containerSpecifier:[self objectSpecifier] key:@"messages" uniqueID:[msg messageIdentifier]] autorelease]];
418                                                                 [_messages replaceObjectAtIndex:i withObject:msg];
419                                                         } else if( [_messages count] == i ) {
420                                                                 msg = [[JVChatMessage allocWithZone:nil] _initWithNode:subNode andTranscript:self];
421                                                                 id classDesc = [NSClassDescription classDescriptionForClass:[self class]];
422                                                                 [msg setObjectSpecifier:[[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDesc containerSpecifier:[self objectSpecifier] key:@"messages" uniqueID:[msg messageIdentifier]] autorelease]];
423                                                                 [_messages insertObject:msg atIndex:i];
424                                                         } else continue;
425                                                         if( msg ) [ret addObject:msg];
426                                                         [msg release];
427                                                 }
428
429                                                 if( ++i > ( range.location + range.length ) ) goto done;
430                                         }
431                                 } while( subNode && ( subNode = subNode -> next ) );
432                         }
433                 } while( node && ( node = node -> next ) );
434
435         done:
436                 return [ret autorelease];
437         } return nil;
438 }
439
440 - (JVChatMessage *) messageAtIndex:(unsigned long) index {
441         NSRange range = NSMakeRange( index, 1 );
442
443         @synchronized( self ) {
444                 if( [_messages count] > index ) {
445                         id obj = [_messages objectAtIndex:index];
446                         if( ! [obj isKindOfClass:[NSNull class]] ) {
447                                 return obj;
448                         }
449                 }
450         }
451
452         return [[self messagesInRange:range] lastObject];
453 }
454
455 - (JVChatMessage *) messageWithIdentifier:(NSString *) identifier {
456         NSParameterAssert( identifier != nil );
457         NSParameterAssert( [identifier length] > 0 );
458
459         @synchronized( self ) {
460                 const char *ident = [identifier UTF8String];
461                 xmlNode *foundNode = NULL;
462
463                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
464                 do {
465                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
466                                 xmlNode *subNode = node -> children;
467                                 do {
468                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) ) {
469                                                 xmlChar *prop = xmlGetProp( subNode, (xmlChar *) "id" );
470                                                 if( prop && ! strcmp( (char *) prop, ident ) ) foundNode = subNode;
471                                                 if( prop ) xmlFree( prop );
472                                                 if( foundNode ) break;
473                                         }
474                                 } while( subNode && ( subNode = subNode -> next ) );
475                         }
476                 } while( node && ( node = node -> next ) );
477
478                 return ( foundNode ? [[[JVChatMessage allocWithZone:nil] _initWithNode:foundNode andTranscript:self] autorelease] : nil );
479         } return nil;
480 }
481
482 - (JVChatMessage *) lastMessage {
483         @synchronized( self ) {
484                 xmlNode *foundNode = NULL;
485                 xmlNode *node = xmlGetLastChild( xmlDocGetRootElement( _xmlLog ) );
486
487                 do {
488                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
489                                 xmlNode *subNode = xmlGetLastChild( node );
490                                 do {
491                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) ) {
492                                                 foundNode = subNode;
493                                                 break;
494                                         }
495                                 } while( subNode && ( subNode = subNode -> prev ) );
496                         }
497                 } while( node && ( node = node -> prev ) );
498
499                 return ( foundNode ? [[[JVChatMessage allocWithZone:nil] _initWithNode:foundNode andTranscript:self] autorelease] : nil );
500         } return nil;
501 }
502
503 #pragma mark -
504
505 - (BOOL) containsMessageWithIdentifier:(NSString *) identifier {
506         NSParameterAssert( identifier != nil );
507         NSParameterAssert( [identifier length] > 0 );
508
509         @synchronized( self ) {
510                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
511                 const char *ident = [identifier UTF8String];
512                 BOOL found = NO;
513
514                 do {
515                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) node -> name ) ) {
516                                 xmlNode *subNode = node -> children;
517                                 do {
518                                         if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "message", (char *) subNode -> name ) ) {
519                                                 xmlChar *prop = xmlGetProp( subNode, (xmlChar *) "id" );
520                                                 if( prop && ! strcmp( (char *) prop, ident ) ) found = YES;
521                                                 if( prop ) xmlFree( prop );
522                                                 if( found ) return YES;
523                                         }
524                                 } while( subNode && ( subNode = subNode -> next ) );
525                         }
526                 } while( node && ( node = node -> next ) );
527         }
528
529         return NO;
530 }
531
532 #pragma mark -
533
534 - (JVChatMessage *) appendMessage:(JVChatMessage *) message {
535         return [self appendMessage:message forceNewEnvelope:NO];
536 }
537
538 - (JVChatMessage *) appendMessage:(JVChatMessage *) message forceNewEnvelope:(BOOL) forceEnvelope {
539         NSParameterAssert( message != nil );
540         NSParameterAssert( [message node] != NULL );
541         NSParameterAssert( [message transcript] != self );
542
543         xmlNode *root = NULL, *child = NULL, *parent = NULL;
544
545         @synchronized( self ) {
546                 if( ! _requiresNewEnvelope && ! forceEnvelope ) {
547                         // check if the last node is an envelope by the same sender (and maybe source), if so append this message to that envelope
548                         xmlNode *lastChild = xmlGetLastChild( xmlDocGetRootElement( _xmlLog ) );
549                         if( lastChild && lastChild -> type == XML_ELEMENT_NODE && ! strcmp( "envelope", (char *) lastChild -> name ) ) {
550                                 NSString *msgSource = [[message source] absoluteString];
551
552                                 xmlChar *sourceStr = xmlGetProp( lastChild, (xmlChar *) "source" );
553                                 NSString *source = ( sourceStr ? [NSString stringWithUTF8String:(char *) sourceStr] : nil );
554                                 xmlFree( sourceStr );
555
556                                 if( ( ! msgSource && ! source ) || [msgSource isEqualToString:source] ) { // same chat source, proceed to sender check
557                                         xmlNode *subNode = lastChild -> children;
558                                         do {
559                                                 if( subNode && subNode -> type == XML_ELEMENT_NODE && ! strcmp( "sender", (char *) subNode -> name ) ) {
560                                                         NSString *identifier = [message senderIdentifier];
561                                                         NSString *nickname = [message senderNickname];
562                                                         NSString *name = [message senderName];
563
564                                                         xmlChar *senderNameStr = xmlNodeGetContent( subNode );
565                                                         NSString *senderName = [NSString stringWithUTF8String:(char *) senderNameStr];
566                                                         xmlFree( senderNameStr );
567
568                                                         NSString *senderNickname = nil;
569                                                         NSString *senderIdentifier = nil;
570
571                                                         xmlChar *prop = xmlGetProp( subNode, (xmlChar *) "nickname" );
572                                                         if( prop ) senderNickname = [NSString stringWithUTF8String:(char *) prop];
573                                                         xmlFree( prop );
574
575                                                         prop = xmlGetProp( subNode, (xmlChar *) "identifier" );
576                                                         if( prop ) senderIdentifier = [NSString stringWithUTF8String:(char *) prop];
577                                                         xmlFree( prop );
578
579                                                         if( [senderIdentifier isEqualToString:identifier] || [senderNickname isEqualToString:nickname] || [senderName isEqualToString:name] )
580                                                                 parent = lastChild;
581
582                                                         break;
583                                                 }
584                                         } while( subNode && ( subNode = subNode -> next ) );
585                                 }
586                         }
587                 }
588
589                 if( ! parent ) { // make a new envelope to append
590                         root = xmlNewNode( NULL, (xmlChar *) "envelope" );
591                         root = xmlAddChild( xmlDocGetRootElement( _xmlLog ), root );
592
593                         if( [message source] ) xmlSetProp( root, (xmlChar *) "source", (xmlChar *) [[[message source] absoluteString] UTF8String] );
594
595                         if( [message ignoreStatus] == JVUserIgnored )
596                                 xmlSetProp( root, (xmlChar *) "ignored", (xmlChar *) "yes" );
597
598                         xmlNode *subNode = ((xmlNode *) [message node]) -> parent -> children;
599
600                         do {
601                                 if( ! strcmp( "sender", (char *) subNode -> name ) ) break;
602                         } while( subNode && ( subNode = subNode -> next ) );
603
604                         child = xmlDocCopyNode( subNode, _xmlLog, 1 );
605                         xmlAddChild( root, child );
606
607                         child = xmlDocCopyNode( (xmlNode *) [message node], _xmlLog, 1 );
608                         xmlAddChild( root, child );
609                 } else { // append message to an existing envelope
610                         root = parent;
611                         child = (xmlNode *) [message node];
612                         child = xmlAddChild( parent, xmlDocCopyNode( child, _xmlLog, 1 ) );
613                 }
614
615                 [self _enforceElementLimit];
616                 [self _incrementalWriteToLog:root continuation:( parent ? YES : NO )];
617
618                 if( _logFile ) {
619                         NSString *lastDateString = [[message date] description];
620                         fsetxattr( [_logFile fileDescriptor], "lastMessageDate", [lastDateString UTF8String], [lastDateString length], 0, 0 );
621                 }
622
623                 _requiresNewEnvelope = NO;
624
625                 return [[[JVChatMessage allocWithZone:nil] _initWithNode:child andTranscript:self] autorelease];
626         } return nil;
627 }
628
629 - (NSArray *) appendMessages:(NSArray *) messages {
630         return [self appendMessages:messages forceNewEnvelope:NO];
631 }
632
633 - (NSArray *) appendMessages:(NSArray *) messages forceNewEnvelope:(BOOL) forceEnvelope {
634         NSEnumerator *enumerator = [messages objectEnumerator];
635         JVChatMessage *message = nil;
636         NSMutableArray *ret = [[NSMutableArray allocWithZone:nil] initWithCapacity:[messages count]];
637
638         if( forceEnvelope ) _requiresNewEnvelope = YES;
639
640         while( ( message = [enumerator nextObject] ) ) {
641                 if( ! [message isKindOfClass:[JVChatMessage class]] ) continue;
642                 @synchronized( ( [message transcript] ? (id) [message transcript] : (id) message ) ) {
643                         message = [self appendMessage:message];
644                         if( message ) [ret addObject:message];
645                 }
646         }
647
648         return [ret autorelease];
649 }
650
651 #pragma mark -
652
653 - (NSArray *) sessions {
654         return [self sessionsInRange:NSMakeRange( 0, -1 )]; // will stop at the total number of sessions.
655 }
656
657 - (NSArray *) sessionsInRange:(NSRange) range {
658         if( ! range.length ) return [NSArray array];
659
660         @synchronized( self ) {
661                 unsigned long i = 0;
662                 NSMutableArray *ret = [[NSMutableArray allocWithZone:nil] initWithCapacity:range.length];
663
664                 xmlNode *node = xmlDocGetRootElement( _xmlLog ) -> children;
665                 do {
666                         if( node && node -> type == XML_ELEMENT_NODE && ! strcmp( "session", (char *) node -> name ) ) {
667                                 if( NSLocationInRange( i, range ) ) {
668                                         JVChatSession *session = [[JVChatSession allocWithZone:nil] _initWithNode:node andTranscript:self];
669                                         if( session ) [ret addObject:session];
670                                         [session release];
671                                 }
672
673                                 if( ++i > ( range.location + range.length ) ) goto done