tools/xep227toprosody.lua
changeset 12202 341bc2081bb7
parent 12201 95d25e620dc2
child 12203 12eaa2fdd75b
equal deleted inserted replaced
12201:95d25e620dc2 12202:341bc2081bb7
     1 #!/usr/bin/env lua
       
     2 -- Prosody IM
       
     3 -- Copyright (C) 2008-2009 Matthew Wild
       
     4 -- Copyright (C) 2008-2009 Waqas Hussain
       
     5 -- Copyright (C) 2010      Stefan Gehn
       
     6 --
       
     7 -- This project is MIT/X11 licensed. Please see the
       
     8 -- COPYING file in the source package for more information.
       
     9 --
       
    10 
       
    11 -- FIXME: XEP-0227 supports XInclude but luaexpat does not
       
    12 --
       
    13 -- XEP-227 elements and their current level of support:
       
    14 -- Hosts : supported
       
    15 -- Users : supported
       
    16 -- Rosters : supported, needs testing
       
    17 -- Offline Messages : supported, needs testing
       
    18 -- Private XML Storage : supported, needs testing
       
    19 -- vCards : supported, needs testing
       
    20 -- Privacy Lists: UNSUPPORTED
       
    21 --   http://xmpp.org/extensions/xep-0227.html#privacy-lists
       
    22 --   mod_privacy uses dm.load(username, host, "privacy"); and stores stanzas 1:1
       
    23 -- Incoming Subscription Requests : supported
       
    24 
       
    25 package.path = package.path..";../?.lua";
       
    26 package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager
       
    27 
       
    28 local my_name = arg[0];
       
    29 if my_name:match("[/\\]") then
       
    30 	package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua");
       
    31 	package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so");
       
    32 end
       
    33 
       
    34 -- ugly workaround for getting datamanager to work outside of prosody :(
       
    35 prosody = { };
       
    36 prosody.platform = "unknown";
       
    37 if os.getenv("WINDIR") then
       
    38 	prosody.platform = "windows";
       
    39 elseif package.config:sub(1,1) == "/" then
       
    40 	prosody.platform = "posix";
       
    41 end
       
    42 
       
    43 local lxp = require "lxp";
       
    44 local st = require "util.stanza";
       
    45 local xmppstream = require "util.xmppstream";
       
    46 local new_xmpp_handlers = xmppstream.new_sax_handlers;
       
    47 local dm = require "util.datamanager"
       
    48 dm.set_data_path("data");
       
    49 
       
    50 local ns_separator = xmppstream.ns_separator;
       
    51 local ns_pattern = xmppstream.ns_pattern;
       
    52 
       
    53 local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
       
    54 
       
    55 -----------------------------------------------------------------------
       
    56 
       
    57 function store_vcard(username, host, stanza)
       
    58 	-- create or update vCard for username@host
       
    59 	local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza));
       
    60 	print("["..(err or "success").."] stored vCard: "..username.."@"..host);
       
    61 end
       
    62 
       
    63 function store_password(username, host, password)
       
    64 	-- create or update account for username@host
       
    65 	local ret, err = dm.store(username, host, "accounts", {password = password});
       
    66 	print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password);
       
    67 end
       
    68 
       
    69 function store_roster(username, host, roster_items)
       
    70 	-- fetch current roster-table for username@host if they already have one
       
    71 	local roster = dm.load(username, host, "roster") or {};
       
    72 	-- merge imported roster-items with loaded roster
       
    73 	for item_tag in roster_items:childtags("item") do
       
    74 		-- jid for this roster-item
       
    75 		local item_jid = item_tag.attr.jid
       
    76 		-- validate item stanzas
       
    77 		if (item_jid ~= "") then
       
    78 			-- prepare roster item
       
    79 			-- TODO: is the subscription attribute optional?
       
    80 			local item = {subscription = item_tag.attr.subscription, groups = {}};
       
    81 			-- optional: give roster item a real name
       
    82 			if item_tag.attr.name then
       
    83 				item.name = item_tag.attr.name;
       
    84 			end
       
    85 			-- optional: iterate over group stanzas inside item stanza
       
    86 			for group_tag in item_tag:childtags("group") do
       
    87 				local group_name = group_tag:get_text();
       
    88 				if (group_name ~= "") then
       
    89 					item.groups[group_name] = true;
       
    90 				else
       
    91 					print("[error] invalid group stanza: "..group_tag:pretty_print());
       
    92 				end
       
    93 			end
       
    94 			-- store item in roster
       
    95 			roster[item_jid] = item;
       
    96 			print("[success] roster entry: " ..username.."@"..host.." - "..item_jid);
       
    97 		else
       
    98 			print("[error] invalid roster stanza: " ..item_tag:pretty_print());
       
    99 		end
       
   100 
       
   101 	end
       
   102 	-- store merged roster-table
       
   103 	local ret, err = dm.store(username, host, "roster", roster);
       
   104 	print("["..(err or "success").."] stored roster: " ..username.."@"..host);
       
   105 end
       
   106 
       
   107 function store_private(username, host, private_items)
       
   108 	local private = dm.load(username, host, "private") or {};
       
   109 	for _, ch in ipairs(private_items.tags) do
       
   110 		--print("private :"..ch:pretty_print());
       
   111 		private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch);
       
   112 		print("[success] private item: " ..username.."@"..host.." - "..ch.name);
       
   113 	end
       
   114 	local ret, err = dm.store(username, host, "private", private);
       
   115 	print("["..(err or "success").."] stored private: " ..username.."@"..host);
       
   116 end
       
   117 
       
   118 function store_offline_messages(username, host, offline_messages)
       
   119 	-- TODO: maybe use list_load(), append and list_store() instead
       
   120 	--       of constantly reopening the file with list_append()?
       
   121 	for ch in offline_messages:childtags("message", "jabber:client") do
       
   122 		--print("message :"..ch:pretty_print());
       
   123 		local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch));
       
   124 		print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from);
       
   125 	end
       
   126 end
       
   127 
       
   128 
       
   129 function store_subscription_request(username, host, presence_stanza)
       
   130 	local from_bare = presence_stanza.attr.from;
       
   131 
       
   132 	-- fetch current roster-table for username@host if they already have one
       
   133 	local roster = dm.load(username, host, "roster") or {};
       
   134 
       
   135 	local item = roster[from_bare];
       
   136 	if item and (item.subscription == "from" or item.subscription == "both") then
       
   137 		return; -- already subscribed, do nothing
       
   138 	end
       
   139 
       
   140 	-- add to table of pending subscriptions
       
   141 	if not roster.pending then roster.pending = {}; end
       
   142 	roster.pending[from_bare] = true;
       
   143 
       
   144 	-- store updated roster-table
       
   145 	local ret, err = dm.store(username, host, "roster", roster);
       
   146 	print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare);
       
   147 end
       
   148 
       
   149 -----------------------------------------------------------------------
       
   150 
       
   151 local curr_host = "";
       
   152 local user_name = "";
       
   153 
       
   154 
       
   155 local cb = {
       
   156 	stream_tag = "user",
       
   157 	stream_ns = xmlns_xep227,
       
   158 };
       
   159 function cb.streamopened(session, attr)
       
   160 	session.notopen = false;
       
   161 	user_name = attr.name;
       
   162 	store_password(user_name, curr_host, attr.password);
       
   163 end
       
   164 function cb.streamclosed(session)
       
   165 	session.notopen = true;
       
   166 	user_name = "";
       
   167 end
       
   168 function cb.handlestanza(session, stanza)
       
   169 	--print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
       
   170 	if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then
       
   171 		store_vcard(user_name, curr_host, stanza);
       
   172 	elseif (stanza.name == "query") then
       
   173 		if (stanza.attr.xmlns == "jabber:iq:roster") then
       
   174 			store_roster(user_name, curr_host, stanza);
       
   175 		elseif (stanza.attr.xmlns == "jabber:iq:private") then
       
   176 			store_private(user_name, curr_host, stanza);
       
   177 		end
       
   178 	elseif (stanza.name == "offline-messages") then
       
   179 		store_offline_messages(user_name, curr_host, stanza);
       
   180 	elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then
       
   181 		store_subscription_request(user_name, curr_host, stanza);
       
   182 	else
       
   183 		print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
       
   184 	end
       
   185 end
       
   186 
       
   187 local user_handlers = new_xmpp_handlers({ notopen = true }, cb);
       
   188 
       
   189 -----------------------------------------------------------------------
       
   190 
       
   191 local lxp_handlers = {
       
   192 	--count = 0
       
   193 };
       
   194 
       
   195 -- TODO: error handling for invalid opening elements if curr_host is empty
       
   196 function lxp_handlers.StartElement(parser, elementname, attributes)
       
   197 	local curr_ns, name = elementname:match(ns_pattern);
       
   198 	if name == "" then
       
   199 		curr_ns, name = "", curr_ns;
       
   200 	end
       
   201 	--io.write("+ ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
       
   202 	--count = count + 1;
       
   203 	if curr_host ~= "" then
       
   204 		-- forward to xmlhandlers
       
   205 		user_handlers.StartElement(parser, elementname, attributes);
       
   206 	elseif (curr_ns == xmlns_xep227) and (name == "host") then
       
   207 		curr_host = attributes["jid"]; -- start of host element
       
   208 		print("Begin parsing host "..curr_host);
       
   209 	elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
       
   210 		io.stderr:write("Unhandled XML element: ", name, "\n");
       
   211 		os.exit(1);
       
   212 	end
       
   213 end
       
   214 
       
   215 -- TODO: error handling for invalid closing elements if host is empty
       
   216 function lxp_handlers.EndElement(parser, elementname)
       
   217 	local curr_ns, name = elementname:match(ns_pattern);
       
   218 	if name == "" then
       
   219 		curr_ns, name = "", curr_ns;
       
   220 	end
       
   221 	--count = count - 1;
       
   222 	--io.write("- ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
       
   223 	if curr_host ~= "" then
       
   224 		if (curr_ns == xmlns_xep227) and (name == "host") then
       
   225 			print("End parsing host "..curr_host);
       
   226 			curr_host = "" -- end of host element
       
   227 		else
       
   228 			-- forward to xmlhandlers
       
   229 			user_handlers.EndElement(parser, elementname);
       
   230 		end
       
   231 	elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
       
   232 		io.stderr:write("Unhandled XML element: ", name, "\n");
       
   233 		os.exit(1);
       
   234 	end
       
   235 end
       
   236 
       
   237 function lxp_handlers.CharacterData(parser, string)
       
   238 	if curr_host ~= "" then
       
   239 		-- forward to xmlhandlers
       
   240 		user_handlers.CharacterData(parser, string);
       
   241 	end
       
   242 end
       
   243 
       
   244 -----------------------------------------------------------------------
       
   245 
       
   246 local arg = ...;
       
   247 local help = "/? -? ? /h -h /help -help --help";
       
   248 if not arg or help:find(arg, 1, true) then
       
   249 	print([[XEP-227 importer for Prosody
       
   250 
       
   251   Usage: xep227toprosody.lua filename.xml
       
   252 
       
   253 ]]);
       
   254 	os.exit(1);
       
   255 end
       
   256 
       
   257 local file = io.open(arg);
       
   258 if not file then
       
   259 	io.stderr:write("Could not open file: ", arg, "\n");
       
   260 	os.exit(0);
       
   261 end
       
   262 
       
   263 local parser = lxp.new(lxp_handlers, ns_separator);
       
   264 for l in file:lines() do
       
   265 	parser:parse(l);
       
   266 end
       
   267 parser:parse();
       
   268 parser:close();
       
   269 file:close();