Don't validate command menu items when no list objects are selected.
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];
74 #pragma mark Command handling
76 * @brief We've connected
78 * Send the commands the user wants sent when we do so. Creates a fake conversation to pipe them through.
84 PurpleConversation *conv = fakeConversation(self.purpleAccount);
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];
92 command = [command stringByReplacingOccurrencesOfString:@"$me" withString:self.displayName];
96 PurpleCmdStatus cmdStatus = purple_cmd_do_command(conv, [command UTF8String], [command UTF8String], &error);
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]);
108 // The fakeConversation was allocated; now free it.
113 * @brief Send a raw command to the IRC server.
115 - (void)sendRawCommand:(NSString *)command
117 PurpleConnection *connection = purple_account_get_connection(account);
122 const char *quote = [command UTF8String];
123 irc_cmd_quote(connection->proto_data, NULL, NULL, "e);
127 * @brief This creates a fake PurpleConversation
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.
132 * This is taken from irchelper.c, the pidgin plugin.
134 static PurpleConversation *fakeConversation(PurpleAccount *account)
136 PurpleConversation *conv;
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;
150 - (NSString *)encodedAttributedStringForSendingContentMessage:(AIContentMessage *)inContentMessage
153 NSString *encodedString = nil;
154 BOOL didCommand = [self.purpleAdapter attemptPurpleCommandOnMessage:inContentMessage.message.string
155 fromAccount:(AIAccount *)inContentMessage.source
156 inChat:inContentMessage.chat];
158 NSRange meRange = [inContentMessage.message.string rangeOfString:@"/me " options:NSCaseInsensitiveSearch];
160 if (!didCommand || meRange.location == 0) {
161 if (meRange.location == 0) {
162 inContentMessage.sendContent = NO;
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)"
167 encodedString = [AIHTMLDecoder encodeHTML:(inContentMessage.chat.isSecure ? inContentMessage.message : [inContentMessage.message attributedStringByConvertingLinksToStrings])
170 includingColorTags:YES
173 closeStyleTagsOnFontChange:YES
177 attachmentsAsText:YES
178 onlyIncludeOutgoingImages:NO
181 allowJavascriptURLs:YES];
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]];
189 return encodedString;
193 #pragma mark Libpurple
194 - (const char *)protocolPlugin
199 - (const char *)purpleAccountName
201 return [[NSString stringWithFormat:@"%@@%@", self.formattedUID, self.host] UTF8String];
204 - (NSString *)defaultUsername
209 - (NSString *)defaultRealname
211 return AILocalizedString(@"Adium User", nil);
214 - (void)configurePurpleAccount
216 [super configurePurpleAccount];
218 purple_account_set_username(self.purpleAccount, self.purpleAccountName);
221 NSString *encoding = [self preferenceForKey:KEY_IRC_ENCODING group:GROUP_ACCOUNT_STATUS] ?: @"UTF-8";
222 purple_account_set_string(self.purpleAccount, "encoding", [encoding UTF8String]);
224 if (![encoding isEqualToString:@"UTF-8"]) {
225 purple_account_set_bool(self.purpleAccount, "autodetect_utf8", TRUE);
229 BOOL useSSL = [[self preferenceForKey:KEY_IRC_USE_SSL group:GROUP_ACCOUNT_STATUS] boolValue];
230 purple_account_set_bool(self.purpleAccount, "ssl", useSSL);
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]);
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]);
242 * @brief Our display name; either retrieve our current nickname, or return our stored one.
244 - (NSString *)displayName
246 // Try and get the purple display name, since it changes without telling us.
248 PurpleConnection *purpleConnection = purple_account_get_connection(account);
250 if (purpleConnection) {
251 return [NSString stringWithUTF8String:purple_connection_get_display_name(purpleConnection)];
255 return self.formattedUID;
260 * @brief Re-create the chat's join options.
262 - (NSDictionary *)extractChatCreationDictionaryFromConversation:(PurpleConversation *)conv
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");
268 [dict setObject: [NSString stringWithUTF8String:pass] forKey:@"password"];
273 #pragma mark Server contacts (NickServ, ChanServ)
275 * @brief Sends a raw command to identify for the nickname
277 - (void)identifyForName:(NSString *)name password:(NSString *)inPassword
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]];
286 [self sendRawCommand:[NSString stringWithFormat:@"NICKSERV identify %@ %@", name, inPassword]];
291 * @brief Is this contact a server contact?
293 BOOL contactUIDIsServerContact(NSString *contactUID)
295 return (([contactUID caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
296 ([contactUID caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
297 ([contactUID rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound));
301 * @brief Can we send an offline message to this contact?
303 * We can only send offline messages to the server contacts, since such a message might cause us to connect
305 - (BOOL)canSendOfflineMessageToContact:(AIListContact *)inContact
307 return contactUIDIsServerContact(inContact.UID);
311 * @brief Don't autoreply to server contacts (services) or FreeNode's stupidity.
313 - (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
315 return !contactUIDIsServerContact(message.source.UID);
319 * @brief Don't log server contacts (services) or FreeNode's stupidity.
321 - (BOOL)shouldLogChat:(AIChat *)chat
323 NSString *source = chat.listObject.UID;
324 BOOL shouldLog = YES;
326 if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
327 ([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
328 ([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) {
332 return (shouldLog && [super shouldLogChat:chat]);
335 #pragma mark Chat handling
338 * @brief Allow the chat to close unless we're quitting.
340 - (BOOL)closeChat:(AIChat*)chat
345 return [super closeChat:chat];
349 * @brief Do group chats support topics?
351 - (BOOL)groupChatsSupportTopic
357 * @brief Our flags in a chat
359 - (AIGroupChatFlags)flagsInChat:(AIChat *)chat
361 NSString *ourUID = [NSString stringWithUTF8String:purple_normalize(self.purpleAccount, [self.displayName UTF8String])];
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]];
367 #pragma mark Action Menu
368 -(NSMenu*)actionMenuForChat:(AIChat*)chat
372 NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects;
373 AIListObject *listObject = nil;
375 if (listObjects.count) {
376 listObject = [listObjects objectAtIndex:0];
379 menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
380 [NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction],
381 [NSNumber numberWithInteger:Context_Contact_Manage],
383 forListObject:listObject
388 [menu addItem:[NSMenuItem separatorItem]];
390 [menu addItemWithTitle:AILocalizedString(@"Op", nil)
396 [menu addItemWithTitle:AILocalizedString(@"Deop", nil)
398 action:@selector(deop)
402 [menu addItemWithTitle:AILocalizedString(@"Voice", nil)
404 action:@selector(voice)
408 [menu addItemWithTitle:AILocalizedString(@"Devoice", nil)
410 action:@selector(devoice)
414 [menu addItem:[NSMenuItem separatorItem]];
416 [menu addItemWithTitle:AILocalizedString(@"Kick", nil)
418 action:@selector(kick)
420 tag:AIRequiresHalfop];
422 [menu addItemWithTitle:AILocalizedString(@"Ban", nil)
424 action:@selector(ban)
426 tag:AIRequiresHalfop];
428 [menu addItemWithTitle:AILocalizedString(@"Bankick", nil)
430 action:@selector(bankick)
432 tag:AIRequiresHalfop];
437 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
439 AIOperationRequirement req = menuItem.tag;
440 AIChat *chat = adium.interfaceController.activeChat;
442 if (!chat.chatContainer.messageViewController.selectedListObjects.count) {
446 AIGroupChatFlags flags = [self flagsInChat:chat];
449 case AIRequiresHalfop:
450 return ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp);
454 return ((flags & AIGroupChatOp) == AIGroupChatOp);
461 #pragma mark Action Menu's Actions
462 - (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag
464 AIChat *chat = adium.interfaceController.activeChat;
465 NSArray *objects = chat.chatContainer.messageViewController.selectedListObjects;
467 NSMutableString *names = [NSMutableString string];
469 for (NSUInteger x = 0; x < objects.count; x++) {
470 [names appendString:((AIListObject *)[objects objectAtIndex:x]).UID];
471 [names appendString:@" "];
473 if ((x+1) % 4 == 0 || x+1 == objects.count) {
474 if ([operation isEqualToString:@"MODE"]) {
475 [self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@",
477 (apply ? @"+" : @"-"),
478 [@"" stringByPaddingToLength:(x + 1) % 4 ?: 4
482 } else if ([operation isEqualToString:@"KICK"]) {
483 [self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@",
485 [names stringByReplacingOccurrencesOfString:@" " withString:@","]]];
488 [names setString:@""];
495 [self apply:YES operation:@"MODE" flag:@"o"];
500 [self apply:NO operation:@"MODE" flag:@"o"];
505 [self apply:YES operation:@"MODE" flag:@"v"];
510 [self apply:NO operation:@"MODE" flag:@"v"];
515 [self apply:NO operation:@"KICK" flag:nil];
520 [self apply:YES operation:@"MODE" flag:@"b"];