src/irc_net.c
author Tero Marttila <terom@fixme.fi>
Thu, 23 Apr 2009 17:17:33 +0300
changeset 147 fd97eb3c183a
parent 140 aa390e52eda8
child 150 e8018446b336
permissions -rw-r--r--
fix bug with irc_net_destroy/users
#include "irc_net.h"
#include "log.h"

#include <stdlib.h>
#include <string.h>
#include <assert.h>

const char* irc_net_name (struct irc_net *net)
{
    return net ? net->info.network : NULL;
}

/**
 * Something happaned which caused our irc_conn to fail. Destroy it, and recover. XXX: somehow
 */
static void irc_net_error (struct irc_net *net, struct error_info *err)
{
    struct irc_chan *chan = NULL;

    // log an error
    log_err_info(err, "irc_conn failed");

    // destroy connection and set NULL
    irc_conn_destroy(net->conn);
    net->conn = NULL;

    // update channel state
    TAILQ_FOREACH(chan, &net->channels, net_channels) {
        // XXX: notify channel somehow
    }

    // XXX: reconnect?
}

/**
 * Do a lookup for an irc_user with the given nickname, and return it it found, else NULL.
 *
 * This does not refcount anything.
 */
static struct irc_user* irc_net_lookup_user (struct irc_net *net, const char *nickname)
{
    struct irc_user *user = NULL;

    // look for it...
    LIST_FOREACH(user, &net->users, net_users) {
        if (irc_cmp_nick(nickname, user->nickname) == 0)
            // found existing
            return user;
    }
    
    // not found
    return NULL;
}

/**
 * Our irc_conn has registered. Join our channels
 */
static void irc_net_conn_registered (struct irc_conn *conn, void *arg)
{
    struct irc_net *net = arg;
    struct irc_chan *chan = NULL;
    struct error_info err;

    (void) conn;

    // join our channels
    TAILQ_FOREACH(chan, &net->channels, net_channels) {
        if ((ERROR_CODE(&err) = irc_chan_join(chan)))
            // XXX: this should be some kind of irc_chan_error instead
            irc_net_error(net, &err);
    }
}

/**
 * Our irc_conn has spontaneously failed, destroy it
 */
static void irc_net_conn_error (struct irc_conn *conn, struct error_info *err, void *arg)
{
    struct irc_net *net = arg;

    (void) conn;
    
    irc_net_error(net, err);
}

/**
 * Our irc_conn has quit succesfully
 */
static void irc_net_conn_quit (struct irc_conn *conn, void *arg)
{
    struct irc_net *net = arg;

    // clean up the conn
    irc_conn_destroy(conn);
    net->conn = NULL;

    // XXX: notify user
}

/**
 * Our irc_conn_callbacks list
 */
struct irc_conn_callbacks _conn_callbacks = {
    .on_registered  = &irc_net_conn_registered,
    .on_error       = &irc_net_conn_error,
    .on_quit        = &irc_net_conn_quit,
};

/**
 * Propagate the command to the appropriate irc_chan based on the given name
 */
static void irc_net_propagate_chan (struct irc_net *net, const struct irc_line *line, const char *channel)
{
    struct irc_chan *chan;

    // look up channel
    if ((chan = irc_net_get_chan(net, channel)) == NULL) {
        log_warn("unknown channel: %s", channel);
        return;
    }

    // propagate
    irc_cmd_invoke(&chan->handlers, line);
}

/**
 * Propagate line to irc_chan based on args[0]
 */ 
static void irc_net_on_chan0 (const struct irc_line *line, void *arg)
{
    struct irc_net *net = arg;
    
    irc_net_propagate_chan(net, line, line->args[0]);
}

/**
 * Propagate line to irc_chan based on args[1]
 */ 
static void irc_net_on_chan1 (const struct irc_line *line, void *arg)
{
    struct irc_net *net = arg;
    
    irc_net_propagate_chan(net, line, line->args[1]);
}

/**
 * Propagate line to irc_chan based on args[2]
 */ 
static void irc_net_on_chan2 (const struct irc_line *line, void *arg)
{
    struct irc_net *net = arg;
    
    irc_net_propagate_chan(net, line, line->args[2]);
}

/**
 * Propagate line to irc_chans based on the source nickname and chan_users.
 */
static void irc_net_on_chanuser (const struct irc_line *line, void *arg)
{
    struct irc_net *net = arg;
    struct irc_chan *chan;
    
    // scan each channel
    TAILQ_FOREACH(chan, &net->channels, net_channels) {
        if (irc_chan_get_user(chan, line->source->nickname)) {
            // propagate
            irc_cmd_invoke(&chan->handlers, line);
        }
    }
}

/**
 * :nm NICK <nickname>
 *
 * Look for a relevant irc_user, and rename them.
 *
 * Ignores if the irc_user could not be found, then propagates to the irc_chan's that the user is on, and then renames
 * the irc_user.
 */
static void irc_net_on_NICK (const struct irc_line *line, void *arg)
{
    struct irc_net *net = arg;
    struct irc_user *user;
    err_t err;

    // ignore for unknown
    if ((user = irc_net_lookup_user(net, line->source->nickname)) == NULL)
        return;

    // propagate to channels
    irc_net_on_chanuser(line, arg);

    // rename them
    if ((err = irc_user_rename(user, line->args[0])))
        // XXX: what to do?
        log_err(err, "irc_user_rename failed");
}

/**
 * :nm (PRIVMSG|NOTICE) <target> <message>
 *
 * Either propagate to channel if found, otherwise handle as a privmsg
 */
static void irc_net_on_msg (const struct irc_line *line, void *arg)
{
    struct irc_net *net = arg;

    // XXX: does two lookups
    if (irc_net_get_chan(net, line->args[0])) {
        // short-circuit to on_chan0
        irc_net_on_chan0(line, arg);
    
    } else {
        // XXX: callbacks for privmsgs
        
    }
}

/**
 * Our irc_cmd handler list
 */
static struct irc_cmd_handler _cmd_handlers[] = {
    // propagate certain messages to the appropriate channel
    {   "QUIT",             &irc_net_on_chanuser    },
    {   "JOIN",             &irc_net_on_chan0       },
    {   "PART",             &irc_net_on_chan0       },
    {   "MODE",             &irc_net_on_chan0       },
    {   "TOPIC",            &irc_net_on_chan0       },
    {   "KICK",             &irc_net_on_chan0       },
    {   IRC_RPL_NAMREPLY,   &irc_net_on_chan2       },
    {   IRC_RPL_ENDOFNAMES, &irc_net_on_chan1       },
    {   "CTCP ACTION",      &irc_net_on_chan0       },
    
    // special-case handling for others
    {   "NICK",             &irc_net_on_NICK        },
    // Note: no need to handle QUIT, as the channels should take care of irc_net_put_user'ing it away.
    {   "PRIVMSG",          &irc_net_on_msg         },
    {   "NOTICE",           &irc_net_on_msg         },
    
    {   NULL,               NULL                    }
};

/**
 * The given socket is now connected, so create the irc_conn and bind it in to us
 */
static err_t irc_net_connected (struct irc_net *net, struct sock_stream *sock, struct error_info *err)
{
    // create the irc connection state
    if (irc_conn_create(&net->conn, sock, &_conn_callbacks, net, err))
        return ERROR_CODE(err);

    // add our command handlers
    if ((ERROR_CODE(err) = irc_conn_add_cmd_handlers (net->conn, _cmd_handlers, net)))
        return ERROR_CODE(err);

    // register
    if ((ERROR_CODE(err) = irc_conn_register(net->conn, &net->info.register_info)))
        return ERROR_CODE(err);

    // ok
    return SUCCESS;
}

static void irc_net_on_connect (struct sock_stream *sock, struct error_info *conn_err, void *arg)
{
    struct irc_net *net = arg;
    struct error_info err;

    // XXX: better error handling
    
    // trap errors
    if (conn_err)
        FATAL_ERROR(conn_err, "async connect failed");
    
    // ok, yay
    if (irc_net_connected(net, sock, &err))
        FATAL_ERROR(&err, "irc_net_connected failed");

    // XXX: cleanup
}

err_t irc_net_create (struct irc_net **net_ptr, const struct irc_net_info *info, struct error_info *err)
{
    struct irc_net *net;
    struct sock_stream *sock = NULL;
    
    // allocate
    if ((net = calloc(1, sizeof(*net))) == NULL)
        return SET_ERROR(err, ERR_CALLOC);

    // initialize
    // XXX: info shouldn't be copied directly
    net->info = *info;
    TAILQ_INIT(&net->channels);
    LIST_INIT(&net->users);

    if (info->raw_sock) {
        log_debug("connected using raw socket: %p", info->raw_sock);

        // direct sock_stream connection
        sock = info->raw_sock;

        // then create the conn right away
        if (irc_net_connected(net, sock, err))
            goto error;

    } else if (info->ssl_cred) {
        // aquire a ref
        // NOTE: before any error handling
        sock_ssl_client_cred_get(net->info.ssl_cred);
        
        log_debug("connecting to [%s]:%s using SSL", info->hostname, info->service);

        // connect
        if (sock_ssl_connect_async(&sock, info->hostname, info->service, net->info.ssl_cred, &irc_net_on_connect, net, err))
            goto error;

    } else {
        log_debug("connecting to [%s]:%s", info->hostname, info->service);
            
        // begin async connect
        if (sock_tcp_connect_async(&sock, info->hostname, info->service, &irc_net_on_connect, net, err))
            goto error;
        
    }

    // ok
    *net_ptr = net;

    return SUCCESS;

error:
    if (sock && !net->conn)
        // we need to clean up the socket ourself
        sock_stream_release(sock);
    
    // cleanup main
    irc_net_destroy(net);

    return ERROR_CODE(err);
}

void irc_net_destroy (struct irc_net *net)
{
    struct irc_chan *chan_next = TAILQ_FIRST(&net->channels), *chan;
    struct irc_user *user_next, *user;

    // our conn
    if (net->conn)
        irc_conn_destroy(net->conn);

    // our channels
    while ((chan = chan_next)) {
        chan_next = TAILQ_NEXT(chan, net_channels);

        irc_chan_destroy(chan);
    }

    // our users
    // XXX: this disregards external refs
    for (user_next = LIST_FIRST(&net->users); (user = user_next); ) {
        user_next = LIST_NEXT(user, net_users);

        irc_user_destroy(user);
    }

    // our info
    if (net->info.ssl_cred)
        sock_ssl_client_cred_put(net->info.ssl_cred);

    // ourselves
    free(net);
}

err_t irc_net_add_chan (struct irc_net *net, struct irc_chan **chan_ptr, const struct irc_chan_info *info, struct error_info *err)
{
    struct irc_chan *chan;

    // create the new irc_chan struct
    if (irc_chan_create(&chan, net, info, err))
        return ERROR_CODE(err);
    
    // add to network list
    TAILQ_INSERT_TAIL(&net->channels, chan, net_channels);
    
    // currently connected?
    if (net->conn && net->conn->registered) {
        // then join
        if ((ERROR_CODE(err) = irc_chan_join(chan)))
            return ERROR_CODE(err);
    }

    // ok
    if (chan_ptr)
        *chan_ptr = chan;

    return SUCCESS;
}

struct irc_chan* irc_net_get_chan (struct irc_net *net, const char *channel)
{
    struct irc_chan *chan = NULL;

    // look for it...
    TAILQ_FOREACH(chan, &net->channels, net_channels) {
        if (strcasecmp(chan->info.channel, channel) == 0)
            // found it
            return chan;
    }

    // no such channel
    return NULL;
}

err_t irc_net_get_user (struct irc_net *net, struct irc_user **user_ptr, const char *nickname)
{
    struct irc_user *user;
    err_t err;
    
    // lookup
    if ((user = irc_net_lookup_user(net, nickname)) == NULL) {
        // create a new user struct
        if ((err = irc_user_create(&user, net, nickname)))
            return err;

        // add to our list
        LIST_INSERT_HEAD(&net->users, user, net_users);

    } else {
        // check for refcount overflow
        assert(user->refcount < SIZE_MAX);
    }

    // 'get' it
    user->refcount++;

    // ok
    *user_ptr = user;

    return SUCCESS;
}

void irc_net_put_user (struct irc_net *net, struct irc_user *user)
{
    (void) net;

    assert(user->refcount > 0);

    // nothing if it's still in use elsewhere
    if (--user->refcount)
        return;

    // remove from list
    LIST_REMOVE(user, net_users);

    // destroy
    irc_user_destroy(user);
}

err_t irc_net_quit (struct irc_net *net, const char *message)
{
   if (!net->conn)
       return ERR_IRC_NET_STATE;
    
    // send the QUIT message, and then we can wait for the reply
    return irc_conn_QUIT(net->conn, message);
}