mod_admin_messageconsole.lua
author Mikael Berthe <mikael@lilotux.net>
Tue, 31 Jul 2012 18:46:54 +0200
branch0.8
changeset 2 e193f80521cc
parent 1 7265595dbc3b
permissions -rw-r--r--
Backport to Prosody 0.8

-- Prosody IM
--
-- This module depends on Prosody's admin_telnet module
-- and some code (redirect_output() and onincoming() from console_listener)
-- is duplicated here...
--
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
-- Copyright (C) 2012 Mikael Berthe
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

local st = require "util.stanza";
local um_is_admin = require "core.usermanager".is_admin;

local telnet_def_env;
local default_env_mt;
local telnet_commands;

local host = module.host;

-- Create our own session.  print() will store the results in a text
-- string.  send(), quit(), disconnect() are no-op.
local function new_session ()
	local session = {
			send	    = function ()  end;
			quit	    = function ()  end;
			disconnect  = function ()  end;
			};

	-- Initialization
	-- Hack for 0.8 - Let's get dependencies now as we're almost
	-- certain the admin_telnet module has been loaded by now.
	if not telnet_def_env then
		local prosody = _G.prosody;
		local console = prosody.console;

		telnet_def_env = console.env;
		default_env_mt = { __index = telnet_def_env };

		telnet_commands = console.commands;
	end

	session.print = function (...)
		local t = {};
		for i=1,select("#", ...) do
			t[i] = tostring(select(i, ...));
		end
		local text = "| "..table.concat(t, "\t");
		if session.fulltext then
		    session.fulltext = session.fulltext .. "\n" .. text;
		else
		    session.fulltext = text;
		end
	end

	session.env = setmetatable({}, default_env_mt);

	-- Load up environment with helper objects
	for name, t in pairs(telnet_def_env) do
		if type(t) == "table" then
			session.env[name] = setmetatable({ session = session },
							 { __index = t });
		end
	end

	return session;
end

-- This function is 100% duplicated from mod_admin_telnet.
function onincoming(session, data)

	local commands = telnet_commands;
	local partial = session.partial_data;
	if partial then
		data = partial..data;
	end

	local function redirect_output(_G, session)
		local env = setmetatable({ print = session.print },
			     { __index = function (t, k) return rawget(_G, k); end });
		env.dofile = function(name)
			local f, err = loadfile(name);
			if not f then return f, err; end
			return setfenv(f, env)();
		end;
		return env;
	end

	for line in data:gmatch("[^\n]*[\n\004]") do
		-- Handle data (loop allows us to break to add \0 after response)
		repeat
			local useglobalenv;

			if line:match("^>") then
				line = line:gsub("^>", "");
				useglobalenv = true;
			elseif line == "\004" then
				commands["bye"](session, line);
				break;
			else
				local command = line:lower();
				command = line:match("^%w+") or line:match("%p");
				if commands[command] then
					commands[command](session, line);
					break;
				end
			end

			session.env._ = line;

			local chunkname = "=console";
			local chunk, err = loadstring("return "..line, chunkname);
			if not chunk then
				chunk, err = loadstring(line, chunkname);
				if not chunk then
					err = err:gsub("^%[string .-%]:%d+: ", "");
					err = err:gsub("^:%d+: ", "");
					err = err:gsub("'<eof>'", "the end of the line");
					session.print("Sorry, I couldn't understand that... "..err);
					break;
				end
			end

			setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil);

			local ranok, taskok, message = pcall(chunk);

			if not (ranok or message or useglobalenv) and commands[line:lower()] then
				commands[line:lower()](session, line);
				break;
			end

			if not ranok then
				session.print("Fatal error while running command, it did not complete");
				session.print("Error: "..taskok);
				break;
			end

			if not message then
				session.print("Result: "..tostring(taskok));
				break;
			elseif (not taskok) and message then
				session.print("Command completed with a problem");
				session.print("Message: "..tostring(message));
				break;
			end

			session.print("OK: "..tostring(message));
		until true

		session.send(string.char(0));
	end
	session.partial_data = data:match("[^\n]+$");
end

function on_message(event)
	-- Check the type of the incoming stanza to avoid loops:
	if event.stanza.attr.type == "error" then
		return; -- We do not want to reply to these, so leave.
	end

	local userjid = event.stanza.attr.from;
	local bodytag = event.stanza:get_child("body");
	local body = bodytag and bodytag:get_text() or "";
	if not body or body == "" then
		-- We do not reply to empty messages (chatstates, etc.)
		return true;
	end

	-- Check the requester is an admin user
	if not um_is_admin(userjid, module.host) then
		module:log("info", "Ignored request from non-admin: %s",
			   userjid);
		return;
	end

	-- Create a session in order to use an admin_telnet-like environment
	local session = new_session();

	-- Process the message using admin_telnet's onincoming function
	onincoming(session, body.."\n");

	-- Strip trailing blank line
	session.fulltext = tostring(session.fulltext):gsub("\n\|%s*$", "")

	-- Send the reply stanza
	local reply_stanza = st.message({ from = host, to = userjid,
					type = "chat" });
	reply_stanza = reply_stanza:body(session.fulltext);
	core_post_stanza(hosts[module.host], reply_stanza);

	return true;
end

local function on_presence(event)

	local send_presence = false;

	local userjid = event.stanza.attr.from;

	-- Check the requester is an admin user
	if not um_is_admin(userjid, module.host) then
		module:log("info", "Ignored presence from non-admin: %s",
			   userjid);
		return;
	end

	if (event.stanza.attr.type == "subscribe") then
		module:log("info", "Subscription request from %s", userjid);
		send_presence = true;
		-- Send a subscription ack
		local presence_stanza = st.presence({ from = host,
					to = userjid, type = "subscribed",
					id = event.stanza.attr.id });
		core_post_stanza(hosts[module.host], presence_stanza);
	elseif (event.stanza.attr.type == "probe") then
		send_presence = true;
	elseif (event.stanza.attr.type == "unsubscribe") then
		-- For information only...
		module:log("info", "Unsubscription request from %s", userjid);
	end

	if (send_presence == true) then
		-- Send a presence stanza
		local presence_stanza = st.presence({ from = host,
						      to = userjid });
		core_post_stanza(hosts[module.host], presence_stanza);
	end
	return true;
end

module:hook("message/bare", on_message);
module:hook("presence/bare", on_presence);

-- vim:set noet sts=8 sw=8: