1: -module(smart_markers_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("common_test/include/ct.hrl").
    5: -include_lib("stdlib/include/assert.hrl").
    6: -include_lib("escalus/include/escalus_xmlns.hrl").
    7: -include_lib("exml/include/exml.hrl").
    8: -include("muc_light.hrl").
    9: -define(NS_ESL_SMART_MARKERS, <<"esl:xmpp:smart-markers:0">>).
   10: -define(NS_STANZAID, <<"urn:xmpp:sid:0">>).
   11: 
   12: -import(distributed_helper, [mim/0, rpc/4, subhost_pattern/1]).
   13: -import(domain_helper, [host_type/0]).
   14: -import(config_parser_helper, [mod_config/2]).
   15: 
   16: %%% Suite configuration
   17: all() ->
   18:     case (not ct_helper:is_ct_running())
   19:          orelse mongoose_helper:is_rdbms_enabled(host_type()) of
   20:         true -> all_cases();
   21:         false -> {skip, require_rdbms}
   22:     end.
   23: 
   24: all_cases() ->
   25:     [
   26:      {group, regular},
   27:      {group, async_pools}
   28:     ].
   29: 
   30: groups() ->
   31:     inbox_helper:maybe_run_in_parallel(groups1()) ++ groups2().
   32: 
   33: groups1() ->
   34:     [
   35:      {one2one, [],
   36:       [
   37:        error_set_iq,
   38:        error_bad_peer,
   39:        error_no_peer_given,
   40:        error_bad_timestamp,
   41:        marker_is_stored,
   42:        marker_can_be_fetched,
   43:        marker_for_thread_can_be_fetched,
   44:        marker_after_timestamp_can_be_fetched,
   45:        marker_after_timestamp_for_threadid_can_be_fetched,
   46:        remove_markers_when_removed_user,
   47:        repeated_markers_produce_no_warnings
   48:       ]},
   49:      {muclight, [],
   50:       [
   51:        marker_is_stored_for_room,
   52:        marker_can_be_fetched_for_room,
   53:        marker_is_removed_when_user_leaves_room,
   54:        markers_are_removed_when_room_is_removed
   55:       ]},
   56:      {keep_private, [],
   57:       [
   58:        marker_is_not_routed_nor_fetchable,
   59:        fetching_room_answers_only_own_marker
   60:       ]}
   61:     ].
   62: 
   63: groups2() ->
   64:     [
   65:      {regular, [],
   66:       [
   67:        {group, one2one},
   68:        {group, muclight},
   69:        {group, keep_private}
   70:       ]},
   71:      {async_pools, [],
   72:       [
   73:        {group, one2one},
   74:        {group, muclight},
   75:        {group, keep_private}
   76:       ]}
   77:     ].
   78: 
   79: suite() ->
   80:     escalus:suite().
   81: 
   82: init_per_suite(Config) ->
   83:     escalus:init_per_suite(Config).
   84: 
   85: end_per_suite(Config) ->
   86:     escalus:end_per_suite(Config).
   87: 
   88: init_per_group(regular, Config) ->
   89:     [{merge_opts, #{backend => rdbms}} | Config];
   90: init_per_group(async_pools, Config) ->
   91:     [{merge_opts, #{backend => rdbms_async,
   92:                     async_writer => #{pool_size => 2}}} | Config];
   93: init_per_group(GroupName, Config) ->
   94:     AsyncType = ?config(merge_opts, Config),
   95:     HostType = domain_helper:host_type(),
   96:     Config1 = dynamic_modules:save_modules(HostType, Config),
   97:     ok = dynamic_modules:ensure_modules(HostType, group_to_module(GroupName, AsyncType)),
   98:     Config1.
   99: 
  100: group_to_module(one2one, MergeOpts) ->
  101:     [{mod_smart_markers, mod_config(mod_smart_markers, MergeOpts)}];
  102: group_to_module(keep_private, MergeOpts) ->
  103:     [{mod_smart_markers, mod_config(mod_smart_markers, MergeOpts#{keep_private => true})},
  104:      {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}];
  105: group_to_module(muclight, MergeOpts) ->
  106:     [{mod_smart_markers, mod_config(mod_smart_markers, MergeOpts)},
  107:      {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}].
  108: 
  109: end_per_group(muclight, Config) ->
  110:     muc_light_helper:clear_db(host_type()),
  111:     end_per_group(generic, Config);
  112: end_per_group(_, Config) ->
  113:     escalus_fresh:clean(),
  114:     dynamic_modules:restore_modules(Config),
  115:     Config.
  116: 
  117: init_per_testcase(repeated_markers_produce_no_warnings = TC, Config) ->
  118:     logger_ct_backend:start(),
  119:     escalus:init_per_testcase(TC, Config);
  120: init_per_testcase(Name, Config) ->
  121:     escalus:init_per_testcase(Name, Config).
  122: 
  123: end_per_testcase(repeated_markers_produce_no_warnings = TC, Config) ->
  124:     logger_ct_backend:stop(),
  125:     escalus:end_per_testcase(TC, Config);
  126: end_per_testcase(Name, Config) ->
  127:     escalus:end_per_testcase(Name, Config).
  128: 
  129: %%% tests
  130: error_set_iq(Config) ->
  131:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  132:         Query = escalus_stanza:query_el(?NS_ESL_SMART_MARKERS, []),
  133:         Iq = escalus_stanza:iq(<<"set">>, [Query]),
  134:         escalus:send(Alice, Iq),
  135:         Response = escalus:wait_for_stanza(Alice),
  136:         escalus:assert(is_iq_error, [Iq], Response)
  137:     end).
  138: 
  139: error_bad_peer(Config) ->
  140:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  141:         Iq = iq_fetch_marker([{<<"peer">>, <<"/@">>}]),
  142:         escalus:send(Alice, Iq),
  143:         Response = escalus:wait_for_stanza(Alice),
  144:         escalus:assert(is_iq_error, [Iq], Response)
  145:     end).
  146: 
  147: error_no_peer_given(Config) ->
  148:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  149:         Iq = iq_fetch_marker([]),
  150:         escalus:send(Alice, Iq),
  151:         Response = escalus:wait_for_stanza(Alice),
  152:         escalus:assert(is_iq_error, [Iq], Response)
  153:     end).
  154: 
  155: error_bad_timestamp(Config) ->
  156:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  157:         PeerJid = <<"peer@localhost">>,
  158:         Iq = iq_fetch_marker([{<<"peer">>, PeerJid}, {<<"after">>, <<"baddate">>}]),
  159:         escalus:send(Alice, Iq),
  160:         Response = escalus:wait_for_stanza(Alice),
  161:         escalus:assert(is_iq_error, [Iq], Response)
  162:     end).
  163: 
  164: marker_is_stored(Config) ->
  165:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  166:         send_message_respond_marker(Alice, Bob),
  167:         AliceJid = jid:from_binary(escalus_client:full_jid(Alice)),
  168:         BobJid = jid:from_binary(escalus_client:full_jid(Bob)),
  169:         mongoose_helper:wait_until(
  170:           fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) > 0 end, true)
  171:     end).
  172: 
  173: marker_can_be_fetched(Config) ->
  174:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  175:         send_message_respond_marker(Alice, Bob),
  176:         send_message_respond_marker(Bob, Alice),
  177:         verify_marker_fetch(Bob, Alice),
  178:         verify_marker_fetch(Alice, Bob)
  179:     end).
  180: 
  181: marker_is_not_routed_nor_fetchable(Config) ->
  182:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  183:         MsgId = escalus_stanza:id(),
  184:         Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, <<"Hello!">>), MsgId),
  185:         escalus:send(Alice, Msg),
  186:         escalus:wait_for_stanza(Bob),
  187:         ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId),
  188:         escalus:send(Bob, ChatMarker),
  189:         escalus_assert:has_no_stanzas(Alice), %% Marker is filtered, Alice won't receive the marker
  190:         verify_marker_fetch_is_empty(Alice, Bob), %% Alice won't see Bob's marker
  191:         verify_marker_fetch(Bob, Alice) %% Bob will see his own marker
  192:     end).
  193: 
  194: fetching_room_answers_only_own_marker(Config) ->
  195:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  196:         Users = [Alice, Bob, Kate],
  197:         RoomId = create_room(Alice, [Bob, Kate], Config),
  198:         RoomBinJid = muc_light_helper:room_bin_jid(RoomId),
  199:         send_msg_to_room(Users, RoomBinJid, Alice, escalus_stanza:id()),
  200:         send_msg_to_room(Users, RoomBinJid, Bob, escalus_stanza:id()),
  201:         MsgId = send_msg_to_room(Users, RoomBinJid, Kate, escalus_stanza:id()),
  202:         ChatMarker = escalus_stanza:setattr(
  203:                        escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, MsgId),
  204:                        <<"type">>, <<"groupchat">>),
  205:         [ begin
  206:               escalus:send(User, ChatMarker),
  207:               {ok, MarkersThatUserHasInRoom} = verify_marker_fetch(User, RoomBinJid),
  208:               ?assertEqual(1, length(MarkersThatUserHasInRoom))
  209:           end || User <- [Alice, Bob] ]
  210:     end).
  211: 
  212: marker_for_thread_can_be_fetched(Config) ->
  213:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  214:         ThreadId = <<"some-thread-id">>,
  215:         send_message_respond_marker(Alice, Bob),
  216:         send_message_respond_marker(Alice, Bob, ThreadId),
  217:         verify_marker_fetch(Bob, Alice, ThreadId, undefined)
  218:     end).
  219: 
  220: marker_after_timestamp_can_be_fetched(Config) ->
  221:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  222:         TS = rpc(mim(), erlang, system_time, [microsecond]),
  223:         BinTS = list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])),
  224:         send_message_respond_marker(Alice, Bob),
  225:         send_message_respond_marker(Alice, Bob),
  226:         verify_marker_fetch(Bob, Alice, undefined, BinTS)
  227:     end).
  228: 
  229: marker_after_timestamp_for_threadid_can_be_fetched(Config) ->
  230:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  231:         ThreadId = <<"some-thread-id">>,
  232:         TS = rpc(mim(), erlang, system_time, [microsecond]),
  233:         BinTS = list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])),
  234:         send_message_respond_marker(Alice, Bob),
  235:         send_message_respond_marker(Alice, Bob, ThreadId),
  236:         verify_marker_fetch(Bob, Alice, ThreadId, BinTS)
  237:     end).
  238: 
  239: remove_markers_when_removed_user(Config) ->
  240:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  241:         Body = <<"Hello Bob!">>,
  242:         MsgId = escalus_stanza:id(),
  243:         Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, Body), MsgId),
  244:         escalus:send(Alice, Msg),
  245:         escalus:wait_for_stanza(Bob),
  246:         ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId),
  247:         escalus:send(Bob, ChatMarker),
  248:         escalus:wait_for_stanza(Alice),
  249:         AliceJid = jid:from_binary(escalus_client:full_jid(Alice)),
  250:         BobJid = jid:from_binary(escalus_client:full_jid(Bob)),
  251:         mongoose_helper:wait_until(fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) > 0 end, true),
  252:         unregister_user(Bob),
  253:         mongoose_helper:wait_until(fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) end, 0)
  254:     end).
  255: 
  256: repeated_markers_produce_no_warnings(Config) ->
  257:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  258:         MsgId = escalus_stanza:id(),
  259:         Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, <<"Hello!">>), MsgId),
  260:         escalus:send(Alice, Msg),
  261:         escalus:wait_for_stanza(Bob),
  262:         ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId),
  263:         [MarkerEl] = ChatMarker#xmlel.children,
  264:         RepeatedChatMarker = ChatMarker#xmlel{children =  [MarkerEl, MarkerEl]},
  265:         logger_ct_backend:capture(warning),
  266:         escalus:send(Bob, RepeatedChatMarker),
  267:         escalus:wait_for_stanza(Alice),
  268:         logger_ct_backend:stop_capture(),
  269:         FilterFun = fun(_, WMsg) -> re:run(WMsg, "{bad_result,{updated,0}}") /= nomatch end,
  270:         [] = logger_ct_backend:recv(FilterFun)
  271:     end).
  272: 
  273: marker_is_stored_for_room(Config) ->
  274:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  275:                         fun(Alice, Bob, Kate) ->
  276:         Users = [Alice, Bob, Kate],
  277:         RoomId = create_room(Alice, [Bob, Kate], Config),
  278:         RoomBinJid = muc_light_helper:room_bin_jid(RoomId),
  279:         one_marker_in_room(Users, RoomBinJid, Alice, Bob),
  280:         BobJid = jid:from_binary(escalus_client:full_jid(Bob)),
  281:         mongoose_helper:wait_until(
  282:           fun() -> length(fetch_markers_for_users(BobJid, jid:from_binary(RoomBinJid))) > 0 end, true)
  283:     end).
  284: 
  285: marker_can_be_fetched_for_room(Config) ->
  286:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  287:                         fun(Alice, Bob, Kate) ->
  288:         Users = [Alice, Bob, Kate],
  289:         RoomId = create_room(Alice, [Bob, Kate], Config),
  290:         RoomBinJid = muc_light_helper:room_bin_jid(RoomId),
  291:         one_marker_in_room(Users, RoomBinJid, Alice, Bob),
  292:         verify_marker_fetch(Bob, RoomBinJid)
  293:     end).
  294: 
  295: marker_is_removed_when_user_leaves_room(Config) ->
  296:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}],
  297:                         fun(Alice, Bob) ->
  298:         Users = [Alice, Bob],
  299:         RoomId = create_room(Alice, [Bob], Config),
  300:         RoomBinJid = muc_light_helper:room_bin_jid(RoomId),
  301:         RoomJid = jid:from_binary(RoomBinJid),
  302:         one_marker_in_room(Users, RoomBinJid, Alice, Bob),
  303:         BobJid = jid:from_binary(escalus_client:full_jid(Bob)),
  304:         mongoose_helper:wait_until(
  305:           fun() -> length(fetch_markers_for_users(BobJid, RoomJid)) > 0 end, true),
  306:         % Remove Bob from the room
  307:         muc_light_helper:user_leave(RoomId, Bob, [Alice]),
  308:         mongoose_helper:wait_until(
  309:           fun() -> length(fetch_markers_for_users(BobJid, RoomJid)) end, 0)
  310:     end).
  311: 
  312: markers_are_removed_when_room_is_removed(Config) ->
  313:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  314:         Users = [Alice, Bob],
  315:         RoomId = create_room(Alice, [Bob], Config),
  316:         RoomBinJid = muc_light_helper:room_bin_jid(RoomId),
  317:         RoomJid = jid:from_binary(RoomBinJid),
  318:         one_marker_in_room(Users, RoomBinJid, Alice, Bob),
  319:         BobJid = jid:from_binary(escalus_client:full_jid(Bob)),
  320:         mongoose_helper:wait_until(
  321:           fun() -> length(fetch_markers_for_users(BobJid, RoomJid)) > 0 end, true),
  322:         %% The room is then deleted
  323:         delete_room(Alice, Users, RoomBinJid),
  324:         [ begin
  325:               Jid = jid:from_binary(escalus_client:full_jid(User)),
  326:               mongoose_helper:wait_until(
  327:                 fun() -> length(fetch_markers_for_users(Jid, RoomJid)) end, 0)
  328:           end || User <- Users ]
  329:     end).
  330: 
  331: %%% helpers
  332: fetch_markers_for_users(From, To) ->
  333:     MRs = rpc(mim(), mod_smart_markers_backend, get_chat_markers,
  334:               [host_type(), To, undefined, 0]),
  335:     [MR || #{from := FR} = MR <- MRs, jid:are_bare_equal(From, FR)].
  336: 
  337: iq_fetch_marker(Attrs) ->
  338:     Query = escalus_stanza:query_el(?NS_ESL_SMART_MARKERS, Attrs, []),
  339:     escalus_stanza:iq(<<"get">>, [Query]).
  340: 
  341: create_room(Owner, Members, Config) ->
  342:     RoomId = muc_helper:fresh_room_name(),
  343:     MucHost = muc_light_helper:muc_host(),
  344:     muc_light_helper:create_room(RoomId, MucHost, Owner, Members, Config, muc_light_helper:ver(1)),
  345:     RoomId.
  346: 
  347: delete_room(Owner, Users, RoomBinJid) ->
  348:     Destroy = escalus_stanza:to(escalus_stanza:iq_set(?NS_MUC_LIGHT_DESTROY, []), RoomBinJid),
  349:     escalus:send(Owner, Destroy),
  350:     AffUsersChanges = [{User, none} || User <- Users ],
  351:     muc_light_helper:verify_aff_bcast([], AffUsersChanges, [?NS_MUC_LIGHT_DESTROY]),
  352:     escalus:assert(is_iq_result, escalus:wait_for_stanza(Owner)).
  353: 
  354: one_marker_in_room(Users, RoomBinJid, Writer, Marker) ->
  355:     MsgId = escalus_stanza:id(),
  356:     StanzaId = send_msg_to_room(Users, RoomBinJid, Writer, MsgId),
  357:     mark_msg(Users, RoomBinJid, Marker, StanzaId).
  358: 
  359: send_msg_to_room(Users, RoomBinJid, Writer, MsgId) ->
  360:     Msg = escalus_stanza:set_id(escalus_stanza:groupchat_to(RoomBinJid, <<"Hello">>), MsgId),
  361:     escalus:send(Writer, Msg),
  362:     Msgs = [ escalus:wait_for_stanza(User) || User <- Users ],
  363:     get_id(hd(Msgs), MsgId).
  364: 
  365: mark_msg(Users, RoomBinJid, Marker, StanzaId) ->
  366:     ChatMarker = escalus_stanza:setattr(
  367:                    escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, StanzaId),
  368:                    <<"type">>, <<"groupchat">>),
  369:     escalus:send(Marker, ChatMarker),
  370:     [ escalus:wait_for_stanza(User) || User <- Users ],
  371:     StanzaId.
  372: 
  373: send_message_respond_marker(MsgWriter, MarkerAnswerer) ->
  374:     send_message_respond_marker(MsgWriter, MarkerAnswerer, undefined).
  375: 
  376: send_message_respond_marker(MsgWriter, MarkerAnswerer, MaybeThread) ->
  377:     MsgId = escalus_stanza:id(),
  378:     Msg = add_thread_id(escalus_stanza:set_id(
  379:                           escalus_stanza:chat_to(MarkerAnswerer, <<"Hello!">>),
  380:                           MsgId),
  381:                         MaybeThread),
  382:     escalus:send(MsgWriter, Msg),
  383:     escalus:wait_for_stanza(MarkerAnswerer),
  384:     ChatMarker = add_thread_id(escalus_stanza:chat_marker(
  385:                                  MsgWriter, <<"displayed">>, MsgId),
  386:                                MaybeThread),
  387:     escalus:send(MarkerAnswerer, ChatMarker),
  388:     escalus:wait_for_stanza(MsgWriter).
  389: 
  390: verify_marker_fetch(MarkingUser, MarkedUser) ->
  391:     verify_marker_fetch(MarkingUser, MarkedUser, undefined, undefined).
  392: 
  393: verify_marker_fetch(MarkingUser, MarkedUser, Thread, After) ->
  394:         MarkedUserBJid = case is_binary(MarkedUser) of
  395:                              true -> [{<<"peer">>, MarkedUser}];
  396:                              false -> [{<<"peer">>, escalus_utils:jid_to_lower(escalus_client:short_jid(MarkedUser))}]
  397:                          end,
  398:         MaybeThread = case Thread of
  399:                           undefined -> [];
  400:                           _ -> [{<<"thread">>, Thread}]
  401:                       end,
  402:         MaybeAfter = case After of
  403:                           undefined -> [];
  404:                           _ -> [{<<"after">>, After}]
  405:                       end,
  406:         Iq = iq_fetch_marker(MarkedUserBJid ++ MaybeThread ++ MaybeAfter),
  407:         %% using wait_until to ensure that assertions are eventually passing
  408:         mongoose_helper:wait_until(
  409:           fun() ->
  410:                   escalus:send(MarkingUser, Iq),
  411:                   Response = escalus:wait_for_stanza(MarkingUser),
  412:                   escalus:assert(is_iq_result, [Iq], Response),
  413:                   Markers = [Marker | _] = exml_query:paths(
  414:                                              Response, [{element_with_ns, <<"query">>, ?NS_ESL_SMART_MARKERS},
  415:                                                         {element, <<"marker">>}]),
  416:                   ?assertNotEqual(undefined, Marker),
  417:                   ?assertNotEqual(undefined, exml_query:attr(Marker, <<"timestamp">>)),
  418:                   ?assertEqual(<<"displayed">>, exml_query:attr(Marker, <<"type">>)),
  419:                   ?assertEqual(Thread, exml_query:attr(Marker, <<"thread">>)),
  420:                   ?assertNotEqual(undefined, exml_query:attr(Marker, <<"id">>)),
  421:                   lists:sort(Markers)
  422:           end,
  423:           fun(_) -> true end, %% always positively validate the return value.
  424:           #{name => fetch_marker}).
  425: 
  426: verify_marker_fetch_is_empty(MarkingUser, MarkedUser) ->
  427:         MarkedUserBJid = escalus_utils:jid_to_lower(escalus_client:short_jid(MarkedUser)),
  428:         Iq = iq_fetch_marker([{<<"peer">>, MarkedUserBJid}]),
  429:         escalus:send(MarkingUser, Iq),
  430:         Response = escalus:wait_for_stanza(MarkingUser),
  431:         escalus:assert(is_iq_result, [Iq], Response),
  432:         Markers = exml_query:paths(Response, [{element_with_ns, <<"query">>, ?NS_ESL_SMART_MARKERS},
  433:                                               {element, <<"marker">>}]),
  434:         ?assertEqual([], Markers).
  435: 
  436: get_id(Packet, Def) ->
  437:     exml_query:path(
  438:       Packet, [{element_with_ns, <<"stanza-id">>, ?NS_STANZAID}, {attr, <<"id">>}], Def).
  439: 
  440: add_thread_id(Msg, undefined) ->
  441:     Msg;
  442: add_thread_id(#xmlel{children = Children} = Msg, ThreadID) ->
  443:     ThreadEl = #xmlel{name = <<"thread">>,
  444:                       children = [#xmlcdata{content = ThreadID}]},
  445:     Msg#xmlel{children = [ThreadEl | Children]}.
  446: 
  447: unregister_user(Client) ->
  448:     Jid = jid:from_binary(escalus_client:short_jid(Client)),
  449:     rpc(mim(), ejabberd_auth, remove_user, [Jid]).