main.c
author Myhailo Danylenko <isbear@ukrpost.net>
Mon, 23 Feb 2009 17:05:43 +0200
changeset 2 a88a395e6868
parent 1 7d87d323c889
child 3 a5f864d4207f
permissions -rw-r--r--
Delete commands and features on unloading


#include <glib.h>
#include <gmodule.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>    // getenv
#include <string.h>    // strcmp

#include "util.h"
#include "logprint.h"  // scr_LogPrint
#include "screen.h"    // scr_Beep, scr_WriteIncomingMessage
#include "hbuf.h"      // HBB_PREFIX_INFO
#include "commands.h"  // process_command, cmd_add, cmd_del
#include "xmpp.h"      // xmpp_getstatus, xmpp_getstatusmsg, lconnection, xmpp_add_feature, xmpp_del_feature
#include "roster.h"    // imstatus2char, foreach_buddy, buddy_*, current_buddy, BUDDATA, ROSTER_TYPE_*
#include "utils.h"     // from_utf8, jidtodisp
#include "hooks.h"     // hk_add_handler, hk_del_handler
#include "settings.h"  // settings_set, settings_del, settings_get


// global lua state object, necessary for uninitialization function
static lua_State *lua = NULL;

// caller sould g_free result
char *mcabber_config_filename (const char *file)
{
	const char *home = getenv ("HOME");
	if (!home)
		return NULL;
	return g_strconcat (home, "/.mcabber/", file ? file : "", NULL);
}

/// print
/// Prints its arguments to log with default priority.
/// A: string/number, ...
static int lua_global_print (lua_State *L)
{
	lua_concat (L, lua_gettop (L));
	scr_LogPrint (LPRINT_LOGNORM, lua_tostring (L, -1));
	return 0;
}

/// dopath
/// Loads lua file from default location.
/// A: string (filename, without ".lua")
static int lua_global_dopath (lua_State *L)
{
	const char *name = luaL_checkstring (L, 1);
	size_t      size = lua_objlen (L, 1);
	char       *path;
	if (!strncmp (name + size - 4, ".lua", 4))
		path = mcabber_config_filename (name);
	else {
		char *fname = g_strconcat (name, ".lua", NULL);
		path = mcabber_config_filename (fname);
		g_free (fname);
	}

	if (luaL_loadfile (L, path))
		scr_LogPrint (LPRINT_LOGNORM, "lua: Unable to compile file %s: %s", path, lua_tostring (L, -1));
	else if (lua_pcall (lua, 0, LUA_MULTRET, 0))
		scr_LogPrint(LPRINT_LOGNORM, "lua: Runtime error in file %s: %s", path, lua_tostring (L, -1));

	g_free (path);
	return 0;
}

/// main.config_file
/// Adds mcabber default config location path to config file name.
/// Note: deprecated, use dopath.
/// A: string (filename)
/// R: string (full path)
static int lua_main_config_file (lua_State *L)
{
	char *home = mcabber_config_filename (luaL_checkstring (L, 1));
	if (!home) {
		lua_pushstring (L, "Cannot find home dir!");
		lua_error (L);
	}
	lua_pushstring (L, home);
	g_free (home);
	return 1;
}

/// log print type
/// G:
static const string2enum_t lua_lprint[] = {
	{ "normal",  LPRINT_NORMAL  },
	{ "log",     LPRINT_LOG     },
	{ "debug",   LPRINT_DEBUG   },
	{ "notutf0", LPRINT_NOTUTF8 },
	{ NULL,      0              },
};

/// roster type
/// G:
static const string2enum_t lua_roster_type[] = {
	{ "user",    ROSTER_TYPE_USER    },
	{ "group",   ROSTER_TYPE_GROUP   },
	{ "agent",   ROSTER_TYPE_AGENT   },
	{ "room",    ROSTER_TYPE_ROOM    },
	{ "special", ROSTER_TYPE_SPECIAL },
	{ NULL,      0                   },
};

/// main.log
/// Prints message to log.
/// A: log print type, message, message...
static int lua_main_log (lua_State *L)
{
	int type = luaL_checkenum_multi (L, 1, lua_lprint);
	lua_concat (L, lua_gettop (L) - 1);
	scr_LogPrint (type, lua_tostring (L, -1));
	return 0;
}

/// main.option
/// Sets or gets value of mcabber option.
/// You can specify nil as a value to delete option.
/// XXX: Should we do types here?
/// A: string (option name), string (value, optional)
/// R: string (value, optional)
static int lua_main_option (lua_State *L)
{
	const char *name = luaL_checkstring (L, 1);
	if (lua_gettop (L) > 1) { // Set
		if (lua_type (L, 2) == LUA_TNIL) // Unset
			settings_del (SETTINGS_TYPE_OPTION, name);
		else { // Set
			const char *value = luaL_checkstring (L, 2);
			settings_set (SETTINGS_TYPE_OPTION, name, value);
		}
		return 0;
	} else { // Get
		const char *value = settings_get (SETTINGS_TYPE_OPTION, name);
		if (value)
			lua_pushstring (L, value);
		else
			lua_pushnil (L);
		return 1;
	}
}

/// main.connection
/// Returns lightuserdata of mcabber's loudmouth connection.
/// This can be very useful with lua-loudmouth, and not much otherwise.
/// R: lightuserdata
static int lua_main_connection (lua_State *L)
{
	lua_pushlightuserdata (L, lconnection);
	return 1;
}

/// main.print_info
/// Prints a system message to buddy's window.
/// A: string (jid), string (message)
static int lua_main_print_info (lua_State *L)
{
	char *to = jidtodisp (luaL_checkstring (L, 1));
	scr_WriteIncomingMessage (to, luaL_checkstring (L, 2), 0, HBB_PREFIX_INFO, 0);
	g_free (to);
	return 0;
}

/// main.beep
/// Beeps with system speaker.
static int lua_main_beep (lua_State *L)
{
	scr_Beep ();
	return 0;
}

/// main.run
/// Runs specified mcabber command.
/// A: string
static int lua_main_run (lua_State *L)
{
	process_command (luaL_checkstring (L, 1), TRUE);
	return 0;
}

/// main.status
/// Returns your current status.
/// R: string (status letter), string (status message)
static int lua_main_status (lua_State *L)
{
	char *sm = from_utf8 (xmpp_getstatusmsg ());
	lua_pushlstring (L, &imstatus2char[xmpp_getstatus ()], 1);
	lua_pushstring (L, sm);
	g_free (sm);
	return 2;
}

// expects table on top
static void lua_rosterlist_callback (gpointer buddy, lua_State *L)
{
	lua_pushnumber (L, lua_objlen (L, -1) + 1);
	lua_pushstring (L, buddy_getjid (buddy));
	lua_settable (L, -3);
}

/// main.roster
/// Returns array of jids of buddies in roster.
/// R: table
static int lua_main_roster (lua_State *L)
{
	lua_newtable (L);
	foreach_buddy (ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM, (void (*) (gpointer buddy, void *data))lua_rosterlist_callback, L);
	return 1;
}

/// main.current_buddy
/// Returns jid of current selected buddy.
/// R: string
static int lua_main_current_buddy (lua_State *L)
{
	lua_pushstring (L, buddy_getjid (BUDDATA (current_buddy)));
	return 1;
}

typedef struct {
	lua_State *L;
	gpointer   buddy;
} lua_state_and_buddy_t; // :)

// expects table on top!
static void lua_buddy_resources_callback (gpointer resource, lua_state_and_buddy_t *d)
{
	lua_pushstring (d->L, resource);
	lua_createtable (d->L, 0, 3);
	lua_pushstring (d->L, "priority");
	lua_pushnumber (d->L, buddy_getresourceprio (d->buddy, resource));
	lua_settable   (d->L, -3);
	lua_pushstring  (d->L, "status");
	lua_pushlstring (d->L, &imstatus2char[buddy_getstatus (d->buddy, resource)], 1);
	lua_settable    (d->L, -3);
	lua_pushstring (d->L, "message");
	lua_pushstring (d->L, buddy_getstatusmsg (d->buddy, resource));
	lua_settable   (d->L, -3);
	lua_settable (d->L, -3);
}

/// main.buddy_info
/// Returns a hash table with information on specified buddy.
/// Table contains fields type, name, onserver and resources.
/// Resources is also a hash table, that contains tables with information,
/// specific for each resource. In each resource table there are fields
/// priority, status and message.
/// A: string (jid)
/// R: table
static int lua_main_buddy_info (lua_State *L)
{
	char                  *jid   = jidtodisp (luaL_checkstring (L, 1));
	GSList                *buddy = roster_find (jid, jidsearch, ROSTER_TYPE_USER|ROSTER_TYPE_AGENT|ROSTER_TYPE_ROOM);
	lua_state_and_buddy_t  snb;
	g_free (jid);

	if (!buddy) {
		lua_pushnil (L);
		return 1;
	}

	lua_createtable (L, 0, 3);
	lua_pushstring (L, "type");
	luaL_pushenum  (L, buddy_gettype (BUDDATA (buddy)), lua_roster_type);
	lua_settable   (L, -3);
	lua_pushstring (L, "name");
	lua_pushstring (L, buddy_getname (BUDDATA (buddy)));
	lua_settable   (L, -3);
	lua_pushstring  (L, "onserver");
	lua_pushboolean (L, buddy_getonserverflag (BUDDATA (buddy)));
	lua_settable    (L, -3);
	lua_pushstring  (L, "resources");
	lua_createtable (L, 0, 0);
	snb.L     = L;
	snb.buddy = BUDDATA (buddy);
	g_slist_foreach (buddy_getresources (BUDDATA (buddy)), (GFunc) lua_buddy_resources_callback, &snb);
	lua_settable    (L, -3);
	
	return 1;
}

// XMPP DISCO FEATURES

GSList *lua_added_features = NULL;

/// main.add_feature
/// Adds xmlns to disco#info features list.
/// A: string (xmlns)
static int lua_main_add_feature (lua_State *L)
{
	const char *xmlns = luaL_checkstring (L, 1);
	xmpp_add_feature (xmlns);
	lua_added_features = g_slist_prepend (lua_added_features, g_strdup (xmlns));
	return 0;
}

/// main.del_feature
/// Removes xmlns from disco#info features list.
/// A: stirng (xmlns)
static int lua_main_del_feature (lua_State *L)
{
	const char *xmlns = luaL_checkstring (L, 1);
	GSList     *el    = g_slist_find_custom (lua_added_features, xmlns, (GCompareFunc) strcmp);
	xmpp_del_feature (xmlns);
	if (el) {
		g_free (el->data);
		lua_added_features = g_slist_delete_link (lua_added_features, el);
	}
	return 0;
}

// MCABBER COMMANDS

typedef struct {
	int        reference;
	lua_State *L;
} lua_command_callback_t;

static GSList *lua_added_commands = NULL;

/// command function
/// Function to handle newly registered command.
/// A: string (arguments)
void lua_main_command_handler (char *args, lua_command_callback_t *cb)
{
	lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
	lua_pushstring (cb->L, args);
	if (lua_pcall (cb->L, 1, 0, 0)) {
		scr_LogPrint (LPRINT_LOGNORM, "lua: Command execution error: %s", lua_tostring (cb->L, -1));
		lua_pop (cb->L, 1);
	}
}

/// main.add_command
/// Associates mcabber command name with lua function.
/// A: string (command name), command function
static int lua_main_add_command (lua_State *L)
{
	const char             *name = luaL_checkstring (L, 1);
	lua_command_callback_t *cb;
	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	cb = luaL_malloc (L, sizeof (lua_command_callback_t));
	cb->reference = luaL_ref (L, LUA_REGISTRYINDEX);
	cb->L         = L;
	cmd_add (name, "", 0, 0, (void (*) (char *p)) lua_main_command_handler, cb);
	lua_added_commands = g_slist_prepend (lua_added_commands, g_strdup (name));
	return 0;
}

/// main.del_command
/// Removes command from a list of commands.
/// A: string (command name)
static int lua_main_del_command (lua_State *L)
{
	const char             *name = luaL_checkstring (L, 1);
	GSList                 *el   = g_slist_find_custom (lua_added_commands, name, (GCompareFunc) strcmp);
	lua_command_callback_t *cb   = cmd_del (name);
	if (cb) {
		luaL_unref (cb->L, LUA_REGISTRYINDEX, cb->reference);
		luaL_free (cb->L, cb);
	}
	if (el) {
		g_free (el->data);
		lua_added_commands = g_slist_delete_link (lua_added_commands, el);
	}
	return 0;
}

// TIMER

#define LUA_TIMER_PRIORITY ( G_PRIORITY_HIGH_IDLE )

typedef struct {
	int        reference;
	lua_State *L;
} lua_timer_callback_t;

static void lua_timer_callback_destroy (lua_timer_callback_t *cb)
{
	luaL_unref (cb->L, LUA_REGISTRYINDEX, cb->reference);
	luaL_free (cb->L, cb);
}

/// timer function
/// Function, that will be called periodically until it returns false.
/// R: boolean
static gboolean lua_timer_callback (lua_timer_callback_t *cb)
{
	int ret;
	lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
	if (lua_pcall (cb->L, 0, 1, 0)) {
		scr_LogPrint (LPRINT_LOGNORM, "lua: Timer callback execution error: %s", lua_tostring (cb->L, -1));
		lua_pop (cb->L, 1);
		return FALSE;
	}
	ret = lua_toboolean (cb->L, -1);
	lua_pop (cb->L, 1);
	return ret;
}

/// main.timer
/// Creates new timer function, that will be called periodically.
/// A: integer (interval, seconds), timer function
static int lua_main_timer (lua_State *L)
{
	int                   interval = luaL_checkint (L, 1);
	lua_timer_callback_t *cb;
	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	cb = luaL_malloc (L, sizeof (lua_timer_callback_t));
	cb->reference = luaL_ref (L, LUA_REGISTRYINDEX);
	cb->L         = L;

	g_timeout_add_seconds_full (LUA_TIMER_PRIORITY, interval, (GSourceFunc) lua_timer_callback, cb, (GDestroyNotify) lua_timer_callback_destroy);
	return 0;
}

// BACKGROUND PIPE READING

#define LUA_BGREAD_BUFFER ( 4096 )

typedef struct {
	lua_State *L;
	FILE      *fd;
	int        reference;
} lua_bgread_callback_t;

static gchar lua_bgread_buffer[LUA_BGREAD_BUFFER];

static void lua_bgread_callback_destroy (lua_bgread_callback_t *cb)
{
	luaL_unref (cb->L, LUA_REGISTRYINDEX, cb->reference);
	pclose (cb->fd);
	luaL_free (cb->L, cb);
}

/// background reading function
/// Function, that processes output from pipe in asynchroneous way.
/// A: string (data) or nil (eof)
/// R: boolean (false if reading should be terminated)
static gboolean
lua_bgread_callback (GIOChannel *source, GIOCondition condition, lua_bgread_callback_t *cb)
{
	int ret = TRUE;

	if (condition | G_IO_IN) { // data
		while (TRUE) {
			gsize read = 0;
			g_io_channel_read_chars (source, lua_bgread_buffer, LUA_BGREAD_BUFFER, &read, NULL);
			if (!read) // exausted
				break;

			lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
			lua_pushlstring (cb->L, lua_bgread_buffer, read);
			if (lua_pcall (cb->L, 1, 1, 0)) {
				scr_LogPrint (LPRINT_LOGNORM, "lua: Bgread callback execution error: %s", lua_tostring (cb->L, -1));
				lua_pop (cb->L, 1);
				return FALSE;
			}
			ret = lua_toboolean (cb->L, -1);
			lua_pop (cb->L, 1);
			if (!ret) // enough
				return FALSE;
		}
	}

	if (condition & ~G_IO_IN) { // err or hup
		lua_rawgeti (cb->L, LUA_REGISTRYINDEX, cb->reference);
		lua_pushnil (cb->L);
		if (lua_pcall (cb->L, 1, 1, 0)) {
			scr_LogPrint (LPRINT_LOGNORM, "lua: Bgread callback execution error: %s", lua_tostring (cb->L, -1));
			lua_pop (cb->L, 1);
			return FALSE;
		}
		ret = lua_toboolean (cb->L, -1);
		lua_pop (cb->L, 1);
	}

	return ret;
}

/// main.bgread
/// Runs specified command and passes its output to a given function.
/// A: string (command), background reading function
static int lua_main_bgread (lua_State *L)
{
	const char            *command = luaL_checkstring (L, 1);
	lua_bgread_callback_t *cb;
	FILE                  *fd;
	GIOChannel            *channel;
	const char            *charset = NULL;
	luaL_argcheck (L, lua_isfunction (L, 2), 2, "function expected");

	fd = popen (command, "r");
	if (!fd) {
		lua_pushstring (L, "Error opening pipe");
		lua_error (L);
	}

	channel = g_io_channel_unix_new (fileno (fd));
	// We, most likely, need this,
	// But we cannot use this,
	// It will block.
	//if (!g_get_charset (&charset))
	g_io_channel_set_encoding (channel, charset, NULL);
	g_io_channel_set_buffered (channel, FALSE);
	g_io_channel_set_flags (channel, G_IO_FLAG_NONBLOCK, NULL);
	g_io_channel_set_close_on_unref (channel, TRUE);

	cb = luaL_malloc (L, sizeof (lua_bgread_callback_t));
	cb->reference = luaL_ref (L, LUA_REGISTRYINDEX);
	cb->L         = L;
	cb->fd        = fd;

	g_io_add_watch_full (channel, G_PRIORITY_HIGH_IDLE, G_IO_IN|G_IO_HUP|G_IO_ERR, (GIOFunc) lua_bgread_callback, cb, (GDestroyNotify) lua_bgread_callback_destroy);
	return 0;
}

// MAIN INITIALIZATION CODE

#define LUA_HOOK_NAME ( "hook_handler" )

static void lua_hook (hk_arg_t *args, lua_State *L)
{
	hk_arg_t *arg = args;
	lua_getglobal (lua, LUA_HOOK_NAME);
	if (!lua_isfunction (lua, -1)) {
		lua_pop (lua, 1);
		return;
	}
	lua_newtable (L);
	while (arg->name != NULL) {
		lua_pushstring (L, arg->name);
		lua_pushstring (L, arg->value);
		lua_settable (L, -3);
		arg++;
	}
	if (lua_pcall (lua, 1, 0, 0)) {
		scr_LogPrint (LPRINT_NORMAL, "lua: Error in hook_handler: %s", lua_tostring (lua, -1));
		lua_pop (lua, 1);
	}
}

static void *lua_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
	if (nsize == 0) {
		g_free (ptr);
		return NULL;
	} else
		return g_realloc (ptr, nsize);
}

static const luaL_Reg lua_reg_main[] = {
	{ "config_file",   lua_main_config_file   },
	{ "connection",    lua_main_connection    },
	{ "log",           lua_main_log           },
	{ "option",        lua_main_option        },
	{ "add_feature",   lua_main_add_feature   },
	{ "del_feature",   lua_main_del_feature   },
	{ "add_command",   lua_main_add_command   },
	{ "del_command",   lua_main_del_command   },
	{ "print_info",    lua_main_print_info    },
	{ "beep",          lua_main_beep          },
	{ "run",           lua_main_run           },
	{ "status",        lua_main_status        },
	{ "roster",        lua_main_roster        },
	{ "current_buddy", lua_main_current_buddy },
	{ "buddy_info",    lua_main_buddy_info    },
	{ "timer",         lua_main_timer         },
	{ "bgread",        lua_main_bgread        },
	{ NULL,            NULL                   },
};

const gchar *g_module_check_init (GModule *module)
{
	char *initfile;

	lua = lua_newstate (lua_alloc, NULL);
	if (!lua) {
		scr_LogPrint (LPRINT_LOGNORM, "lua: Initialization error");
		return "Lua initialization error";
	}

	luaL_openlibs (lua);

	luaL_register (lua, "main", lua_reg_main);
	lua_pop (lua, 1); // XXX
	lua_register (lua, "dopath", lua_global_dopath);
	lua_register (lua, "print",  lua_global_print );
	
	initfile = mcabber_config_filename ("mcabberrc.lua");
	if (!initfile)
		scr_LogPrint (LPRINT_LOGNORM, "lua: Cannot determine config file name");
	else {
		if (luaL_loadfile(lua, initfile)) {
			scr_LogPrint (LPRINT_LOGNORM, "lua: Unable to compile rc file: %s", lua_tostring (lua, -1));
			lua_pop (lua, 1);
		} else if (lua_pcall (lua, 0, LUA_MULTRET, 0)) {
			scr_LogPrint (LPRINT_LOGNORM, "lua: Runtime error in rc file: %s", lua_tostring(lua, -1));
			lua_pop (lua, 1);
		} else
			scr_LogPrint (LPRINT_LOGNORM, "Loaded mcabberrc.lua");
		g_free (initfile);
	}

	hk_add_handler ((hk_handler_t) lua_hook, lua);

	lua_getglobal (lua, "hook_start");
	if (!lua_isfunction (lua, -1))
		lua_pop (lua, 1);
	else if (lua_pcall (lua, 0, 0, 0)) {
		scr_LogPrint (LPRINT_NORMAL, "lua: Error in hook_start: %s", lua_tostring (lua, -1));
		lua_pop (lua, 1);
	}
	return NULL;
}

static void lua_features_destroy (char *xmlns, gpointer ignore)
{
	xmpp_del_feature (xmlns);
	g_free (xmlns);
}

static void lua_commands_destroy (char *name, gpointer ignore)
{
	lua_command_callback_t *cb = cmd_del (name);
	if (cb) {
		luaL_unref (cb->L, LUA_REGISTRYINDEX, cb->reference);
		luaL_free (cb->L, cb);
	}
	g_free (name);
}

void g_module_unload (GModule *module)
{
	if (lua) {
		lua_getglobal (lua, "hook_quit");
		if (!lua_isfunction (lua, -1))
			lua_pop (lua, 1);
		else if (lua_pcall (lua, 0, 0, 0)) {
			scr_LogPrint (LPRINT_NORMAL, "lua: Error in hook_quit: %s", lua_tostring (lua, -1));
			lua_pop (lua, 1);
		}

		hk_del_handler ((hk_handler_t) lua_hook, lua);

		g_slist_foreach (lua_added_features, (GFunc) lua_features_destroy, NULL);
		g_slist_free (lua_added_features);
		lua_added_features = NULL;

		g_slist_foreach (lua_added_commands, (GFunc) lua_commands_destroy, NULL);
		g_slist_free (lua_added_commands);
		lua_added_commands = NULL;

		lua_close (lua);
		lua = NULL;
	}
}