src/irc_log.c
author Tero Marttila <terom@fixme.fi>
Thu, 02 Apr 2009 02:25:35 +0300
changeset 110 43e9a7984955
parent 109 bfe9b9a8fe5b
child 112 10ada0d1c7d7
permissions -rw-r--r--
fix operation of module_unload/module_destroy so that unloaded modules are now destroyed once they have been unloaded
#include "module.h"
#include "irc_chan.h"
#include "config.h"
#include "error.h"
#include "log.h"

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

#include <event2/event.h>
#include <evsql.h>

/**
 * The irc_log module state.
 */
struct irc_log_ctx {
    /** The nexus this module is loaded for */
    struct nexus *nexus;

    /** The database connection */
    struct evsql *db;

    /**
     * Our logged channels
     */
    TAILQ_HEAD(irc_log_ctx_channels, irc_log_chan) channels;
    
    /** Are we currently unloading ourself (via irc_log_unload) ? */
    bool unloading;

    /** The `struct module` passed to us once we unload() */
    struct module *module;
};

/**
 * State required to use irc_cmd_handler with irc_chan.handlers.
 */
struct irc_log_chan {
    /** The irc_log context */
    struct irc_log_ctx *ctx;

    /** The target channel */
    struct irc_chan *chan;

    /** Are we stopping (irc_log_chan_stop called)? */
    bool stopping;

    /** We are part of the irc_log_ctx.channels list */
    TAILQ_ENTRY(irc_log_chan) ctx_channels;
};

/*
 * Forward-declare
 */
static void irc_log_chan_destroy (struct irc_log_chan *chan_ctx);

/**
 * The irc_log_ctx has completed stopping all the channels, and should now destroy itself
 */
static void irc_log_stopped (struct irc_log_ctx *ctx)
{
    log_debug("module unload() completed");
    
    // notify
    module_unloaded(ctx->module);
}

/**
 * The irc_log_chan has completed shutdown
 */
static void irc_log_chan_stopped (struct irc_log_chan *chan_ctx)
{
    struct irc_log_ctx *ctx = chan_ctx->ctx;

    log_debug("destroying the irc_log_chan (chan=%s)", irc_chan_name(chan_ctx->chan));

    // destroy the channel
    irc_log_chan_destroy(chan_ctx);

    // was it the final channel for unloading?
    if (ctx->unloading && TAILQ_EMPTY(&ctx->channels))
        irc_log_stopped(ctx);
}

/**
 * Our evsql result handler for irc_log_event INSERTs.
 */
static void irc_log_on_sql_result (struct evsql_result *res, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;
    err_t err;

    // check errors
    if ((err = evsql_result_check(res)))
        log_error("irc_log_event: %s", evsql_result_error(res));

    // ok, don't need the result anymore
    evsql_result_free(res);

    // if stopping, then handle that
    if (chan_ctx->stopping)
        irc_log_chan_stopped(chan_ctx);
}

/**
 * Log the event into our database.
 *
 * Any of chan_ctx, source, target, message can be NULL to insert NULLs
 */
static err_t irc_log_event (struct irc_log_ctx *ctx, struct irc_log_chan *chan_ctx, const struct irc_nm *source,
        const char *type, const char *target, const char *message)
{
    struct evsql_query *query;
    
    // unpack the nick/user/hostname args, as source can be NULL
    const char *network = chan_ctx ? chan_ctx->chan->net->info.network : NULL;
    const char *channel = chan_ctx ? irc_chan_name(chan_ctx->chan) : NULL;
    const char *nickname = source ? source->nickname : NULL;
    const char *username = source ? source->username : NULL;
    const char *hostname = source ? source->hostname : NULL;

    // our SQL query
    static struct evsql_query_info sql = {
        "INSERT INTO logs ("
        "  network, channel, nickname, username, hostname, type, target, message"
        " ) VALUES ("
        "  $1::varchar, $2::varchar, $3::varchar, $4::varchar, $5::varchar, $6::varchar, $7::varchar, $8::varchar"
        " )", { 
            EVSQL_TYPE(STRING),     // network
            EVSQL_TYPE(STRING),     // channel
            EVSQL_TYPE(STRING),     // nickname
            EVSQL_TYPE(STRING),     // username
            EVSQL_TYPE(STRING),     // hostname
            EVSQL_TYPE(STRING),     // type
            EVSQL_TYPE(STRING),     // target
            EVSQL_TYPE(STRING),     // message
            EVSQL_TYPE_END
        }
    };
    
    // drop lines if not connected
    if (ctx->db == NULL) {
        log_warn("no database connected");

        return ERR_MODULE_CONF;
    }

    // run the INSERT
    if ((query = evsql_query_exec(ctx->db, NULL, &sql, &irc_log_on_sql_result, chan_ctx,
        network, channel, nickname, username, hostname, type, target, message
    )) == NULL) {
        // XXX: get evsql_query error info somehow...
        log_error("evsql_query_exec failed for %s:%s - %s!%s@%s - %s -> %s - %s", 
            network, channel, nickname, username, hostname, type, target, message
        );

        return ERR_EVSQL_QUERY_EXEC;
    }

    // ok
    log_debug("%s:%s - %s!%s@%s - %s -> %s - %s", 
        network, channel, nickname, username, hostname, type, target, message
    );
    
    return SUCCESS;
}

/**
 * Log a simple channel event of the form:
 *
 * :nm <type> <channel> [<msg>]
 */
static void irc_log_on_chan_generic (const struct irc_line *line, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;
    
    const char *msg = line->args[1];

    irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, NULL, msg);
}

/**
 * Log a NICK event on a channel
 *
 * :nm NICK <nickname>
 *
 * This logs the new nickname as the target
 */
static void irc_log_on_chan_NICK (const struct irc_line *line, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;

    const char *nickname = line->args[0];

    // log it
    irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, nickname, NULL);
}

/**
 * Log a QUIT event on a channel
 *
 * :nm QUIT [<message>]
 */
static void irc_log_on_chan_QUIT (const struct irc_line *line, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;

    const char *message = line->args[0];

    // log it
    irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, NULL, message);
}

/**
 * Log a MODE event on a channel
 *
 * :nm MODE <channel> [<modearg> [...]]
 *
 * This conacts all the modeargs together, and logs that as the message
 */
static void irc_log_on_chan_MODE (const struct irc_line *line, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;
    char message[512], *ptr = message;
    const char *cmdarg;
    
    // iterate over each arg
    FOREACH_IRC_LINE_ARGS(line, cmdarg) {
        // separate with spaces
        if (cmdarg > line->args[0])
            *ptr++ = ' ';
        
        // append
        ptr += snprintf(ptr, (message + 512 - ptr), "%s", cmdarg);

        // buffer full?
        if (ptr > message + 512)
            break;
    }
    
    // log
    irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, NULL, message);
}

/**
 * Log a KICK event on a channel
 *
 * :nm KICK <channel> <target> [<comment>]
 */
static void irc_log_on_chan_KICK (const struct irc_line *line, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;
    
    const char *target = line->args[1];    
    const char *msg = line->args[2];

    irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, target, msg);
}

/**
 * Log a CTCP ACTION message on a channel
 *
 * :nm "CTCP ACTION" <channel> <action>
 */
static void irc_log_on_chan_CTCP_ACTION (const struct irc_line *line, void *arg)
{
    struct irc_log_chan *chan_ctx = arg;

    const char *action = line->args[1];

    irc_log_event(chan_ctx->ctx, chan_ctx, line->source, "ACTION", NULL, action);
}

/**
 * Our low-level channel-specific message handlers for logged channels.
 *
 * Note that these get a `struct irc_log_chan*` as an argument.
 */
static struct irc_cmd_handler _chan_cmd_handlers[] = {
    {   "NICK",         &irc_log_on_chan_NICK           },
    {   "QUIT",         &irc_log_on_chan_QUIT           },
    {   "JOIN",         &irc_log_on_chan_generic        },
    {   "PART",         &irc_log_on_chan_generic        },
    {   "MODE",         &irc_log_on_chan_MODE           },
    {   "TOPIC",        &irc_log_on_chan_generic        },
    {   "KICK",         &irc_log_on_chan_KICK           },
    {   "PRIVMSG",      &irc_log_on_chan_generic        },
    {   "NOTICE",       &irc_log_on_chan_generic        },
    {   "CTCP ACTION",  &irc_log_on_chan_CTCP_ACTION    },
    {   NULL,           NULL                            }
};

/**
 * Our high-level channel-specific callbacks for logged channels.
 *
 * Note that these get a `struct irc_log_chan*` as an argument.
 */
static struct irc_chan_callbacks _chan_callbacks = {
    .on_self_join       = NULL,
    .on_msg             = NULL,
};

/**
 * Release resources associated with the given irc_log_chan without doing any clean shutdown stuff
 */
static void irc_log_chan_destroy (struct irc_log_chan *chan_ctx)
{
    // remove any handlers/callbacks
    irc_cmd_remove(&chan_ctx->chan->handlers, _chan_cmd_handlers, chan_ctx);
    irc_chan_remove_callbacks(chan_ctx->chan, &_chan_callbacks, chan_ctx);

    // remove from channels list
    TAILQ_REMOVE(&chan_ctx->ctx->channels, chan_ctx, ctx_channels);

    // free ourselves
    free(chan_ctx);
}

/**
 * Begin logging the given channel
 */
static err_t irc_log_chan (struct irc_log_ctx *ctx, struct irc_chan *chan, struct error_info *err)
{
    struct irc_log_chan *chan_ctx;

    // alloc
    if ((chan_ctx = calloc(1, sizeof(*chan_ctx))) == NULL)
        return SET_ERROR(err, ERR_CALLOC);

    // store
    chan_ctx->ctx = ctx;
    chan_ctx->chan = chan;

    // add to channels list
    TAILQ_INSERT_TAIL(&ctx->channels, chan_ctx, ctx_channels);

    // add low-level handlers
    if ((ERROR_CODE(err) = irc_cmd_add(&chan_ctx->chan->handlers, _chan_cmd_handlers, chan_ctx)))
        goto error;

    // add channel callbacks
    if ((ERROR_CODE(err) = irc_chan_add_callbacks(chan_ctx->chan, &_chan_callbacks, chan_ctx)))
        goto error;
    
    // log an OPEN message
    // XXX: move this to when we first JOIN the channel
    if ((ERROR_CODE(err) = irc_log_event(ctx, chan_ctx, NULL, "OPEN", NULL, NULL)))
        goto error;

    // ok
    log_info("logging channel %s:%s", chan_ctx->chan->net->info.network, irc_chan_name(chan_ctx->chan));

    return SUCCESS;

error:
    // cleanup
    irc_log_chan_destroy(chan_ctx);
    
    return ERROR_CODE(err);
}

/**
 * Stop logging the given channel, shutting it down nicely and then releasing its resources.
 *
 * If shutting it down nicely fails, this just destroys it.
 */
static err_t irc_log_chan_stop (struct irc_log_chan *chan_ctx)
{
    err_t err;

    // mark it as stopping, so the result callback knows to destroy it
    chan_ctx->stopping = true;

    // log the CLOSE event
    if ((err = irc_log_event(chan_ctx->ctx, chan_ctx, NULL, "CLOSE", NULL, NULL)))
        goto error;

    // ok
    return SUCCESS;

error:
    // destroy it uncleanly
    irc_log_chan_destroy(chan_ctx);

    return err;
}

/**
 * Allocate and initialize an irc_log_ctx. This doesn't do very much, the real magic happens in irc_log_conf.
 */
static err_t irc_log_init (struct nexus *nexus, void **ctx_ptr, struct error_info *err)
{
    struct irc_log_ctx *ctx;
    
    // allocate
    if ((ctx = calloc(1, sizeof(*ctx))) == NULL)
        return SET_ERROR(err, ERR_CALLOC);

    // store
    ctx->nexus = nexus;

    // initialize
    TAILQ_INIT(&ctx->channels);

    log_info("module initialized");

    // ok
    *ctx_ptr = ctx;

    return SUCCESS;
}

/**
 * Process the irc_log.db_info config option.
 *
 * Creates a new evsql handle.
 *
 * Fails if ctx->db is already set.
 */
static err_t irc_log_conf_db_info (void *_ctx, char *value, struct error_info *err)
{
    struct irc_log_ctx *ctx = _ctx;

    log_info("connect to database: %s", value);

    // already connected?
    if (ctx->db)
        RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "irc_log.db_info already set");
    
    // create a new evsql handle
    if ((ctx->db = evsql_new_pq(ctx->nexus->ev_base, value, NULL, NULL)) == NULL)
       return SET_ERROR(err, ERR_EVSQL_NEW_PQ);
    
    // ok
    return SUCCESS;
}

/**
 * Process the irc_log.channel config option.
 *
 * Creates a new irc_log_chan context, looks up the channel, adds our command handlers, and logs an OPEN event.
 *
 * Fails if the value is invalid, we don't have a database connected, the channel doesn't exist, adding our command
 * handlers/callbacks fails, or sending the initial INSERT-OPEN query fails.
 */
static err_t irc_log_conf_channel (void *_ctx, struct irc_chan *chan, struct error_info *err)
{
    struct irc_log_ctx *ctx = _ctx;
    
    // have a db configured?
    if (!ctx->db)
        RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "irc_log.channel used without any irc_log.db_info");

    // begin logging it
    if (irc_log_chan(ctx, chan, err))
        return ERROR_CODE(err);
    
    // ok
    return SUCCESS;
}

/**
 * Our configuration options
 */
struct config_option irc_log_config_options[] = {
    CONFIG_OPT_STRING(      "db_info",  &irc_log_conf_db_info,  "[<key>=<value> [...]]",    "set database connection info, see libpq docs"  ),
    CONFIG_OPT_IRC_CHAN(    "channel",  &irc_log_conf_channel,  "<channel>",                "log the given channel"                         ),

    CONFIG_OPT_END
};

/**
 * Deinitialize, logging CLOSE events for all channels, and removing any hooks we've added
 */
static err_t irc_log_unload (void *_ctx, struct module *module)
{
    struct irc_log_ctx *ctx = _ctx;
    struct irc_log_chan *chan_ctx;

    // update our state to mark ourselves as unloading
    ctx->unloading = true;
    ctx->module = module;

    // stop logging each channel
    TAILQ_FOREACH(chan_ctx, &ctx->channels, ctx_channels) {
        irc_log_chan_stop(chan_ctx);
    }
    
    // wait for all the channels to be stopped, which will call irc_log_stopped
    return SUCCESS;
}

/**
 * We can safely destroy the evsql instance from here, since we're outside of any callbacks from this module
 */
static void irc_log_destroy (void *_ctx)
{
    struct irc_log_ctx *ctx = _ctx;

    log_debug("destroying the irc_log_ctx");

    // destroy the evsql instance
    evsql_destroy(ctx->db);

    // ...no more
    free(ctx);
}

/**
 * The module function table
 */
struct module_desc irc_log_module = {
    .init               = &irc_log_init,
    .config_options     = irc_log_config_options,
    .unload             = &irc_log_unload,
    .destroy            = &irc_log_destroy
};