Comparison

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
comparison
equal deleted inserted replaced
226:efd66bcc62c1 227:31019cb93d59
1 -- Copyright (C) 2011 Kim Alvefur
2 --
3 -- This project is MIT/X11 licensed. Please see the
4 -- COPYING file in the source package for more information.
5 --
6
7 -- TODO
8 -- function lua_to_xep54()
9 -- function lua_to_text()
10 -- replace text_to_xep54() and xep54_to_text() with intermediate lua?
11
12 local st = verse or require "util.stanza";
13 local t_insert, t_concat = table.insert, table.concat;
14 local type = type;
15 local next, pairs, ipairs = next, pairs, ipairs;
16
17 module "vcard"
18
19 local vCard_dtd;
20
21 local function vCard_esc(s)
22 return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
23 end
24
25 local function vCard_unesc(s)
26 return s:gsub("\\?[\\nt:;,]", {
27 ["\\\\"] = "\\",
28 ["\\n"] = "\n",
29 ["\\t"] = "\t",
30 ["\\:"] = ":",
31 ["\\;"] = ";",
32 ["\\,"] = ",",
33 [":"] = "\29",
34 [";"] = "\30",
35 [","] = "\31",
36 });
37 end
38
39 function text_to_xep54(data)
40 --[[ TODO
41 return lua_to_xep54(text_to_lua(data));
42 --]]
43 data = data
44 :gsub("\r\n","\n")
45 :gsub("\n ", "")
46 :gsub("\n\n+","\n");
47 local c = st.stanza("xCard", { xmlns = "vcard-temp" });
48 for line in data:gmatch("[^\n]+") do
49 local line = vCard_unesc(line);
50 local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
51 value = value:gsub("\29",":");
52 line = nil;
53 if params and #params > 0 then
54 local _params = {};
55 for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
56 local _vt = {};
57 for _p in v:gmatch("[^\31]*") do
58 _vt[#_vt]=_p
59 _vt[_p]=true;
60 end
61 _params[k]=isval == "" or _vt;
62 end
63 params = _params;
64 end
65 if name == "BEGIN" and value == "VCARD" then
66 c:tag("vCard", { xmlns = "vcard-temp" });
67 elseif name == "END" and value == "VCARD" then
68 c:up();
69 elseif vCard_dtd[name] then
70 local dtd = vCard_dtd[name];
71 c:tag(name);
72 if dtd.types then
73 for _, t in ipairs(dtd.types) do
74 if ( params.TYPE and params.TYPE[t] == true)
75 or params[t] == true then
76 c:tag(t):up();
77 end
78 end
79 end
80 if dtd.props then
81 for _, p in ipairs(dtd.props) do
82 if params[p] then
83 if params[p] == true then
84 c:tag(p):up();
85 else
86 for _, prop in ipairs(params[p]) do
87 c:tag(p):text(prop):up();
88 end
89 end
90 end
91 end
92 end
93 if dtd == "text" then
94 c:text(value);
95 elseif dtd.value then
96 c:tag(dtd.value):text(value):up();
97 elseif dtd.values then
98 local values = dtd.values;
99 local i = 1;
100 local value = "\30"..value;
101 for p in value:gmatch("\30([^\30]*)") do
102 c:tag(values[i]):text(p):up();
103 if i < #values then
104 i = i + 1;
105 end
106 end
107 end
108 c:up();
109 end
110 end
111 return c;
112 end
113
114 function text_to_lua(data) --table
115 data = data
116 :gsub("\r\n","\n")
117 :gsub("\n ", "")
118 :gsub("\n\n+","\n");
119 local vCards = {};
120 local c; -- current item
121 for line in data:gmatch("[^\n]+") do
122 local line = vCard_unesc(line);
123 local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
124 value = value:gsub("\29",":");
125 line = nil;
126 if #params > 0 then
127 local _params = {};
128 for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
129 local _vt = {};
130 for _p in v:gmatch("[^\31]*") do
131 _vt[#_vt]=_p
132 _vt[_p]=true;
133 end
134 _params[k]=isval == "" or _vt;
135 end
136 params = _params;
137 end
138 if name == "BEGIN" and value == "VCARD" then
139 c = {};
140 vCards[#vCards+1] = c;
141 elseif name == "END" and value == "VCARD" then
142 c = nil;
143 elseif vCard_dtd[name] then
144 local dtd = vCard_dtd[name];
145 local p = { name = name };
146 c[#c+1]=p;
147 --c[name]=p;
148 local up = c;
149 c = p;
150 if dtd.types then
151 for _, t in ipairs(dtd.types) do
152 if ( params.TYPE and params.TYPE[t] == true)
153 or params[t] == true then
154 c.TYPE=t;
155 end
156 end
157 end
158 if dtd.props then
159 for _, p in ipairs(dtd.props) do
160 if params[p] then
161 if params[p] == true then
162 c[p]=true;
163 else
164 for _, prop in ipairs(params[p]) do
165 c[p]=prop;
166 end
167 end
168 end
169 end
170 end
171 if dtd == "text" or dtd.value then
172 t_insert(c, value);
173 elseif dtd.values then
174 local value = "\30"..value;
175 for p in value:gmatch("\30([^\30]*)") do
176 t_insert(c, p);
177 end
178 end
179 c = up;
180 end
181 end
182 return vCards;
183 end
184
185 local function vCard_prop(item) -- single item staza object to text line
186 local prop_name = item.name;
187 local prop_def = vCard_dtd[prop_name];
188 if not prop_def then return nil end
189
190 local value, params = "", {};
191
192 if prop_def == "text" then
193 value = item:get_text();
194 elseif type(prop_def) == "table" then
195 if prop_def.value then --single item
196 value = item:get_child_text(prop_def.value) or "";
197 elseif prop_def.values then --array
198 local value_names = prop_def.values;
199 value = {};
200 if value_names.behaviour == "repeat-last" then
201 for i=1,#item do
202 t_insert(value, item[i]:get_text() or "");
203 end
204 else
205 for i=1,#value_names do
206 t_insert(value, item:get_child_text(value_names[i]) or "");
207 end
208 end
209 elseif prop_def.names then
210 local names = prop_def.names;
211 for i=1,#names do
212 if item:get_child(names[i]) then
213 value = names[i];
214 break;
215 end
216 end
217 end
218
219 if prop_def.props_verbatim then
220 for k,v in pairs(prop_def.props_verbatim) do
221 params[k] = v;
222 end
223 end
224
225 if prop_def.types then
226 local types = prop_def.types;
227 params.TYPE = {};
228 for i=1,#types do
229 if item:get_child(types[i]) then
230 t_insert(params.TYPE, types[i]:lower());
231 end
232 end
233 if #params.TYPE == 0 then
234 params.TYPE = nil;
235 end
236 end
237
238 if prop_def.props then
239 local props = prop_def.props;
240 for i=1,#props do
241 local prop = props[i]
242 local p = item:get_child_text(prop);
243 if p then
244 params[prop] = params[prop] or {};
245 t_insert(params[prop], p);
246 end
247 end
248 end
249 else
250 return nil
251 end
252
253 if type(value) == "table" then
254 for i=1,#value do
255 value[i]=vCard_esc(value[i]);
256 end
257 value = t_concat(value, ";");
258 else
259 value = vCard_esc(value);
260 end
261
262 if next(params) then
263 local sparams = "";
264 for k,v in pairs(params) do
265 sparams = sparams .. (";%s=%s"):format(k, t_concat(v,","));
266 end
267 params = sparams;
268 else
269 params = "";
270 end
271
272 return ("%s%s:%s"):format(item.name, params, value)
273 :gsub(("."):rep(75), "%0\r\n "):gsub("\r\n $","");
274 end
275
276 function xep54_to_text(vCard)
277 --[[ TODO
278 return lua_to_text(xep54_to_lua(vCard))
279 --]]
280 local r = {};
281 t_insert(r, "BEGIN:VCARD");
282 for i = 1,#vCard do
283 local item = vCard[i];
284 if item.name then
285 local s = vCard_prop(item);
286 if s then
287 t_insert(r, s);
288 end
289 end
290 end
291 t_insert(r, "END:VCARD");
292 return t_concat(r, "\r\n");
293 end
294
295 local function xep54_item_to_lua(item)
296 local prop_name = item.name;
297 local prop_def = vCard_dtd[prop_name];
298 if not prop_def then return nil end
299
300 local prop = { name = prop_name };
301
302 if prop_def == "text" then
303 prop[1] = item:get_text();
304 elseif type(prop_def) == "table" then
305 if prop_def.value then --single item
306 prop[1] = item:get_child_text(prop_def.value) or "";
307 elseif prop_def.values then --array
308 local value_names = prop_def.values;
309 if value_names.behaviour == "repeat-last" then
310 for i=1,#item do
311 t_insert(prop, item[i]:get_text() or "");
312 end
313 else
314 for i=1,#value_names do
315 t_insert(prop, item:get_child_text(value_names[i]) or "");
316 end
317 end
318 elseif prop_def.names then
319 local names = prop_def.names;
320 for i=1,#names do
321 if item:get_child(names[i]) then
322 prop[1] = names[i];
323 break;
324 end
325 end
326 end
327
328 if prop_def.props_verbatim then
329 for k,v in pairs(prop_def.props_verbatim) do
330 prop[k] = v;
331 end
332 end
333
334 if prop_def.types then
335 local types = prop_def.types;
336 prop.TYPE = {};
337 for i=1,#types do
338 if item:get_child(types[i]) then
339 t_insert(prop.TYPE, types[i]:lower());
340 end
341 end
342 if #prop.TYPE == 0 then
343 prop.TYPE = nil;
344 end
345 end
346
347 -- A key-value pair, within a key-value pair?
348 if prop_def.props then
349 local params = prop_def.props;
350 for i=1,#params do
351 local name = params[i]
352 local data = item:get_child_text(prop_name);
353 if prop_text then
354 prop[prop_name] = prop[prop_name] or {};
355 t_insert(prop[prop_name], prop_text);
356 end
357 end
358 end
359 else
360 return nil
361 end
362
363 return prop;
364 end
365
366 local function xep54_vCard_to_lua(vCard)
367 local tags = vCard.tags;
368 local t = {};
369 for i=1,#tags do
370 t[i] = xep54_item_to_lua(tags[i]);
371 end
372 return t
373 end
374
375 function xep54_to_lua(vCard)
376 if vCard.attr.xmlns ~= "vcard-temp" then
377 return false
378 end
379 if vCard.name == "xCard" then
380 local t = {};
381 local vCards = vCard.tags;
382 for i=1,#vCards do
383 local ti = xep54_vCard_to_lua(vCards[i]);
384 t[i] = ti;
385 --t[ti.name] = ti;
386 end
387 return t
388 elseif vCard.name == "vCard" then
389 return xep54_vCard_to_lua(vCard)
390 end
391 end
392
393 -- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
394 vCard_dtd = {
395 VERSION = "text", --MUST be 3.0, so parsing is redundant
396 FN = "text",
397 N = {
398 values = {
399 "FAMILY",
400 "GIVEN",
401 "MIDDLE",
402 "PREFIX",
403 "SUFFIX",
404 },
405 },
406 NICKNAME = "text",
407 PHOTO = {
408 props_verbatim = { ENCODING = { "b" } },
409 props = { "TYPE" },
410 value = "BINVAL", --{ "EXTVAL", },
411 },
412 BDAY = "text",
413 ADR = {
414 types = {
415 "HOME",
416 "WORK",
417 "POSTAL",
418 "PARCEL",
419 "DOM",
420 "INTL",
421 "PREF",
422 },
423 values = {
424 "POBOX",
425 "EXTADD",
426 "STREET",
427 "LOCALITY",
428 "REGION",
429 "PCODE",
430 "CTRY",
431 }
432 },
433 LABEL = {
434 types = {
435 "HOME",
436 "WORK",
437 "POSTAL",
438 "PARCEL",
439 "DOM",
440 "INTL",
441 "PREF",
442 },
443 value = "LINE",
444 },
445 TEL = {
446 types = {
447 "HOME",
448 "WORK",
449 "VOICE",
450 "FAX",
451 "PAGER",
452 "MSG",
453 "CELL",
454 "VIDEO",
455 "BBS",
456 "MODEM",
457 "ISDN",
458 "PCS",
459 "PREF",
460 },
461 value = "NUMBER",
462 },
463 EMAIL = {
464 types = {
465 "HOME",
466 "WORK",
467 "INTERNET",
468 "PREF",
469 "X400",
470 },
471 value = "USERID",
472 },
473 JABBERID = "text",
474 MAILER = "text",
475 TZ = "text",
476 GEO = {
477 values = {
478 "LAT",
479 "LON",
480 },
481 },
482 TITLE = "text",
483 ROLE = "text",
484 LOGO = "copy of PHOTO",
485 AGENT = "text",
486 ORG = {
487 values = {
488 behaviour = "repeat-last",
489 "ORGNAME",
490 "ORGUNIT",
491 }
492 },
493 CATEGORIES = {
494 values = "KEYWORD",
495 },
496 NOTE = "text",
497 PRODID = "text",
498 REV = "text",
499 SORTSTRING = "text",
500 SOUND = "copy of PHOTO",
501 UID = "text",
502 URL = "text",
503 CLASS = {
504 names = { -- The item.name is the value if it's one of these.
505 "PUBLIC",
506 "PRIVATE",
507 "CONFIDENTIAL",
508 },
509 },
510 KEY = {
511 props = { "TYPE" },
512 value = "CRED",
513 },
514 DESC = "text",
515 };
516 vCard_dtd.LOGO = vCard_dtd.PHOTO;
517 vCard_dtd.SOUND = vCard_dtd.PHOTO;
518 return _M