-
Notifications
You must be signed in to change notification settings - Fork 2
/
FiSHController.m
706 lines (596 loc) · 30.3 KB
/
FiSHController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
// FiSHy, a plugin for Colloquy providing Blowfish encryption.
// Copyright (C) 2007 Henning Kiel
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#import "FiSHController.h"
#import "Models/JVChatMessage.h"
#import "Controllers/JVChatWindowController.h"
#import "Controllers/JVChatController.h"
#import "Panels/JVChatRoomPanel.h"
#import "Panels/JVDirectChatPanel.h"
#import "Panels/JVChatRoomPanel.h"
#import "Chat Core/MVChatUser.h"
#import "Chat Core/MVChatRoom.h"
#import "Chat Core/MVChatConnection.h"
#import "Additions/NSStringAdditions.h"
#import "Models/JVChatTranscript.h"
#import "FiSHSecretStore.h"
#import "FiSHBlowfish.h"
#import "FiSHEncryptionPrefs.h"
#import "NSString+FiSHyExtensions.h"
#import "FiSHMisc.h"
#define FiSHYDummyConnection @"FiSHYDummyConnection"
// Postfix of encrypted messages to mark them visibly for the user.
// TODO: Make this user-configurable.
#define FiSHEncryptedMessageMarker NSLocalizedString(@"Encrypted message", @"Encrypted message")
#define FiSHExpectedEncryptionMarker NSLocalizedString(@"Unencrypted message in encrypted room!", @"Unencrypted message in encrypted room!")
#define FiSHEncryptionOverriddenMarker NSLocalizedString(@"Encryption overridden", @"Encryption overridden")
// Command used to trigger an automatic key exchange. Syntax: /keyx [nick]. If no nick is given, the nick from the current query is used.
NSString *FiSHKeyExchangeCommand = @"keyx";
// Command used to set a key manually. Syntax: /setkey [#channel/nick] newkey. Contrary to keys from automated key exchange, these will be saved to Keychain. If only one argument is given, will use it as key, and will try to deduce the #channel/nick from current view's target.
NSString *FiSHSetKeyCommand = @"setkey";
// Commands to set encryption preference for a chat-room/query
NSString *FiSHPreferEncCommand = @"enableEnc";
NSString *FiSHAvoidEncCommand = @"disableEnc";
// Command to override encryption for a single message
NSString *FiSHOverrideEncCommand = @"+p";
// Command to print out FiSHys version.
NSString *FiSHAboutFiSHyCommand = @"aboutfishy";
// Command to set an encrypted topic.
NSString *FiSHSetEncryptedTopicCommand = @"encTopic";
@interface FiSHController (FiSHyPrivate)
- (void) joinedDirectChat:(JVDirectChatPanel *)directChat;
@end
@implementation FiSHController
#pragma mark MVChatPlugin
- (id)initWithManager:(MVChatPluginManager *) manager;
{
if (self = [super init])
{
DLog(@"Loading FiSHy.");
// Prepare the About Window to use the version as specified in the main bundle
NSBundle *fishyBundle = [NSBundle bundleForClass:[self class]];
NSNib *aboutNib = [[[NSNib alloc] initWithNibNamed:@"AboutDialog" bundle:fishyBundle] autorelease];
[aboutNib instantiateNibWithOwner:self topLevelObjects:nil];
NSString *aboutVersion = [fishyBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
[aboutVersionField_ setStringValue:[NSString stringWithFormat:[aboutVersionField_ stringValue], aboutVersion]];
keyExchanger_ = [[FiSHKeyExchanger alloc] initWithDelegate:self];
urlToConnectionCache_ = [[NSMutableDictionary alloc] init];
blowFisher_ = [[FiSHBlowfish alloc] init];
encPrefs_ = [[FiSHEncryptionPrefs alloc] init];
// Add encryption setting notices to rooms and queries already open when we were loaded, and try to decrypt topics.
NSSet *openChatPanels = [[NSClassFromString(@"JVChatController") defaultController] chatViewControllersKindOfClass:NSClassFromString(@"JVDirectChatPanel")];
NSEnumerator *chatPanelsEnum = [openChatPanels objectEnumerator];
JVDirectChatPanel *aChatPanel = nil;
while ((aChatPanel = [chatPanelsEnum nextObject]))
{
[self joinedDirectChat:aChatPanel];
}
}
return self;
}
- (void)dealloc;
{
[blowFisher_ release];
[urlToConnectionCache_ release];
[keyExchanger_ release];
[super dealloc];
}
#pragma mark Action methods
- (IBAction) showAboutWindow:(id)sender;
{
[aboutWindow_ orderFront:sender];
}
#pragma mark FiSHKeyExchangerDelegate
- (void)sendPrivateMessage:(NSString *)message to:(NSString *)receiver on:(id)connectionURL;
{
MVChatConnection *theConnection = [urlToConnectionCache_ objectForKey:connectionURL];
[theConnection sendRawMessageWithFormat:[NSString stringWithFormat:@"NOTICE %@ :%@", receiver, message]];
}
- (void)outputStatusInformation:(NSString *)statusInfo forContext:(NSString *)chatContext on:(id)connectionURL;
{
MVChatConnection *theConnection = [urlToConnectionCache_ objectForKey:connectionURL];
MVChatUser *theUser = [theConnection chatUserWithUniqueIdentifier:chatContext];
JVDirectChatPanel *thePanel = [[NSClassFromString(@"JVChatController") defaultController] chatViewControllerForUser:theUser
ifExists:NO
userInitiated:NO];
[thePanel addEventMessageToDisplay:statusInfo withName:@"KeyExchangeInfo" andAttributes:nil];
}
- (void)keyExchanger:(FiSHKeyExchanger *)keyExchanger finishedKeyExchangeFor:(NSString *)nickname onConnection:(id)connection succesfully:(BOOL)succesfully;
{
// If the key exchange was succesfull, prefer encryption for that query.
if (succesfully)
{
[self outputStatusInformation:NSLocalizedString(@"Encryption is enabled for this room.", @"Encryption is enabled for this room.") forContext:nickname on:connection];
// TODO: Handle service correclty.
[encPrefs_ setTemporaryPreference:FiSHEncPrefPreferEncrypted forService:nil account:nickname];
}
}
#pragma mark MVIRCChatConnectionPlugin
- (void) processIncomingMessageAsData:(NSMutableData *) message from:(MVChatUser *) sender to:(id) receiver attributes:(NSMutableDictionary *)msgAttributes;
{
// We only support IRC connections.
if (!FiSHIsIRCConnection([sender connection]))
return;
// If we received a notice, it is possible, that it's a key exchange request/response. Let the FiSHKeyExchanger decide this. If it is a key exchange we won't handle it here and return directly.
if ([[msgAttributes objectForKey:@"notice"] boolValue])
{
MVChatConnection *theConnection = (MVChatConnection *)[sender connection];
[urlToConnectionCache_ setObject:theConnection forKey:[[theConnection url] absoluteString]];
BOOL isKeyExchangeMessage = [keyExchanger_ processPrivateMessageAsData:message
from:[sender nickname]
on:[[theConnection url] absoluteString]];
if (isKeyExchangeMessage)
{
// FiSHKeyExchanger handled the notice for us, so make Colloquy ignore it.
[message setLength:0];
return;
}
}
// Get the secret for the current room/nick and connection. If we don't have one, just display the message without changes.
NSString *accountName = nil;
NSString *secret = nil;
// The receiver is either the local user, or a chat room.
if ([receiver isKindOfClass:NSClassFromString(@"MVChatRoom")])
accountName = FiSHNameForChatObject(receiver);
else
accountName = FiSHNameForChatObject(sender);
if (!accountName)
{
DLog(@"Ignoring unsupported chat object type.");
return;
}
// TODO: Handle service/connection correctly.
secret = [[FiSHSecretStore sharedSecretStore] secretForService:nil account:accountName];
if (!secret)
return;
// Try to decrypt the raw encrypted text.
NSData *decryptedData = nil;
FiSHCypherResult decryptionResult = [blowFisher_ decodeData:message intoData:&decryptedData key:secret];
switch (decryptionResult)
{
case FiSHCypherTextCut:
case FiSHCypherSuccess:
[message setData:decryptedData];
[msgAttributes setObject:[NSNumber numberWithBool:YES] forKey:@"decrypted"];
case FiSHCypherBadCharacters:
case FiSHCypherUnknownError:
case FiSHCypherPlainText:
[msgAttributes setObject:[NSNumber numberWithInt:decryptionResult] forKey:@"FiSHyResult"];
break;
default:
DLog(@"Unexpected/unknown blowfish result.");
}
}
- (void) processOutgoingMessageAsData:(NSMutableData *) message to:(id) receiver attributes:(NSDictionary *)msgAttributes;
{
// We only support IRC connection. As we don't know what receiver we get passed here, extra checks are performed.
if (![receiver respondsToSelector:@selector(connection)])
return;
if (!FiSHIsIRCConnection([receiver connection]))
return;
// Check if the user has overridden encryption temporarily.
if ([[msgAttributes objectForKey:@"sendUnencrypted"] boolValue])
return;
// Check if the message is for a target for which encryption is enabled.
if (![[msgAttributes objectForKey:@"shouldEncrypt"] boolValue])
return;
NSString *errorMessage = nil;
// Check if we have a secret for the current room/nick.
NSString *accountName = FiSHNameForChatObject(receiver);
if (!accountName)
{
DLog(@"Ignoring unsupported chat object type.");
return;
}
// TODO: Handle service/connection correctly.
NSString *secret = [[FiSHSecretStore sharedSecretStore] secretForService:nil account:accountName];
if (!secret)
{
errorMessage = NSLocalizedString(@"Encryption is enabled, but you don't have a key. The message was not sent.", @"Encryption is enabled, but you don't have a key. The message was not sent.");
goto bail;
}
// Try to encrypt the raw outgoing message.
// TODO: Should we provide cutting of overlong messages into multiple messages here? Done right, this also would include limiting the rate we send out messages to not get kicked from the server.
NSData *encryptedData = nil;
[blowFisher_ encodeData:message intoData:&encryptedData key:secret];
if (!encryptedData)
{
errorMessage = NSLocalizedString(@"Encrypting the message failed. The message was not sent.", @"Encrypting the message failed. The message was not sent.");
goto bail;
}
[message setData:encryptedData];
return;
bail:
// If encryption failed for some reason, cancel sending the message, and tell the user.
[message setLength:0];
JVDirectChatPanel *thePanel = nil;
if ([receiver isKindOfClass:NSClassFromString(@"MVChatRoom")])
thePanel = [[NSClassFromString(@"JVChatController") defaultController] chatViewControllerForRoom:receiver
ifExists:NO];
else
thePanel = [[NSClassFromString(@"JVChatController") defaultController] chatViewControllerForUser:receiver
ifExists:NO
userInitiated:NO];
[thePanel addEventMessageToDisplay:errorMessage withName:@"EncryptionFailedInfo" andAttributes:nil];
return;
}
- (void) processTopicAsData:(NSMutableData *) topic inRoom:(MVChatRoom *) room author:(MVChatUser *) author;
{
// We only support IRC connection.
if (!FiSHIsIRCConnection([room connection]))
return;
// Get the secret for the room and connection. If we don't have one, just display the topic without changes.
NSString *accountName = FiSHNameForChatObject(room);
NSString *secret = nil;
// TODO: Handle service/connection correctly.
secret = [[FiSHSecretStore sharedSecretStore] secretForService:nil account:accountName];
if (!secret)
return;
[room setAttribute:[NSNumber numberWithBool:NO] forKey:@"decryptedTopic"];
// Try to decrypt the raw encrypted topic.
NSData *decryptedTopicData = nil;
FiSHCypherResult decryptionResult = [blowFisher_ decodeData:topic intoData:&decryptedTopicData key:secret];
switch (decryptionResult)
{
case FiSHCypherTextCut:
case FiSHCypherSuccess:
[topic setData:decryptedTopicData];
[room setAttribute:[NSNumber numberWithBool:YES] forKey:@"decryptedTopic"];
case FiSHCypherBadCharacters:
case FiSHCypherUnknownError:
case FiSHCypherPlainText:
[room setAttribute:[NSNumber numberWithInt:decryptionResult] forKey:@"FiSHyResult"];
break;
default:
DLog(@"Unexpected/unknown blowfish result.");
break;
}
}
#pragma mark MVChatPluginDirectChatSupport
/// Called whenever a message gets added to a chat panel.
/**
Based on the attributes the incoming message has, we change its appearance to the user here. For received messages we already had a chance to decrypt it, and if so, have set the @"decrypted" attribute. For messages of the local user the message already went through processOutgoingMessage:inView:, were attributes were set if the message should be encrypted later, or not.
Received messages which were encrypted in a room for which encryption is avoided are marked, as well as unencrypted messages in a room for which encryption is preferred.
Encrypted messages of the local user are marked if sent in a room for which encryption is avoided.
*/
- (void)processIncomingMessage:(JVMutableChatMessage *)message inView:(id <JVChatViewController>)aView;
{
// We only support IRC connection.
if (!FiSHIsIRCConnection([aView connection]))
return;
// Make sure that aView really is a direct chat panel.
JVDirectChatPanel *view = FiSHDirectChatPanelForChatViewController(aView);
if (!view)
{
DLog(@"Ignoring unsupported chat controller type.");
return;
}
// Get the name of the chat object the message is directed at.
NSString *accountName = FiSHNameForChatObject([view target]);
if (!accountName)
{
DLog(@"Ignoring unsupported chat object type.");
return;
}
// TODO: Handle the @"FiSHyResult" attribute.
// Prepare a string to mark the message if it either was encrypted in an encryption-avoiding context or unencrypted in an encryption-preferring context.
// At two places we have to differentiate between received messages and messages from the local user. Received messages can have a @"decrypted"-attribute while local messages can have a @"shouldEncrypt"-attribute. The marker for unencrypted remote messages in an encryption-preferring context is different than for user-overridden encryption for local messages.
NSString *messageMarker = nil;
BOOL hasBeenOrWillBeEncrypted = [message senderIsLocalUser] ? [[message attributeForKey:@"shouldEncrypt"] boolValue] : [[message attributeForKey:@"decrypted"] boolValue];
FiSHEncPrefKey encPref = [encPrefs_ preferenceForService:nil account:accountName];
if (hasBeenOrWillBeEncrypted && encPref == FiSHEncPrefAvoidEncrypted)
{
messageMarker = FiSHEncryptedMessageMarker;
} else if (!hasBeenOrWillBeEncrypted && encPref == FiSHEncPrefPreferEncrypted)
{
messageMarker = [message senderIsLocalUser] ? FiSHEncryptionOverriddenMarker : FiSHExpectedEncryptionMarker;
} else
return;
NSTextStorage *body = [message body];
[body appendAttributedString:[[[NSAttributedString alloc] initWithString:messageMarker
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
[NSSet setWithObjects:@"error", @"encoding", nil], @"CSSClasses",
nil]
] autorelease]];
}
/// Called whenever a message gets sent over a ChatViewController.
/**
This is the first place we get in contact with an outgoing message. Depending on user's preferences regarding the channel/query the message was sent in we set its attributes, so that at the lower layer, in processOutgoingMessageAsData::: we can encrypt it.
This method won't be called for message sent with one of the sendMessage: methods in Chat Core.
*/
- (void)processOutgoingMessage:(JVMutableChatMessage *)message inView:(id <JVChatViewController>)aView;
{
// We only support IRC connection.
if (!FiSHIsIRCConnection([aView connection]))
return;
// Make sure that aView really is a direct chat panel.
JVDirectChatPanel *view = FiSHDirectChatPanelForChatViewController(aView);
if (!view)
{
DLog(@"Ignoring unsupported chat controller type.");
return;
}
// Get the name of the chat object the message is directed at.
NSString *accountName = FiSHNameForChatObject([view target]);
if (!accountName)
{
DLog(@"Ignoring unsupported chat object type.");
return;
}
// Check, if the user prefers encryption for this target. If so, mark the message, so that we can encrypt it later.
// TODO: Handle service.
FiSHEncPrefKey encPref = [encPrefs_ preferenceForService:nil account:accountName];
if (encPref == FiSHEncPrefPreferEncrypted)
{
[message setAttribute:[NSNumber numberWithBool:YES] forKey:@"shouldEncrypt"];
} else if (encPref == FiSHEncPrefAvoidEncrypted)
{
[message setAttribute:[NSNumber numberWithBool:YES] forKey:@"sendUnencrypted"];
}
}
- (void) topicChangedTo:(NSAttributedString *) topic inRoom:(JVChatRoomPanel *) roomPanel by:(JVChatRoomMember *) member;
{
// We only support IRC connection.
if (!FiSHIsIRCConnection([[roomPanel target] connection]))
return;
MVChatRoom *room = [roomPanel target];
if ([[room attributeForKey:@"decryptedTopic"] boolValue])
{
DLog(@"Showing decrypted topic");
}
}
#pragma mark MVChatPluginRoomSupport
- (void) joinedRoom:(JVChatRoomPanel *) roomPanel;
{
// We only support IRC connection.
if (!FiSHIsIRCConnection([[roomPanel target] connection]))
return;
[self joinedDirectChat:roomPanel];
// Try to encrypt the topic. We have to do this here, as processTopicAsData::: isn't called for joined rooms.
MVChatRoom *room = [roomPanel target];
NSData *oldTopic = [room topic];
NSMutableData *mutableTopic = [oldTopic mutableCopy];
[self processTopicAsData:mutableTopic inRoom:room author:[room topicAuthor]];
// Use the private _setTopic here, so we don't change topic-author or date. Use performSelector: so the compiler doesn't complain about unknown methods.
[room performSelector:@selector(_setTopic:) withObject:mutableTopic];
if ([[room attributeForKey:@"decryptedTopic"] boolValue])
{
DLog(@"Showing decrypted topic");
}
}
#pragma mark Command handlers
/// User command to initiate a key exchange
/**
view can be nil, if this command was not typed in a direct chat panel.
*/
- (BOOL)processKeyExchangeCommandWithArguments:(NSAttributedString *)arguments toConnection:(MVChatConnection *)connection inDirectChatPanel:(JVDirectChatPanel *)view;
{
NSString *argumentString = [arguments string];
// If no argument has been given, try to deduce it from the current view. This only works for queries, as key exchange for channels is not supported.
if (!argumentString || [argumentString length] <= 0)
{
if ([[view target] isKindOfClass:NSClassFromString(@"MVChatUser")])
argumentString = [[view target] nickname];
else
{
DLog(@"No argument provided to /keyx, aborting.");
return YES;
}
}
// Check if argument is a channel-name. If so, cancel.
if ([argumentString hasPrefix:@"#"] || [argumentString hasPrefix:@"&"])
{
[view addEventMessageToDisplay:@"Key exchange is not supported for channels" withName:@"KeyExchangeUnsupportedForChannels" andAttributes:nil];
DLog(@"Key exchange is only supported for nicknames, not for channels, aborting.");
return YES;
}
// Everything's fine, start the key exchange.
[urlToConnectionCache_ setObject:connection forKey:[[connection url] absoluteString]];
[keyExchanger_ requestTemporarySecretFor:argumentString onConnection:[[connection url] absoluteString]];
return YES;
}
/// User command to set the key for a room/nick
/**
view can be nil, if this command was not typed in a direct chat panel.
*/
- (BOOL)processSetKeyCommandWithArguments:(NSAttributedString *)arguments toConnection:(MVChatConnection *)connection inDirectChatPanel:(JVDirectChatPanel *)view;
{
NSArray *argumentList = [[arguments string] FiSH_arguments];
NSString *secret = nil;
NSString *account = nil;
// If two arguments, proceed. If one, try to deduce target by current view's target. If anything else, abort.
if (!argumentList || [argumentList count] <= 0)
{
DLog(@"SetKey expects exactly 2 arguments, aborting.");
return YES;
} else if ([argumentList count] == 1)
{
// If only one argument has been given, use that as secret, and try to deduce the account from the current view.
account = FiSHNameForChatObject([view target]);
if (!account)
{
DLog(@"SetKey expects exactly 2 arguments, aborting.");
return YES;
}
secret = [argumentList objectAtIndex:0];
} else if ([argumentList count] == 2)
{
secret = [argumentList objectAtIndex:1];
account = [argumentList objectAtIndex:0];
}
// Everything's fine, set the key.
// TODO: Handle service/connection correctly.
if (![[FiSHSecretStore sharedSecretStore] storeSecret:secret forService:nil account:account isTemporary:NO])
{
[view addEventMessageToDisplay:NSLocalizedString(@"Failed to save the key.", @"Failed to save the key.") withName:@"KeySaveError" andAttributes:nil];
return YES;
}
[view addEventMessageToDisplay:NSLocalizedString(@"Key saved to Keychain.", @"Key saved to Keychain.") withName:@"KeySavedToKeychain" andAttributes:nil];
[encPrefs_ setPreference:FiSHEncPrefPreferEncrypted forService:nil account:account];
[view addEventMessageToDisplay:NSLocalizedString(@"Encryption is enabled for this room.", @"Encryption is enabled for this room.") withName:@"EncryptionEnabledForRoom" andAttributes:nil];
return YES;
}
/// User command to send an unencrypted message to an unencrypted room/nick.
/**
view can be nil, if this command was not typed in a direct chat panel.
*/
- (BOOL)processSendUnecryptedCommandWithArguments:(NSAttributedString *)arguments toConnection:(MVChatConnection *)connection inDirectChatPanel:(JVDirectChatPanel *)view;
{
if (!view)
{
DLog(@"Command only supported in chat windows.");
return YES;
}
JVMutableChatMessage *msg = [[[NSClassFromString(@"JVMutableChatMessage") alloc] initWithText:arguments sender:[connection localUser]] autorelease];
[msg setAttribute:[NSNumber numberWithBool:YES] forKey:@"sendUnencrypted"];
[view echoSentMessageToDisplay:msg];
[view sendMessage:msg];
return YES;
}
/// User command to enable/disable encryption for a room/nick.
/**
view can be nil, if this command was not typed in a direct chat panel.
*/
- (BOOL) processEncryptionPreferenceCommandWithArguments:(NSAttributedString *)arguments toConnection:(MVChatConnection *)connection inDirectChatPanel:(JVDirectChatPanel *)view pref:(FiSHEncPrefKey)encPref;
{
NSArray *argumentList = [[arguments string] FiSH_arguments];
NSString *targetName = nil;
if ([argumentList count] == 1)
{
targetName = [argumentList objectAtIndex:0];
} else if ([argumentList count] == 0 && view)
{
targetName = [[view target] isKindOfClass:NSClassFromString(@"MVChatUser")] ? [[view target] nickname] : [[view target] name];
} else
{
DLog(@"Command expects exactly 1 argument");
return YES;
}
// TODO: Differenciate between services here.
[encPrefs_ setPreference:encPref forService:nil account:targetName];
switch (encPref)
{
case FiSHEncPrefPreferEncrypted:
[view addEventMessageToDisplay:NSLocalizedString(@"Encryption is enabled for this room.", @"Encryption is enabled for this room.") withName:@"EncryptionEnabledForRoom" andAttributes:nil];
break;
case FiSHEncPrefAvoidEncrypted:
[view addEventMessageToDisplay:NSLocalizedString(@"Encryption is disabled for this room.", @"Encryption is disabled for this room.") withName:@"EncryptionDisabledForRoom" andAttributes:nil];
break;
default:
break;
}
return YES;
}
/// User command to set an encrypted topic.
/**
view can be nil, if this command was not typed in a direct chat panel.
*/
- (BOOL)processSetEncryptedTopicCommandWithArguments:(NSAttributedString *)arguments toConnection:(MVChatConnection *)connection inDirectChatPanel:(JVDirectChatPanel *)view;
{
NSArray *argumentList = [[arguments string] FiSH_arguments];
MVChatRoom *room = nil;
NSString *newTopic = nil;
// If two arguments, proceed. If one, try to deduce target by current view's target. If anything else, abort.
if (!argumentList || [argumentList count] <= 0 || [argumentList count] > 2)
{
DLog(@"encTopic expects exactly 2 arguments, aborting.");
return YES;
} else if ([argumentList count] == 1)
{
// If only one argument has been given, use that as the new topic, and try to deduce the room from the current view.
room = [view target];
if (!room || ![room isKindOfClass:NSClassFromString(@"MVChatRoom")])
{
DLog(@"encTopic expects exactly 2 arguments, aborting.");
return YES;
}
newTopic = [argumentList objectAtIndex:0];
} else if ([argumentList count] == 2)
{
room = [connection joinedChatRoomWithName:[argumentList objectAtIndex:0]];
if (!room)
{
DLog(@"encTopic invoked for unknown room. You have to be in a room to set an encrypted topic.");
return YES;
}
newTopic = [argumentList objectAtIndex:1];
}
// Get the key for the room.
// Check if we have a secret for the current room/nick.
// TODO: Handle service/connection correctly.
NSString *theKey = [[FiSHSecretStore sharedSecretStore] secretForService:nil account:FiSHNameForChatObject(room)];
if (!theKey)
{
NSString *errorMessage = NSLocalizedString(@"Trying to set a topic for a room without a key. The topic was not set.", @"Trying to set a topic for a room without a key. The topic was not set.");
DLog(errorMessage);
[view addEventMessageToDisplay:errorMessage withName:@"FiSHy" andAttributes:nil];
return YES;
}
// Everything's fine, encrypt the topic.
NSData *newTopicData = nil;
[blowFisher_ encodeData:[newTopic dataUsingEncoding:[room encoding]] intoData:&newTopicData key:theKey];
newTopic = [[[NSString alloc] initWithData:newTopicData encoding:NSASCIIStringEncoding] autorelease];
[room setTopic:[[[NSAttributedString alloc] initWithString:newTopic] autorelease]];
return YES;
}
#pragma mark MVChatPluginCommandSupport
/// Process user commands.
/**
Called by Colloquy whenever the user types a string which starts with a single /.
*/
- (BOOL)processUserCommand:(NSString *) command withArguments:(NSAttributedString *) arguments toConnection:(MVChatConnection *) connection inView:(id <JVChatViewController>)aView;
{
// We only support IRC connection.
if (!FiSHIsIRCConnection(connection))
return NO;
// If aView is a direct chat panel, cast it to supply it later to the command handlers.
// The only other case here probably are transcript and console panels.
JVDirectChatPanel *view = FiSHDirectChatPanelForChatViewController(aView);
// Check for correct command string. We don't care about case.
if ([command isCaseInsensitiveEqualToString:FiSHKeyExchangeCommand])
return [self processKeyExchangeCommandWithArguments:arguments toConnection:connection inDirectChatPanel:view];
if ([command isCaseInsensitiveEqualToString:FiSHSetKeyCommand])
return [self processSetKeyCommandWithArguments:arguments toConnection:connection inDirectChatPanel:view];
if ([command isCaseInsensitiveEqualToString:FiSHOverrideEncCommand])
return [self processSendUnecryptedCommandWithArguments:arguments toConnection:connection inDirectChatPanel:view];
if ([command isCaseInsensitiveEqualToString:FiSHPreferEncCommand])
return [self processEncryptionPreferenceCommandWithArguments:arguments toConnection:connection inDirectChatPanel:view pref:FiSHEncPrefPreferEncrypted];
if ([command isCaseInsensitiveEqualToString:FiSHAvoidEncCommand])
return [self processEncryptionPreferenceCommandWithArguments:arguments toConnection:connection inDirectChatPanel:view pref:FiSHEncPrefAvoidEncrypted];
if ([command isCaseInsensitiveEqualToString:FiSHSetEncryptedTopicCommand])
return [self processSetEncryptedTopicCommandWithArguments:arguments toConnection:connection inDirectChatPanel:view];
if ([command isCaseInsensitiveEqualToString:FiSHAboutFiSHyCommand])
{
[self showAboutWindow:aView];
return YES;
}
return NO;
}
@end
@implementation FiSHController (FiSHyPrivate)
// TODO: implement the following in colloquy
#pragma mark MVDirectChatSupport (not yet implemented in Colloquy)
// TODO: Move this out of private category when implemented in colloquy.
- (void) joinedDirectChat:(JVDirectChatPanel *)directChat;
{
JVDirectChatPanel *thePanel = [[NSClassFromString(@"JVChatController") defaultController] chatViewControllerForRoom:[directChat target]
ifExists:NO];
if ([encPrefs_ preferenceForService:nil account:[[directChat target] name]] == FiSHEncPrefPreferEncrypted)
[thePanel addEventMessageToDisplay:NSLocalizedString(@"Encryption is enabled for this room.", @"Encryption is enabled for this room.") withName:@"EncryptionEnabledForRoom" andAttributes:nil];
if ([encPrefs_ preferenceForService:nil account:[[directChat target] name]] == FiSHEncPrefAvoidEncrypted)
[thePanel addEventMessageToDisplay:NSLocalizedString(@"Encryption is disabled for this room.", @"Encryption is disabled for this room.") withName:@"EncryptionDisabledForRoom" andAttributes:nil];
}
@end