root/trunk/Models/JVChatMessage.m

Revision 3609, 18.0 kB (checked in by timothy, 2 years ago)

Fixes a leak in Smart Transcript message matching.

Line 
1 #import <libxml/tree.h>
2
3 #import "JVChatMessage.h"
4 #import "JVBuddy.h"
5 #import "JVChatRoomMember.h"
6 #import "JVChatTranscript.h"
7 #import "JVChatRoomPanel.h"
8 #import "JVChatRoomMember.h"
9 #import "NSAttributedStringMoreAdditions.h"
10
11 @interface JVChatTranscript (JVChatTranscriptPrivate)
12 - (void) _loadMessage:(JVChatMessage *) message;
13 - (void) _loadSenderForMessage:(JVChatMessage *) message;
14 - (void) _loadBodyForMessage:(JVChatMessage *) message;
15 @end
16
17 #pragma mark -
18
19 @implementation JVChatMessage
20 + (void) initialize {
21         [super initialize];
22         static BOOL tooLate = NO;
23         if( ! tooLate ) {
24                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceMessage:toString: ) toConvertFromClass:[JVChatMessage class] toClass:[NSString class]];
25                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceString:toMessage: ) toConvertFromClass:[NSString class] toClass:[JVChatMessage class]];
26                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceMessage:toTextStorage: ) toConvertFromClass:[JVChatMessage class] toClass:[NSTextStorage class]];
27                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceTextStorage:toMessage: ) toConvertFromClass:[NSTextStorage class] toClass:[JVChatMessage class]];
28                 tooLate = YES;
29         }
30 }
31
32 + (id) coerceString:(id) value toMessage:(Class) class {
33         return [[[JVMutableChatMessage allocWithZone:nil] initWithText:value sender:nil] autorelease];
34 }
35
36 + (id) coerceMessage:(id) value toString:(Class) class {
37         return [value bodyAsPlainText];
38 }
39
40 + (id) coerceTextStorage:(id) value toMessage:(Class) class {
41         return [[[JVMutableChatMessage allocWithZone:nil] initWithText:value sender:nil] autorelease];
42 }
43
44 + (id) coerceMessage:(id) value toTextStorage:(Class) class {
45         return [value body];
46 }
47
48 #pragma mark -
49
50 - (void) load {
51         if( _loaded ) return;
52         [_transcript _loadMessage:self];
53 }
54
55 - (void) loadBody {
56         if( _bodyLoaded ) return;
57         [_transcript _loadBodyForMessage:self];
58 }
59
60 - (void) loadSender {
61         if( _senderLoaded ) return;
62         [_transcript _loadSenderForMessage:self];
63 }
64
65 #pragma mark -
66
67 - (id) init {
68         if( ( self = [super init] ) ) {
69                 _ignoreStatus = JVNotIgnored;
70                 _type = JVChatMessageNormalType;
71         }
72
73         return self;
74 }
75
76 - (id) mutableCopyWithZone:(NSZone *) zone {
77         JVMutableChatMessage *ret =  nil;
78
79         @synchronized( _transcript ) {
80                 ret = [[JVMutableChatMessage allocWithZone:zone] init];
81
82                 ret -> _loaded = YES;
83                 ret -> _senderLoaded = YES;
84                 ret -> _bodyLoaded = YES;
85
86                 // release anything alloced in [JVMutableChatMessage init] and [JVChatMessage init] that we copy below
87                 [ret -> _date release];
88
89                 ret -> _senderIsLocalUser = [self senderIsLocalUser];
90                 ret -> _senderIdentifier = [[self senderIdentifier] copyWithZone:zone];
91                 ret -> _senderName = [[self senderName] copyWithZone:zone];
92                 ret -> _senderHostmask = [[self senderHostmask] copyWithZone:zone];
93                 ret -> _senderClass = [[self senderClass] copyWithZone:zone];
94                 ret -> _senderBuddyIdentifier = [[self senderBuddyIdentifier] copyWithZone:zone];
95                 ret -> _attributedMessage = [[self body] mutableCopyWithZone:zone];
96                 ret -> _source = [[self source] copyWithZone:zone];
97                 ret -> _date = [[self date] copyWithZone:zone];
98                 ret -> _action = [self isAction];
99                 ret -> _highlighted = [self isHighlighted];
100                 ret -> _ignoreStatus = [self ignoreStatus];
101                 ret -> _type = [self type];
102                 ret -> _attributes = [[self attributes] copyWithZone:zone];
103         }
104
105         return ret;
106 }
107
108 - (void) dealloc {
109         [_messageIdentifier release];
110         [_attributedMessage release];
111         [_date release];
112         [_source release];
113         [_objectSpecifier release];
114         [_attributes release];
115
116         [_senderIdentifier release];
117         [_senderName release];
118         [_senderNickname release];
119         [_senderHostmask release];
120         [_senderClass release];
121         [_senderBuddyIdentifier release];
122
123         _node = NULL;
124         _transcript = nil;
125         _messageIdentifier = nil;
126         _attributedMessage = nil;
127         _date = nil;
128         _source = nil;
129         _objectSpecifier = nil;
130
131         _senderIdentifier = nil;
132         _senderName = nil;
133         _senderNickname = nil;
134         _senderHostmask = nil;
135         _senderClass = nil;
136         _senderBuddyIdentifier = nil;
137
138         if( _doc ) xmlFreeDoc( _doc );
139         _doc = NULL;
140
141         [super dealloc];
142 }
143
144 #pragma mark -
145
146 - (void *) node {
147         if( ! _node ) {
148                 if( _doc ) xmlFreeDoc( _doc );
149                 _doc = xmlNewDoc( (xmlChar *) "1.0" );
150
151                 xmlNodePtr child = NULL;
152                 xmlNodePtr root = xmlNewNode( NULL, (xmlChar *) "envelope" );
153                 xmlDocSetRootElement( _doc, root );
154
155                 if( _source ) xmlSetProp( root, (xmlChar *) "source", (xmlChar *) [[[self source] absoluteString] UTF8String] );
156
157                 id sender = nil;
158                 if( [self respondsToSelector:@selector( sender )] )
159                         sender = [self performSelector:@selector( sender )];
160
161                 if( sender && [sender respondsToSelector:@selector( xmlDescriptionWithTagName: )] ) {
162                         const char *sendDesc = [(NSString *)[sender performSelector:@selector( xmlDescriptionWithTagName: ) withObject:@"sender"] UTF8String];
163
164                         if( sendDesc ) {
165                                 xmlDocPtr tempDoc = xmlParseMemory( sendDesc, strlen( sendDesc ) );
166                                 if( ! tempDoc ) return NULL; // somthing bad with the message contents
167
168                                 child = xmlDocCopyNode( xmlDocGetRootElement( tempDoc ), _doc, 1 );
169                                 xmlAddChild( root, child );
170                                 xmlFreeDoc( tempDoc );
171                         }
172                 } else {
173                         child = xmlNewTextChild( root, NULL, (xmlChar *) "sender", ( [self senderName] ? (xmlChar *) [[self senderName] UTF8String] : (xmlChar *) "" ) );
174                         if( [self senderIsLocalUser] ) xmlSetProp( child, (xmlChar *) "self", (xmlChar *) "yes" );
175                         if( [self senderNickname] && ! [[self senderName] isEqualToString:[self senderNickname]] )
176                                 xmlSetProp( child, (xmlChar *) "nickname", (xmlChar *) [[self senderNickname] UTF8String] );
177                         if( [self senderHostmask] )
178                                 xmlSetProp( child, (xmlChar *) "hostmask", (xmlChar *) [[self senderNickname] UTF8String] );
179                         if( [self senderIdentifier] )
180                                 xmlSetProp( child, (xmlChar *) "identifier", (xmlChar *) [[self senderIdentifier] UTF8String] );
181                         if( [self senderClass] )
182                                 xmlSetProp( child, (xmlChar *) "class", (xmlChar *) [[self senderClass] UTF8String] );
183                         if( [self senderBuddyIdentifier] && ! [self senderIsLocalUser] )
184                                 xmlSetProp( child, (xmlChar *) "buddy", (xmlChar *) [[self senderBuddyIdentifier] UTF8String] );
185                 }
186
187                 NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"IgnoreFonts", [NSNumber numberWithBool:YES], @"IgnoreFontSizes", nil];
188                 NSString *htmlMessage = ( [self body] ? [[self body] HTMLFormatWithOptions:options] : @"" );
189                 const char *msgStr = [[NSString stringWithFormat:@"<message>%@</message>", [htmlMessage stringByStrippingIllegalXMLCharacters]] UTF8String];
190                 xmlDocPtr msgDoc = xmlParseMemory( msgStr, strlen( msgStr ) );
191                 if( ! msgDoc ) return NULL; // somthing bad with the message contents
192
193                 _node = child = xmlDocCopyNode( xmlDocGetRootElement( msgDoc ), _doc, 1 );
194                 xmlSetProp( child, (xmlChar *) "id", (xmlChar *) [[self messageIdentifier] UTF8String] );
195                 xmlSetProp( child, (xmlChar *) "received", (xmlChar *) [[[self date] description] UTF8String] );
196                 if( [self isAction] ) xmlSetProp( child, (xmlChar *) "action", (xmlChar *) "yes" );
197                 if( [self isHighlighted] ) xmlSetProp( child, (xmlChar *) "highlight", (xmlChar *) "yes" );
198                 if( [self ignoreStatus] == JVMessageIgnored ) xmlSetProp( child, (xmlChar *) "ignored", (xmlChar *) "yes" );
199                 else if( [self ignoreStatus] == JVUserIgnored ) xmlSetProp( root, (xmlChar *) "ignored", (xmlChar *) "yes" );
200                 if( [self type] == JVChatMessageNoticeType ) xmlSetProp( child, (xmlChar *) "type", (xmlChar *) "notice" );
201                 xmlAddChild( root, child );
202
203                 xmlFreeDoc( msgDoc );
204         }
205
206         return _node;
207 }
208
209 - (void) _setNode:(xmlNode *) node {
210         if( _doc ) {
211                 xmlFreeDoc( _doc );
212                 _doc = NULL;
213         }
214
215         _node = node;
216 }
217
218 #pragma mark -
219
220 - (NSDate *) date {
221         [self load];
222         return _date;
223 }
224
225 #pragma mark -
226
227 - (unsigned) consecutiveOffset {
228         [self load];
229         return _consecutiveOffset;
230 }
231
232 #pragma mark -
233
234 - (NSString *) senderName {
235         [self loadSender];
236         return _senderName;
237 }
238
239 - (NSString *) senderIdentifier {
240         [self loadSender];
241         return _senderIdentifier;
242 }
243
244 - (NSString *) senderNickname {
245         [self loadSender];
246         return _senderNickname;
247 }
248
249 - (NSString *) senderHostmask {
250         [self loadSender];
251         return _senderHostmask;
252 }
253
254 - (NSString *) senderClass {
255         [self loadSender];
256         return _senderClass;
257 }
258
259 - (NSString *) senderBuddyIdentifier {
260         [self loadSender];
261         return _senderBuddyIdentifier;
262 }
263
264 - (BOOL) senderIsLocalUser {
265         [self loadSender];
266         return _senderIsLocalUser;
267 }
268
269 #pragma mark -
270
271 - (NSTextStorage *) body {
272         [self loadBody];
273         return _attributedMessage;
274 }
275
276 - (NSString *) bodyAsPlainText {
277         return [[self body] string];
278 }
279
280 - (NSString *) bodyAsHTML {
281         NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], @"IgnoreFonts", [NSNumber numberWithBool:YES], @"IgnoreFontSizes", nil];
282         return [[self body] HTMLFormatWithOptions:options];
283 }
284
285 #pragma mark -
286
287 - (BOOL) isAction {
288         [self load];
289         return _action;
290 }
291
292 - (BOOL) isHighlighted {
293         [self load];
294         return _highlighted;
295 }
296
297 - (JVIgnoreMatchResult) ignoreStatus {
298         [self load];
299         return _ignoreStatus;
300 }
301
302 - (JVChatMessageType) type {
303         [self load];
304         return _type;
305 }
306
307 #pragma mark -
308
309 - (NSURL *) source {
310         [self load];
311         return _source;
312 }
313
314 - (JVChatTranscript *) transcript {
315         return _transcript;
316 }
317
318 - (NSString *) messageIdentifier {
319         return _messageIdentifier;
320 }
321
322 #pragma mark -
323
324 - (NSScriptObjectSpecifier *) objectSpecifier {
325         return _objectSpecifier;
326 }
327
328 - (void) setObjectSpecifier:(NSScriptObjectSpecifier *) objectSpecifier {
329         id old = _objectSpecifier;
330         _objectSpecifier = [objectSpecifier retain];
331         [old release];
332 }
333
334 #pragma mark -
335
336 - (NSDictionary *) attributes {
337         // Add important attributes which are set via normal setters, and therefore don't exist normally in the attributes-dict.
338         if( ! _attributes )
339                 _attributes = [[NSMutableDictionary alloc] init];
340         [_attributes setObject:[NSNumber numberWithBool:_action] forKey:@"action"];
341
342         return _attributes;
343 }
344
345 - (id) attributeForKey:(id) key {
346         return [_attributes objectForKey:key];
347 }
348
349 #pragma mark -
350
351 - (NSString *) description {
352         return [self bodyAsPlainText];
353 }
354
355 - (NSString *) debugDescription {
356         return [NSString stringWithFormat:@"<%@ 0x%x [%@]: (%@) %@>", NSStringFromClass( [self class] ), (unsigned long) self, _messageIdentifier, [self senderNickname], [self body]];
357 }
358
359 #pragma mark -
360
361 - (id) valueForUndefinedKey:(NSString *) key {
362         if( [NSScriptCommand currentCommand] ) {
363                 [[NSScriptCommand currentCommand] setScriptErrorNumber:1000];
364                 [[NSScriptCommand currentCommand] setScriptErrorString:[NSString stringWithFormat:@"The message id %@ doesn't have the \"%@\" property.", [self messageIdentifier], key]];
365                 return nil;
366         }
367
368         return [super valueForUndefinedKey:key];
369 }
370
371 - (void) setValue:(id) value forUndefinedKey:(NSString *) key {
372         if( [NSScriptCommand currentCommand] ) {
373                 // this is a non-mutable message, give AppleScript a good error if this is a script command call
374                 [[NSScriptCommand currentCommand] setScriptErrorNumber:1000];
375                 [[NSScriptCommand currentCommand] setScriptErrorString:[NSString stringWithFormat:@"The properties of message id %@ are read only.", key, [self messageIdentifier]]];
376                 return;
377         }
378
379         [super setValue:value forUndefinedKey:key];
380 }
381 @end
382
383 #pragma mark -
384
385 @implementation JVMutableChatMessage
386 + (void) initialize {
387         [super initialize];
388         static BOOL tooLate = NO;
389         if( ! tooLate ) {
390                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceMessage:toString: ) toConvertFromClass:[JVMutableChatMessage class] toClass:[NSString class]];
391                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceString:toMessage: ) toConvertFromClass:[NSString class] toClass:[JVMutableChatMessage class]];
392                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceMessage:toTextStorage: ) toConvertFromClass:[JVMutableChatMessage class] toClass:[NSTextStorage class]];
393                 [[NSScriptCoercionHandler sharedCoercionHandler] registerCoercer:[self class] selector:@selector( coerceTextStorage:toMessage: ) toConvertFromClass:[NSTextStorage class] toClass:[JVMutableChatMessage class]];
394                 tooLate = YES;
395         }
396 }
397
398 + (id) messageWithText:(id) body sender:(id) sender {
399         return [[[self allocWithZone:nil] initWithText:body sender:sender] autorelease];
400 }
401
402 #pragma mark -
403
404 - (id) init {
405         if( ( self = [super init] ) ) {
406                 _loaded = YES;
407                 _bodyLoaded = YES;
408                 _senderLoaded = YES;
409                 [self setDate:[NSDate date]];
410                 [self setMessageIdentifier:[NSString locallyUniqueString]];
411         }
412
413         return self;
414 }
415
416 - (id) initWithText:(id) body sender:(id) sender {
417         if( ( self = [self init] ) ) {
418                 [self setBody:body];
419                 [self setSender:sender];
420         }
421
422         return self;
423 }
424
425 - (void) dealloc {
426         [_sender release];
427         _sender = nil;
428
429         [super dealloc];
430 }
431
432 #pragma mark -
433
434 - (void) setDate:(NSDate *) date {
435         [self _setNode:NULL];
436         id old = _date;
437         _date = [date copyWithZone:[self zone]];
438         [old release];
439 }
440
441 #pragma mark -
442
443 - (void) setSender:(id) sender {
444         [self _setNode:NULL];
445         id old = _sender;
446         _sender = [sender retain];
447         [old release];
448 }
449
450 - (id) sender {
451         return _sender;
452 }
453
454 - (NSString *) senderName {
455         if( [[self sender] respondsToSelector:@selector( displayName )] )
456                 return [[self sender] displayName];
457         return [super senderName];
458 }
459
460 - (NSString *) senderIdentifier {
461         id identifier = nil;
462
463         if( [[self sender] isKindOfClass:[MVChatUser class]] ) {
464                 identifier = [(MVChatUser *)[self sender] uniqueIdentifier];
465         } else if( [[self sender] isKindOfClass:[JVChatRoomMember class]] ) {
466                 identifier = [[(JVChatRoomMember *)[self sender] user] uniqueIdentifier];
467         }
468
469         if( [identifier isKindOfClass:[NSData class]] )
470                 identifier = [identifier base64Encoding];
471
472         return ( identifier ? identifier : [super senderIdentifier] );
473 }
474
475 - (NSString *) senderNickname {
476         if( [[self sender] respondsToSelector:@selector( nickname )] )
477                 return [[self sender] nickname];
478         return [super senderNickname];
479 }
480
481 - (NSString *) senderHostmask {
482         if( [[self sender] respondsToSelector:@selector( hostmask )] )
483                 return [[self sender] hostmask];
484         if( [[self sender] isKindOfClass:[MVChatUser class]] )
485                 return [NSString stringWithFormat:@"%@@%@", [(MVChatUser *)[self sender] username], [(MVChatUser *)[self sender] address]];
486         return [super senderNickname];
487 }
488
489 - (NSString *) senderClass {
490         if( [[self sender] isKindOfClass:[JVChatRoomMember class]] ) {
491                 if( [(JVChatRoomMember *)[self sender] serverOperator] ) return @"server operator";
492                 else if( [(JVChatRoomMember *)[self sender] roomFounder] ) return @"room founder";
493                 else if( [(JVChatRoomMember *)[self sender] roomAdministrator] ) return @"room administrator";
494                 else if( [(JVChatRoomMember *)[self sender] operator] ) return @"operator";
495                 else if( [(JVChatRoomMember *)[self sender] halfOperator] ) return @"half operator";
496                 else if( [(JVChatRoomMember *)[self sender] voice] ) return @"voice";
497         } else if( [[self sender] isKindOfClass:[MVChatUser class]] ) {
498                 if( [(MVChatUser *)[self sender] isServerOperator] ) return @"server operator";
499         }
500
501         return [super senderClass];
502 }
503
504 - (NSString *) senderBuddyIdentifier {
505         if( [[self sender] isKindOfClass:[JVChatRoomMember class]] )
506                 return [[[self sender] buddy] uniqueIdentifier];
507         return [super senderBuddyIdentifier];
508 }
509
510 - (BOOL) senderIsLocalUser {
511         if( [[self sender] respondsToSelector:@selector( isLocalUser )] )
512                 return [[self sender] isLocalUser];
513         return [super senderIsLocalUser];
514 }
515
516 #pragma mark -
517
518 - (void) setBody:(id) message {
519         [self _setNode:NULL];
520         if( ! _attributedMessage ) {
521                 if( [message isKindOfClass:[NSTextStorage class]] ) _attributedMessage = [message mutableCopyWithZone:nil];
522                 else if( [message isKindOfClass:[NSAttributedString class]] ) _attributedMessage = [[NSTextStorage allocWithZone:nil] initWithAttributedString:message];
523                 else if( [message isKindOfClass:[NSString class]] ) _attributedMessage = [[NSTextStorage allocWithZone:nil] initWithString:(NSString *)message];
524         } else if( _attributedMessage && [message isKindOfClass:[NSAttributedString class]] ) {
525                 [_attributedMessage setAttributedString:message];
526         } else if( _attributedMessage && [message isKindOfClass:[NSString class]] ) {
527                 id string = [[NSAttributedString allocWithZone:nil] initWithString:(NSString *)message];
528                 [_attributedMessage setAttributedString:string];
529                 [string release];
530         }
531 }
532
533 - (void) setBodyAsPlainText:(NSString *) message {
534         [self setBody:message];
535 }
536
537 - (void) setBodyAsHTML:(NSString *) message {
538         [self setBody:[NSAttributedString attributedStringWithXHTMLFragment:message baseURL:nil defaultAttributes:nil]];
539 }
540
541 #pragma mark -
542
543 - (void) setAction:(BOOL) action {
544         [self _setNode:NULL];
545         _action = action;
546 }
547
548 - (void) setHighlighted:(BOOL) highlighted {
549         [self _setNode:NULL];
550         _highlighted = highlighted;
551 }
552
553 - (void) setIgnoreStatus:(JVIgnoreMatchResult) ignoreStatus {
554         [self _setNode:NULL];
555         _ignoreStatus = ignoreStatus;
556 }
557
558 - (void) setType:(JVChatMessageType) type {
559         [self _setNode:NULL];
560         _type = type;
561 }
562
563 #pragma mark -
564
565 - (void) setSource:(NSURL *) source {
566         [self _setNode:NULL];
567         id old = _source;
568         _source = [source copyWithZone:[self zone]];
569         [old release];
570 }
571
572 - (void) setMessageIdentifier:(NSString *) identifier {
573         [self _setNode:NULL];
574         id old = _messageIdentifier;
575         _messageIdentifier = [identifier copyWithZone:[self zone]];
576         [old release];
577 }
578
579 - (NSMutableDictionary *) attributes {
580         // This depends on the implementation of JVChatMessage using an NSMutableDictionary for its attributes, and actually returning a mutable version of it.
581         return (NSMutableDictionary *)[super attributes];
582 }
583
584 - (void) setAttributes:(NSDictionary *) attributes {
585         [self _setNode:NULL];
586         id old = _attributes;
587         _attributes = [attributes mutableCopyWithZone:[self zone]];
588         [old release];
589 }
590
591 - (void) setAttribute:(id) object forKey:(id) key {
592         if( ! _attributes )
593                 _attributes = [[NSMutableDictionary alloc] init];
594         [_attributes setObject:object forKey:key];
595 }
596
597 #pragma mark -
598
599 - (void) setValue:(id) value forUndefinedKey:(NSString *) key {
600         if( [NSScriptCommand currentCommand] ) {
601                 [[NSScriptCommand currentCommand] setScriptErrorNumber:1000];
602                 [[NSScriptCommand currentCommand] setScriptErrorString:[NSString stringWithFormat:@"The \"%@\" property of message id %@ is read only.", key, [self messageIdentifier]]];
603                 return;
604         }
605
606         [super setValue:value forUndefinedKey:key];
607 }
608 @end
Note: See TracBrowser for help on using the browser.