Never send an autoreply to an IRC message. The server does this for us. Fixes #12621.
5 // Created by Evan Schoenberg on 3/4/06.
6 // Copyright 2006 The Adium Team. All rights reserved.
9 #import "ESIRCAccount.h"
10 #import <Adium/AIHTMLDecoder.h>
11 #import <Adium/AIChat.h>
12 #import <Adium/AIContentMessage.h>
13 #import <Adium/AIListContact.h>
14 #import <Adium/AIMenuControllerProtocol.h>
15 #import "AIMessageViewController.h"
16 #import <AIUtilities/AIMenuAdditions.h>
17 #import <AIUtilities/AIAttributedStringAdditions.h>
18 #import <libpurple/irc.h>
19 #import <libpurple/cmds.h>
20 #import "SLPurpleCocoaAdapter.h"
22 @interface SLPurpleCocoaAdapter ()
23 - (BOOL)attemptPurpleCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat;
26 @interface ESIRCAccount()
27 - (void)sendRawCommand:(NSString *)command;
28 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag;
31 static PurpleConversation *fakeConversation(PurpleAccount *account);
33 @implementation ESIRCAccount
36 * @brief Our explicit formatted UID contains our hostname, so we can differentiate ourself.
38 - (NSString *)explicitFormattedUID
41 return [NSString stringWithFormat:@"%@ (%@)", self.host, self.displayName];
43 return self.displayName;
47 #pragma mark IRC-ism overloads
50 * @brief We always want to autocomplete the UID.
52 - (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat
58 * @brief Use the object ID for password name
60 * We mess around a lot with the UID. This lets it actually save right.
62 - (BOOL)useInternalObjectIDForPasswordName
67 - (BOOL)openChat:(AIChat *)chat
69 chat.hideUserIconAndStatus = YES;
71 return [super openChat:chat];
75 * @brief Open the info inspector when getting info
77 * A user can /whois; we want to display info for this case.
79 - (void)openInspectorForContactInfo:(AIListContact *)theContact
81 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIShowContactInfo" object:theContact];
84 #pragma mark Command handling
86 * @brief We've connected
88 * Send the commands the user wants sent when we do so. Creates a fake conversation to pipe them through.
94 PurpleConversation *conv = fakeConversation(self.purpleAccount);
96 for (NSString *command in [[self preferenceForKey:KEY_IRC_COMMANDS
97 group:GROUP_ACCOUNT_STATUS] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]) {
98 if ([command hasPrefix:@"/"]) {
99 command = [command substringFromIndex:1];
102 command = [command stringByReplacingOccurrencesOfString:@"$me" withString:self.displayName];
104 if (command.length) {
106 PurpleCmdStatus cmdStatus = purple_cmd_do_command(conv, [command UTF8String], [command UTF8String], &error);
108 if (cmdStatus == PURPLE_CMD_STATUS_NOT_FOUND) {
109 // If it's not found, send it as a raw command like we do in chats.
110 [self sendRawCommand:command];
111 } else if (cmdStatus != PURPLE_CMD_STATUS_OK) {
112 // The command failed with something other than "not found" - log it.
113 AILogWithSignature(@"Command (%@) failed: %d - %@", command, cmdStatus, [NSString stringWithUTF8String:error]);
118 // The fakeConversation was allocated; now free it.
121 // Set a fake display name preference since we differ from global always.
122 [self setPreference:[[NSAttributedString stringWithString:@"Adium"] dataRepresentation]
123 forKey:KEY_ACCOUNT_DISPLAY_NAME
124 group:GROUP_ACCOUNT_STATUS];
128 * @brief Send a raw command to the IRC server.
130 - (void)sendRawCommand:(NSString *)command
132 PurpleConnection *connection = purple_account_get_connection(account);
137 const char *quote = [command UTF8String];
138 irc_cmd_quote(connection->proto_data, NULL, NULL, "e);
142 * @brief This creates a fake PurpleConversation
144 * This fake conversation is used for sending purple_cmd_do_command() messages, which requires
145 * a conversation for the command to occur. Free this when finished.
147 * This is taken from irchelper.c, the pidgin plugin.
149 static PurpleConversation *fakeConversation(PurpleAccount *account)
151 PurpleConversation *conv;
153 conv = g_new0(PurpleConversation, 1);
154 conv->type = PURPLE_CONV_TYPE_IM;
155 /* If we use this then the conversation updated signal is fired and
156 * other plugins might start doing things to our conversation, such as
157 * setting data on it which we would then need to free etc. It's easier
158 * just to be more hacky by setting account directly. */
159 /* purple_conversation_set_account(conv, account); */
160 conv->account = account;
165 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
168 NSString *encodedString = nil;
169 BOOL didCommand = [self.purpleAdapter attemptPurpleCommandOnMessage:inContentMessage.message.string
170 fromAccount:(AIAccount *)inContentMessage.source
171 inChat:inContentMessage.chat];
173 NSRange meRange = [inContentMessage.message.string rangeOfString:@"/me " options:NSCaseInsensitiveSearch];
175 if (!didCommand || meRange.location == 0) {
176 if (meRange.location == 0) {
177 inContentMessage.sendContent = NO;
179 /* If we're sending a message on an encryption chat (can this even happen on irc?), we can encode the HTML normally, as links will go through fine.
180 * If we're sending a message normally, IRC will drop the title of any link, so we preprocess it to be in the form "title (link)"
182 encodedString = [AIHTMLDecoder encodeHTML:(inContentMessage.chat.isSecure ? inContentMessage.message : [inContentMessage.message attributedStringByConvertingLinksToURLStrings])
185 includingColorTags:YES
188 closeStyleTagsOnFontChange:YES
192 attachmentsAsText:YES
193 onlyIncludeOutgoingImages:NO
196 allowJavascriptURLs:YES];
199 if (!didCommand && [inContentMessage.message.string hasPrefix:@"/"]) {
200 // Try to send it to the server, if we don't know what it is; definitely don't display.
201 [self sendRawCommand:[inContentMessage.message.string substringFromIndex:1]];
204 return encodedString;
208 #pragma mark Libpurple
209 - (const char *)protocolPlugin
214 - (const char *)purpleAccountName
216 return [[NSString stringWithFormat:@"%@@%@", self.formattedUID, self.host] UTF8String];
219 - (NSString *)defaultUsername
224 - (NSString *)defaultRealname
226 return AILocalizedString(@"Adium User", nil);
229 - (void)configurePurpleAccount
231 [super configurePurpleAccount];
233 purple_account_set_username(self.purpleAccount, self.purpleAccountName);
236 NSString *encoding = [self preferenceForKey:KEY_IRC_ENCODING group:GROUP_ACCOUNT_STATUS] ?: @"UTF-8";
237 purple_account_set_string(self.purpleAccount, "encoding", [encoding UTF8String]);
239 if (![encoding isEqualToString:@"UTF-8"]) {
240 purple_account_set_bool(self.purpleAccount, "autodetect_utf8", TRUE);
244 BOOL useSSL = [[self preferenceForKey:KEY_IRC_USE_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
245 purple_account_set_bool(self.purpleAccount, "ssl", useSSL);
247 // Username (for connecting)
248 NSString *username = [self preferenceForKey:KEY_IRC_USERNAME group:GROUP_ACCOUNT_STATUS] ?: self.defaultUsername;
249 purple_account_set_string(self.purpleAccount, "username", [username UTF8String]);
251 // Realname (for connecting)
252 NSString *realname = [self preferenceForKey:KEY_IRC_REALNAME group:GROUP_ACCOUNT_STATUS] ?: self.defaultRealname;
253 purple_account_set_string(self.purpleAccount, "realname", [realname UTF8String]);
257 * @brief Our display name; either retrieve our current nickname, or return our stored one.
259 - (NSString *)displayName
261 // Try and get the purple display name, since it changes without telling us.
263 PurpleConnection *purpleConnection = purple_account_get_connection(account);
265 if (purpleConnection) {
266 return [NSString stringWithUTF8String:purple_connection_get_display_name(purpleConnection)];
270 return self.formattedUID;
275 * @brief Re-create the chat's join options.
277 - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv
279 NSMutableDictionary *dict = [NSMutableDictionary dictionary];
280 [dict setObject:[NSString stringWithUTF8String:purple_conversation_get_name(conv)] forKey:@"channel"];
281 const char *pass = purple_conversation_get_data(conv, "password");
283 [dict setObject: [NSString stringWithUTF8String:pass] forKey:@"password"];
289 * @brief Should an autoreply be sent to this message?
291 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
296 #pragma mark Server contacts (NickServ, ChanServ)
298 * @brief Sends a raw command to identify for the nickname
300 - (void)identifyForName:(NSString *)name password:(NSString *)inPassword
302 if ([self.host rangeOfString:@"quakenet" options:NSCaseInsensitiveSearch].location != NSNotFound) {
303 [self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG Q@CServe.quakenet.org :AUTH %@ %@", name, inPassword]];
304 } else if ([self.host rangeOfString:@"undernet" options:NSCaseInsensitiveSearch].location != NSNotFound) {
305 [self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG X@channels.undernet.org :LOGIN %@ %@", name, inPassword]];
306 } else if ([self.host rangeOfString:@"gamesurge" options:NSCaseInsensitiveSearch].location != NSNotFound) {
307 [self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG AuthServ@Services.GameSurge.net :AUTH %@ %@", name, inPassword]];
309 [self sendRawCommand:[NSString stringWithFormat:@"NICKSERV identify %@", inPassword]];
314 * @brief Is this contact a server contact?
316 BOOL contactUIDIsServerContact(NSString *contactUID)
318 return (([contactUID caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
319 ([contactUID caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
320 ([contactUID rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound));
324 * @brief Can we send an offline message to this contact?
326 * We can only send offline messages to the server contacts, since such a message might cause us to connect
328 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
330 return contactUIDIsServerContact(inContact.UID);
334 * @brief Don't autoreply to server contacts (services) or FreeNode's stupidity.
336 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
338 return !contactUIDIsServerContact(message.source.UID);
342 * @brief Don't log server contacts (services) or FreeNode's stupidity.
344 - (BOOL)shouldLogChat:(AIChat *)chat
346 NSString *source = chat.listObject.UID;
347 BOOL shouldLog = YES;
349 if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
350 ([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
351 ([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) {
355 return (shouldLog && [super shouldLogChat:chat]);
358 #pragma mark Chat handling
361 * @brief Allow the chat to close unless we're quitting.
363 - (BOOL)closeChat:(AIChat*)chat
368 return [super closeChat:chat];
372 * @brief Do group chats support topics?
374 - (BOOL)groupChatsSupportTopic
380 * @brief Our flags in a chat
382 - (AIGroupChatFlags)flagsInChat:(AIChat *)chat
384 NSString *ourUID = [NSString stringWithUTF8String:purple_normalize(self.purpleAccount, [self.displayName UTF8String])];
386 // XXX Once we don't create a fake contact for ourself, we should do this the right way.
387 return [chat flagsForContact:[self contactWithUID:ourUID]];
390 #pragma mark Action Menu
391 -(NSMenu*)actionMenuForChat:(AIChat*)chat
395 NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects;
396 AIListObject *listObject = nil;
398 if (listObjects.count) {
399 listObject = [listObjects objectAtIndex:0];
402 menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
403 [NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction],
404 [NSNumber numberWithInteger:Context_Contact_Manage],
406 forListObject:listObject
411 [menu addItem:[NSMenuItem separatorItem]];
413 [menu addItemWithTitle:AILocalizedString(@"Op", nil)
419 [menu addItemWithTitle:AILocalizedString(@"Deop", nil)
421 action:@selector(deop)
425 [menu addItemWithTitle:AILocalizedString(@"Voice", nil)
427 action:@selector(voice)
431 [menu addItemWithTitle:AILocalizedString(@"Devoice", nil)
433 action:@selector(devoice)
437 [menu addItem:[NSMenuItem separatorItem]];
439 [menu addItemWithTitle:AILocalizedString(@"Kick", nil)
441 action:@selector(kick)
443 tag:AIRequiresHalfop];
445 [menu addItemWithTitle:AILocalizedString(@"Ban", nil)
447 action:@selector(ban)
449 tag:AIRequiresHalfop];
451 [menu addItemWithTitle:AILocalizedString(@"Bankick", nil)
453 action:@selector(bankick)
455 tag:AIRequiresHalfop];
460 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
462 AIOperationRequirement req = menuItem.tag;
463 AIChat *chat = adium.interfaceController.activeChat;
465 if (!chat.chatContainer.messageViewController.selectedListObjects.count) {
469 AIGroupChatFlags flags = [self flagsInChat:chat];
472 case AIRequiresHalfop:
473 return ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp);
477 return ((flags & AIGroupChatOp) == AIGroupChatOp);
484 #pragma mark Action Menu's Actions
485 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag
487 AIChat *chat = adium.interfaceController.activeChat;
488 NSArray *objects = chat.chatContainer.messageViewController.selectedListObjects;
490 NSMutableString *names = [NSMutableString string];
492 for (NSUInteger x = 0; x < objects.count; x++) {
493 AIListObject *listObject = [objects objectAtIndex:x];
495 if ([flag isEqualToString:@"b"] && [listObject valueForProperty:@"User Host"]) {
496 [names appendString:[NSString stringWithFormat:@"*!%@", [listObject valueForProperty:@"User Host"]]];
498 [names appendString:listObject.UID];
501 [names appendString:@" "];
503 if ((x+1) % 4 == 0 || x+1 == objects.count) {
504 if ([operation isEqualToString:@"MODE"]) {
505 [self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@",
507 (apply ? @"+" : @"-"),
508 [@"" stringByPaddingToLength:(x + 1) % 4 ?: 4
512 } else if ([operation isEqualToString:@"KICK"]) {
513 [self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@",
515 [names stringByReplacingOccurrencesOfString:@" " withString:@","]]];
518 [names setString:@""];
525 [self apply:YES operation:@"MODE" flag:@"o"];
530 [self apply:NO operation:@"MODE" flag:@"o"];
535 [self apply:YES operation:@"MODE" flag:@"v"];
540 [self apply:NO operation:@"MODE" flag:@"v"];
545 [self apply:NO operation:@"KICK" flag:nil];
550 [self apply:YES operation:@"MODE" flag:@"b"];
559 #pragma mark File transfer
560 - (BOOL)canSendFolders
565 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
567 [super _beginSendOfFileTransfer:fileTransfer];
570 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
572 [super acceptFileTransferRequest:fileTransfer];
575 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
577 [super rejectFileReceiveRequest:fileTransfer];
580 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
582 [super cancelFileTransfer:fileTransfer];