diff options
Diffstat (limited to 'tools')
-rwxr-xr-x | tools/ejabberd2prosody.lua | 249 | ||||
-rw-r--r-- | tools/ejabberdsql2prosody.lua | 306 | ||||
-rw-r--r-- | tools/erlparse.lua | 198 | ||||
-rw-r--r-- | tools/jabberd14sql2prosody.lua | 647 | ||||
-rw-r--r-- | tools/migration/Makefile | 39 | ||||
-rw-r--r-- | tools/migration/migrator.cfg.lua | 26 | ||||
-rw-r--r-- | tools/migration/migrator/jabberd14.lua | 142 | ||||
-rw-r--r-- | tools/migration/migrator/mtools.lua | 56 | ||||
-rw-r--r-- | tools/migration/migrator/prosody_files.lua | 138 | ||||
-rw-r--r-- | tools/migration/migrator/prosody_sql.lua | 200 | ||||
-rw-r--r-- | tools/migration/prosody-migrator.lua | 132 | ||||
-rw-r--r-- | tools/openfire2prosody.lua | 103 | ||||
-rwxr-xr-x | tools/xep227toprosody.lua | 263 |
13 files changed, 2499 insertions, 0 deletions
diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua new file mode 100755 index 00000000..c11e41d9 --- /dev/null +++ b/tools/ejabberd2prosody.lua @@ -0,0 +1,249 @@ +#!/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. +-- + + + +package.path = package.path ..";../?.lua"; + +if arg[0]:match("[/\\]") then + package.path = package.path .. ";"..arg[0]:gsub("[^/\\]*$", "?.lua"); +end + +local erlparse = require "erlparse"; + +prosody = {}; + +package.loaded["util.logger"] = {init = function() return function() end; end} +local serialize = require "util.serialization".serialize; +local st = require "util.stanza"; +local dm = require "util.datamanager" +dm.set_data_path("data"); + +function build_stanza(tuple, stanza) + assert(type(tuple) == "table", "XML node is of unexpected type: "..type(tuple)); + if tuple[1] == "xmlelement" then + assert(type(tuple[2]) == "string", "element name has type: "..type(tuple[2])); + assert(type(tuple[3]) == "table", "element attribute array has type: "..type(tuple[3])); + assert(type(tuple[4]) == "table", "element children array has type: "..type(tuple[4])); + local name = tuple[2]; + local attr = {}; + for _, a in ipairs(tuple[3]) do + if type(a[1]) == "string" and type(a[2]) == "string" then attr[a[1]] = a[2]; end + end + local up; + if stanza then stanza:tag(name, attr); up = true; else stanza = st.stanza(name, attr); end + for _, a in ipairs(tuple[4]) do build_stanza(a, stanza); end + if up then stanza:up(); else return stanza end + elseif tuple[1] == "xmlcdata" then + assert(type(tuple[2]) == "string", "XML CDATA has unexpected type: "..type(tuple[2])); + stanza:text(tuple[2]); + else + error("unknown element type: "..serialize(tuple)); + end +end +function build_time(tuple) + local Megaseconds,Seconds,Microseconds = unpack(tuple); + return Megaseconds * 1000000 + Seconds; +end + +function vcard(node, host, stanza) + local ret, err = dm.store(node, host, "vcard", st.preserialize(stanza)); + print("["..(err or "success").."] vCard: "..node.."@"..host); +end +function password(node, host, password) + local ret, err = dm.store(node, host, "accounts", {password = password}); + print("["..(err or "success").."] accounts: "..node.."@"..host); +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: " ..node.."@"..host.." - "..jid); +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 +function privacy(node, host, default, lists) + local privacy = { lists = {} }; + local count = 0; + if default then privacy.default = default; end + for _, inlist in ipairs(lists) do + local name, items = inlist[1], inlist[2]; + local list = { name = name; items = {}; }; + local orders = {}; + for _, item in pairs(items) do + repeat + if item[1] ~= "listitem" then print("[error] privacy: unhandled item: "..tostring(item[1])); break; end + local _type, value = item[2], item[3]; + if _type == "jid" then + if type(value) ~= "table" then print("[error] privacy: jid value is not valid: "..tostring(value)); break; end + local _node, _host, _resource = value[1], value[2], value[3]; + if (type(_node) == "table") then _node = nil; end + if (type(_host) == "table") then _host = nil; end + if (type(_resource) == "table") then _resource = nil; end + value = (_node and _node.."@".._host or _host)..(_resource and "/".._resource or ""); + elseif _type == "none" then + _type = nil; + value = nil; + elseif _type == "group" then + if type(value) ~= "string" then print("[error] privacy: group value is not string: "..tostring(value)); break; end + elseif _type == "subscription" then + if value~="both" and value~="from" and value~="to" and value~="none" then + print("[error] privacy: subscription value is invalid: "..tostring(value)); break; + end + else print("[error] privacy: invalid item type: "..tostring(_type)); break; end + local action = item[4]; + if action ~= "allow" and action ~= "deny" then print("[error] privacy: unhandled action: "..tostring(action)); break; end + local order = item[5]; + if type(order) ~= "number" or order<0 then print("[error] privacy: order is not numeric: "..tostring(order)); break; end + if orders[order] then print("[error] privacy: duplicate order value: "..tostring(order)); break; end + orders[order] = true; + local match_all = item[6]; + local match_iq = item[7]; + local match_message = item[8]; + local match_presence_in = item[9]; + local match_presence_out = item[10]; + list.items[#list.items+1] = { + type = _type; + value = value; + action = action; + order = order; + message = match_message == "true"; + iq = match_iq == "true"; + ["presence-in"] = match_presence_in == "true"; + ["presence-out"] = match_presence_out == "true"; + }; + until true; + end + table.sort(list.items, function(a, b) return a.order < b.order; end); + if privacy.lists[list.name] then print("[warn] duplicate privacy list: "..tostring(list.name)); end + privacy.lists[list.name] = list; + count = count + 1; + end + if default and not privacy.lists[default] then + if default == "none" then privacy.default = nil; + else print("[warn] default privacy list doesn't exist: "..tostring(default)); end + end + local ret, err = dm.store(node, host, "privacy", privacy); + print("["..(err or "success").."] privacy: " ..node.."@"..host.." - "..count.." list(s)"); +end + + +local filters = { + passwd = function(tuple) + password(tuple[2][1], tuple[2][2], tuple[3]); + end; + vcard = function(tuple) + vcard(tuple[2][1], tuple[2][2], build_stanza(tuple[3])); + end; + roster = function(tuple) + local node = tuple[3][1]; local host = tuple[3][2]; + local contact = (type(tuple[4][1]) == "table") and tuple[4][2] or tuple[4][1].."@"..tuple[4][2]; + local name = tuple[5]; local subscription = tuple[6]; + local ask = tuple[7]; local groups = tuple[8]; + if type(name) ~= type("") then name = nil; end + if ask == "none" then + ask = nil; + elseif ask == "out" then + ask = "subscribe" + elseif ask == "in" then + roster_pending(node, host, contact); + ask = nil; + elseif ask == "both" then + roster_pending(node, host, contact); + ask = "subscribe"; + else error("Unknown ask type: "..ask); end + if subscription ~= "both" and subscription ~= "from" and subscription ~= "to" and subscription ~= "none" then error(subscription) end + local item = {name = name, ask = ask, subscription = subscription, groups = {}}; + for _, g in ipairs(groups) do + if type(g) == "string" then + item.groups[g] = true; + end + end + roster(node, host, contact, item); + end; + private_storage = function(tuple) + private_storage(tuple[2][1], tuple[2][2], tuple[2][3], build_stanza(tuple[3])); + end; + offline_msg = function(tuple) + offline_msg(tuple[2][1], tuple[2][2], build_time(tuple[3]), build_stanza(tuple[7])); + end; + privacy = function(tuple) + privacy(tuple[2][1], tuple[2][2], tuple[3], tuple[4]); + end; + config = function(tuple) + if tuple[2] == "hosts" then + local output = io.output(); io.output("prosody.cfg.lua"); + io.write("-- Configuration imported from ejabberd --\n"); + io.write([[Host "*" + modules_enabled = { + "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. + "legacyauth"; -- Legacy authentication. Only used by some old clients and bots. + "roster"; -- Allow users to have a roster. Recommended ;) + "register"; -- Allow users to register on this server using a client + "tls"; -- Add support for secure TLS on c2s/s2s connections + "vcard"; -- Allow users to set vCards + "private"; -- Private XML storage (for room bookmarks, etc.) + "version"; -- Replies to server version requests + "dialback"; -- s2s dialback support + "uptime"; + "disco"; + "time"; + "ping"; + --"selftests"; + }; +]]); + for _, h in ipairs(tuple[3]) do + io.write("Host \"" .. h .. "\"\n"); + end + io.output(output); + print("prosody.cfg.lua created"); + end + end; +}; + +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[ejabberd db dump importer for Prosody + + Usage: ejabberd2prosody.lua filename.txt + +The file can be generated from ejabberd using: + sudo ./bin/ejabberdctl dump filename.txt + +Note: The path of ejabberdctl depends on your ejabberd installation, and ejabberd needs to be running for ejabberdctl to work.]]); + os.exit(1); +end +local count = 0; +local t = {}; +for item in erlparse.parseFile(arg) do + count = count + 1; + local name = item[1]; + t[name] = (t[name] or 0) + 1; + --print(count, serialize(item)); + if filters[name] then filters[name](item); end +end +--print(serialize(t)); diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua new file mode 100644 index 00000000..43720643 --- /dev/null +++ b/tools/ejabberdsql2prosody.lua @@ -0,0 +1,306 @@ +#!/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 serialize = require "util.serialization".serialize; +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; +package.loaded["util.logger"] = {init = function() return function() end; end} +local dm = require "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 pushback(ch) + if last then error(); end + last = 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, host = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not(arg and host) 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 t = parseFile(arg); +for name, data in pairs(t) 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 i, row in ipairs(t["users"] or NULL) do + local node, password = row.username, row.password; + local ret, err = dm.store(node, host, "accounts", {password = password}); + print("["..(err or "success").."] accounts: "..node.."@"..host); +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 i, row in ipairs(t["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, host, contact); + ask = nil; + elseif ask == "B" then + roster_pending(node, host, contact); + ask = "subscribe"; + else error("Unknown ask type: "..ask); end + local item = {name = name, ask = ask, subscription = subscription, groups = {}}; + roster(node, host, contact, item); +end +for i, row in ipairs(t["rostergroups"] or NULL) do + roster_group(row.username, host, row.jid, row.grp); +end +for i, row in ipairs(t["vcard"] or NULL) do + local ret, err = dm.store(row.username, host, "vcard", st.preserialize(parse_xml(row.vcard))); + print("["..(err or "success").."] vCard: "..row.username.."@"..host); +end +for i, row in ipairs(t["private_storage"] or NULL) do + private_storage(row.username, host, row.namespace, parse_xml(row.data)); +end +table.sort(t["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 i, row in ipairs(t["spool"] or NULL) do + local stanza = parse_xml(row.xml); + 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, host, t, stanza); +end diff --git a/tools/erlparse.lua b/tools/erlparse.lua new file mode 100644 index 00000000..174585d3 --- /dev/null +++ b/tools/erlparse.lua @@ -0,0 +1,198 @@ +-- 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. +-- + +local string_byte, string_char = string.byte, string.char; +local t_concat, t_insert = table.concat, table.insert; +local type, tonumber, tostring = type, tonumber, tostring; + +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 pushback(ch) + if last then error(); end + last = ch; +end +local function peek() + if not last then last = read(); end + return last; +end + +local _A, _a, _Z, _z, _0, _9, __, _at, _space, _minus = string_byte("AaZz09@_ -", 1, 10); +local function isLowerAlpha(ch) + ch = string_byte(ch) or 0; + return (ch >= _a and ch <= _z); +end +local function isNumeric(ch) + ch = string_byte(ch) or 0; + return (ch >= _0 and ch <= _9) or ch == _minus; +end +local function isAtom(ch) + ch = string_byte(ch) or 0; + return (ch >= _A and ch <= _Z) or (ch >= _a and ch <= _z) or (ch >= _0 and ch <= _9) or ch == __ or ch == _at; +end +local function isSpace(ch) + ch = string_byte(ch) or "x"; + return ch <= _space; +end + +local escapes = {["\\b"]="\b", ["\\d"]="\127", ["\\e"]="\27", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]=" ", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"}; +local function readString() + read("\""); -- skip quote + local slash = nil; + local str = {}; + while true do + local ch = read(); + if slash then + slash = slash..ch; + if not escapes[slash] then error("Unknown escape sequence: "..slash); end + str[#str+1] = escapes[slash]; + slash = nil; + elseif ch == "\"" then + break; + elseif ch == "\\" then + slash = ch; + else + str[#str+1] = ch; + end + end + return t_concat(str); +end +local function readAtom1() + local var = { read() }; + while isAtom(peek()) do + var[#var+1] = read(); + end + return t_concat(var); +end +local function readAtom2() + local str = { read("'") }; + local slash = nil; + while true do + local ch = read(); + str[#str+1] = ch; + if ch == "'" and not slash then break; end + end + return t_concat(str); +end +local function readNumber() + local num = { read() }; + while isNumeric(peek()) do + num[#num+1] = read(); + end + if peek() == "." then + num[#num+1] = read(); + while isNumeric(peek()) do + num[#num+1] = read(); + end + end + return tonumber(t_concat(num)); +end +local readItem = nil; +local function readTuple() + local t = {}; + local s = {}; -- string representation + read(); -- read {, or [, or < + while true do + local item = readItem(); + if not item then break; end + if type(item) ~= "number" or item > 255 then + s = nil; + elseif s then + s[#s+1] = string_char(item); + end + t_insert(t, item); + end + read(); -- read }, or ], or > + if s and #s > 0 then + return t_concat(s) + else + return t + end; +end +local function readBinary() + read("<"); -- read < + -- Discard PIDs + if isNumeric(peek()) then + while peek() ~= ">" do read(); end + read(">"); + return {}; + end + local t = readTuple(); + read(">") -- read > + local ch = peek(); + if type(t) == "string" then + -- binary is a list of integers + return t; + elseif type(t) == "table" then + if t[1] then + -- binary contains string + return t[1]; + else + -- binary is empty + return ""; + end; + else + error(); + end +end +readItem = function() + local ch = peek(); + if ch == nil then return nil end + if ch == "{" or ch == "[" then + return readTuple(); + elseif isLowerAlpha(ch) then + return readAtom1(); + elseif ch == "'" then + return readAtom2(); + elseif isNumeric(ch) then + return readNumber(); + elseif ch == "\"" then + return readString(); + elseif ch == "<" then + return readBinary(); + elseif isSpace(ch) or ch == "," or ch == "|" then + read(); + return readItem(); + else + --print("Unknown char: "..ch); + return nil; + end +end +local function readChunk() + local x = readItem(); + if x then read("."); end + return x; +end +local function readFile(filename) + file = io.open(filename); + if not file then error("File not found: "..filename); os.exit(0); end + return function() + local x = readChunk(); + if not x and peek() then error("Invalid char: "..peek()); end + return x; + end; +end + +module "erlparse" + +function parseFile(file) + return readFile(file); +end + +return _M; diff --git a/tools/jabberd14sql2prosody.lua b/tools/jabberd14sql2prosody.lua new file mode 100644 index 00000000..b85d2c20 --- /dev/null +++ b/tools/jabberd14sql2prosody.lua @@ -0,0 +1,647 @@ +#!/usr/bin/env lua + + +do + + +local _parse_sql_actions = { [0] = + 0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13, + 2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0, + 3, 8, 3, 0, 3, 12, 4, 0, 2, 3, 7, 4, 0, 3, 8, 11 +}; + +local _parse_sql_trans_keys = { [0] = + 0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82, + 69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65, + 65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69, + 9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, + 10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42, + 42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69, + 32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84, + 32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, + 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40, + 10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32, + 96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77, + 77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69, + 89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69, + 32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10, + 59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65, + 66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32, + 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, + 32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83, + 69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84, + 79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, + 86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85, + 69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92, + 41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41, + 57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67, + 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, + 83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87, + 87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84, + 32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67, + 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, + 69, 83, 83, 69, 69, 9, 85, 0 +}; + +local _parse_sql_key_spans = { [0] = + 0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1, + 39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1, + 1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, + 1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 50, 50, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 77 +}; + +local _parse_sql_index_offsets = { [0] = + 0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121, + 123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684, + 686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919, + 921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161, + 1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471, + 1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683, + 1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972, + 1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411, + 2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623, + 2625, 2627, 2678, 2729, 2736, 2738, 2740, 2742, 2744, 2746, 2748, 2750, 2752, 2754, 2756, 2758, 2760 +}; + +local _parse_sql_indicies = { [0] = + 0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, + 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7, + 1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20, + 1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, + 1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, + 1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38, + 1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1, + 43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52, + 1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48, + 1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, + 62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68, + 1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1, + 1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77, + 1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91, + 1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73, + 1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, + 100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, + 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, + 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, + 71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118, + 1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128, + 1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, + 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6, + 1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142, + 1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, + 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, + 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149, + 147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151, + 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, + 151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1, + 162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, + 165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, + 167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, + 171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1, + 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, + 1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179, + 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, + 1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184, + 1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192, + 1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1, + 199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, + 199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132, + 1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, + 209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1, + 216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1, + 6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227, + 1, 1, 1, 1, 228, 1, 1, 229, 1, 1, 1, 1, 1, 1, 230, 1, 231, 1, 0 +}; + +local _parse_sql_trans_targs = { [0] = + 2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18, + 19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34, + 34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67, + 68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, + 90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, + 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125, + 126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141, + 142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151, + 148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, + 171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190, + 191, 192, 193, 194, 1, 3, 6, 94, 118, 158, 178, 183 +}; + +local _parse_sql_trans_actions = { [0] = + 1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, + 1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1, + 51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +local parse_sql_start = 196; +local parse_sql_first_final = 196; +local parse_sql_error = 0; + +local parse_sql_en_main = 196; + + + + +local _sql_unescapes = setmetatable({ + ["\\0"] = "\0"; + ["\\'"] = "'"; + ["\\\""] = "\""; + ["\\b"] = "\b"; + ["\\n"] = "\n"; + ["\\r"] = "\r"; + ["\\t"] = "\t"; + ["\\Z"] = "\26"; + ["\\\\"] = "\\"; + ["\\%"] = "%"; + ["\\_"] = "_"; +},{ __index = function(t, s) assert(false, "Unknown escape sequences: "..s); end }); + +function parse_sql(data, h) + local p = 1; + local pe = #data + 1; + local cs; + + local pos_char, pos_line = 1, 1; + + local mark, token; + local table_name, columns, value_lists, value_list, value_count; + + + cs = parse_sql_start; + +-- ragel flat exec + + local testEof = false; + local _slen = 0; + local _trans = 0; + local _keys = 0; + local _inds = 0; + local _acts = 0; + local _nacts = 0; + local _tempval = 0; + local _goto_level = 0; + local _resume = 10; + local _eof_trans = 15; + local _again = 20; + local _test_eof = 30; + local _out = 40; + + while true do -- goto loop + local _continue = false; + repeat + local _trigger_goto = false; + if _goto_level <= 0 then + +-- noEnd + if p == pe then + _goto_level = _test_eof; + _continue = true; break; + end + + +-- errState != 0 + if cs == 0 then + _goto_level = _out; + _continue = true; break; + end + end -- _goto_level <= 0 + + if _goto_level <= _resume then + _keys = cs * 2; -- LOCATE_TRANS + _inds = _parse_sql_index_offsets[cs]; + _slen = _parse_sql_key_spans[cs]; + + if _slen > 0 and + _parse_sql_trans_keys[_keys] <= data:byte(p) and + data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then + _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ]; + else _trans =_parse_sql_indicies[ _inds + _slen ]; end + + cs = _parse_sql_trans_targs[_trans]; + + if _parse_sql_trans_actions[_trans] ~= 0 then + _acts = _parse_sql_trans_actions[_trans]; + _nacts = _parse_sql_actions[_acts]; + _acts = _acts + 1; + + while _nacts > 0 do + _nacts = _nacts - 1; + _acts = _acts + 1; + _tempval = _parse_sql_actions[_acts - 1]; + + -- start action switch + if _tempval == 0 then --4 FROM_STATE_ACTION_SWITCH +-- line 34 "sql.rl" -- end of line directive + pos_char = pos_char + 1; -- ACTION + elseif _tempval == 1 then --4 FROM_STATE_ACTION_SWITCH +-- line 35 "sql.rl" -- end of line directive + pos_line = pos_line + 1; pos_char = 1; -- ACTION + elseif _tempval == 2 then --4 FROM_STATE_ACTION_SWITCH +-- line 38 "sql.rl" -- end of line directive + mark = p; -- ACTION + elseif _tempval == 3 then --4 FROM_STATE_ACTION_SWITCH +-- line 39 "sql.rl" -- end of line directive + token = data:sub(mark, p-1); -- ACTION + elseif _tempval == 4 then --4 FROM_STATE_ACTION_SWITCH +-- line 52 "sql.rl" -- end of line directive + table.insert(columns, token); columns[#columns] = token; -- ACTION + elseif _tempval == 5 then --4 FROM_STATE_ACTION_SWITCH +-- line 58 "sql.rl" -- end of line directive + table_name,columns = token,{}; -- ACTION + elseif _tempval == 6 then --4 FROM_STATE_ACTION_SWITCH +-- line 59 "sql.rl" -- end of line directive + h.create(table_name, columns); -- ACTION + elseif _tempval == 7 then --4 FROM_STATE_ACTION_SWITCH +-- line 65 "sql.rl" -- end of line directive + + value_count = value_count + 1; value_list[value_count] = token:gsub("\\.", _sql_unescapes); + -- ACTION + elseif _tempval == 8 then --4 FROM_STATE_ACTION_SWITCH +-- line 68 "sql.rl" -- end of line directive + value_count = value_count + 1; value_list[value_count] = tonumber(token); -- ACTION + elseif _tempval == 9 then --4 FROM_STATE_ACTION_SWITCH +-- line 69 "sql.rl" -- end of line directive + value_count = value_count + 1; -- ACTION + elseif _tempval == 10 then --4 FROM_STATE_ACTION_SWITCH +-- line 71 "sql.rl" -- end of line directive + value_list,value_count = {},0; -- ACTION + elseif _tempval == 11 then --4 FROM_STATE_ACTION_SWITCH +-- line 71 "sql.rl" -- end of line directive + table.insert(value_lists, value_list); -- ACTION + elseif _tempval == 12 then --4 FROM_STATE_ACTION_SWITCH +-- line 74 "sql.rl" -- end of line directive + table_name,value_lists = token,{}; -- ACTION + elseif _tempval == 13 then --4 FROM_STATE_ACTION_SWITCH +-- line 75 "sql.rl" -- end of line directive + h.insert(table_name, value_lists); -- ACTION + end +-- line 355 "sql.lua" -- end of line directive + -- end action switch + end -- while _nacts + end + + if _trigger_goto then _continue = true; break; end + end -- endif + + if _goto_level <= _again then + if cs == 0 then + _goto_level = _out; + _continue = true; break; + end + p = p + 1; + if p ~= pe then + _goto_level = _resume; + _continue = true; break; + end + end -- _goto_level <= _again + + if _goto_level <= _test_eof then + end -- _goto_level <= _test_eof + + if _goto_level <= _out then break; end + _continue = true; + until true; + if not _continue then break; end + end -- endif _goto_level <= out + + -- end of execute block + + + if cs < parse_sql_first_final then + print("parse_sql: there was an error, line "..pos_line.." column "..pos_char); + else + print("Success. EOF at line "..pos_line.." column "..pos_char) + end +end + +end + +-- import modules +package.path = [[C:\Documents and Settings\Waqas\Desktop\mercurial\prosody-hg\?.lua;]]..package.path; + +-- ugly workaround for getting datamanager to work outside of prosody :( +prosody = { }; +prosody.platform = "unknown"; +if os.getenv("WINDIR") then + prosody.platform = "windows"; +elseif package.config:sub(1,1) == "/" then + prosody.platform = "_posix"; +end +package.loaded["util.logger"] = {init = function() return function() end; end} + +local dm = require "util.datamanager"; +dm.set_data_path("data"); + +local datetime = require "util.datetime"; + +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; + +function store_password(username, host, password) + -- create or update account for username@host + local ret, err = dm.store(username, host, "accounts", {password = password}); + print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); +end + +function store_vcard(username, host, stanza) + -- create or update vCard for username@host + local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza)); + print("["..(err or "success").."] stored vCard: "..username.."@"..host); +end + +function store_roster(username, host, roster_items) + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + -- merge imported roster-items with loaded roster + for item_tag in roster_items:childtags() do + -- jid for this roster-item + local item_jid = item_tag.attr.jid + -- validate item stanzas + if (item_tag.name == "item") and (item_jid ~= "") then + -- prepare roster item + -- TODO: is the subscription attribute optional? + local item = {subscription = item_tag.attr.subscription, groups = {}}; + -- optional: give roster item a real name + if item_tag.attr.name then + item.name = item_tag.attr.name; + end + -- optional: iterate over group stanzas inside item stanza + for group_tag in item_tag:childtags() do + local group_name = group_tag:get_text(); + if (group_tag.name == "group") and (group_name ~= "") then + item.groups[group_name] = true; + else + print("[error] invalid group stanza: "..group_tag:pretty_print()); + end + end + -- store item in roster + roster[item_jid] = item; + print("[success] roster entry: " ..username.."@"..host.." - "..item_jid); + else + print("[error] invalid roster stanza: " ..item_tag:pretty_print()); + end + + end + -- store merged roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored roster: " ..username.."@"..host); +end + +function store_subscription_request(username, host, presence_stanza) + local from_bare = presence_stanza.attr.from; + + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + + local item = roster[from_bare]; + if item and (item.subscription == "from" or item.subscription == "both") then + return; -- already subscribed, do nothing + end + + -- add to table of pending subscriptions + if not roster.pending then roster.pending = {}; end + roster.pending[from_bare] = true; + + -- store updated roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare); +end + +local os_date = os.date; +local os_time = os.time; +local os_difftime = os.difftime; +function datetime_parse(s) + if s then + local year, month, day, hour, min, sec, tzd; + year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); + if year then + local time_offset = os_difftime(os_time(os_date("*t")), os_time(os_date("!*t"))); -- to deal with local timezone + local tzd_offset = 0; + if tzd ~= "" and tzd ~= "Z" then + local sign, h, m = tzd:match("([+%-])(%d%d):?(%d*)"); + if not sign then return; end + if #m ~= 2 then m = "0"; end + h, m = tonumber(h), tonumber(m); + tzd_offset = h * 60 * 60 + m * 60; + if sign == "-" then tzd_offset = -tzd_offset; end + end + sec = (sec + time_offset) - tzd_offset; + return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false}); + end + end +end + +function store_offline_messages(username, host, stanza) + -- TODO: maybe use list_load(), append and list_store() instead + -- of constantly reopening the file with list_append()? + --for ch in offline_messages:childtags() do + --print("message :"..ch:pretty_print()); + stanza.attr.node = nil; + + local stamp = stanza:get_child("x", "jabber:x:delay"); + if not stamp or not stamp.attr.stamp then print(2) return; end + + for i=1,#stanza do if stanza[i] == stamp then table.remove(stanza, i); break; end end + for i=1,#stanza.tags do if stanza.tags[i] == stamp then table.remove(stanza.tags, i); break; end end + + local parsed_stamp = datetime_parse(stamp.attr.stamp); + if not parsed_stamp then print(1, stamp.attr.stamp) return; end + + stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(parsed_stamp), datetime.legacy(parsed_stamp); + local ret, err = dm.list_append(username, host, "offline", st.preserialize(stanza)); + print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..stanza.attr.from); + --end +end + +-- load data +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[XEP-227 importer for Prosody + + Usage: jabberd14sql2prosody.lua filename.sql +]]); + os.exit(1); +end +local f = io.open(arg); +local s = f:read("*a"); +f:close(); + +local table_count = 0; +local insert_count = 0; +local row_count = 0; +-- parse +parse_sql(s, { + create = function(table_name, columns) + --[[print(table_name);]] + table_count = table_count + 1; + end; + insert = function(table_name, value_lists) + --[[print(table_name, #value_lists);]] + insert_count = insert_count + 1; + row_count = row_count + #value_lists; + + for _,value_list in ipairs(value_lists) do + if table_name == "users" then + local user, realm, password = unpack(value_list); + store_password(user, realm, password); + elseif table_name == "roster" then + local user, realm, xml = unpack(value_list); + local stanza,err = parse_xml(xml); + if stanza then + store_roster(user, realm, stanza); + else + print("[error] roster: XML parsing failed for "..user.."@"..realm..": "..err); + end + elseif table_name == "vcard" then + local user, realm, name, email, nickname, birthday, photo, xml = unpack(value_list); + if xml then + local stanza,err = parse_xml(xml); + if stanza then + store_vcard(user, realm, stanza); + else + print("[error] vcard: XML parsing failed for "..user.."@"..realm..": "..err); + end + else + --print("[warn] vcard: NULL vCard for "..user.."@"..realm..": "..err); + end + elseif table_name == "storedsubscriptionrequests" then + local user, realm, fromjid, xml = unpack(value_list); + local stanza,err = parse_xml(xml); + if stanza then + store_subscription_request(user, realm, stanza); + else + print("[error] storedsubscriptionrequests: XML parsing failed for "..user.."@"..realm..": "..err); + end + elseif table_name == "messages" then + --local user, realm, node, correspondent, type, storetime, delivertime, subject, body, xml = unpack(value_list); + local user, realm, type, xml = value_list[1], value_list[2], value_list[5], value_list[10]; + if type == "offline" and xml ~= "" then + local stanza,err = parse_xml(xml); + if stanza then + store_offline_messages(user, realm, stanza); + else + print("[error] offline messages: XML parsing failed for "..user.."@"..realm..": "..err); + print(unpack(value_list)); + end + end + end + end + end; +}); + +print("table_count", table_count); +print("insert_count", insert_count); +print("row_count", row_count); + diff --git a/tools/migration/Makefile b/tools/migration/Makefile new file mode 100644 index 00000000..ae402bd2 --- /dev/null +++ b/tools/migration/Makefile @@ -0,0 +1,39 @@ + +include ../../config.unix + +BIN = $(DESTDIR)$(PREFIX)/bin +CONFIG = $(DESTDIR)$(SYSCONFDIR) +SOURCE = $(DESTDIR)$(PREFIX)/lib/prosody +DATA = $(DESTDIR)$(DATADIR) +MAN = $(DESTDIR)$(PREFIX)/share/man + +INSTALLEDSOURCE = $(PREFIX)/lib/prosody +INSTALLEDCONFIG = $(SYSCONFDIR) +INSTALLEDMODULES = $(PREFIX)/lib/prosody/modules +INSTALLEDDATA = $(DATADIR) + +SOURCE_FILES = migrator/*.lua + +all: prosody-migrator.install migrator.cfg.lua.install prosody-migrator.lua $(SOURCE_FILES) + +install: prosody-migrator.install migrator.cfg.lua.install + install -d $(BIN) $(CONFIG) $(SOURCE) $(SOURCE)/migrator + install -d $(MAN)/man1 + install -d $(SOURCE)/migrator + install -m755 ./prosody-migrator.install $(BIN)/prosody-migrator + install -m644 $(SOURCE_FILES) $(SOURCE)/migrator + test -e $(CONFIG)/migrator.cfg.lua || install -m644 migrator.cfg.lua.install $(CONFIG)/migrator.cfg.lua + +clean: + rm -f prosody-migrator.install + rm -f migrator.cfg.lua.install + +prosody-migrator.install: prosody-migrator.lua + sed "1s/\blua\b/$(RUNWITH)/; \ + s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ + s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|;" \ + < prosody-migrator.lua > prosody-migrator.install + +migrator.cfg.lua.install: migrator.cfg.lua + sed "s|^local data_path = .*;$$|local data_path = '$(INSTALLEDDATA)';|;" \ + < migrator.cfg.lua > migrator.cfg.lua.install diff --git a/tools/migration/migrator.cfg.lua b/tools/migration/migrator.cfg.lua new file mode 100644 index 00000000..fa37f2a3 --- /dev/null +++ b/tools/migration/migrator.cfg.lua @@ -0,0 +1,26 @@ +local data_path = "../../data"; + +input { + type = "prosody_files"; + path = data_path; +} + +output { + type = "prosody_sql"; + driver = "SQLite3"; + database = data_path.."/prosody.sqlite"; +} + +--[[ + +input { + type = "prosody_files"; + path = data_path; +} +output { + type = "prosody_sql"; + driver = "SQLite3"; + database = data_path.."/prosody.sqlite"; +} + +]] diff --git a/tools/migration/migrator/jabberd14.lua b/tools/migration/migrator/jabberd14.lua new file mode 100644 index 00000000..2f0b0b78 --- /dev/null +++ b/tools/migration/migrator/jabberd14.lua @@ -0,0 +1,142 @@ + +local lfs = require "lfs"; +local st = require "util.stanza"; +local parse_xml = require "util.xml".parse; +local os_getenv = os.getenv; +local io_open = io.open; +local assert = assert; +local ipairs = ipairs; +local coroutine = coroutine; +local print = print; + +module "jabberd14" + +local function is_dir(path) return lfs.attributes(path, "mode") == "directory"; end +local function is_file(path) return lfs.attributes(path, "mode") == "file"; end +local function clean_path(path) + return path:gsub("\\", "/"):gsub("//+", "/"):gsub("^~", os_getenv("HOME") or "~"); +end + +local function load_xml(path) + local f, err = io_open(path); + if not f then return f, err; end + local data = f:read("*a"); + f:close(); + if not data then return; end + return parse_xml(data); +end + +local function load_spool_file(host, filename, path) + local xml = load_xml(path); + if not xml then return; end + + local register_element = xml:get_child("query", "jabber:iq:register"); + local username_element = register_element and register_element:get_child("username", "jabber:iq:register"); + local password_element = register_element and register_element:get_child("password", "jabber:iq:auth"); + local username = username_element and username_element:get_text(); + local password = password_element and password_element:get_text(); + if not username then + print("[warn] Missing /xdb/{jabber:iq:register}register/username> in file "..filename) + return; + elseif username..".xml" ~= filename then + print("[warn] Missing /xdb/{jabber:iq:register}register/username does not match filename "..filename); + return; + end + + local userdata = { + user = username; + host = host; + stores = {}; + }; + local stores = userdata.stores; + stores.accounts = { password = password }; + + for i=1,#xml.tags do + local tag = xml.tags[i]; + local xname = (tag.attr.xmlns or "")..":"..tag.name; + if tag.attr.j_private_flag == "1" and tag.attr.xmlns then + -- Private XML + stores.private = stores.private or {}; + tag.attr.j_private_flag = nil; + stores.private[tag.attr.xmlns] = st.preserialize(tag); + elseif xname == "jabber:iq:auth:password" then + if stores.accounts.password ~= tag:get_text() then + if password then + print("[warn] conflicting passwords") + else + stores.accounts.password = tag:get_text(); + end + end + elseif xname == "jabber:iq:register:query" then + -- already processed + elseif xname == "jabber:xdb:nslist:foo" then + -- ignore + elseif xname == "jabber:iq:auth:0k:zerok" then + -- ignore + elseif xname == "jabber:iq:roster:query" then + -- Roster + local roster = {}; + local subscription_types = { from = true, to = true, both = true, none = true }; + for _,item_element in ipairs(tag.tags) do + assert(item_element.name == "item"); + assert(item_element.attr.jid); + assert(subscription_types[item_element.attr.subscription]); + assert((item_element.attr.ask or "subscribe") == "subscribe") + if item_element.name == "item" then + local groups = {}; + for _,group_element in ipairs(item_element.tags) do + assert(group_element.name == "group"); + groups[group_element:get_text()] = true; + end + local item = { + name = item_element.attr.name; + subscription = item_element.attr.subscription; + ask = item_element.attr.ask; + groups = groups; + }; + roster[item_element.attr.jid] = item; + end + end + stores.roster = roster; + elseif xname == "jabber:iq:last:query" then + -- Last activity + elseif xname == "jabber:x:offline:foo" then + -- Offline messages + elseif xname == "vcard-temp:vCard" then + -- vCards + stores.vcard = st.preserialize(tag); + else + print("[warn] Unknown tag: "..xname); + end + end + return userdata; +end + +local function loop_over_users(path, host, cb) + for file in lfs.dir(path) do + if file:match("%.xml$") then + local user = load_spool_file(host, file, path.."/"..file); + if user then cb(user); end + end + end +end +local function loop_over_hosts(path, cb) + for host in lfs.dir(path) do + if host ~= "." and host ~= ".." and is_dir(path.."/"..host) then + loop_over_users(path.."/"..host, host, cb); + end + end +end + +function reader(input) + local path = clean_path(assert(input.path, "no input.path specified")); + assert(is_dir(path), "input.path is not a directory"); + + if input.host then + return coroutine.wrap(function() loop_over_users(input.path, input.host, coroutine.yield) end); + else + return coroutine.wrap(function() loop_over_hosts(input.path, coroutine.yield) end); + end +end + +return _M; diff --git a/tools/migration/migrator/mtools.lua b/tools/migration/migrator/mtools.lua new file mode 100644 index 00000000..e7b774bb --- /dev/null +++ b/tools/migration/migrator/mtools.lua @@ -0,0 +1,56 @@ + + +local print = print; +local t_insert = table.insert; +local t_sort = table.sort; + +module "mtools" + +function sorted(params) + + local reader = params.reader; -- iterator to get items from + local sorter = params.sorter; -- sorting function + local filter = params.filter; -- filter function + + local cache = {}; + for item in reader do + if filter then item = filter(item); end + if item then t_insert(cache, item); end + end + if sorter then + t_sort(cache, sorter); + end + local i = 0; + return function() + i = i + 1; + return cache[i]; + end; + +end + +function merged(reader, merger) + + local item1 = reader(); + local merged = { item1 }; + return function() + while true do + if not item1 then return nil; end + local item2 = reader(); + if not item2 then item1 = nil; return merged; end + if merger(item1, item2) then + --print("merged") + item1 = item2; + t_insert(merged, item1); + else + --print("unmerged", merged) + item1 = item2; + local tmp = merged; + merged = { item1 }; + return tmp; + end + end + end; + +end + +return _M; diff --git a/tools/migration/migrator/prosody_files.lua b/tools/migration/migrator/prosody_files.lua new file mode 100644 index 00000000..4462fb3e --- /dev/null +++ b/tools/migration/migrator/prosody_files.lua @@ -0,0 +1,138 @@ + +local print = print; +local assert = assert; +local setmetatable = setmetatable; +local tonumber = tonumber; +local char = string.char; +local coroutine = coroutine; +local lfs = require "lfs"; +local loadfile = loadfile; +local pcall = pcall; +local mtools = require "migrator.mtools"; +local next = next; +local pairs = pairs; +local json = require "util.json"; +local os_getenv = os.getenv; + +prosody = {}; +local dm = require "util.datamanager" + +module "prosody_files" + +local function is_dir(path) return lfs.attributes(path, "mode") == "directory"; end +local function is_file(path) return lfs.attributes(path, "mode") == "file"; end +local function clean_path(path) + return path:gsub("\\", "/"):gsub("//+", "/"):gsub("^~", os_getenv("HOME") or "~"); +end +local encode, decode; do + local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end }); + decode = function (s) return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes)); end + encode = function (s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end +end +local function decode_dir(x) + if x:gsub("%%%x%x", ""):gsub("[a-zA-Z0-9]", "") == "" then + return decode(x); + end +end +local function decode_file(x) + if x:match(".%.dat$") and x:gsub("%.dat$", ""):gsub("%%%x%x", ""):gsub("[a-zA-Z0-9]", "") == "" then + return decode(x:gsub("%.dat$", "")); + end +end +local function prosody_dir(path, ondir, onfile, ...) + for x in lfs.dir(path) do + local xpath = path.."/"..x; + if decode_dir(x) and is_dir(xpath) then + ondir(xpath, x, ...); + elseif decode_file(x) and is_file(xpath) then + onfile(xpath, x, ...); + end + end +end + +local function handle_root_file(path, name) + --print("root file: ", decode_file(name)) + coroutine.yield { user = nil, host = nil, store = decode_file(name) }; +end +local function handle_host_file(path, name, host) + --print("host file: ", decode_dir(host).."/"..decode_file(name)) + coroutine.yield { user = nil, host = decode_dir(host), store = decode_file(name) }; +end +local function handle_store_file(path, name, store, host) + --print("store file: ", decode_file(name).."@"..decode_dir(host).."/"..decode_dir(store)) + coroutine.yield { user = decode_file(name), host = decode_dir(host), store = decode_dir(store) }; +end +local function handle_host_store(path, name, host) + prosody_dir(path, function() end, handle_store_file, name, host); +end +local function handle_host_dir(path, name) + prosody_dir(path, handle_host_store, handle_host_file, name); +end +local function handle_root_dir(path) + prosody_dir(path, handle_host_dir, handle_root_file); +end + +local function decode_user(item) + local userdata = { + user = item[1].user; + host = item[1].host; + stores = {}; + }; + for i=1,#item do -- loop over stores + local result = {}; + local store = item[i]; + userdata.stores[store.store] = store.data; + store.user = nil; store.host = nil; store.store = nil; + end + return userdata; +end + +function reader(input) + local path = clean_path(assert(input.path, "no input.path specified")); + assert(is_dir(path), "input.path is not a directory"); + local iter = coroutine.wrap(function()handle_root_dir(path);end); + -- get per-user stores, sorted + local iter = mtools.sorted { + reader = function() + local x = iter(); + if x then + dm.set_data_path(path); + local err; + x.data, err = dm.load(x.user, x.host, x.store); + if x.data == nil and err then + error(("Error loading data at path %s for %s@%s (%s store)") + :format(path, x.user or "<nil>", x.host or "<nil>", x.store or "<nil>"), 0); + end + return x; + end + end; + sorter = function(a, b) + local a_host, a_user, a_store = a.host or "", a.user or "", a.store or ""; + local b_host, b_user, b_store = b.host or "", b.user or "", b.store or ""; + return a_host > b_host or (a_host==b_host and a_user > b_user) or (a_host==b_host and a_user==b_user and a_store > b_store); + end; + }; + -- merge stores to get users + iter = mtools.merged(iter, function(a, b) + return (a.host == b.host and a.user == b.user); + end); + + return function() + local x = iter(); + return x and decode_user(x); + end +end + +function writer(output) + local path = clean_path(assert(output.path, "no output.path specified")); + assert(is_dir(path), "output.path is not a directory"); + return function(item) + if not item then return; end -- end of input + dm.set_data_path(path); + for store, data in pairs(item.stores) do + assert(dm.store(item.user, item.host, store, data)); + end + end +end + +return _M; diff --git a/tools/migration/migrator/prosody_sql.lua b/tools/migration/migrator/prosody_sql.lua new file mode 100644 index 00000000..27b5835e --- /dev/null +++ b/tools/migration/migrator/prosody_sql.lua @@ -0,0 +1,200 @@ + +local assert = assert; +local have_DBI, DBI = pcall(require,"DBI"); +local print = print; +local type = type; +local next = next; +local pairs = pairs; +local t_sort = table.sort; +local json = require "util.json"; +local mtools = require "migrator.mtools"; +local tostring = tostring; +local tonumber = tonumber; + +if not have_DBI then + error("LuaDBI (required for SQL support) was not found, please see http://prosody.im/doc/depends#luadbi", 0); +end + +module "prosody_sql" + +local function create_table(connection, params) + local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);"; + if params.driver == "PostgreSQL" then + create_sql = create_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT"); + end + + local stmt = connection:prepare(create_sql); + if stmt then + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)"; + if params.driver == "PostgreSQL" then + index_sql = index_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + index_sql = index_sql:gsub("`([,)])", "`(20)%1"); + end + local stmt, err = connection:prepare(index_sql); + local ok, commit_ok, commit_err; + if stmt then + ok, err = assert(stmt:execute()); + commit_ok, commit_err = assert(connection:commit()); + end + elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0 + -- Failed to create, but check existing MySQL table here + local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + if stmt:rowcount() > 0 then + local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + print("Database table automatically upgraded"); + end + end + repeat until not stmt:fetch(); + end + end + end +end + +local function serialize(value) + local t = type(value); + if t == "string" or t == "boolean" or t == "number" then + return t, tostring(value); + elseif t == "table" then + local value,err = json.encode(value); + if value then return "json", value; end + return nil, err; + end + return nil, "Unhandled value type: "..t; +end +local function deserialize(t, value) + if t == "string" then return value; + elseif t == "boolean" then + if value == "true" then return true; + elseif value == "false" then return false; end + elseif t == "number" then return tonumber(value); + elseif t == "json" then + return json.decode(value); + end +end + +local function decode_user(item) + local userdata = { + user = item[1][1].user; + host = item[1][1].host; + stores = {}; + }; + for i=1,#item do -- loop over stores + local result = {}; + local store = item[i]; + for i=1,#store do -- loop over store data + local row = store[i]; + local k = row.key; + local v = deserialize(row.type, row.value); + if k and v then + if k ~= "" then result[k] = v; elseif type(v) == "table" then + for a,b in pairs(v) do + result[a] = b; + end + end + end + userdata.stores[store[1].store] = result; + end + end + return userdata; +end + +function reader(input) + local dbh = assert(DBI.Connect( + assert(input.driver, "no input.driver specified"), + assert(input.database, "no input.database specified"), + input.username, input.password, + input.host, input.port + )); + assert(dbh:ping()); + local stmt = assert(dbh:prepare("SELECT * FROM prosody")); + assert(stmt:execute()); + local keys = {"host", "user", "store", "key", "type", "value"}; + local f,s,val = stmt:rows(true); + -- get SQL rows, sorted + local iter = mtools.sorted { + reader = function() val = f(s, val); return val; end; + filter = function(x) + for i=1,#keys do + if not x[keys[i]] then return false; end -- TODO log error, missing field + end + if x.host == "" then x.host = nil; end + if x.user == "" then x.user = nil; end + if x.store == "" then x.store = nil; end + return x; + end; + sorter = function(a, b) + local a_host, a_user, a_store = a.host or "", a.user or "", a.store or ""; + local b_host, b_user, b_store = b.host or "", b.user or "", b.store or ""; + return a_host > b_host or (a_host==b_host and a_user > b_user) or (a_host==b_host and a_user==b_user and a_store > b_store); + end; + }; + -- merge rows to get stores + iter = mtools.merged(iter, function(a, b) + return (a.host == b.host and a.user == b.user and a.store == b.store); + end); + -- merge stores to get users + iter = mtools.merged(iter, function(a, b) + return (a[1].host == b[1].host and a[1].user == b[1].user); + end); + return function() + local x = iter(); + return x and decode_user(x); + end; +end + +function writer(output, iter) + local dbh = assert(DBI.Connect( + assert(output.driver, "no output.driver specified"), + assert(output.database, "no output.database specified"), + output.username, output.password, + output.host, output.port + )); + assert(dbh:ping()); + create_table(dbh, output); + local stmt = assert(dbh:prepare("SELECT * FROM prosody")); + assert(stmt:execute()); + local stmt = assert(dbh:prepare("DELETE FROM prosody")); + assert(stmt:execute()); + local insert_sql = "INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)"; + if output.driver == "PostgreSQL" then + insert_sql = insert_sql:gsub("`", "\""); + end + local insert = assert(dbh:prepare(insert_sql)); + + return function(item) + if not item then assert(dbh:commit()) return dbh:close(); end -- end of input + local host = item.host or ""; + local user = item.user or ""; + for store, data in pairs(item.stores) do + -- TODO transactions + local extradata = {}; + for key, value in pairs(data) do + if type(key) == "string" and key ~= "" then + local t, value = assert(serialize(value)); + local ok, err = assert(insert:execute(host, user, store, key, t, value)); + else + extradata[key] = value; + end + end + if next(extradata) ~= nil then + local t, extradata = assert(serialize(extradata)); + local ok, err = assert(insert:execute(host, user, store, "", t, extradata)); + end + end + end; +end + + +return _M; diff --git a/tools/migration/prosody-migrator.lua b/tools/migration/prosody-migrator.lua new file mode 100644 index 00000000..7c933b88 --- /dev/null +++ b/tools/migration/prosody-migrator.lua @@ -0,0 +1,132 @@ +#!/usr/bin/env lua + +CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR"); +CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR"); + +-- Substitute ~ with path to home directory in paths +if CFG_CONFIGDIR then + CFG_CONFIGDIR = CFG_CONFIGDIR:gsub("^~", os.getenv("HOME")); +end + +if CFG_SOURCEDIR then + CFG_SOURCEDIR = CFG_SOURCEDIR:gsub("^~", os.getenv("HOME")); +end + +local default_config = (CFG_CONFIGDIR or ".").."/migrator.cfg.lua"; + +-- Command-line parsing +local options = {}; +local handled_opts = 0; +for i = 1, #arg do + if arg[i]:sub(1,2) == "--" then + local opt, val = arg[i]:match("([%w-]+)=?(.*)"); + if opt then + options[(opt:sub(3):gsub("%-", "_"))] = #val > 0 and val or true; + end + handled_opts = i; + else + break; + end +end +table.remove(arg, handled_opts); + +if CFG_SOURCEDIR then + package.path = CFG_SOURCEDIR.."/?.lua;"..package.path; + package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath; +else + package.path = "../../?.lua;"..package.path + package.cpath = "../../?.so;"..package.cpath +end + +local envloadfile = require "util.envload".envloadfile; + +-- Load config file +local function loadfilein(file, env) + if loadin then + return loadin(env, io.open(file):read("*a")); + else + return envloadfile(file, env); + end +end + +local config_file = options.config or default_config; +local from_store = arg[1] or "input"; +local to_store = arg[2] or "output"; + +config = {}; +local config_env = setmetatable({}, { __index = function(t, k) return function(tbl) config[k] = tbl; end; end }); +local config_chunk, err = loadfilein(config_file, config_env); +if not config_chunk then + print("There was an error loading the config file, check the file exists"); + print("and that the syntax is correct:"); + print("", err); + os.exit(1); +end + +config_chunk(); + +local have_err; +if #arg > 0 and #arg ~= 2 then + have_err = true; + print("Error: Incorrect number of parameters supplied."); +end +if not config[from_store] then + have_err = true; + print("Error: Input store '"..from_store.."' not found in the config file."); +end +if not config[to_store] then + have_err = true; + print("Error: Output store '"..to_store.."' not found in the config file."); +end + +function load_store_handler(name) + local store_type = config[name].type; + if not store_type then + print("Error: "..name.." store type not specified in the config file"); + return false; + else + local ok, err = pcall(require, "migrator."..store_type); + if not ok then + if package.loaded["migrator."..store_type] then + print(("Error: Failed to initialize '%s' store:\n\t%s") + :format(name, err)); + else + print(("Error: Unrecognised store type for '%s': %s") + :format(from_store, store_type)); + end + return false; + end + end + return true; +end + +have_err = have_err or not(load_store_handler(from_store, "input") and load_store_handler(to_store, "output")); + +if have_err then + print(""); + print("Usage: "..arg[0].." FROM_STORE TO_STORE"); + print("If no stores are specified, 'input' and 'output' are used."); + print(""); + print("The available stores in your migrator config are:"); + print(""); + for store in pairs(config) do + print("", store); + end + print(""); + os.exit(1); +end + +local itype = config[from_store].type; +local otype = config[to_store].type; +local reader = require("migrator."..itype).reader(config[from_store]); +local writer = require("migrator."..otype).writer(config[to_store]); + +local json = require "util.json"; + +io.stderr:write("Migrating...\n"); +for x in reader do + --print(json.encode(x)) + writer(x); +end +writer(nil); -- close +io.stderr:write("Done!\n"); diff --git a/tools/openfire2prosody.lua b/tools/openfire2prosody.lua new file mode 100644 index 00000000..bdea9a63 --- /dev/null +++ b/tools/openfire2prosody.lua @@ -0,0 +1,103 @@ +#!/usr/bin/env lua +-- Prosody IM +-- Copyright (C) 2008-2009 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +package.path = package.path..";../?.lua"; +package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager + +-- ugly workaround for getting datamanager to work outside of prosody :( +prosody = { }; +prosody.platform = "unknown"; +if os.getenv("WINDIR") then + prosody.platform = "windows"; +elseif package.config:sub(1,1) == "/" then + prosody.platform = "posix"; +end + +local parse_xml = require "util.xml".parse; + +----------------------------------------------------------------------- + +package.loaded["util.logger"] = {init = function() return function() end; end} +local dm = require "util.datamanager" +dm.set_data_path("data"); + +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[Openfire importer for Prosody + + Usage: openfire2prosody.lua filename.xml hostname + +]]); + os.exit(1); +end + +local host = select(2, ...) or "localhost"; + +local file = assert(io.open(arg)); +local data = assert(file:read("*a")); +file:close(); + +local xml = assert(parse_xml(data)); + +assert(xml.name == "Openfire", "The input file is not an Openfire XML export"); + +local substatus_mapping = { ["0"] = "none", ["1"] = "to", ["2"] = "from", ["3"] = "both" }; + +for _,tag in ipairs(xml.tags) do + if tag.name == "User" then + local username, password, roster; + + for _,tag in ipairs(tag.tags) do + if tag.name == "Username" then + username = tag:get_text(); + elseif tag.name == "Password" then + password = tag:get_text(); + elseif tag.name == "Roster" then + roster = {}; + local pending = {}; + for _,tag in ipairs(tag.tags) do + if tag.name == "Item" then + local jid = assert(tag.attr.jid, "Roster item has no JID"); + if tag.attr.substatus ~= "-1" then + local item = {}; + item.name = tag.attr.name; + item.subscription = assert(substatus_mapping[tag.attr.substatus], "invalid substatus"); + item.ask = tag.attr.askstatus == "0" and "subscribe" or nil; + + local groups = {}; + for _,tag in ipairs(tag) do + if tag.name == "Group" then + groups[tag:get_text()] = true; + end + end + item.groups = groups; + roster[jid] = item; + end + if tag.attr.recvstatus == "1" then pending[jid] = true; end + end + end + + if next(pending) then + roster[false] = { pending = pending }; + end + end + end + + assert(username and password, "No username or password"); + + local ret, err = dm.store(username, host, "accounts", {password = password}); + print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); + + if roster then + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored roster: "..username.."@"..host.." = "..password); + end + end +end + diff --git a/tools/xep227toprosody.lua b/tools/xep227toprosody.lua new file mode 100755 index 00000000..b5156f45 --- /dev/null +++ b/tools/xep227toprosody.lua @@ -0,0 +1,263 @@ +#!/usr/bin/env lua +-- Prosody IM +-- Copyright (C) 2008-2009 Matthew Wild +-- Copyright (C) 2008-2009 Waqas Hussain +-- Copyright (C) 2010 Stefan Gehn +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +-- FIXME: XEP-0227 supports XInclude but luaexpat does not +-- +-- XEP-227 elements and their current level of support: +-- Hosts : supported +-- Users : supported +-- Rosters : supported, needs testing +-- Offline Messages : supported, needs testing +-- Private XML Storage : supported, needs testing +-- vCards : supported, needs testing +-- Privacy Lists: UNSUPPORTED +-- http://xmpp.org/extensions/xep-0227.html#privacy-lists +-- mod_privacy uses dm.load(username, host, "privacy"); and stores stanzas 1:1 +-- Incoming Subscription Requests : supported + +package.path = package.path..";../?.lua"; +package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager + +-- ugly workaround for getting datamanager to work outside of prosody :( +prosody = { }; +prosody.platform = "unknown"; +if os.getenv("WINDIR") then + prosody.platform = "windows"; +elseif package.config:sub(1,1) == "/" then + prosody.platform = "posix"; +end + +local lxp = require "lxp"; +local st = require "util.stanza"; +local xmppstream = require "util.xmppstream"; +local new_xmpp_handlers = xmppstream.new_sax_handlers; +local dm = require "util.datamanager" +dm.set_data_path("data"); + +local ns_separator = xmppstream.ns_separator; +local ns_pattern = xmppstream.ns_pattern; + +local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns"; + +----------------------------------------------------------------------- + +function store_vcard(username, host, stanza) + -- create or update vCard for username@host + local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza)); + print("["..(err or "success").."] stored vCard: "..username.."@"..host); +end + +function store_password(username, host, password) + -- create or update account for username@host + local ret, err = dm.store(username, host, "accounts", {password = password}); + print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); +end + +function store_roster(username, host, roster_items) + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + -- merge imported roster-items with loaded roster + for item_tag in roster_items:childtags("item") do + -- jid for this roster-item + local item_jid = item_tag.attr.jid + -- validate item stanzas + if (item_jid ~= "") then + -- prepare roster item + -- TODO: is the subscription attribute optional? + local item = {subscription = item_tag.attr.subscription, groups = {}}; + -- optional: give roster item a real name + if item_tag.attr.name then + item.name = item_tag.attr.name; + end + -- optional: iterate over group stanzas inside item stanza + for group_tag in item_tag:childtags("group") do + local group_name = group_tag:get_text(); + if (group_name ~= "") then + item.groups[group_name] = true; + else + print("[error] invalid group stanza: "..group_tag:pretty_print()); + end + end + -- store item in roster + roster[item_jid] = item; + print("[success] roster entry: " ..username.."@"..host.." - "..item_jid); + else + print("[error] invalid roster stanza: " ..item_tag:pretty_print()); + end + + end + -- store merged roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored roster: " ..username.."@"..host); +end + +function store_private(username, host, private_items) + local private = dm.load(username, host, "private") or {}; + for _, ch in ipairs(private_items.tags) do + --print("private :"..ch:pretty_print()); + private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch); + print("[success] private item: " ..username.."@"..host.." - "..ch.name); + end + local ret, err = dm.store(username, host, "private", private); + print("["..(err or "success").."] stored private: " ..username.."@"..host); +end + +function store_offline_messages(username, host, offline_messages) + -- TODO: maybe use list_load(), append and list_store() instead + -- of constantly reopening the file with list_append()? + for ch in offline_messages:childtags("message", "jabber:client") do + --print("message :"..ch:pretty_print()); + local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch)); + print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from); + end +end + + +function store_subscription_request(username, host, presence_stanza) + local from_bare = presence_stanza.attr.from; + + -- fetch current roster-table for username@host if he already has one + local roster = dm.load(username, host, "roster") or {}; + + local item = roster[from_bare]; + if item and (item.subscription == "from" or item.subscription == "both") then + return; -- already subscribed, do nothing + end + + -- add to table of pending subscriptions + if not roster.pending then roster.pending = {}; end + roster.pending[from_bare] = true; + + -- store updated roster-table + local ret, err = dm.store(username, host, "roster", roster); + print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare); +end + +----------------------------------------------------------------------- + +local curr_host = ""; +local user_name = ""; + + +local cb = { + stream_tag = "user", + stream_ns = xmlns_xep227, +}; +function cb.streamopened(session, attr) + session.notopen = false; + user_name = attr.name; + store_password(user_name, curr_host, attr.password); +end +function cb.streamclosed(session) + session.notopen = true; + user_name = ""; +end +function cb.handlestanza(session, stanza) + --print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or "")); + if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then + store_vcard(user_name, curr_host, stanza); + elseif (stanza.name == "query") then + if (stanza.attr.xmlns == "jabber:iq:roster") then + store_roster(user_name, curr_host, stanza); + elseif (stanza.attr.xmlns == "jabber:iq:private") then + store_private(user_name, curr_host, stanza); + end + elseif (stanza.name == "offline-messages") then + store_offline_messages(user_name, curr_host, stanza); + elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then + store_subscription_request(user_name, curr_host, stanza); + else + print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or "")); + end +end + +local user_handlers = new_xmpp_handlers({ notopen = true }, cb); + +----------------------------------------------------------------------- + +local lxp_handlers = { + --count = 0 +}; + +-- TODO: error handling for invalid opening elements if curr_host is empty +function lxp_handlers.StartElement(parser, elementname, attributes) + local curr_ns, name = elementname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + --io.write("+ ", string.rep(" ", count), name, " (", curr_ns, ")", "\n") + --count = count + 1; + if curr_host ~= "" then + -- forward to xmlhandlers + user_handlers:StartElement(elementname, attributes); + elseif (curr_ns == xmlns_xep227) and (name == "host") then + curr_host = attributes["jid"]; -- start of host element + print("Begin parsing host "..curr_host); + elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then + io.stderr:write("Unhandled XML element: ", name, "\n"); + os.exit(1); + end +end + +-- TODO: error handling for invalid closing elements if host is empty +function lxp_handlers.EndElement(parser, elementname) + local curr_ns, name = elementname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + --count = count - 1; + --io.write("- ", string.rep(" ", count), name, " (", curr_ns, ")", "\n") + if curr_host ~= "" then + if (curr_ns == xmlns_xep227) and (name == "host") then + print("End parsing host "..curr_host); + curr_host = "" -- end of host element + else + -- forward to xmlhandlers + user_handlers:EndElement(elementname); + end + elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then + io.stderr:write("Unhandled XML element: ", name, "\n"); + os.exit(1); + end +end + +function lxp_handlers.CharacterData(parser, string) + if curr_host ~= "" then + -- forward to xmlhandlers + user_handlers:CharacterData(string); + end +end + +----------------------------------------------------------------------- + +local arg = ...; +local help = "/? -? ? /h -h /help -help --help"; +if not arg or help:find(arg, 1, true) then + print([[XEP-227 importer for Prosody + + Usage: xep227toprosody.lua filename.xml + +]]); + os.exit(1); +end + +local file = io.open(arg); +if not file then + io.stderr:write("Could not open file: ", arg, "\n"); + os.exit(0); +end + +local parser = lxp.new(lxp_handlers, ns_separator); +for l in file:lines() do + parser:parse(l); +end +parser:parse(); +parser:close(); +file:close(); |