adium/adium

Updated Changes.txt for 1.5.10.4.
adium-1.5.10.4
2017-04-25, Robert Vehse
09440b7b46e0
Updated Changes.txt for 1.5.10.4.
/*
* 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 "ESIRCAccount.h"
#import <Adium/AIHTMLDecoder.h>
#import <Adium/AIChat.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;
@end
@interface ESIRCAccount()
- (void)sendRawCommand:(NSString *)command;
- (void)apply:(BOOL)apply operation:(NSString *)operation flag:(NSString *)flag;
- (void)op;
- (void)deop;
- (void)devoice;
- (void)kick;
- (void)ban;
- (void)bankick;
@end
static PurpleConversation *fakeConversation(PurpleAccount *account);
@implementation ESIRCAccount
/*!
* @brief Our explicit formatted UID contains our hostname, so we can differentiate ourself.
*/
- (NSString *)explicitFormattedUID
{
if (self.host) {
return [NSString stringWithFormat:@"%@ (%@)", self.host, self.displayName];
} else {
return self.displayName;
}
}
#pragma mark IRC-ism overloads
/*!
* @brief We always want to autocomplete the UID.
*/
- (BOOL)chatShouldAutocompleteUID:(AIChat *)inChat
{
return YES;
}
/*!
* @brief Use the object ID for password name
*
* We mess around a lot with the UID. This lets it actually save right.
*/
- (BOOL)useInternalObjectIDForPasswordName
{
return YES;
}
- (BOOL)openChat:(AIChat *)chat
{
chat.hideUserIconAndStatus = YES;
return [super openChat:chat];
}
- (BOOL)shouldDisplayOutgoingMUCMessages
{
return NO;
}
/*!
* @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
/*!
* @brief We've connected
*
* Send the commands the user wants sent when we do so. Creates a fake conversation to pipe them through.
*/
- (void)didConnect
{
[super didConnect];
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];
if (command.length) {
char *error;
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.
g_free(conv);
// 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];
}
/*!
* @brief Send a raw command to the IRC server.
*/
- (void)sendRawCommand:(NSString *)command
{
PurpleConnection *connection = purple_account_get_connection(account);
if (!connection)
return;
const char *quote = [command UTF8String];
irc_cmd_quote(connection->proto_data, NULL, NULL, &quote);
}
/*!
* @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); */
conv->account = account;
return conv;
}
- (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) {
if (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;
/* Remove the "/say" */
if (hasSlashSay)
messageAttributedString = [messageAttributedString attributedSubstringFromRange:NSMakeRange([@"/say " length],
messageAttributedString.length - [@"/say " length])];
encodedString = [AIHTMLDecoder encodeHTML:(inContentMessage.chat.isSecure ?
messageAttributedString :
[messageAttributedString attributedStringByConvertingLinksToURLStrings])
headers:NO
fontTags:YES
includingColorTags:YES
closeFontTags:YES
styleTags:YES
closeStyleTagsOnFontChange:YES
encodeNonASCII:NO
encodeSpaces:NO
imagesPath:nil
attachmentsAsText:YES
onlyIncludeOutgoingImages:NO
simpleTagsOnly:YES
bodyBackground: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]];
return nil;
} else {
return encodedString;
}
}
#pragma mark Libpurple
- (const char *)protocolPlugin
{
return "prpl-irc";
}
- (const char *)purpleAccountName
{
return [[NSString stringWithFormat:@"%@@%@", self.formattedUID, self.host] UTF8String];
}
- (NSString *)defaultUsername
{
return @"Adium";
}
- (NSString *)defaultRealname
{
return AILocalizedString(@"Adium User", nil);
}
- (void)configurePurpleAccount
{
[super configurePurpleAccount];
purple_account_set_username(self.purpleAccount, self.purpleAccountName);
// Encoding
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);
}
// Use SSL
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]);
}
/*!
* @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.
if (account) {
PurpleConnection *purpleConnection = purple_account_get_connection(account);
if (purpleConnection) {
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");
if (pass)
[dict setObject: [NSString stringWithUTF8String:pass] forKey:@"password"];
return dict;
}
/*!
* @brief Should an autoreply be sent to this message?
*/
- (BOOL)shouldSendAutoreplyToMessage:(AIContentMessage *)message
{
return NO;
}
#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]];
} else {
[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;
BOOL shouldLog = YES;
if (source && (([source caseInsensitiveCompare:@"nickserv"] == NSOrderedSame) ||
([source caseInsensitiveCompare:@"chanserv"] == NSOrderedSame) ||
([source rangeOfString:@"-connect" options:(NSBackwardsSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound))) {
shouldLog = NO;
}
return (shouldLog && [super shouldLogChat:chat]);
}
#pragma mark Chat handling
/*!
* @brief Allow the chat to close unless we're quitting.
*/
- (BOOL)closeChat:(AIChat*)chat
{
if(adium.isQuitting)
return NO;
else
return [super closeChat:chat];
}
/*!
* @brief Do group chats support topics?
*/
- (BOOL)groupChatsSupportTopic
{
return YES;
}
/*!
* @brief Our flags in a chat
*/
- (AIGroupChatFlags)flagsInChat:(AIChat *)chat
{
NSString *ourUID = [NSString stringWithUTF8String:purple_normalize(self.purpleAccount, [self.displayName UTF8String])];
// XXX Once we don't create a fake contact for ourself, we should do this the right way.
return [chat flagsForContact:[self contactWithUID:ourUID]];
}
#pragma mark Action Menu
-(NSMenu*)actionMenuForChat:(AIChat*)chat
{
NSMenu *menu;
NSArray *listObjects = chat.chatContainer.messageViewController.selectedListObjects;
AIListObject *listObject = nil;
if (listObjects.count) {
listObject = [listObjects objectAtIndex:0];
}
menu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
[NSNumber numberWithInteger:Context_Contact_GroupChat_ParticipantAction],
[NSNumber numberWithInteger:Context_Contact_Manage],
nil]
forListObject:listObject
inChat:chat];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:AILocalizedString(@"Op", nil)
target:self
action:@selector(op)
keyEquivalent:@""
tag:AIRequiresOp];
[menu addItemWithTitle:AILocalizedString(@"Deop", nil)
target:self
action:@selector(deop)
keyEquivalent:@""
tag:AIRequiresOp];
[menu addItemWithTitle:AILocalizedString(@"Voice", nil)
target:self
action:@selector(voice)
keyEquivalent:@""
tag:AIRequiresOp];
[menu addItemWithTitle:AILocalizedString(@"Devoice", nil)
target:self
action:@selector(devoice)
keyEquivalent:@""
tag:AIRequiresOp];
[menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:AILocalizedString(@"Kick", nil)
target:self
action:@selector(kick)
keyEquivalent:@""
tag:AIRequiresHalfop];
[menu addItemWithTitle:AILocalizedString(@"Ban", nil)
target:self
action:@selector(ban)
keyEquivalent:@""
tag:AIRequiresHalfop];
[menu addItemWithTitle:AILocalizedString(@"Bankick", nil)
target:self
action:@selector(bankick)
keyEquivalent:@""
tag:AIRequiresHalfop];
return menu;
}
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
AIOperationRequirement req = (AIOperationRequirement)menuItem.tag;
AIChat *chat = adium.interfaceController.activeChat;
BOOL anySelected = chat.chatContainer.messageViewController.selectedListObjects.count > 0;
AIGroupChatFlags flags = [self flagsInChat:chat];
switch (req) {
case AIRequiresHalfop:
return (anySelected && ((flags & AIGroupChatOp) == AIGroupChatOp || (flags & AIGroupChatHalfOp) == AIGroupChatHalfOp));
break;
case AIRequiresOp:
return (anySelected && ((flags & AIGroupChatOp) == AIGroupChatOp));
break;
case AIRequiresNoLevel:
return anySelected;
break;
default:
return YES;
break;
}
}
#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"]]];
} else {
[names appendString:listObject.UID];
}
[names appendString:@" "];
if ((x+1) % 4 == 0 || x+1 == objects.count) {
if ([operation isEqualToString:@"MODE"]) {
[self sendRawCommand:[NSString stringWithFormat:@"MODE %@ %@%@ %@",
chat.name,
(apply ? @"+" : @"-"),
[@"" stringByPaddingToLength:(x + 1) % 4 ?: 4
withString:flag
startingAtIndex:0],
names]];
} else if ([operation isEqualToString:@"KICK"]) {
[self sendRawCommand:[NSString stringWithFormat:@"KICK %@ %@",
chat.name,
[names stringByReplacingOccurrencesOfString:@" " withString:@","]]];
}
[names setString:@""];
}
}
}
- (void)op
{
[self apply:YES operation:@"MODE" flag:@"o"];
}
- (void)deop
{
[self apply:NO operation:@"MODE" flag:@"o"];
}
- (void)voice
{
[self apply:YES operation:@"MODE" flag:@"v"];
}
- (void)devoice
{
[self apply:NO operation:@"MODE" flag:@"v"];
}
- (void)kick
{
[self apply:NO operation:@"KICK" flag:nil];
}
- (void)ban
{
[self apply:YES operation:@"MODE" flag:@"b"];
}
- (void)bankick
{
[self ban];
[self kick];
}
#pragma mark File transfer
- (BOOL)canSendFolders
{
return NO;
}
- (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];
}
@end