* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import <Adium/AIHTMLDecoder.h> #import <Adium/AIContentMessage.h> #import <Adium/AIListContact.h> #import <Adium/AIMenuControllerProtocol.h> #import "AIMessageViewController.h" #import <AIUtilities/AIMenuAdditions.h> #import <AIUtilities/AIAttributedStringAdditions.h> #import <libpurple/irc.h> #import <libpurple/cmds.h> #import "SLPurpleCocoaAdapter.h" @interface SLPurpleCocoaAdapter () - (BOOL)attemptPurpleCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat; @interface ESIRCAccount() - (void)sendRawCommand:(NSString *)command; - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag; static PurpleConversation *fakeConversation(PurpleAccount *account); @implementation ESIRCAccount * @brief Our explicit formatted UID contains our hostname, so we can differentiate ourself. - (NSString *)explicitFormattedUID return [NSString stringWithFormat:@"%@ (%@)", self.host, self.displayName]; [consoleController close]; [consoleController release]; #pragma mark IRC-ism overloads * @brief We always want to autocomplete the UID. - (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat * @brief Use the object ID for password name * We mess around a lot with the UID. This lets it actually save right. - (BOOL)useInternalObjectIDForPasswordName - (BOOL)openChat:(AIGroupChat *)chat chat.hideUserIconAndStatus = YES; return [super openChat:chat]; - (BOOL)shouldDisplayOutgoingMUCMessages * @brief Open the info inspector when getting info * A user can /whois; we want to display info for this case. - (void)openInspectorForContactInfo:(AIListContact *)theContact [[NSNotificationCenter defaultCenter] postNotificationName:@"AIShowContactInfo" object:theContact]; #pragma mark Command handling //Always enable the XML console for debug builds //For non-debug builds, only enable it if the preference is set enableConsole = [[NSUserDefaults standardUserDefaults] boolForKey:@"AIIRCConsole"]; * Send the commands the user wants sent when we do so. Creates a fake conversation to pipe them through. if ([self enableConsole]) { if (!consoleController) consoleController = [[AIIRCConsoleController alloc] init]; [consoleController setPurpleConnection:purple_account_get_connection(account)]; PurpleConversation *conv = fakeConversation(self.purpleAccount); for (NSString *command in [[self preferenceForKey:KEY_IRC_COMMANDS group:GROUP_ACCOUNT_STATUS] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]) { if ([command hasPrefix:@"/"]) { command = [command substringFromIndex:1]; command = [command stringByReplacingOccurrencesOfString:@"$me" withString:self.displayName]; PurpleCmdStatus cmdStatus = purple_cmd_do_command(conv, [command UTF8String], [command UTF8String], &error); if (cmdStatus == PURPLE_CMD_STATUS_NOT_FOUND) { // If it's not found, send it as a raw command like we do in chats. [self sendRawCommand:command]; } else if (cmdStatus != PURPLE_CMD_STATUS_OK) { // The command failed with something other than "not found" - log it. AILogWithSignature(@"Command (%@) failed: %d - %@", command, cmdStatus, [NSString stringWithUTF8String:error]); // The fakeConversation was allocated; now free it. // Set a fake display name preference since we differ from global always. [self setPreference:[[NSAttributedString stringWithString:@"Adium"] dataRepresentation] forKey:KEY_ACCOUNT_DISPLAY_NAME group:GROUP_ACCOUNT_STATUS]; [consoleController setPurpleConnection:NULL]; * @brief Send a raw command to the IRC server. - (void)sendRawCommand:(NSString *)command PurpleConnection *connection = purple_account_get_connection(account); const char *quote = [command UTF8String]; irc_cmd_quote(connection->proto_data, NULL, NULL, "e); * @brief This creates a fake PurpleConversation * This fake conversation is used for sending purple_cmd_do_command() messages, which requires * a conversation for the command to occur. Free this when finished. * This is taken from irchelper.c, the pidgin plugin. static PurpleConversation *fakeConversation(PurpleAccount *account) PurpleConversation *conv; conv = g_new0(PurpleConversation, 1); conv->type = PURPLE_CONV_TYPE_IM; /* If we use this then the conversation updated signal is fired and * other plugins might start doing things to our conversation, such as * setting data on it which we would then need to free etc. It's easier * just to be more hacky by setting account directly. */ /* purple_conversation_set_account(conv, account); */ - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage NSString *encodedString = nil; NSString *messageString = inContentMessage.message.string; BOOL didCommand = [self.purpleAdapter attemptPurpleCommandOnMessage:messageString fromAccount:(AIAccount *)inContentMessage.source inChat:inContentMessage.chat]; BOOL hasSlashMe = ([messageString rangeOfString:@"/me " options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0); /* /say is a special case; it's not actually a command, but an instruction to display the following text (even if * that text would normally be a command itself). BOOL hasSlashSay = ([messageString rangeOfString:@"/say " options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0); if (!didCommand || hasSlashMe) { inContentMessage.sendContent = NO; if (inContentMessage.chat.isGroupChat) { inContentMessage.displayContent = NO; /* If we're sending a message on an encrypted direct msg, we can encode the HTML normally, as links will go through fine. * However, in all other cases, IRC will drop the title of any link, so we preprocess it to be in the form "title (link)" NSAttributedString *messageAttributedString = inContentMessage.message; messageAttributedString = [messageAttributedString attributedSubstringFromRange:NSMakeRange([@"/say " length], messageAttributedString.length - [@"/say " length])]; encodedString = [AIHTMLDecoder encodeHTML:(inContentMessage.chat.isSecure ? messageAttributedString : [messageAttributedString attributedStringByConvertingLinksToURLStrings]) closeStyleTagsOnFontChange:YES onlyIncludeOutgoingImages:NO allowJavascriptURLs:YES]; if (!didCommand && !hasSlashSay && [messageString hasPrefix:@"/"]) { // Try to send it to the server, if we don't know what it is; definitely don't display. [self sendRawCommand:[messageString substringFromIndex:1]]; - (const char *)protocolPlugin - (const char *)purpleAccountName return [[NSString stringWithFormat:@"%@@%@", self.formattedUID, self.host] UTF8String]; - (NSString *)defaultUsername - (NSString *)defaultRealname return AILocalizedString(@"Adium User", nil); - (void)configurePurpleAccount [super configurePurpleAccount]; purple_account_set_username(self.purpleAccount, self.purpleAccountName); NSString *encoding = [self preferenceForKey:KEY_IRC_ENCODING group:GROUP_ACCOUNT_STATUS] ?: @"UTF-8"; purple_account_set_string(self.purpleAccount, "encoding", [encoding UTF8String]); if (![encoding isEqualToString:@"UTF-8"]) { purple_account_set_bool(self.purpleAccount, "autodetect_utf8", TRUE); BOOL useSSL = [[self preferenceForKey:KEY_IRC_USE_SSL group:GROUP_ACCOUNT_STATUS] boolValue]; purple_account_set_bool(self.purpleAccount, "ssl", useSSL); // Username (for connecting) NSString *username = [self preferenceForKey:KEY_IRC_USERNAME group:GROUP_ACCOUNT_STATUS] ?: self.defaultUsername; purple_account_set_string(self.purpleAccount, "username", [username UTF8String]); // Realname (for connecting) NSString *realname = [self preferenceForKey:KEY_IRC_REALNAME group:GROUP_ACCOUNT_STATUS] ?: self.defaultRealname; purple_account_set_string(self.purpleAccount, "realname", [realname UTF8String]); BOOL useSASL = [[self preferenceForKey:KEY_IRC_USE_SASL group:GROUP_ACCOUNT_STATUS] boolValue]; purple_account_set_bool(self.purpleAccount, "sasl", useSASL); BOOL insecureSASLPlain = [[self preferenceForKey:KEY_IRC_INSECURE_SASL_PLAIN group:GROUP_ACCOUNT_STATUS] boolValue]; purple_account_set_bool(self.purpleAccount, "auth_plain_in_clear", insecureSASLPlain); * @brief Our display name; either retrieve our current nickname, or return our stored one. - (NSString *)displayName // Try and get the purple display name, since it changes without telling us. PurpleConnection *purpleConnection = purple_account_get_connection(account); return [NSString stringWithUTF8String:purple_connection_get_display_name(purpleConnection)]; return self.formattedUID; * @brief Re-create the chat's join options. - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setObject:[NSString stringWithUTF8String:purple_conversation_get_name(conv)] forKey:@"channel"]; const char *pass = purple_conversation_get_data(conv, "password"); [dict setObject: [NSString stringWithUTF8String:pass] forKey:@"password"]; * @brief Should an autoreply be sent to this message? - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message - (PurpleSslConnection *)secureConnection { PurpleConnection *gc = purple_account_get_connection(self.purpleAccount); return ((gc && gc->proto_data) ? ((struct irc_conn*)purple_account_get_connection(self.purpleAccount)->proto_data)->gsc : NULL); return (self.online && [self secureConnection]); #pragma mark Server contacts (NickServ, ChanServ) * @brief Sends a raw command to identify for the nickname - (void)identifyForName:(NSString *)name password:(NSString *)inPassword if ([self.host rangeOfString:@"quakenet" options:NSCaseInsensitiveSearch].location != NSNotFound) { [self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG Q@CServe.quakenet.org :AUTH %@ %@", name, inPassword]]; } else if ([self.host rangeOfString:@"undernet" options:NSCaseInsensitiveSearch].location != NSNotFound) { [self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG X@channels.undernet.org :LOGIN %@ %@", name, inPassword]]; } else if ([self.host rangeOfString:@"gamesurge" options:NSCaseInsensitiveSearch].location != NSNotFound) { [self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG AuthServ@Services.GameSurge.net :AUTH %@ %@", name, inPassword]]; [self sendRawCommand:[NSString stringWithFormat:@"NICKSERV identify %@", inPassword]]; * @brief Is this contact a server contact? BOOL contactUIDIsServerContact(NSString *contactUID) return (([contactUID caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) || ([contactUID caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) || ([contactUID rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound)); * @brief Can we send an offline message to this contact? * We can only send offline messages to the server contacts, since such a message might cause us to connect - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact return contactUIDIsServerContact(inContact.UID); * @brief Don't log server contacts (services) or FreeNode's stupidity. - (BOOL)shouldLogChat:(AIChat *)chat NSString *source = chat.listObject.UID; if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) || ([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) || ([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) { return (shouldLog && [super shouldLogChat:chat]); #pragma mark Chat handling * @brief Allow the chat to close unless we're quitting. - (BOOL)closeChat:(AIChat*)chat return [super closeChat:chat]; * @brief Do group chats support topics? - (BOOL)groupChatsSupportTopic * @brief Our flags in a chat - (AIGroupChatFlags)flagsInChat:(AIGroupChat *)chat // XXX Once we don't create a fake contact for ourself, we should do this the right way. return [chat flagsForNick:self.displayName]; - (IBAction)showConsole:(id)sender { [consoleController showWindow:sender]; - (NSArray *)accountActionMenuItems if ([self enableConsole]) { NSMenuItem *xmlConsoleMenuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Console",nil) action:@selector(showConsole:) [xmlConsoleMenuItem setTarget:self]; return [[NSArray arrayWithObject:[xmlConsoleMenuItem autorelease]] arrayByAddingObjectsFromArray:[super accountActionMenuItems]]; return [super accountActionMenuItems]; -(NSMenu*)actionMenuForChat:(AIChat*)chat NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects; AIListObject *listObject = nil; listObject = [listObjects objectAtIndex:0]; menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects: [NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction], [NSNumber numberWithInteger:Context_Contact_Manage], [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:AILocalizedString(@"Op", nil) [menu addItemWithTitle:AILocalizedString(@"Deop", nil) [menu addItemWithTitle:AILocalizedString(@"Voice", nil) [menu addItemWithTitle:AILocalizedString(@"Devoice", nil) action:@selector(devoice) [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:AILocalizedString(@"Kick", nil) [menu addItemWithTitle:AILocalizedString(@"Ban", nil) [menu addItemWithTitle:AILocalizedString(@"Bankick", nil) action:@selector(bankick) - (BOOL)validateMenuItem:(NSMenuItem *)menuItem AIOperationRequirement req = (AIOperationRequirement)menuItem.tag; AIChat *chat = adium.interfaceController.activeChat; BOOL anySelected = chat.chatContainer.messageViewController.selectedListObjects.count > 0; if (!chat.isGroupChat) return YES; AIGroupChatFlags flags = [self flagsInChat:(AIGroupChat *)chat]; return (anySelected && ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp)); return (anySelected && ((flags & AIGroupChatOp) == AIGroupChatOp)); #pragma mark Action Menu's Actions - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag AIChat *chat = adium.interfaceController.activeChat; NSArray *objects = chat.chatContainer.messageViewController.selectedListObjects; NSMutableString *names = [NSMutableString string]; for (NSUInteger x = 0; x < objects.count; x++) { AIListObject *listObject = [objects objectAtIndex:x]; if ([flag isEqualToString:@"b"] && [listObject valueForProperty:@"User Host"]) { [names appendString:[NSString stringWithFormat:@"*!%@", [listObject valueForProperty:@"User Host"]]]; [names appendString:listObject.UID]; [names appendString:@" "]; if ((x+1) % 4 == 0 || x+1 == objects.count) { if ([operation isEqualToString:@"MODE"]) { [self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@", [@"" stringByPaddingToLength:(x + 1) % 4 ?: 4 } else if ([operation isEqualToString:@"KICK"]) { [self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@", [names stringByReplacingOccurrencesOfString:@" " withString:@","]]]; [self apply:YES operation:@"MODE" flag:@"o"]; [self apply:NO operation:@"MODE" flag:@"o"]; [self apply:YES operation:@"MODE" flag:@"v"]; [self apply:NO operation:@"MODE" flag:@"v"]; [self apply:NO operation:@"KICK" flag:nil]; [self apply:YES operation:@"MODE" flag:@"b"]; #pragma mark File transfer - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer [super _beginSendOfFileTransfer:fileTransfer]; - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer [super acceptFileTransferRequest:fileTransfer]; - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer [super rejectFileReceiveRequest:fileTransfer]; - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer [super cancelFileTransfer:fileTransfer];