Merge 0.10->trunk
[prosody.git] / tools / ejabberdsql2prosody.lua
1 #!/usr/bin/env lua
2 -- Prosody IM
3 -- Copyright (C) 2008-2010 Matthew Wild
4 -- Copyright (C) 2008-2010 Waqas Hussain
5 --
6 -- This project is MIT/X11 licensed. Please see the
7 -- COPYING file in the source package for more information.
8 --
9
10 prosody = {};
11
12 package.path = package.path ..";../?.lua";
13
14 local my_name = arg[0];
15 if my_name:match("[/\\]") then
16         package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua");
17         package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so");
18 end
19
20
21 local serialize = require "util.serialization".serialize;
22 local st = require "util.stanza";
23 local parse_xml = require "util.xml".parse;
24 package.loaded["util.logger"] = {init = function() return function() end; end}
25 local dm = require "util.datamanager"
26 dm.set_data_path("data");
27
28 function parseFile(filename)
29 ------
30
31 local file = nil;
32 local last = nil;
33 local line = 1;
34 local function read(expected)
35         local ch;
36         if last then
37                 ch = last; last = nil;
38         else
39                 ch = file:read(1);
40                 if ch == "\n" then line = line + 1; end
41         end
42         if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end
43         return ch;
44 end
45 local function pushback(ch)
46         if last then error(); end
47         last = ch;
48 end
49 local function peek()
50         if not last then last = read(); end
51         return last;
52 end
53
54 local escapes = {
55         ["\\0"] = "\0";
56         ["\\'"] = "'";
57         ["\\\""] = "\"";
58         ["\\b"] = "\b";
59         ["\\n"] = "\n";
60         ["\\r"] = "\r";
61         ["\\t"] = "\t";
62         ["\\Z"] = "\26";
63         ["\\\\"] = "\\";
64         ["\\%"] = "%";
65         ["\\_"] = "_";
66 }
67 local function unescape(s)
68         return escapes[s] or error("Unknown escape sequence: "..s);
69 end
70 local function readString()
71         read("'");
72         local s = "";
73         while true do
74                 local ch = peek();
75                 if ch == "\\" then
76                         s = s..unescape(read()..read());
77                 elseif ch == "'" then
78                         break;
79                 else
80                         s = s..read();
81                 end
82         end
83         read("'");
84         return s;
85 end
86 local function readNonString()
87         local s = "";
88         while true do
89                 if peek() == "," or peek() == ")" then
90                         break;
91                 else
92                         s = s..read();
93                 end
94         end
95         return tonumber(s);
96 end
97 local function readItem()
98         if peek() == "'" then
99                 return readString();
100         else
101                 return readNonString();
102         end
103 end
104 local function readTuple()
105         local items = {}
106         read("(");
107         while peek() ~= ")" do
108                 table.insert(items, readItem());
109                 if peek() == ")" then break; end
110                 read(",");
111         end
112         read(")");
113         return items;
114 end
115 local function readTuples()
116         if peek() ~= "(" then read("("); end
117         local tuples = {};
118         while true do
119                 table.insert(tuples, readTuple());
120                 if peek() == "," then read() end
121                 if peek() == ";" then break; end
122         end
123         return tuples;
124 end
125 local function readTableName()
126         local tname = "";
127         while peek() ~= "`" do tname = tname..read(); end
128         return tname;
129 end
130 local function readInsert()
131         if peek() == nil then return nil; end
132         for ch in ("INSERT INTO `"):gmatch(".") do -- find line starting with this
133                 if peek() == ch then
134                         read(); -- found
135                 else -- match failed, skip line
136                         while peek() and read() ~= "\n" do end
137                         return nil;
138                 end
139         end
140         local tname = readTableName();
141         read("`"); read(" ") -- expect this
142         if peek() == "(" then -- skip column list
143                 repeat until read() == ")";
144                 read(" ");
145         end
146         for ch in ("VALUES "):gmatch(".") do read(ch); end -- expect this
147         local tuples = readTuples();
148         read(";"); read("\n");
149         return tname, tuples;
150 end
151
152 local function readFile(filename)
153         file = io.open(filename);
154         if not file then error("File not found: "..filename); os.exit(0); end
155         local t = {};
156         while true do
157                 local tname, tuples = readInsert();
158                 if tname then
159                         if t[tname] then
160                                 local t_name = t[tname];
161                                 for i=1,#tuples do
162                                         table.insert(t_name, tuples[i]);
163                                 end
164                         else
165                                 t[tname] = tuples;
166                         end
167                 elseif peek() == nil then
168                         break;
169                 end
170         end
171         return t;
172 end
173
174 return readFile(filename);
175
176 ------
177 end
178
179 local arg, host = ...;
180 local help = "/? -? ? /h -h /help -help --help";
181 if not(arg and host) or help:find(arg, 1, true) then
182         print([[ejabberd SQL DB dump importer for Prosody
183
184   Usage: ejabberdsql2prosody.lua filename.txt hostname
185
186 The file can be generated using mysqldump:
187   mysqldump db_name > filename.txt]]);
188         os.exit(1);
189 end
190 local map = {
191         ["last"] = {"username", "seconds", "state"};
192         ["privacy_default_list"] = {"username", "name"};
193         ["privacy_list"] = {"username", "name", "id"};
194         ["privacy_list_data"] = {"id", "t", "value", "action", "ord", "match_all", "match_iq", "match_message", "match_presence_in", "match_presence_out"};
195         ["private_storage"] = {"username", "namespace", "data"};
196         ["rostergroups"] = {"username", "jid", "grp"};
197         ["rosterusers"] = {"username", "jid", "nick", "subscription", "ask", "askmessage", "server", "subscribe", "type"};
198         ["spool"] = {"username", "xml", "seq"};
199         ["users"] = {"username", "password"};
200         ["vcard"] = {"username", "vcard"};
201         --["vcard_search"] = {};
202 }
203 local NULL = {};
204 local t = parseFile(arg);
205 for name, data in pairs(t) do
206         local m = map[name];
207         if m then
208                 if #data > 0 and #data[1] ~= #m then
209                         print("[warning] expected "..#m.." columns for table `"..name.."`, found "..#data[1]);
210                 end
211                 for i=1,#data do
212                         local row = data[i];
213                         for j=1,#m do
214                                 row[m[j]] = row[j];
215                                 row[j] = nil;
216                         end
217                 end
218         end
219 end
220 --print(serialize(t));
221
222 for i, row in ipairs(t["users"] or NULL) do
223         local node, password = row.username, row.password;
224         local ret, err = dm.store(node, host, "accounts", {password = password});
225         print("["..(err or "success").."] accounts: "..node.."@"..host);
226 end
227
228 function roster(node, host, jid, item)
229         local roster = dm.load(node, host, "roster") or {};
230         roster[jid] = item;
231         local ret, err = dm.store(node, host, "roster", roster);
232         print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid);
233 end
234 function roster_pending(node, host, jid)
235         local roster = dm.load(node, host, "roster") or {};
236         roster.pending = roster.pending or {};
237         roster.pending[jid] = true;
238         local ret, err = dm.store(node, host, "roster", roster);
239         print("["..(err or "success").."] roster-pending: " ..node.."@"..host.." - "..jid);
240 end
241 function roster_group(node, host, jid, group)
242         local roster = dm.load(node, host, "roster") or {};
243         local item = roster[jid];
244         if not item then print("Warning: No roster item "..jid.." for user "..node..", can't put in group "..group); return; end
245         item.groups[group] = true;
246         local ret, err = dm.store(node, host, "roster", roster);
247         print("["..(err or "success").."] roster-group: " ..node.."@"..host.." - "..jid.." - "..group);
248 end
249 function private_storage(node, host, xmlns, stanza)
250         local private = dm.load(node, host, "private") or {};
251         private[stanza.name..":"..xmlns] = st.preserialize(stanza);
252         local ret, err = dm.store(node, host, "private", private);
253         print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns);
254 end
255 function offline_msg(node, host, t, stanza)
256         stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t);
257         stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t);
258         local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza));
259         print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t));
260 end
261 for i, row in ipairs(t["rosterusers"] or NULL) do
262         local node, contact = row.username, row.jid;
263         local name = row.nick;
264         if name == "" then name = nil; end
265         local subscription = row.subscription;
266         if subscription == "N" then
267                 subscription = "none"
268         elseif subscription == "B" then
269                 subscription = "both"
270         elseif subscription == "F" then
271                 subscription = "from"
272         elseif subscription == "T" then
273                 subscription = "to"
274         else error("Unknown subscription type: "..subscription) end;
275         local ask = row.ask;
276         if ask == "N" then
277                 ask = nil;
278         elseif ask == "O" then
279                 ask = "subscribe";
280         elseif ask == "I" then
281                 roster_pending(node, host, contact);
282                 ask = nil;
283         elseif ask == "B" then
284                 roster_pending(node, host, contact);
285                 ask = "subscribe";
286         else error("Unknown ask type: "..ask); end
287         local item = {name = name, ask = ask, subscription = subscription, groups = {}};
288         roster(node, host, contact, item);
289 end
290 for i, row in ipairs(t["rostergroups"] or NULL) do
291         roster_group(row.username, host, row.jid, row.grp);
292 end
293 for i, row in ipairs(t["vcard"] or NULL) do
294         local stanza, err = parse_xml(row.vcard);
295         if stanza then
296                 local ret, err = dm.store(row.username, host, "vcard", st.preserialize(stanza));
297                 print("["..(err or "success").."] vCard: "..row.username.."@"..host);
298         else
299                 print("[error] vCard XML parse failed: "..row.username.."@"..host);
300         end
301 end
302 for i, row in ipairs(t["private_storage"] or NULL) do
303         local stanza, err = parse_xml(row.data);
304         if stanza then
305                 private_storage(row.username, host, row.namespace, stanza);
306         else
307                 print("[error] Private XML parse failed: "..row.username.."@"..host);
308         end
309 end
310 table.sort(t["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case
311 local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones
312 local date_parse = function(s)
313         local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)");
314         return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset});
315 end
316 for i, row in ipairs(t["spool"] or NULL) do
317         local stanza, err = parse_xml(row.xml);
318         if stanza then
319                 local last_child = stanza.tags[#stanza.tags];
320                 if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end
321                 if last_child.name ~= "x" and last_child.attr.xmlns ~= "jabber:x:delay" then error("Last child of offline message is not a timestamp"); end
322                 stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil;
323                 local t = date_parse(last_child.attr.stamp);
324                 offline_msg(row.username, host, t, stanza);
325         else
326                 print("[error] Offline message XML parsing failed: "..row.username.."@"..host);
327         end
328 end