aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/ejabberd2prosody.lua114
-rw-r--r--tools/ejabberdsql2prosody.lua103
-rw-r--r--tools/erlparse.lua94
-rw-r--r--tools/jabberd14sql2prosody.lua647
-rw-r--r--tools/migration/Makefile39
-rw-r--r--tools/migration/migrator.cfg.lua26
-rw-r--r--tools/migration/migrator/jabberd14.lua142
-rw-r--r--tools/migration/migrator/mtools.lua56
-rw-r--r--tools/migration/migrator/prosody_files.lua138
-rw-r--r--tools/migration/migrator/prosody_sql.lua200
-rw-r--r--tools/migration/prosody-migrator.lua132
-rw-r--r--tools/openfire2prosody.lua103
-rwxr-xr-xtools/xep227toprosody.lua263
13 files changed, 1952 insertions, 105 deletions
diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua
index 4fef3f3a..c11e41d9 100755
--- a/tools/ejabberd2prosody.lua
+++ b/tools/ejabberd2prosody.lua
@@ -1,7 +1,7 @@
#!/usr/bin/env lua
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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.
@@ -9,25 +9,39 @@
-require "erlparse";
-
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";
-package.loaded["util.logger"] = {init = function() return function() end; end}
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 attr[a[1]] = a[2]; end
+ 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));
@@ -44,7 +58,7 @@ function vcard(node, host, stanza)
end
function password(node, host, password)
local ret, err = dm.store(node, host, "accounts", {password = password});
- print("["..(err or "success").."] accounts: "..node.."@"..host.." = "..password);
+ print("["..(err or "success").."] accounts: "..node.."@"..host);
end
function roster(node, host, jid, item)
local roster = dm.load(node, host, "roster") or {};
@@ -71,6 +85,70 @@ function offline_msg(node, host, t, stanza)
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 = {
@@ -86,13 +164,24 @@ local filters = {
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
+ if ask == "none" then
+ ask = nil;
+ elseif ask == "out" then
+ ask = "subscribe"
+ elseif ask == "in" then
roster_pending(node, host, contact);
- return;
- else error(ask) end
+ 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 item.groups[g] = true; end
+ 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)
@@ -101,6 +190,9 @@ local filters = {
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");
diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua
index 4aace085..43720643 100644
--- a/tools/ejabberdsql2prosody.lua
+++ b/tools/ejabberdsql2prosody.lua
@@ -1,15 +1,18 @@
#!/usr/bin/env lua
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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");
@@ -19,12 +22,16 @@ 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); end
- if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil")); end
+ 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)
@@ -123,7 +130,12 @@ local function readInsert()
end
end
local tname = readTableName();
- for ch in ("` VALUES "):gmatch(".") do read(ch); end -- expect this
+ 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;
@@ -136,8 +148,8 @@ local function readFile(filename)
while true do
local tname, tuples = readInsert();
if tname then
- if t[name] then
- local t_name = t[name];
+ if t[tname] then
+ local t_name = t[tname];
for i=1,#tuples do
table.insert(t_name, tuples[i]);
end
@@ -156,58 +168,6 @@ return readFile(filename);
------
end
--- XML parser
-local parse_xml = (function()
- local entity_map = setmetatable({
- ["amp"] = "&";
- ["gt"] = ">";
- ["lt"] = "<";
- ["apos"] = "'";
- ["quot"] = "\"";
- }, {__index = function(_, s)
- if s:sub(1,1) == "#" then
- if s:sub(2,2) == "x" then
- return string.char(tonumber(s:sub(3), 16));
- else
- return string.char(tonumber(s:sub(2)));
- end
- end
- end
- });
- local function xml_unescape(str)
- return (str:gsub("&(.-);", entity_map));
- end
- local function parse_tag(s)
- local name,sattr=(s):gmatch("([^%s]+)(.*)")();
- local attr = {};
- for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end
- return name, attr;
- end
- return function(xml)
- local stanza = st.stanza("root");
- local regexp = "<([^>]*)>([^<]*)";
- for elem, text in xml:gmatch(regexp) do
- if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
- elseif elem:sub(1,1) == "/" then -- end tag
- elem = elem:sub(2);
- stanza:up(); -- TODO check for start-end tag name match
- elseif elem:sub(-1,-1) == "/" then -- empty tag
- elem = elem:sub(1,-2);
- local name,attr = parse_tag(elem);
- stanza:tag(name, attr):up();
- else -- start tag
- local name,attr = parse_tag(elem);
- stanza:tag(name, attr);
- end
- if #text ~= 0 then -- text
- stanza:text(xml_unescape(text));
- end
- end
- return stanza.tags[1];
- end
-end)();
--- end of XML parser
-
local arg, host = ...;
local help = "/? -? ? /h -h /help -help --help";
if not(arg and host) or help:find(arg, 1, true) then
@@ -254,7 +214,7 @@ end
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.." = "..password);
+ print("["..(err or "success").."] accounts: "..node.."@"..host);
end
function roster(node, host, jid, item)
@@ -284,6 +244,12 @@ function private_storage(node, host, xmlns, 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;
@@ -321,5 +287,20 @@ for i, row in ipairs(t["vcard"] or NULL) do
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, st.preserialize(parse_xml(row.data)));
+ 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
index f2d410a3..174585d3 100644
--- a/tools/erlparse.lua
+++ b/tools/erlparse.lua
@@ -1,21 +1,27 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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); end
- if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil")); end
+ 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)
@@ -27,91 +33,113 @@ local function peek()
return last;
end
-local _A, _a, _Z, _z, _0, _9, __, _at, _space = string.byte("AaZz09@_ ", 1, 9);
+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;
+ 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);
+ 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;
+ 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";
+ 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 = "";
+ local str = {};
while true do
local ch = read();
- if ch == "\"" and not slash then break; end
- str = str..ch;
+ 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
- str = str:gsub("\\.", {["\\b"]="\b", ["\\d"]="\d", ["\\e"]="\e", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]="\s", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"});
- return str;
+ return t_concat(str);
end
local function readAtom1()
- local var = read();
+ local var = { read() };
while isAtom(peek()) do
- var = var..read();
+ var[#var+1] = read();
end
- return var;
+ return t_concat(var);
end
local function readAtom2()
- local str = read("'");
+ local str = { read("'") };
local slash = nil;
while true do
local ch = read();
- str = str..ch;
+ str[#str+1] = ch;
if ch == "'" and not slash then break; end
end
- return str;
+ return t_concat(str);
end
local function readNumber()
- local num = read();
+ local num = { read() };
while isNumeric(peek()) do
- num = num..read();
+ num[#num+1] = read();
+ end
+ if peek() == "." then
+ num[#num+1] = read();
+ while isNumeric(peek()) do
+ num[#num+1] = read();
+ end
end
- return tonumber(num);
+ return tonumber(t_concat(num));
end
local readItem = nil;
local function readTuple()
local t = {};
- local s = ""; -- string representation
+ local s = {}; -- string representation
read(); -- read {, or [, or <
while true do
local item = readItem();
if not item then break; end
- if type(item) ~= type(0) or item > 255 then
+ if type(item) ~= "number" or item > 255 then
s = nil;
elseif s then
- s = s..string.char(item);
+ s[#s+1] = string_char(item);
end
- table.insert(t, item);
+ t_insert(t, item);
end
read(); -- read }, or ], or >
- if s and s ~= "" then
- return s
+ 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) == type("") then
+ if type(t) == "string" then
-- binary is a list of integers
return t;
- elseif type(t) == type({}) then
+ elseif type(t) == "table" then
if t[1] then
-- binary contains string
return t[1];
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();