From 529f38bd8878b6b1bea2b5457031ce936aab8d80 Mon Sep 17 00:00:00 2001 From: Markus Mittendrein Date: Mon, 6 Oct 2014 15:03:54 +0200 Subject: addedd communi --- libcommuni/src/util/irccommandparser.cpp | 597 +++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 libcommuni/src/util/irccommandparser.cpp (limited to 'libcommuni/src/util/irccommandparser.cpp') diff --git a/libcommuni/src/util/irccommandparser.cpp b/libcommuni/src/util/irccommandparser.cpp new file mode 100644 index 0000000..e686a37 --- /dev/null +++ b/libcommuni/src/util/irccommandparser.cpp @@ -0,0 +1,597 @@ +/* + Copyright (C) 2008-2014 The Communi Project + + You may use this file under the terms of BSD license as follows: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "irccommandparser.h" +#include "irccommandparser_p.h" +#include "irctoken_p.h" +#include + +IRC_BEGIN_NAMESPACE + +/*! + \file irccommandparser.h + \brief \#include <IrcCommandParser> + */ + +/*! + \class IrcCommandParser irccommandparser.h + \ingroup util + \brief Parses commands from user input. + + \section syntax Syntax + + Since the list of supported commands and the exact syntax for each + command is application specific, IrcCommandParser does not provide + any built-in command syntaxes. It is left up to the applications + to introduce the supported commands and syntaxes. + IrcCommandParser supports the following command syntax markup: + + Syntax | Example | Description + -------------------|----------------------|------------ + <param> | <target> | A required parameter. + (<param>) | (<key>) | An optional parameter. + <param...> | <message...> | A required parameter, multiple words accepted. (1) + (<param...>) | (<message...>) | An optional parameter, multiple words accepted. (1) + (<\#param>) | (<\#channel>) | An optional channel parameter. (2) + [param] | [target] | Inject the current target. + + -# Multi-word parameters are only supported in the last parameter position. + -# An optional channel parameter is filled up with the current channel when absent. + + The following example presents introducing some typical commands. + \code + IrcCommandParser* parser = new IrcCommandParser(this); + parser->addCommand(IrcCommand::Join, "JOIN <#channel> ()"); + parser->addCommand(IrcCommand::Part, "PART (<#channel>) ()"); + parser->addCommand(IrcCommand::Kick, "KICK (<#channel>) ()"); + parser->addCommand(IrcCommand::CtcpAction, "ME [target] "); + parser->addCommand(IrcCommand::CtcpAction, "ACTION "); + \endcode + + \note The parameter names are insignificant, but descriptive + parameter names are recommended for the sake of readability. + + \section context Context + + Notice that commands are often context sensitive. While some command + may accept an optional parameter that is filled up with the current + target (channel/query) name when absent, another command may always + inject the current target name as a certain parameter. Therefore + IrcCommandParser must be kept up-to-date with the \ref target + "current target" and the \ref channels "list of channels". + + \code + // currently in a query, and also present on some channels + parser->setTarget("jpnurmi"); + parser->setChannels(QStringList() << "#communi" << "#freenode"); + \endcode + + \section command-triggers Command triggers + + IrcCommandParser serves as a generic parser for typical IRC commands. + It can be utilized for parsing commands from user input in GUI clients, + and from messages from other clients when implementing IRC bots. + + The command parsing behavior is controlled by setting up command + \ref triggers. Whilst a typical GUI client might use \c "/" as a command + trigger, an IRC bot might use \c "!" and the nick name of the bot. The + following snippet illustrates a typical GUI client usage. + + \code + parser->setTarget("#communi"); + parser->setTriggers(QStringList() << "/"); + parser->parse(input); + \endcode + + \p + Input | Result | Description + ------------------|---------------------|------------ + "hello" | IrcCommand::Message | No matching command trigger => a message "hello" to \#communi + "/join #channel" | IrcCommand::Join | Matching command trigger => a command to join "#channel" + + See the \ref bot "bot example" to see how the parser can be effectively utilized for IRC bots. + + \section parse-custom-commands Custom commands + + The parser also supports such custom client specific commands that + are not sent to the server. Since IrcCommand does not know how to + handle custom commands, the parser treats them as a special case + injecting the command as a first parameter. + + \code + IrcParser parser; + parser.addCommand(IrcCommand::Custom, "QUERY "); + IrcCommand* command = parser.parse("/query jpnurmi"); + Q_ASSERT(command->type() == IrcCommand::Custom); + qDebug() << command->parameters(); // ("QUERY", "jpnurmi") + \endcode + */ + +/*! + \enum IrcCommandParser::Detail + This enum describes the available syntax details. + */ + +/*! + \var IrcCommandParser::Full + \brief The syntax in full details + */ + +/*! + \var IrcCommandParser::NoTarget + \brief The syntax has injected [target] removed + */ + +/*! + \var IrcCommandParser::NoPrefix + \brief The syntax has \#channel prefixes removed + */ + +/*! + \var IrcCommandParser::NoEllipsis + \brief The syntax has ellipsis... removed + */ + +/*! + \var IrcCommandParser::NoParentheses + \brief The syntax has parentheses () removed + */ + +/*! + \var IrcCommandParser::NoBrackets + \brief The syntax has brackets [] removed + */ + +/*! + \var IrcCommandParser::NoAngles + \brief The syntax has angle brackets <> removed + */ + +/*! + \var IrcCommandParser::Visual + \brief The syntax suitable for visual representation + */ + +#ifndef IRC_DOXYGEN +IrcCommandParserPrivate::IrcCommandParserPrivate() : tolerant(false) +{ +} + +QList IrcCommandParserPrivate::find(const QString& command) const +{ + QList result; + foreach (const IrcCommandInfo& cmd, commands) { + if (cmd.command == command) + result += cmd; + } + return result; +} + +static inline bool isOptional(const QString& token) +{ + return token.startsWith(QLatin1Char('(')) && token.endsWith(QLatin1Char(')')); +} + +static inline bool isMultiWord(const QString& token) +{ + return token.contains(QLatin1String("...")); +} + +static inline bool isChannel(const QString& token) +{ + return token.contains(QLatin1Char('#')); +} + +static inline bool isCurrent(const QString& token) +{ + return token.startsWith(QLatin1Char('[')) && token.endsWith(QLatin1Char(']')); +} + +IrcCommandInfo IrcCommandParserPrivate::parseSyntax(IrcCommand::Type type, const QString& syntax) +{ + IrcCommandInfo cmd; + QStringList tokens = syntax.split(QLatin1Char(' '), QString::SkipEmptyParts); + if (!tokens.isEmpty()) { + cmd.type = type; + cmd.command = tokens.takeFirst().toUpper(); + cmd.syntax = tokens.join(QLatin1String(" ")); + cmd.max = tokens.count(); + + IrcParameterInfo param; + for (int i = 0; i < tokens.count(); ++i) { + const QString& token = tokens.at(i); + param.optional = isOptional(token); + param.channel = isChannel(token); + param.current = isCurrent(token); + param.multi = isMultiWord(token); + if (!param.optional) + ++cmd.min; + if (param.optional && param.channel) + ++cmd.min; + const bool last = (i == tokens.count() - 1); + if (last && param.multi) + cmd.max = INT_MAX; + cmd.params += param; + } + } + return cmd; +} + +IrcCommand* IrcCommandParserPrivate::parseCommand(const IrcCommandInfo& command, const QString& input) const +{ + IrcCommand* cmd = 0; + QStringList params; + if (processParameters(command, input, ¶ms)) { + const int count = params.count(); + if (count >= command.min && count <= command.max) { + cmd = new IrcCommand; + cmd->setType(command.type); + if (command.type == IrcCommand::Custom) + params.prepend(command.command); + cmd->setParameters(params); + } + } + return cmd; +} + +bool IrcCommandParserPrivate::processParameters(const IrcCommandInfo& command, const QString& input, QStringList* params) const +{ + IrcTokenizer tokenizer(input); + for (int i = 0; i < command.params.count(); ++i) { + const IrcParameterInfo& info = command.params.at(i); + const IrcToken token = tokenizer.at(0); + if (info.optional && info.channel) { + if (onChannel()) { + if (!token.isValid() || !channels.contains(token.text(), Qt::CaseInsensitive)) { + params->append(target); + } else if (token.isValid()) { + tokenizer = tokenizer.mid(1); + params->append(token.text()); + } + } else if (!channels.contains(token.text())) { + return false; + } + } else if (info.current) { + params->append(target); + } else if (info.multi) { + const QString multi = tokenizer.toString(); + if (!multi.isEmpty()) { + params->append(multi); + tokenizer.clear(); + } + } else { + tokenizer = tokenizer.mid(1); + if (token.isValid()) + params->append(token.text()); + } + } + return tokenizer.isEmpty(); +} + +bool IrcCommandParserPrivate::processCommand(QString* input, int* removed) const +{ + foreach (const QString& trigger, triggers) { + if (tolerant && trigger.length() == 1 && (input->startsWith(trigger.repeated(2)) || input->startsWith(trigger + QLatin1Char(' ')))) { + // treat "//cmd" and "/ /cmd" as message (-> "/cmd") + input->remove(0, 1); + if (removed) + *removed = 1; + return false; + } else if (input->startsWith(trigger)) { + input->remove(0, trigger.length()); + if (removed) + *removed = trigger.length(); + return true; + } + } + return false; +} + +bool IrcCommandParserPrivate::processMessage(QString* input, int* removed) const +{ + if (input->isEmpty()) + return false; + if (triggers.isEmpty()) + return tolerant; + if (processCommand(input, removed)) + return false; + return tolerant; +} + +bool IrcCommandParserPrivate::onChannel() const +{ + return channels.contains(target, Qt::CaseInsensitive); +} +#endif // IRC_DOXYGEN + +/*! + Constructs a command parser with \a parent. + */ +IrcCommandParser::IrcCommandParser(QObject* parent) : QObject(parent), d_ptr(new IrcCommandParserPrivate) +{ +} + +/*! + Destructs the command parser. + */ +IrcCommandParser::~IrcCommandParser() +{ +} + +/*! + This property holds the known commands. + + The commands are uppercased and in alphabetical order. + + \par Access function: + \li QStringList commands() const + + \par Notifier signal: + \li void commandsChanged(const QStringList& commands) + + \sa addCommand(), removeCommand() + */ +QStringList IrcCommandParser::commands() const +{ + Q_D(const IrcCommandParser); + return d->commands.uniqueKeys(); +} + +/*! + Returns syntax for the given \a command in given \a details level. + */ +QString IrcCommandParser::syntax(const QString& command, Details details) const +{ + Q_D(const IrcCommandParser); + IrcCommandInfo info = d->find(command.toUpper()).value(0); + if (!info.command.isEmpty()) { + QString str = info.fullSyntax(); + if (details != Full) { + if (details & NoTarget) + str.remove(QRegExp("\\[[^\\]]+\\]")); + if (details & NoPrefix) + str.remove("#"); + if (details & NoEllipsis) + str.remove("..."); + if (details & NoParentheses) + str.remove("(").remove(")"); + if (details & NoBrackets) + str.remove("[").remove("]"); + if (details & NoAngles) + str.remove("<").remove(">"); + } + return str.simplified(); + } + return QString(); +} + +/*! + Adds a command with \a type and \a syntax. + */ +void IrcCommandParser::addCommand(IrcCommand::Type type, const QString& syntax) +{ + Q_D(IrcCommandParser); + IrcCommandInfo cmd = d->parseSyntax(type, syntax); + if (!cmd.command.isEmpty()) { + const bool contains = d->commands.contains(cmd.command); + d->commands.insert(cmd.command, cmd); + if (!contains) + emit commandsChanged(commands()); + } +} + +/*! + Removes the command with \a type and \a syntax. + */ +void IrcCommandParser::removeCommand(IrcCommand::Type type, const QString& syntax) +{ + Q_D(IrcCommandParser); + bool changed = false; + QMutableMapIterator it(d->commands); + while (it.hasNext()) { + IrcCommandInfo cmd = it.next().value(); + if (cmd.type == type && (syntax.isEmpty() || !syntax.compare(cmd.fullSyntax(), Qt::CaseInsensitive))) { + it.remove(); + if (!d->commands.contains(cmd.command)) + changed = true; + } + } + if (changed) + emit commandsChanged(commands()); +} + +/*! + This property holds the available channels. + + \par Access functions: + \li QStringList channels() const + \li void setChannels(const QStringList& channels) [slot] + + \par Notifier signal: + \li void channelsChanged(const QStringList& channels) + + \sa IrcBufferModel::channels() + */ +QStringList IrcCommandParser::channels() const +{ + Q_D(const IrcCommandParser); + return d->channels; +} + +void IrcCommandParser::setChannels(const QStringList& channels) +{ + Q_D(IrcCommandParser); + if (d->channels != channels) { + d->channels = channels; + emit channelsChanged(channels); + } +} + +/*! + This property holds the current target. + + \par Access functions: + \li QString target() const + \li void setTarget(const QString& target) [slot] + + \par Notifier signal: + \li void targetChanged(const QString& target) + */ +QString IrcCommandParser::target() const +{ + Q_D(const IrcCommandParser); + return d->target; +} + +void IrcCommandParser::setTarget(const QString& target) +{ + Q_D(IrcCommandParser); + if (d->target != target) { + d->target = target; + emit targetChanged(target); + } +} + +/*! + This property holds the command triggers. + + \par Access functions: + \li QStringList triggers() const + \li void setTriggers(const QStringList& triggers) [slot] + + \par Notifier signal: + \li void triggersChanged(const QStringList& triggers) + */ +QStringList IrcCommandParser::triggers() const +{ + Q_D(const IrcCommandParser); + return d->triggers; +} + +void IrcCommandParser::setTriggers(const QStringList& triggers) +{ + Q_D(IrcCommandParser); + if (d->triggers != triggers) { + d->triggers = triggers; + emit triggersChanged(triggers); + } +} + +/*! + \property bool IrcCommandParser::tolerant + + This property holds whether the parser is tolerant. + + A tolerant parser creates message commands out of input that does not + start with a command trigger, and raw server commands when the input + starts with a command trigger but the command is unrecognized. Known + commands with invalid arguments are still considered invalid. + + The default value is \c false. + + \par Access functions: + \li bool isTolerant() const + \li void setTolerant(bool tolerant) + + \par Notifier signal: + \li void tolerancyChanged(bool tolerant) + + \sa IrcCommand::Quote + */ +bool IrcCommandParser::isTolerant() const +{ + Q_D(const IrcCommandParser); + return d->tolerant; +} + +void IrcCommandParser::setTolerant(bool tolerant) +{ + Q_D(IrcCommandParser); + if (d->tolerant != tolerant) { + d->tolerant = tolerant; + emit tolerancyChanged(tolerant); + } +} + +/*! + Parses and returns the command for \a input, or \c 0 if the input is not valid. + */ +IrcCommand* IrcCommandParser::parse(const QString& input) const +{ + Q_D(const IrcCommandParser); + QString message = input; + if (d->processMessage(&message)) { + return IrcCommand::createMessage(d->target, message.trimmed()); + } else if (!message.isEmpty()) { + IrcTokenizer tokenizer(message); + const QString command = tokenizer.at(0).text().toUpper(); + QString params = tokenizer.mid(1).toString(); + const QList commands = d->find(command); + if (!commands.isEmpty()) { + foreach (const IrcCommandInfo& c, commands) { + IrcCommand* cmd = d->parseCommand(c, params); + if (cmd) + return cmd; + } + } else if (d->tolerant) { + IrcCommandInfo custom = d->parseSyntax(IrcCommand::Quote, QString(QLatin1String("%1 ()")).arg(command)); + params.prepend(custom.command + QLatin1Char(' ')); + return d->parseCommand(custom, params); + } + } + return 0; +} + +/*! + Clears the list of commands. + + \sa reset() + */ +void IrcCommandParser::clear() +{ + Q_D(IrcCommandParser); + if (!d->commands.isEmpty()) { + d->commands.clear(); + emit commandsChanged(QStringList()); + } +} + +/*! + Resets the channels and the current target. + + \sa clear() + */ +void IrcCommandParser::reset() +{ + setChannels(QStringList()); + setTarget(QString()); +} + +#include "moc_irccommandparser.cpp" + +IRC_END_NAMESPACE -- cgit v1.2.3-54-g00ecf