#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- prosody = {}; package.path = package.path ..";../?.lua"; local my_name = arg[0]; if my_name:match("[/\\]") then package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end if not pcall(require, "prosody.loader") then pcall(require, "loader"); end local serialize = require "prosody.util.serialization".serialize; local st = require "prosody.util.stanza"; local parse_xml = require "prosody.util.xml".parse; package.loaded["prosody.util.logger"] = {init = function() return function() end; end} local dm = require "prosody.util.datamanager" dm.set_data_path("data"); function parseFile(filename) ------ local file = nil; local last = nil; local line = 1; local function read(expected) local ch; if last then ch = last; last = nil; else ch = file:read(1); if ch == "\n" then line = line + 1; end end if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end return ch; end local function peek() if not last then last = read(); end return last; end local escapes = { ["\\0"] = "\0"; ["\\'"] = "'"; ["\\\""] = "\""; ["\\b"] = "\b"; ["\\n"] = "\n"; ["\\r"] = "\r"; ["\\t"] = "\t"; ["\\Z"] = "\26"; ["\\\\"] = "\\"; ["\\%"] = "%"; ["\\_"] = "_"; } local function unescape(s) return escapes[s] or error("Unknown escape sequence: "..s); end local function readString() read("'"); local s = ""; while true do local ch = peek(); if ch == "\\" then s = s..unescape(read()..read()); elseif ch == "'" then break; else s = s..read(); end end read("'"); return s; end local function readNonString() local s = ""; while true do if peek() == "," or peek() == ")" then break; else s = s..read(); end end return tonumber(s); end local function readItem() if peek() == "'" then return readString(); else return readNonString(); end end local function readTuple() local items = {} read("("); while peek() ~= ")" do table.insert(items, readItem()); if peek() == ")" then break; end read(","); end read(")"); return items; end local function readTuples() if peek() ~= "(" then read("("); end local tuples = {}; while true do table.insert(tuples, readTuple()); if peek() == "," then read() end if peek() == ";" then break; end end return tuples; end local function readTableName() local tname = ""; while peek() ~= "`" do tname = tname..read(); end return tname; end local function readInsert() if peek() == nil then return nil; end for ch in ("INSERT INTO `"):gmatch(".") do -- find line starting with this if peek() == ch then read(); -- found else -- match failed, skip line while peek() and read() ~= "\n" do end return nil; end end local tname = readTableName(); read("`"); read(" ") -- expect this if peek() == "(" then -- skip column list repeat until read() == ")"; read(" "); end for ch in ("VALUES "):gmatch(".") do read(ch); end -- expect this local tuples = readTuples(); read(";"); read("\n"); return tname, tuples; end local function readFile(filename) file = io.open(filename); if not file then error("File not found: "..filename); os.exit(0); end local t = {}; while true do local tname, tuples = readInsert(); if tname then if t[tname] then local t_name = t[tname]; for i=1,#tuples do table.insert(t_name, tuples[i]); end else t[tname] = tuples; end elseif peek() == nil then break; end end return t; end return readFile(filename); ------ end local arg, hostname = ...; local help = "/? -? ? /h -h /help -help --help"; if not(arg and hostname) or help:find(arg, 1, true) then print([[ejabberd SQL DB dump importer for Prosody Usage: ejabberdsql2prosody.lua filename.txt hostname The file can be generated using mysqldump: mysqldump db_name > filename.txt]]); os.exit(1); end local map = { ["last"] = {"username", "seconds", "state"}; ["privacy_default_list"] = {"username", "name"}; ["privacy_list"] = {"username", "name", "id"}; ["privacy_list_data"] = {"id", "t", "value", "action", "ord", "match_all", "match_iq", "match_message", "match_presence_in", "match_presence_out"}; ["private_storage"] = {"username", "namespace", "data"}; ["rostergroups"] = {"username", "jid", "grp"}; ["rosterusers"] = {"username", "jid", "nick", "subscription", "ask", "askmessage", "server", "subscribe", "type"}; ["spool"] = {"username", "xml", "seq"}; ["users"] = {"username", "password"}; ["vcard"] = {"username", "vcard"}; --["vcard_search"] = {}; } local NULL = {}; local parsed = parseFile(arg); for name, data in pairs(parsed) do local m = map[name]; if m then if #data > 0 and #data[1] ~= #m then print("[warning] expected "..#m.." columns for table `"..name.."`, found "..#data[1]); end for i=1,#data do local row = data[i]; for j=1,#m do row[m[j]] = row[j]; row[j] = nil; end end end end --print(serialize(t)); for _, row in ipairs(parsed["users"] or NULL) do local node, password = row.username, row.password; local ret, err = dm.store(node, hostname, "accounts", {password = password}); print("["..(err or "success").."] accounts: "..node.."@"..hostname); end function roster(node, host, jid, item) local roster = dm.load(node, host, "roster") or {}; roster[jid] = item; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); end function roster_pending(node, host, jid) local roster = dm.load(node, host, "roster") or {}; roster.pending = roster.pending or {}; roster.pending[jid] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster-pending: " ..node.."@"..host.." - "..jid); end function roster_group(node, host, jid, group) local roster = dm.load(node, host, "roster") or {}; local item = roster[jid]; if not item then print("Warning: No roster item "..jid.." for user "..node..", can't put in group "..group); return; end item.groups[group] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster-group: " ..node.."@"..host.." - "..jid.." - "..group); end function private_storage(node, host, xmlns, stanza) local private = dm.load(node, host, "private") or {}; private[stanza.name..":"..xmlns] = st.preserialize(stanza); local ret, err = dm.store(node, host, "private", private); print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); end function offline_msg(node, host, t, stanza) stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza)); print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t)); end for _, row in ipairs(parsed["rosterusers"] or NULL) do local node, contact = row.username, row.jid; local name = row.nick; if name == "" then name = nil; end local subscription = row.subscription; if subscription == "N" then subscription = "none" elseif subscription == "B" then subscription = "both" elseif subscription == "F" then subscription = "from" elseif subscription == "T" then subscription = "to" else error("Unknown subscription type: "..subscription) end; local ask = row.ask; if ask == "N" then ask = nil; elseif ask == "O" then ask = "subscribe"; elseif ask == "I" then roster_pending(node, hostname, contact); ask = nil; elseif ask == "B" then roster_pending(node, hostname, contact); ask = "subscribe"; else error("Unknown ask type: "..ask); end local item = {name = name, ask = ask, subscription = subscription, groups = {}}; roster(node, hostname, contact, item); end for _, row in ipairs(parsed["rostergroups"] or NULL) do roster_group(row.username, hostname, row.jid, row.grp); end for _, row in ipairs(parsed["vcard"] or NULL) do local stanza, err = parse_xml(row.vcard); if stanza then local ret, err = dm.store(row.username, hostname, "vcard", st.preserialize(stanza)); print("["..(err or "success").."] vCard: "..row.username.."@"..hostname); else print("[error] vCard XML parse failed: "..row.username.."@"..hostname); end end for _, row in ipairs(parsed["private_storage"] or NULL) do local stanza, err = parse_xml(row.data); if stanza then private_storage(row.username, hostname, row.namespace, stanza); else print("[error] Private XML parse failed: "..row.username.."@"..hostname); end end table.sort(parsed["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones local date_parse = function(s) local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)"); return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset}); end for _, row in ipairs(parsed["spool"] or NULL) do local stanza, err = parse_xml(row.xml); if stanza then local last_child = stanza.tags[#stanza.tags]; if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end 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 stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil; local t = date_parse(last_child.attr.stamp); offline_msg(row.username, hostname, t, stanza); else print("[error] Offline message XML parsing failed: "..row.username.."@"..hostname); end end