3 -- Copyright (C) 2008-2009 Matthew Wild
4 -- Copyright (C) 2008-2009 Waqas Hussain
5 -- Copyright (C) 2010 Stefan Gehn
7 -- This project is MIT/X11 licensed. Please see the
8 -- COPYING file in the source package for more information.
11 -- FIXME: XEP-0227 supports XInclude but luaexpat does not
13 -- XEP-227 elements and their current level of support:
16 -- Rosters : supported, needs testing
17 -- Offline Messages : supported, needs testing
18 -- Private XML Storage : supported, needs testing
19 -- vCards : supported, needs testing
20 -- Privacy Lists: UNSUPPORTED
21 -- http://xmpp.org/extensions/xep-0227.html#privacy-lists
22 -- mod_privacy uses dm.load(username, host, "privacy"); and stores stanzas 1:1
23 -- Incoming Subscription Requests : supported
25 package
.path
= package
.path
..";../?.lua";
26 package
.cpath
= package
.cpath
..";../?.so"; -- needed for util.pposix used in datamanager
28 local my_name
= arg
[0];
29 if my_name
:match("[/\\]") then
30 package
.path
= package
.path
..";"..my_name
:gsub("[^/\\]+$", "../?.lua");
31 package
.cpath
= package
.cpath
..";"..my_name
:gsub("[^/\\]+$", "../?.so");
34 -- ugly workaround for getting datamanager to work outside of prosody :(
36 prosody
.platform
= "unknown";
37 if os
.getenv("WINDIR") then
38 prosody
.platform
= "windows";
39 elseif package
.config
:sub(1,1) == "/" then
40 prosody
.platform
= "posix";
43 local lxp
= require
"lxp";
44 local st
= require
"util.stanza";
45 local xmppstream
= require
"util.xmppstream";
46 local new_xmpp_handlers
= xmppstream
.new_sax_handlers
;
47 local dm
= require
"util.datamanager"
48 dm
.set_data_path("data");
50 local ns_separator
= xmppstream
.ns_separator
;
51 local ns_pattern
= xmppstream
.ns_pattern
;
53 local xmlns_xep227
= "http://www.xmpp.org/extensions/xep-0227.html#ns";
55 -----------------------------------------------------------------------
57 function store_vcard(username
, host
, stanza
)
58 -- create or update vCard for username@host
59 local ret
, err
= dm
.store(username
, host
, "vcard", st
.preserialize(stanza
));
60 print("["..(err
or "success").."] stored vCard: "..username
.."@"..host
);
63 function store_password(username
, host
, password
)
64 -- create or update account for username@host
65 local ret
, err
= dm
.store(username
, host
, "accounts", {password
= password
});
66 print("["..(err
or "success").."] stored account: "..username
.."@"..host
.." = "..password
);
69 function store_roster(username
, host
, roster_items
)
70 -- fetch current roster-table for username@host if he already has one
71 local roster
= dm
.load(username
, host
, "roster") or {};
72 -- merge imported roster-items with loaded roster
73 for item_tag
in roster_items
:childtags("item") do
74 -- jid for this roster-item
75 local item_jid
= item_tag
.attr
.jid
76 -- validate item stanzas
77 if (item_jid
~= "") then
78 -- prepare roster item
79 -- TODO: is the subscription attribute optional?
80 local item
= {subscription
= item_tag
.attr
.subscription
, groups
= {}};
81 -- optional: give roster item a real name
82 if item_tag
.attr
.name
then
83 item
.name
= item_tag
.attr
.name
;
85 -- optional: iterate over group stanzas inside item stanza
86 for group_tag
in item_tag
:childtags("group") do
87 local group_name
= group_tag
:get_text();
88 if (group_name
~= "") then
89 item
.groups
[group_name
] = true;
91 print("[error] invalid group stanza: "..group_tag
:pretty_print());
94 -- store item in roster
95 roster
[item_jid
] = item
;
96 print("[success] roster entry: " ..username
.."@"..host
.." - "..item_jid
);
98 print("[error] invalid roster stanza: " ..item_tag
:pretty_print());
102 -- store merged roster-table
103 local ret
, err
= dm
.store(username
, host
, "roster", roster
);
104 print("["..(err
or "success").."] stored roster: " ..username
.."@"..host
);
107 function store_private(username
, host
, private_items
)
108 local private
= dm
.load(username
, host
, "private") or {};
109 for _
, ch
in ipairs(private_items
.tags
) do
110 --print("private :"..ch:pretty_print());
111 private
[ch
.name
..":"..ch
.attr
.xmlns
] = st
.preserialize(ch
);
112 print("[success] private item: " ..username
.."@"..host
.." - "..ch
.name
);
114 local ret
, err
= dm
.store(username
, host
, "private", private
);
115 print("["..(err
or "success").."] stored private: " ..username
.."@"..host
);
118 function store_offline_messages(username
, host
, offline_messages
)
119 -- TODO: maybe use list_load(), append and list_store() instead
120 -- of constantly reopening the file with list_append()?
121 for ch
in offline_messages
:childtags("message", "jabber:client") do
122 --print("message :"..ch:pretty_print());
123 local ret
, err
= dm
.list_append(username
, host
, "offline", st
.preserialize(ch
));
124 print("["..(err
or "success").."] stored offline message: " ..username
.."@"..host
.." - "..ch
.attr
.from
);
129 function store_subscription_request(username
, host
, presence_stanza
)
130 local from_bare
= presence_stanza
.attr
.from
;
132 -- fetch current roster-table for username@host if he already has one
133 local roster
= dm
.load(username
, host
, "roster") or {};
135 local item
= roster
[from_bare
];
136 if item
and (item
.subscription
== "from" or item
.subscription
== "both") then
137 return; -- already subscribed, do nothing
140 -- add to table of pending subscriptions
141 if not roster
.pending
then roster
.pending
= {}; end
142 roster
.pending
[from_bare
] = true;
144 -- store updated roster-table
145 local ret
, err
= dm
.store(username
, host
, "roster", roster
);
146 print("["..(err
or "success").."] stored subscription request: " ..username
.."@"..host
.." - "..from_bare
);
149 -----------------------------------------------------------------------
151 local curr_host
= "";
152 local user_name
= "";
157 stream_ns
= xmlns_xep227
,
159 function cb
.streamopened(session
, attr
)
160 session
.notopen
= false;
161 user_name
= attr
.name
;
162 store_password(user_name
, curr_host
, attr
.password
);
164 function cb
.streamclosed(session
)
165 session
.notopen
= true;
168 function cb
.handlestanza(session
, stanza
)
169 --print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
170 if (stanza
.name
== "vCard") and (stanza
.attr
.xmlns
== "vcard-temp") then
171 store_vcard(user_name
, curr_host
, stanza
);
172 elseif (stanza
.name
== "query") then
173 if (stanza
.attr
.xmlns
== "jabber:iq:roster") then
174 store_roster(user_name
, curr_host
, stanza
);
175 elseif (stanza
.attr
.xmlns
== "jabber:iq:private") then
176 store_private(user_name
, curr_host
, stanza
);
178 elseif (stanza
.name
== "offline-messages") then
179 store_offline_messages(user_name
, curr_host
, stanza
);
180 elseif (stanza
.name
== "presence") and (stanza
.attr
.xmlns
== "jabber:client") then
181 store_subscription_request(user_name
, curr_host
, stanza
);
183 print("UNHANDLED stanza "..stanza
.name
.." xmlns: "..(stanza
.attr
.xmlns
or ""));
187 local user_handlers
= new_xmpp_handlers({ notopen
= true }, cb
);
189 -----------------------------------------------------------------------
191 local lxp_handlers
= {
195 -- TODO: error handling for invalid opening elements if curr_host is empty
196 function lxp_handlers
.StartElement(parser
, elementname
, attributes
)
197 local curr_ns
, name
= elementname
:match(ns_pattern
);
199 curr_ns
, name
= "", curr_ns
;
201 --io.write("+ ", string.rep(" ", count), name, " (", curr_ns, ")", "\n")
203 if curr_host
~= "" then
204 -- forward to xmlhandlers
205 user_handlers
.StartElement(parser
, elementname
, attributes
);
206 elseif (curr_ns
== xmlns_xep227
) and (name
== "host") then
207 curr_host
= attributes
["jid"]; -- start of host element
208 print("Begin parsing host "..curr_host
);
209 elseif (curr_ns
~= xmlns_xep227
) or (name
~= "server-data") then
210 io
.stderr
:write("Unhandled XML element: ", name
, "\n");
215 -- TODO: error handling for invalid closing elements if host is empty
216 function lxp_handlers
.EndElement(parser
, elementname
)
217 local curr_ns
, name
= elementname
:match(ns_pattern
);
219 curr_ns
, name
= "", curr_ns
;
222 --io.write("- ", string.rep(" ", count), name, " (", curr_ns, ")", "\n")
223 if curr_host
~= "" then
224 if (curr_ns
== xmlns_xep227
) and (name
== "host") then
225 print("End parsing host "..curr_host
);
226 curr_host
= "" -- end of host element
228 -- forward to xmlhandlers
229 user_handlers
.EndElement(parser
, elementname
);
231 elseif (curr_ns
~= xmlns_xep227
) or (name
~= "server-data") then
232 io
.stderr
:write("Unhandled XML element: ", name
, "\n");
237 function lxp_handlers
.CharacterData(parser
, string)
238 if curr_host
~= "" then
239 -- forward to xmlhandlers
240 user_handlers
.CharacterData(parser
, string);
244 -----------------------------------------------------------------------
247 local help
= "/? -? ? /h -h /help -help --help";
248 if not arg
or help
:find(arg
, 1, true) then
249 print([[XEP-227 importer for Prosody
251 Usage: xep227toprosody.lua filename.xml
257 local file
= io
.open(arg
);
259 io
.stderr
:write("Could not open file: ", arg
, "\n");
263 local parser
= lxp
.new(lxp_handlers
, ns_separator
);
264 for l
in file
:lines() do