File

tools/xep227toprosody.lua @ 9713:aed9e6b1242a

Merge 0.11 -> trunk
author Matthew Wild <mwild1@gmail.com>
date Wed, 19 Dec 2018 13:25:59 +0000
parent 6870:d5b416070f70
child 11680:a3d3fb9d0d43
line wrap: on
line source

#!/usr/bin/env lua
-- Prosody IM
-- Copyright (C) 2008-2009 Matthew Wild
-- Copyright (C) 2008-2009 Waqas Hussain
-- Copyright (C) 2010      Stefan Gehn
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--

-- FIXME: XEP-0227 supports XInclude but luaexpat does not
--
-- XEP-227 elements and their current level of support:
-- Hosts : supported
-- Users : supported
-- Rosters : supported, needs testing
-- Offline Messages : supported, needs testing
-- Private XML Storage : supported, needs testing
-- vCards : supported, needs testing
-- Privacy Lists: UNSUPPORTED
--   http://xmpp.org/extensions/xep-0227.html#privacy-lists
--   mod_privacy uses dm.load(username, host, "privacy"); and stores stanzas 1:1
-- Incoming Subscription Requests : supported

package.path = package.path..";../?.lua";
package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager

local my_name = arg[0];
if my_name:match("[/\\]") then
	package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua");
	package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so");
end

-- ugly workaround for getting datamanager to work outside of prosody :(
prosody = { };
prosody.platform = "unknown";
if os.getenv("WINDIR") then
	prosody.platform = "windows";
elseif package.config:sub(1,1) == "/" then
	prosody.platform = "posix";
end

local lxp = require "lxp";
local st = require "util.stanza";
local xmppstream = require "util.xmppstream";
local new_xmpp_handlers = xmppstream.new_sax_handlers;
local dm = require "util.datamanager"
dm.set_data_path("data");

local ns_separator = xmppstream.ns_separator;
local ns_pattern = xmppstream.ns_pattern;

local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";

-----------------------------------------------------------------------

function store_vcard(username, host, stanza)
	-- create or update vCard for username@host
	local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza));
	print("["..(err or "success").."] stored vCard: "..username.."@"..host);
end

function store_password(username, host, password)
	-- create or update account for username@host
	local ret, err = dm.store(username, host, "accounts", {password = password});
	print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password);
end

function store_roster(username, host, roster_items)
	-- fetch current roster-table for username@host if he already has one
	local roster = dm.load(username, host, "roster") or {};
	-- merge imported roster-items with loaded roster
	for item_tag in roster_items:childtags("item") do
		-- jid for this roster-item
		local item_jid = item_tag.attr.jid
		-- validate item stanzas
		if (item_jid ~= "") then
			-- prepare roster item
			-- TODO: is the subscription attribute optional?
			local item = {subscription = item_tag.attr.subscription, groups = {}};
			-- optional: give roster item a real name
			if item_tag.attr.name then
				item.name = item_tag.attr.name;
			end
			-- optional: iterate over group stanzas inside item stanza
			for group_tag in item_tag:childtags("group") do
				local group_name = group_tag:get_text();
				if (group_name ~= "") then
					item.groups[group_name] = true;
				else
					print("[error] invalid group stanza: "..group_tag:pretty_print());
				end
			end
			-- store item in roster
			roster[item_jid] = item;
			print("[success] roster entry: " ..username.."@"..host.." - "..item_jid);
		else
			print("[error] invalid roster stanza: " ..item_tag:pretty_print());
		end

	end
	-- store merged roster-table
	local ret, err = dm.store(username, host, "roster", roster);
	print("["..(err or "success").."] stored roster: " ..username.."@"..host);
end

function store_private(username, host, private_items)
	local private = dm.load(username, host, "private") or {};
	for _, ch in ipairs(private_items.tags) do
		--print("private :"..ch:pretty_print());
		private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch);
		print("[success] private item: " ..username.."@"..host.." - "..ch.name);
	end
	local ret, err = dm.store(username, host, "private", private);
	print("["..(err or "success").."] stored private: " ..username.."@"..host);
end

function store_offline_messages(username, host, offline_messages)
	-- TODO: maybe use list_load(), append and list_store() instead
	--       of constantly reopening the file with list_append()?
	for ch in offline_messages:childtags("message", "jabber:client") do
		--print("message :"..ch:pretty_print());
		local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch));
		print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from);
	end
end


function store_subscription_request(username, host, presence_stanza)
	local from_bare = presence_stanza.attr.from;

	-- fetch current roster-table for username@host if he already has one
	local roster = dm.load(username, host, "roster") or {};

	local item = roster[from_bare];
	if item and (item.subscription == "from" or item.subscription == "both") then
		return; -- already subscribed, do nothing
	end

	-- add to table of pending subscriptions
	if not roster.pending then roster.pending = {}; end
	roster.pending[from_bare] = true;

	-- store updated roster-table
	local ret, err = dm.store(username, host, "roster", roster);
	print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare);
end

-----------------------------------------------------------------------

local curr_host = "";
local user_name = "";


local cb = {
	stream_tag = "user",
	stream_ns = xmlns_xep227,
};
function cb.streamopened(session, attr)
	session.notopen = false;
	user_name = attr.name;
	store_password(user_name, curr_host, attr.password);
end
function cb.streamclosed(session)
	session.notopen = true;
	user_name = "";
end
function cb.handlestanza(session, stanza)
	--print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
	if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then
		store_vcard(user_name, curr_host, stanza);
	elseif (stanza.name == "query") then
		if (stanza.attr.xmlns == "jabber:iq:roster") then
			store_roster(user_name, curr_host, stanza);
		elseif (stanza.attr.xmlns == "jabber:iq:private") then
			store_private(user_name, curr_host, stanza);
		end
	elseif (stanza.name == "offline-messages") then
		store_offline_messages(user_name, curr_host, stanza);
	elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then
		store_subscription_request(user_name, curr_host, stanza);
	else
		print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
	end
end

local user_handlers = new_xmpp_handlers({ notopen = true }, cb);

-----------------------------------------------------------------------

local lxp_handlers = {
	--count = 0
};

-- TODO: error handling for invalid opening elements if curr_host is empty
function lxp_handlers.StartElement(parser, elementname, attributes)
	local curr_ns, name = elementname:match(ns_pattern);
	if name == "" then
		curr_ns, name = "", curr_ns;
	end
	--io.write("+ ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
	--count = count + 1;
	if curr_host ~= "" then
		-- forward to xmlhandlers
		user_handlers.StartElement(parser, elementname, attributes);
	elseif (curr_ns == xmlns_xep227) and (name == "host") then
		curr_host = attributes["jid"]; -- start of host element
		print("Begin parsing host "..curr_host);
	elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
		io.stderr:write("Unhandled XML element: ", name, "\n");
		os.exit(1);
	end
end

-- TODO: error handling for invalid closing elements if host is empty
function lxp_handlers.EndElement(parser, elementname)
	local curr_ns, name = elementname:match(ns_pattern);
	if name == "" then
		curr_ns, name = "", curr_ns;
	end
	--count = count - 1;
	--io.write("- ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
	if curr_host ~= "" then
		if (curr_ns == xmlns_xep227) and (name == "host") then
			print("End parsing host "..curr_host);
			curr_host = "" -- end of host element
		else
			-- forward to xmlhandlers
			user_handlers.EndElement(parser, elementname);
		end
	elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
		io.stderr:write("Unhandled XML element: ", name, "\n");
		os.exit(1);
	end
end

function lxp_handlers.CharacterData(parser, string)
	if curr_host ~= "" then
		-- forward to xmlhandlers
		user_handlers.CharacterData(parser, string);
	end
end

-----------------------------------------------------------------------

local arg = ...;
local help = "/? -? ? /h -h /help -help --help";
if not arg or help:find(arg, 1, true) then
	print([[XEP-227 importer for Prosody

  Usage: xep227toprosody.lua filename.xml

]]);
	os.exit(1);
end

local file = io.open(arg);
if not file then
	io.stderr:write("Could not open file: ", arg, "\n");
	os.exit(0);
end

local parser = lxp.new(lxp_handlers, ns_separator);
for l in file:lines() do
	parser:parse(l);
end
parser:parse();
parser:close();
file:close();