src/irc_conn.c
author Tero Marttila <terom@fixme.fi>
Thu, 28 May 2009 01:17:36 +0300
branchnew-lib-errors
changeset 219 cefec18b8268
parent 217 7728d6ec3abf
permissions -rw-r--r--
some of the lib/transport stuff compiles
#include "irc_conn.h"
#include "irc_cmd.h"
#include "irc_proto.h"
#include "log.h"

#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,
 * and should be considered as destroyed after this returns.
 *
 * For conveniance, this returns the ERROR_CODE
 */
static err_t irc_conn_set_error (struct irc_conn *conn, error_t *err)
{
    // notify user callback
    conn->callbacks.on_error(conn, err, conn->cb_arg);

    return ERROR_CODE(err);
}


/**
 * Update irc_conn.nickname
 */
static err_t irc_conn_set_nickname (struct irc_conn *conn, const char *nickname)
{
    error_t err;

    // drop old nickname
    free(conn->nickname);

    // strdup
    if ((conn->nickname = strdup(nickname)) == NULL) {
        SET_ERROR(&err, ERR_STRDUP);

        // notify
        return irc_conn_set_error(conn, &err);
    }

    // ok
    return SUCCESS;
}

/**
 * 001 <nick> :Welcome to the Internet Relay Network <nick>!<user>@<host>
 */
static void on_RPL_WELCOME (const struct irc_line *line, void *arg)
{
    struct irc_conn *conn = arg;

    // update state
    conn->registering = false;
    conn->registered = true;
    
    // set our real nickname from the message target
    if (irc_conn_set_nickname(conn, line->args[0]))
        return;

    // trigger callback
    if (conn->callbacks.on_registered)
        conn->callbacks.on_registered(conn, conn->cb_arg);
}

/**
 * PING <server1> [ <server2> ]
 *
 * Send a 'PONG <server1>` reply right away.
 */ 
static void on_PING (const struct irc_line *line, void *arg)
{
    struct irc_conn *conn = arg;

    // just reply
    irc_conn_PONG(conn, line->args[0]);
}

/**
 * NICK <nickname>
 *
 * If the prefix is us, then update our nickname
 */
static void on_NICK (const struct irc_line *line, void *arg)
{
    struct irc_conn *conn = arg;

    // ignore if it's not us
    if (!line->source || irc_cmp_nick(line->source->nickname, conn->nickname))
        return;

    // update our nickname
    irc_conn_set_nickname(conn, line->args[0]);
}

/**
 * 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 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]) + 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
 */
static void irc_conn_on_line (char *line_buf, void *arg) 
{
    struct irc_conn *conn = arg;
    struct irc_line line;
    struct irc_nm nm;
    int err;
    
    // log
    log_debug("%s", line_buf);

    // parse
    if ((err = irc_line_parse(&line, &nm, line_buf))) {
        log_warn("invalid line: %s: %s\n", line_buf, error_name(err));
        return;
    }

    // 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
 */
static void irc_conn_on_error (error_t *err, void *arg)
{
    struct irc_conn *conn = arg;

    // EOF after quit?
    if (ERROR_CODE(err) == ERR_EOF && conn->quitting) {
        // udpate states
        conn->registered = false;
        conn->quitting = false;
        conn->quit = true;

        // callback
        if (conn->callbacks.on_quit)
            conn->callbacks.on_quit(conn, conn->cb_arg);

    } else {
        // log
        log_error(err, "transport error");
        
        // trash ourselves
        irc_conn_set_error(conn, err);
    }
}

static struct line_proto_callbacks irc_conn_lp_callbacks = {
    .on_line        = &irc_conn_on_line,
    .on_error       = &irc_conn_on_error,
};

// XXX: ugly hack to get at an event_base
#include "sock_internal.h"

struct event_base **ev_base_ptr = &_sock_stream_ctx.ev_base;

err_t irc_conn_create (struct irc_conn **conn_ptr, transport_t *transport, const struct irc_conn_callbacks *callbacks, 
        void *cb_arg, error_t *err)
{
    struct irc_conn *conn;

    // alloc new state struct
    if ((conn = calloc(1, sizeof(struct irc_conn))) == NULL)
        return SET_ERROR(err, ERR_CALLOC);

    // init state
    conn->callbacks = *callbacks;
    conn->cb_arg = cb_arg;

    // initialize command handlers
    irc_cmd_init(&conn->handlers);
    irc_cmd_init(&conn->ctcp_handlers);
    
    // add the core handlers 
    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, transport, IRC_LINE_MAX * 1.5, &irc_conn_lp_callbacks, conn, err))
        goto error;

    // create the outgoing line queue
    if (irc_queue_create(&conn->out_queue, *ev_base_ptr, conn->lp, err))
        goto error;

    // ok
    *conn_ptr = conn;

    return SUCCESS;

error:
    // release
    irc_conn_destroy(conn);

    return ERROR_CODE(err);    
}

void irc_conn_destroy (struct irc_conn *conn)
{
    // the line_proto
    if (conn->lp)
        line_proto_destroy(conn->lp);

    // the queue
    if (conn->out_queue)
        irc_queue_destroy(conn->out_queue);
    
    // the command handlers
    irc_cmd_clear(&conn->handlers);
    irc_cmd_clear(&conn->ctcp_handlers);

    // additional data
    free(conn->nickname);

    // the irc_conn itself
    free(conn);
}

err_t irc_conn_add_cmd_handlers (struct irc_conn *conn, struct irc_cmd_handler *handlers, void *arg)
{
    // use the irc_cmd stuff
    return irc_cmd_add(&conn->handlers, handlers, arg);
}

err_t irc_conn_register (struct irc_conn *conn, const struct irc_conn_register_info *info)
{
    err_t err;

    if (conn->registering || conn->registered)
        return ERR_IRC_CONN_REGISTER_STATE;

    // send the initial messages
    if (
            (err = irc_conn_NICK(conn, info->nickname))
        ||  (err = irc_conn_USER(conn, info->username, info->realname))
    )
        return err;

    // set state
    conn->registering = true;
    
    // ok
    return SUCCESS;
}

bool irc_conn_self (struct irc_conn *conn, const char *nickname)
{
    return conn && nickname && conn->nickname && (irc_cmp_nick(conn->nickname, nickname) == 0);
}

err_t irc_conn_send (struct irc_conn *conn, const struct irc_line *line)
{
    // just proxy off to irc_quuee
    return irc_queue_process(conn->out_queue, line);
}

err_t irc_conn_NICK (struct irc_conn *conn, const char *nickname)
{
    // NICK <nickname>
    struct irc_line line = {
        NULL, "NICK", { nickname }
    };
    
    return irc_conn_send(conn, &line);
}

err_t irc_conn_USER (struct irc_conn *conn, const char *username, const char *realname)
{
    // USER <user> <mode> * <realname>
    struct irc_line line = {
        NULL, "USER", { username, "0", "*", realname }
    };
    
    return irc_conn_send(conn, &line);
}

err_t irc_conn_PONG (struct irc_conn *conn, const char *target)
{
    // PONG <server> [ <server2> ]
    // params are actually the wrong way around now, but nobody cares
    struct irc_line line = {
        NULL, "PONG", { target }
    };

    return irc_conn_send(conn, &line);
}

err_t irc_conn_JOIN (struct irc_conn *conn, const char *channel)
{
    // JOIN (<channel> [ "," <channel> [ ... ] ]) [<key> [ "," <key> [ ... ] ] ]
    struct irc_line line = {
        NULL, "JOIN", { channel }
    };

    return irc_conn_send(conn, &line);
}

err_t irc_conn_PRIVMSG (struct irc_conn *conn, const char *target, const char *message)
{
    // PRIVMSG <msgtarget> <message>
    struct irc_line line = {
        NULL, "PRIVMSG", { target, message }
    };

    return irc_conn_send(conn, &line);
}

err_t irc_conn_NOTICE (struct irc_conn *conn, const char *target, const char *message)
{
    // NOTICE <msgtarget> <message>
    struct irc_line line = {
        NULL, "NOTICE", { target, message }
    };

    return irc_conn_send(conn, &line);
}

err_t irc_conn_QUIT (struct irc_conn *conn, const char *message)
{
    err_t err;

    struct irc_line line = {
        NULL, "QUIT", { message }
    };
    
    // state check
    if (conn->quitting || conn->quit)
        return ERR_IRC_CONN_QUIT_STATE;
    
    // try and send
    if ((err = irc_conn_send(conn, &line)))
        return err;

    // mark as quitting
    conn->quitting = true;
    
    // ok
    return SUCCESS;
}