1: %% @doc This suite tests both old ejabberd_commands module, which is slowly getting deprecated, 2: %% and the new mongoose_commands implementation. 3: -module(commands_SUITE). 4: -compile([export_all, nowarn_export_all]). 5: 6: -include_lib("exml/include/exml.hrl"). 7: -include_lib("eunit/include/eunit.hrl"). 8: -include("ejabberd_commands.hrl"). 9: -include("jlib.hrl"). 10: 11: -define(PRT(X, Y), ct:pal("~p: ~p", [X, Y])). 12: 13: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 14: %%%% suite configuration 15: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 16: 17: all() -> 18: [ 19: {group, old_commands}, 20: {group, new_commands} 21: ]. 22: 23: groups() -> 24: [ 25: {old_commands, [sequence], 26: [old_list, 27: old_exec, 28: old_access_ctl 29: ] 30: }, 31: {new_commands, [sequence], 32: [new_type_checker, 33: new_reg_unreg, 34: new_failedreg, 35: new_list, 36: new_execute, 37: different_types, 38: errors_are_readable 39: ] 40: } 41: ]. 42: 43: init_per_suite(C) -> 44: application:ensure_all_started(jid), 45: ok = mnesia:start(), 46: C. 47: 48: end_per_suite(_) -> 49: mnesia:stop(), 50: mnesia:delete_schema([node()]), 51: ok. 52: 53: init_per_group(old_commands, C) -> 54: Pid = spawn(fun ec_holder/0), 55: [{helper_proc, Pid} | C]; 56: init_per_group(new_commands, C) -> 57: Pid = spawn(fun mc_holder/0), 58: [{helper_proc, Pid} | C]. 59: 60: end_per_group(old_commands, C) -> 61: ejabberd_commands:unregister_commands(commands_old()), 62: stop_helper_proc(C), 63: C; 64: end_per_group(new_commands, C) -> 65: mongoose_commands:unregister(commands_new()), 66: stop_helper_proc(C), 67: C. 68: 69: stop_helper_proc(C) -> 70: Pid = proplists:get_value(helper_proc, C), 71: Pid ! stop. 72: 73: init_per_testcase(_, C) -> 74: [mongoose_config:set_opt(Key, Value) || {Key, Value} <- opts()], 75: meck:new(ejabberd_auth_dummy, [non_strict]), 76: meck:expect(ejabberd_auth_dummy, get_password_s, fun(_, _) -> <<"">> end), 77: meck:new(mongoose_domain_api), 78: meck:expect(mongoose_domain_api, get_domain_host_type, fun(H) -> {ok, H} end), 79: C. 80: 81: end_per_testcase(_, _C) -> 82: [mongoose_config:unset_opt(Key) || {Key, _Value} <- opts()], 83: meck:unload(). 84: 85: opts() -> 86: [{{auth, <<"localhost">>}, #{methods => [dummy]}}, 87: {{access, <<"localhost">>}, #{experts_only => [#{acl => coder, value => allow}, 88: #{acl => manager, value => allow}, 89: #{acl => all, value => deny}]}}, 90: {{acl, <<"localhost">>}, #{coder => [#{user => <<"zenek">>, match => current_domain}]}}]. 91: 92: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 93: %%%% test methods 94: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 95: 96: 97: old_list(_C) -> 98: %% list 99: Rlist = ejabberd_commands:list_commands(), 100: {command_one, _, "do nothing and return"} = proplists:lookup(command_one, Rlist), 101: %% get definition 102: Rget = ejabberd_commands:get_command_definition(command_one), 103: % we should get back exactly the definition we provided 104: [Cone | _] = commands_old(), 105: Cone = Rget, 106: %% get interface 107: {Argspec, Retspec} = ejabberd_commands:get_command_format(command_one), 108: [{msg, binary}] = Argspec, 109: {res, restuple} = Retspec, 110: %% list by tags 111: Tagcomm = ejabberd_commands:get_tags_commands(), 112: ?assertEqual(length(proplists:get_value("one", Tagcomm)), 1), 113: ?assertEqual(length(proplists:get_value("two", Tagcomm)), 2), 114: ?assertEqual(length(proplists:get_value("three", Tagcomm)), 1), 115: ok. 116: 117: old_exec(_C) -> 118: %% execute 119: <<"bzzzz">> = ejabberd_commands:execute_command(command_one, [<<"bzzzz">>]), 120: Res = ejabberd_commands:execute_command(command_one, [123]), 121: ?PRT("invalid type ignored", Res), %% there is no arg type check 122: Res2 = ejabberd_commands:execute_command(command_two, [123]), 123: ?PRT("invalid return type ignored", Res2), %% nor return 124: %% execute unknown command 125: {error, command_unknown} = ejabberd_commands:execute_command(command_seven, [123]), 126: ok. 127: 128: old_access_ctl(_C) -> 129: %% with no auth method it is all fine 130: checkauth(true, [], noauth), 131: %% noauth fails if first item is not 'all' (users) 132: checkauth(account_unprivileged, [{none, none, []}], noauth), 133: %% if here we allow all commands to noauth 134: checkauth(true, [{all, all, []}], noauth), 135: %% and here only command_one 136: checkauth(true, [{all, [command_one], []}], noauth), 137: %% so this'd fail 138: checkauth(account_unprivileged, [{all, [command_two], []}], noauth), 139: % now we provide a role name, this requires a user and triggers password and acl check 140: % this fails because password is bad 141: checkauth(invalid_account_data, [{some_acl_role, [command_one], []}], {<<"zenek">>, <<"localhost">>, <<"bbb">>}), 142: % this, because of acl 143: checkauth(account_unprivileged, [{some_acl_role, [command_one], []}], {<<"zenek">>, <<"localhost">>, <<"">>}), 144: % and this should work, because we define command_one as available to experts only, while acls in config 145: % (see ggo/1) state that experts-only funcs are available to coders and managers, and zenek is a coder, gah. 146: checkauth(true, [{experts_only, [command_one], []}], {<<"zenek">>, <<"localhost">>, <<"">>}), 147: ok. 148: 149: 150: new_type_checker(_C) -> 151: true = t_check_type({msg, binary}, <<"zzz">>), 152: true = t_check_type({msg, integer}, 127), 153: {false, _} = t_check_type({{a, binary}, {b, integer}}, 127), 154: true = t_check_type({{a, binary}, {b, integer}}, {<<"z">>, 127}), 155: true = t_check_type({ok, {msg, integer}}, {ok, 127}), 156: true = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>}), 157: {false, _} = t_check_type({k, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>}), 158: {false, _} = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, "z"}), 159: {false, _} = t_check_type({ok, {msg, integer}, {val, binary}}, {ok, 127, <<"z">>, 333}), 160: true = t_check_type([integer], []), 161: true = t_check_type([integer], [1, 2, 3]), 162: {false, _} = t_check_type([integer], [1, <<"z">>, 3]), 163: true = t_check_type([], [1, 2, 3]), 164: true = t_check_type([], []), 165: true = t_check_type({msg, boolean}, true), 166: true = t_check_type({msg, boolean}, false), 167: {false, _} = t_check_type({msg, boolean}, <<"true">>), 168: ok. 169: 170: t_check_type(Spec, Value) -> 171: R = try mongoose_commands:check_type(argument, Spec, Value) of 172: true -> true 173: catch 174: E -> 175: {false, E} 176: end, 177: R. 178: 179: new_reg_unreg(_C) -> 180: L1 = length(commands_new()), 181: L2 = L1 + length(commands_new_temp()), 182: ?assertEqual(length(mongoose_commands:list(admin)), L1), 183: mongoose_commands:register(commands_new_temp()), 184: ?assertEqual(length(mongoose_commands:list(admin)), L2), 185: mongoose_commands:unregister(commands_new_temp()), 186: ?assertEqual(length(mongoose_commands:list(admin)), L1), 187: ok. 188: 189: failedreg([]) -> ok; 190: failedreg([Cmd|Tail]) -> 191: ?assertThrow({invalid_command_definition, _}, mongoose_commands:register([Cmd])), 192: failedreg(Tail). 193: 194: new_failedreg(_C) -> 195: failedreg(commands_new_lame()). 196: 197: 198: new_list(_C) -> 199: %% for admin 200: Rlist = mongoose_commands:list(admin), 201: [Cmd] = [C || C <- Rlist, mongoose_commands:name(C) == command_one], 202: command_one = mongoose_commands:name(Cmd), 203: <<"do nothing and return">> = mongoose_commands:desc(Cmd), 204: %% list by category 205: [_] = mongoose_commands:list(admin, <<"user">>), 206: [] = mongoose_commands:list(admin, <<"nocategory">>), 207: %% list by category and action 208: [_] = mongoose_commands:list(admin, <<"user">>, read), 209: [] = mongoose_commands:list(admin, <<"user">>, update), 210: %% get definition 211: Rget = mongoose_commands:get_command(admin, command_one), 212: command_one = mongoose_commands:name(Rget), 213: read = mongoose_commands:action(Rget), 214: [] = mongoose_commands:identifiers(Rget), 215: {error, denied, _} = mongoose_commands:get_command(ujid(), command_one), 216: %% list for a user 217: Ulist = mongoose_commands:list(ujid()), 218: [UCmd] = [UC || UC <- Ulist, mongoose_commands:name(UC) == command_foruser], 219: 220: command_foruser = mongoose_commands:name(UCmd), 221: URget = mongoose_commands:get_command(ujid(), command_foruser), 222: command_foruser = mongoose_commands:name(URget), 223: ok. 224: 225: 226: new_execute(_C) -> 227: {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, command_one, [<<"bzzzz">>]), 228: Cmd = mongoose_commands:get_command(admin, command_one), 229: {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, Cmd, [<<"bzzzz">>]), 230: %% call with a map 231: {ok, <<"bzzzz">>} = mongoose_commands:execute(admin, command_one, #{msg => <<"bzzzz">>}), 232: %% command which returns just ok 233: ok = mongoose_commands:execute(admin, command_noreturn, [<<"bzzzz">>]), 234: %% this user has no permissions 235: {error, denied, _} = mongoose_commands:execute(ujid(), command_one, [<<"bzzzz">>]), 236: %% command is not registered 237: {error, not_implemented, _} = mongoose_commands:execute(admin, command_seven, [<<"bzzzz">>]), 238: %% invalid arguments 239: {error, type_error, _} = mongoose_commands:execute(admin, command_one, [123]), 240: {error, type_error, _} = mongoose_commands:execute(admin, command_one, []), 241: {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{}), 242: {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{msg => 123}), 243: {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{notthis => <<"bzzzz">>}), 244: {error, type_error, _} = mongoose_commands:execute(admin, command_one, #{msg => <<"bzzzz">>, redundant => 123}), 245: %% backend func throws exception 246: {error, internal, _} = mongoose_commands:execute(admin, command_one, [<<"throw">>]), 247: %% backend func returns error 248: {error, internal, <<"byleco">>} = mongoose_commands:execute(admin, command_one, [<<"error">>]), 249: % user executes his command 250: {ok, <<"bzzzz">>} = mongoose_commands:execute(ujid(), command_foruser, #{msg => <<"bzzzz">>}), 251: % a caller arg 252: % called by admin 253: {ok, <<"admin@localhost/zbzzzz">>} = mongoose_commands:execute(admin, 254: command_withcaller, 255: #{caller => <<"admin@localhost/z">>, 256: msg => <<"bzzzz">>}), 257: % called by user 258: {ok, <<"zenek@localhost/zbzzzz">>} = mongoose_commands:execute(<<"zenek@localhost">>, 259: command_withcaller, 260: #{caller => <<"zenek@localhost/z">>, 261: msg => <<"bzzzz">>}), 262: % call by user but jids do not match 263: {error, denied, _} = mongoose_commands:execute(<<"wacek@localhost">>, 264: command_withcaller, 265: #{caller => <<"zenek@localhost/z">>, 266: msg => <<"bzzzz">>}), 267: {ok, 30} = mongoose_commands:execute(admin, command_withoptargs, #{msg => <<"a">>}), 268: {ok, 18} = mongoose_commands:execute(admin, command_withoptargs, #{msg => <<"a">>, value => 6}), 269: ok. 270: 271: different_types(_C) -> 272: mongoose_commands:register(commands_new_temp2()), 273: {ok, <<"response1">>} = mongoose_commands:execute(admin, command_two, [10, 15]), 274: {ok, <<"response2">>} = mongoose_commands:execute(admin, command_three, [10, <<"binary">>]), 275: mongoose_commands:unregister(commands_new_temp2()), 276: ok. 277: 278: errors_are_readable(_C) -> 279: {error, internal, TextBin} = mongoose_commands:execute(admin, make_error, [<<"oops">>]), 280: Map = parse_binary_term(TextBin), 281: [<<"oops">>] = maps:get(args, Map), 282: admin = maps:get(caller, Map), 283: error = maps:get(class, Map), 284: make_error = maps:get(command_name, Map), 285: <<"oops">> = maps:get(reason, Map), 286: [_|_] = maps:get(stacktrace, Map), 287: command_failed = maps:get(what, Map), 288: ok. 289: 290: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 291: %%%% definitions 292: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 293: 294: commands_new() -> 295: [ 296: [ 297: {name, command_one}, 298: {category, <<"user">>}, 299: {desc, <<"do nothing and return">>}, 300: {module, ?MODULE}, 301: {function, cmd_one}, 302: {action, read}, 303: {args, [{msg, binary}]}, 304: {result, {msg, binary}} 305: ], 306: [ 307: {name, command_noreturn}, 308: {category, <<"message">>}, 309: {desc, <<"do nothing and return nothing">>}, 310: {module, ?MODULE}, 311: {function, cmd_one}, 312: {action, create}, 313: {args, [{msg, binary}]}, 314: {result, ok} 315: ], 316: [ 317: {name, command_foruser}, 318: {category, <<"another">>}, 319: {desc, <<"this is available for a user">>}, 320: {module, ?MODULE}, 321: {function, cmd_one}, 322: {action, read}, 323: {security_policy, [user]}, 324: {args, [{msg, binary}]}, 325: {result, {msg, binary}} 326: ], 327: [ 328: {name, command_withoptargs}, 329: {category, <<"yetanother">>}, 330: {desc, <<"this is available for a user">>}, 331: {module, ?MODULE}, 332: {function, cmd_one_withvalue}, 333: {action, read}, 334: {security_policy, [user]}, 335: {args, [{msg, binary}]}, 336: {optargs, [{value, integer, 10}]}, 337: {result, {nvalue, integer}} 338: ], 339: [ 340: {name, command_withcaller}, 341: {category, <<"another">>}, 342: {desc, <<"this has a 'caller' argument, returns caller ++ msg">>}, 343: {module, ?MODULE}, 344: {function, cmd_concat}, 345: {action, create}, 346: {security_policy, [user]}, 347: {args, [{caller, binary}, {msg, binary}]}, 348: {result, {msg, binary}} 349: ], 350: [ 351: {name, make_error}, 352: {category, <<"testing">>}, 353: {desc, <<"Just to test an error">>}, 354: {module, erlang}, 355: {function, error}, 356: {action, read}, 357: {args, [{error, binary}]}, 358: {result, []} 359: ] 360: ]. 361: 362: commands_new_temp() -> 363: %% this is to check registering/unregistering commands 364: [ 365: [ 366: {name, command_temp}, 367: {category, <<"user">>}, 368: {desc, <<"do nothing and return">>}, 369: {module, ?MODULE}, 370: {function, cmd_one}, 371: {action, create}, % different action 372: {args, [{msg, binary}]}, 373: {result, {msg, binary}} 374: ], 375: [ 376: {name, command_one_arity}, 377: {category, <<"user">>}, 378: {desc, <<"do nothing and return">>}, 379: {module, ?MODULE}, 380: {function, cmd_one}, 381: {action, read}, 382: {args, [{msg, binary}, {whatever, integer}]}, % different arity 383: {result, {msg, binary}} 384: ], 385: [ 386: {name, command_one_two}, 387: {category, <<"user">>}, 388: {subcategory, <<"rosters">>}, % has subcategory 389: {desc, <<"do nothing and return">>}, 390: {module, ?MODULE}, 391: {function, cmd_one}, 392: {action, read}, 393: {args, [{msg, binary}]}, 394: {result, {msg, binary}} 395: ], 396: [ 397: {name, command_temp2}, 398: {category, <<"user">>}, 399: {desc, <<"this one specifies identifiers">>}, 400: {module, ?MODULE}, 401: {function, cmd_one}, 402: {action, update}, 403: {identifiers, [ident]}, 404: {args, [{ident, integer}, {msg, binary}]}, 405: {result, {msg, binary}} 406: ] 407: ]. 408: 409: commands_new_temp2() -> 410: %% This is for extra test with different arg types 411: [ 412: [ 413: {name, command_two}, 414: {category, <<"animals">>}, 415: {desc, <<"some">>}, 416: {module, ?MODULE}, 417: {function, the_same_types}, 418: {action, read}, 419: {args, [{one, integer}, {two, integer}]}, 420: {result, {msg, binary}} 421: ], 422: [ 423: {name, command_three}, 424: {category, <<"music">>}, 425: {desc, <<"two args, different types">>}, 426: {module, ?MODULE}, 427: {function, different_types}, 428: {action, read}, 429: {args, [{one, integer}, {two, binary}]}, 430: {result, {msg, binary}} 431: ] 432: ]. 433: 434: commands_new_lame() -> 435: [ 436: [ 437: {name, command_one} % missing values 438: ], 439: [ 440: {name, command_one}, 441: {category, []} %% should be binary 442: ], 443: [ 444: {name, command_one}, 445: {category, <<"user">>}, 446: {desc, <<"do nothing and return">>}, 447: {module, ?MODULE}, 448: {function, cmd_one}, 449: {action, andnowforsomethingcompletelydifferent} %% not one of allowed values 450: ], 451: [ 452: {name, command_one}, 453: {category, <<"user">>}, 454: {desc, <<"do nothing and return">>}, 455: {module, ?MODULE}, 456: {function, cmd_one}, 457: {action, delete}, 458: {args, [{msg, binary}, integer]}, %% args have to be a flat list of named arguments 459: {result, {msg, binary}} 460: ], 461: %% We do not crash if command is already registered because some modules are loaded more then once 462: %% [ 463: %% {name, command_one}, %% everything is fine, but it is already registered 464: %% {category, another}, 465: %% {desc, "do nothing and return"}, 466: %% {module, ?MODULE}, 467: %% {function, cmd_one}, 468: %% {action, read}, 469: %% {args, [{msg, binary}]}, 470: %% {result, {msg, binary}} 471: %% ], 472: [ 473: {name, command_one}, 474: {category, <<"another">>}, 475: {desc, <<"do nothing and return">>}, 476: {module, ?MODULE}, 477: {function, cmd_one}, 478: {action, update}, %% an 'update' command has to specify identifiers 479: {args, [{msg, binary}]}, 480: {result, {msg, binary}} 481: ], 482: [ 483: {name, command_one}, 484: {category, <<"another">>}, 485: {desc, <<"do nothing and return">>}, 486: {module, ?MODULE}, 487: {function, cmd_one}, 488: {action, update}, 489: {identifiers, [1]}, %% ...and they must be atoms... 490: {args, [{msg, binary}]}, 491: {result, {msg, binary}} 492: ], 493: [ 494: {name, command_one}, 495: {category, <<"another">>}, 496: {desc, <<"do nothing and return">>}, 497: {module, ?MODULE}, 498: {function, cmd_one}, 499: {action, update}, 500: {identifiers, [ident]}, %% ...which are present in args 501: {args, [{msg, binary}]}, 502: {result, {msg, binary}} 503: ], 504: [ 505: {name, command_seven}, %% name is different... 506: {category, <<"user">>}, 507: {desc, <<"do nothing and return">>}, 508: {module, ?MODULE}, 509: {function, cmd_one}, 510: {action, read}, %% ...but another command with the same category and action and arity is already registered 511: {args, [{msg, binary}]}, 512: {result, {msg, binary}} 513: ], 514: [ 515: {name, command_seven}, 516: {category, <<"user">>}, 517: {desc, <<"do nothing and return">>}, 518: {module, ?MODULE}, 519: {function, cmd_one}, 520: {action, delete}, 521: {security_policy, [wrong]}, % invalid security definition 522: {args, [{msg, binary}]}, 523: {result, {msg, binary}} 524: %% ], 525: %% [ 526: %% {name, command_seven}, 527: %% {category, user}, 528: %% {desc, "do nothing and return"}, 529: %% {module, ?MODULE}, 530: %% {function, cmd_one}, 531: %% {action, delete}, 532: %% {security_policy, []}, % invalid security definition 533: %% {args, [{msg, binary}]}, 534: %% {result, {msg, binary}} 535: ] 536: ]. 537: 538: commands_old() -> 539: [ 540: #ejabberd_commands{name = command_one, tags = [one], 541: desc = "do nothing and return", 542: module = ?MODULE, function = cmd_one, 543: args = [{msg, binary}], 544: result = {res, restuple}}, 545: #ejabberd_commands{name = command_two, tags = [two], 546: desc = "this returns wrong type", 547: module = ?MODULE, function = cmd_two, 548: args = [{msg, binary}], 549: result = {res, restuple}}, 550: #ejabberd_commands{name = command_three, tags = [two, three], 551: desc = "do nothing and return", 552: module = ?MODULE, function = cmd_three, 553: args = [{msg, binary}], 554: result = {res, restuple}} 555: ]. 556: 557: cmd_one(<<"throw">>) -> 558: C = 12, 559: <<"A", C/binary>>; 560: cmd_one(<<"error">>) -> 561: {error, internal, <<"byleco">>}; 562: cmd_one(M) -> 563: M. 564: 565: cmd_one_withvalue(_Msg, Value) -> 566: Value * 3. 567: 568: cmd_two(M) -> 569: M. 570: 571: the_same_types(10, 15) -> 572: <<"response1">>; 573: the_same_types(_, _) -> 574: <<"wrong response">>. 575: 576: different_types(10, <<"binary">>) -> 577: <<"response2">>; 578: different_types(_, _) -> 579: <<"wrong content">>. 580: 581: cmd_concat(A, B) -> 582: <<A/binary, B/binary>>. 583: 584: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 585: %%%% utilities 586: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 587: 588: %% this is a bit stupid, but we need a process which would hold ets table 589: ec_holder() -> 590: ejabberd_commands:init(), 591: ejabberd_commands:register_commands(commands_old()), 592: receive 593: _ -> ok 594: end. 595: 596: mc_holder() -> 597: % we have to do it here to avoid race condition and random failures 598: {ok, Pid} = gen_hook:start_link(), 599: mongoose_commands:init(), 600: mongoose_commands:register(commands_new()), 601: receive 602: _ -> ok 603: end, 604: erlang:exit(Pid, kill). 605: 606: checkauth(true, AccessCommands, Auth) -> 607: B = <<"bzzzz">>, 608: B = ejabberd_commands:execute_command(AccessCommands, Auth, command_one, [B]); 609: checkauth(ErrMess, AccessCommands, Auth) -> 610: B = <<"bzzzz">>, 611: {error, ErrMess} = ejabberd_commands:execute_command(AccessCommands, Auth, command_one, [B]). 612: 613: ujid() -> 614: <<"zenek@localhost/k">>. 615: %% #jid{user = <<"zenek">>, server = <<"localhost">>, resource = "k", 616: %% luser = <<"zenek">>, lserver = <<"localhost">>, lresource = "k"}. 617: 618: parse_binary_term(TextBin) -> 619: {ok, Tokens, _} = erl_scan:string(binary_to_list(TextBin) ++ "."), 620: {ok, Abstract} = erl_parse:parse_exprs(Tokens), 621: {value, Value, _} = erl_eval:exprs(Abstract, erl_eval:new_bindings()), 622: Value.