Plugins/Purple Service/ESIRCAccount.m
author Zachary West <zacw@adium.im>
Sat Oct 31 22:58:03 2009 -0400 (2009-10-31)
changeset 2845 e47bfb289f64
parent 2844 ecd301a427cc
child 3078 1b883db24823
permissions -rw-r--r--
Allow menu items to be validated if they aren't channel operations. Fixes #13280.
     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 /*!
   335  * @brief Don't log server contacts (services) or FreeNode's stupidity.
   336  */
   337 - (BOOL)shouldLogChat:(AIChat *)chat
   338 {
   339 	NSString *source = chat.listObject.UID;
   340 	BOOL shouldLog = YES;
   341 	
   342 	if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
   343 				   ([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
   344 				   ([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) {
   345 		shouldLog = NO;	
   346 	}
   347 
   348 	return (shouldLog && [super shouldLogChat:chat]);
   349 }
   350 
   351 #pragma mark Chat handling
   352 
   353 /*!
   354  * @brief Allow the chat to close unless we're quitting.
   355  */
   356 - (BOOL)closeChat:(AIChat*)chat
   357 {
   358 	if(adium.isQuitting)
   359 		return NO;
   360 	else
   361 		return [super closeChat:chat];
   362 }
   363 
   364 /*!
   365  * @brief Do group chats support topics?
   366  */
   367 - (BOOL)groupChatsSupportTopic
   368 {
   369 	return YES;
   370 }
   371 
   372 /*!
   373  * @brief Our flags in a chat
   374  */
   375 - (AIGroupChatFlags)flagsInChat:(AIChat *)chat
   376 {
   377 	NSString *ourUID = [NSString stringWithUTF8String:purple_normalize(self.purpleAccount, [self.displayName UTF8String])];
   378 	
   379 	// XXX Once we don't create a fake contact for ourself, we should do this the right way.
   380 	return [chat flagsForContact:[self contactWithUID:ourUID]];
   381 }
   382 
   383 #pragma mark Action Menu
   384 -(NSMenu*)actionMenuForChat:(AIChat*)chat
   385 {
   386 	NSMenu *menu;
   387 	
   388 	NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects;
   389 	AIListObject *listObject = nil;
   390 	
   391 	if (listObjects.count) {
   392 		listObject = [listObjects objectAtIndex:0];
   393 	}
   394 	
   395 	menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
   396 															   [NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction],		
   397 															   [NSNumber numberWithInteger:Context_Contact_Manage],
   398 															   nil]
   399 												forListObject:listObject
   400 													   inChat:chat];
   401 	
   402 	
   403 	
   404 	[menu addItem:[NSMenuItem separatorItem]];
   405 	
   406 	[menu addItemWithTitle:AILocalizedString(@"Op", nil)
   407 					target:self
   408 					action:@selector(op)
   409 			 keyEquivalent:@""
   410 					   tag:AIRequiresOp];
   411 	
   412 	[menu addItemWithTitle:AILocalizedString(@"Deop", nil)
   413 					target:self
   414 					action:@selector(deop)
   415 			 keyEquivalent:@""
   416 					   tag:AIRequiresOp];
   417 	
   418 	[menu addItemWithTitle:AILocalizedString(@"Voice", nil)
   419 					target:self
   420 					action:@selector(voice)
   421 			 keyEquivalent:@""
   422 					   tag:AIRequiresOp];
   423 	
   424 	[menu addItemWithTitle:AILocalizedString(@"Devoice", nil)
   425 					target:self
   426 					action:@selector(devoice)
   427 			 keyEquivalent:@""
   428 					   tag:AIRequiresOp];
   429 
   430 	[menu addItem:[NSMenuItem separatorItem]];
   431 	
   432 	[menu addItemWithTitle:AILocalizedString(@"Kick", nil)
   433 					target:self
   434 					action:@selector(kick)
   435 			 keyEquivalent:@""
   436 					   tag:AIRequiresHalfop];
   437 	
   438 	[menu addItemWithTitle:AILocalizedString(@"Ban", nil)
   439 					target:self
   440 					action:@selector(ban)
   441 			 keyEquivalent:@""
   442 					   tag:AIRequiresHalfop];
   443 	
   444 	[menu addItemWithTitle:AILocalizedString(@"Bankick", nil)
   445 					target:self
   446 					action:@selector(bankick)
   447 			 keyEquivalent:@""
   448 					   tag:AIRequiresHalfop];
   449 	
   450 	return menu;
   451 }
   452 
   453 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
   454 {
   455 	AIOperationRequirement req = menuItem.tag;
   456 	AIChat *chat = adium.interfaceController.activeChat;
   457 	BOOL anySelected = chat.chatContainer.messageViewController.selectedListObjects.count > 0;
   458 		
   459 	AIGroupChatFlags flags = [self flagsInChat:chat];
   460 	
   461 	switch (req) {
   462 		case AIRequiresHalfop:
   463 			return (anySelected && ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp));
   464 			break;
   465 			
   466 		case AIRequiresOp:
   467 			return (anySelected && ((flags & AIGroupChatOp) == AIGroupChatOp));
   468 			break;
   469 			
   470 		case AIRequiresNoLevel:
   471 			return anySelected;
   472 			break;
   473 			
   474 		default:
   475 			return YES;
   476 			break;
   477 	}
   478 }
   479 
   480 #pragma mark Action Menu's Actions
   481 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag
   482 {
   483 	AIChat *chat = adium.interfaceController.activeChat;
   484 	NSArray *objects = chat.chatContainer.messageViewController.selectedListObjects;
   485 	
   486 	NSMutableString *names = [NSMutableString string];
   487 	
   488 	for (NSUInteger x = 0; x < objects.count; x++) {
   489 		AIListObject *listObject = [objects objectAtIndex:x];
   490 		
   491 		if ([flag isEqualToString:@"b"] && [listObject valueForProperty:@"User Host"]) {
   492 			[names appendString:[NSString stringWithFormat:@"*!%@", [listObject valueForProperty:@"User Host"]]];
   493 		} else {
   494 			[names appendString:listObject.UID];
   495 		}
   496 		
   497 		[names appendString:@" "];
   498 		
   499 		if ((x+1) % 4 == 0 || x+1 == objects.count) {
   500 			if ([operation isEqualToString:@"MODE"]) {
   501 				[self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@",
   502 									  chat.name,
   503 									  (apply ? @"+" : @"-"),
   504 									  [@"" stringByPaddingToLength:(x + 1) % 4 ?: 4
   505 														withString:flag 
   506 												   startingAtIndex:0],
   507 									  names]];
   508 			} else if ([operation isEqualToString:@"KICK"]) {
   509 				[self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@",
   510 									  chat.name,
   511 									  [names stringByReplacingOccurrencesOfString:@" " withString:@","]]];
   512 			}
   513 			
   514 			[names setString:@""];
   515 		}
   516 	}
   517 }
   518 
   519 - (void)op
   520 {
   521 	[self apply:YES operation:@"MODE" flag:@"o"];
   522 }
   523 
   524 - (void)deop
   525 {
   526 	[self apply:NO operation:@"MODE" flag:@"o"];
   527 }
   528 
   529 - (void)voice
   530 {
   531 	[self apply:YES operation:@"MODE" flag:@"v"];
   532 }
   533 
   534 - (void)devoice
   535 {
   536 	[self apply:NO operation:@"MODE" flag:@"v"];
   537 }
   538 
   539 - (void)kick
   540 {
   541 	[self apply:NO operation:@"KICK" flag:nil];
   542 }
   543 
   544 - (void)ban
   545 {
   546 	[self apply:YES operation:@"MODE" flag:@"b"];
   547 }
   548 
   549 - (void)bankick
   550 {
   551 	[self ban];
   552 	[self kick];
   553 }
   554 
   555 #pragma mark File transfer
   556 - (BOOL)canSendFolders
   557 {
   558 	return NO;
   559 }
   560 
   561 - (void)beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
   562 {
   563 	[super _beginSendOfFileTransfer:fileTransfer];
   564 }
   565 
   566 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
   567 {
   568     [super acceptFileTransferRequest:fileTransfer];    
   569 }
   570 
   571 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
   572 {
   573     [super rejectFileReceiveRequest:fileTransfer];    
   574 }
   575 
   576 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
   577 {
   578 	[super cancelFileTransfer:fileTransfer];
   579 }
   580 
   581 @end