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