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