# HG changeset patch # User Tero Marttila # Date 1238365837 -10800 # Node ID 2791bb73bbee825490391943d71aa85bed8a5792 # Parent c8e2dac08207381e797df4308042978bb7413c0c implement some simple CTCP diff -r c8e2dac08207 -r 2791bb73bbee src/irc_conn.c --- a/src/irc_conn.c Fri Mar 27 17:22:43 2009 +0200 +++ b/src/irc_conn.c Mon Mar 30 01:30:37 2009 +0300 @@ -5,6 +5,7 @@ #include #include +#include /** * Handle an async error on this IRC connection that we could not recover from any other way, the protocol is now dead, @@ -91,19 +92,96 @@ } /** + * CTCP ACTION handler + */ +static void irc_conn_on_CTCP_ACTION (const struct irc_line *line, void *arg) +{ + struct irc_conn *conn = arg; + + // build the pseudo-line and invoke + struct irc_line action_line = { + .source = line->source, + .command = "CTCP ACTION", + .args = { + line->args[0], + line->args[1], + NULL + } + }; + + // invoke the general command handlers + irc_cmd_invoke(&conn->handlers, &action_line); +} + +/** * Our command handlers */ -static struct irc_cmd_handler _cmd_handlers[] = { +static struct irc_cmd_handler irc_conn_handlers[] = { { IRC_RPL_WELCOME, &on_RPL_WELCOME }, { "PING", &on_PING }, { "NICK", &on_NICK }, { NULL, NULL, }, + +}, irc_conn_ctcp_handlers[] = { + { "ACTION", &irc_conn_on_CTCP_ACTION }, + { NULL, NULL } }; /** + * Incoming CTCP message handler + * + * We only handle "simple" CTCP messages, i.e. those that begin and end with X-DELIM and contain a single tagged + * extended message. The full range of CTCP quoting etc specified in the "offical" spec referenced below is not + * implemented, as it is rarely used, or even implemented. + * + * http://www.irchelp.org/irchelp/rfc/ctcpspec.html + */ +static void irc_conn_on_CTCP (struct irc_conn *conn, const struct irc_line *line) +{ + // copy the message data into a mutable buffer + char data_buf[strlen(line->args[1])], *data = data_buf; + strcpy(data_buf, line->args[1]); + + // should only be called when this is true... + assert(*data++ == '\001'); + + // tokenize the extended message + // XXX: do something with the trailing data? + char *msg = strsep(&data, "\001"); + + // parse the "command" tag + char *tag = strsep(&msg, " "); + + // invalid if missing + if (tag == NULL) + return log_warn("CTCP message with no tag: '%s'", data); + + // parse the CTCP "line" + struct irc_line ctcp_line = { + // the sender of the message + .source = line->source, + + // the CTCP extended message tag + .command = tag, + .args = { + // the destination of the message + line->args[0], + + // the rest of the CTCP extended message payload + msg, + + NULL + } + }; + + // invoke the CTCP command handlers + irc_cmd_invoke(&conn->ctcp_handlers, &ctcp_line); +} + +/** * Incoming line handler */ -void irc_conn_on_line (char *line_buf, void *arg) +static void irc_conn_on_line (char *line_buf, void *arg) { struct irc_conn *conn = arg; struct irc_line line; @@ -119,14 +197,20 @@ return; } - // invoke command handlers - irc_cmd_invoke(&conn->handlers, &line); + // trap CTCP messages + if (strcasecmp(line.command, "PRIVMSG") == 0 && line.args[1][0] == '\001') + // parse and invoke the CTCP command handlers + irc_conn_on_CTCP(conn, &line); + + else + // invoke command handlers + irc_cmd_invoke(&conn->handlers, &line); } /** * Transport failed */ -void irc_conn_on_error (struct error_info *err, void *arg) +static void irc_conn_on_error (struct error_info *err, void *arg) { struct irc_conn *conn = arg; @@ -150,7 +234,7 @@ } } -static struct line_proto_callbacks _lp_callbacks = { +static struct line_proto_callbacks irc_conn_lp_callbacks = { .on_line = &irc_conn_on_line, .on_error = &irc_conn_on_error, }; @@ -170,13 +254,17 @@ // initialize command handlers irc_cmd_init(&conn->handlers); + irc_cmd_init(&conn->ctcp_handlers); // add the core handlers - if ((ERROR_CODE(err) = irc_conn_add_cmd_handlers(conn, _cmd_handlers, conn))) + if ( + (ERROR_CODE(err) = irc_cmd_add(&conn->handlers, irc_conn_handlers, conn)) + || (ERROR_CODE(err) = irc_cmd_add(&conn->ctcp_handlers, irc_conn_ctcp_handlers, conn)) + ) goto error; // create the line_proto, with our on_line handler - if (line_proto_create(&conn->lp, sock, IRC_LINE_MAX * 1.5, &_lp_callbacks, conn, err)) + if (line_proto_create(&conn->lp, sock, IRC_LINE_MAX * 1.5, &irc_conn_lp_callbacks, conn, err)) goto error; // ok diff -r c8e2dac08207 -r 2791bb73bbee src/irc_conn.h --- a/src/irc_conn.h Fri Mar 27 17:22:43 2009 +0200 +++ b/src/irc_conn.h Mon Mar 30 01:30:37 2009 +0300 @@ -5,8 +5,8 @@ * @file * * Support for connections to IRC servers, a rather fundamental thing. This holds the line_proto to handle the network - * communications, and then takes care of sending/receiving commands, as well as updating some core state like - * current nickname. + * communications, and then takes care of sending/receiving commands, as well as some core functionality like + * responding to PING requests, and tracking our current nickname. */ struct irc_conn; @@ -19,16 +19,16 @@ #include /** - * The info required to register with irc_conn_register + * The info required to register using irc_conn_register() */ struct irc_conn_register_info { - /* Nickname to use on that server */ + /** Nickname to use on that server */ const char *nickname; - /* Username to supply */ + /** Username to supply */ const char *username; - /* Realname to supply */ + /** Realname to supply */ const char *realname; }; @@ -37,13 +37,13 @@ */ struct irc_conn_callbacks { /** - * irc_conn_register has completed. + * Recieved RPL_WELCOME, so irc_conn_register has completed, and we are now registered. */ void (*on_registered) (struct irc_conn *conn, void *arg); /** * The connection has failed in some way, and can not be considered useable anymore. Sending messages won't work, - * and no more messages will be received. The connection should be destroyed, probably from this callback. + * and no more messages will be received. The connection should be destroyed, probably directly from this callback. * * NOTE: Implementing this callback is mandatory */ @@ -52,29 +52,42 @@ /** * The connection was closed cleanly after irc_conn_QUIT. * - * You probably want to destroy the irc_conn now to clean up + * You probably want to destroy the irc_conn now to clean up. */ void (*on_quit) (struct irc_conn *conn, void *arg); }; -/* - * Connection state +/** + * A single sock_stream connection to a single IRC server, providing a nice interface to the low-level IRC protocol. + * + * Create a irc_conn from a connected sock_stream using irc_conn_create(), providing the high-level callbacks. Then, + * register handlers for the IRC protocol messages recieved from the server using irc_conn_add_cmd_handlers() or + * irc_cmd_add and irc_conn::handlers/irc_conn::ctcp_handlers. + * + * You can then perform the registration step using irc_conn_register(), providing a irc_conn_register_info struct with + * the relevant info. + * + * Once you have succesfully registered (RPL_WELCOME recieved, irc_conn_callbacks::on_registered called, + * irc_conn::registered set), you can then send messages to the server in the form of irc_line's using the set of + * irc_conn_send functions. */ struct irc_conn { - /* We are a line-based protocol */ + /** We are a line-based protocol, this wraps the given sock_stream */ struct line_proto *lp; - /** Callbacks */ + /** High-level callbacks */ struct irc_conn_callbacks callbacks; /** Opaque argument for callbacks */ void *cb_arg; - /** @group flags @{ */ + /** @defgroup irc_conn_status Status flags + * @{ + */ /** Registration request sent */ bool registering; - /** Registered (as in, we have a working nickname)? */ + /** Registered (as in, we have a working nickname) */ bool registered; /** Quit message sent, waiting for server to close connection */ @@ -85,28 +98,85 @@ // @} - /** Our current real nickname */ + /** + * Our current, actual nickname. This is set to NULL while we are still registering, but is then initialized when we + * get the first RPL_WELCOME message, and then kept up-to-date by handling relevant NICK messages. + */ char *nickname; - /** Command handlers */ + /** + * General IRC command handlers. These handlers are invoked directly for all irc_line's recieved from the IRC + * server, everything including core PING/NICK/RPL_WELCOME handling is implemented using these. You should add + * your own handlers using irc_conn_add_cmd_handlers() to implement whatever functionality it is that you want; + * see irc_cmd.h for more info about how these work. The "command" field is the literal IRC command as received + * from the server itself. + * + * So for example, the following line: + * \verbatim :nick!user@host PRIVMSG #foo :Hello everyone! \endverbatim + * + * results in the following irc_line: + * \code + * { { "nick", "user", "host" }, "PRIVMSG", { "#foo", "Hello everyone!", NULL } } + * \endcode + * + * As with all rules, there are exceptions - CTCP messages are handled differently. PRIVMSG's which contain + * CTCP extended messages are trapped before these handlers are invoked, and are instead processed using + * irc_conn::ctcp_handlers. Some built-in CTCP message handlers are provided that then submit these messages back + * to this main irc_conn::handlers list, as pseudo-irc_line's with a command field like "CTCP ACTION". These + * include: + * + * CTCP ACTION - { { "nick", "user", "host" }, "CTCP ACTION", "#foo", "sends an action message" } + * + * @see irc_cmd.h + * @see irc_line + * @see irc_conn::ctcp_handlers + */ irc_cmd_handlers_t handlers; + + /** + * CTCP command handlers. These handlers are invoked for all PRIVMSG's recieved from the IRC server which begin + * with the CTCP X-DELIM char (in other words, a simplified CTCP spec is implemented). These PRIVMSG's will not be + * seen using irc_conn::handlers. These CTCP messages are then parsed, and a pseudo-irc_line is created, with the + * same source as the real message, the command set to the CTCP extended message tag, args[0] as the PRIVMSG's + * destination, and args[1] as the rest of the CTCP extended message payload. + * + * So for example, the following line: + * \verbatim + :nick!user@host PRIVMSG #foo :\001ACTION does something lame\001 + \endverbatim + * + * results in the following irc_line: + * \code + * { { "nick", "user", "host" }, "ACTION", "#foo", "does something lame" } + * \endcode + * + * Internally, some of these CTCP messages are then redirected back to handlers for convenience; currently only + * "ACTION". + * + * @see irc_cmd.h + * @see irc_line + * @see irc_conn::handlers + */ + irc_cmd_handlers_t ctcp_handlers; }; /** - * Create a new irc_conn using the given sock_stream, which should be connected to an IRC server. + * Create a new irc_conn using the given sock_stream, which should be connected to an IRC server (i.e. ready for + * read/write). * - * This does not yet send any requests to the server, it only sets up the core state. Use irc_conn_register to register - * with the server. + * This does not yet send any requests to the server, it only sets up the core state. Use irc_conn_register() to + * actually register with the server. * - * On success, the resulting irc_conn is returned via *conn with SUCCESS. Otherwise, -ERR_* and error info is returned + * On success, the resulting irc_conn is returned via *conn_ptr with SUCCESS. Otherwise, ERR_* with error info returned * via *err. * - * @param conn the new irc_conn structure is returned via this pointer + * @param conn_ptr returned new irc_conn structure * @param sock the socket connected to the IRC server - * @param err errors are returned via this pointer - * @return error code + * @param callbacks the high-level status callbacks, required + * @param cb_arg opqaue context argument for callbacks + * @param err returned error info */ -err_t irc_conn_create (struct irc_conn **conn, struct sock_stream *sock, const struct irc_conn_callbacks *callbacks, +err_t irc_conn_create (struct irc_conn **conn_ptr, struct sock_stream *sock, const struct irc_conn_callbacks *callbacks, void *cb_arg, struct error_info *err); /** @@ -120,9 +190,8 @@ * Add a new chain of command handler callbacks to be used to handle irc_lines from the server. The given arg will be * passed to the callbacks as the context argument. The chain will be appended to the end of the current list of chains. * - * XXX: rename to not conflict with irc_conn_register() - * - * @param chain the array of irc_cmd_handler structs, terminated with a NULL entry + * @param conn the connection to use + * @param handlers the array of irc_cmd_handler structs, terminated with a NULL entry * @param arg the context argument to use for the callbacks */ err_t irc_conn_add_cmd_handlers (struct irc_conn *conn, struct irc_cmd_handler *handlers, void *arg); @@ -131,62 +200,107 @@ * Register with the IRC server using the given registration info (initial nickname etc.) * * This sends the NICK/USER command sequence. + * + * @param conn the connection to use + * @param info the information required to register, including nickname/username/etc */ err_t irc_conn_register (struct irc_conn *conn, const struct irc_conn_register_info *info); /** - * @group Simple request functions + * Send a generic IRC message by formatting the given irc_line and sending it as a line. * - * The error handling of these functions is such that the error return code is can be used or ignored as convenient, - * as connection-fatal errors will be handled internally. + * Use the irc_conn_COMMAND functions defined below for a more convenient interface. + * + * An error is returned if the line contains invalid data or writing the line to the sock_stream fails. Possible socket + * failures will also be reported using irc_conn_callbacks::on_error, so ignoring return values from this is usually + * OK... + * + * @param conn the IRC protocol connection + * @param line the irc_line protocol line to send + */ +err_t irc_conn_send (struct irc_conn *conn, const struct irc_line *line); + +/** + * @defgroup irc_conn_COMMAND Simple request functions + * + * These functions all simply build the corresponding irc_line struct, and then pass it on to irc_conn_send(). * * @{ */ /** - * Send a generic IRC message + * Send a NICK message. Usually either called indirectly via irc_conn_register, or used to change to a new nickname. + * + * If succesfull, this will result in a NICK message for our current nickname; irc_conn will handle this to update + * irc_conn::nickname. * * @param conn the IRC protocol connection - * @param line the irc_line protocol line to send - * @return error code - */ -err_t irc_conn_send (struct irc_conn *conn, const struct irc_line *line); - -/** - * Send a NICK message + * @param nickname the new nickname to use * - * @param nickname the new nickname to use + * Possible errors (from RFC2812): + * ERR_NONICKNAMEGIVEN + * ERR_NICKNAMEINUSE + * ERR_UNAVAILRESOURCE + * ERR_ERRONEUSNICKNAME + * ERR_NICKCOLLISION + * ERR_RESTRICTED */ err_t irc_conn_NICK (struct irc_conn *conn, const char *nickname); /** - * Send a USER message + * Send a USER message. Usually called indirectly via irc_conn_register. * - * @param username the username to register with, may be replaced with ident reply + * @param conn the IRC protocol connection + * @param username the username to register with, may be replaced with username from ident reply * @param realname the full-name to register with + * + * Possible errors (from RFC2812): + * ERR_NEEDMOREPARAMS + * ERR_ALREADYREGISTRED */ err_t irc_conn_USER (struct irc_conn *conn, const char *username, const char *realname); /** - * Send a PONG message to the given target + * Send a PONG message to the given target. * + * Note that you do not need to handle the normal PING/PONG cycle, irc_conn does this for you. + * + * @param conn the IRC protocol connection * @param target the PING source, aka. the target to send the PONG reply to + * + * Possible errors (from RFC2812): + * ERR_NOORIGIN + * ERR_NOSUCHSERVER */ err_t irc_conn_PONG (struct irc_conn *conn, const char *target); /** - * Send a JOIN message for the given channel + * Send a simple JOIN message for the given channel. Note that this does not implement all possible arguments. * - * XXX: this doesn't implement the full command options + * If succesfull, this will result in a JOIN message for us on the given channel, plus a series of + * RPL_NAMREPLY/RPL_ENDOFNAMES/RPL_TOPIC/etc messages. * + * @param conn the IRC protocol connection * @param channel the full channel name to join + * + * Possible errors (from RFC2812): + * ERR_NEEDMOREPARAMS ERR_BANNEDFROMCHAN + * ERR_INVITEONLYCHAN ERR_BADCHANNELKEY + * ERR_CHANNELISFULL ERR_BADCHANMASK + * ERR_NOSUCHCHANNEL ERR_TOOMANYCHANNELS + * ERR_TOOMANYTARGETS ERR_UNAVAILRESOURCE + * */ err_t irc_conn_JOIN (struct irc_conn *conn, const char *channel); /** - * Send a QUIT message to the server. + * Send a QUIT message to the server. The server will reply with an ERROR message and close the connection. * - * This updates our state as disconnecting, and waits for the server to close the connection cleanly. + * This updates our state as disconnecting, and once EOF is recieved, the irc_conn_callbacks::on_quit callback is + * called. + * + * @param conn the IRC protocol connection + * @param message the quit message, which may be displayed to other clients */ err_t irc_conn_QUIT (struct irc_conn *conn, const char *message);