view mod_adhoc_cmd_admin/mod_adhoc_cmd_admin.lua @ 234:abcb59ab355c

Add new motd_sequential module. This module lets you define numbered messages shown to each user in order, but only once per user, and persistent across server restarts. Useful for notifying users of added features and changes in an incremental fashion.
author Jeff Mitchell <jeffrey.mitchell@gmail.com>
date Wed, 04 Aug 2010 22:29:51 +0000
parents 648c24de9040
children 161067865850
line wrap: on
line source

-- Copyright (C) 2009-2010 Florian Zeitz
-- 
-- This file is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

local _G = _G;

local prosody = _G.prosody;
local hosts = prosody.hosts;

local t_concat = table.concat;

local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
local usermanager_get_password = require "core.usermanager".get_password;
local usermanager_set_password = require "core.usermanager".set_password or
	function (username, password, host) return usermanager_create_user(username, password, host) end;
local is_admin = require "core.usermanager".is_admin;

local rm_load_roster = require "core.rostermanager".load_roster;

local st, jid, uuid = require "util.stanza", require "util.jid", require "util.uuid";
local timer_add_task = require "util.timer".add_task;
local dataforms_new = require "util.dataforms".new;
module:log("debug", module:get_name());
local adhoc_new = module:require "adhoc".new;

local add_user_layout = dataforms_new{
	title = "Adding a User";
	instructions = "Fill out this form to add a user.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for the account to be added" };
	{ name = "password", type = "text-private", label = "The password for this account" };
	{ name = "password-verify", type = "text-private", label = "Retype password" };
};

local change_user_password_layout = dataforms_new{
	title = "Changing a User Password";
	instructions = "Fill out this form to change a user's password.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" };
	{ name = "password", type = "text-private", required = true, label = "The password for this account" };
};

local delete_user_layout = dataforms_new{
	title = "Deleting a User";
	instructions = "Fill out this form to delete a user.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) to delete" };
};

local end_user_session_layout = dataforms_new{
	title = "Ending a User Session";
	instructions = "Fill out this form to end a user's session.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" };
};

local get_user_password_layout = dataforms_new{
	title = "Getting User's Password";
	instructions = "Fill out this form to get a user's password.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", label = "The Jabber ID for which to retrieve the password" };
};

local get_user_password_result_layout = dataforms_new{
	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", label = "JID" };
	{ name = "password", type = "text-single", label = "Password" };
};

local get_user_roster_layout = dataforms_new{
	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", label = "The Jabber ID for which to retrieve the roster" };
};

local get_user_roster_result_layout = dataforms_new{
	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", label = "This is the roster for" };
	{ name = "roster", type = "text-multi", label = "Roster XML" };
};

local get_user_stats_layout = dataforms_new{
	title = "Get User Statistics";
	instructions = "Fill out this form to gather user statistics.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "accountjid", type = "jid-single", label = "The Jabber ID for statistics" };
};

local get_user_stats_result_layout = dataforms_new{
	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "ipaddresses", type = "text-multi", label = "IP Addresses" };
	{ name = "rostersize", type = "text-single", label = "Roster size" };
	{ name = "onlineresources", type = "text-multi", label = "Online Resources" };
};

local get_online_users_layout = dataforms_new{
	title = "Getting List of Online Users";
	instructions = "How many users should be returned at most?";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "max_items", type = "list-single", label = "Maximum number of users",
		value = { "25", "50", "75", "100", "150", "200", "all" } };
	{ name = "details", type = "boolean", label = "Show details" };
};

local get_online_users_result_layout = dataforms_new{
	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "onlineuserjids", type = "text-multi", label = "The list of all online users" };
};

local announce_layout = dataforms_new{
	title = "Making an Announcement";
	instructions = "Fill out this form to make an announcement to all\nactive users of this service.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "subject", type = "text-single", label = "Subject" };
	{ name = "announcement", type = "text-multi", required = true, label = "Announcement" };
};

local shut_down_service_layout = dataforms_new{
	title = "Shutting Down the Service";
	instructions = "Fill out this form to shut down the service.";

	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
	{ name = "delay", type = "list-single", label = "Time delay before shutting down",
		value = { {label = "30 seconds", value = "30"},
			  {label = "60 seconds", value = "60"},
			  {label = "90 seconds", value = "90"},
			  {label = "2 minutes", value = "120"},
			  {label = "3 minutes", value = "180"},
			  {label = "4 minutes", value = "240"},
			  {label = "5 minutes", value = "300"},
		};
	};
	{ name = "announcement", type = "text-multi", label = "Announcement" };
};

function add_user_command_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end
		local fields = add_user_layout:data(data.form);
		local username, host, resource = jid.split(fields.accountjid);
		if data.to ~= host then
			return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. data.to}};
		end
		if (fields["password"] == fields["password-verify"]) and username and host then
			if usermanager_user_exists(username, host) then
				return { status = "completed", error = { message = "Account already exists" } };
			else
				if usermanager_create_user(username, fields.password, host) then
					module:log("info", "Created new account " .. username.."@"..host);
					return { status = "completed", info = "Account successfully created" };
				else
					return { status = "completed", error = { message = "Failed to write data to disk" } };
				end
			end
		else
			module:log("debug", (fields.accountjid or "<nil>") .. " " .. (fields.password or "<nil>") .. " "
				.. (fields["password-verify"] or "<nil>"));
			return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } };
		end
	else
		return { status = "executing", form = add_user_layout }, "executing";
	end
end

function change_user_password_command_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end
		local fields = change_user_password_layout:data(data.form);
		local username, host, resource = jid.split(fields.accountjid);
		if data.to ~= host then
			return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. data.to}};
		end
		if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then
			return { status = "completed", info = "Password successfully changed" };
		else
			return { status = "completed", error = { message = "User does not exist" } };
		end
	else
		return { status = "executing", form = change_user_password_layout }, "executing";
	end
end

function disconnect_user(match_jid)
	local node, hostname, givenResource = jid.split(match_jid);
	local host = hosts[hostname];
	local sessions = host.sessions[node] and host.sessions[node].sessions;
	for resource, session in pairs(sessions or {}) do
		if not givenResource or (resource == givenResource) then
			module:log("debug", "Disconnecting "..node.."@"..hostname.."/"..resource);
			session:close();
		end
	end
	return true;
end

function delete_user_command_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end
		local fields = delete_user_layout:data(data.form);
		local failed = {};
		local succeeded = {};
		for _, aJID in ipairs(fields.accountjids) do
			local username, host, resource = jid.split(aJID);
			if (host == data.to) and  usermanager_user_exists(username, host) and disconnect_user(aJID) and usermanager_create_user(username, nil, host) then
				module:log("debug", "User " .. aJID .. " has been deleted");
				succeeded[#succeeded+1] = aJID;
			else
				module:log("debug", "Tried to delete non-existant user "..aJID);
				failed[#failed+1] = aJID;
			end
		end
		return {status = "completed", info = (#succeeded ~= 0 and
				"The following accounts were successfully deleted:\n"..t_concat(succeeded, "\n").."\n" or "")..
				(#failed ~= 0 and
				"The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") };
	else
		return { status = "executing", form = delete_user_layout }, "executing";
	end
end

function end_user_session_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end

		local fields = end_user_session_layout:data(data.form);
		local failed = {};
		local succeeded = {};
		for _, aJID in ipairs(fields.accountjids) do
			local username, host, resource = jid.split(aJID);
			if (host == data.to) and  usermanager_user_exists(username, host) and disconnect_user(aJID) then
				succeeded[#succeeded+1] = aJID;
			else
				failed[#failed+1] = aJID;
			end
		end
		return {status = "completed", info = (#succeeded ~= 0 and
				"The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "")..
				(#failed ~= 0 and
				"The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") };
	else
		return { status = "executing", form = end_user_session_layout }, "executing";
	end
end

function get_user_password_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end
		local fields = get_user_password_layout:data(data.form);
		local user, host, resource = jid.split(fields.accountjid);
		local accountjid = "";
		local password = "";
		if host ~= data.to then
			return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. data.to } };
		elseif usermanager_user_exists(user, host) then
			accountjid = fields.accountjid;
			password = usermanager_get_password(user, host);
		else
			return { status = "completed", error = { message = "User does not exist" } };
		end
		return { status = "completed", result = { layout = get_user_password_result_layout, data = {accountjid = accountjid, password = password} } };
	else
		return { status = "executing", form = get_user_password_layout }, "executing";
	end
end

function get_user_roster_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end

		local fields = add_user_layout:data(data.form);

		local user, host, resource = jid.split(fields.accountjid);
		if host ~= data.to then
			return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. data.to } };
		elseif not usermanager_user_exists(user, host) then
			return { status = "completed", error = { message = "User does not exist" } };
		end
		local roster = rm_load_roster(user, host);

		local query = st.stanza("query", { xmlns = "jabber:iq:roster" });
		for jid in pairs(roster) do
			if jid ~= "pending" and jid then
				query:tag("item", {
					jid = jid,
					subscription = roster[jid].subscription,
					ask = roster[jid].ask,
					name = roster[jid].name,
				});
				for group in pairs(roster[jid].groups) do
					query:tag("group"):text(group):up();
				end
				query:up();
			end
		end

		local query_text = query:__tostring(); -- TODO: Use upcoming pretty_print() function
		query_text = query_text:gsub("><", ">\n<");

		local result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result");
		result:add_child(query);
		return { status = "completed", other = result };
	else
		return { status = "executing", form = get_user_roster_layout }, "executing";
	end
end

function get_user_stats_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end

		local fields = get_user_stats_layout:data(data.form);

		local user, host, resource = jid.split(fields.accountjid);
		if host ~= data.to then
			return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. data.to } };
		elseif not usermanager_user_exists(user, host) then
			return { status = "completed", error = { message = "User does not exist" } };
		end
		local roster = rm_load_roster(user, host);
		local rostersize = 0;
		local IPs = "";
		local resources = "";
		for jid in pairs(roster) do
			if jid ~= "pending" and jid then
				rostersize = rostersize + 1;
			end
		end
		for resource, session in pairs((hosts[host].sessions[user] and hosts[host].sessions[user].sessions) or {}) do
			resources = resources .. "\n" .. resource;
			IPs = IPs .. "\n" .. session.ip;
		end
		return { status = "completed", result = {layout = get_user_stats_result_layout, data = {ipaddresses = IPs, rostersize = tostring(rostersize),
			onlineresources = resources}} };
	else
		return { status = "executing", form = get_user_stats_layout }, "executing";
	end
end

function get_online_users_command_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end

		local fields = get_online_users_layout:data(data.form);

		local max_items = nil
		if fields.max_items ~= "all" then
			max_items = tonumber(fields.max_items);
		end
		local count = 0;
		local users = {};
		for username, user in pairs(hosts[data.to].sessions or {}) do
			if (max_items ~= nil) and (count >= max_items) then
				break;
			end
			users[#users+1] = username.."@"..data.to;
			count = count + 1;
			if fields.details then
				for resource, session in pairs(user.sessions or {}) do
					local status, priority = "unavailable", tostring(session.priority or "-");
					if session.presence then
						status = session.presence:child_with_name("show");
						if status then
							status = status:get_text() or "[invalid!]";
						else
							status = "available";
						end
					end
					users[#users+1] = " - "..resource..": "..status.."("..priority..")";
				end
			end
		end
		return { status = "completed", result = {layout = get_online_users_result_layout, data = {onlineuserjids=t_concat(users, "\n")}} };
	else
		return { status = "executing", form = get_online_users_layout }, "executing";
	end
end

function send_to_online(message, server)
	if server then
		sessions = { [server] = hosts[server] };
	else
		sessions = hosts;
	end

	local c = 0;
	for domain, session in pairs(sessions) do
		for user in pairs(session.sessions or {}) do
			c = c + 1;
			message.attr.from = domain;
			message.attr.to = user.."@"..domain;
			core_post_stanza(session, message);
		end
	end

	return c;
end

function announce_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end

		local fields = announce_layout:data(data.form);

		module:log("info", "Sending server announcement to all online users");
		local message = st.message({type = "headline"}, fields.announcement):up()
			:tag("subject"):text(fields.subject or "Announcement");
		
		local count = send_to_online(message, data.to);
		
		module:log("info", "Announcement sent to %d online users", count);
		return { status = "completed", info = "Announcement sent." };
	else
		return { status = "executing", form = announce_layout }, "executing";
	end

	return true;
end

function shut_down_service_handler(self, data, state)
	if state then
		if data.action == "cancel" then
			return { status = "canceled" };
		end

		local fields = shut_down_service_layout:data(data.form);

		if fields.announcement then
			local message = st.message({type = "headline"}, fields.announcement):up()
				:tag("subject"):text("Server is shutting down");
			send_to_online(message);
		end

		timer_add_task(tonumber(fields.delay or "5"), prosody.shutdown);

		return { status = "completed", info = "Server is about to shut down" };
	else
		return { status = "executing", form = shut_down_service_layout }, "executing";
	end

	return true;
end

local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin");
local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin");
local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin");
local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin");
local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin");
local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin");
local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users", get_online_users_command_handler, "admin"); 
local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "admin");

module:add_item("adhoc", add_user_desc);
module:add_item("adhoc", announce_desc);
module:add_item("adhoc", change_user_password_desc);
module:add_item("adhoc", delete_user_desc);
module:add_item("adhoc", end_user_session_desc);
module:add_item("adhoc", get_user_password_desc);
module:add_item("adhoc", get_user_roster_desc);
module:add_item("adhoc", get_user_stats_desc);
module:add_item("adhoc", get_online_users_desc);
module:add_item("adhoc", shut_down_service_desc);