terom@57: #include "module.h" terom@57: #include "irc_chan.h" terom@57: #include "error.h" terom@23: #include "log.h" terom@23: terom@57: #include terom@55: #include terom@55: terom@57: #include terom@23: #include terom@23: terom@23: /** terom@68: * The irc_log module state. terom@23: */ terom@57: struct irc_log_ctx { terom@56: /** The nexus this module is loaded for */ terom@56: struct nexus *nexus; terom@56: terom@23: /** The database connection */ terom@23: struct evsql *db; terom@70: terom@70: /** terom@70: * Our logged channels terom@70: */ terom@70: TAILQ_HEAD(irc_log_ctx_channels, irc_log_chan) channels; terom@70: terom@70: /** Are we currently unloading ourself (via irc_log_unload) ? */ terom@70: bool unloading; terom@57: }; terom@23: terom@68: /** terom@68: * State required to use irc_cmd_handler with irc_chan.handlers. terom@68: */ terom@68: struct irc_log_chan { terom@68: /** The irc_log context */ terom@68: struct irc_log_ctx *ctx; terom@68: terom@68: /** The target channel */ terom@68: struct irc_chan *chan; terom@70: terom@70: /** Are we stopping (irc_log_chan_stop called)? */ terom@70: bool stopping; terom@70: terom@70: /** We are part of the irc_log_ctx.channels list */ terom@70: TAILQ_ENTRY(irc_log_chan) ctx_channels; terom@68: }; terom@68: terom@70: /* terom@70: * Forward-declare terom@70: */ terom@70: static void irc_log_chan_destroy (struct irc_log_chan *chan_ctx); terom@70: terom@70: /** terom@70: * The irc_log_ctx has completed stopping all the channels, and should now destroy itself terom@70: */ terom@70: static void irc_log_stopped (struct irc_log_ctx *ctx) terom@70: { terom@70: // schedule an evsql_destroy terom@70: if (evsql_destroy_next(ctx->db)) terom@70: log_fatal("evsql_destroy_next failed"); terom@70: terom@70: // free ourself terom@70: free(ctx); terom@70: } terom@70: terom@70: /** terom@70: * The irc_log_chan has completed shutdown terom@70: */ terom@70: static void irc_log_chan_stopped (struct irc_log_chan *chan_ctx) terom@70: { terom@70: struct irc_log_ctx *ctx = chan_ctx->ctx; terom@70: terom@70: // destroy the channel terom@70: irc_log_chan_destroy(chan_ctx); terom@70: terom@70: // was it the final channel for unloading? terom@70: if (ctx->unloading && TAILQ_EMPTY(&ctx->channels)) terom@70: irc_log_stopped(ctx); terom@70: } terom@70: terom@68: /** terom@68: * Our evsql result handler for irc_log_event INSERTs. terom@68: */ terom@67: static void irc_log_on_sql_result (struct evsql_result *res, void *arg) terom@67: { terom@70: struct irc_log_chan *chan_ctx = arg; terom@67: err_t err; terom@67: terom@70: // check errors terom@67: if ((err = evsql_result_check(res))) terom@67: log_error("irc_log_event: %s", evsql_result_error(res)); terom@67: terom@70: // ok, don't need the result anymore terom@67: evsql_result_free(res); terom@70: terom@70: // if stopping, then handle that terom@70: if (chan_ctx->stopping) terom@70: irc_log_chan_stopped(chan_ctx); terom@67: } terom@67: terom@68: /** terom@68: * Log the event into our database. terom@68: * terom@70: * Any of chan_ctx, source, target, message can be NULL to insert NULLs terom@68: */ terom@70: static err_t irc_log_event (struct irc_log_ctx *ctx, struct irc_log_chan *chan_ctx, const struct irc_nm *source, terom@67: const char *type, const char *target, const char *message) terom@67: { terom@67: struct evsql_query *query; terom@68: terom@68: // unpack the nick/user/hostname args, as source can be NULL terom@70: const char *network = chan_ctx ? chan_ctx->chan->net->info.network : NULL; terom@70: const char *channel = chan_ctx ? irc_chan_name(chan_ctx->chan) : NULL; terom@68: const char *nickname = source ? source->nickname : NULL; terom@68: const char *username = source ? source->username : NULL; terom@68: const char *hostname = source ? source->hostname : NULL; terom@67: terom@67: // our SQL query terom@67: static struct evsql_query_info sql = { terom@67: "INSERT INTO logs (" terom@67: " network, channel, nickname, username, hostname, type, target, message" terom@67: " ) VALUES (" terom@67: " $1::varchar, $2::varchar, $3::varchar, $4::varchar, $5::varchar, $6::varchar, $7::varchar, $8::varchar" terom@67: " )", { terom@67: EVSQL_TYPE(STRING), // network terom@67: EVSQL_TYPE(STRING), // channel terom@67: EVSQL_TYPE(STRING), // nickname terom@67: EVSQL_TYPE(STRING), // username terom@67: EVSQL_TYPE(STRING), // hostname terom@67: EVSQL_TYPE(STRING), // type terom@67: EVSQL_TYPE(STRING), // target terom@67: EVSQL_TYPE(STRING), // message terom@67: EVSQL_TYPE_END terom@67: } terom@67: }; terom@68: terom@68: // drop lines if not connected terom@68: if (ctx->db == NULL) { terom@68: log_warn("no database connected"); terom@68: terom@68: return ERR_MODULE_CONF; terom@68: } terom@67: terom@67: // run the INSERT terom@70: if ((query = evsql_query_exec(ctx->db, NULL, &sql, &irc_log_on_sql_result, chan_ctx, terom@70: network, channel, nickname, username, hostname, type, target, message terom@67: )) == NULL) { terom@67: // XXX: get evsql_query error info somehow... terom@67: log_error("evsql_query_exec failed for %s:%s - %s!%s@%s - %s -> %s - %s", terom@70: network, channel, nickname, username, hostname, type, target, message terom@67: ); terom@67: terom@68: return ERR_EVSQL_QUERY_EXEC; terom@67: } terom@68: terom@68: // ok terom@68: log_debug("%s:%s - %s!%s@%s - %s -> %s - %s", terom@70: network, channel, nickname, username, hostname, type, target, message terom@68: ); terom@68: terom@68: return SUCCESS; terom@67: } terom@67: terom@68: /** terom@68: * Parse the prefix into a nickmask and pass on to irc_log_event terom@68: */ terom@70: static err_t irc_log_event_prefix (struct irc_log_ctx *ctx, struct irc_log_chan *chan_ctx, const char *prefix, terom@68: const char *type, const char *target, const char *message) terom@23: { terom@68: char prefix_buf[IRC_PREFIX_MAX]; terom@68: struct irc_nm nm; terom@68: err_t err; terom@23: terom@68: // parse nickmask terom@68: if ((err = irc_nm_parse(&nm, prefix_buf, prefix))) terom@68: return err; terom@68: terom@68: // log terom@70: return irc_log_event(ctx, chan_ctx, &nm, type, target, message); terom@23: } terom@23: terom@68: /** terom@68: * Log a simple channel event of the form: terom@68: * terom@68: * :nm [] terom@68: */ terom@68: static void irc_log_on_chan_generic (const struct irc_line *line, void *arg) terom@68: { terom@68: struct irc_log_chan *chan_ctx = arg; terom@68: terom@68: const char *msg = line->args[1]; terom@68: terom@70: irc_log_event_prefix(chan_ctx->ctx, chan_ctx, line->prefix, line->command, NULL, msg); terom@68: } terom@68: terom@68: /** terom@68: * Log a MODE event on a channel terom@68: * terom@68: * :nm MODE [ [...]] terom@68: * terom@68: * This conacts all the modeargs together, and logs that as the message terom@68: */ terom@68: static void irc_log_on_chan_MODE (const struct irc_line *line, void *arg) terom@68: { terom@68: struct irc_log_chan *chan_ctx = arg; terom@68: char message[512], *ptr = message; terom@68: const char *cmdarg; terom@68: terom@68: // iterate over each arg terom@68: FOREACH_IRC_LINE_ARGS(line, cmdarg) { terom@68: // separate with spaces terom@68: if (cmdarg > line->args[0]) terom@68: *ptr++ = ' '; terom@68: terom@68: // append terom@68: ptr += snprintf(ptr, (message + 512 - ptr), "%s", cmdarg); terom@68: terom@68: // buffer full? terom@68: if (ptr > message + 512) terom@68: break; terom@68: } terom@68: terom@68: // log terom@70: irc_log_event_prefix(chan_ctx->ctx, chan_ctx, line->prefix, line->command, NULL, message); terom@68: } terom@68: terom@68: /** terom@68: * Log a KICK event on a channel terom@68: * terom@68: * :nm KICK [] terom@68: */ terom@68: static void irc_log_on_chan_KICK (const struct irc_line *line, void *arg) terom@68: { terom@68: struct irc_log_chan *chan_ctx = arg; terom@68: terom@68: const char *target = line->args[1]; terom@68: const char *msg = line->args[2]; terom@68: terom@70: irc_log_event_prefix(chan_ctx->ctx, chan_ctx, line->prefix, line->command, target, msg); terom@68: } terom@68: terom@68: /** terom@68: * Our low-level channel-specific message handlers for logged channels. terom@68: * terom@68: * Note that these get a `struct irc_log_chan*` as an argument. terom@68: */ terom@68: static struct irc_cmd_handler _chan_cmd_handlers[] = { terom@68: { "JOIN", &irc_log_on_chan_generic }, terom@68: { "PART", &irc_log_on_chan_generic }, terom@68: { "MODE", &irc_log_on_chan_MODE }, terom@68: { "TOPIC", &irc_log_on_chan_generic }, terom@68: { "KICK", &irc_log_on_chan_KICK }, terom@68: { "PRIVMSG", &irc_log_on_chan_generic }, terom@68: { "NOTICE", &irc_log_on_chan_generic }, terom@68: { NULL, NULL } terom@68: }; terom@68: terom@68: /** terom@68: * Our high-level channel-specific callbacks for logged channels. terom@68: * terom@68: * Note that these get a `struct irc_log_chan*` as an argument. terom@68: */ terom@38: static struct irc_chan_callbacks _chan_callbacks = { terom@68: .on_self_join = NULL, terom@68: .on_msg = NULL, terom@23: }; terom@23: terom@69: /** terom@69: * Release resources associated with the given irc_log_chan without doing any clean shutdown stuff terom@69: */ terom@69: static void irc_log_chan_destroy (struct irc_log_chan *chan_ctx) terom@69: { terom@69: // remove any handlers/callbacks terom@69: irc_cmd_remove(&chan_ctx->chan->handlers, _chan_cmd_handlers, chan_ctx); terom@69: irc_chan_remove_callbacks(chan_ctx->chan, &_chan_callbacks, chan_ctx); terom@69: terom@70: // remove from channels list terom@70: TAILQ_REMOVE(&chan_ctx->ctx->channels, chan_ctx, ctx_channels); terom@70: terom@69: // free ourselves terom@69: free(chan_ctx); terom@69: } terom@69: terom@69: /** terom@69: * Begin logging the given channel terom@69: */ terom@69: static err_t irc_log_chan (struct irc_log_ctx *ctx, struct irc_chan *chan, struct error_info *err) terom@69: { terom@69: struct irc_log_chan *chan_ctx; terom@69: terom@69: // alloc terom@69: if ((chan_ctx = calloc(1, sizeof(*chan_ctx))) == NULL) terom@69: return SET_ERROR(err, ERR_CALLOC); terom@69: terom@69: // store terom@69: chan_ctx->ctx = ctx; terom@69: chan_ctx->chan = chan; terom@69: terom@70: // add to channels list terom@70: TAILQ_INSERT_TAIL(&ctx->channels, chan_ctx, ctx_channels); terom@70: terom@69: // add low-level handlers terom@69: if ((ERROR_CODE(err) = irc_cmd_add(&chan_ctx->chan->handlers, _chan_cmd_handlers, chan_ctx))) terom@69: goto error; terom@69: terom@69: // add channel callbacks terom@69: if ((ERROR_CODE(err) = irc_chan_add_callbacks(chan_ctx->chan, &_chan_callbacks, chan_ctx))) terom@69: goto error; terom@69: terom@69: // log an OPEN message terom@69: // XXX: move this to when we first JOIN the channel terom@70: if ((ERROR_CODE(err) = irc_log_event(ctx, chan_ctx, NULL, "OPEN", NULL, NULL))) terom@69: goto error; terom@69: terom@69: // ok terom@69: log_info("logging channel %s:%s", chan_ctx->chan->net->info.network, irc_chan_name(chan_ctx->chan)); terom@69: terom@69: return SUCCESS; terom@69: terom@69: error: terom@69: // cleanup terom@69: irc_log_chan_destroy(chan_ctx); terom@69: terom@69: return ERROR_CODE(err); terom@69: } terom@69: terom@69: /** terom@70: * Stop logging the given channel, shutting it down nicely and then releasing its resources. terom@70: * terom@70: * If shutting it down nicely fails, this just destroys it. terom@70: */ terom@70: static err_t irc_log_chan_stop (struct irc_log_chan *chan_ctx) terom@70: { terom@70: err_t err; terom@70: terom@70: // mark it as stopping, so the result callback knows to destroy it terom@70: chan_ctx->stopping = true; terom@70: terom@70: // log the CLOSE event terom@70: if ((err = irc_log_event(chan_ctx->ctx, chan_ctx, NULL, "CLOSE", NULL, NULL))) terom@70: goto error; terom@70: terom@70: // ok terom@70: return SUCCESS; terom@70: terom@70: error: terom@70: // destroy it uncleanly terom@70: irc_log_chan_destroy(chan_ctx); terom@70: terom@70: return err; terom@70: } terom@70: terom@70: /** terom@69: * Allocate and initialize an irc_log_ctx. This doesn't do very much, the real magic happens in irc_log_conf. terom@69: */ terom@57: static err_t irc_log_init (struct nexus *nexus, void **ctx_ptr, struct error_info *err) terom@23: { terom@55: struct irc_log_ctx *ctx; terom@57: terom@57: // allocate terom@57: if ((ctx = calloc(1, sizeof(*ctx))) == NULL) terom@57: return SET_ERROR(err, ERR_CALLOC); terom@55: terom@56: // store terom@57: ctx->nexus = nexus; terom@56: terom@70: // initialize terom@70: TAILQ_INIT(&ctx->channels); terom@70: terom@65: log_info("module initialized"); terom@65: terom@55: // ok terom@57: *ctx_ptr = ctx; terom@57: terom@69: return SUCCESS; terom@55: } terom@55: terom@68: /** terom@68: * Process the irc_log.db_info config option. terom@68: * terom@68: * Creates a new evsql handle. terom@68: * terom@68: * Fails if ctx->db is already set. terom@68: */ terom@68: static err_t irc_log_conf_db_info (struct irc_log_ctx *ctx, char *value, struct error_info *err) terom@68: { terom@68: log_info("connect to database: %s", value); terom@68: terom@68: // already connected? terom@68: if (ctx->db) terom@68: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "irc_log.db_info already set"); terom@68: terom@68: // create a new evsql handle terom@68: if ((ctx->db = evsql_new_pq(ctx->nexus->ev_base, value, NULL, NULL)) == NULL) terom@68: return SET_ERROR(err, ERR_EVSQL_NEW_PQ); terom@68: terom@68: // ok terom@68: return SUCCESS; terom@68: } terom@68: terom@68: /** terom@68: * Process the irc_log.channel config option. terom@68: * terom@68: * Creates a new irc_log_chan context, looks up the channel, adds our command handlers, and logs an OPEN event. terom@68: * terom@68: * Fails if the value is invalid, we don't have a database connected, the channel doesn't exist, adding our command terom@68: * handlers/callbacks fails, or sending the initial INSERT-OPEN query fails. terom@68: */ terom@68: static err_t irc_log_conf_channel (struct irc_log_ctx *ctx, char *value, struct error_info *err) terom@68: { terom@68: const char *network, *channel; terom@69: struct irc_chan *chan; terom@68: terom@68: // parse required args terom@68: if ((network = strsep(&value, ":")) == NULL) terom@68: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "invalid for irc_log.channel"); terom@68: terom@68: if ((channel = strsep(&value, ":")) == NULL) terom@68: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "invalid for irc_log.channel"); terom@68: terom@68: // extraneous stuff? terom@68: if (value) terom@68: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "trailing data for irc_log.channel"); terom@68: terom@68: // have a db configured? terom@68: if (!ctx->db) terom@68: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "irc_log.channel used without any irc_log.db_info"); terom@68: terom@68: // get the channel? terom@69: if ((chan = irc_client_get_chan(ctx->nexus->client, network, channel)) == NULL) terom@69: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "unknown channel name"); terom@68: terom@69: // begin logging it terom@69: if (irc_log_chan(ctx, chan, err)) terom@69: return ERROR_CODE(err); terom@68: terom@68: // ok terom@68: return SUCCESS; terom@68: } terom@68: terom@68: /** terom@68: * Set a config option terom@68: */ terom@70: static err_t irc_log_conf (void *_ctx, const char *name, char *value, struct error_info *err) terom@55: { terom@70: struct irc_log_ctx *ctx = _ctx; terom@67: terom@70: // wrong state terom@70: if (ctx->unloading) terom@70: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "module is being unloaded"); terom@70: terom@70: // apply the config setting terom@55: if (strcmp(name, "db_info") == 0) { terom@68: return irc_log_conf_db_info(ctx, value, err); terom@55: terom@67: } else if (strcmp(name, "channel") == 0) { terom@68: return irc_log_conf_channel(ctx, value, err); terom@55: terom@55: } else { terom@66: RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "unknown configuration option"); terom@23: } terom@23: terom@23: // ok terom@23: return SUCCESS; terom@23: } terom@23: terom@57: /** terom@70: * Deinitialize, logging CLOSE events for all channels, and removing any hooks we've added terom@70: */ terom@70: static err_t irc_log_unload (void *_ctx) terom@70: { terom@70: struct irc_log_ctx *ctx = _ctx; terom@70: struct irc_log_chan *chan_ctx; terom@70: terom@70: // update our state to mark ourselves as unloading terom@70: ctx->unloading = true; terom@70: terom@70: // stop logging each channel terom@70: TAILQ_FOREACH(chan_ctx, &ctx->channels, ctx_channels) { terom@70: irc_log_chan_stop(chan_ctx); terom@70: } terom@70: terom@70: // wait for all the channels to be stopped, which will call irc_log_stopped terom@70: return SUCCESS; terom@70: } terom@70: terom@70: /** terom@57: * The module function table terom@57: */ terom@57: struct module_funcs irc_log_funcs = { terom@57: .init = &irc_log_init, terom@57: .conf = &irc_log_conf, terom@70: .unload = &irc_log_unload, terom@57: }; terom@68: