3 -- Copyright (C) 2008-2009 Matthew Wild
4 -- Copyright (C) 2008-2009 Waqas Hussain
5 -- Copyright (C) 2010 Stefan Gehn
7 -- This project is MIT/X11 licensed. Please see the
8 -- COPYING file in the source package for more information.
11 -- FIXME: XEP-0227 supports XInclude but luaexpat does not
13 -- XEP-227 elements and their current level of support:
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
25 package.path = package.path..";../?.lua";
26 package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager
28 -- ugly workaround for getting datamanager to work outside of prosody :(
30 prosody.platform = "unknown";
31 if os.getenv("WINDIR") then
32 prosody.platform = "windows";
33 elseif package.config:sub(1,1) == "/" then
34 prosody.platform = "posix";
37 local lxp = require "lxp";
38 local st = require "util.stanza";
39 local xmppstream = require "util.xmppstream";
40 local new_xmpp_handlers = xmppstream.new_sax_handlers;
41 local dm = require "util.datamanager"
42 dm.set_data_path("data");
44 local ns_separator = xmppstream.ns_separator;
45 local ns_pattern = xmppstream.ns_pattern;
47 local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
49 -----------------------------------------------------------------------
51 function store_vcard(username, host, stanza)
52 -- create or update vCard for username@host
53 local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza));
54 print("["..(err or "success").."] stored vCard: "..username.."@"..host);
57 function store_password(username, host, password)
58 -- create or update account for username@host
59 local ret, err = dm.store(username, host, "accounts", {password = password});
60 print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password);
63 function store_roster(username, host, roster_items)
64 -- fetch current roster-table for username@host if he already has one
65 local roster = dm.load(username, host, "roster") or {};
66 -- merge imported roster-items with loaded roster
67 for item_tag in roster_items:childtags("item") do
68 -- jid for this roster-item
69 local item_jid = item_tag.attr.jid
70 -- validate item stanzas
71 if (item_jid ~= "") then
72 -- prepare roster item
73 -- TODO: is the subscription attribute optional?
74 local item = {subscription = item_tag.attr.subscription, groups = {}};
75 -- optional: give roster item a real name
76 if item_tag.attr.name then
77 item.name = item_tag.attr.name;
79 -- optional: iterate over group stanzas inside item stanza
80 for group_tag in item_tag:childtags("group") do
81 local group_name = group_tag:get_text();
82 if (group_name ~= "") then
83 item.groups[group_name] = true;
85 print("[error] invalid group stanza: "..group_tag:pretty_print());
88 -- store item in roster
89 roster[item_jid] = item;
90 print("[success] roster entry: " ..username.."@"..host.." - "..item_jid);
92 print("[error] invalid roster stanza: " ..item_tag:pretty_print());
96 -- store merged roster-table
97 local ret, err = dm.store(username, host, "roster", roster);
98 print("["..(err or "success").."] stored roster: " ..username.."@"..host);
101 function store_private(username, host, private_items)
102 local private = dm.load(username, host, "private") or {};
103 for _, ch in ipairs(private_items.tags) do
104 --print("private :"..ch:pretty_print());
105 private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch);
106 print("[success] private item: " ..username.."@"..host.." - "..ch.name);
108 local ret, err = dm.store(username, host, "private", private);
109 print("["..(err or "success").."] stored private: " ..username.."@"..host);
112 function store_offline_messages(username, host, offline_messages)
113 -- TODO: maybe use list_load(), append and list_store() instead
114 -- of constantly reopening the file with list_append()?
115 for ch in offline_messages:childtags("message", "jabber:client") do
116 --print("message :"..ch:pretty_print());
117 local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch));
118 print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from);
123 function store_subscription_request(username, host, presence_stanza)
124 local from_bare = presence_stanza.attr.from;
126 -- fetch current roster-table for username@host if he already has one
127 local roster = dm.load(username, host, "roster") or {};
129 local item = roster[from_bare];
130 if item and (item.subscription == "from" or item.subscription == "both") then
131 return; -- already subscribed, do nothing
134 -- add to table of pending subscriptions
135 if not roster.pending then roster.pending = {}; end
136 roster.pending[from_bare] = true;
138 -- store updated roster-table
139 local ret, err = dm.store(username, host, "roster", roster);
140 print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare);
143 -----------------------------------------------------------------------
145 local curr_host = "";
146 local user_name = "";
151 stream_ns = xmlns_xep227,
153 function cb.streamopened(session, attr)
154 session.notopen = false;
155 user_name = attr.name;
156 store_password(user_name, curr_host, attr.password);
158 function cb.streamclosed(session)
159 session.notopen = true;
162 function cb.handlestanza(session, stanza)
163 --print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
164 if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then
165 store_vcard(user_name, curr_host, stanza);
166 elseif (stanza.name == "query") then
167 if (stanza.attr.xmlns == "jabber:iq:roster") then
168 store_roster(user_name, curr_host, stanza);
169 elseif (stanza.attr.xmlns == "jabber:iq:private") then
170 store_private(user_name, curr_host, stanza);
172 elseif (stanza.name == "offline-messages") then
173 store_offline_messages(user_name, curr_host, stanza);
174 elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then
175 store_subscription_request(user_name, curr_host, stanza);
177 print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
181 local user_handlers = new_xmpp_handlers({ notopen = true }, cb);
183 -----------------------------------------------------------------------
185 local lxp_handlers = {
189 -- TODO: error handling for invalid opening elements if curr_host is empty
190 function lxp_handlers.StartElement(parser, elementname, attributes)
191 local curr_ns, name = elementname:match(ns_pattern);
193 curr_ns, name = "", curr_ns;
195 --io.write("+ ", string.rep(" ", count), name, " (", curr_ns, ")", "\n")
197 if curr_host ~= "" then
198 -- forward to xmlhandlers
199 user_handlers:StartElement(elementname, attributes);
200 elseif (curr_ns == xmlns_xep227) and (name == "host") then
201 curr_host = attributes["jid"]; -- start of host element
202 print("Begin parsing host "..curr_host);
203 elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
204 io.stderr:write("Unhandled XML element: ", name, "\n");
209 -- TODO: error handling for invalid closing elements if host is empty
210 function lxp_handlers.EndElement(parser, elementname)
211 local curr_ns, name = elementname:match(ns_pattern);
213 curr_ns, name = "", curr_ns;
216 --io.write("- ", string.rep(" ", count), name, " (", curr_ns, ")", "\n")
217 if curr_host ~= "" then
218 if (curr_ns == xmlns_xep227) and (name == "host") then
219 print("End parsing host "..curr_host);
220 curr_host = "" -- end of host element
222 -- forward to xmlhandlers
223 user_handlers:EndElement(elementname);
225 elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
226 io.stderr:write("Unhandled XML element: ", name, "\n");
231 function lxp_handlers.CharacterData(parser, string)
232 if curr_host ~= "" then
233 -- forward to xmlhandlers
234 user_handlers:CharacterData(string);
238 -----------------------------------------------------------------------
241 local help = "/? -? ? /h -h /help -help --help";
242 if not arg or help:find(arg, 1, true) then
243 print([[XEP-227 importer for Prosody
245 Usage: xep227toprosody.lua filename.xml
251 local file = io.open(arg);
253 io.stderr:write("Could not open file: ", arg, "\n");
257 local parser = lxp.new(lxp_handlers, ns_separator);
258 for l in file:lines() do