You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
konversation/konversation/src/outputfilter.cpp

1791 lines
63 KiB

/*
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.
*/
/*
Copyright (C) 2002 Dario Abatianni <eisfuchs@tigress.com>
Copyright (C) 2005 Ismail Donmez <ismail@kde.org>
Copyright (C) 2005 Peter Simonsson <psn@linux.se>
Copyright (C) 2005 John Tapsell <johnflux@gmail.com>
Copyright (C) 2005-2008 Eike Hein <hein@kde.org>
*/
#include "outputfilter.h"
#include "konversationapplication.h"
#include "konversationmainwindow.h"
#include "awaymanager.h"
#include "ignore.h"
#include "server.h"
#include "irccharsets.h"
#include "linkaddressbook/addressbook.h"
#include "query.h"
#include <tqstringlist.h>
#include <tqfile.h>
#include <tqfileinfo.h>
#include <tqregexp.h>
#include <tqmap.h>
#include <tqvaluelist.h>
#include <tqtextcodec.h>
#include <klocale.h>
#include <kdebug.h>
#include <tdeio/passdlg.h>
#include <tdeconfig.h>
#include <tdeversion.h>
#include <kshell.h>
#include <tdesocketaddress.h>
#include <kresolver.h>
#include <kreverseresolver.h>
#include <kmessagebox.h>
namespace Konversation
{
OutputFilter::OutputFilter(Server* server)
: TQObject(server)
{
m_server = server;
}
OutputFilter::~OutputFilter()
{
}
// replace all aliases in the string and return true if anything got replaced at all
bool OutputFilter::replaceAliases(TQString& line)
{
TQStringList aliasList=Preferences::aliasList();
TQString cc(Preferences::commandChar());
// check if the line starts with a defined alias
for(unsigned int index=0;index<aliasList.count();index++)
{
// cut alias pattern from definition
TQString aliasPattern(aliasList[index].section(' ',0,0));
// cut first word from command line, so we do not wrongly find an alias
// that starts with the same letters, like /m would override /me
TQString lineStart=line.section(' ',0,0);
// pattern found?
// TODO: cc may be a regexp character here ... we should escape it then
if (lineStart==cc+aliasPattern)
{
TQString aliasReplace;
// cut alias replacement from definition
if ( aliasList[index].contains("%p") )
aliasReplace = aliasList[index].section(' ',1);
else
aliasReplace = aliasList[index].section(' ',1 )+' '+line.section(' ',1 );
// protect "%%"
aliasReplace.replace("%%","%\x01");
// replace %p placeholder with rest of line
aliasReplace.replace("%p",line.section(' ',1));
// restore "%<1>" as "%%"
aliasReplace.replace("%\x01","%%");
// modify line
line=aliasReplace;
// return "replaced"
return true;
}
} // for
return false;
}
TQStringList OutputFilter::splitForEncoding(const TQString& inputLine, uint max)
{
uint sublen = 0; //The encoded length since the last split
int charLength = 0; //the length of this char
int lastBreakPoint = 0;
//FIXME should we run this through the encoder first, checking with "canEncode"?
TQString text = inputLine; // the text we'll send, currently in Unicode
TQStringList finals; // The strings we're going to output
TQString channelCodecName=Preferences::channelEncoding(m_server->getDisplayName(), destination);
//Get the codec we're supposed to use. This must not fail. (not verified)
TQTextCodec* codec;
// I copied this bit straight out of Server::send
if (channelCodecName.isEmpty())
{
codec = m_server->getIdentity()->getCodec();
}
else
{
codec = Konversation::IRCCharsets::self()->codecForName(channelCodecName);
}
Q_ASSERT(codec);
int index = 0;
while(text.length() > max)
{
// The most important bit - turn the current char into a TQCString so we can measure it
TQCString ch = codec->fromUnicode(TQString(text[index]));
charLength = ch.length();
// If adding this char puts us over the limit:
if (charLength + sublen > max)
{
if(lastBreakPoint != 0)
{
finals.append(text.left(lastBreakPoint + 1));
text = text.mid(lastBreakPoint + 1);
}
else
{
finals.append(text.left(index));
text = text.mid(index);
}
lastBreakPoint = 0;
sublen = 0;
index = 0;
}
else if (text[index].isSpace() || text[index].isPunct())
{
lastBreakPoint = index;
}
++index;
sublen += charLength;
}
if (!text.isEmpty())
{
finals.append(text);
}
return finals;
}
OutputFilterResult OutputFilter::parse(const TQString& myNick,const TQString& originalLine,const TQString& name)
{
setCommandChar();
OutputFilterResult result;
destination=name;
TQString inputLine(originalLine);
if(inputLine.isEmpty() || inputLine == "\n")
return result;
//Protect against nickserv auth being sent as a message on the off chance
// someone didn't notice leading spaces
{
TQString testNickServ( inputLine.stripWhiteSpace() );
if(testNickServ.startsWith(commandChar+"nickserv", false)
|| testNickServ.startsWith(commandChar+"ns", false))
{
inputLine = testNickServ;
}
}
if(!Preferences::disableExpansion())
{
// replace placeholders
inputLine.replace("%%","%\x01"); // make sure to protect double %%
inputLine.replace("%B","\x02"); // replace %B with bold char
inputLine.replace("%C","\x03"); // replace %C with color char
inputLine.replace("%G","\x07"); // replace %G with ASCII BEL 0x07
inputLine.replace("%I","\x09"); // replace %I with italics char
inputLine.replace("%O","\x0f"); // replace %O with reset to default char
inputLine.replace("%S","\x13"); // replace %S with strikethru char
// inputLine.replace(TQRegExp("%?"),"\x15");
inputLine.replace("%R","\x16"); // replace %R with reverse char
inputLine.replace("%U","\x1f"); // replace %U with underline char
inputLine.replace("%\x01","%"); // restore double %% as single %
}
TQString line=inputLine.lower();
// Convert double command chars at the beginning to single ones
if(line.startsWith(commandChar+commandChar) && !destination.isEmpty())
{
inputLine=inputLine.mid(1);
goto BYPASS_COMMAND_PARSING;
}
// Server command?
else if(line.startsWith(commandChar))
{
TQString command = inputLine.section(' ', 0, 0).mid(1).lower();
TQString parameter = inputLine.section(' ', 1);
if (command !="topic")
parameter = parameter.stripWhiteSpace();
if (command == "join") result = parseJoin(parameter);
else if(command == "part") result = parsePart(parameter);
else if(command == "leave") result = parsePart(parameter);
else if(command == "quit") result = parseQuit(parameter);
else if(command == "close") result = parseClose(parameter);
else if(command == "notice") result = parseNotice(parameter);
else if(command == "j") result = parseJoin(parameter);
else if(command == "me") result = parseMe(parameter, destination);
else if(command == "msg") result = parseMsg(myNick,parameter, false);
else if(command == "m") result = parseMsg(myNick,parameter, false);
else if(command == "smsg") result = parseSMsg(parameter);
else if(command == "query") result = parseMsg(myNick,parameter, true);
else if(command == "op") result = parseOp(parameter);
else if(command == "deop") result = parseDeop(myNick,parameter);
else if(command == "hop") result = parseHop(parameter);
else if(command == "dehop") result = parseDehop(myNick,parameter);
else if(command == "voice") result = parseVoice(parameter);
else if(command == "unvoice") result = parseUnvoice(myNick,parameter);
else if(command == "devoice") result = parseUnvoice(myNick,parameter);
else if(command == "ctcp") result = parseCtcp(parameter);
else if(command == "ping") result = parseCtcp(parameter.section(' ', 0, 0) + " ping");
else if(command == "kick") result = parseKick(parameter);
else if(command == "topic") result = parseTopic(parameter);
else if(command == "away") parseAway(parameter);
else if(command == "unaway") parseBack();
else if(command == "back") parseBack();
else if(command == "invite") result = parseInvite(parameter);
else if(command == "exec") result = parseExec(parameter);
else if(command == "notify") result = parseNotify(parameter);
else if(command == "oper") result = parseOper(myNick,parameter);
else if(command == "ban") result = parseBan(parameter);
else if(command == "unban") result = parseUnban(parameter);
else if(command == "kickban") result = parseBan(parameter,true);
else if(command == "ignore") result = parseIgnore(parameter);
else if(command == "unignore") result = parseUnignore(parameter);
else if(command == "quote") result = parseQuote(parameter);
else if(command == "say") result = parseSay(parameter);
else if(command == "list") result = parseList(parameter);
else if(command == "names") result = parseNames(parameter);
else if(command == "raw") result = parseRaw(parameter);
else if(command == "dcc") result = parseDcc(parameter);
else if(command == "konsole") parseKonsole();
else if(command == "aaway") KonversationApplication::instance()->getAwayManager()->requestAllAway(parameter);
else if(command == "aunaway") KonversationApplication::instance()->getAwayManager()->requestAllUnaway();
else if(command == "aback") KonversationApplication::instance()->getAwayManager()->requestAllUnaway();
else if(command == "ame") result = parseAme(parameter);
else if(command == "amsg") result = parseAmsg(parameter);
else if(command == "omsg") result = parseOmsg(parameter);
else if(command == "onotice") result = parseOnotice(parameter);
else if(command == "server") parseServer(parameter);
else if(command == "reconnect") emit reconnectServer();
else if(command == "disconnect") emit disconnectServer();
else if(command == "charset") result = parseCharset(parameter);
else if(command == "encoding") result = parseCharset(parameter);
else if(command == "setkey") result = parseSetKey(parameter);
else if(command == "delkey") result = parseDelKey(parameter);
else if(command == "showkey") result = parseShowKey(parameter);
else if(command == "dns") result = parseDNS(parameter);
else if(command == "kill") result = parseKill(parameter);
else if(command == "queuetuner") result = parseShowTuner(parameter);
// Forward unknown commands to server
else
{
result.toServer = inputLine.mid(1);
result.type = Message;
}
}
// Ordinary message to channel/query?
else if(!destination.isEmpty())
{
BYPASS_COMMAND_PARSING:
TQStringList outputList=splitForEncoding(inputLine, m_server->getPreLength("PRIVMSG", destination));
if (outputList.count() > 1)
{
result.output=TQString();
result.outputList=outputList;
for ( TQStringList::Iterator it = outputList.begin(); it != outputList.end(); ++it )
{
result.toServerList += "PRIVMSG " + destination + " :" + *it;
}
}
else
{
result.output = inputLine;
result.toServer = "PRIVMSG " + destination + " :" + inputLine;
}
result.type = Message;
}
// Eveything else goes to the server unchanged
else
{
result.toServer = inputLine;
result.output = inputLine;
result.typeString = i18n("Raw");
result.type = Program;
}
return result;
}
OutputFilterResult OutputFilter::parseShowTuner(const TQString &parameter)
{
KonversationApplication *konvApp = static_cast<KonversationApplication*>(TDEApplication::kApplication());
OutputFilterResult result;
if(parameter.isEmpty() || parameter == "on")
konvApp->showQueueTuner(true);
else if(parameter == "off")
konvApp->showQueueTuner(false);
else
result = usage(i18n("Usage: %1queuetuner [on | off]").arg(commandChar));
return result;
}
OutputFilterResult OutputFilter::parseOp(const TQString &parameter)
{
return changeMode(parameter,'o','+');
}
OutputFilterResult OutputFilter::parseDeop(const TQString &ownNick, const TQString &parameter)
{
return changeMode(addNickToEmptyNickList(ownNick,parameter),'o','-');
}
OutputFilterResult OutputFilter::parseHop(const TQString &parameter)
{
return changeMode(parameter, 'h', '+');
}
OutputFilterResult OutputFilter::parseDehop(const TQString &ownNick, const TQString &parameter)
{
return changeMode(addNickToEmptyNickList(ownNick,parameter), 'h', '-');
}
OutputFilterResult OutputFilter::parseVoice(const TQString &parameter)
{
return changeMode(parameter,'v','+');
}
OutputFilterResult OutputFilter::parseUnvoice(const TQString &ownNick, const TQString &parameter)
{
return changeMode(addNickToEmptyNickList(ownNick,parameter),'v','-');
}
OutputFilterResult OutputFilter::parseJoin(TQString& channelName)
{
OutputFilterResult result;
if(channelName.contains(",")) // Protect against #foo,0 tricks
channelName = channelName.remove(",0");
//else if(channelName == "0") // FIXME IRC RFC 2812 section 3.2.1
if (channelName.isEmpty())
{
if (destination.isEmpty() || !isAChannel(destination))
return usage(i18n("Usage: %1JOIN <channel> [password]").arg(commandChar));
channelName=destination;
}
else if (!isAChannel(channelName))
channelName = "#" + channelName.stripWhiteSpace();
Channel* channel = m_server->getChannelByName(channelName);
if (channel)
{
// Note that this relies on the channels-flush-nicklists-on-disconnect behavior.
if (!channel->numberOfNicks())
result.toServer = "JOIN " + channelName;
if (channel->joined()) emit showView (channel);
}
else
result.toServer = "JOIN " + channelName;
return result;
}
OutputFilterResult OutputFilter::parseKick(const TQString &parameter)
{
OutputFilterResult result;
if(isAChannel(destination))
{
// get nick to kick
TQString victim = parameter.left(parameter.find(" "));
if(victim.isEmpty())
{
result = usage(i18n("Usage: %1KICK <nick> [reason]").arg(commandChar));
}
else
{
// get kick reason (if any)
TQString reason = parameter.mid(victim.length() + 1);
// if no reason given, take default reason
if(reason.isEmpty())
{
reason = m_server->getIdentity()->getKickReason();
}
result.toServer = "KICK " + destination + ' ' + victim + " :" + reason;
}
}
else
{
result = error(i18n("%1KICK only works from within channels.").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::parsePart(const TQString &parameter)
{
OutputFilterResult result;
// No parameter, try default part message
if(parameter.isEmpty())
{
// But only if we actually are in a channel
if(isAChannel(destination))
{
result.toServer = "PART " + destination + " :" + m_server->getIdentity()->getPartReason();
}
else
{
result = error(i18n("%1PART without parameters only works from within a channel or a query.").arg(commandChar));
}
}
else
{
// part a given channel
if(isAChannel(parameter))
{
// get channel name
TQString channel = parameter.left(parameter.find(" "));
// get part reason (if any)
TQString reason = parameter.mid(channel.length() + 1);
// if no reason given, take default reason
if(reason.isEmpty())
{
reason = m_server->getIdentity()->getPartReason();
}
result.toServer = "PART " + channel + " :" + reason;
}
// part this channel with a given reason
else
{
if(isAChannel(destination))
{
result.toServer = "PART " + destination + " :" + parameter;
}
else
{
result = error(i18n("%1PART without channel name only works from within a channel.").arg(commandChar));
}
}
}
return result;
}
OutputFilterResult OutputFilter::parseTopic(const TQString &parameter)
{
OutputFilterResult result;
// No parameter, try to get current topic
if(parameter.isEmpty())
{
// But only if we actually are in a channel
if(isAChannel(destination))
{
result.toServer = "TOPIC " + destination;
}
else
{
result = error(i18n("%1TOPIC without parameters only works from within a channel.").arg(commandChar));
}
}
else
{
// retrieve or set topic of a given channel
if(isAChannel(parameter))
{
// get channel name
TQString channel=parameter.left(parameter.find(" "));
// get topic (if any)
TQString topic=parameter.mid(channel.length()+1);
// if no topic given, retrieve topic
if(topic.isEmpty())
{
m_server->requestTopic(channel);
}
// otherwise set topic there
else
{
result.toServer = "TOPIC " + channel + " :";
//If we get passed a ^A as a topic its a sign we should clear the topic.
//Used to be a \n, but those get smashed by TQStringList::split and readded later
//Now is not the time to fight with that. FIXME
//If anyone out there *can* actually set the topic to a single ^A, now they have to
//specify it twice to get one.
if (topic =="\x01\x01")
result.toServer += '\x01';
else if (topic!="\x01")
result.toServer += topic;
}
}
// set this channel's topic
else
{
if(isAChannel(destination))
{
result.toServer = "TOPIC " + destination + " :" + parameter;
}
else
{
result = error(i18n("%1TOPIC without channel name only works from within a channel.").arg(commandChar));
}
}
}
return result;
}
void OutputFilter::parseAway(TQString& reason)
{
if (reason.isEmpty() && m_server->isAway())
m_server->requestUnaway();
else
m_server->requestAway(reason);
}
void OutputFilter::parseBack()
{
m_server->requestUnaway();
}
OutputFilterResult OutputFilter::parseNames(const TQString &parameter)
{
OutputFilterResult result;
result.toServer = "NAMES ";
if (parameter.isNull())
{
return error(i18n("%1NAMES with no target may disconnect you from the server. Specify '*' if you really want this.").arg(commandChar));
}
else if (parameter != TQChar('*'))
{
result.toServer.append(parameter);
}
return result;
}
OutputFilterResult OutputFilter::parseClose(TQString parm)
{
if (parm.isEmpty())
parm=destination;
if (isAChannel(parm) && m_server->getChannelByName(parm))
m_server->getChannelByName(parm)->closeYourself(false);
else if (m_server->getQueryByName(parm))
m_server->getQueryByName(parm)->closeYourself(false);
else if (parm.isEmpty()) // this can only mean one thing.. we're in the Server tab
m_server->closeYourself(false);
else
return usage(i18n("Usage: %1close [window] closes the named channel or query tab, or the current tab if none specified.").arg(commandChar));
return OutputFilterResult();
}
OutputFilterResult OutputFilter::parseQuit(const TQString &reason)
{
OutputFilterResult result;
result.toServer = "QUIT :";
// if no reason given, take default reason
if(reason.isEmpty())
result.toServer += m_server->getIdentity()->getQuitReason();
else
result.toServer += reason;
return result;
}
OutputFilterResult OutputFilter::parseNotice(const TQString &parameter)
{
OutputFilterResult result;
TQString recipient = parameter.left(parameter.find(" "));
TQString message = parameter.mid(recipient.length()+1);
if(parameter.isEmpty() || message.isEmpty())
{
result = usage(i18n("Usage: %1NOTICE <recipient> <message>").arg(commandChar));
}
else
{
result.typeString = i18n("Notice");
result.toServer = "NOTICE " + recipient + " :" + message;
result.output=i18n("%1 is the message, %2 the recipient nickname","Sending notice \"%2\" to %1.").arg(recipient).arg(message);
result.type = Program;
}
return result;
}
OutputFilterResult OutputFilter::parseMe(const TQString &parameter, const TQString &destination)
{
OutputFilterResult result;
if (!destination.isEmpty() && !parameter.isEmpty())
{
result.toServer = "PRIVMSG " + destination + " :" + '\x01' + "ACTION " + parameter + '\x01';
result.output = parameter;
result.type = Action;
}
else
{
result = usage(i18n("Usage: %1ME text").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::parseMsg(const TQString &myNick, const TQString &parameter, bool isQuery)
{
OutputFilterResult result;
TQString recipient = parameter.section(" ", 0, 0, TQString::SectionSkipEmpty);
TQString message = parameter.section(" ", 1);
TQString output;
if (recipient.isEmpty())
{
result = error("Error: You need to specify a recipient.");
return result;
}
if (isQuery && m_server->isAChannel(recipient))
{
result = error("Error: You cannot open queries to channels.");
return result;
}
if (message.stripWhiteSpace().isEmpty())
{
//empty result - we don't want to send any message to the server
if (!isQuery)
{
result = error("Error: You need to specify a message.");
return result;
}
}
else if (message.startsWith(commandChar+"me"))
{
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + "ACTION " + message.mid(4) + '\x01';
output = TQString("* %1 %2").arg(myNick).arg(message.mid(4));
}
else
{
result.toServer = "PRIVMSG " + recipient + " :" + message;
output = message;
}
::Query *query;
if (isQuery || output.isEmpty())
{
//if this is a /query, always open a query window.
//treat "/msg nick" as "/query nick"
//Note we have to be a bit careful here.
//We only want to create ('obtain') a new nickinfo if we have done /query
//or "/msg nick". Not "/msg nick message".
NickInfoPtr nickInfo = m_server->obtainNickInfo(recipient);
query = m_server->addQuery(nickInfo, true /*we initiated*/);
//force focus if the user did not specify any message
if (output.isEmpty()) emit showView(query);
}
else
{
//We have "/msg nick message"
query = m_server->getQueryByName(recipient);
}
if (query && !output.isEmpty())
{
if (message.startsWith(commandChar+"me"))
//log if and only if the query open
query->appendAction(m_server->getNickname(), message.mid(4));
else
//log if and only if the query open
query->appendQuery(m_server->getNickname(), output);
}
if (output.isEmpty()) return result; //result should be completely empty;
//FIXME - don't do below line if query is focused
result.output = output;
result.typeString= recipient;
result.type = PrivateMessage;
return result;
}
OutputFilterResult OutputFilter::parseSMsg(const TQString &parameter)
{
OutputFilterResult result;
TQString recipient = parameter.left(parameter.find(" "));
TQString message = parameter.mid(recipient.length() + 1);
if(message.startsWith(commandChar + "me"))
{
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + "ACTION " + message.mid(4) + '\x01';
}
else
{
result.toServer = "PRIVMSG " + recipient + " :" + message;
}
return result;
}
OutputFilterResult OutputFilter::parseCtcp(const TQString &parameter)
{
OutputFilterResult result;
// who is the recipient?
TQString recipient = parameter.section(' ', 0, 0);
// what is the first word of the ctcp?
TQString request = parameter.section(' ', 1, 1, TQString::SectionSkipEmpty).upper();
// what is the complete ctcp command?
TQString message = parameter.section(' ', 2, 0xffffff, TQString::SectionSkipEmpty);
TQString out = request;
if (!message.isEmpty())
out+= ' ' + message;
if (request == "PING")
{
unsigned int time_t = TQDateTime::currentDateTime().toTime_t();
result.toServer = TQString("PRIVMSG %1 :\x01PING %2\x01").arg(recipient).arg(time_t);
result.output = i18n("Sending CTCP-%1 request to %2.").arg("PING").arg(recipient);
}
else
{
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + out + '\x01';
result.output = i18n("Sending CTCP-%1 request to %2.").arg(out).arg(recipient);
}
result.typeString = i18n("CTCP");
result.type = Program;
return result;
}
OutputFilterResult OutputFilter::changeMode(const TQString &parameter,char mode,char giveTake)
{
OutputFilterResult result;
// TODO: Make sure this works with +l <limit> and +k <password> also!
TQString token;
TQString tmpToken;
TQStringList nickList = TQStringList::split(' ', parameter);
if(nickList.count())
{
// Check if the user specified a channel
if(isAChannel(nickList[0]))
{
token = "MODE " + nickList[0];
// remove the first element
nickList.remove(nickList.begin());
}
// Add default destination if it is a channel
else if(isAChannel(destination))
{
token = "MODE " + destination;
}
// Only continue if there was no error
if(token.length())
{
unsigned int modeCount = nickList.count();
TQString modes;
modes.fill(mode, modeCount);
token += TQString(" ") + TQChar(giveTake) + modes;
tmpToken = token;
for(unsigned int index = 0; index < modeCount; index++)
{
if((index % 3) == 0)
{
result.toServerList.append(token);
token = tmpToken;
}
token += ' ' + nickList[index];
}
if(token != tmpToken)
{
result.toServerList.append(token);
}
}
}
return result;
}
OutputFilterResult OutputFilter::parseDcc(const TQString &parameter)
{
OutputFilterResult result;
// No parameter, just open DCC panel
if(parameter.isEmpty())
{
emit addDccPanel();
}
else
{
TQString tmpParameter = parameter;
TQStringList parameterList = TQStringList::split(' ', tmpParameter.replace("\\ ", "%20"));
TQString dccType = parameterList[0].lower();
if(dccType=="close")
{
emit closeDccPanel();
}
else if(dccType=="send")
{
if(parameterList.count()==1) // DCC SEND
{
emit requestDccSend();
} // DCC SEND <nickname>
else if(parameterList.count()==2)
{
emit requestDccSend(parameterList[1]);
} // DCC SEND <nickname> <file> [file] ...
else if(parameterList.count()>2)
{
// TODO: make sure this will work:
//output=i18n("Usage: %1DCC SEND nickname [fi6lename] [filename] ...").arg(commandChar);
KURL fileURL(parameterList[2]);
//We could easily check if the remote file exists, but then we might
//end up asking for creditionals twice, so settle for only checking locally
if(!fileURL.isLocalFile() || TQFile::exists( fileURL.path() ))
{
emit openDccSend(parameterList[1],fileURL);
}
else
{
result = error(i18n("File \"%1\" does not exist.").arg(parameterList[2]));
}
}
else // Don't know how this should happen, but ...
{
result = usage(i18n("Usage: %1DCC [SEND nickname filename]").arg(commandChar));
}
}
// TODO: DCC Chat etc. comes here
else if(dccType=="chat")
{
if(parameterList.count()==2)
{
emit openDccChat(parameterList[1]);
}
else
{
result = usage(i18n("Usage: %1DCC [CHAT nickname]").arg(commandChar));
}
}
else
{
result = error(i18n("Unrecognized command %1DCC %2. Possible commands are SEND, CHAT, CLOSE.").arg(commandChar).arg(parameterList[0]));
}
}
return result;
}
OutputFilterResult OutputFilter::sendRequest(const TQString &recipient,const TQString &fileName,const TQString &address,const TQString &port,unsigned long size)
{
OutputFilterResult result;
TQString niftyFileName(fileName);
/*TQFile file(fileName);
TQFileInfo info(file);*/
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + "DCC SEND "
+ fileName
+ ' ' + address + ' ' + port + ' ' + TQString::number(size) + '\x01';
// Dirty hack to avoid printing ""name with spaces.ext"" instead of "name with spaces.ext"
if ((fileName.startsWith("\"")) && (fileName.endsWith("\"")))
niftyFileName = fileName.mid(1, fileName.length()-2);
return result;
}
OutputFilterResult OutputFilter::passiveSendRequest(const TQString& recipient,const TQString &fileName,const TQString &address,unsigned long size,const TQString &token)
{
OutputFilterResult result;
TQString niftyFileName(fileName);
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + "DCC SEND "
+ fileName
+ ' ' + address + " 0 " + TQString::number(size) + ' ' + token + '\x01';
// Dirty hack to avoid printing ""name with spaces.ext"" instead of "name with spaces.ext"
if ((fileName.startsWith("\"")) && (fileName.endsWith("\"")))
niftyFileName = fileName.mid(1, fileName.length()-2);
return result;
}
// Accepting Resume Request
OutputFilterResult OutputFilter::acceptResumeRequest(const TQString &recipient,const TQString &fileName,const TQString &port,int startAt)
{
TQString niftyFileName(fileName);
OutputFilterResult result;
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + "DCC ACCEPT " + fileName + ' ' + port
+ ' ' + TQString::number(startAt) + '\x01';
// Dirty hack to avoid printing ""name with spaces.ext"" instead of "name with spaces.ext"
if ((fileName.startsWith("\"")) && (fileName.endsWith("\"")))
niftyFileName = fileName.mid(1, fileName.length()-2);
return result;
}
OutputFilterResult OutputFilter::resumeRequest(const TQString &sender,const TQString &fileName,const TQString &port,TDEIO::filesize_t startAt)
{
TQString niftyFileName(fileName);
OutputFilterResult result;
/*TQString newFileName(fileName);
newFileName.replace(" ", "_");*/
result.toServer = "PRIVMSG " + sender + " :" + '\x01' + "DCC RESUME " + fileName + ' ' + port + ' '
+ TQString::number(startAt) + '\x01';
// Dirty hack to avoid printing ""name with spaces.ext"" instead of "name with spaces.ext"
if ((fileName.startsWith("\"")) && (fileName.endsWith("\"")))
niftyFileName = fileName.mid(1, fileName.length()-2);
return result;
}
OutputFilterResult OutputFilter::acceptPassiveSendRequest(const TQString& recipient,const TQString &fileName,const TQString &address,const TQString &port,unsigned long size,const TQString &token)
{
OutputFilterResult result;
TQString niftyFileName(fileName);
// "DCC SEND" to receive a file sounds weird, but it's ok.
result.toServer = "PRIVMSG " + recipient + " :" + '\x01' + "DCC SEND "
+ fileName
+ ' ' + address + ' ' + port + ' ' + TQString::number(size) + ' ' + token + '\x01';
// Dirty hack to avoid printing ""name with spaces.ext"" instead of "name with spaces.ext"
if ((fileName.startsWith("\"")) && (fileName.endsWith("\"")))
niftyFileName = fileName.mid(1, fileName.length()-2);
return result;
}
OutputFilterResult OutputFilter::parseInvite(const TQString &parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1INVITE <nick> [channel]").arg(commandChar));
}
else
{
TQString nick = parameter.section(' ', 0, 0, TQString::SectionSkipEmpty);
TQString channel = parameter.section(' ', 1, 1, TQString::SectionSkipEmpty);
if(channel.isEmpty())
{
if(isAChannel(destination))
{
channel = destination;
}
else
{
result = error(i18n("%1INVITE without channel name works only from within channels.").arg(commandChar));
}
}
if(!channel.isEmpty())
{
if(isAChannel(channel))
{
result.toServer = "INVITE " + nick + ' ' + channel;
}
else
{
result = error(i18n("%1 is not a channel.").arg(channel));
}
}
}
return result;
}
OutputFilterResult OutputFilter::parseExec(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1EXEC <script> [parameter list]").arg(commandChar));
}
else
{
TQStringList parameterList = TQStringList::split(' ', parameter);
if(parameterList[0].find("../") == -1)
{
emit launchScript(destination, parameter);
}
else
{
result = error(i18n("Script name may not contain \"../\"!"));
}
}
return result;
}
OutputFilterResult OutputFilter::parseRaw(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty() || parameter == "open")
{
emit openRawLog(true);
}
else if(parameter == "close")
{
emit closeRawLog();
}
else
{
result = usage(i18n("Usage: %1RAW [OPEN | CLOSE]").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::parseNotify(const TQString& parameter)
{
OutputFilterResult result;
TQString groupName = m_server->getDisplayName();
int serverGroupId = -1;
if (m_server->getServerGroup())
serverGroupId = m_server->getServerGroup()->id();
if (!parameter.isEmpty() && serverGroupId != -1)
{
TQStringList list = TQStringList::split(' ', parameter);
for(unsigned int index = 0; index < list.count(); index++)
{
// Try to remove current pattern
if(!Preferences::removeNotify(groupName, list[index]))
{
// If remove failed, try to add it instead
if(!Preferences::addNotify(serverGroupId, list[index]))
{
kdDebug() << "OutputFilter::parseNotify(): Adding failed!" << endl;
}
}
} // endfor
}
// show (new) notify list to user
TQString list = Preferences::notifyStringByGroupName(groupName) + ' ' + Konversation::Addressbook::self()->allContactsNicksForServer(m_server->getServerName(), m_server->getDisplayName()).join(" ");
result.typeString = i18n("Notify");
if(list.isEmpty())
result.output = i18n("Current notify list is empty.");
else
result.output = i18n("Current notify list: %1").arg(list);
result.type = Program;
return result;
}
OutputFilterResult OutputFilter::parseOper(const TQString& myNick,const TQString& parameter)
{
OutputFilterResult result;
TQStringList parameterList = TQStringList::split(' ', parameter);
if(parameter.isEmpty() || parameterList.count() == 1)
{
TQString nick((parameterList.count() == 1) ? parameterList[0] : myNick);
TQString password;
bool keep = false;
int ret = TDEIO::PasswordDialog::getNameAndPassword
(
nick,
password,
&keep,
i18n("Enter user name and password for IRC operator privileges:"),
false,
i18n("IRC Operator Password")
);
if(ret == TDEIO::PasswordDialog::Accepted)
{
result.toServer = "OPER " + nick + ' ' + password;
}
}
else
{
result.toServer = "OPER " + parameter;
}
return result;
}
OutputFilterResult OutputFilter::parseBan(const TQString& parameter, bool kick)
{
OutputFilterResult result;
// assume incorrect syntax first
bool showUsage = true;
if(!parameter.isEmpty())
{
TQStringList parameterList=TQStringList::split(' ',parameter);
TQString channel;
TQString option;
// check for option
TQString lowerParameter = parameterList[0].lower();
bool host = (lowerParameter == "-host");
bool domain = (lowerParameter == "-domain");
bool uhost = (lowerParameter == "-userhost");
bool udomain = (lowerParameter == "-userdomain");
// remove possible option
if (host || domain || uhost || udomain)
{
option = parameterList[0].mid(1);
parameterList.pop_front();
}
// look for channel / ban mask
if (parameterList.count())
{
// user specified channel
if (isAChannel(parameterList[0]))
{
channel = parameterList[0];
parameterList.pop_front();
}
// no channel, so assume current destination as channel
else if (isAChannel(destination))
channel = destination;
else
{
// destination is no channel => error
if (!kick)
result = error(i18n("%1BAN without channel name works only from inside a channel.").arg(commandChar));
else
result = error(i18n("%1KICKBAN without channel name works only from inside a channel.").arg(commandChar));
// no usage information after error
showUsage = false;
}
// signal server to ban this user if all went fine
if (!channel.isEmpty())
{
if (kick)
{
TQString victim = parameterList[0];
parameterList.pop_front();
TQString reason = parameterList.join(" ");
result.toServer = "KICK " + channel + ' ' + victim + " :" + reason;
emit banUsers(TQStringList(victim),channel,option);
}
else
{
emit banUsers(parameterList,channel,option);
}
// syntax was correct, so reset flag
showUsage = false;
}
}
}
if (showUsage)
{
if (!kick)
result = usage(i18n("Usage: %1BAN [-HOST | -DOMAIN | -USERHOST | -USERDOMAIN] [channel] <user|mask>").arg(commandChar));
else
result = usage(i18n("Usage: %1KICKBAN [-HOST | -DOMAIN | -USERHOST | -USERDOMAIN] [channel] <user|mask> [reason]").arg(commandChar));
}
return result;
}
// finally set the ban
OutputFilterResult OutputFilter::execBan(const TQString& mask,const TQString& channel)
{
OutputFilterResult result;
result.toServer = "MODE " + channel + " +b " + mask;
return result;
}
OutputFilterResult OutputFilter::parseUnban(const TQString& parameter)
{
OutputFilterResult result;
// assume incorrect syntax first
bool showUsage=true;
if(!parameter.isEmpty())
{
TQStringList parameterList = TQStringList::split(' ', parameter);
TQString channel;
TQString mask;
// if the user specified a channel
if(isAChannel(parameterList[0]))
{
// get channel
channel = parameterList[0];
// remove channel from parameter list
parameterList.pop_front();
}
// otherwise the current destination must be a channel
else if(isAChannel(destination))
channel = destination;
else
{
// destination is no channel => error
result = error(i18n("%1UNBAN without channel name works only from inside a channel.").arg(commandChar));
// no usage information after error
showUsage = false;
}
// if all went good, signal server to unban this mask
if(!channel.isEmpty())
{
emit unbanUsers(parameterList[0], channel);
// syntax was correct, so reset flag
showUsage = false;
}
}
if(showUsage)
{
result = usage(i18n("Usage: %1UNBAN [channel] pattern").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::execUnban(const TQString& mask,const TQString& channel)
{
OutputFilterResult result;
result.toServer = "MODE " + channel + " -b " + mask;
return result;
}
OutputFilterResult OutputFilter::parseIgnore(const TQString& parameter)
{
OutputFilterResult result;
// assume incorrect syntax first
bool showUsage = true;
// did the user give parameters at all?
if(!parameter.isEmpty())
{
TQStringList parameterList = TQStringList::split(' ', parameter);
// if nothing else said, only ignore channels and queries
int value = Ignore::Channel | Ignore::Query;
// user specified -all option
if(parameterList[0].lower() == "-all")
{
// ignore everything
value = Ignore::All;
parameterList.pop_front();
}
// were there enough parameters?
if(parameterList.count() >= 1)
{
for(unsigned int index=0;index<parameterList.count();index++)
{
if(!parameterList[index].contains('!'))
{
parameterList[index] += "!*";
}
Preferences::addIgnore(parameterList[index] + ',' + TQString::number(value));
}
result.output = i18n("Added %1 to your ignore list.").arg(parameterList.join(", "));
result.typeString = i18n("Ignore");
result.type = Program;
// all went fine, so show no error message
showUsage = false;
}
}
if(showUsage)
{
result = usage(i18n("Usage: %1IGNORE [ -ALL ] <user 1> <user 2> ... <user n>").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::parseUnignore(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1UNIGNORE <user 1> <user 2> ... <user n>").arg(commandChar));
}
else
{
TQString unignore = parameter.simplifyWhiteSpace();
TQStringList unignoreList = TQStringList::split(' ',unignore);
TQStringList succeeded;
TQStringList failed;
// Iterate over potential unignores
for (TQStringList::Iterator it = unignoreList.begin(); it != unignoreList.end(); ++it)
{
// If pattern looks incomplete, try to complete it
if (!(*it).contains('!'))
{
TQString fixedPattern = (*it);
fixedPattern += "!*";
bool success = false;
// Try to remove completed pattern
if (Preferences::removeIgnore(fixedPattern))
{
succeeded.append(fixedPattern);
success = true;
}
// Try to remove the incomplete version too, in case it was added via the GUI ...
// FIXME: Validate patterns in GUI?
if (Preferences::removeIgnore((*it)))
{
succeeded.append((*it));
success = true;
}
if (!success)
failed.append((*it) + "[!*]");
}
// Try to remove seemingly complete pattern
else if (Preferences::removeIgnore((*it)))
succeeded.append((*it));
// Failed to remove given complete pattern
else
failed.append((*it));
}
// Print all successful unignores, in case there were any
if (succeeded.count()>=1)
{
m_server->appendMessageToFrontmost(i18n("Ignore"),i18n("Removed %1 from your ignore list.").arg(succeeded.join(", ")));
}
// One failed unignore
if (failed.count()==1)
{
m_server->appendMessageToFrontmost(i18n("Error"),i18n("No such ignore: %1").arg(failed.join(", ")));
}
// Multiple failed unignores
if (failed.count()>1)
{
m_server->appendMessageToFrontmost(i18n("Error"),i18n("No such ignores: %1").arg(failed.join(", ")));
}
}
return result;
}
OutputFilterResult OutputFilter::parseQuote(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1QUOTE command list").arg(commandChar));
}
else
{
result.toServer = parameter;
}
return result;
}
OutputFilterResult OutputFilter::parseSay(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1SAY text").arg(commandChar));
}
else
{
result.toServer = "PRIVMSG " + destination + " :" + parameter;
result.output = parameter;
}
return result;
}
void OutputFilter::parseKonsole()
{
emit openKonsolePanel();
}
// Accessors
void OutputFilter::setCommandChar() { commandChar=Preferences::commandChar(); }
// # & + and ! are *often*, but not necessarily, channel identifiers. + and ! are non-RFC, so if a server doesn't offer 005 and
// supports + and ! channels, I think thats broken behaviour on their part - not ours.
bool OutputFilter::isAChannel(const TQString &check)
{
Q_ASSERT(m_server);
// XXX if we ever see the assert, we need the ternary
return m_server? m_server->isAChannel(check) : TQString("#&").contains(check.at(0));
}
OutputFilterResult OutputFilter::usage(const TQString& string)
{
OutputFilterResult result;
result.typeString = i18n("Usage");
result.output = string;
result.type = Program;
return result;
}
OutputFilterResult OutputFilter::info(const TQString& string)
{
OutputFilterResult result;
result.typeString = i18n("Info");
result.output = string;
result.type = Program;
return result;
}
OutputFilterResult OutputFilter::error(const TQString& string)
{
OutputFilterResult result;
result.typeString = i18n("Error");
result.output = string;
result.type = Program;
return result;
}
OutputFilterResult OutputFilter::parseAme(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1AME text").arg(commandChar));
}
emit multiServerCommand("me", parameter);
return result;
}
OutputFilterResult OutputFilter::parseAmsg(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1AMSG text").arg(commandChar));
}
emit multiServerCommand("msg", parameter);
return result;
}
void OutputFilter::parseServer(const TQString& parameter)
{
if (parameter.isEmpty() && !m_server->isConnected() && !m_server->isConnecting())
emit reconnectServer();
else
{
TQStringList splitted = TQStringList::split(" ", parameter);
TQString host = splitted[0];
TQString port = "6667";
TQString password;
if (splitted.count() == 3)
emit connectTo(Konversation::CreateNewConnection, splitted[0], splitted[1], splitted[2]);
else if (splitted.count() == 2)
{
if (splitted[0].contains(TQRegExp(":[0-9]+$")))
emit connectTo(Konversation::CreateNewConnection, splitted[0], "", splitted[1]);
else
emit connectTo(Konversation::CreateNewConnection, splitted[0], splitted[1]);
}
else
emit connectTo(Konversation::CreateNewConnection, splitted[0]);
}
}
OutputFilterResult OutputFilter::parseOmsg(const TQString& parameter)
{
OutputFilterResult result;
if(!parameter.isEmpty())
{
result.toServer = "PRIVMSG @"+destination+" :"+parameter;
}
else
{
result = usage(i18n("Usage: %1OMSG text").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::parseOnotice(const TQString& parameter)
{
OutputFilterResult result;
if(!parameter.isEmpty())
{
result.toServer = "NOTICE @"+destination+" :"+parameter;
result.typeString = i18n("Notice");
result.type = Program;
result.output = i18n("Sending notice \"%1\" to %2.").arg(parameter, destination);
}
else
{
result = usage(i18n("Usage: %1ONOTICE text").arg(commandChar));
}
return result;
}
OutputFilterResult OutputFilter::parseCharset(const TQString& charset)
{
OutputFilterResult result;
if (charset.isEmpty ())
{
result = info (i18n("Current encoding is: %1")
.arg(m_server->getIdentity()->getCodec()->name()));
return result;
}
TQString shortName = Konversation::IRCCharsets::self()->ambiguousNameToShortName(charset);
if(!shortName.isEmpty())
{
m_server->getIdentity()->setCodecName(shortName);
emit encodingChanged();
result = info (i18n("Switched to %1 encoding.").arg(shortName));
}
else
{
result = error(i18n("%1 is not a valid encoding.").arg (charset));
}
return result;
}
OutputFilterResult OutputFilter::parseSetKey(const TQString& parameter)
{
TQStringList parms = TQStringList::split(" ", parameter);
if (parms.count() == (0 >> parms.count() > 2))
return usage(i18n("Usage: %1setkey [<nick|channel>] <key> sets the encryption key for nick or channel. %2setkey <key> when in a channel or query tab to set the key for it.").arg(commandChar).arg(commandChar) );
else if (parms.count() == 1)
parms.prepend(destination);
m_server->setKeyForRecipient(parms[0], parms[1].local8Bit());
if (isAChannel(parms[0]) && m_server->getChannelByName(parms[0]))
m_server->getChannelByName(parms[0])->setEncryptedOutput(true);
else if (m_server->getQueryByName(parms[0]))
m_server->getQueryByName(parms[0])->setEncryptedOutput(true);
return info(i18n("The key for %1 has been set.").arg(parms[0]));
}
OutputFilterResult OutputFilter::parseDelKey(const TQString& prametr)
{
TQString parameter(prametr.isEmpty()?destination:prametr);
if(parameter.isEmpty() || parameter.contains(' '))
return usage(i18n("Usage: %1delkey <nick> or <channel> deletes the encryption key for nick or channel").arg(commandChar));
m_server->setKeyForRecipient(parameter, "");
if (isAChannel(parameter) && m_server->getChannelByName(parameter))
m_server->getChannelByName(parameter)->setEncryptedOutput(false);
else if (m_server->getQueryByName(parameter))
m_server->getQueryByName(parameter)->setEncryptedOutput(false);
return info(i18n("The key for %1 has been deleted.").arg(parameter));
}
OutputFilterResult OutputFilter::parseShowKey(const TQString& prametr)
{
TQString parameter(prametr.isEmpty()?destination:prametr);
TQString key(m_server->getKeyForRecipient(parameter));
TQWidget *mw=KonversationApplication::instance()->getMainWindow();
if (!key.isEmpty())
KMessageBox::information(mw, i18n("The key for %1 is \"%2\".").arg(parameter).arg(key), i18n("Blowfish"));
else
KMessageBox::information(mw, i18n("No key has been set for %1.").arg(parameter));
OutputFilterResult result;
return result;
}
OutputFilterResult OutputFilter::parseList(const TQString& parameter)
{
OutputFilterResult result;
emit openChannelList(parameter, true);
return result;
}
OutputFilterResult OutputFilter::parseDNS(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1DNS <nick>").arg(commandChar));
}
else
{
TQStringList splitted = TQStringList::split(" ", parameter);
TQString target = splitted[0];
KIpAddress address(target);
// Parameter is an IP address
if (address.isIPv4Addr() || address.isIPv6Addr())
{
// Disable the reverse resolve codepath on older KDE versions due to many
// distributions shipping visibility-enabled KDE 3.4 and KNetwork not
// coping with it.
#if KDE_IS_VERSION(3,5,1)
KNetwork:: KInetSocketAddress socketAddress(address,0);
TQString resolvedTarget;
TQString serv; // We don't need this, but KReverseResolver::resolve does.
if (KNetwork::KReverseResolver::resolve(socketAddress,resolvedTarget,serv))
{
result.typeString = i18n("DNS");
result.output = i18n("Resolved %1 to: %2").arg(target).arg(resolvedTarget);
result.type = Program;
}
else
{
result = error(i18n("Unable to resolve %1").arg(target));
}
#else
result = error(i18n("Reverse-resolving requires KDE version 3.5.1 or higher."));
#endif
}
// Parameter is presumed to be a host due to containing a dot. Yeah, it's dumb.
// FIXME: The reason we detect the host by occurrence of a dot is the large penalty
// we would incur by using inputfilter to find out if there's a user==target on the
// server - once we have a better API for this, switch to it.
else if (target.contains('.'))
{
KNetwork::KResolverResults resolved = KNetwork::KResolver::resolve(target,"");
if(resolved.error() == KResolver::NoError && resolved.size() > 0)
{
TQString resolvedTarget = resolved.first().address().nodeName();
result.typeString = i18n("DNS");
result.output = i18n("Resolved %1 to: %2").arg(target).arg(resolvedTarget);
result.type = Program;
}
else
{
result = error(i18n("Unable to resolve %1").arg(target));
}
}
// Parameter is either host nor IP, so request a lookup from server, which in
// turn lets inputfilter do the job.
else
{
m_server->resolveUserhost(target);
}
}
return result;
}
TQString OutputFilter::addNickToEmptyNickList(const TQString& nick, const TQString& parameter)
{
TQStringList nickList = TQStringList::split(' ', parameter);
TQString newNickList;
if (nickList.count() == 0)
{
newNickList = nick;
}
// check if list contains only target channel
else if (nickList.count() == 1 && isAChannel(nickList[0]))
{
newNickList = nickList[0] + ' ' + nick;
}
// list contains at least one nick
else
{
newNickList = parameter;
}
return newNickList;
}
OutputFilterResult OutputFilter::parseKill(const TQString& parameter)
{
OutputFilterResult result;
if(parameter.isEmpty())
{
result = usage(i18n("Usage: %1KILL <nick> [comment]").arg(commandChar));
}
else
{
TQString victim = parameter.section(' ', 0, 0);
result.toServer = "KILL " + victim + " :" + parameter.mid(victim.length() + 1);
}
return result;
}
}
#include "outputfilter.moc"
// kate: space-indent on; tab-width 4; indent-width 4; mixed-indent off; replace-tabs on;
// vim: set et sw=4 ts=4 cino=l1,cs,U1: