view lib/irccd/irccd.c @ 975:5ffc8350e84b

irccdctl: add support for rule editing
author David Demelier <markand@malikania.fr>
date Tue, 09 Feb 2021 13:00:32 +0100
parents f365e5be1261
children 2da5064d0cff
line wrap: on
line source

/*
 * irccd.c -- main irccd object
 *
 * Copyright (c) 2013-2021 David Demelier <markand@malikania.fr>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "event.h"
#include "irccd.h"
#include "log.h"
#include "plugin.h"
#include "rule.h"
#include "server.h"
#include "util.h"

struct defer {
	void (*exec)(void *);
	void *data;
};

struct irc irc = {
	.servers = LIST_HEAD_INITIALIZER(),
	.plugins = LIST_HEAD_INITIALIZER(),
	.rules = TAILQ_HEAD_INITIALIZER(irc.rules)
};

static int pipes[2];

static int
is_command(const struct irc_plugin *p, const struct irc_event *ev)
{
	const char *cc;
	size_t ccsz;

	if (ev->type != IRC_EVENT_MESSAGE)
		return 0;

	/* Get the command prefix (e.g !)*/
	cc = ev->server->commandchar;
	ccsz = strlen(cc);

	return strncmp(ev->message.message, cc, ccsz) == 0 &&
	       strncmp(ev->message.message + ccsz, p->name, strlen(p->name)) == 0;
}

static struct irc_event *
to_command(const struct irc_plugin *p, struct irc_event *ev)
{
	char *action;

	/* Convert "!test foo bar" to "foo bar" */
	action = ev->message.message + strlen(ev->server->commandchar) + strlen(p->name);

	while (*action && isspace(*action))
		++action;

	action = strdup(action);
	free(ev->message.message);

	ev->type = IRC_EVENT_COMMAND;
	ev->message.message = action;

	return ev;
}

static int
invokable(const struct irc_plugin *p, const struct irc_event *ev)
{
	switch (ev->type) {
	case IRC_EVENT_COMMAND:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->message.channel, ev->message.origin, p->name, "onCommand");
	case IRC_EVENT_CONNECT:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    NULL, NULL, p->name, "onConnect");
	case IRC_EVENT_DISCONNECT:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    NULL, NULL, p->name, "onDisconnect");
	case IRC_EVENT_INVITE:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->invite.channel, ev->invite.origin, p->name, "onInvite");
	case IRC_EVENT_JOIN:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->join.channel, ev->join.origin, p->name, "onJoin");
	case IRC_EVENT_KICK:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->kick.channel, ev->kick.origin, p->name, "onKick");
		break;
	case IRC_EVENT_ME:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->message.channel, ev->message.origin, p->name, "onMe");
	case IRC_EVENT_MESSAGE:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->message.channel, ev->message.origin, p->name, "onMessage");
	case IRC_EVENT_MODE:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->mode.channel, ev->mode.origin, p->name, "onMode");
	case IRC_EVENT_NAMES:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->names.channel, NULL, p->name, "onNames");
	case IRC_EVENT_NICK:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    NULL, ev->nick.origin, p->name, "onNick");
	case IRC_EVENT_NOTICE:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->notice.channel, ev->notice.origin, p->name, "onNotice");
	case IRC_EVENT_PART:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->part.channel, ev->part.origin, p->name, "onPart");
	case IRC_EVENT_TOPIC:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    ev->topic.channel, ev->topic.origin, p->name, "onTopic");
	case IRC_EVENT_WHOIS:
		return irc_rule_matchlist(&irc.rules, ev->server->name,
		    NULL, NULL, p->name, "onWhois");
	default:
		return 1;
	}
}

static void
invoke(struct irc_event *ev)
{
	struct irc_plugin *p, *tmp, *plgcmd = NULL;

	/*
	 * Invoke for every plugin the event verbatim. Then, the event may match
	 * a plugin name command in that case we need to modify the event but
	 * only one plugin can match by its identifier. For example, the
	 * following plugins are loaded:
	 *
	 * - ask
	 * - hangman
	 * - logger
	 *
	 * If the message is "!ask will I be reach?" then it will invoke
	 * onMessage for hangman and logger but onCommand for ask. As such call
	 * hangman and logger first and modify event before ask.
	 */
	LIST_FOREACH_SAFE(p, &irc.plugins, link, tmp) {
		if (is_command(p, ev))
			plgcmd = p;
		else if (invokable(p, ev))
			irc_plugin_handle(p, ev);
	}

	if (plgcmd && invokable(plgcmd, ev))
		irc_plugin_handle(plgcmd, to_command(plgcmd, ev));
}

static void
pipe_flush(const struct pollfd *fd)
{
	struct defer df = {0};

	if (fd->fd != pipes[0] || !(fd->revents & POLLIN))
		return;

	if (read(fd->fd, &df, sizeof (df)) != sizeof (df))
		err(1, "read");

	df.exec(df.data);
}

static struct irc_plugin *
find_plugin(struct irc_plugin_loader *ldr, const char *base, const char *name)
{
	char path[PATH_MAX], buf[IRC_EXTENSIONS_LEN], *t, *ext;
	struct irc_plugin *p;

	strlcpy(buf, ldr->extensions, sizeof (buf));

	for (t = buf; (ext = strtok_r(t, ":", &t)); ) {
		snprintf(path, sizeof (path), "%s/%s.%s", base, name, ext);
		irc_log_info("irccd: trying %s", path);

		if ((p = irc_plugin_loader_open(ldr, path)))
			return p;
	}

	return NULL;
}

static inline struct irc_plugin *
open_plugin(struct irc_plugin_loader *ldr, const char *path)
{
	return irc_plugin_loader_open(ldr, path);
}

void
irc_bot_init(void)
{
	irc_log_to_console();

	if (pipe(pipes) < 0)
		err(1, "pipe");
}

void
irc_bot_server_add(struct irc_server *s)
{
	assert(s);

	irc_log_info("irccd: added new server: %s", s->name);

	irc_server_incref(s);
	irc_server_connect(s);

	LIST_INSERT_HEAD(&irc.servers, s, link);
}

struct irc_server *
irc_bot_server_get(const char *name)
{
	struct irc_server *s;

	LIST_FOREACH(s, &irc.servers, link)
		if (strcmp(s->name, name) == 0)
			return s;

	return NULL;
}

void
irc_bot_server_remove(const char *name)
{
	struct irc_server *s;

	if (!(s = irc_bot_server_get(name)))
		return;

	irc_server_disconnect(s);

	/* Don't forget to notify plugins. */
	invoke(&(struct irc_event) {
		.type = IRC_EVENT_DISCONNECT,
		.server = s
	});

	LIST_REMOVE(s, link);
	irc_server_decref(s);
}

void
irc_bot_server_clear(void)
{
	struct irc_server *s, *tmp;

	LIST_FOREACH_SAFE(s, &irc.servers, link, tmp)
		irc_bot_server_remove(s->name);
	LIST_INIT(&irc.servers);
}

void
irc_bot_plugin_add(struct irc_plugin *p)
{
	assert(p);

	LIST_INSERT_HEAD(&irc.plugins, p, link);

	irc_log_info("irccd: add new plugin: %s", p->name, p->description);
	irc_log_info("irccd: %s: version %s, from %s (%s license)", p->name,
	    p->version, p->author, p->license);

	irc_plugin_load(p);
}

struct irc_plugin *
irc_bot_plugin_find(const char *name, const char *path)
{
	assert(name);

	char buf[IRC_PATHS_LEN], pathbuf[PATH_MAX], *t, *token;
	struct irc_plugin *p = NULL;
	struct irc_plugin_loader *ldr;

	if (!path)
		irc_log_info("irccd: trying to find plugin %s", name);
	else
		irc_log_info("irccd: opening plugin %s", name);

	SLIST_FOREACH(ldr, &irc.plugin_loaders, link) {
		if (path) {
			if ((p = open_plugin(ldr, path)))
				break;
		} else {
			/* Copy the paths to tokenize it. */
			strlcpy(buf, ldr->paths, sizeof (buf));

			/*
			 * For every directory (separated by colon) call find_plugin
			 * which will append the extension and try to open it.
			 */
			for (t = buf; (token = strtok_r(t, ":", &t)); ) {
				if ((p = find_plugin(ldr, token, name)))
					break;
			}
		}
	}

	if (!p)
		irc_log_warn("irccd: could not find plugin %s", name);

	strlcpy(p->name, name, sizeof (p->name));

	/* Set default paths if they are not set. */
	irc_plugin_set_path(p, "cache", irc_util_printf(pathbuf, sizeof (pathbuf),
	    "%s/plugin/%s", IRCCD_CACHEDIR, p->name));
	irc_plugin_set_path(p, "data", irc_util_printf(pathbuf, sizeof (pathbuf),
	    "%s/plugin/%s", IRCCD_DATADIR, p->name));
	irc_plugin_set_path(p, "config", irc_util_printf(pathbuf, sizeof (pathbuf),
	    "%s/irccd/plugin/%s", IRCCD_SYSCONFDIR, p->name));

	return p;
}

struct irc_plugin *
irc_bot_plugin_get(const char *name)
{
	struct irc_plugin *p;

	LIST_FOREACH(p, &irc.plugins, link)
		if (strcmp(p->name, name) == 0)
			return p;

	return NULL;
}

void
irc_bot_plugin_remove(const char *name)
{
	struct irc_plugin *p;

	if (!(p = irc_bot_plugin_get(name)))
		return;

	irc_plugin_unload(p);
	irc_plugin_finish(p);

	LIST_REMOVE(p, link);
}

void
irc_bot_plugin_loader_add(struct irc_plugin_loader *ldr)
{
	assert(ldr);

	SLIST_INSERT_HEAD(&irc.plugin_loaders, ldr, link);
}

void
irc_bot_plugin_clear(void)
{
	struct irc_plugin *p, *tmp;

	LIST_FOREACH_SAFE(p, &irc.plugins, link, tmp)
		irc_bot_plugin_remove(p->name);
	LIST_INIT(&irc.plugins);
}

void
irc_bot_rule_insert(struct irc_rule *rule, size_t index)
{
	assert(rule);

	if (index == 0)
		TAILQ_INSERT_HEAD(&irc.rules, rule, link);
	else if (index >= irc_bot_rule_size())
		TAILQ_INSERT_TAIL(&irc.rules, rule, link);
	else {
		struct irc_rule *pos;

		for (pos = TAILQ_FIRST(&irc.rules); --index; )
			pos = TAILQ_NEXT(pos, link);

		TAILQ_INSERT_AFTER(&irc.rules, pos, rule, link);
	}
}

struct irc_rule *
irc_bot_rule_get(size_t index)
{
	assert(index < irc_bot_rule_size());

	struct irc_rule *rule;

	for (rule = TAILQ_FIRST(&irc.rules); index-- != 0; )
		rule = TAILQ_NEXT(rule, link);

	return rule;
}

void
irc_bot_rule_move(size_t from, size_t to)
{
	assert(from < irc_bot_rule_size());

	struct irc_rule *f, *t;

	if (from == to)
		return;

	f = t = TAILQ_FIRST(&irc.rules);

	while (from--)
		f = TAILQ_NEXT(f, link);

	TAILQ_REMOVE(&irc.rules, f, link);

	if (to == 0)
		TAILQ_INSERT_HEAD(&irc.rules, f, link);
	else {
		while (TAILQ_NEXT(t, link) && to--)
			t = TAILQ_NEXT(t, link);

		TAILQ_INSERT_AFTER(&irc.rules, t, f, link);
	}
}

void
irc_bot_rule_remove(size_t index)
{
	assert(index < irc_bot_rule_size());

	struct irc_rule *pos = TAILQ_FIRST(&irc.rules);

	for (size_t i = 0; i < index; ++i)
		pos = TAILQ_NEXT(pos, link);

	TAILQ_REMOVE(&irc.rules, pos, link);
}

size_t
irc_bot_rule_size(void)
{
	const struct irc_rule *r;
	size_t total = 0;

	TAILQ_FOREACH(r, &irc.rules, link)
		total++;

	return total;
}

void
irc_bot_rule_clear(void)
{
	struct irc_rule *r, *tmp;

	TAILQ_FOREACH_SAFE(r, &irc.rules, link, tmp)
		irc_rule_finish(r);
	TAILQ_INIT(&irc.rules);
}

size_t
irc_bot_poll_count(void)
{
	size_t i = 1;
	struct irc_server *s;

	LIST_FOREACH(s, &irc.servers, link)
		++i;

	return i;
}

void
irc_bot_prepare(struct pollfd *fds)
{
	assert(fds);

	struct irc_server *s;
	size_t i = 1;

	fds[0].fd = pipes[0];
	fds[0].events = POLLIN;

	LIST_FOREACH(s, &irc.servers, link)
		irc_server_prepare(s, &fds[i++]);
}

void
irc_bot_flush(const struct pollfd *fds)
{
	assert(fds);

	struct irc_server *s;
	size_t i = 1;

	pipe_flush(&fds[0]);

	LIST_FOREACH(s, &irc.servers, link)
		irc_server_flush(s, &fds[i++]);
}

int
irc_bot_dequeue(struct irc_event *ev)
{
	struct irc_server *s;

	LIST_FOREACH(s, &irc.servers, link)
		if (irc_server_poll(s, ev))
			return 1;

	return 0;
}

void
irc_bot_post(void (*exec)(void *), void *data)
{
	struct defer df = {
		.exec = exec,
		.data = data
	};

	if (write(pipes[1], &df, sizeof (df)) != sizeof (df))
		err(1, "write");
}

void
irc_bot_finish(void)
{
	struct irc_plugin_loader *ld, *ldtmp;

	/*
	 * First remove all loaders to mkae sure plugins won't try to load
	 * new plugins.
	 */
	SLIST_FOREACH_SAFE(ld, &irc.plugin_loaders, link, ldtmp)
		irc_plugin_loader_finish(ld);

	irc_bot_server_clear();
	irc_bot_plugin_clear();
	irc_bot_rule_clear();
}