tools/xep227toprosody.lua: Convert to use util.xmppstream
[prosody.git] / tools / xep227toprosody.lua
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 -- ugly workaround for getting datamanager to work outside of prosody :(
29 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";
35 end
36
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");
43
44 local ns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
45 local ns_separator = xmppstream.ns_separator;
46 local ns_pattern = xmppstream.ns_pattern;
47
48 -----------------------------------------------------------------------
49
50 function store_vcard(username, host, stanza)
51         -- create or update vCard for username@host
52         local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza));
53         print("["..(err or "success").."] stored vCard: "..username.."@"..host);
54 end
55
56 function store_password(username, host, password)
57         -- create or update account for username@host
58         local ret, err = dm.store(username, host, "accounts", {password = password});
59         print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password);
60 end
61
62 function store_roster(username, host, roster_items)
63         -- fetch current roster-table for username@host if he already has one
64         local roster = dm.load(username, host, "roster") or {};
65         -- merge imported roster-items with loaded roster
66         for item_tag in roster_items:childtags() do
67                 -- jid for this roster-item
68                 local item_jid = item_tag.attr.jid
69                 -- validate item stanzas
70                 if (item_tag.name == "item") and (item_jid ~= "") then
71                         -- prepare roster item
72                         -- TODO: is the subscription attribute optional?
73                         local item = {subscription = item_tag.attr.subscription, groups = {}};
74                         -- optional: give roster item a real name
75                         if item_tag.attr.name then
76                                 item.name = item_tag.attr.name;
77                         end
78                         -- optional: iterate over group stanzas inside item stanza
79                         for group_tag in item_tag:childtags() do
80                                 local group_name = group_tag:get_text();
81                                 if (group_tag.name == "group") and (group_name ~= "") then
82                                         item.groups[group_name] = true;
83                                 else
84                                         print("[error] invalid group stanza: "..group_tag:pretty_print());
85                                 end
86                         end
87                         -- store item in roster
88                         roster[item_jid] = item;
89                         print("[success] roster entry: " ..username.."@"..host.." - "..item_jid);
90                 else
91                         print("[error] invalid roster stanza: " ..item_tag:pretty_print());
92                 end
93
94         end
95         -- store merged roster-table
96         local ret, err = dm.store(username, host, "roster", roster);
97         print("["..(err or "success").."] stored roster: " ..username.."@"..host);
98 end
99
100 function store_private(username, host, private_items)
101         local private = dm.load(username, host, "private") or {};
102         for ch in private_items:childtags() do
103                 --print("private :"..ch:pretty_print());
104                 private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch);
105                 print("[success] private item: " ..username.."@"..host.." - "..ch.name);
106         end
107         local ret, err = dm.store(username, host, "private", private);
108         print("["..(err or "success").."] stored private: " ..username.."@"..host);
109 end
110
111 function store_offline_messages(username, host, offline_messages)
112         -- TODO: maybe use list_load(), append and list_store() instead
113         --       of constantly reopening the file with list_append()?
114         for ch in offline_messages:childtags() do
115                 --print("message :"..ch:pretty_print());
116                 local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch));
117                 print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from);
118         end
119 end
120
121
122 function store_subscription_request(username, host, presence_stanza)
123         local from_bare = presence_stanza.attr.from;
124
125         -- fetch current roster-table for username@host if he already has one
126         local roster = dm.load(username, host, "roster") or {};
127
128         local item = roster[from_bare];
129         if item and (item.subscription == "from" or item.subscription == "both") then
130                 return; -- already subscribed, do nothing
131         end
132
133         -- add to table of pending subscriptions
134         if not roster.pending then roster.pending = {}; end
135         roster.pending[from_bare] = true;
136
137         -- store updated roster-table
138         local ret, err = dm.store(username, host, "roster", roster);
139         print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare);
140 end
141
142 -----------------------------------------------------------------------
143
144 local curr_host = "";
145 local user_name = "";
146
147
148 local cb = {
149         stream_tag = "user",
150         stream_ns = ns_xep227,
151 };
152 function cb.streamopened(session, attr)
153         session.notopen = false;
154         user_name = attr.name;
155         store_password(user_name, curr_host, attr.password);
156 end
157 function cb.streamclosed(session)
158         session.notopen = true;
159         user_name = "";
160 end
161 function cb.handlestanza(session, stanza)
162         --print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
163         if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then
164                 store_vcard(user_name, curr_host, stanza);
165         elseif (stanza.name == "query") then
166                 if (stanza.attr.xmlns == "jabber:iq:roster") then
167                         store_roster(user_name, curr_host, stanza);
168                 elseif (stanza.attr.xmlns == "jabber:iq:private") then
169                         store_private(user_name, curr_host, stanza);
170                 end
171         elseif (stanza.name == "offline-messages") then
172                 store_offline_messages(user_name, curr_host, stanza);
173         elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then
174                 store_subscription_request(user_name, curr_host, stanza);
175         else
176                 print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
177         end
178 end
179
180 local user_handlers = new_xmpp_handlers({ notopen = true }, cb);
181
182 -----------------------------------------------------------------------
183
184 local lxp_handlers = {
185         --count = 0
186 };
187
188 -- TODO: error handling for invalid opening elements if curr_host is empty
189 function lxp_handlers.StartElement(parser, elementname, attributes)
190         local curr_ns, name = elementname:match(ns_pattern);
191         if name == "" then
192                 curr_ns, name = "", curr_ns;
193         end
194         --io.write("+ ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
195         --count = count + 1;
196         if curr_host ~= "" then
197                 -- forward to xmlhandlers
198                 user_handlers:StartElement(elementname, attributes);
199         elseif (curr_ns == ns_xep227) and (name == "host") then
200                 curr_host = attributes["jid"]; -- start of host element
201                 print("Begin parsing host "..curr_host);
202         elseif (curr_ns ~= ns_xep227) or (name ~= "server-data") then
203                 io.stderr:write("Unhandled XML element: ", name, "\n");
204                 os.exit(1);
205         end
206 end
207
208 -- TODO: error handling for invalid closing elements if host is empty
209 function lxp_handlers.EndElement(parser, elementname)
210         local curr_ns, name = elementname:match(ns_pattern);
211         if name == "" then
212                 curr_ns, name = "", curr_ns;
213         end
214         --count = count - 1;
215         --io.write("- ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
216         if curr_host ~= "" then
217                 if (curr_ns == ns_xep227) and (name == "host") then
218                         print("End parsing host "..curr_host);
219                         curr_host = "" -- end of host element
220                 else
221                         -- forward to xmlhandlers
222                         user_handlers:EndElement(elementname);
223                 end
224         elseif (curr_ns ~= ns_xep227) or (name ~= "server-data") then
225                 io.stderr:write("Unhandled XML element: ", name, "\n");
226                 os.exit(1);
227         end
228 end
229
230 function lxp_handlers.CharacterData(parser, string)
231         if curr_host ~= "" then
232                 -- forward to xmlhandlers
233                 user_handlers:CharacterData(string);
234         end
235 end
236
237 -----------------------------------------------------------------------
238
239 local arg = ...;
240 local help = "/? -? ? /h -h /help -help --help";
241 if not arg or help:find(arg, 1, true) then
242         print([[XEP-227 importer for Prosody
243
244   Usage: xep227toprosody.lua filename.xml
245
246 ]]);
247         os.exit(1);
248 end
249
250 local file = io.open(arg);
251 if not file then
252         io.stderr:write("Could not open file: ", arg, "\n");
253         os.exit(0);
254 end
255
256 local parser = lxp.new(lxp_handlers, ns_separator);
257 for l in file:lines() do
258         parser:parse(l);
259 end
260 parser:parse();
261 parser:close();
262 file:close();