tools/xep227toprosody.lua

1
#!/usr/bin/env lua
2
-- Prosody IM
3
-- Copyright (C) 2008-2009 Matthew Wild
4
-- Copyright (C) 2008-2009 Waqas Hussain
5
-- Copyright (C) 2010      Stefan Gehn
6
-- 
7
-- This project is MIT/X11 licensed. Please see the
8
-- COPYING file in the source package for more information.
9
--
10
 
11
-- FIXME: XEP-0227 supports XInclude but luaexpat does not
12
--
13
-- XEP-227 elements and their current level of support:
14
-- Hosts : supported
15
-- Users : supported
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
24
 
25
package.path = package.path..";../?.lua";
26
package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager
27
 
28
-- ugly workaround for getting datamanager to work outside of prosody :(
29
prosody = { };
30
prosody.platform = "unknown";
31
if os.getenv("WINDIR") then
32
	prosody.platform = "windows";
33
elseif package.config:sub(1,1) == "/" then
34
	prosody.platform = "posix";
35
end
36
 
37
local lxp = require "lxp";
38
local st = require "util.stanza";
39
local xmppstream = require "util.xmppstream";
40
local new_xmpp_handlers = xmppstream.new_sax_handlers;
41
local dm = require "util.datamanager"
42
dm.set_data_path("data");
43
 
44
local ns_separator = xmppstream.ns_separator;
45
local ns_pattern = xmppstream.ns_pattern;
46
 
47
local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
48
 
49
-----------------------------------------------------------------------
50
 
51
function store_vcard(username, host, stanza)
52
	-- create or update vCard for username@host
53
	local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza));
54
	print("["..(err or "success").."] stored vCard: "..username.."@"..host);
55
end
56
 
57
function store_password(username, host, password)
58
	-- create or update account for username@host
59
	local ret, err = dm.store(username, host, "accounts", {password = password});
60
	print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password);
61
end
62
 
63
function store_roster(username, host, roster_items)
64
	-- fetch current roster-table for username@host if he already has one
65
	local roster = dm.load(username, host, "roster") or {};
66
	-- merge imported roster-items with loaded roster
67
	for item_tag in roster_items:childtags() do
68
		-- jid for this roster-item
69
		local item_jid = item_tag.attr.jid
70
		-- validate item stanzas
71
		if (item_tag.name == "item") and (item_jid ~= "") then
72
			-- prepare roster item
73
			-- TODO: is the subscription attribute optional?
74
			local item = {subscription = item_tag.attr.subscription, groups = {}};
75
			-- optional: give roster item a real name
76
			if item_tag.attr.name then
77
				item.name = item_tag.attr.name;
78
			end
79
			-- optional: iterate over group stanzas inside item stanza
80
			for group_tag in item_tag:childtags() do
81
				local group_name = group_tag:get_text();
82
				if (group_tag.name == "group") and (group_name ~= "") then
83
					item.groups[group_name] = true;
84
				else
85
					print("[error] invalid group stanza: "..group_tag:pretty_print());
86
				end
87
			end
88
			-- store item in roster
89
			roster[item_jid] = item;
90
			print("[success] roster entry: " ..username.."@"..host.." - "..item_jid);
91
		else
92
			print("[error] invalid roster stanza: " ..item_tag:pretty_print());
93
		end
94
 
95
	end
96
	-- store merged roster-table
97
	local ret, err = dm.store(username, host, "roster", roster);
98
	print("["..(err or "success").."] stored roster: " ..username.."@"..host);
99
end
100
 
101
function store_private(username, host, private_items)
102
	local private = dm.load(username, host, "private") or {};
103
	for ch in private_items:childtags() do
104
		--print("private :"..ch:pretty_print());
105
		private[ch.name..":"..ch.attr.xmlns] = st.preserialize(ch);
106
		print("[success] private item: " ..username.."@"..host.." - "..ch.name);
107
	end
108
	local ret, err = dm.store(username, host, "private", private);
109
	print("["..(err or "success").."] stored private: " ..username.."@"..host);
110
end
111
 
112
function store_offline_messages(username, host, offline_messages)
113
	-- TODO: maybe use list_load(), append and list_store() instead
114
	--       of constantly reopening the file with list_append()?
115
	for ch in offline_messages:childtags() do
116
		--print("message :"..ch:pretty_print());
117
		local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch));
118
		print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from);
119
	end
120
end
121
 
122
 
123
function store_subscription_request(username, host, presence_stanza)
124
	local from_bare = presence_stanza.attr.from;
125
 
126
	-- fetch current roster-table for username@host if he already has one
127
	local roster = dm.load(username, host, "roster") or {};
128
 
129
	local item = roster[from_bare];
130
	if item and (item.subscription == "from" or item.subscription == "both") then
131
		return; -- already subscribed, do nothing
132
	end
133
 
134
	-- add to table of pending subscriptions
135
	if not roster.pending then roster.pending = {}; end
136
	roster.pending[from_bare] = true;
137
 
138
	-- store updated roster-table
139
	local ret, err = dm.store(username, host, "roster", roster);
140
	print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare);
141
end
142
 
143
-----------------------------------------------------------------------
144
 
145
local curr_host = "";
146
local user_name = "";
147
 
148
 
149
local cb = {
150
	stream_tag = "user",
151
	stream_ns = xmlns_xep227,
152
};
153
function cb.streamopened(session, attr)
154
	session.notopen = false;
155
	user_name = attr.name;
156
	store_password(user_name, curr_host, attr.password);
157
end
158
function cb.streamclosed(session)
159
	session.notopen = true;
160
	user_name = "";
161
end
162
function cb.handlestanza(session, stanza)
163
	--print("Parsed stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
164
	if (stanza.name == "vCard") and (stanza.attr.xmlns == "vcard-temp") then
165
		store_vcard(user_name, curr_host, stanza);
166
	elseif (stanza.name == "query") then
167
		if (stanza.attr.xmlns == "jabber:iq:roster") then
168
			store_roster(user_name, curr_host, stanza);
169
		elseif (stanza.attr.xmlns == "jabber:iq:private") then
170
			store_private(user_name, curr_host, stanza);
171
		end
172
	elseif (stanza.name == "offline-messages") then
173
		store_offline_messages(user_name, curr_host, stanza);
174
	elseif (stanza.name == "presence") and (stanza.attr.xmlns == "jabber:client") then
175
		store_subscription_request(user_name, curr_host, stanza);
176
	else
177
		print("UNHANDLED stanza "..stanza.name.." xmlns: "..(stanza.attr.xmlns or ""));
178
	end
179
end
180
 
181
local user_handlers = new_xmpp_handlers({ notopen = true }, cb);
182
 
183
-----------------------------------------------------------------------
184
 
185
local lxp_handlers = {
186
	--count = 0
187
};
188
 
189
-- TODO: error handling for invalid opening elements if curr_host is empty
190
function lxp_handlers.StartElement(parser, elementname, attributes)
191
	local curr_ns, name = elementname:match(ns_pattern);
192
	if name == "" then
193
		curr_ns, name = "", curr_ns;
194
	end
195
	--io.write("+ ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
196
	--count = count + 1;
197
	if curr_host ~= "" then
198
		-- forward to xmlhandlers
199
		user_handlers:StartElement(elementname, attributes);
200
	elseif (curr_ns == xmlns_xep227) and (name == "host") then
201
		curr_host = attributes["jid"]; -- start of host element
202
		print("Begin parsing host "..curr_host);
203
	elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
204
		io.stderr:write("Unhandled XML element: ", name, "\n");
205
		os.exit(1);
206
	end
207
end
208
 
209
-- TODO: error handling for invalid closing elements if host is empty
210
function lxp_handlers.EndElement(parser, elementname)
211
	local curr_ns, name = elementname:match(ns_pattern);
212
	if name == "" then
213
		curr_ns, name = "", curr_ns;
214
	end
215
	--count = count - 1;
216
	--io.write("- ", string.rep(" ", count), name, "  (", curr_ns, ")", "\n")
217
	if curr_host ~= "" then
218
		if (curr_ns == xmlns_xep227) and (name == "host") then
219
			print("End parsing host "..curr_host);
220
			curr_host = "" -- end of host element
221
		else
222
			-- forward to xmlhandlers
223
			user_handlers:EndElement(elementname);
224
		end
225
	elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
226
		io.stderr:write("Unhandled XML element: ", name, "\n");
227
		os.exit(1);
228
	end
229
end
230
 
231
function lxp_handlers.CharacterData(parser, string)
232
	if curr_host ~= "" then
233
		-- forward to xmlhandlers
234
		user_handlers:CharacterData(string);
235
	end
236
end
237
 
238
-----------------------------------------------------------------------
239
 
240
local arg = ...;
241
local help = "/? -? ? /h -h /help -help --help";
242
if not arg or help:find(arg, 1, true) then
243
	print([[XEP-227 importer for Prosody
244
 
245
  Usage: xep227toprosody.lua filename.xml
246
 
247
]]);
248
	os.exit(1);
249
end
250
 
251
local file = io.open(arg);
252
if not file then
253
	io.stderr:write("Could not open file: ", arg, "\n");
254
	os.exit(0);
255
end
256
 
257
local parser = lxp.new(lxp_handlers, ns_separator);
258
for l in file:lines() do
259
	parser:parse(l);
260
end
261
parser:parse();
262
parser:close();
263
file:close();