| 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(); |