Merge 0.10->trunk
[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 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 he already has 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 he already has 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();