Plugins/Purple Service/ESIRCAccount.m
author Zachary West <zacw@adium.im>
Sat Oct 31 22:42:41 2009 -0400 (2009-10-31)
changeset 2843 5784d39a3ed5
parent 2275 ad85d31457ce
child 2844 ecd301a427cc
permissions -rw-r--r--
Never send an autoreply to an IRC message. The server does this for us. Fixes #12621.
     1 //
     2 //  ESIRCAccount.m
     3 //  Adium
     4 //
     5 //  Created by Evan Schoenberg on 3/4/06.
     6 //  Copyright 2006 The Adium Team. All rights reserved.
     7 //
     8 
     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"
    21 
    22 @interface SLPurpleCocoaAdapter ()
    23 - (BOOL)attemptPurpleCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat;
    24 @end
    25 
    26 @interface ESIRCAccount()
    27 - (void)sendRawCommand:(NSString *)command;
    28 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag;
    29 @end
    30 
    31 static PurpleConversation *fakeConversation(PurpleAccount *account);
    32 
    33 @implementation ESIRCAccount
    34 
    35 /*!
    36  * @brief Our explicit formatted UID contains our hostname, so we can differentiate ourself.
    37  */
    38 - (NSString *)explicitFormattedUID
    39 {
    40 	if (self.host) {
    41 		return [NSString stringWithFormat:@"%@ (%@)", self.host, self.displayName];
    42 	} else {
    43 		return self.displayName;
    44 	}
    45 }
    46 
    47 #pragma mark IRC-ism overloads
    48 
    49 /*!
    50  * @brief We always want to autocomplete the UID.
    51  */
    52 - (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat
    53 {
    54 	return YES;
    55 }
    56 
    57 /*!
    58  * @brief Use the object ID for password name
    59  *
    60  * We mess around a lot with the UID. This lets it actually save right.
    61  */
    62 - (BOOL)useInternalObjectIDForPasswordName
    63 {
    64 	return YES;
    65 }
    66 
    67 - (BOOL)openChat:(AIChat *)chat
    68 {
    69 	chat.hideUserIconAndStatus = YES;
    70 	
    71 	return [super openChat:chat];
    72 }
    73 
    74 /*!
    75  * @brief Open the info inspector when getting info
    76  *
    77  * A user can /whois; we want to display info for this case.
    78  */
    79 - (void)openInspectorForContactInfo:(AIListContact *)theContact
    80 {
    81 	[[NSNotificationCenter defaultCenter] postNotificationName:@"AIShowContactInfo" object:theContact];
    82 }
    83 
    84 #pragma mark Command handling
    85 /*!
    86  * @brief We've connected
    87  *
    88  * Send the commands the user wants sent when we do so. Creates a fake conversation to pipe them through.
    89  */
    90 - (void)didConnect
    91 {
    92 	[super didConnect];
    93 	
    94 	PurpleConversation *conv = fakeConversation(self.purpleAccount);
    95 	
    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];
   100 		}
   101 		
   102 		command = [command stringByReplacingOccurrencesOfString:@"$me" withString:self.displayName];
   103 		
   104 		if (command.length) {
   105 			char *error;
   106 			PurpleCmdStatus cmdStatus = purple_cmd_do_command(conv, [command UTF8String], [command UTF8String], &error);
   107 			
   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]);
   114 			}
   115 		}
   116 	}
   117 	
   118 	// The fakeConversation was allocated; now free it.
   119 	g_free(conv);
   120 	
   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];
   125 }
   126 
   127 /*!
   128  * @brief Send a raw command to the IRC server.
   129  */
   130 - (void)sendRawCommand:(NSString *)command
   131 {
   132 	PurpleConnection *connection = purple_account_get_connection(account);
   133 	
   134 	if (!connection)
   135 		return;
   136 
   137 	const char *quote = [command UTF8String];
   138 	irc_cmd_quote(connection->proto_data, NULL, NULL, &quote);	
   139 }
   140 
   141 /*!
   142  * @brief This creates a fake PurpleConversation
   143  *
   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.
   146  *
   147  * This is taken from irchelper.c, the pidgin plugin.
   148  */
   149 static PurpleConversation *fakeConversation(PurpleAccount *account)
   150 {
   151 	PurpleConversation *conv;
   152 	
   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;
   161 	
   162 	return conv;
   163 }
   164 
   165 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
   166 {
   167 
   168 	NSString	*encodedString = nil;
   169 	BOOL		didCommand = [self.purpleAdapter attemptPurpleCommandOnMessage:inContentMessage.message.string
   170 																   fromAccount:(AIAccount *)inContentMessage.source
   171 																	    inChat:inContentMessage.chat];
   172 	
   173 	NSRange meRange = [inContentMessage.message.string rangeOfString:@"/me " options:NSCaseInsensitiveSearch];
   174 
   175 	if (!didCommand || meRange.location == 0) {
   176 		if (meRange.location == 0) {
   177 			inContentMessage.sendContent = NO;
   178 		}
   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)"
   181 		 */
   182 		encodedString = [AIHTMLDecoder encodeHTML:(inContentMessage.chat.isSecure ? inContentMessage.message : [inContentMessage.message attributedStringByConvertingLinksToURLStrings])
   183 										  headers:NO
   184 										 fontTags:YES
   185 							   includingColorTags:YES
   186 									closeFontTags:YES
   187 										styleTags:YES
   188 					   closeStyleTagsOnFontChange:YES
   189 								   encodeNonASCII:NO
   190 									 encodeSpaces:NO
   191 									   imagesPath:nil
   192 								attachmentsAsText:YES
   193 						onlyIncludeOutgoingImages:NO
   194 								   simpleTagsOnly:YES
   195 								   bodyBackground:NO
   196 							  allowJavascriptURLs:YES];
   197 	}
   198 	
   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]];
   202 		return nil;
   203 	} else {
   204 		return encodedString;
   205 	}
   206 }
   207 
   208 #pragma mark Libpurple
   209 - (const char *)protocolPlugin
   210 {
   211 	return "prpl-irc";
   212 }
   213 
   214 - (const char *)purpleAccountName
   215 {
   216 	return [[NSString stringWithFormat:@"%@@%@", self.formattedUID, self.host] UTF8String];
   217 }
   218 
   219 - (NSString *)defaultUsername
   220 {
   221 	return @"Adium";
   222 }
   223 
   224 - (NSString *)defaultRealname
   225 {
   226 	return AILocalizedString(@"Adium User", nil);
   227 }
   228 
   229 - (void)configurePurpleAccount
   230 {
   231 	[super configurePurpleAccount];
   232 
   233 	purple_account_set_username(self.purpleAccount, self.purpleAccountName);
   234 	
   235 	// Encoding
   236 	NSString *encoding = [self preferenceForKey:KEY_IRC_ENCODING group:GROUP_ACCOUNT_STATUS] ?: @"UTF-8";
   237 	purple_account_set_string(self.purpleAccount, "encoding", [encoding UTF8String]);
   238 	
   239 	if (![encoding isEqualToString:@"UTF-8"]) {
   240 		purple_account_set_bool(self.purpleAccount, "autodetect_utf8", TRUE);
   241 	}
   242 	
   243 	// Use SSL
   244 	BOOL useSSL = [[self preferenceForKey:KEY_IRC_USE_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
   245 	purple_account_set_bool(self.purpleAccount, "ssl", useSSL);
   246 	
   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]);
   250 	
   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]);
   254 }
   255 
   256 /*!
   257  * @brief Our display name; either retrieve our current nickname, or return our stored one.
   258  */
   259 - (NSString *)displayName
   260 {
   261 	// Try and get the purple display name, since it changes without telling us.
   262 	if (account) {
   263 		PurpleConnection	*purpleConnection = purple_account_get_connection(account);
   264 		
   265 		if (purpleConnection) {
   266 			return [NSString stringWithUTF8String:purple_connection_get_display_name(purpleConnection)];
   267 		}
   268 	}
   269 	
   270 	return self.formattedUID;
   271 }
   272 
   273 
   274 /*!
   275  * @brief Re-create the chat's join options.
   276  */
   277 - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv
   278 {
   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");
   282 	if (pass)
   283 		[dict setObject: [NSString stringWithUTF8String:pass] forKey:@"password"];
   284 	
   285 	return dict;
   286 }
   287 
   288 /*!
   289  * @brief Should an autoreply be sent to this message?
   290  */
   291 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
   292 {
   293 	return NO;
   294 }
   295 
   296 #pragma mark Server contacts (NickServ, ChanServ)
   297 /*!
   298  * @brief Sends a raw command to identify for the nickname
   299  */
   300 - (void)identifyForName:(NSString *)name password:(NSString *)inPassword
   301 {
   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]];
   308 	} else {
   309 		[self sendRawCommand:[NSString stringWithFormat:@"NICKSERV identify %@", inPassword]];	
   310 	}
   311 }
   312 
   313 /*!
   314  * @brief Is this contact a server contact?
   315  */
   316 BOOL contactUIDIsServerContact(NSString *contactUID)
   317 {
   318 	return (([contactUID caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
   319 			([contactUID caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
   320 			([contactUID rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound));
   321 }
   322 
   323 /*!
   324  * @brief Can we send an offline message to this contact?
   325  *
   326  * We can only send offline messages to the server contacts, since such a message might cause us to connect
   327  */
   328 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
   329 {
   330 	return contactUIDIsServerContact(inContact.UID);
   331 }
   332 
   333 /*!
   334  * @brief Don't autoreply to server contacts (services) or FreeNode's stupidity.
   335  */
   336 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
   337 {
   338 	return !contactUIDIsServerContact(message.source.UID);
   339 }
   340 
   341 /*!
   342  * @brief Don't log server contacts (services) or FreeNode's stupidity.
   343  */
   344 - (BOOL)shouldLogChat:(AIChat *)chat
   345 {
   346 	NSString *source = chat.listObject.UID;
   347 	BOOL shouldLog = YES;
   348 	
   349 	if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
   350 				   ([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
   351 				   ([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) {
   352 		shouldLog = NO;	
   353 	}
   354 
   355 	return (shouldLog && [super shouldLogChat:chat]);
   356 }
   357 
   358 #pragma mark Chat handling
   359 
   360 /*!
   361  * @brief Allow the chat to close unless we're quitting.
   362  */
   363 - (BOOL)closeChat:(AIChat*)chat
   364 {
   365 	if(adium.isQuitting)
   366 		return NO;
   367 	else
   368 		return [super closeChat:chat];
   369 }
   370 
   371 /*!
   372  * @brief Do group chats support topics?
   373  */
   374 - (BOOL)groupChatsSupportTopic
   375 {
   376 	return YES;
   377 }
   378 
   379 /*!
   380  * @brief Our flags in a chat
   381  */
   382 - (AIGroupChatFlags)flagsInChat:(AIChat *)chat
   383 {
   384 	NSString *ourUID = [NSString stringWithUTF8String:purple_normalize(self.purpleAccount, [self.displayName UTF8String])];
   385 	
   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]];
   388 }
   389 
   390 #pragma mark Action Menu
   391 -(NSMenu*)actionMenuForChat:(AIChat*)chat
   392 {
   393 	NSMenu *menu;
   394 	
   395 	NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects;
   396 	AIListObject *listObject = nil;
   397 	
   398 	if (listObjects.count) {
   399 		listObject = [listObjects objectAtIndex:0];
   400 	}
   401 	
   402 	menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
   403 															   [NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction],		
   404 															   [NSNumber numberWithInteger:Context_Contact_Manage],
   405 															   nil]
   406 												forListObject:listObject
   407 													   inChat:chat];
   408 	
   409 	
   410 	
   411 	[menu addItem:[NSMenuItem separatorItem]];
   412 	
   413 	[menu addItemWithTitle:AILocalizedString(@"Op", nil)
   414 					target:self
   415 					action:@selector(op)
   416 			 keyEquivalent:@""
   417 					   tag:AIRequiresOp];
   418 	
   419 	[menu addItemWithTitle:AILocalizedString(@"Deop", nil)
   420 					target:self
   421 					action:@selector(deop)
   422 			 keyEquivalent:@""
   423 					   tag:AIRequiresOp];
   424 	
   425 	[menu addItemWithTitle:AILocalizedString(@"Voice", nil)
   426 					target:self
   427 					action:@selector(voice)
   428 			 keyEquivalent:@""
   429 					   tag:AIRequiresOp];
   430 	
   431 	[menu addItemWithTitle:AILocalizedString(@"Devoice", nil)
   432 					target:self
   433 					action:@selector(devoice)
   434 			 keyEquivalent:@""
   435 					   tag:AIRequiresOp];
   436 
   437 	[menu addItem:[NSMenuItem separatorItem]];
   438 	
   439 	[menu addItemWithTitle:AILocalizedString(@"Kick", nil)
   440 					target:self
   441 					action:@selector(kick)
   442 			 keyEquivalent:@""
   443 					   tag:AIRequiresHalfop];
   444 	
   445 	[menu addItemWithTitle:AILocalizedString(@"Ban", nil)
   446 					target:self
   447 					action:@selector(ban)
   448 			 keyEquivalent:@""
   449 					   tag:AIRequiresHalfop];
   450 	
   451 	[menu addItemWithTitle:AILocalizedString(@"Bankick", nil)
   452 					target:self
   453 					action:@selector(bankick)
   454 			 keyEquivalent:@""
   455 					   tag:AIRequiresHalfop];
   456 	
   457 	return menu;
   458 }
   459 
   460 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
   461 {
   462 	AIOperationRequirement req = menuItem.tag;
   463 	AIChat *chat = adium.interfaceController.activeChat;
   464 	
   465 	if (!chat.chatContainer.messageViewController.selectedListObjects.count) {
   466 		return NO;
   467 	}
   468 	
   469 	AIGroupChatFlags flags = [self flagsInChat:chat];
   470 	
   471 	switch (req) {
   472 		case AIRequiresHalfop:
   473 			return ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp);
   474 			break;
   475 			
   476 		case AIRequiresOp:
   477 			return ((flags & AIGroupChatOp) == AIGroupChatOp);
   478 			break;
   479 	}
   480 	
   481 	return NO;
   482 }
   483 
   484 #pragma mark Action Menu's Actions
   485 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag
   486 {
   487 	AIChat *chat = adium.interfaceController.activeChat;
   488 	NSArray *objects = chat.chatContainer.messageViewController.selectedListObjects;
   489 	
   490 	NSMutableString *names = [NSMutableString string];
   491 	
   492 	for (NSUInteger x = 0; x < objects.count; x++) {
   493 		AIListObject *listObject = [objects objectAtIndex:x];
   494 		
   495 		if ([flag isEqualToString:@"b"] && [listObject valueForProperty:@"User Host"]) {
   496 			[names appendString:[NSString stringWithFormat:@"*!%@", [listObject valueForProperty:@"User Host"]]];
   497 		} else {
   498 			[names appendString:listObject.UID];
   499 		}
   500 		
   501 		[names appendString:@" "];
   502 		
   503 		if ((x+1) % 4 == 0 || x+1 == objects.count) {
   504 			if ([operation isEqualToString:@"MODE"]) {
   505 				[self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@",
   506 									  chat.name,
   507 									  (apply ? @"+" : @"-"),
   508 									  [@"" stringByPaddingToLength:(x + 1) % 4 ?: 4
   509 														withString:flag 
   510 												   startingAtIndex:0],
   511 									  names]];
   512 			} else if ([operation isEqualToString:@"KICK"]) {
   513 				[self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@",
   514 									  chat.name,
   515 									  [names stringByReplacingOccurrencesOfString:@" " withString:@","]]];
   516 			}
   517 			
   518 			[names setString:@""];
   519 		}
   520 	}
   521 }
   522 
   523 - (void)op
   524 {
   525 	[self apply:YES operation:@"MODE" flag:@"o"];
   526 }
   527 
   528 - (void)deop
   529 {
   530 	[self apply:NO operation:@"MODE" flag:@"o"];
   531 }
   532 
   533 - (void)voice
   534 {
   535 	[self apply:YES operation:@"MODE" flag:@"v"];
   536 }
   537 
   538 - (void)devoice
   539 {
   540 	[self apply:NO operation:@"MODE" flag:@"v"];
   541 }
   542 
   543 - (void)kick
   544 {
   545 	[self apply:NO operation:@"KICK" flag:nil];
   546 }
   547 
   548 - (void)ban
   549 {
   550 	[self apply:YES operation:@"MODE" flag:@"b"];
   551 }
   552 
   553 - (void)bankick
   554 {
   555 	[self ban];
   556 	[self kick];
   557 }
   558 
   559 #pragma mark File transfer
   560 - (BOOL)canSendFolders
   561 {
   562 	return NO;
   563 }
   564 
   565 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
   566 {
   567 	[super _beginSendOfFileTransfer:fileTransfer];
   568 }
   569 
   570 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
   571 {
   572     [super acceptFileTransferRequest:fileTransfer];    
   573 }
   574 
   575 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
   576 {
   577     [super rejectFileReceiveRequest:fileTransfer];    
   578 }
   579 
   580 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
   581 {
   582 	[super cancelFileTransfer:fileTransfer];
   583 }
   584 
   585 @end