implement some simple CTCP
authorTero Marttila <terom@fixme.fi>
Mon, 30 Mar 2009 01:30:37 +0300
changeset 84 2791bb73bbee
parent 83 c8e2dac08207
child 85 75bc8b164ef8
implement some simple CTCP
src/irc_conn.c
src/irc_conn.h
--- 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 <stdlib.h>
 #include <string.h>
+#include <assert.h>
 
 /**
  * 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
--- 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 <stdbool.h>
 
 /**
- * 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);