Software /
code /
verse
Diff
util/vcard.lua @ 227:31019cb93d59
util.vcard: Add util for converting vCard3 to/from XEP 54
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sun, 06 Nov 2011 20:03:20 +0100 |
child | 298:1897dc6a07bb |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/vcard.lua Sun Nov 06 20:03:20 2011 +0100 @@ -0,0 +1,518 @@ +-- Copyright (C) 2011 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +-- TODO +-- function lua_to_xep54() +-- function lua_to_text() +-- replace text_to_xep54() and xep54_to_text() with intermediate lua? + +local st = verse or require "util.stanza"; +local t_insert, t_concat = table.insert, table.concat; +local type = type; +local next, pairs, ipairs = next, pairs, ipairs; + +module "vcard" + +local vCard_dtd; + +local function vCard_esc(s) + return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n"); +end + +local function vCard_unesc(s) + return s:gsub("\\?[\\nt:;,]", { + ["\\\\"] = "\\", + ["\\n"] = "\n", + ["\\t"] = "\t", + ["\\:"] = ":", + ["\\;"] = ";", + ["\\,"] = ",", + [":"] = "\29", + [";"] = "\30", + [","] = "\31", + }); +end + +function text_to_xep54(data) + --[[ TODO + return lua_to_xep54(text_to_lua(data)); + --]] + data = data + :gsub("\r\n","\n") + :gsub("\n ", "") + :gsub("\n\n+","\n"); + local c = st.stanza("xCard", { xmlns = "vcard-temp" }); + for line in data:gmatch("[^\n]+") do + local line = vCard_unesc(line); + local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); + value = value:gsub("\29",":"); + line = nil; + if params and #params > 0 then + local _params = {}; + for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do + local _vt = {}; + for _p in v:gmatch("[^\31]*") do + _vt[#_vt]=_p + _vt[_p]=true; + end + _params[k]=isval == "" or _vt; + end + params = _params; + end + if name == "BEGIN" and value == "VCARD" then + c:tag("vCard", { xmlns = "vcard-temp" }); + elseif name == "END" and value == "VCARD" then + c:up(); + elseif vCard_dtd[name] then + local dtd = vCard_dtd[name]; + c:tag(name); + if dtd.types then + for _, t in ipairs(dtd.types) do + if ( params.TYPE and params.TYPE[t] == true) + or params[t] == true then + c:tag(t):up(); + end + end + end + if dtd.props then + for _, p in ipairs(dtd.props) do + if params[p] then + if params[p] == true then + c:tag(p):up(); + else + for _, prop in ipairs(params[p]) do + c:tag(p):text(prop):up(); + end + end + end + end + end + if dtd == "text" then + c:text(value); + elseif dtd.value then + c:tag(dtd.value):text(value):up(); + elseif dtd.values then + local values = dtd.values; + local i = 1; + local value = "\30"..value; + for p in value:gmatch("\30([^\30]*)") do + c:tag(values[i]):text(p):up(); + if i < #values then + i = i + 1; + end + end + end + c:up(); + end + end + return c; +end + +function text_to_lua(data) --table + data = data + :gsub("\r\n","\n") + :gsub("\n ", "") + :gsub("\n\n+","\n"); + local vCards = {}; + local c; -- current item + for line in data:gmatch("[^\n]+") do + local line = vCard_unesc(line); + local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); + value = value:gsub("\29",":"); + line = nil; + if #params > 0 then + local _params = {}; + for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do + local _vt = {}; + for _p in v:gmatch("[^\31]*") do + _vt[#_vt]=_p + _vt[_p]=true; + end + _params[k]=isval == "" or _vt; + end + params = _params; + end + if name == "BEGIN" and value == "VCARD" then + c = {}; + vCards[#vCards+1] = c; + elseif name == "END" and value == "VCARD" then + c = nil; + elseif vCard_dtd[name] then + local dtd = vCard_dtd[name]; + local p = { name = name }; + c[#c+1]=p; + --c[name]=p; + local up = c; + c = p; + if dtd.types then + for _, t in ipairs(dtd.types) do + if ( params.TYPE and params.TYPE[t] == true) + or params[t] == true then + c.TYPE=t; + end + end + end + if dtd.props then + for _, p in ipairs(dtd.props) do + if params[p] then + if params[p] == true then + c[p]=true; + else + for _, prop in ipairs(params[p]) do + c[p]=prop; + end + end + end + end + end + if dtd == "text" or dtd.value then + t_insert(c, value); + elseif dtd.values then + local value = "\30"..value; + for p in value:gmatch("\30([^\30]*)") do + t_insert(c, p); + end + end + c = up; + end + end + return vCards; +end + +local function vCard_prop(item) -- single item staza object to text line + local prop_name = item.name; + local prop_def = vCard_dtd[prop_name]; + if not prop_def then return nil end + + local value, params = "", {}; + + if prop_def == "text" then + value = item:get_text(); + elseif type(prop_def) == "table" then + if prop_def.value then --single item + value = item:get_child_text(prop_def.value) or ""; + elseif prop_def.values then --array + local value_names = prop_def.values; + value = {}; + if value_names.behaviour == "repeat-last" then + for i=1,#item do + t_insert(value, item[i]:get_text() or ""); + end + else + for i=1,#value_names do + t_insert(value, item:get_child_text(value_names[i]) or ""); + end + end + elseif prop_def.names then + local names = prop_def.names; + for i=1,#names do + if item:get_child(names[i]) then + value = names[i]; + break; + end + end + end + + if prop_def.props_verbatim then + for k,v in pairs(prop_def.props_verbatim) do + params[k] = v; + end + end + + if prop_def.types then + local types = prop_def.types; + params.TYPE = {}; + for i=1,#types do + if item:get_child(types[i]) then + t_insert(params.TYPE, types[i]:lower()); + end + end + if #params.TYPE == 0 then + params.TYPE = nil; + end + end + + if prop_def.props then + local props = prop_def.props; + for i=1,#props do + local prop = props[i] + local p = item:get_child_text(prop); + if p then + params[prop] = params[prop] or {}; + t_insert(params[prop], p); + end + end + end + else + return nil + end + + if type(value) == "table" then + for i=1,#value do + value[i]=vCard_esc(value[i]); + end + value = t_concat(value, ";"); + else + value = vCard_esc(value); + end + + if next(params) then + local sparams = ""; + for k,v in pairs(params) do + sparams = sparams .. (";%s=%s"):format(k, t_concat(v,",")); + end + params = sparams; + else + params = ""; + end + + return ("%s%s:%s"):format(item.name, params, value) + :gsub(("."):rep(75), "%0\r\n "):gsub("\r\n $",""); +end + +function xep54_to_text(vCard) + --[[ TODO + return lua_to_text(xep54_to_lua(vCard)) + --]] + local r = {}; + t_insert(r, "BEGIN:VCARD"); + for i = 1,#vCard do + local item = vCard[i]; + if item.name then + local s = vCard_prop(item); + if s then + t_insert(r, s); + end + end + end + t_insert(r, "END:VCARD"); + return t_concat(r, "\r\n"); +end + +local function xep54_item_to_lua(item) + local prop_name = item.name; + local prop_def = vCard_dtd[prop_name]; + if not prop_def then return nil end + + local prop = { name = prop_name }; + + if prop_def == "text" then + prop[1] = item:get_text(); + elseif type(prop_def) == "table" then + if prop_def.value then --single item + prop[1] = item:get_child_text(prop_def.value) or ""; + elseif prop_def.values then --array + local value_names = prop_def.values; + if value_names.behaviour == "repeat-last" then + for i=1,#item do + t_insert(prop, item[i]:get_text() or ""); + end + else + for i=1,#value_names do + t_insert(prop, item:get_child_text(value_names[i]) or ""); + end + end + elseif prop_def.names then + local names = prop_def.names; + for i=1,#names do + if item:get_child(names[i]) then + prop[1] = names[i]; + break; + end + end + end + + if prop_def.props_verbatim then + for k,v in pairs(prop_def.props_verbatim) do + prop[k] = v; + end + end + + if prop_def.types then + local types = prop_def.types; + prop.TYPE = {}; + for i=1,#types do + if item:get_child(types[i]) then + t_insert(prop.TYPE, types[i]:lower()); + end + end + if #prop.TYPE == 0 then + prop.TYPE = nil; + end + end + + -- A key-value pair, within a key-value pair? + if prop_def.props then + local params = prop_def.props; + for i=1,#params do + local name = params[i] + local data = item:get_child_text(prop_name); + if prop_text then + prop[prop_name] = prop[prop_name] or {}; + t_insert(prop[prop_name], prop_text); + end + end + end + else + return nil + end + + return prop; +end + +local function xep54_vCard_to_lua(vCard) + local tags = vCard.tags; + local t = {}; + for i=1,#tags do + t[i] = xep54_item_to_lua(tags[i]); + end + return t +end + +function xep54_to_lua(vCard) + if vCard.attr.xmlns ~= "vcard-temp" then + return false + end + if vCard.name == "xCard" then + local t = {}; + local vCards = vCard.tags; + for i=1,#vCards do + local ti = xep54_vCard_to_lua(vCards[i]); + t[i] = ti; + --t[ti.name] = ti; + end + return t + elseif vCard.name == "vCard" then + return xep54_vCard_to_lua(vCard) + end +end + +-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd +vCard_dtd = { + VERSION = "text", --MUST be 3.0, so parsing is redundant + FN = "text", + N = { + values = { + "FAMILY", + "GIVEN", + "MIDDLE", + "PREFIX", + "SUFFIX", + }, + }, + NICKNAME = "text", + PHOTO = { + props_verbatim = { ENCODING = { "b" } }, + props = { "TYPE" }, + value = "BINVAL", --{ "EXTVAL", }, + }, + BDAY = "text", + ADR = { + types = { + "HOME", + "WORK", + "POSTAL", + "PARCEL", + "DOM", + "INTL", + "PREF", + }, + values = { + "POBOX", + "EXTADD", + "STREET", + "LOCALITY", + "REGION", + "PCODE", + "CTRY", + } + }, + LABEL = { + types = { + "HOME", + "WORK", + "POSTAL", + "PARCEL", + "DOM", + "INTL", + "PREF", + }, + value = "LINE", + }, + TEL = { + types = { + "HOME", + "WORK", + "VOICE", + "FAX", + "PAGER", + "MSG", + "CELL", + "VIDEO", + "BBS", + "MODEM", + "ISDN", + "PCS", + "PREF", + }, + value = "NUMBER", + }, + EMAIL = { + types = { + "HOME", + "WORK", + "INTERNET", + "PREF", + "X400", + }, + value = "USERID", + }, + JABBERID = "text", + MAILER = "text", + TZ = "text", + GEO = { + values = { + "LAT", + "LON", + }, + }, + TITLE = "text", + ROLE = "text", + LOGO = "copy of PHOTO", + AGENT = "text", + ORG = { + values = { + behaviour = "repeat-last", + "ORGNAME", + "ORGUNIT", + } + }, + CATEGORIES = { + values = "KEYWORD", + }, + NOTE = "text", + PRODID = "text", + REV = "text", + SORTSTRING = "text", + SOUND = "copy of PHOTO", + UID = "text", + URL = "text", + CLASS = { + names = { -- The item.name is the value if it's one of these. + "PUBLIC", + "PRIVATE", + "CONFIDENTIAL", + }, + }, + KEY = { + props = { "TYPE" }, + value = "CRED", + }, + DESC = "text", +}; +vCard_dtd.LOGO = vCard_dtd.PHOTO; +vCard_dtd.SOUND = vCard_dtd.PHOTO; +return _M