changeset 750:1e1eb83a3f0b

Irccd: IRC code cleanup
author David Demelier <markand@malikania.fr>
date Fri, 03 Aug 2018 19:31:00 +0200
parents 7851d95f2fbb
children 8876412ba633
files libirccd-js/irccd/js/util_jsapi.cpp libirccd/irccd/daemon/irc.cpp libirccd/irccd/daemon/irc.hpp libirccd/irccd/daemon/server.cpp plugins/links/links.cpp tests/src/libirccd/irc/main.cpp
diffstat 6 files changed, 1150 insertions(+), 245 deletions(-) [+]
line wrap: on
line diff
--- a/libirccd-js/irccd/js/util_jsapi.cpp	Fri Aug 03 12:50:30 2018 +0200
+++ b/libirccd-js/irccd/js/util_jsapi.cpp	Fri Aug 03 19:31:00 2018 +0200
@@ -293,7 +293,7 @@
 duk_ret_t Util_splituser(duk_context* ctx)
 {
     return wrap(ctx, [&] {
-        return dukx_push(ctx, irc::user::parse(dukx_require<std::string>(ctx, 0)).nick());
+        return dukx_push(ctx, irc::user::parse(dukx_require<std::string>(ctx, 0)).nick);
     });
 }
 
@@ -317,7 +317,7 @@
 duk_ret_t Util_splithost(duk_context* ctx)
 {
     return wrap(ctx, [&] {
-        return dukx_push(ctx, irc::user::parse(dukx_require<std::string>(ctx, 0)).host());
+        return dukx_push(ctx, irc::user::parse(dukx_require<std::string>(ctx, 0)).host);
     });
 }
 
--- a/libirccd/irccd/daemon/irc.cpp	Fri Aug 03 12:50:30 2018 +0200
+++ b/libirccd/irccd/daemon/irc.cpp	Fri Aug 03 19:31:00 2018 +0200
@@ -26,11 +26,33 @@
 using boost::asio::async_read_until;
 using boost::asio::async_write;
 
-namespace irccd {
+namespace irccd::irc {
+
+auto message::get(unsigned short index) const noexcept -> const std::string&
+{
+    static const std::string dummy;
+
+    return (index >= args.size()) ? dummy : args[index];
+}
+
+auto message::is_ctcp(unsigned index) const noexcept -> bool
+{
+    const auto a = get(index);
 
-namespace irc {
+    if (a.empty())
+        return false;
+
+    return a.front() == 0x01 && a.back() == 0x01;
+}
 
-message message::parse(const std::string& line)
+auto message::ctcp(unsigned index) const -> std::string
+{
+    assert(is_ctcp(index));
+
+    return args[index].substr(1, args[index].size() - 1);
+}
+
+auto message::parse(const std::string& line) -> message
 {
     std::istringstream iss(line);
     std::string prefix;
@@ -71,35 +93,18 @@
         args.push_back(std::move(arg));
     }
 
-    return {std::move(prefix), std::move(command), std::move(args)};
+    return { std::move(prefix), std::move(command), std::move(args) };
 }
 
-bool message::is_ctcp(unsigned index) const noexcept
-{
-    const auto a = arg(index);
-
-    if (a.empty())
-        return false;
-
-    return a.front() == 0x01 && a.back() == 0x01;
-}
-
-std::string message::ctcp(unsigned index) const
-{
-    assert(is_ctcp(index));
-
-    return args_[index].substr(1, args_[index].size() - 1);
-}
-
-user user::parse(std::string_view line)
+auto user::parse(std::string_view line) -> user
 {
     if (line.empty())
-        return {"", ""};
+        return { "", "" };
 
     const auto pos = line.find("!");
 
     if (pos == std::string::npos)
-        return {std::string(line), ""};
+        return { std::string(line), "" };
 
     return {
         std::string(line.substr(0, pos)),
@@ -274,6 +279,4 @@
 
 #endif // !IRCCD_HAVE_SSL
 
-} // !irc
-
-} // !irccd
+} // !irccd::irc
--- a/libirccd/irccd/daemon/irc.hpp	Fri Aug 03 12:50:30 2018 +0200
+++ b/libirccd/irccd/daemon/irc.hpp	Fri Aug 03 19:31:00 2018 +0200
@@ -38,215 +38,1166 @@
 #   include <boost/asio/ssl.hpp>
 #endif
 
-namespace irccd {
-
-namespace irc {
-
-class message;
+namespace irccd::irc {
 
 /**
  * \brief Describe errors.
+ *
+ * See [RFC1459 (6.1)](https://tools.ietf.org/html/rfc1459#section-6.1).
  */
 enum class err {
+    /**
+     * ERR_NOSUCHNICK
+     *
+     * "<nickname> :No such nick/channel"
+     *
+     * Used to indicate the nickname parameter supplied to a
+     * command is currently unused.
+     */
     nosuchnick = 401,
+
+    /**
+     * ERR_NOSUCHSERVER
+     *
+     * "<server name> :No such server"
+     *
+     * Used to indicate the server name given currently
+     * doesn't exist.
+     */
     nosuchserver = 402,
+
+    /**
+     * ERR_NOSUCHCHANNEL
+     *
+     * "<channel name> :No such channel"
+     *
+     * Used to indicate the given channel name is invalid.
+     */
     nosuchchannel = 403,
+
+    /**
+     * ERR_CANNOTSENDTOCHAN
+     *
+     * "<channel name> :Cannot send to channel"
+     *
+     * Sent to a user who is either (a) not on a channel
+     * which is mode +n or (b) not a chanop (or mode +v) on
+     * a channel which has mode +m set and is trying to send
+     * a PRIVMSG message to that channel.
+     */
     cannotsendtochan = 404,
+
+    /**
+     * ERR_TOOMANYCHANNELS
+     *
+     * "<channel name> :You have joined too many channels"
+     *
+     * Sent to a user when they have joined the maximum
+     * number of allowed channels and they try to join
+     * another channel.
+     */
     toomanychannels = 405,
+
+    /**
+     * ERR_WASNOSUCHNICK
+     *
+     * "<nickname> :There was no such nickname"
+     *
+     * Returned by WHOWAS to indicate there is no history
+     * information for that nickname.
+     */
     wasnosuchnick = 406,
+
+    /**
+     * ERR_TOOMANYTARGETS
+     *
+     * "<target> :Duplicate recipients. No message delivered"
+     *
+     * Returned to a client which is attempting to send a
+     * PRIVMSG/NOTICE using the user@host destination format
+     * and for a user@host which has several occurrences.
+     */
     toomanytargets = 407,
+
+    /**
+     * ERR_NOORIGIN
+     *
+     * ":No origin specified"
+     *
+     * PING or PONG message missing the originator parameter
+     * which is required since these commands must work
+     * without valid prefixes.
+     */
     noorigin = 409,
+
+    /**
+     * ERR_NORECIPIENT
+     *
+     * ":No recipient given (<command>)"
+     */
     norecipient = 411,
+
+    /**
+     * ERR_NOTEXTTOSEND
+     *
+     * ":No text to send"
+     */
     notexttosend = 412,
+
+    /**
+     * ERR_NOTOPLEVEL
+     *
+     * "<mask> :No toplevel domain specified"
+     */
     notoplevel = 413,
+
+    /**
+     * ERR_WILDTOPLEVEL
+     *
+     * "<mask> :Wildcard in toplevel domain"
+     *
+     * are returned by PRIVMSG to indicate that
+     * the message wasn't delivered for some reason.
+     * ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that
+     * are returned when an invalid use of
+     * "PRIVMSG $<server>" or "PRIVMSG #<host>" is attempted.
+     */
     wildtoplevel = 414,
+
+    /**
+     * ERR_UNKNOWNCOMMAND
+     *
+     * "<command> :Unknown command"
+     *
+     * Returned to a registered client to indicate that the
+     * command sent is unknown by the server.
+     */
     unknowncommand = 421,
+
+    /**
+     * ERR_NOMOTD
+     *
+     * ":MOTD File is missing"
+     *
+     * Server's MOTD file could not be opened by the server.
+     */
     nomotd = 422,
+
+    /**
+     * ERR_NOADMININFO
+     *
+     * "<server> :No administrative info available"
+     *
+     * Returned by a server in response to an ADMIN message
+     * when there is an error in finding the appropriate
+     * information.
+     */
     noadmininfo = 423,
+
+    /**
+     * ERR_FILEERROR
+     *
+     * ":File error doing <file op> on <file>"
+     *
+     * Generic error message used to report a failed file
+     * operation during the processing of a message.
+     */
     fileerror = 424,
+
+    /**
+     * ERR_NONICKNAMEGIVEN
+     *
+     * ":No nickname given"
+     *
+     * Returned when a nickname parameter expected for a
+     * command and isn't found.
+     */
     nonicknamegiven = 431,
+
+    /**
+     * ERR_ERRONEUSNICKNAME
+     *
+     * "<nick> :Erroneus nickname"
+     *
+     * Returned after receiving a NICK message which contains
+     * characters which do not fall in the defined set.  See
+     * section x.x.x for details on valid nicknames.
+     */
     erroneusnickname = 432,
+
+    /**
+     * ERR_NICKNAMEINUSE
+     *
+     * "<nick> :Nickname is already in use"
+     *
+     * Returned when a NICK message is processed that results
+     * in an attempt to change to a currently existing
+     * nickname.
+     */
     nicknameinuse = 433,
+
+    /**
+     * ERR_NICKCOLLISION
+     *
+     * "<nick> :Nickname collision KILL"
+     *
+     * Returned by a server to a client when it detects a
+     * nickname collision (registered of a NICK that
+     * already exists by another server).
+     */
     nickcollision = 436,
+
+    /**
+     * ERR_USERNOTINCHANNEL
+     *
+     * "<nick> <channel> :They aren't on that channel"
+     *
+     * Returned by the server to indicate that the target
+     * user of the command is not on the given channel.
+     */
     usernotinchannel = 441,
+
+    /**
+     * ERR_NOTONCHANNEL
+     *
+     * "<channel> :You're not on that channel"
+     *
+     * Returned by the server whenever a client tries to
+     * perform a channel effecting command for which the
+     * client isn't a member.
+     */
     notonchannel = 442,
+
+    /**
+     * ERR_USERONCHANNEL
+     *
+     * "<user> <channel> :is already on channel"
+     *
+     * Returned when a client tries to invite a user to a
+     * channel they are already on.
+     */
     useronchannel = 443,
+
+    /**
+     * ERR_NOLOGIN
+     *
+     * "<user> :User not logged in"
+     *
+     * Returned by the summon after a SUMMON command for a
+     * user was unable to be performed since they were not
+     * logged in.
+     */
     nologin = 444,
+
+    /**
+     * ERR_SUMMONDISABLED
+     *
+     * ":SUMMON has been disabled"
+     *
+     * Returned as a response to the SUMMON command.  Must be
+     * returned by any server which does not implement it.
+     */
     summondisabled = 445,
+
+    /**
+     * ERR_USERSDISABLED
+     *
+     * ":USERS has been disabled"
+     *
+     * Returned as a response to the USERS command.  Must be
+     * returned by any server which does not implement it.
+     */
     usersdisabled = 446,
+
+    /**
+     * ERR_NOTREGISTERED
+     *
+     * ":You have not registered"
+     *
+     * Returned by the server to indicate that the client
+     * must be registered before the server will allow it
+     * to be parsed in detail.
+     */
     notregistered = 451,
+
+    /**
+     * ERR_NEEDMOREPARAMS
+     *
+     * "<command> :Not enough parameters"
+     *
+     * Returned by the server by numerous commands to
+     * indicate to the client that it didn't supply enough
+     * parameters.
+     */
     needmoreparams = 461,
+
+    /**
+     * ERR_ALREADYREGISTRED
+     *
+     * ":You may not reregister"
+     *
+     * Returned by the server to any link which tries to
+     * change part of the registered details (such as
+     * password or user details from second USER message).
+     */
     alreadyregistred = 462,
+
+    /**
+     * ERR_NOPERMFORHOST
+     *
+     * ":Your host isn't among the privileged"
+     *
+     * Returned to a client which attempts to register with
+     * a server which does not been setup to allow
+     * connections from the host the attempted connection
+     * is tried.
+     */
     nopermforhost = 463,
+
+    /**
+     * ERR_PASSWDMISMATCH
+     *
+     * ":Password incorrect"
+     *
+     * Returned to indicate a failed attempt at registering
+     * a connection for which a password was required and
+     * was either not given or incorrect.
+     */
     passwdmismatch = 464,
+
+    /**
+     * ERR_YOUREBANNEDCREEP
+     *
+     * ":You are banned from this server"
+     *
+     * Returned after an attempt to connect and register
+     * yourself with a server which has been setup to
+     * explicitly deny connections to you.
+     */
     yourebannedcreep = 465,
+
+    /**
+     * ERR_KEYSET
+     *
+     * "<channel> :Channel key already set"
+     */
     keyset = 467,
+
+    /**
+     * ERR_CHANNELISFULL
+     *
+     * "<channel> :Cannot join channel (+l)"
+     */
     channelisfull = 471,
+
+    /**
+     * ERR_UNKNOWNMODE
+     *
+     * "<char> :is unknown mode char to me"
+     */
     unknownmode = 472,
+
+    /**
+     * ERR_INVITEONLYCHAN
+     *
+     * "<channel> :Cannot join channel (+i)"
+     */
     inviteonlychan = 473,
+
+    /**
+     * ERR_BANNEDFROMCHAN
+     *
+     * "<channel> :Cannot join channel (+b)"
+     */
     bannedfromchan = 474,
+
+    /**
+     * ERR_BADCHANNELKEY
+     *
+     * "<channel> :Cannot join channel (+k)"
+     */
     badchannelkey = 475,
+
+    /**
+     * ERR_NOPRIVILEGES
+     *
+     * ":Permission Denied- You're not an IRC operator"
+     *
+     * Any command requiring operator privileges to operate
+     * must return this error to indicate the attempt was
+     * unsuccessful.
+     */
     noprivileges = 481,
+
+    /**
+     * ERR_CHANOPRIVSNEEDED
+     *
+     * "<channel> :You're not channel operator"
+     *
+     * Any command requiring 'chanop' privileges (such as
+     * MODE messages) must return this error if the client
+     * making the attempt is not a chanop on the specified
+     * channel.
+     */
     chanoprivsneeded = 482,
+
+    /**
+     * ERR_CANTKILLSERVER
+     *
+     * ":You cant kill a server!"
+     *
+     * Any attempts to use the KILL command on a server
+     * are to be refused and this error returned directly
+     * to the client.
+     */
     cantkillserver = 483,
+
+    /**
+     * ERR_NOOPERHOST
+     *
+     * ":No O-lines for your host"
+     *
+     * If a client sends an OPER message and the server has
+     * not been configured to allow connections from the
+     * client's host as an operator, this error must be
+     * returned.
+     */
     nooperhost = 491,
+
+    /**
+     * ERR_UMODEUNKNOWNFLAG
+     *
+     * ":Unknown MODE flag"
+     *
+     * Returned by the server to indicate that a MODE
+     * message was sent with a nickname parameter and that
+     * the a mode flag sent was not recognized.
+     */
     umodeunknownflag = 501,
+
+    /**
+     * ERR_USERSDONTMATCH
+     *
+     * ":Cant change mode for other users"
+     *
+     * Error sent to any user trying to view or change the
+     * user mode for a user other than themselves.
+     */
     usersdontmatch = 502
 };
 
 /**
  * \brief Describe numeric replies.
+ *
+ * See [RFC1459 (6.2)](https://tools.ietf.org/html/rfc1459#section-6.2).
  */
 enum class rpl {
+    /**
+     * RPL_NONE
+     *
+     * Dummy reply number. Not used.
+     */
     none = 300,
+
+    /**
+     * RPL_USERHOST
+     *
+     * ":[<reply>{<space><reply>}]"
+     *
+     * Reply format used by USERHOST to list replies to
+     * the query list.  The reply string is composed as
+     * follows:
+     *
+     * <reply> ::= <nick>['*'] '=' <'+'|'-'><hostname>
+     *
+     * The '*' indicates whether the client has registered
+     * as an Operator.  The '-' or '+' characters represent
+     * whether the client has set an AWAY message or not
+     * respectively.
+     */
     userhost = 302,
+
+    /**
+     * RPL_ISON
+     *
+     * ":[<nick> {<space><nick>}]"
+     *
+     * Reply format used by ISON to list replies to the
+     * query list.
+     */
     ison = 303,
+
+    /**
+     * RPL_AWAY
+     *
+     * "<nick> :<away message>"
+     */
     away = 301,
+
+    /**
+     * RPL_UNAWAY
+     *
+     * ":You are no longer marked as being away"
+     */
     unaway = 305,
+
+    /**
+     * RPL_NOWAWAY
+     *
+     * ":You have been marked as being away"
+     *
+     * These replies are used with the AWAY command (if
+     * allowed).  RPL_AWAY is sent to any client sending a
+     * PRIVMSG to a client which is away.  RPL_AWAY is only
+     * sent by the server to which the client is connected.
+     * Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the
+     * client removes and sets an AWAY message.
+     */
     nowaway = 306,
+
+    /**
+     * RPL_WHOISUSER
+     *
+     * "<nick> <user> <host> * :<real name>"
+     */
     whoisuser = 311,
+
+    /**
+     * RPL_WHOISSERVER
+     *
+     * "<nick> <server> :<server info>"
+     */
     whoisserver = 312,
+
+    /**
+     * RPL_WHOISOPERATOR
+     *
+     * "<nick> :is an IRC operator"
+     */
     whoisoperator = 313,
+
+    /**
+     * RPL_WHOISIDLE
+     *
+     * "<nick> <integer> :seconds idle"
+     */
     whoisidle = 317,
+
+    /**
+     * RPL_ENDOFWHOIS
+     *
+     * "<nick> :End of /WHOIS list"
+     */
     endofwhois = 318,
+
+    /**
+     * RPL_WHOISCHANNELS
+     *
+     * "<nick> :{[@|+]<channel><space>}"
+     *
+     * Replies 311 - 313, 317 - 319 are all replies
+     * generated in response to a WHOIS message.  Given that
+     * there are enough parameters present, the answering
+     * server must either formulate a reply out of the above
+     * numerics (if the query nick is found) or return an
+     * error reply.  The '*' in RPL_WHOISUSER is there as
+     * the literal character and not as a wild card.  For
+     * each reply set, only RPL_WHOISCHANNELS may appear
+     * more than once (for long lists of channel names).
+     * The '@' and '+' characters next to the channel name
+     * indicate whether a client is a channel operator or
+     * has been granted permission to speak on a moderated
+     * channel.  The RPL_ENDOFWHOIS reply is used to mark
+     * the end of processing a WHOIS message.
+     */
     whoischannels = 319,
+
+    /**
+     * RPL_WHOWASUSER
+     *
+     * "<nick> <user> <host> * :<real name>"
+     */
     whowasuser = 314,
+
+    /**
+     * RPL_ENDOFWHOWAS
+     *
+     * "<nick> :End of WHOWAS"
+     *
+     * When replying to a WHOWAS message, a server must use
+     * the replies RPL_WHOWASUSER, RPL_WHOISSERVER or
+     * ERR_WASNOSUCHNICK for each nickname in the presented
+     * list.  At the end of all reply batches, there must
+     * be RPL_ENDOFWHOWAS (even if there was only one reply
+     * and it was an error).
+     */
     endofwhowas = 369,
+
+    /**
+     * RPL_LISTSTART
+     *
+     * "Channel :Users  Name"
+     */
     liststart = 321,
+
+    /**
+     * RPL_LIST
+     *
+     * "<channel> <# visible> :<topic>"
+     */
     list = 322,
+
+    /**
+     * RPL_LISTEND
+     *
+     * ":End of /LIST"
+     *
+     * Replies RPL_LISTSTART, RPL_LIST, RPL_LISTEND mark
+     * the start, actual replies with data and end of the
+     * server's response to a LIST command.  If there are
+     * no channels available to return, only the start
+     * and end reply must be sent.
+     */
     listend = 323,
+
+    /**
+     * RPL_CHANNELMODEIS
+     *
+     * "<channel> <mode> <mode params>"
+     */
     channelmodeis = 324,
+
+    /**
+     * RPL_NOTOPIC
+     *
+     * "<channel> :No topic is set"
+     */
     notopic = 331,
+
+    /**
+     * RPL_TOPIC
+     *
+     * "<channel> :<topic>"
+     *
+     * When sending a TOPIC message to determine the
+     * channel topic, one of two replies is sent.  If
+     * the topic is set, RPL_TOPIC is sent back else
+     * RPL_NOTOPIC.
+     */
     topic = 332,
+
+    /**
+     * RPL_INVITING
+     *
+     * "<channel> <nick>"
+     *
+     * Returned by the server to indicate that the
+     * attempted INVITE message was successful and is
+     * being passed onto the end client.
+     */
     inviting = 341,
+
+    /**
+     * RPL_SUMMONING
+     *
+     * "<user> :Summoning user to IRC"
+     *
+     * Returned by a server answering a SUMMON message to
+     * indicate that it is summoning that user.
+     */
     summoning = 342,
+
+    /**
+     * RPL_VERSION
+     *
+     * "<version>.<debuglevel> <server> :<comments>"
+     *
+     * Reply by the server showing its version details.
+     * The <version> is the version of the software being
+     * used (including any patchlevel revisions) and the
+     * <debuglevel> is used to indicate if the server is
+     * running in "debug mode".
+     *
+     * The "comments" field may contain any comments about
+     * the version or further version details.
+     */
     version = 351,
+
+    /**
+     * RPL_WHOREPLY
+     *
+     * "<channel> <user> <host> <server> <nick> \
+     *  <H|G>[*][@|+] :<hopcount> <real name>"
+     */
     whoreply = 352,
+
+    /**
+     * RPL_ENDOFWHO
+     *
+     * "<name> :End of /WHO list"
+     *
+     * The RPL_WHOREPLY and RPL_ENDOFWHO pair are used
+     * to answer a WHO message.  The RPL_WHOREPLY is only
+     * sent if there is an appropriate match to the WHO
+     * query.  If there is a list of parameters supplied
+     * with a WHO message, a RPL_ENDOFWHO must be sent
+     * after processing each list item with <name> being
+     * the item.
+     */
     endofwho = 315,
+
+    /**
+     * RPL_NAMREPLY
+     *
+     * "<channel> :[[@|+]<nick> [[@|+]<nick> [...]]]"
+     */
     namreply = 353,
+
+    /**
+     * RPL_ENDOFNAMES
+     *
+     * "<channel> :End of /NAMES list"
+     *
+     * To reply to a NAMES message, a reply pair consisting
+     * of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the
+     * server back to the client.  If there is no channel
+     * found as in the query, then only RPL_ENDOFNAMES is
+     * returned.  The exception to this is when a NAMES
+     * message is sent with no parameters and all visible
+     * channels and contents are sent back in a series of
+     * RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark
+     * the end.
+     */
     endofnames = 366,
+
+    /**
+     * RPL_LINKS
+     *
+     * "<mask> <server> :<hopcount> <server info>"
+     */
     links = 364,
+
+    /**
+     * RPL_ENDOFLINKS
+     *
+     * "<mask> :End of /LINKS list"
+     *
+     * In replying to the LINKS message, a server must send
+     * replies back using the RPL_LINKS numeric and mark the
+     * end of the list using an RPL_ENDOFLINKS reply.
+     */
     endoflinks = 365,
+
+    /**
+     * RPL_BANLIST
+     *
+     * "<channel> <banid>"
+     */
     banlist = 367,
+
+    /**
+     * RPL_ENDOFBANLIST
+     *
+     * "<channel> :End of channel ban list"
+     *
+     * When listing the active 'bans' for a given channel,
+     * a server is required to send the list back using the
+     * RPL_BANLIST and RPL_ENDOFBANLIST messages.  A separate
+     * RPL_BANLIST is sent for each active banid.  After the
+     * banids have been listed (or if none present) a
+     * RPL_ENDOFBANLIST must be sent.
+     */
     endofbanlist = 368,
+
+    /**
+     * RPL_INFO
+     *
+     * ":<string>"
+     */
     info = 371,
+
+    /**
+     * RPL_ENDOFINFO
+     *
+     * ":End of /INFO list"
+     *
+     * A server responding to an INFO message is required to
+     * send all its 'info' in a series of RPL_INFO messages
+     * with a RPL_ENDOFINFO reply to indicate the end of the
+     * replies.
+     */
     endofinfo = 374,
+
+    /**
+     * RPL_MOTDSTART
+     *
+     * ":- <server> Message of the day - "
+     */
     motdstart = 375,
+
+    /**
+     * RPL_MOTD
+     *
+     * ":- <text>"
+     */
     motd = 372,
+
+    /**
+     * RPL_ENDOFMOTD
+     *
+     * ":End of /MOTD command"
+     *
+     * When responding to the MOTD message and the MOTD file
+     * is found, the file is displayed line by line, with
+     * each line no longer than 80 characters, using
+     * RPL_MOTD format replies.  These should be surrounded
+     * by a RPL_MOTDSTART (before the RPL_MOTDs) and an
+     * RPL_ENDOFMOTD (after).
+     */
     endofmotd = 376,
+
+    /**
+     * RPL_YOUREOPER
+     *
+     * ":You are now an IRC operator"
+     *
+     * RPL_YOUREOPER is sent back to a client which has
+     * just successfully issued an OPER message and gained
+     * operator status.
+     */
     youreoper = 381,
+
+    /**
+     * RPL_REHASHING
+     *
+     * "<config file> :Rehashing"
+     *
+     * If the REHASH option is used and an operator sends
+     * a REHASH message, an RPL_REHASHING is sent back to
+     * the operator.
+     */
     rehashing = 382,
+
+    /**
+     * RPL_TIME
+     *
+     * "<server> :<string showing server's local time>"
+     *
+     * When replying to the TIME message, a server must send
+     * the reply using the RPL_TIME format above.  The string
+     * showing the time need only contain the correct day and
+     * time there.  There is no further requirement for the
+     * time string.
+     */
     time = 391,
+
+    /**
+     * RPL_USERSSTART
+     *
+     * ":UserID   Terminal  Host"
+     */
     userstart = 392,
+
+    /**
+     * RPL_USERS
+     *
+     * ":%-8s %-9s %-8s"
+     */
     users = 393,
+
+    /**
+     * RPL_ENDOFUSERS
+     *
+     * ":End of users"
+     */
     endofusers = 394,
+
+    /**
+     * RPL_NOUSERS
+     *
+     * ":Nobody logged in"
+     *
+     * If the USERS message is handled by a server, the
+     * replies RPL_USERSTART, RPL_USERS, RPL_ENDOFUSERS and
+     * RPL_NOUSERS are used.  RPL_USERSSTART must be sent
+     * first, following by either a sequence of RPL_USERS
+     * or a single RPL_NOUSER.  Following this is
+     * RPL_ENDOFUSERS.
+     */
     nousers = 395,
+
+    /**
+     * RPL_TRACELINK
+     *
+     * "Link <version & debug level> <destination> <next server>"
+     */
     tracelink = 200,
+
+    /**
+     * RPL_TRACECONNECTING
+     *
+     * "Try. <class> <server>"
+     */
     traceconnecting = 201,
+
+    /**
+     * RPL_TRACEHANDSHAKE
+     *
+     * "H.S. <class> <server>"
+     */
     tracehandshake = 202,
+
+    /**
+     * RPL_TRACEUNKNOWN
+     *
+     * "???? <class> [<client IP address in dot form>]"
+     */
     traceunknown = 203,
+
+    /**
+     * RPL_TRACEOPERATOR
+     *
+     * "Oper <class> <nick>"
+     */
     traceoperator = 204,
+
+    /**
+     * RPL_TRACEUSER
+     *
+     * "User <class> <nick>"
+     */
     traceuser = 205,
+
+    /**
+     * RPL_TRACESERVER
+     *
+     * "Serv <class> <int>S <int>C <server> \
+     *  <nick!user|*!*>@<host|server>
+     */
     traceserver = 206,
+
+    /**
+     * RPL_TRACENEWTYPE
+     *
+     * "<newtype> 0 <client name>"
+     */
     tracenewtype = 208,
+
+    /**
+     * RPL_TRACELOG
+     *
+     * "File <logfile> <debug level>"
+     *
+     * The RPL_TRACE* are all returned by the server in
+     * response to the TRACE message.  How many are
+     * returned is dependent on the the TRACE message and
+     * whether it was sent by an operator or not.  There
+     * is no predefined order for which occurs first.
+     * Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and
+     * RPL_TRACEHANDSHAKE are all used for connections
+     * which have not been fully established and are either
+     * unknown, still attempting to connect or in the
+     * process of completing the 'server handshake'.
+     * RPL_TRACELINK is sent by any server which handles
+     * a TRACE message and has to pass it on to another
+     * server.  The list of RPL_TRACELINKs sent in
+     * response to a TRACE command traversing the IRC
+     * network should reflect the actual connectivity of
+     * the servers themselves along that path.
+     * RPL_TRACENEWTYPE is to be used for any connection
+     * which does not fit in the other categories but is
+     * being displayed anyway.
+     */
     tracelog = 261,
+
+    /**
+     * RPL_STATSLINKINFO
+     *
+     * "<linkname> <sendq> <sent messages> \
+     *  <sent bytes> <received messages> \
+     *  <received bytes> <time open>"
+     */
     statslinkinfo = 211,
+
+    /**
+     * RPL_STATSCOMMANDS
+     *
+     * "<command> <count>"
+     */
     statscommands = 212,
+
+    /**
+     * RPL_STATSCLINE
+     *
+     * "C <host> * <name> <port> <class>"
+     */
     statscline = 213,
+
+    /**
+     * RPL_STATSNLINE
+     *
+     * "N <host> * <name> <port> <class>"
+     */
     statsnline = 214,
+
+    /**
+     * RPL_STATSILINE
+     *
+     * "I <host> * <host> <port> <class>"
+     */
     statsiline = 215,
+
+    /**
+     * RPL_STATSKLINE
+     *
+     * K <host> * <username> <port> <class>"
+     */
     statskline = 216,
+
+    /**
+     * RPL_STATSYLINE
+     *
+     * "Y <class> <ping frequency> <connect frequency> <max sendq>"
+     */
     statsyline = 218,
+
+    /**
+     * RPL_ENDOFSTATS
+     *
+     * "<stats letter> :End of /STATS report"
+     */
     endofstats = 219,
+
+    /**
+     * RPL_STATSLLINE
+     *
+     * "L <hostmask> * <servername> <maxdepth>"
+     */
     statslline = 241,
+
+    /**
+     * RPL_STATSUPTIME
+     *
+     * ":Server Up %d days %d:%02d:%02d"
+     */
     statsuptime = 242,
+
+    /**
+     * RPL_STATSOLINE
+     *
+     * "O <hostmask> * <name>"
+     */
     statsoline = 243,
+
+    /**
+     * RPL_STATSHLINE
+     *
+     * "H <hostmask> * <servername>"
+     */
     statshline = 244,
+
+    /**
+     * RPL_UMODEIS
+     *
+     * "<user mode string>"
+     *
+     * To answer a query about a client's own mode,
+     * RPL_UMODEIS is sent back.
+     */
     umodeis = 221,
+
+    /**
+     * RPL_LUSERCLIENT
+     *
+     * ":There are <integer> users and <integer> \
+     *  invisible on <integer> servers"
+     */
     luserclient = 251,
+
+    /**
+     * RPL_LUSEROP
+     *
+     * "<integer> :operator(s) online"
+     */
     luserop = 252,
+
+    /**
+     * RPL_LUSERUNKNOWN
+     *
+     * "<integer> :unknown connection(s)"
+     */
     luserunknown = 253,
+
+    /**
+     * RPL_LUSERCHANNELS
+     *
+     * "<integer> :channels formed"
+     */
     luserchannels = 254,
+
+    /**
+     * RPL_LUSERME
+     *
+     * ":I have <integer> clients and <integer> servers"
+     *
+     * In processing an LUSERS message, the server
+     * sends a set of replies from RPL_LUSERCLIENT,
+     * RPL_LUSEROP, RPL_USERUNKNOWN,
+     * RPL_LUSERCHANNELS and RPL_LUSERME.  When
+     * replying, a server must send back
+     * RPL_LUSERCLIENT and RPL_LUSERME.  The other
+     * replies are only sent back if a non-zero count
+     * is found for them.
+     */
     luserme = 255,
+
+    /**
+     * RPL_ADMINME
+     *
+     * "<server> :Administrative info"
+     */
     adminme = 256,
+
+    /**
+     * RPL_ADMINLOC1
+     *
+     * ":<admin info>"
+     */
     adminloc1 = 257,
+
+    /**
+     * RPL_ADMINLOC2
+     *
+     * ":<admin info>"
+     */
     adminloc2 = 258,
+
+    /**
+     * RPL_ADMINEMAIL
+     *
+     * ":<admin info>"
+     *
+     * When replying to an ADMIN message, a server
+     * is expected to use replies RLP_ADMINME
+     * through to RPL_ADMINEMAIL and provide a text
+     * message with each.  For RPL_ADMINLOC1 a
+     * description of what city, state and country
+     * the server is in is expected, followed by
+     * details of the university and department
+     * (RPL_ADMINLOC2) and finally the administrative
+     * contact for the server (an email address here
+     * is required) in RPL_ADMINEMAIL.
+     */
     adminemail = 259
 };
 
 /**
  * \brief Describe a IRC message
  */
-class message {
-private:
-    std::string prefix_;             //!< optional prefix
-    std::string command_;            //!< command (maybe string or code)
-    std::vector<std::string> args_;  //!< parameters
-
-public:
-    /**
-     * Constructor.
-     *
-     * \param prefix the optional prefix
-     * \param command the command string or number
-     * \param args the arguments
-     */
-    inline message(std::string prefix = "", std::string command = "", std::vector<std::string> args = {}) noexcept
-        : prefix_(std::move(prefix))
-        , command_(std::move(command))
-        , args_(std::move(args))
-    {
-    }
-
-    /**
-     * Get the prefix.
-     *
-     * \return the prefix
-     */
-    inline const std::string& prefix() const noexcept
-    {
-        return prefix_;
-    }
-
-    /**
-     * Get the command.
-     *
-     * \return the command
-     */
-    inline const std::string& command() const noexcept
-    {
-        return command_;
-    }
-
-    /**
-     * Get the arguments.
-     *
-     * \return the arguments
-     */
-    inline const std::vector<std::string>& args() const noexcept
-    {
-        return args_;
-    }
-
-    /**
-     * Check if the message is defined.
-     *
-     * \return true if not empty
-     */
-    inline operator bool() const noexcept
-    {
-        return !command_.empty();
-    }
-
-    /**
-     * Check if the message is empty.
-     *
-     * \return true if empty
-     */
-    inline bool operator!() const noexcept
-    {
-        return command_.empty();
-    }
+struct message {
+    std::string prefix;             //!< optional prefix
+    std::string command;            //!< command (maybe string or code)
+    std::vector<std::string> args;  //!< parameters
 
     /**
      * Check if the command is of the given enum number.
@@ -254,10 +1205,10 @@
      * \return true if command is a number and equals to e
      */
     template <typename Enum>
-    inline bool is(Enum e) const noexcept
+    auto is(Enum e) const noexcept -> bool
     {
         try {
-            return std::stoi(command_) == static_cast<int>(e);
+            return std::stoi(command) == static_cast<int>(e);
         } catch (...) {
             return false;
         }
@@ -270,12 +1221,7 @@
      * \param index the index
      * \return a string or empty if out of bounds
      */
-    inline const std::string& arg(unsigned short index) const noexcept
-    {
-        static const std::string dummy;
-
-        return (index >= args_.size()) ? dummy : args_[index];
-    }
+    auto get(unsigned short index) const noexcept -> const std::string&;
 
     /**
      * Tells if the message is a CTCP.
@@ -283,7 +1229,7 @@
      * \param index the param index (maybe out of bounds)
      * \return true if CTCP
      */
-    bool is_ctcp(unsigned index) const noexcept;
+    auto is_ctcp(unsigned index) const noexcept -> bool;
 
     /**
      * Parse a CTCP message.
@@ -292,7 +1238,7 @@
      * \param index the param index
      * \return the CTCP command
      */
-    std::string ctcp(unsigned index) const;
+    auto ctcp(unsigned index) const -> std::string;
 
     /**
      * Parse a IRC message.
@@ -300,49 +1246,15 @@
      * \param line the buffer content (without `\r\n`)
      * \return the message (maybe empty if line is empty)
      */
-    static message parse(const std::string& line);
+    static auto parse(const std::string& line) -> message;
 };
 
 /**
  * \brief Describe a user.
  */
-class user {
-private:
-    std::string nick_;
-    std::string host_;
-
-public:
-    /**
-     * Construct a user.
-     *
-     * \param nick the nickname
-     * \param host the hostname
-     */
-    inline user(std::string nick, std::string host) noexcept
-        : nick_(std::move(nick))
-        , host_(std::move(host))
-    {
-    }
-
-    /**
-     * Get the nick part.
-     *
-     * \return the nickname
-     */
-    inline const std::string& nick() const noexcept
-    {
-        return nick_;
-    }
-
-    /**
-     * Get the host part.
-     *
-     * \return the host part
-     */
-    inline const std::string& host() const noexcept
-    {
-        return host_;
-    }
+struct user {
+    std::string nick;
+    std::string host;
 
     /**
      * Parse a nick/host combination.
@@ -350,7 +1262,7 @@
      * \param line the line to parse
      * \return a user
      */
-    static user parse(std::string_view line);
+    static auto parse(std::string_view line) -> user;
 };
 
 /**
@@ -580,8 +1492,6 @@
 
 #endif // !IRCCD_HAVE_SSL
 
-} // !irc
-
-} // !irccd
+} // !irccd::irc
 
 #endif // !IRCCD_IRC_HPP
--- a/libirccd/irccd/daemon/server.cpp	Fri Aug 03 12:50:30 2018 +0200
+++ b/libirccd/irccd/daemon/server.cpp	Fri Aug 03 19:31:00 2018 +0200
@@ -132,15 +132,15 @@
      * params[1] == channel
      * params[2] == End of NAMES list
      */
-    if (msg.args().size() < 3 || msg.arg(1) == "")
+    if (msg.args.size() < 3 || msg.get(1) == "")
         return false;
 
-    const auto it = names_map_.find(msg.arg(1));
+    const auto it = names_map_.find(msg.get(1));
 
     if (it != names_map_.end()) {
         handler({}, names_event{
             shared_from_this(),
-            msg.arg(1),
+            msg.get(1),
             std::vector<std::string>(it->second.begin(), it->second.end())
         });
 
@@ -159,7 +159,7 @@
      * params[1] == nickname
      * params[2] == End of WHOIS list
      */
-    const auto it = whois_map_.find(msg.arg(1));
+    const auto it = whois_map_.find(msg.get(1));
 
     if (it != whois_map_.end()) {
         handler({}, whois_event{shared_from_this(), it->second});
@@ -172,19 +172,19 @@
 auto server::dispatch_invite(const irc::message& msg, const recv_handler& handler) -> bool
 {
     // If join-invite is set, join the channel.
-    if ((flags_ & options::join_invite) == options::join_invite && is_self(msg.arg(0)))
-        join(msg.arg(1));
+    if ((flags_ & options::join_invite) == options::join_invite && is_self(msg.get(0)))
+        join(msg.get(1));
 
-    handler({}, invite_event{shared_from_this(), msg.prefix(), msg.arg(1), msg.arg(0)});
+    handler({}, invite_event{shared_from_this(), msg.prefix, msg.get(1), msg.get(0)});
 
     return true;
 }
 
 auto server::dispatch_isupport(const irc::message& msg) -> bool
 {
-    for (unsigned int i = 0; i < msg.args().size(); ++i) {
-        if (msg.arg(i).compare(0, 6, "PREFIX") == 0) {
-            modes_ = isupport_extract_prefixes(msg.arg(i));
+    for (unsigned int i = 0; i < msg.args.size(); ++i) {
+        if (msg.get(i).compare(0, 6, "PREFIX") == 0) {
+            modes_ = isupport_extract_prefixes(msg.get(i));
             break;
         }
     }
@@ -194,26 +194,26 @@
 
 auto server::dispatch_join(const irc::message& msg, const recv_handler& handler) -> bool
 {
-    if (is_self(msg.prefix()))
-        jchannels_.insert(msg.arg(0));
+    if (is_self(msg.prefix))
+        jchannels_.insert(msg.get(0));
 
-    handler({}, join_event{shared_from_this(), msg.prefix(), msg.arg(0)});
+    handler({}, join_event{shared_from_this(), msg.prefix, msg.get(0)});
 
     return true;
 }
 
 auto server::dispatch_kick(const irc::message& msg, const recv_handler& handler) -> bool
 {
-    if (is_self(msg.arg(1))) {
+    if (is_self(msg.get(1))) {
         // Remove the channel from the joined list.
-        jchannels_.erase(msg.arg(0));
+        jchannels_.erase(msg.get(0));
 
         // Rejoin the channel if the option has been set and I was kicked.
         if ((flags_ & options::auto_rejoin) == options::auto_rejoin)
-            join(msg.arg(0));
+            join(msg.get(0));
     }
 
-    handler({}, kick_event{shared_from_this(), msg.prefix(), msg.arg(0), msg.arg(1), msg.arg(2)});
+    handler({}, kick_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1), msg.get(2)});
 
     return true;
 }
@@ -222,12 +222,12 @@
 {
     handler({}, mode_event{
         shared_from_this(),
-        msg.prefix(),
-        msg.arg(0),
-        msg.arg(1),
-        msg.arg(2),
-        msg.arg(3),
-        msg.arg(4)
+        msg.prefix,
+        msg.get(0),
+        msg.get(1),
+        msg.get(2),
+        msg.get(3),
+        msg.get(4)
     });
 
     return true;
@@ -246,14 +246,14 @@
      * IDEA for the future: maybe give the appropriate mode as a second
      * parameter in onNames.
      */
-    if (msg.args().size() < 4 || msg.arg(2) == "" || msg.arg(3) == "")
+    if (msg.args.size() < 4 || msg.get(2) == "" || msg.get(3) == "")
         return false;
 
-    auto users = string_util::split(msg.arg(3), " \t");
+    auto users = string_util::split(msg.get(3), " \t");
 
     // The listing may add some prefixes, remove them if needed.
     for (const auto& u : users)
-        names_map_[msg.arg(2)].insert(clean_prefix(modes_, u));
+        names_map_[msg.get(2)].insert(clean_prefix(modes_, u));
 
     return false;
 }
@@ -261,17 +261,17 @@
 auto server::dispatch_nick(const irc::message& msg, const recv_handler& handler) -> bool
 {
     // Update our nickname.
-    if (is_self(msg.prefix()))
-        nickname_ = msg.arg(0);
+    if (is_self(msg.prefix))
+        nickname_ = msg.get(0);
 
-    handler({}, nick_event{shared_from_this(), msg.prefix(), msg.arg(0)});
+    handler({}, nick_event{shared_from_this(), msg.prefix, msg.get(0)});
 
     return true;
 }
 
 auto server::dispatch_notice(const irc::message& msg, const recv_handler& handler) -> bool
 {
-    handler({}, notice_event{shared_from_this(), msg.prefix(), msg.arg(0), msg.arg(1)});
+    handler({}, notice_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
 
     return true;
 }
@@ -279,45 +279,45 @@
 auto server::dispatch_part(const irc::message& msg, const recv_handler& handler) -> bool
 {
     // Remove the channel from the joined list if I left a channel.
-    if (is_self(msg.prefix()))
-        jchannels_.erase(msg.arg(1));
+    if (is_self(msg.prefix))
+        jchannels_.erase(msg.get(1));
 
-    handler({}, part_event{shared_from_this(), msg.prefix(), msg.arg(0), msg.arg(1)});
+    handler({}, part_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
 
     return true;
 }
 
 auto server::dispatch_ping(const irc::message& msg) -> bool
 {
-    assert(msg.command() == "PING");
+    assert(msg.command == "PING");
 
-    send(str(format("PONG %1%") % msg.arg(0)));
+    send(str(format("PONG %1%") % msg.get(0)));
 
     return false;
 }
 
 auto server::dispatch_privmsg(const irc::message& msg, const recv_handler& handler) -> bool
 {
-    assert(msg.command() == "PRIVMSG");
+    assert(msg.command == "PRIVMSG");
 
     if (msg.is_ctcp(1)) {
         auto cmd = msg.ctcp(1);
 
         if (cmd.compare(0, 6, "ACTION") == 0)
-            handler({}, me_event{shared_from_this(), msg.prefix(), msg.arg(0), cmd.substr(7)});
+            handler({}, me_event{shared_from_this(), msg.prefix, msg.get(0), cmd.substr(7)});
         else
             return false;
     } else
-        handler({}, message_event{shared_from_this(), msg.prefix(), msg.arg(0), msg.arg(1)});
+        handler({}, message_event{shared_from_this(), msg.prefix, msg.get(0), msg.get(1)});
 
     return true;
 }
 
 auto server::dispatch_topic(const irc::message& msg, const recv_handler& handler) -> bool
 {
-    assert(msg.command() == "TOPIC");
+    assert(msg.command == "TOPIC");
 
-    handler({}, topic_event{shared_from_this(), msg.arg(0), msg.arg(1), msg.arg(2)});
+    handler({}, topic_event{shared_from_this(), msg.get(0), msg.get(1), msg.get(2)});
 
     return true;
 }
@@ -331,13 +331,13 @@
      * params[1] == nickname
      * params[2] == list of channels with their prefixes
      */
-    if (msg.args().size() < 3 || msg.arg(1) == "" || msg.arg(2) == "")
+    if (msg.args.size() < 3 || msg.get(1) == "" || msg.get(2) == "")
         return false;
 
-    auto it = whois_map_.find(msg.arg(1));
+    auto it = whois_map_.find(msg.get(1));
 
     if (it != whois_map_.end()) {
-        auto channels = string_util::split(msg.arg(2), " \t");
+        auto channels = string_util::split(msg.get(2), " \t");
 
         // Clean their prefixes.
         for (auto& s : channels)
@@ -361,15 +361,15 @@
      * params[4] == * (no idea what is that)
      * params[5] == realname
      */
-    if (msg.args().size() < 6 || msg.arg(1) == "" || msg.arg(2) == "" || msg.arg(3) == "" || msg.arg(5) == "")
+    if (msg.args.size() < 6 || msg.get(1) == "" || msg.get(2) == "" || msg.get(3) == "" || msg.get(5) == "")
         return false;
 
     whois_info info;
 
-    info.nick = msg.arg(1);
-    info.user = msg.arg(2);
-    info.host = msg.arg(3);
-    info.realname = msg.arg(5);
+    info.nick = msg.get(1);
+    info.user = msg.get(2);
+    info.host = msg.get(3);
+    info.realname = msg.get(5);
 
     whois_map_.emplace(info.nick, info);
 
@@ -384,25 +384,25 @@
         handled = dispatch_isupport(message);
     else if (message.is(irc::err::nomotd) || message.is(irc::rpl::endofmotd))
         handled = dispatch_connect(message, handler);
-    else if (message.command() == "INVITE")
+    else if (message.command == "INVITE")
         handled = dispatch_invite(message, handler);
-    else if (message.command() == "JOIN")
+    else if (message.command == "JOIN")
         handled = dispatch_join(message, handler);
-    else if (message.command() == "KICK")
+    else if (message.command == "KICK")
         handled = dispatch_kick(message, handler);
-    else if (message.command() == "MODE")
+    else if (message.command == "MODE")
         handled = dispatch_mode(message, handler);
-    else if (message.command() == "NICK")
+    else if (message.command == "NICK")
         handled = dispatch_nick(message, handler);
-    else if (message.command() == "NOTICE")
+    else if (message.command == "NOTICE")
         handled = dispatch_notice(message, handler);
-    else if (message.command() == "TOPIC")
+    else if (message.command == "TOPIC")
         handled = dispatch_topic(message, handler);
-    else if (message.command() == "PART")
+    else if (message.command == "PART")
         handled = dispatch_part(message, handler);
-    else if (message.command() == "PING")
+    else if (message.command == "PING")
         handled = dispatch_ping(message);
-    else if (message.command() == "PRIVMSG")
+    else if (message.command == "PRIVMSG")
         handled = dispatch_privmsg(message, handler);
     else if (message.is(irc::rpl::namreply))
         handled = dispatch_namreply(message);
@@ -649,7 +649,7 @@
 
 auto server::is_self(std::string_view target) const noexcept -> bool
 {
-    return nickname_ == irc::user::parse(target).nick();
+    return nickname_ == irc::user::parse(target).nick;
 }
 
 void server::connect(connect_handler handler) noexcept
--- a/plugins/links/links.cpp	Fri Aug 03 12:50:30 2018 +0200
+++ b/plugins/links/links.cpp	Fri Aug 03 19:31:00 2018 +0200
@@ -165,7 +165,7 @@
     string_util::subst subst;
 
     subst.keywords.emplace("channel", channel_);
-    subst.keywords.emplace("nickname", irc::user::parse(origin_).nick());
+    subst.keywords.emplace("nickname", irc::user::parse(origin_).nick);
     subst.keywords.emplace("origin", origin_);
     subst.keywords.emplace("server", server_->get_id());
     subst.keywords.emplace("title", title);
--- a/tests/src/libirccd/irc/main.cpp	Fri Aug 03 12:50:30 2018 +0200
+++ b/tests/src/libirccd/irc/main.cpp	Fri Aug 03 19:31:00 2018 +0200
@@ -29,32 +29,24 @@
 
 BOOST_AUTO_TEST_CASE(no_prefix)
 {
-    irc::message m;
-
-    BOOST_TEST(!m);
+    const auto m = irc::message::parse("PRIVMSG jean :bonjour à toi");
 
-    m = irc::message::parse("PRIVMSG jean :bonjour à toi");
-    BOOST_TEST(m);
-    BOOST_TEST(m.prefix().empty());
-    BOOST_TEST(m.command() == "PRIVMSG");
-    BOOST_TEST(m.args().size() == 2U);
-    BOOST_TEST(m.args()[0] == "jean");
-    BOOST_TEST(m.args()[1] == "bonjour à toi");
+    BOOST_TEST(m.prefix.empty());
+    BOOST_TEST(m.command == "PRIVMSG");
+    BOOST_TEST(m.args.size() == 2U);
+    BOOST_TEST(m.args[0] == "jean");
+    BOOST_TEST(m.args[1] == "bonjour à toi");
 }
 
 BOOST_AUTO_TEST_CASE(prefix)
 {
-    irc::message m;
-
-    BOOST_TEST(!m);
+    const auto m = irc::message::parse(":127.0.0.1 PRIVMSG jean :bonjour à toi");
 
-    m = irc::message::parse(":127.0.0.1 PRIVMSG jean :bonjour à toi");
-    BOOST_TEST(m);
-    BOOST_TEST(m.prefix() == "127.0.0.1");
-    BOOST_TEST(m.command() == "PRIVMSG");
-    BOOST_TEST(m.args().size() == 2U);
-    BOOST_TEST(m.args()[0] == "jean");
-    BOOST_TEST(m.args()[1] == "bonjour à toi");
+    BOOST_TEST(m.prefix == "127.0.0.1");
+    BOOST_TEST(m.command == "PRIVMSG");
+    BOOST_TEST(m.args.size() == 2U);
+    BOOST_TEST(m.args[0] == "jean");
+    BOOST_TEST(m.args[1] == "bonjour à toi");
 }
 
 BOOST_AUTO_TEST_SUITE_END()
@@ -63,23 +55,23 @@
 
 BOOST_AUTO_TEST_CASE(basics)
 {
-    auto user = irc::user::parse("jean!~jean@127.0.0.1");
+    const auto user = irc::user::parse("jean!~jean@127.0.0.1");
 
-    BOOST_TEST(user.nick() == "jean");
-    BOOST_TEST(user.host() == "~jean@127.0.0.1");
+    BOOST_TEST(user.nick == "jean");
+    BOOST_TEST(user.host == "~jean@127.0.0.1");
 
-    auto usersimple = irc::user::parse("jean");
+    const auto usersimple = irc::user::parse("jean");
 
-    BOOST_TEST(usersimple.nick() == "jean");
-    BOOST_TEST(usersimple.host().empty());
+    BOOST_TEST(usersimple.nick == "jean");
+    BOOST_TEST(usersimple.host.empty());
 }
 
 BOOST_AUTO_TEST_CASE(empty)
 {
-    auto user = irc::user::parse("");
+    const auto user = irc::user::parse("");
 
-    BOOST_TEST(user.nick().empty());
-    BOOST_TEST(user.host().empty());
+    BOOST_TEST(user.nick.empty());
+    BOOST_TEST(user.host.empty());
 }
 
 BOOST_AUTO_TEST_SUITE_END()