root/tags/2C11/JVChatController.m

Revision 1885, 44.6 kB (checked in by timothy, 4 years ago)

Removed the "room" user-info key requirement on MVChatConnectionUserNicknameChangedNotification. The propagation of the change is up to the application (if the user is in multiple rooms).

Line 
1 #import <ChatCore/MVChatConnection.h>
2 #import <ChatCore/MVChatScriptPlugin.h>
3 #import <ChatCore/NSAttributedStringAdditions.h>
4
5 #import "JVChatController.h"
6 #import "MVApplicationController.h"
7 #import "MVConnectionsController.h"
8 #import "JVChatWindowController.h"
9 #import "JVTabbedChatWindowController.h"
10 #import "JVNotificationController.h"
11 #import "JVChatTranscript.h"
12 #import "JVDirectChat.h"
13 #import "JVChatRoom.h"
14 #import "JVChatConsole.h"
15 #import "KAIgnoreRule.h"
16
17 #import <libxml/parser.h>
18
19 static JVChatController *sharedInstance = nil;
20
21 @interface JVChatController (JVChatControllerPrivate)
22 - (void) _addWindowController:(JVChatWindowController *) windowController;
23 - (void) _addViewControllerToPreferedWindowController:(id <JVChatViewController>) controller andFocus:(BOOL) focus;
24 @end
25
26 #pragma mark -
27
28 @implementation JVChatController
29 + (JVChatController *) defaultManager {
30         extern JVChatController *sharedInstance;
31         if( ! sharedInstance && [MVApplicationController isTerminating] ) return nil;
32         return ( sharedInstance ? sharedInstance : ( sharedInstance = [[self alloc] init] ) );
33 }
34
35 #pragma mark -
36
37 - (id) init {
38         if( ( self = [super init] ) ) {
39                 _chatWindows = [[NSMutableArray array] retain];
40                 _chatControllers = [[NSMutableArray array] retain];
41                 _ignoreRules = [[NSMutableArray alloc] init];
42
43                 NSEnumerator *permanentRulesEnumerator = [[[NSUserDefaults standardUserDefaults] objectForKey:@"JVIgnoreRules"] objectEnumerator];
44                 NSData *archivedRule = nil;
45                 while( ( archivedRule = [permanentRulesEnumerator nextObject] ) )
46                         [_ignoreRules addObject:[NSKeyedUnarchiver unarchiveObjectWithData:archivedRule]];
47
48                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _joinedRoom: ) name:MVChatConnectionJoinedRoomNotification object:nil];
49                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _leftRoom: ) name:MVChatConnectionLeftRoomNotification object:nil];
50                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _existingRoomMembers: ) name:MVChatConnectionRoomExistingMemberListNotification object:nil];
51                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _joinWhoList: ) name:MVChatConnectionGotJoinWhoListNotification object:nil];
52                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberJoinedRoom: ) name:MVChatConnectionUserJoinedRoomNotification object:nil];
53                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberLeftRoom: ) name:MVChatConnectionUserLeftRoomNotification object:nil];
54                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberQuit: ) name:MVChatConnectionUserQuitNotification object:nil];
55                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberInvitedToRoom: ) name:MVChatConnectionInvitedToRoomNotification object:nil];
56                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberNicknameChanged: ) name:MVChatConnectionUserNicknameChangedNotification object:nil];
57                
58                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberModeChanged: ) name:MVChatConnectionGotMemberModeNotification object:nil];
59                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _memberKicked: ) name:MVChatConnectionUserKickedFromRoomNotification object:nil];
60                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _kickedFromRoom: ) name:MVChatConnectionKickedFromRoomNotification object:nil];
61                
62                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _newBan: ) name:MVChatConnectionNewBanNotification object:nil];
63                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _removedBan: ) name:MVChatConnectionRemovedBanNotification object:nil];
64                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _banlistReceived: ) name:MVChatConnectionBanlistReceivedNotification object:nil];
65
66                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _gotPrivateMessage: ) name:MVChatConnectionGotPrivateMessageNotification object:nil];
67                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _gotRoomMessage: ) name:MVChatConnectionGotRoomMessageNotification object:nil];
68                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _roomTopicChanged: ) name:MVChatConnectionGotRoomTopicNotification object:nil];
69         }
70         return self;
71 }
72
73 - (void) dealloc {
74         extern JVChatController *sharedInstance;
75
76         [[NSNotificationCenter defaultCenter] removeObserver:self];
77         if( self == sharedInstance ) sharedInstance = nil;
78
79         [_ignoreRules release];
80         [_chatWindows release];
81         [_chatControllers release];
82
83         _chatWindows = nil;
84         _chatControllers = nil;
85
86         [super dealloc];
87 }
88
89 #pragma mark -
90
91 - (NSSet *) allChatWindowControllers {
92         return [[[NSSet setWithArray:_chatWindows] retain] autorelease];
93 }
94
95 - (JVChatWindowController *) newChatWindowController {
96         JVChatWindowController *windowController = nil;
97         if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVUseTabbedWindows"] )
98                 windowController = [[[JVTabbedChatWindowController alloc] init] autorelease];
99         else windowController = [[[JVChatWindowController alloc] init] autorelease];
100         [self _addWindowController:windowController];
101         [windowController showWindow:nil];
102         return [[windowController retain] autorelease];
103 }
104
105 - (void) disposeChatWindowController:(JVChatWindowController *) controller {
106         NSParameterAssert( controller != nil );
107         NSAssert1( [_chatWindows containsObject:controller], @"%@ is not a member of chat controller.", controller );
108
109         id view = nil;
110         NSEnumerator *enumerator = [[controller allChatViewControllers] objectEnumerator];
111         while( ( view = [enumerator nextObject] ) )
112                 [self disposeViewController:view];
113
114         [_chatWindows removeObject:controller];
115 }
116
117 #pragma mark -
118
119 - (NSSet *) allChatViewControllers {
120         return [[[NSSet setWithArray:_chatControllers] retain] autorelease];
121 }
122
123 - (NSSet *) chatViewControllersWithConnection:(MVChatConnection *) connection {
124         NSMutableSet *ret = [NSMutableSet set];
125         id <JVChatViewController> item = nil;
126         NSEnumerator *enumerator = nil;
127
128         NSParameterAssert( connection != nil );
129
130         enumerator = [_chatControllers objectEnumerator];
131         while( ( item = [enumerator nextObject] ) )
132                 if( [item connection] == connection )
133                         [ret addObject:item];
134
135         return [[ret retain] autorelease];
136 }
137
138 - (NSSet *) chatViewControllersOfClass:(Class) class {
139         NSMutableSet *ret = [NSMutableSet set];
140         id <JVChatViewController> item = nil;
141         NSEnumerator *enumerator = nil;
142
143         NSParameterAssert( class != NULL );
144
145         enumerator = [_chatControllers objectEnumerator];
146         while( ( item = [enumerator nextObject] ) )
147                 if( [item isMemberOfClass:class] )
148                         [ret addObject:item];
149
150         return [[ret retain] autorelease];
151 }
152
153 - (NSSet *) chatViewControllersKindOfClass:(Class) class {
154         NSMutableSet *ret = [NSMutableSet set];
155         id <JVChatViewController> item = nil;
156         NSEnumerator *enumerator = nil;
157
158         NSParameterAssert( class != NULL );
159
160         enumerator = [_chatControllers objectEnumerator];
161         while( ( item = [enumerator nextObject] ) )
162                 if( [item isKindOfClass:class] )
163                         [ret addObject:item];
164
165         return [[ret retain] autorelease];
166 }
167
168 - (JVChatRoom *) chatViewControllerForRoom:(NSString *) room withConnection:(MVChatConnection *) connection ifExists:(BOOL) exists {
169         id <JVChatViewController> ret = nil;
170         NSEnumerator *enumerator = nil;
171
172         NSParameterAssert( room != nil );
173         NSParameterAssert( connection != nil );
174
175         enumerator = [_chatControllers objectEnumerator];
176         while( ( ret = [enumerator nextObject] ) )
177                 if( [ret isMemberOfClass:[JVChatRoom class]] && [ret connection] == connection && [[(JVChatRoom *)ret target] caseInsensitiveCompare:room] == NSOrderedSame )
178                         break;
179
180         if( ! ret && ! exists ) {
181                 if( ( ret = [[[JVChatRoom alloc] initWithTarget:room forConnection:connection] autorelease] ) ) {
182                         [_chatControllers addObject:ret];
183                         [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
184                 }
185         }
186
187         return [[ret retain] autorelease];
188 }
189
190 - (JVDirectChat *) chatViewControllerForUser:(NSString *) user withConnection:(MVChatConnection *) connection ifExists:(BOOL) exists {
191         return [self chatViewControllerForUser:user withConnection:connection ifExists:exists userInitiated:YES];
192 }
193
194 - (JVDirectChat *) chatViewControllerForUser:(NSString *) user withConnection:(MVChatConnection *) connection ifExists:(BOOL) exists userInitiated:(BOOL) initiated {
195         id <JVChatViewController> ret = nil;
196         NSEnumerator *enumerator = nil;
197
198         NSParameterAssert( user != nil );
199         NSParameterAssert( connection != nil );
200
201         enumerator = [_chatControllers objectEnumerator];
202         while( ( ret = [enumerator nextObject] ) )
203                 if( [ret isMemberOfClass:[JVDirectChat class]] && [ret connection] == connection && [[(JVDirectChat *)ret target] caseInsensitiveCompare:user] == NSOrderedSame )
204                         break;
205
206         if( ! ret && ! exists ) {
207                 if( ( ret = [[[JVDirectChat alloc] initWithTarget:user forConnection:connection] autorelease] ) ) {
208                         [_chatControllers addObject:ret];
209                         [self _addViewControllerToPreferedWindowController:ret andFocus:initiated];
210                 }
211         }
212
213         return [[ret retain] autorelease];
214 }
215
216 - (JVChatTranscript *) chatViewControllerForTranscript:(NSString *) filename {
217         id <JVChatViewController> ret = nil;
218         if( ( ret = [[[JVChatTranscript alloc] initWithTranscript:filename] autorelease] ) ) {
219                 [_chatControllers addObject:ret];
220                 [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
221         }
222         return [[ret retain] autorelease];
223 }
224
225 - (JVChatConsole *) chatConsoleForConnection:(MVChatConnection *) connection ifExists:(BOOL) exists {
226         id <JVChatViewController> ret = nil;
227         NSEnumerator *enumerator = nil;
228
229         NSParameterAssert( connection != nil );
230
231         enumerator = [_chatControllers objectEnumerator];
232         while( ( ret = [enumerator nextObject] ) )
233                 if( [ret isMemberOfClass:[JVChatConsole class]] && [ret connection] == connection )
234                         break;
235
236         if( ! ret && ! exists ) {
237                 if( ( ret = [[[JVChatConsole alloc] initWithConnection:connection] autorelease] ) ) {
238                         [_chatControllers addObject:ret];
239                         [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
240                 }
241         }
242
243         return [[ret retain] autorelease];
244 }
245
246 #pragma mark -
247
248 - (void) disposeViewController:(id <JVChatViewController>) controller {
249         NSParameterAssert( controller != nil );
250         NSAssert1( [_chatControllers containsObject:controller], @"%@ is not a member of chat controller.", controller );
251         if( [controller respondsToSelector:@selector( willDispose )] )
252                 [(NSObject *)controller willDispose];
253         [[controller windowController] removeChatViewController:controller];
254         [_chatControllers removeObject:controller];
255 }
256
257 - (void) detachViewController:(id <JVChatViewController>) controller {
258         NSParameterAssert( controller != nil );
259         NSAssert1( [_chatControllers containsObject:controller], @"%@ is not a member of chat controller.", controller );
260
261         [[controller retain] autorelease];
262
263         JVChatWindowController *windowController = [self newChatWindowController];
264         [[controller windowController] removeChatViewController:controller];
265         [windowController addChatViewController:controller];
266 }
267
268 #pragma mark -
269
270 - (IBAction) detachView:(id) sender {
271         id <JVChatViewController> view = [sender representedObject];
272         if( ! view ) return;
273         [self detachViewController:view];
274 }
275
276 #pragma mark -
277 #pragma mark Ignores
278
279 - (JVIgnoreMatchResult) shouldIgnoreUser:(NSString *) name withMessage:(NSAttributedString *) message inView:(id <JVChatViewController>) view {
280         JVIgnoreMatchResult ignoreResult = JVNotIgnored;
281         NSEnumerator *renum = [[[MVConnectionsController defaultManager] ignoreRulesForConnection:[view connection]] objectEnumerator];
282         KAIgnoreRule *rule = nil;
283
284         while( ( ignoreResult == JVNotIgnored ) && ( ( rule = [renum nextObject] ) ) )
285                 ignoreResult = [rule matchUser:name message:[message string] inView:view];
286
287         return ignoreResult;
288 }
289 @end
290
291 #pragma mark -
292
293 @implementation JVChatController (JVChatControllerPrivate)
294 - (void) _joinedRoom:(NSNotification *) notification {
295         JVChatConsole *console = [[JVChatController defaultManager] chatConsoleForConnection:[notification object] ifExists:YES];
296         [console pause];
297
298         JVChatRoom *room = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:NO];
299         [room joined];
300 }
301
302 - (void) _leftRoom:(NSNotification *) notification {
303         if( ! [[notification object] isConnected] ) return;
304         JVChatRoom *room = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
305         if( ! room ) return;
306         [room parting];
307         if( ! [room keepAfterPart] )
308                 [self disposeViewController:room];
309 }
310
311 - (void) _existingRoomMembers:(NSNotification *) notification {
312         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
313         [controller addExistingMembersToChat:[[notification userInfo] objectForKey:@"members"]];
314 }
315
316 - (void) _joinWhoList:(NSNotification *) notification {
317         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
318         [controller addWhoInformationToMembers:[[notification userInfo] objectForKey:@"list"]];
319 }
320
321 - (void) _memberJoinedRoom:(NSNotification *) notification {
322         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
323         [controller addMemberToChat:[[notification userInfo] objectForKey:@"who"] withInformation:[[notification userInfo] objectForKey:@"info"]];
324 }
325
326 - (void) _memberLeftRoom:(NSNotification *) notification {
327         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
328         [controller removeChatMember:[[notification userInfo] objectForKey:@"who"] withReason:[[notification userInfo] objectForKey:@"reason"]];
329 }
330
331 - (void) _memberQuit:(NSNotification *) notification {
332         NSString *who = [[notification userInfo] objectForKey:@"who"];
333         NSEnumerator *enumerator = [[self chatViewControllersWithConnection:[notification object]] objectEnumerator];
334         id controller = nil;
335        
336         while( ( controller = [enumerator nextObject] ) ) {
337                 if( [controller isKindOfClass:[JVChatRoom class]] ) {
338                         [controller removeChatMember:[[notification userInfo] objectForKey:@"who"] withReason:[[notification userInfo] objectForKey:@"reason"]];
339                 } else if( [controller isKindOfClass:[JVDirectChat class]] && [[controller target] isEqualToString:who] ) {
340                         // [controller unavailable];
341                 }
342         }
343 }
344
345 - (void) _memberInvitedToRoom:(NSNotification *) notification {
346         NSString *room = [[notification userInfo] objectForKey:@"room"];
347         NSString *by = [[notification userInfo] objectForKey:@"from"];
348         MVChatConnection *connection = [notification object];
349
350         NSString *title = NSLocalizedString( @"Chat Room Invite", "member invited to room title" );
351         NSString *message = [NSString stringWithFormat:NSLocalizedString( @"You were invited to join %@ by %@. Would you like to accept this invitation and join this room?", "you were invited to join a chat room status message" ), room, by];
352
353         // This should not be modal. Fix sometime.
354         if( NSRunInformationalAlertPanel( title, message, NSLocalizedString( @"Join", "join button" ), NSLocalizedString( @"Decline", "decline button" ), nil ) == NSOKButton )
355                 [connection joinChatRoom:room];
356
357         NSMutableDictionary *context = [NSMutableDictionary dictionary];
358         [context setObject:NSLocalizedString( @"Invited to Chat", "bubble title invited to room" ) forKey:@"title"];
359         [context setObject:[NSString stringWithFormat:NSLocalizedString( @"You were invited to %@ by %@.", "bubble message invited to room" ), room, by] forKey:@"description"];
360         [[JVNotificationController defaultManager] performNotification:@"JVChatRoomInvite" withContextInfo:context];
361 }
362
363 - (void) _memberNicknameChanged:(NSNotification *) notification {
364         MVChatConnection *connection = [notification object];
365         NSEnumerator *enumerator = [[self chatViewControllersOfClass:[JVChatRoom class]] objectEnumerator];
366         JVChatRoom *room = nil;
367
368         while( ( room = [enumerator nextObject] ) ) {
369                 if( [room connection] == connection ) {
370                         [room changeChatMember:[[notification userInfo] objectForKey:@"oldNickname"] to:[[notification userInfo] objectForKey:@"newNickname"]];
371                 }
372         }
373
374         JVDirectChat *chat = [self chatViewControllerForUser:[[notification userInfo] objectForKey:@"oldNickname"] withConnection:[notification object] ifExists:YES];
375         [chat setTarget:[[notification userInfo] objectForKey:@"newNickname"]];
376 }
377
378 - (void) _memberModeChanged:(NSNotification *) notification {
379         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
380         BOOL enabled = [[[notification userInfo] objectForKey:@"enabled"] boolValue];
381         MVChatMemberMode mode = [[[notification userInfo] objectForKey:@"mode"] unsignedIntValue];
382
383         if( enabled && mode == MVChatMemberOperatorMode ) {
384                 [controller promoteChatMember:[[notification userInfo] objectForKey:@"who"] by:[[notification userInfo] objectForKey:@"by"]];
385         } else if( ! enabled && mode == MVChatMemberOperatorMode ) {
386                 [controller demoteChatMember:[[notification userInfo] objectForKey:@"who"] by:[[notification userInfo] objectForKey:@"by"]];
387         } else if( enabled && mode == MVChatMemberVoiceMode ) {
388                 [controller voiceChatMember:[[notification userInfo] objectForKey:@"who"] by:[[notification userInfo] objectForKey:@"by"]];
389         } else if( ! enabled && mode == MVChatMemberVoiceMode ) {
390                 [controller devoiceChatMember:[[notification userInfo] objectForKey:@"who"] by:[[notification userInfo] objectForKey:@"by"]];
391         }
392 }
393
394 - (void) _memberKicked:(NSNotification *) notification {
395         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
396         [controller chatMember:[[notification userInfo] objectForKey:@"who"] kickedBy:[[notification userInfo] objectForKey:@"by"] forReason:[[notification userInfo] objectForKey:@"reason"]];
397 }
398
399 - (void) _kickedFromRoom:(NSNotification *) notification {
400         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
401         [controller kickedFromChatBy:[[notification userInfo] objectForKey:@"by"] forReason:[[notification userInfo] objectForKey:@"reason"]];
402 }
403
404 - (void) _newBan:(NSNotification *) notification {
405         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
406         [controller newBan:[[notification userInfo] objectForKey:@"ban"] by:[[notification userInfo] objectForKey:@"by"]];
407 }
408
409 - (void) _removedBan:(NSNotification *) notification {
410         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
411         [controller removedBan:[[notification userInfo] objectForKey:@"ban"] by:[[notification userInfo] objectForKey:@"by"]];
412 }
413
414 - (void) _banlistReceived:(NSNotification *) notification {
415         JVChatConsole *console = [[JVChatController defaultManager] chatConsoleForConnection:[notification object] ifExists:YES];
416         [console resume];
417        
418         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
419         [controller banlistReceived];
420 }
421
422 - (void) _gotPrivateMessage:(NSNotification *) notification {
423         BOOL hideFromUser = NO;
424         NSString *user = [[notification userInfo] objectForKey:@"from"];
425         NSData *message = [[notification userInfo] objectForKey:@"message"];
426
427         if( [[[notification userInfo] objectForKey:@"auto"] boolValue] ) {
428                 MVChatConnection *connection = [notification object];
429
430                 if( ! [self chatViewControllerForUser:user withConnection:connection ifExists:YES] )
431                         hideFromUser = YES;
432
433                 if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatAlwaysShowNotices"] )
434                         hideFromUser = NO;
435
436                 if( [user isEqualToString:@"NickServ"] || [user isEqualToString:@"MemoServ"] ) {
437                         NSMutableDictionary *options = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithUnsignedInt:[connection encoding]], @"StringEncoding", [NSNumber numberWithBool:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatStripMessageColors"]], @"IgnoreFontColors", [NSNumber numberWithBool:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatStripMessageFormatting"]], @"IgnoreFontTraits", [NSFont systemFontOfSize:11.], @"BaseFont", nil];
438                         NSAttributedString *messageString = [NSAttributedString attributedStringWithIRCFormat:message options:options];
439                         if( ! messageString ) {
440                                 [options setObject:[NSNumber numberWithUnsignedInt:[NSString defaultCStringEncoding]] forKey:@"StringEncoding"];
441                                 messageString = [NSAttributedString attributedStringWithIRCFormat:message options:options];
442                         }
443
444                         if( [user isEqualToString:@"NickServ"] ) {
445                                 if( [[messageString string] rangeOfString:@"password accepted" options:NSCaseInsensitiveSearch].location != NSNotFound ) {
446                                         NSMutableDictionary *context = [NSMutableDictionary dictionary];
447                                         [context setObject:NSLocalizedString( @"You Have Been Identified", "identified bubble title" ) forKey:@"title"];
448                                         [context setObject:[NSString stringWithFormat:@"%@ on %@", [messageString string], [connection server]] forKey:@"description"];
449                                         [context setObject:[NSImage imageNamed:@"Keychain"] forKey:@"image"];
450                                         [[JVNotificationController defaultManager] performNotification:@"JVNickNameIdentifiedWithServer" withContextInfo:context];
451                                 }
452                         } else if( [user isEqualToString:@"MemoServ"] ) {
453                                 if( [[messageString string] rangeOfString:@"new memo" options:NSCaseInsensitiveSearch].location != NSNotFound && [[messageString string] rangeOfString:@" no " options:NSCaseInsensitiveSearch].location == NSNotFound ) {
454                                         NSMutableDictionary *context = [NSMutableDictionary dictionary];
455                                         [context setObject:NSLocalizedString( @"You Have New Memos", "new memos bubble title" ) forKey:@"title"];
456                                         [context setObject:messageString forKey:@"description"];
457                                         [context setObject:[NSImage imageNamed:@"Stickies"] forKey:@"image"];
458                                         [context setObject:self forKey:@"target"];
459                                         [context setObject:NSStringFromSelector( @selector( _checkMemos: ) ) forKey:@"action"];
460                                         [context setObject:connection forKey:@"representedObject"];
461                                         [[JVNotificationController defaultManager] performNotification:@"JVNewMemosFromServer" withContextInfo:context];
462                                 }       
463                         }
464                 }
465         }
466
467         if( ! hideFromUser && ( [self shouldIgnoreUser:user withMessage:nil inView:nil] == JVNotIgnored ) ) {
468                 JVDirectChat *controller = [self chatViewControllerForUser:user withConnection:[notification object] ifExists:NO userInitiated:NO];
469                 [controller addMessageToDisplay:message fromUser:user asAction:[[[notification userInfo] objectForKey:@"action"] boolValue]];
470         }
471 }
472
473 - (void) _gotRoomMessage:(NSNotification *) notification {
474         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
475         [controller addMessageToDisplay:[[notification userInfo] objectForKey:@"message"] fromUser:[[notification userInfo] objectForKey:@"from"] asAction:[[[notification userInfo] objectForKey:@"action"] boolValue]];
476 }
477
478 - (void) _roomTopicChanged:(NSNotification *) notification {
479         JVChatRoom *controller = [self chatViewControllerForRoom:[[notification userInfo] objectForKey:@"room"] withConnection:[notification object] ifExists:YES];
480         id author = [[notification userInfo] objectForKey:@"author"];
481         if( [author isMemberOfClass:[NSNull class]] ) author = nil;
482         [controller changeTopic:[[notification userInfo] objectForKey:@"topic"] by:author displayChange:( ! [[[notification userInfo] objectForKey:@"justJoined"] boolValue] )];
483 }
484
485 - (void) _addWindowController:(JVChatWindowController *) windowController {
486         [_chatWindows addObject:windowController];
487 }
488
489 - (void) _addViewControllerToPreferedWindowController:(id <JVChatViewController>) controller andFocus:(BOOL) focus {
490         JVChatWindowController *windowController = nil;
491         id <JVChatViewController> viewController = nil;
492         Class modeClass = NULL;
493         NSEnumerator *enumerator = nil;
494         BOOL kindOfClass = NO;
495
496         NSParameterAssert( controller != nil );
497
498         int mode = [[NSUserDefaults standardUserDefaults] integerForKey:[NSStringFromClass( [controller class] ) stringByAppendingString:@"PreferredOpenMode"]];
499         BOOL groupByServer = (BOOL) mode & 32;
500         mode &= ~32;
501
502         switch( mode ) {
503         default:
504         case 0:
505                 windowController = nil;
506                 break;
507         case 1:
508                 enumerator = [_chatWindows objectEnumerator];
509                 while( ( windowController = [enumerator nextObject] ) )
510                         if( [[windowController window] isMainWindow] || ! [[NSApplication sharedApplication] isActive] )
511                                 break;
512                 if( ! windowController ) windowController = [_chatWindows lastObject];
513                 break;
514         case 2:
515                 modeClass = [JVChatRoom class];
516                 goto groupByClass;
517         case 3:
518                 modeClass = [JVDirectChat class];
519                 goto groupByClass;
520         case 4:
521                 modeClass = [JVChatTranscript class];
522                 goto groupByClass;
523         case 5:
524                 modeClass = [JVChatConsole class];
525                 goto groupByClass;
526         case 6:
527                 modeClass = [JVDirectChat class];
528                 kindOfClass = YES;
529                 goto groupByClass;
530         groupByClass:
531                 if( groupByServer ) {
532                         if( kindOfClass ) enumerator = [[self chatViewControllersKindOfClass:modeClass] objectEnumerator];
533                         else enumerator = [[self chatViewControllersOfClass:modeClass] objectEnumerator];
534                         while( ( viewController = [enumerator nextObject] ) ) {
535                                 if( controller != viewController && [viewController connection] == [controller connection] ) {
536                                         windowController = [viewController windowController];
537                                         if( windowController ) break;
538                                 }
539                         }
540                 } else {
541                         if( kindOfClass ) enumerator = [[self chatViewControllersKindOfClass:modeClass] objectEnumerator];
542                         else enumerator = [[self chatViewControllersOfClass:modeClass] objectEnumerator];
543                         while( ( viewController = [enumerator nextObject] ) ) {
544                                 if( controller != viewController ) {
545                                         windowController = [viewController windowController];
546                                         if( windowController ) break;
547                                 }
548                         }
549                 }
550                 break;
551         }
552
553         if( ! windowController ) windowController = [self newChatWindowController];
554
555         [windowController addChatViewController:controller];
556         if( focus || [[windowController allChatViewControllers] count] == 1 )
557                 [windowController showChatViewController:controller];
558 }
559
560 - (IBAction) _checkMemos:(id) sender {
561         MVChatConnection *connection = [sender representedObject];
562         NSAttributedString *message = [[[NSAttributedString alloc] initWithString:@"read all"] autorelease];
563         [connection sendMessage:message withEncoding:[connection encoding] toUser:@"MemoServ" asAction:NO];
564         [self chatViewControllerForUser:@"MemoServ" withConnection:connection ifExists:NO];
565 }
566
567 @end
568
569 #pragma mark -
570
571 @implementation MVChatScriptPlugin (MVChatScriptPluginCommandSupport)
572 - (BOOL) processUserCommand:(NSString *) command withArguments:(NSAttributedString *) arguments toConnection:(MVChatConnection *) connection inView:(id <JVChatViewController>) view {
573         NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:command, @"----", [arguments string], @"pcC1", view, @"pcC2", nil];
574         id result = [self callScriptHandler:'pcCX' withArguments:args forSelector:_cmd];
575         return ( [result isKindOfClass:[NSNumber class]] ? [result boolValue] : NO );
576 }
577 @end
578
579 #pragma mark -
580
581 @implementation JVChatWindowController (JVChatWindowControllerObjectSpecifier)
582 - (NSScriptObjectSpecifier *) objectSpecifier {
583         id classDescription = [NSClassDescription classDescriptionForClass:[JVChatController class]];
584         NSScriptObjectSpecifier *container = [[JVChatController defaultManager] objectSpecifier];
585         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"chatWindows" uniqueID:[self uniqueIdentifier]] autorelease];
586 }
587 @end
588
589 #pragma mark -
590
591 @implementation JVChatTranscript (JVChatTranscriptObjectSpecifier)
592 - (NSScriptObjectSpecifier *) objectSpecifier {
593         id classDescription = [NSClassDescription classDescriptionForClass:[JVChatController class]];
594         NSScriptObjectSpecifier *container = [[JVChatController defaultManager] objectSpecifier];
595         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"chatViews" uniqueID:[self uniqueIdentifier]] autorelease];
596 }
597 @end
598
599 #pragma mark -
600
601 @implementation JVChatConsole (JVChatConsoleObjectSpecifier)
602 - (NSScriptObjectSpecifier *) objectSpecifier {
603         id classDescription = [NSClassDescription classDescriptionForClass:[JVChatController class]];
604         NSScriptObjectSpecifier *container = [[JVChatController defaultManager] objectSpecifier];
605         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"chatViews" uniqueID:[self uniqueIdentifier]] autorelease];
606 }
607 @end
608
609 #pragma mark -
610
611 @implementation JVChatController (JVChatControllerScripting)
612 - (JVChatWindowController *) newChatWindowScriptCommand:(NSScriptCommand *) command {
613         return [self newChatWindowController];
614 }
615
616 - (void) startChatScriptCommand:(NSScriptCommand *) command {
617         MVChatConnection *connection = [[command evaluatedArguments] objectForKey:@"connection"];
618         NSString *user = [[command evaluatedArguments] objectForKey:@"user"];
619
620         if( ! [user length] ) {
621                 [NSException raise:NSInvalidArgumentException format:@"Invalid user nickname."];
622                 return;
623         }
624
625         if( ! connection || ! [connection isConnected] ) {
626                 [NSException raise:NSInvalidArgumentException format:@"Invalid conenction or it is not connected."];
627                 return;
628         }
629
630         [self chatViewControllerForUser:user withConnection:connection ifExists:NO];
631 }
632
633 #pragma mark -
634
635 - (NSArray *) chatWindows {
636         return _chatWindows;
637 }
638
639 - (JVChatWindowController *) valueInChatWindowsAtIndex:(unsigned) index {
640         return [_chatWindows objectAtIndex:index];
641 }
642
643 - (JVChatWindowController *) valueInChatWindowsWithUniqueID:(id) identifier {
644         NSEnumerator *enumerator = [_chatWindows objectEnumerator];
645         JVChatWindowController *window = nil;
646
647         while( ( window = [enumerator nextObject] ) )
648                 if( [[window uniqueIdentifier] isEqual:identifier] )
649                         return window;
650
651         return nil;
652 }
653
654 - (void) addInChatWindows:(JVChatWindowController *) window {
655         [self _addWindowController:window];
656 }
657
658 - (void) insertInChatWindows:(JVChatWindowController *) window {
659         [self _addWindowController:window];
660 }
661
662 - (void) insertInChatWindows:(JVChatWindowController *) window atIndex:(unsigned) index {
663         [self _addWindowController:window];
664 }
665
666 - (void) removeFromChatWindowsAtIndex:(unsigned) index {
667         JVChatWindowController *window = [[self chatWindows] objectAtIndex:index];
668         [[window window] orderOut:nil];
669         [self disposeChatWindowController:window];
670 }
671
672 - (void) replaceInChatWindows:(JVChatWindowController *) window atIndex:(unsigned) index {
673         [NSException raise:NSOperationNotSupportedForKeyException format:@"Can't replace a chat window."];
674 }
675
676 #pragma mark -
677
678 - (void) raiseCantAddChatViewsException {
679         [NSException raise:NSOperationNotSupportedForKeyException format:@"Can't insert a chat view. Read only."];
680 }
681
682 #pragma mark -
683
684 - (NSArray *) chatViews {
685         return _chatControllers;
686 }
687
688 - (id <JVChatViewController>) valueInChatViewsAtIndex:(unsigned) index {
689         return [_chatControllers objectAtIndex:index];
690 }
691
692 - (id <JVChatViewController>) valueInChatViewsWithUniqueID:(id) identifier {
693         NSEnumerator *enumerator