Plugins/Purple Service/ESIRCAccount.m
author Zachary West <zacw@adiumx.com>
Thu Apr 09 02:14:33 2009 +0000 (2009-04-09)
changeset 1618 d11404141154
parent 1615 65891d9888fe
child 1619 b6e64cfa97bf
permissions -rw-r--r--
Don't validate command menu items when no list objects are selected.
     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 #pragma mark Command handling
    75 /*!
    76  * @brief We've connected
    77  *
    78  * Send the commands the user wants sent when we do so. Creates a fake conversation to pipe them through.
    79  */
    80 - (void)didConnect
    81 {
    82 	[super didConnect];
    83 	
    84 	PurpleConversation *conv = fakeConversation(self.purpleAccount);
    85 	
    86 	for (NSString *command in [[self preferenceForKey:KEY_IRC_COMMANDS
    87 												group:GROUP_ACCOUNT_STATUS] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]) {
    88 		if ([command hasPrefix:@"/"]) {
    89 			command = [command substringFromIndex:1];
    90 		}
    91 		
    92 		command = [command stringByReplacingOccurrencesOfString:@"$me" withString:self.displayName];
    93 		
    94 		if (command.length) {
    95 			char *error;
    96 			PurpleCmdStatus cmdStatus = purple_cmd_do_command(conv, [command UTF8String], [command UTF8String], &error);
    97 			
    98 			if (cmdStatus == PURPLE_CMD_STATUS_NOT_FOUND) {
    99 				// If it's not found, send it as a raw command like we do in chats.
   100 				[self sendRawCommand:command];
   101 			} else if (cmdStatus != PURPLE_CMD_STATUS_OK) {
   102 				// The command failed with something other than "not found" - log it.
   103 				AILogWithSignature(@"Command (%@) failed: %d - %@", command, cmdStatus, [NSString stringWithUTF8String:error]);
   104 			}
   105 		}
   106 	}
   107 	
   108 	// The fakeConversation was allocated; now free it.
   109 	g_free(conv);
   110 }
   111 
   112 /*!
   113  * @brief Send a raw command to the IRC server.
   114  */
   115 - (void)sendRawCommand:(NSString *)command
   116 {
   117 	PurpleConnection *connection = purple_account_get_connection(account);
   118 	
   119 	if (!connection)
   120 		return;
   121 
   122 	const char *quote = [command UTF8String];
   123 	irc_cmd_quote(connection->proto_data, NULL, NULL, &quote);	
   124 }
   125 
   126 /*!
   127  * @brief This creates a fake PurpleConversation
   128  *
   129  * This fake conversation is used for sending purple_cmd_do_command() messages, which requires
   130  * a conversation for the command to occur. Free this when finished.
   131  *
   132  * This is taken from irchelper.c, the pidgin plugin.
   133  */
   134 static PurpleConversation *fakeConversation(PurpleAccount *account)
   135 {
   136 	PurpleConversation *conv;
   137 	
   138 	conv = g_new0(PurpleConversation, 1);
   139 	conv->type = PURPLE_CONV_TYPE_IM;
   140 	/* If we use this then the conversation updated signal is fired and
   141 	 * other plugins might start doing things to our conversation, such as
   142 	 * setting data on it which we would then need to free etc. It's easier
   143 	 * just to be more hacky by setting account directly. */
   144 	/* purple_conversation_set_account(conv, account); */
   145 	conv->account = account;
   146 	
   147 	return conv;
   148 }
   149 
   150 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
   151 {
   152 
   153 	NSString	*encodedString = nil;
   154 	BOOL		didCommand = [self.purpleAdapter attemptPurpleCommandOnMessage:inContentMessage.message.string
   155 																   fromAccount:(AIAccount *)inContentMessage.source
   156 																	    inChat:inContentMessage.chat];
   157 	
   158 	NSRange meRange = [inContentMessage.message.string rangeOfString:@"/me " options:NSCaseInsensitiveSearch];
   159 
   160 	if (!didCommand || meRange.location == 0) {
   161 		if (meRange.location == 0) {
   162 			inContentMessage.sendContent = NO;
   163 		}
   164 		/* 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.
   165 		 * 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)"
   166 		 */
   167 		encodedString = [AIHTMLDecoder encodeHTML:(inContentMessage.chat.isSecure ? inContentMessage.message : [inContentMessage.message attributedStringByConvertingLinksToStrings])
   168 										  headers:NO
   169 										 fontTags:YES
   170 							   includingColorTags:YES
   171 									closeFontTags:YES
   172 										styleTags:YES
   173 					   closeStyleTagsOnFontChange:YES
   174 								   encodeNonASCII:NO
   175 									 encodeSpaces:NO
   176 									   imagesPath:nil
   177 								attachmentsAsText:YES
   178 						onlyIncludeOutgoingImages:NO
   179 								   simpleTagsOnly:YES
   180 								   bodyBackground:NO
   181 							  allowJavascriptURLs:YES];
   182 	}
   183 	
   184 	if (!didCommand && [inContentMessage.message.string hasPrefix:@"/"]) {
   185 		// Try to send it to the server, if we don't know what it is; definitely don't display.
   186 		[self sendRawCommand:[inContentMessage.message.string substringFromIndex:1]];
   187 		return nil;
   188 	} else {
   189 		return encodedString;
   190 	}
   191 }
   192 
   193 #pragma mark Libpurple
   194 - (const char *)protocolPlugin
   195 {
   196 	return "prpl-irc";
   197 }
   198 
   199 - (const char *)purpleAccountName
   200 {
   201 	return [[NSString stringWithFormat:@"%@@%@", self.formattedUID, self.host] UTF8String];
   202 }
   203 
   204 - (NSString *)defaultUsername
   205 {
   206 	return @"Adium";
   207 }
   208 
   209 - (NSString *)defaultRealname
   210 {
   211 	return AILocalizedString(@"Adium User", nil);
   212 }
   213 
   214 - (void)configurePurpleAccount
   215 {
   216 	[super configurePurpleAccount];
   217 
   218 	purple_account_set_username(self.purpleAccount, self.purpleAccountName);
   219 	
   220 	// Encoding
   221 	NSString *encoding = [self preferenceForKey:KEY_IRC_ENCODING group:GROUP_ACCOUNT_STATUS] ?: @"UTF-8";
   222 	purple_account_set_string(self.purpleAccount, "encoding", [encoding UTF8String]);
   223 	
   224 	if (![encoding isEqualToString:@"UTF-8"]) {
   225 		purple_account_set_bool(self.purpleAccount, "autodetect_utf8", TRUE);
   226 	}
   227 	
   228 	// Use SSL
   229 	BOOL useSSL = [[self preferenceForKey:KEY_IRC_USE_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
   230 	purple_account_set_bool(self.purpleAccount, "ssl", useSSL);
   231 	
   232 	// Username (for connecting)
   233 	NSString *username = [self preferenceForKey:KEY_IRC_USERNAME group:GROUP_ACCOUNT_STATUS] ?: self.defaultUsername;
   234 	purple_account_set_string(self.purpleAccount, "username", [username UTF8String]);
   235 	
   236 	// Realname (for connecting)
   237 	NSString *realname = [self preferenceForKey:KEY_IRC_REALNAME group:GROUP_ACCOUNT_STATUS] ?: self.defaultRealname;
   238 	purple_account_set_string(self.purpleAccount, "realname", [realname UTF8String]);
   239 }
   240 
   241 /*!
   242  * @brief Our display name; either retrieve our current nickname, or return our stored one.
   243  */
   244 - (NSString *)displayName
   245 {
   246 	// Try and get the purple display name, since it changes without telling us.
   247 	if (account) {
   248 		PurpleConnection	*purpleConnection = purple_account_get_connection(account);
   249 		
   250 		if (purpleConnection) {
   251 			return [NSString stringWithUTF8String:purple_connection_get_display_name(purpleConnection)];
   252 		}
   253 	}
   254 	
   255 	return self.formattedUID;
   256 }
   257 
   258 
   259 /*!
   260  * @brief Re-create the chat's join options.
   261  */
   262 - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv
   263 {
   264 	NSMutableDictionary *dict = [NSMutableDictionary dictionary];
   265 	[dict setObject:[NSString stringWithUTF8String:purple_conversation_get_name(conv)] forKey:@"channel"];
   266 	const char *pass = purple_conversation_get_data(conv, "password");
   267 	if (pass)
   268 		[dict setObject: [NSString stringWithUTF8String:pass] forKey:@"password"];
   269 	
   270 	return dict;
   271 }
   272 
   273 #pragma mark Server contacts (NickServ, ChanServ)
   274 /*!
   275  * @brief Sends a raw command to identify for the nickname
   276  */
   277 - (void)identifyForName:(NSString *)name password:(NSString *)inPassword
   278 {
   279 	if ([self.host rangeOfString:@"quakenet" options:NSCaseInsensitiveSearch].location != NSNotFound) {
   280 		[self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG Q@CServe.quakenet.org :AUTH %@ %@", name, inPassword]];
   281 	} else if ([self.host rangeOfString:@"undernet" options:NSCaseInsensitiveSearch].location != NSNotFound) {
   282 		[self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG X@channels.undernet.org :LOGIN %@ %@", name, inPassword]];
   283 	} else if ([self.host rangeOfString:@"gamesurge" options:NSCaseInsensitiveSearch].location != NSNotFound) {
   284 		[self sendRawCommand:[NSString stringWithFormat:@"PRIVMSG AuthServ@Services.GameSurge.net :AUTH %@ %@", name, inPassword]];
   285 	} else {
   286 		[self sendRawCommand:[NSString stringWithFormat:@"NICKSERV identify %@ %@", name, inPassword]];	
   287 	}
   288 }
   289 
   290 /*!
   291  * @brief Is this contact a server contact?
   292  */
   293 BOOL contactUIDIsServerContact(NSString *contactUID)
   294 {
   295 	return (([contactUID caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
   296 			([contactUID caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
   297 			([contactUID rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound));
   298 }
   299 
   300 /*!
   301  * @brief Can we send an offline message to this contact?
   302  *
   303  * We can only send offline messages to the server contacts, since such a message might cause us to connect
   304  */
   305 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
   306 {
   307 	return contactUIDIsServerContact(inContact.UID);
   308 }
   309 
   310 /*!
   311  * @brief Don't autoreply to server contacts (services) or FreeNode's stupidity.
   312  */
   313 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
   314 {
   315 	return !contactUIDIsServerContact(message.source.UID);
   316 }
   317 
   318 /*!
   319  * @brief Don't log server contacts (services) or FreeNode's stupidity.
   320  */
   321 - (BOOL)shouldLogChat:(AIChat *)chat
   322 {
   323 	NSString *source = chat.listObject.UID;
   324 	BOOL shouldLog = YES;
   325 	
   326 	if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
   327 				   ([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
   328 				   ([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) {
   329 		shouldLog = NO;	
   330 	}
   331 
   332 	return (shouldLog && [super shouldLogChat:chat]);
   333 }
   334 
   335 #pragma mark Chat handling
   336 
   337 /*!
   338  * @brief Allow the chat to close unless we're quitting.
   339  */
   340 - (BOOL)closeChat:(AIChat*)chat
   341 {
   342 	if(adium.isQuitting)
   343 		return NO;
   344 	else
   345 		return [super closeChat:chat];
   346 }
   347 
   348 /*!
   349  * @brief Do group chats support topics?
   350  */
   351 - (BOOL)groupChatsSupportTopic
   352 {
   353 	return YES;
   354 }
   355 
   356 /*!
   357  * @brief Our flags in a chat
   358  */
   359 - (AIGroupChatFlags)flagsInChat:(AIChat *)chat
   360 {
   361 	NSString *ourUID = [NSString stringWithUTF8String:purple_normalize(self.purpleAccount, [self.displayName UTF8String])];
   362 	
   363 	// XXX Once we don't create a fake contact for ourself, we should do this the right way.
   364 	return [chat flagsForContact:[self contactWithUID:ourUID]];
   365 }
   366 
   367 #pragma mark Action Menu
   368 -(NSMenu*)actionMenuForChat:(AIChat*)chat
   369 {
   370 	NSMenu *menu;
   371 	
   372 	NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects;
   373 	AIListObject *listObject = nil;
   374 	
   375 	if (listObjects.count) {
   376 		listObject = [listObjects objectAtIndex:0];
   377 	}
   378 	
   379 	menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
   380 															   [NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction],		
   381 															   [NSNumber numberWithInteger:Context_Contact_Manage],
   382 															   nil]
   383 												forListObject:listObject
   384 													   inChat:chat];
   385 	
   386 	
   387 	
   388 	[menu addItem:[NSMenuItem separatorItem]];
   389 	
   390 	[menu addItemWithTitle:AILocalizedString(@"Op", nil)
   391 					target:self
   392 					action:@selector(op)
   393 			 keyEquivalent:@""
   394 					   tag:AIRequiresOp];
   395 	
   396 	[menu addItemWithTitle:AILocalizedString(@"Deop", nil)
   397 					target:self
   398 					action:@selector(deop)
   399 			 keyEquivalent:@""
   400 					   tag:AIRequiresOp];
   401 	
   402 	[menu addItemWithTitle:AILocalizedString(@"Voice", nil)
   403 					target:self
   404 					action:@selector(voice)
   405 			 keyEquivalent:@""
   406 					   tag:AIRequiresOp];
   407 	
   408 	[menu addItemWithTitle:AILocalizedString(@"Devoice", nil)
   409 					target:self
   410 					action:@selector(devoice)
   411 			 keyEquivalent:@""
   412 					   tag:AIRequiresOp];
   413 
   414 	[menu addItem:[NSMenuItem separatorItem]];
   415 	
   416 	[menu addItemWithTitle:AILocalizedString(@"Kick", nil)
   417 					target:self
   418 					action:@selector(kick)
   419 			 keyEquivalent:@""
   420 					   tag:AIRequiresHalfop];
   421 	
   422 	[menu addItemWithTitle:AILocalizedString(@"Ban", nil)
   423 					target:self
   424 					action:@selector(ban)
   425 			 keyEquivalent:@""
   426 					   tag:AIRequiresHalfop];
   427 	
   428 	[menu addItemWithTitle:AILocalizedString(@"Bankick", nil)
   429 					target:self
   430 					action:@selector(bankick)
   431 			 keyEquivalent:@""
   432 					   tag:AIRequiresHalfop];
   433 	
   434 	return menu;
   435 }
   436 
   437 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
   438 {
   439 	AIOperationRequirement req = menuItem.tag;
   440 	AIChat *chat = adium.interfaceController.activeChat;
   441 	
   442 	if (!chat.chatContainer.messageViewController.selectedListObjects.count) {
   443 		return NO;
   444 	}
   445 	
   446 	AIGroupChatFlags flags = [self flagsInChat:chat];
   447 	
   448 	switch (req) {
   449 		case AIRequiresHalfop:
   450 			return ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp);
   451 			break;
   452 			
   453 		case AIRequiresOp:
   454 			return ((flags & AIGroupChatOp) == AIGroupChatOp);
   455 			break;
   456 	}
   457 	
   458 	return NO;
   459 }
   460 
   461 #pragma mark Action Menu's Actions
   462 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag
   463 {
   464 	AIChat *chat = adium.interfaceController.activeChat;
   465 	NSArray *objects = chat.chatContainer.messageViewController.selectedListObjects;
   466 	
   467 	NSMutableString *names = [NSMutableString string];
   468 	
   469 	for (NSUInteger x = 0; x < objects.count; x++) {
   470 		[names appendString:((AIListObject *)[objects objectAtIndex:x]).UID];
   471 		[names appendString:@" "];
   472 		
   473 		if ((x+1) % 4 == 0 || x+1 == objects.count) {
   474 			if ([operation isEqualToString:@"MODE"]) {
   475 				[self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@",
   476 									  chat.name,
   477 									  (apply ? @"+" : @"-"),
   478 									  [@"" stringByPaddingToLength:(x + 1) % 4 ?: 4
   479 														withString:flag 
   480 												   startingAtIndex:0],
   481 									  names]];
   482 			} else if ([operation isEqualToString:@"KICK"]) {
   483 				[self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@",
   484 									  chat.name,
   485 									  [names stringByReplacingOccurrencesOfString:@" " withString:@","]]];
   486 			}
   487 			
   488 			[names setString:@""];
   489 		}
   490 	}
   491 }
   492 
   493 - (void)op
   494 {
   495 	[self apply:YES operation:@"MODE" flag:@"o"];
   496 }
   497 
   498 - (void)deop
   499 {
   500 	[self apply:NO operation:@"MODE" flag:@"o"];
   501 }
   502 
   503 - (void)voice
   504 {
   505 	[self apply:YES operation:@"MODE" flag:@"v"];
   506 }
   507 
   508 - (void)devoice
   509 {
   510 	[self apply:NO operation:@"MODE" flag:@"v"];
   511 }
   512 
   513 - (void)kick
   514 {
   515 	[self apply:NO operation:@"KICK" flag:nil];
   516 }
   517 
   518 - (void)ban
   519 {
   520 	[self apply:YES operation:@"MODE" flag:@"b"];
   521 }
   522 
   523 - (void)bankick
   524 {
   525 	[self ban];
   526 	[self kick];
   527 }
   528 
   529 @end