diff --git a/big_tests/tests/rest_client_SUITE.erl b/big_tests/tests/rest_client_SUITE.erl index 141e7dcf616..0e75d5e3e89 100644 --- a/big_tests/tests/rest_client_SUITE.erl +++ b/big_tests/tests/rest_client_SUITE.erl @@ -19,10 +19,12 @@ -import(config_parser_helper, [mod_config/2]). -define(OK, {<<"200">>, <<"OK">>}). +-define(CREATED, {<<"201">>, <<"Created">>}). -define(NOCONTENT, {<<"204">>, <<"No Content">>}). --define(NOT_FOUND, {<<"404">>, _}). --define(NOT_IMPLEMENTED, {<<"501">>, _}). +-define(NOT_FOUND, {<<"404">>, <<"Not Found">>}). +-define(BAD_REQUEST, {<<"400">>, <<"Bad Request">>}). -define(UNAUTHORIZED, {<<"401">>, <<"Unauthorized">>}). +-define(FORBIDDEN, {<<"403">>, <<"Forbidden">>}). %% -------------------------------------------------------------------- %% Common Test stuff @@ -32,43 +34,49 @@ all() -> [{group, messages}, {group, muc}, {group, muc_config}, + {group, muc_disabled}, {group, roster}, {group, messages_with_props}, + {group, messages_with_thread}, {group, security}]. groups() -> [{messages_with_props, [parallel], message_with_props_test_cases()}, {messages_with_thread, [parallel], message_with_thread_test_cases()}, {messages, [parallel], message_test_cases()}, - {muc, [pararell], muc_test_cases()}, + {muc, [parallel], muc_test_cases()}, {muc_config, [], muc_config_cases()}, + {muc_disabled, [parallel], muc_disabled_cases()}, {roster, [parallel], roster_test_cases()}, {security, [], security_test_cases()}]. message_test_cases() -> [msg_is_sent_and_delivered_over_xmpp, msg_is_sent_and_delivered_over_sse, + message_sending_errors, all_messages_are_archived, messages_with_user_are_archived, - messages_can_be_paginated]. + messages_can_be_paginated, + message_query_errors]. muc_test_cases() -> [room_is_created, room_is_created_with_given_identifier, + room_creation_errors, + room_query_errors, user_is_invited_to_a_room, user_is_removed_from_a_room, + user_removal_errors, rooms_can_be_listed, owner_can_leave_a_room_and_auto_select_owner, user_can_leave_a_room, - invitation_to_room_is_forbidden_for_non_memeber, + invitation_to_room_is_forbidden_for_non_member, msg_is_sent_and_delivered_in_room, - sending_message_with_wrong_body_results_in_bad_request, - sending_message_with_no_body_results_in_bad_request, - sending_markable_message_with_no_body_results_in_bad_request, - sending_message_not_in_JSON_results_in_bad_request, sending_message_by_not_room_member_results_in_forbidden, + sending_invalid_message_to_room_results_in_bad_request, messages_are_archived_in_room, chat_markers_are_archived_in_room, + room_message_query_errors, markable_property_is_archived_in_room, only_room_participant_can_read_messages, messages_can_be_paginated_in_room, @@ -85,16 +93,21 @@ muc_config_cases() -> [ config_can_be_changed_by_owner, config_cannot_be_changed_by_member, + config_cannot_be_changed_by_non_member, + config_change_errors, config_can_be_changed_by_all ]. +muc_disabled_cases() -> + [muc_disabled_errors]. + roster_test_cases() -> [add_contact_and_invite, add_contact_and_be_invited, add_and_remove, add_and_remove_some_contacts_properly, add_and_remove_some_contacts_with_nonexisting, - break_stuff]. + roster_errors]. message_with_props_test_cases() -> [ @@ -108,7 +121,7 @@ message_with_thread_test_cases() -> [msg_with_thread_is_sent_and_delivered_over_xmpp, msg_with_thread_can_be_parsed, msg_with_thread_and_parent_is_sent_and_delivered_over_xmpp, - msg_with_thread_and_parent_can_be_parse, + msg_with_thread_and_parent_can_be_parsed, msg_without_thread_can_be_parsed, msg_without_thread_is_sent_and_delivered_over_xmpp]. @@ -135,11 +148,18 @@ init_modules(Config) -> dynamic_modules:ensure_modules(HostType, required_modules(suite)), Config2. -init_per_group(_GN, C) -> - C. - -end_per_group(_GN, C) -> - C. +init_per_group(muc_disabled = GN, Config) -> + HostType = host_type(), + Config1 = dynamic_modules:save_modules(HostType, Config), + dynamic_modules:ensure_modules(HostType, required_modules(GN)), + Config1; +init_per_group(_GN, Config) -> + Config. + +end_per_group(muc_disabled, Config) -> + dynamic_modules:restore_modules(Config); +end_per_group(_GN, _Config) -> + ok. init_per_testcase(config_can_be_changed_by_all = TC, Config) -> HostType = host_type(), @@ -161,9 +181,9 @@ init_per_testcase(TC, Config) -> msg_with_malformed_props_can_be_parsed, msg_with_malformed_props_is_sent_and_delivered_over_xmpp, msg_with_thread_is_sent_and_delivered_over_xmpp, - msg_with_thread_can_be_parse, + msg_with_thread_can_be_parsed, msg_with_thread_and_parent_is_sent_and_delivered_over_xmpp, - msg_with_thread_and_parent_can_be_parse, + msg_with_thread_and_parent_can_be_parsed, msg_without_thread_can_be_parsed, msg_without_thread_is_sent_and_delivered_over_xmpp ], @@ -178,6 +198,8 @@ end_per_testcase(TC, C) -> %% Module configuration - set up per suite and for special test cases %% TODO: include MAM configuration here +required_modules(muc_disabled) -> + [{mod_muc_light, stopped}]; required_modules(SuiteOrTC) -> Opts = maps:merge(common_muc_light_opts(), muc_light_opts(SuiteOrTC)), [{mod_muc_light, mod_config(mod_muc_light, Opts)}]. @@ -216,6 +238,19 @@ msg_is_sent_and_delivered_over_sse(ConfigIn) -> stop_sse(Conn). +message_sending_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + BobJID = user_jid(Bob), + M = #{to => BobJID, body => <<"hello, ", BobJID/binary, " it's me">>}, + Cred = credentials({alice, Alice}), + {?BAD_REQUEST, <<"Missing message body">>} = + post(client, <<"/messages">>, maps:remove(body, M), Cred), + {?BAD_REQUEST, <<"Missing recipient JID">>} = + post(client, <<"/messages">>, maps:remove(to, M), Cred), + {?BAD_REQUEST, <<"Invalid recipient JID">>} = + post(client, <<"/messages">>, M#{to => <<"@invalid">>}, Cred) + end). + room_msg_is_sent_and_delivered_over_sse(ConfigIn) -> Config = escalus_fresh:create_users(ConfigIn, [{alice, 1}, {bob, 1}]), Bob = escalus_users:get_userspec(Config, bob), @@ -255,7 +290,7 @@ all_messages_are_archived(Config) -> AliceJID = maps:get(to, M1), AliceCreds = {AliceJID, user_password(alice)}, GetPath = lists:flatten("/messages/"), - {{<<"200">>, <<"OK">>}, Msgs} = rest_helper:gett(client, GetPath, AliceCreds), + {?OK, Msgs} = rest_helper:gett(client, GetPath, AliceCreds), Received = [_Msg1, _Msg2, _Msg3] = rest_helper:decode_maplist(Msgs), assert_messages(Sent, Received) @@ -268,7 +303,7 @@ messages_with_user_are_archived(Config) -> KateJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Kate)), AliceCreds = {AliceJID, user_password(alice)}, GetPath = lists:flatten(["/messages/", binary_to_list(KateJID)]), - {{<<"200">>, <<"OK">>}, Msgs} = rest_helper:gett(client, GetPath, AliceCreds), + {?OK, Msgs} = rest_helper:gett(client, GetPath, AliceCreds), Recv = [_Msg2] = rest_helper:decode_maplist(Msgs), assert_messages([M3], Recv) @@ -299,6 +334,21 @@ messages_can_be_paginated(Config) -> <<"B">> = maps:get(body, Oldest2) end). +message_query_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), + Creds = credentials({alice, Alice}), + Path = <<"/messages/", BobJID/binary>>, + {?BAD_REQUEST, <<"Invalid interlocutor JID">>} = + rest_helper:gett(client, <<"/messages/@invalid">>, Creds), + {?BAD_REQUEST, <<"Invalid limit">>} = + rest_helper:gett(client, <>, Creds), + {?BAD_REQUEST, <<"Invalid value of 'before'">>} = + rest_helper:gett(client, <>, Creds), + {?BAD_REQUEST, <<"Invalid query string">>} = + rest_helper:gett(client, <>, Creds) + end). + room_is_created(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> RoomID = given_new_room({alice, Alice}), @@ -306,6 +356,23 @@ room_is_created(Config) -> assert_room_info(Alice, RoomInfo) end). +room_query_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + RoomID = given_new_room_with_users({alice, Alice}, []), + Creds = credentials({bob, Bob}), + {?NOT_FOUND, <<"Room not found">>} = + rest_helper:gett(client, <<"/rooms/badroom">>, Creds), + {?FORBIDDEN, _} = + rest_helper:gett(client, <<"/rooms/", RoomID/binary>>, Creds) + end). + +muc_disabled_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Creds = credentials({alice, Alice}), + {?NOT_FOUND, <<"MUC Light server not found">>} = + rest_helper:gett(client, <<"/rooms/badroom">>, Creds) + end). + room_is_created_with_given_identifier(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> GivenRoomID = muc_helper:fresh_room_name(), @@ -314,6 +381,26 @@ room_is_created_with_given_identifier(Config) -> assert_room_info(Alice, RoomInfo) end). +room_creation_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + RoomID = muc_helper:fresh_room_name(), + Room = #{name => <<"My Room">>, subject => <<"My Secrets">>}, + Path = <<"/rooms/", RoomID/binary>>, + Creds = credentials({alice, Alice}), + {?BAD_REQUEST, <<"Missing room ID">>} = + putt(client, "/rooms", Room, Creds), + {?BAD_REQUEST, <<"Invalid room ID">>} = + putt(client, "/rooms/@invalid", Room, Creds), + {?BAD_REQUEST, <<"Missing room name">>} = + putt(client, Path, maps:remove(name, Room), Creds), + {?BAD_REQUEST, <<"Missing room subject">>} = + putt(client, Path, maps:remove(subject, Room), Creds), + {?CREATED, _} = + putt(client, Path, Room, Creds), + {?FORBIDDEN, _} = + putt(client, Path, Room, Creds) + end). + config_can_be_changed_by_owner(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> RoomID = muc_helper:fresh_room_name(), @@ -333,10 +420,32 @@ config_cannot_be_changed_by_member(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> RoomID = given_new_room_with_users({alice, Alice}, [{bob, Bob}]), RoomJID = room_jid(RoomID, Config), - {{<<"403">>,<<"Forbidden">>},<<>>} = + {?FORBIDDEN, _} = when_config_change({bob, Bob}, RoomJID, <<"other_name">>, <<"other_subject">>), NewRoomInfo = get_room_info({bob, Bob}, RoomID), - assert_property_value(<<"name">>,<<"new_room_name">>, NewRoomInfo) + assert_property_value(<<"name">>, <<"new_room_name">>, NewRoomInfo) + end). + +config_cannot_be_changed_by_non_member(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + RoomID = given_new_room_with_users({alice, Alice}, []), + RoomJID = room_jid(RoomID, Config), + {?FORBIDDEN, _} = + when_config_change({bob, Bob}, RoomJID, <<"other_name">>, <<"other_subject">>), + NewRoomInfo = get_room_info({alice, Alice}, RoomID), + assert_property_value(<<"name">>, <<"new_room_name">>, NewRoomInfo) + end). + +config_change_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + RoomID = given_new_room_with_users({alice, Alice}, []), + RoomJID = room_jid(RoomID, Config), + {?NOT_FOUND, _} = + when_config_change({alice, Alice}, <<"badroom">>, <<"other_name">>, <<"other_subject">>), + {?BAD_REQUEST, <<"Validation failed ", _/binary>>} = + when_config_change({alice, Alice}, RoomJID, <<"other_name">>, 123), + NewRoomInfo = get_room_info({alice, Alice}, RoomID), + assert_property_value(<<"name">>, <<"new_room_name">>, NewRoomInfo) end). config_can_be_changed_by_all(Config) -> @@ -383,6 +492,20 @@ user_is_removed_from_a_room(Config) -> assert_aff_change_stanza(Stanza, Bob, <<"none">>) end). +user_removal_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + RoomID = given_new_room_with_users({alice, Alice}, [{bob, Bob}]), + Path = <<"/rooms/", RoomID/binary, "/users/">>, + BobJid = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), + Creds = credentials({alice, Alice}), + {?BAD_REQUEST, <<"Invalid user JID: @invalid">>} = + rest_helper:delete(client, <>, Creds), + {?BAD_REQUEST, <<"Missing JID">>} = + rest_helper:delete(client, Path, Creds), + {?NOT_FOUND, <<"Room does not exist">>} = + rest_helper:delete(client, <<"/rooms/badroom/users/", BobJid/binary>>, Creds) + end). + owner_can_leave_a_room_and_auto_select_owner(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> RoomID = given_new_room_with_users({alice, Alice}, [{bob, Bob}]), @@ -400,11 +523,10 @@ user_can_leave_a_room(Config) -> assert_aff_change_stanza(Stanza, Bob, <<"none">>) end). -invitation_to_room_is_forbidden_for_non_memeber(Config) -> +invitation_to_room_is_forbidden_for_non_member(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> RoomID = given_new_room({alice, Alice}), - {{<<"403">>, <<"Forbidden">>}, _ } = invite_to_room({bob, Bob}, RoomID, - <<"auser@domain.com">>) + {?FORBIDDEN, _ } = invite_to_room({bob, Bob}, RoomID, <<"auser@domain.com">>) end). msg_is_sent_and_delivered_in_room(Config) -> @@ -412,53 +534,37 @@ msg_is_sent_and_delivered_in_room(Config) -> given_new_room_with_users_and_msgs({alice, Alice}, [{bob, Bob}]) end). - sending_message_by_not_room_member_results_in_forbidden(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> Sender = {alice, Alice}, RoomID = given_new_room_with_users(Sender, []), Result = given_message_sent_to_room(RoomID, {bob, Bob}, #{body => <<"Hello, I'm not member">>}), - ?assertMatch({{<<"403">>, <<"Forbidden">>}, _}, Result) + ?assertMatch({?FORBIDDEN, _}, Result) end). -sending_message_with_wrong_body_results_in_bad_request(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Sender = {alice, Alice}, - RoomID = given_new_room_with_users(Sender, []), - Result = given_message_sent_to_room(RoomID, Sender, #{body => #{nested => <<"structure">>}}), - ?assertMatch({{<<"400">>, <<"Bad Request">>}, <<"Invalid body, it must be a string">>}, Result) - end). - -sending_message_with_no_body_results_in_bad_request(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Sender = {alice, Alice}, - RoomID = given_new_room_with_users(Sender, []), - Result = given_message_sent_to_room(RoomID, Sender, #{no_body => <<"This should be in body element">>}), - ?assertMatch({{<<"400">>, <<"Bad Request">>}, <<"No valid message elements">>}, Result) - end). - -sending_markable_message_with_no_body_results_in_bad_request(Config) -> - escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> - Sender = {alice, Alice}, - RoomID = given_new_room_with_users(Sender, []), - Result = given_message_sent_to_room(RoomID, Sender, #{markable => true}), - ?assertMatch({{<<"400">>, <<"Bad Request">>}, <<"No valid message elements">>}, Result) - end). - -sending_message_not_in_JSON_results_in_bad_request(Config) -> +sending_invalid_message_to_room_results_in_bad_request(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> Sender = {alice, Alice}, RoomID = given_new_room_with_users(Sender, []), - Result = given_message_sent_to_room(RoomID, Sender, <<"This is not JSON object">>), - ?assertMatch({{<<"400">>, <<"Bad Request">>}, <<"Request body is not a valid JSON">>}, Result) + InvalidMarker = #{type => <<"bad">>, id => <<"some_id">>}, + {?BAD_REQUEST, <<"Invalid message body">>} = + given_message_sent_to_room(RoomID, Sender, #{body => #{body => <<"Too nested">>}}), + {?BAD_REQUEST, <<"No valid message elements">>} = + given_message_sent_to_room(RoomID, Sender, #{no_body => <<"This should be in body">>}), + {?BAD_REQUEST, <<"No valid message elements">>} = + given_message_sent_to_room(RoomID, Sender, #{markable => true}), + {?BAD_REQUEST, <<"Invalid chat marker">>} = + given_message_sent_to_room(RoomID, Sender, #{chat_marker => InvalidMarker}), + {?BAD_REQUEST, <<"Invalid request body">>} = + given_message_sent_to_room(RoomID, Sender, <<"This is not JSON object">>) end). messages_are_archived_in_room(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> {RoomID, _Msgs} = given_new_room_with_users_and_msgs({alice, Alice}, [{bob, Bob}]), mam_helper:maybe_wait_for_archive(Config), - {{<<"200">>, <<"OK">>}, Result} = get_room_messages({alice, Alice}, RoomID), + {?OK, Result} = get_room_messages({alice, Alice}, RoomID), [Aff, _Msg1, _Msg2] = rest_helper:decode_maplist(Result), %% The oldest message is aff change <<"affiliation">> = maps:get(type, Aff), @@ -475,14 +581,14 @@ chat_markers_are_archived_in_room(Config) -> Markers = [#{ chat_marker => #{ type => Type, id => MarkedID } } || Type <- MarkerTypes ], RoomID = given_new_room_with_users({alice, Alice}, [{bob, Bob}]), lists:foreach(fun(Marker) -> - {{<<"200">>, <<"OK">>}, {_Result}} = + {?OK, {_Result}} = given_message_sent_to_room(RoomID, {bob, Bob}, Marker), [ escalus:wait_for_stanza(Client) || Client <- [Alice, Bob] ] end, Markers), mam_helper:maybe_wait_for_archive(Config), % WHEN an archive is queried via HTTP - {{<<"200">>, <<"OK">>}, Result} = get_room_messages({alice, Alice}, RoomID), + {?OK, Result} = get_room_messages({alice, Alice}, RoomID), % THEN these markers are retrieved and in proper order and with valid payload % (we discard remaining msg fields, they are tested by other cases) @@ -497,13 +603,13 @@ markable_property_is_archived_in_room(Config) -> % GIVEN a markable message is sent in the room MarkableMsg = #{ markable => true, body => <<"Floor is lava!">> }, RoomID = given_new_room_with_users({alice, Alice}, [{bob, Bob}]), - {{<<"200">>, <<"OK">>}, {_Result}} + {?OK, {_Result}} = given_message_sent_to_room(RoomID, {bob, Bob}, MarkableMsg), [ escalus:wait_for_stanza(Client) || Client <- [Alice, Bob] ], mam_helper:maybe_wait_for_archive(Config), % WHEN an archive is queried via HTTP - {{<<"200">>, <<"OK">>}, Result} = get_room_messages({alice, Alice}, RoomID), + {?OK, Result} = get_room_messages({alice, Alice}, RoomID), % THEN the retrieved message has markable property [_Aff, Msg] = rest_helper:decode_maplist(Result), @@ -513,7 +619,7 @@ markable_property_is_archived_in_room(Config) -> only_room_participant_can_read_messages(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> RoomID = given_new_room({alice, Alice}), - {{<<"403">>, <<"Forbidden">>}, _} = get_room_messages({bob, Bob}, RoomID), + {?FORBIDDEN, _} = get_room_messages({bob, Bob}, RoomID), ok end). @@ -542,6 +648,19 @@ messages_can_be_paginated_in_room(Config) -> assert_room_messages(OldestMsg2, hd(lists:keysort(1, GenMsgs2))) end). +room_message_query_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + RoomID = given_new_room_with_users({alice, Alice}, []), + Creds = credentials({alice, Alice}), + Path = <<"/rooms/", RoomID/binary, "/messages">>, + {?BAD_REQUEST, <<"Invalid limit">>} = + rest_helper:gett(client, <>, Creds), + {?BAD_REQUEST, <<"Invalid value of 'before'">>} = + rest_helper:gett(client, <>, Creds), + {?BAD_REQUEST, <<"Invalid query string">>} = + rest_helper:gett(client, <>, Creds) + end). + room_is_created_with_given_jid(Config) -> escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> RoomID = muc_helper:fresh_room_name(), @@ -776,7 +895,7 @@ assert_room_messages(RecvMsg, {_ID, _GenFrom, GenMsg}) -> get_room_info(User, RoomID) -> Creds = credentials(User), - {{<<"200">>, <<"OK">>}, {Result}} = rest_helper:gett(client, <<"/rooms/", RoomID/binary>>, + {?OK, {Result}} = rest_helper:gett(client, <<"/rooms/", RoomID/binary>>, Creds), Result. @@ -799,7 +918,7 @@ wait_for_room_msg(Msg, User) -> given_message_sent_to_room(RoomID, Sender) -> Body = #{body => <<"Hi all!">>}, HTTPResult = given_message_sent_to_room(RoomID, Sender, Body), - {{<<"200">>, <<"OK">>}, {Result}} = HTTPResult, + {?OK, {Result}} = HTTPResult, MsgId = proplists:get_value(<<"id">>, Result), true = is_binary(MsgId), {UserJID, _} = credentials(Sender), @@ -832,7 +951,7 @@ given_new_room(Owner, RoomID, RoomName) -> given_user_invited({_, Inviter} = Owner, RoomID, Invitee) -> JID = user_jid(Invitee), - {{<<"204">>, <<"No Content">>}, _} = invite_to_room(Owner, RoomID, JID), + {?NOCONTENT, _} = invite_to_room(Owner, RoomID, JID), maybe_wait_for_aff_stanza(Invitee, Invitee), maybe_wait_for_aff_stanza(Inviter, Invitee). @@ -878,7 +997,7 @@ send_message(User, From, To) -> BobJID = user_jid(To), M = #{to => BobJID, body => <<"hello, ", BobJID/binary, " it's me">>}, Cred = credentials({User, From}), - {{<<"200">>, <<"OK">>}, {Result}} = post(client, <<"/messages">>, M, Cred), + {?OK, {Result}} = post(client, <<"/messages">>, M, Cred), ID = proplists:get_value(<<"id">>, Result), M#{id => ID, from => AliceJID}. @@ -889,7 +1008,7 @@ get_messages(MeCreds, Other, Count) -> get_messages(GetPath, MeCreds). get_messages(Path, Creds) -> - {{<<"200">>, <<"OK">>}, Msgs} = rest_helper:gett(client, Path, Creds), + {?OK, Msgs} = rest_helper:gett(client, Path, Creds), rest_helper:decode_maplist(Msgs). get_messages(MeCreds, Other, Before, Count) -> @@ -906,7 +1025,7 @@ get_messages_with_props(MeCreds, Other, Count) -> get_messages_with_props(GetPath, MeCreds). get_messages_with_props(Path, Creds) -> - {{<<"200">>, <<"OK">>}, Msgs} = rest_helper:gett(client, Path, Creds), + {?OK, Msgs} = rest_helper:gett(client, Path, Creds), Msgs. get_messages_with_props(MeCreds, Other, Before, Count) -> @@ -929,13 +1048,13 @@ get_room_messages(Client, RoomID, Count, Before) -> create_room({_AliceJID, _} = Creds, RoomName, Subject) -> Room = #{name => RoomName, subject => Subject}, - {{<<"200">>, <<"OK">>}, {Result}} = rest_helper:post(client, <<"/rooms">>, Room, Creds), + {?CREATED, {Result}} = rest_helper:post(client, <<"/rooms">>, Room, Creds), proplists:get_value(<<"id">>, Result). create_room_with_id({_AliceJID, _} = Creds, RoomName, Subject, RoomID) -> Res = create_room_with_id_request(Creds, RoomName, Subject, RoomID), case Res of - {{<<"201">>, <<"Created">>}, {Result}} -> + {?CREATED, {Result}} -> proplists:get_value(<<"id">>, Result); _ -> ct:fail(#{issue => create_room_with_id_failed, @@ -954,7 +1073,7 @@ create_room_with_id_request(Creds, RoomName, Subject, RoomID) -> get_my_rooms(User) -> Creds = credentials(User), - {{<<"200">>, <<"OK">>}, Rooms} = rest_helper:gett(client, <<"/rooms">>, Creds), + {?OK, Rooms} = rest_helper:gett(client, <<"/rooms">>, Creds), Rooms. assert_messages([], []) -> @@ -1307,31 +1426,28 @@ add_contact_check_roster_push(Contact, {_, RosterOwnerSpec} = RosterOwner) -> escalus:assert(is_roster_set, Push), ok. - -break_stuff(Config) -> - escalus:fresh_story( - Config, [{alice, 1}, {bob, 1}], - fun(Alice, Bob) -> - AliceJID = escalus_utils:jid_to_lower( - escalus_client:short_jid(Alice)), - BCred = credentials({bob, Bob}), - AddContact = #{jid => AliceJID}, - {?NOCONTENT, _} = post(client, <<"/contacts">>, AddContact, - BCred), - PutPath = lists:flatten(["/contacts/", binary_to_list(AliceJID)]), - {?NOT_IMPLEMENTED, _} = putt(client, PutPath, - #{action => <<"nosuchaction">>}, - BCred), - BadPutPath = "/contacts/zorro@localhost", - {?NOT_FOUND, _} = putt(client, BadPutPath, - #{action => <<"invite">>}, - BCred), - BadGetPath = "/contacts/zorro@localhost", - {?NOT_FOUND, _} = gett(client, BadGetPath, BCred), - ok - end - ), - ok. +roster_errors(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun roster_errors_story/2). + +roster_errors_story(Alice, Bob) -> + AliceJID = user_jid(Alice), + BCred = credentials({bob, Bob}), + {?BAD_REQUEST, <<"Missing JID">>} = + post(client, <<"/contacts">>, #{}, BCred), + {?BAD_REQUEST, <<"Invalid JID: @invalid">>} = + post(client, <<"/contacts">>, #{jid => <<"@invalid">>}, BCred), + {?BAD_REQUEST, <<"Invalid action">>} = + putt(client, <<"/contacts/", AliceJID/binary>>, #{action => <<"nosuchaction">>}, BCred), + {?BAD_REQUEST, <<"Missing action">>} = + putt(client, <<"/contacts/", AliceJID/binary>>, #{}, BCred), + {?NOT_FOUND, _} = + post(client, <<"/contacts">>, #{jid => <<"zorro@localhost">>}, BCred), + {?NOT_FOUND, _} = + putt(client, <<"/contacts/zorro@localhost">>, #{action => <<"invite">>}, BCred), + {?NOT_FOUND, _} = + gett(client, <<"/contacts/zorro@localhost">>, BCred), + {?NOT_FOUND, _} = + delete(client, <<"/contacts/zorro@localhost">>, BCred). -spec room_jid(RoomID :: binary(), Config :: list()) -> RoomJID :: binary(). room_jid(RoomID, Config) -> diff --git a/src/event_pusher/mod_event_pusher_hook_translator.erl b/src/event_pusher/mod_event_pusher_hook_translator.erl index e0ef8b96f07..5bcfabda6a1 100644 --- a/src/event_pusher/mod_event_pusher_hook_translator.erl +++ b/src/event_pusher/mod_event_pusher_hook_translator.erl @@ -136,6 +136,5 @@ hooks(HostType) -> {unset_presence_hook, HostType, fun ?MODULE:user_not_present/3, #{}, 90}, {user_available_hook, HostType, fun ?MODULE:user_present/3, #{}, 90}, {user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 90}, - {rest_user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 90}, {unacknowledged_message, HostType, fun ?MODULE:unacknowledged_message/3, #{}, 90} ]. diff --git a/src/mam/mod_mam_pm.erl b/src/mam/mod_mam_pm.erl index 4896f3c3aff..c558484afde 100644 --- a/src/mam/mod_mam_pm.erl +++ b/src/mam/mod_mam_pm.erl @@ -672,7 +672,6 @@ hooks(HostType) -> [ {disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}, {user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 60}, - {rest_user_send_packet, HostType, fun ?MODULE:user_send_packet/3, #{}, 60}, {filter_local_packet, HostType, fun ?MODULE:filter_packet/3, #{}, 60}, {remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50}, {anonymous_purge_hook, HostType, fun ?MODULE:remove_user/3, #{}, 50}, diff --git a/src/mongoose_client_api/mongoose_client_api.erl b/src/mongoose_client_api/mongoose_client_api.erl index 29bd0fd4261..950cc787d2f 100644 --- a/src/mongoose_client_api/mongoose_client_api.erl +++ b/src/mongoose_client_api/mongoose_client_api.erl @@ -5,26 +5,22 @@ %% mongoose_http_handler callbacks -export([config_spec/0, routes/1]). --export([init/2]). --export([content_types_provided/2]). --export([is_authorized/2]). --export([options/2]). --export([allowed_methods/2]). --export([to_json/2]). --export([bad_request/2]). --export([bad_request/3]). --export([forbidden_request/2]). --export([forbidden_request/3]). --export([json_to_map/1]). - --ignore_xref([allowed_methods/2, content_types_provided/2, forbidden_request/3, - options/2, to_json/2]). +%% Utilities for the handler modules +-export([init/2, + is_authorized/2, + parse_body/1, + parse_qs/1, + try_handle_request/3, + throw_error/2]). -include("mongoose.hrl"). -include("mongoose_config_spec.hrl"). -type handler_options() :: #{path := string(), handlers := [module()], docs := boolean(), atom() => any()}. +-type req() :: cowboy_req:req(). +-type state() :: #{atom() => any()}. +-type error_type() :: bad_request | denied | not_found. -callback routes() -> mongoose_http_handler:routes(). @@ -88,53 +84,6 @@ set_cors_headers(Origin, Req) -> set_cors_header({Header, Value}, Req) -> cowboy_req:set_resp_header(Header, Value, Req). -allowed_methods(Req, State) -> - {[<<"OPTIONS">>, <<"GET">>], Req, State}. - -content_types_provided(Req, State) -> - {[ - {{<<"application">>, <<"json">>, '*'}, to_json} - ], Req, State}. - -options(Req, State) -> - {ok, Req, State}. - -to_json(Req, User) -> - {<<"{}">>, Req, User}. - -bad_request(Req, State) -> - bad_request(Req, <<"Bad request. The details are unknown.">>, State). - -bad_request(Req, Reason, State) -> - reply(400, Req, Reason, State). - -forbidden_request(Req, State) -> - forbidden_request(Req, <<>>, State). - -forbidden_request(Req, Reason, State) -> - reply(403, Req, Reason, State). - -reply(StatusCode, Req, Body, State) -> - maybe_report_error(StatusCode, Req, Body), - Req1 = set_resp_body_if_missing(Body, Req), - Req2 = cowboy_req:reply(StatusCode, Req1), - {stop, Req2, State#{was_replied => true}}. - -set_resp_body_if_missing(Body, Req) -> - case cowboy_req:has_resp_body(Req) of - true -> - Req; - false -> - cowboy_req:set_resp_body(Body, Req) - end. - -maybe_report_error(StatusCode, Req, Body) when StatusCode >= 400 -> - ?LOG_WARNING(#{what => reply_error, - stacktrace => element(2, erlang:process_info(self(), current_stacktrace)), - code => StatusCode, req => Req, reply_body => Body}); -maybe_report_error(_StatusCode, _Req, _Body) -> - ok. - %%-------------------------------------------------------------------- %% Authorization %%-------------------------------------------------------------------- @@ -174,17 +123,60 @@ do_authorize(AuthMethod, MaybeJID, Password, HTTPMethod) -> is_noauth_http_method(<<"OPTIONS">>) -> true; is_noauth_http_method(_) -> false. -%% ------------------------------------------------------------------- -%% @doc -%% Decode JSON binary into map -%% @end -%% ------------------------------------------------------------------- --spec json_to_map(JsonBin :: binary()) -> {ok, Map :: maps:map()} | {error, invalid_json}. - -json_to_map(JsonBin) -> - case catch jiffy:decode(JsonBin, [return_maps]) of - Map when is_map(Map) -> - {ok, Map}; - _ -> - {error, invalid_json} +-spec parse_body(req()) -> #{atom() => jiffy:json_value()}. +parse_body(Req) -> + try + {ok, Body, _Req2} = cowboy_req:read_body(Req), + decoded_json_to_map(jiffy:decode(Body)) + catch Class:Reason:Stacktrace -> + ?LOG_INFO(#{what => parse_body_failed, + class => Class, reason => Reason, stacktrace => Stacktrace}), + throw_error(bad_request, <<"Invalid request body">>) end. + +decoded_json_to_map({L}) when is_list(L) -> + maps:from_list([{binary_to_existing_atom(K), decoded_json_to_map(V)} || {K, V} <- L]); +decoded_json_to_map(V) -> + V. + +-spec parse_qs(req()) -> #{atom() => binary() | true}. +parse_qs(Req) -> + try + maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)]) + catch Class:Reason:Stacktrace -> + ?LOG_INFO(#{what => parse_qs_failed, + class => Class, reason => Reason, stacktrace => Stacktrace}), + throw_error(bad_request, <<"Invalid query string">>) + end. + +-spec try_handle_request(req(), state(), fun((req(), state()) -> Result)) -> Result. +try_handle_request(Req, State, F) -> + try + F(Req, State) + catch throw:#{error_type := ErrorType, message := Msg} -> + error_response(ErrorType, Msg, Req, State) + end. + +-spec throw_error(error_type(), iodata()) -> no_return(). +throw_error(ErrorType, Msg) -> + throw(#{error_type => ErrorType, message => Msg}). + +-spec error_response(error_type(), iodata(), req(), state()) -> {stop, req(), state()}. +error_response(ErrorType, Message, Req, State) -> + BinMessage = iolist_to_binary(Message), + ?LOG(log_level(ErrorType), #{what => mongoose_client_api_error_response, + error_type => ErrorType, + message => BinMessage, + req => Req}), + Req1 = cowboy_req:reply(error_code(ErrorType), #{}, jiffy:encode(BinMessage), Req), + {stop, Req1, State}. + +-spec error_code(error_type()) -> non_neg_integer(). +error_code(bad_request) -> 400; +error_code(denied) -> 403; +error_code(not_found) -> 404. + +-spec log_level(error_type()) -> logger:level(). +log_level(bad_request) -> info; +log_level(denied) -> info; +log_level(not_found) -> info. diff --git a/src/mongoose_client_api/mongoose_client_api_contacts.erl b/src/mongoose_client_api/mongoose_client_api_contacts.erl index 35755d8198b..42d0c3e06a3 100644 --- a/src/mongoose_client_api/mongoose_client_api_contacts.erl +++ b/src/mongoose_client_api/mongoose_client_api_contacts.erl @@ -4,20 +4,19 @@ -export([routes/0]). -behaviour(cowboy_rest). --export([trails/0]). --export([init/2]). --export([content_types_provided/2]). --export([content_types_accepted/2]). --export([is_authorized/2]). --export([allowed_methods/2]). +-export([trails/0, + init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2, + delete_resource/2]). --export([forbidden_request/2]). +-ignore_xref([from_json/2, to_json/2, trails/0]). --export([to_json/2]). --export([from_json/2]). --export([delete_resource/2]). - --ignore_xref([from_json/2, to_json/2, trails/0, forbidden_request/2]). +-import(mongoose_client_api, [parse_body/1, try_handle_request/3, throw_error/2]). -type req() :: cowboy_req:req(). -type state() :: map(). @@ -29,172 +28,97 @@ routes() -> trails() -> mongoose_client_api_contacts_doc:trails(). +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. init(Req, Opts) -> mongoose_client_api:init(Req, Opts). +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> mongoose_client_api:is_authorized(Req, State). +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_provided(Req, State) -> {[ {{<<"application">>, <<"json">>, '*'}, to_json} ], Req, State}. +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_accepted(Req, State) -> {[ {{<<"application">>, <<"json">>, '*'}, from_json} ], Req, State}. +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>], Req, State}. --spec forbidden_request(req(), state()) -> {stop, req(), state()}. -forbidden_request(Req, State) -> - Req1 = cowboy_req:reply(403, Req), - {stop, Req1, State}. - +%% @doc Called for a method of type "GET" -spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. to_json(Req, State) -> - Method = cowboy_req:method(Req), - Jid = cowboy_req:binding(jid, Req), - case Jid of - undefined -> - {ok, Res} = handle_request(Method, State), - {jiffy:encode(Res), Req, State}; - _ -> - Req2 = cowboy_req:reply(404, Req), - {stop, Req2, State} - end. + try_handle_request(Req, State, fun handle_get/2). +%% @doc Called for a method of type "POST" or "PUT" -spec from_json(req(), state()) -> {true | stop, req(), state()}. from_json(Req, State) -> - Method = cowboy_req:method(Req), - {ok, Body, Req1} = cowboy_req:read_body(Req), - case mongoose_client_api:json_to_map(Body) of - {ok, JSONData} -> - Jid = case maps:get(<<"jid">>, JSONData, undefined) of - undefined -> cowboy_req:binding(jid, Req1); - J when is_binary(J) -> J; - _ -> undefined - end, - Action = maps:get(<<"action">>, JSONData, undefined), - handle_request_and_respond(Method, Jid, Action, Req1, State); - _ -> - mongoose_client_api:bad_request(Req1, State) - end. + F = case cowboy_req:method(Req) of + <<"POST">> -> fun handle_post/2; + <<"PUT">> -> fun handle_put/2 + end, + try_handle_request(Req, State, F). %% @doc Called for a method of type "DELETE" -spec delete_resource(req(), state()) -> {true | stop, req(), state()}. delete_resource(Req, State) -> - Jid = cowboy_req:binding(jid, Req), - case Jid of - undefined -> - handle_multiple_deletion(get_requested_contacts(Req), Req, State); - _ -> - handle_single_deletion(Jid, Req, State) - end. - --spec handle_multiple_deletion(undefined | [jid:literal_jid()], req(), state()) -> - {true | stop, req(), state()}. -handle_multiple_deletion(undefined, Req, State) -> - mongoose_client_api:bad_request(Req, State); -handle_multiple_deletion(ToDelete, Req, State = #{jid := CJid}) -> - NotDeleted = delete_contacts(CJid, ToDelete), - RespBody = #{not_deleted => NotDeleted}, - Req2 = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req), - Req3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Req2), - {true, Req3, State}. + try_handle_request(Req, State, fun handle_delete/2). --spec handle_single_deletion(undefined | jid:literal_jid(), req(), state()) -> - {true | stop, req(), state()}. -handle_single_deletion(undefined, Req, State) -> - mongoose_client_api:bad_request(Req, State); -handle_single_deletion(ToDelete, Req, State = #{jid := CJid}) -> - ok = delete_contact(CJid, ToDelete), - {true, Req, State}. - --spec handle_request_and_respond(Method :: binary(), jid:literal_jid() | undefined, - Action :: binary() | undefined, req(), state()) -> - {true | stop, req(), state()}. -handle_request_and_respond(_, undefined, _, Req, State) -> - mongoose_client_api:bad_request(Req, State); -handle_request_and_respond(Method, Jid, Action, Req, State) -> - case handle_request(Method, Jid, Action, State) of - ok -> - {true, Req, State}; - not_implemented -> - Req2 = cowboy_req:reply(501, Req), - {stop, Req2, State}; - not_found -> - Req2 = cowboy_req:reply(404, Req), - {stop, Req2, State} - end. - --spec get_requested_contacts(req()) -> [jid:literal_jid()] | undefined. -get_requested_contacts(Req) -> - Body = get_whole_body(Req, <<"">>), - case mongoose_client_api:json_to_map(Body) of - {ok, #{<<"to_delete">> := ResultJids}} when is_list(ResultJids) -> - case [X || X <- ResultJids, is_binary(X)] of - ResultJids -> - ResultJids; - _ -> - undefined - end; - _ -> - undefined - end. - --spec get_whole_body(req(), binary()) -> binary(). -get_whole_body(Req, Acc) -> - case cowboy_req:read_body(Req) of - {ok, Data, _Req2} -> - <>; - {more, Data, Req2} -> - get_whole_body(Req2, <>) - end. - --spec handle_request(binary(), state()) -> {ok, [jiffy:json_object()]} | {error, any()}. -handle_request(<<"GET">>, #{jid := CallerJid}) -> - list_contacts(CallerJid). +%% Internal functions --spec handle_request(Method :: binary(), jid:literal_jid() | undefined, - Action :: binary() | undefined, state()) -> - ok | not_found | not_implemented | {error, any()}. -handle_request(<<"POST">>, Jid, undefined, #{jid := CallerJid}) -> - add_contact(CallerJid, Jid); -handle_request(Method, Jid, Action, #{jid := CallerJid, creds := Creds}) -> - HostType = mongoose_credentials:host_type(Creds), - case contact_exists(HostType, CallerJid, jid:from_binary(Jid)) of - true -> - handle_contact_request(Method, Jid, Action, CallerJid); - false -> not_found +handle_get(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + assert_no_jid(Bindings), + {ok, Contacts} = mod_roster_api:list_contacts(UserJid), + {jiffy:encode(lists:map(fun roster_info/1, Contacts)), Req, State}. + +handle_post(Req, State = #{jid := UserJid}) -> + Args = parse_body(Req), + ContactJid = get_jid(Args), + case mod_roster_api:add_contact(UserJid, ContactJid, <<>>, []) of + {user_not_exist, Reason} -> + throw_error(not_found, Reason); + {ok, _} -> + {true, Req, State} end. -handle_contact_request(<<"PUT">>, Jid, <<"invite">>, CJid) -> - subscription(CJid, Jid, atom_to_binary(subscribe, latin1)); -handle_contact_request(<<"PUT">>, Jid, <<"accept">>, CJid) -> - subscription(CJid, Jid, atom_to_binary(subscribed, latin1)); -handle_contact_request(_, _, _, _) -> - not_implemented. - --spec contact_exists(mongooseim:host_type(), jid:jid(), jid:jid() | error) -> boolean(). -contact_exists(_, _, error) -> false; -contact_exists(HostType, CallerJid, Jid) -> - LJid = jid:to_lower(Jid), - Res = mod_roster:get_roster_entry(HostType, CallerJid, LJid, short), - Res =/= does_not_exist andalso Res =/= error. - -%% Internal functions +handle_put(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + ContactJid = get_jid(Bindings), + Args = parse_body(Req), + Action = get_action(Args), + assert_contact_exists(UserJid, ContactJid), + {ok, _} = mod_roster_api:subscription(UserJid, ContactJid, Action), + {true, Req, State}. --spec list_contacts(jid:jid()) -> {ok, [jiffy:json_object()]} | {error, any()}. -list_contacts(Caller) -> - case mod_roster_api:list_contacts(Caller) of - {ok, Rosters} -> - {ok, lists:map(fun roster_info/1, Rosters)}; - {ErrorCode, _Msg} -> - {error, ErrorCode} +handle_delete(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + case try_get_jid(Bindings) of + undefined -> + Args = parse_body(Req), + ContactJids = get_jids_to_delete(Args), + NotDeleted = delete_contacts(UserJid, ContactJids), + RespBody = #{not_deleted => lists:map(fun jid:to_binary/1, NotDeleted)}, + Req2 = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req), + Req3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Req2), + {true, Req3, State}; + ContactJid -> + case mod_roster_api:delete_contact(UserJid, ContactJid) of + {contact_not_found, Reason} -> + throw_error(not_found, Reason); + {ok, _} -> + {true, Req, State} + end end. -spec roster_info(mod_roster:roster()) -> jiffy:json_object(). @@ -202,60 +126,52 @@ roster_info(Roster) -> #{jid := Jid, subscription := Sub, ask := Ask} = mod_roster:item_to_map(Roster), #{jid => jid:to_binary(Jid), subscription => Sub, ask => Ask}. --spec add_contact(jid:jid(), jid:literal_jid()) -> ok | {error, any()}. -add_contact(Caller, JabberID) -> - add_contact(Caller, JabberID, <<>>, []). - -add_contact(CallerJid, Other, Name, Groups) -> - case jid:from_binary(Other) of - error -> - {error, invalid_jid}; - Jid -> - Res = mod_roster_api:add_contact(CallerJid, Jid, Name, Groups), - skip_result_msg(Res) +-spec delete_contacts(jid:jid(), [jid:jid()]) -> [jid:jid()]. +delete_contacts(UserJid, ContactJids) -> + lists:filter(fun(ContactJid) -> + case mod_roster_api:delete_contact(UserJid, ContactJid) of + {contact_not_found, _Reason} -> + true; + {ok, _} -> + false + end + end, ContactJids). + +get_jid(#{jid := JidBin}) -> + parse_jid(JidBin); +get_jid(#{}) -> + throw_error(bad_request, <<"Missing JID">>). + +try_get_jid(#{jid := JidBin}) -> + parse_jid(JidBin); +try_get_jid(#{}) -> + undefined. + +assert_no_jid(#{jid := _}) -> + throw_error(not_found, <<"JID provided but not supported">>); +assert_no_jid(#{}) -> + ok. + +assert_contact_exists(UserJid, ContactJid) -> + case mod_roster_api:get_contact(UserJid, ContactJid) of + {ok, _} -> ok; + {_Error, Msg} -> throw_error(not_found, Msg) end. --spec delete_contacts(jid:jid(), [jid:literal_jid()]) -> [jid:literal_jid()]. -delete_contacts(Caller, ToDelete) -> - maybe_delete_contacts(Caller, ToDelete, []). - -maybe_delete_contacts(_, [], NotDeleted) -> NotDeleted; -maybe_delete_contacts(Caller, [H | T], NotDeleted) -> - case delete_contact(Caller, H) of - ok -> - maybe_delete_contacts(Caller, T, NotDeleted); - _Error -> - maybe_delete_contacts(Caller, T, NotDeleted ++ [H]) - end. +get_jids_to_delete(#{to_delete := JidsBin}) -> + lists:map(fun parse_jid/1, JidsBin). --spec delete_contact(jid:jid(), jid:literal_jid()) -> ok | {error, any()}. -delete_contact(CallerJID, Other) -> - case jid:from_binary(Other) of - error -> - {error, invalid_jid}; - Jid -> - Res = mod_roster_api:delete_contact(CallerJID, Jid), - skip_result_msg(Res) - end. - --spec subscription(jid:jid(), jid:literal_jid(), binary()) -> ok | {error, any()}. -subscription(CallerJID, Other, Action) -> - case decode_action(Action) of - error -> - {error, {bad_request, <<"invalid action">>}}; - Act -> - case jid:from_binary(Other) of - error -> - {error, invalid_jid}; - Jid -> - Res = mod_roster_api:subscription(CallerJID, Jid, Act), - skip_result_msg(Res) - end +parse_jid(JidBin) -> + case jid:from_binary(JidBin) of + error -> throw_error(bad_request, <<"Invalid JID: ", JidBin/binary>>); + Jid -> Jid end. -decode_action(<<"subscribe">>) -> subscribe; -decode_action(<<"subscribed">>) -> subscribed; -decode_action(_) -> error. +get_action(#{action := ActionBin}) -> + decode_action(ActionBin); +get_action(#{}) -> + throw_error(bad_request, <<"Missing action">>). -skip_result_msg({ok, _Msg}) -> ok; -skip_result_msg({ErrCode, _Msg}) -> {error, ErrCode}. +decode_action(<<"invite">>) -> subscribe; +decode_action(<<"accept">>) -> subscribed; +decode_action(_) -> throw_error(bad_request, <<"Invalid action">>). diff --git a/src/mongoose_client_api/mongoose_client_api_messages.erl b/src/mongoose_client_api/mongoose_client_api_messages.erl index 6792e88fd2d..c56b8c3b05a 100644 --- a/src/mongoose_client_api/mongoose_client_api_messages.erl +++ b/src/mongoose_client_api/mongoose_client_api_messages.erl @@ -4,27 +4,25 @@ -export([routes/0]). -behaviour(cowboy_rest). --export([trails/0]). --export([init/2]). --export([content_types_provided/2]). --export([content_types_accepted/2]). --export([is_authorized/2]). --export([allowed_methods/2]). - --export([to_json/2]). --export([send_message/2]). - +-export([trails/0, + init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2]). + +%% Used by mongoose_client_api_sse -export([encode/2]). --export([maybe_integer/1]). --export([maybe_before_to_us/2]). +-ignore_xref([to_json/2, from_json/2, trails/0]). + +-import(mongoose_client_api, [parse_body/1, parse_qs/1, try_handle_request/3, throw_error/2]). --ignore_xref([send_message/2, to_json/2, trails/0, - maybe_before_to_us/2]). +-type req() :: cowboy_req:req(). +-type state() :: mongoose_admin_api:state(). --include("mongoose.hrl"). --include("jlib.hrl"). --include("mongoose_rsm.hrl"). -include_lib("exml/include/exml.hrl"). -spec routes() -> mongoose_http_handler:routes(). @@ -34,92 +32,97 @@ routes() -> trails() -> mongoose_client_api_messages_doc:trails(). -init(Req, Opts) -> - mongoose_client_api:init(Req, Opts). +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. +init(Req, State) -> + mongoose_client_api:init(Req, State). +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> mongoose_client_api:is_authorized(Req, State). +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_provided(Req, State) -> {[ {{<<"application">>, <<"json">>, '*'}, to_json} ], Req, State}. +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_accepted(Req, State) -> {[ - {{<<"application">>, <<"json">>, '*'}, send_message} + {{<<"application">>, <<"json">>, '*'}, from_json} ], Req, State}. +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"GET">>, <<"POST">>], Req, State}. -to_json(Req, #{jid := JID} = State) -> - With = cowboy_req:binding(with, Req), - WithJID = maybe_jid(With), - maybe_to_json_with_jid(WithJID, JID, Req, State). - -maybe_to_json_with_jid(error, _, Req, State) -> - Req2 = cowboy_req:reply(404, Req), - {stop, Req2, State}; -maybe_to_json_with_jid(WithJID, #jid{} = JID, Req, State = #{creds := Creds}) -> - HostType = mongoose_credentials:host_type(Creds), - Now = os:system_time(microsecond), - ArchiveID = mod_mam_pm:archive_id_int(HostType, JID), - QS = cowboy_req:parse_qs(Req), - PageSize = maybe_integer(proplists:get_value(<<"limit">>, QS, <<"50">>)), - Before = maybe_integer(proplists:get_value(<<"before">>, QS)), - End = maybe_before_to_us(Before, Now), - RSM = #rsm_in{direction = before, id = undefined}, - R = mod_mam_pm:lookup_messages(HostType, - #{archive_id => ArchiveID, - owner_jid => JID, - rsm => RSM, - borders => undefined, - start_ts => undefined, - end_ts => End, - now => Now, - with_jid => WithJID, - search_text => undefined, - page_size => PageSize, - limit_passed => true, - max_result_limit => 50, - is_simple => true}), - {ok, {_, _, Msgs}} = R, - Resp = [make_json_msg(Msg, MAMId) || #{id := MAMId, packet := Msg} <- Msgs], +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "POST" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + try_handle_request(Req, State, fun handle_post/2). + +handle_get(Req, State = #{jid := OwnerJid}) -> + Bindings = cowboy_req:bindings(Req), + WithJid = get_with_jid(Bindings), + Args = parse_qs(Req), + Limit = get_limit(Args), + Before = get_before(Args), + Rows = mongoose_stanza_api:lookup_recent_messages(OwnerJid, WithJid, Before, Limit), + Resp = [make_json_msg(Msg, MAMId) || #{id := MAMId, packet := Msg} <- Rows], {jiffy:encode(Resp), Req, State}. -send_message(Req, #{user := RawUser, jid := FromJID, creds := Creds} = State) -> - {ok, Body, Req2} = cowboy_req:read_body(Req), - case mongoose_client_api:json_to_map(Body) of - {ok, #{<<"to">> := To, <<"body">> := MsgBody}} when is_binary(To), is_binary(MsgBody) -> - ToJID = jid:from_binary(To), - UUID = uuid:uuid_to_string(uuid:get_v4(), binary_standard), - XMLMsg0 = build_message(RawUser, To, UUID, MsgBody), - Acc0 = mongoose_acc:new(#{ location => ?LOCATION, - host_type => mongoose_credentials:host_type(Creds), - lserver => FromJID#jid.lserver, - from_jid => FromJID, - to_jid => ToJID, - element => XMLMsg0 }), - Acc1 = mongoose_hooks:rest_user_send_packet(Acc0, FromJID, ToJID, XMLMsg0), - XMLMsg1 = mongoose_acc:element(Acc1), - ejabberd_router:route(FromJID, ToJID, Acc1, XMLMsg1), - Resp = #{<<"id">> => UUID}, - Req3 = cowboy_req:set_resp_body(jiffy:encode(Resp), Req2), - {true, Req3, State}; - _ -> - mongoose_client_api:bad_request(Req2, State) - end. - -build_message(From, To, Id, Body) -> - Attrs = [{<<"from">>, From}, - {<<"to">>, To}, - {<<"id">>, Id}, - {<<"type">>, <<"chat">>}], - #xmlel{name = <<"message">>, - attrs = Attrs, - children = [#xmlel{name = <<"body">>, - children = [#xmlcdata{content = Body}]}]}. +handle_post(Req, State = #{jid := From}) -> + Args = parse_body(Req), + To = get_to(Args), + Body = get_body(Args), + Packet = mongoose_stanza_helper:build_message(jid:to_binary(From), jid:to_binary(To), Body), + {ok, _} = mongoose_stanza_helper:route(From, To, Packet, true), + Id = exml_query:attr(Packet, <<"id">>), + Resp = #{<<"id">> => Id}, + Req2 = cowboy_req:set_resp_body(jiffy:encode(Resp), Req), + {true, Req2, State}. + +get_limit(#{limit := LimitBin}) -> + try + Limit = binary_to_integer(LimitBin), + true = Limit >= 0 andalso Limit =< 500, + Limit + catch + _:_ -> throw_error(bad_request, <<"Invalid limit">>) + end; +get_limit(#{}) -> 50. + +get_before(#{before := BeforeBin}) -> + try + 1000 * binary_to_integer(BeforeBin) + catch + _:_ -> throw_error(bad_request, <<"Invalid value of 'before'">>) + end; +get_before(#{}) -> 0. + +get_with_jid(#{with := With}) -> + case jid:from_binary(With) of + error -> throw_error(bad_request, <<"Invalid interlocutor JID">>); + WithJid -> WithJid + end; +get_with_jid(#{}) -> undefined. + +get_to(#{to := To}) -> + case jid:from_binary(To) of + error -> throw_error(bad_request, <<"Invalid recipient JID">>); + ToJid -> ToJid + end; +get_to(#{}) -> throw_error(bad_request, <<"Missing recipient JID">>). + +get_body(#{body := Body}) -> Body; +get_body(#{}) -> throw_error(bad_request, <<"Missing message body">>). make_json_msg(Msg, MAMId) -> {Microsec, _} = mod_mam_utils:decode_compact_uuid(MAMId), @@ -127,55 +130,38 @@ make_json_msg(Msg, MAMId) -> -spec encode(exml:item(), integer()) -> map(). encode(Msg, Timestamp) -> - - %Smack library specific query for properties. - RawMsgProps = exml_query:subelement_with_name_and_ns( - Msg, - <<"properties">>, - <<"http://www.jivesoftware.com/xmlns/xmpp/properties">>), - BodyTag = exml_query:path(Msg, [{element, <<"body">>}]), - ExtensionList = - case RawMsgProps of - #xmlel{children = Children} -> - Props = [convert_prop_child(Child) || Child <- Children], - [{<<"properties">>, maps:from_list(Props)}]; - _ -> - [] - end, - Thread = exml_query:path(Msg, [{element, <<"thread">>}, cdata]), - ThreadParent = exml_query:path(Msg, [{element, <<"thread">>}, {attr, <<"parent">>}]), - ThreadAndThreadParentList = case {Thread, ThreadParent} of - {undefined, undefined} -> - []; - {Thread, undefined} -> - [{<<"thread">>, Thread}]; - {Thread, ThreadParent} -> - [{<<"thread">>, Thread}, {<<"parent">>, ThreadParent}] - end, L = [{<<"from">>, exml_query:attr(Msg, <<"from">>)}, {<<"to">>, exml_query:attr(Msg, <<"to">>)}, {<<"id">>, exml_query:attr(Msg, <<"id">>)}, {<<"body">>, exml_query:cdata(BodyTag)}, - {<<"timestamp">>, Timestamp} | ExtensionList] ++ ThreadAndThreadParentList, + {<<"timestamp">>, Timestamp}] ++ extensions(Msg) ++ thread_and_parent(Msg), maps:from_list(L). +extensions(Msg) -> + SmackNS = <<"http://www.jivesoftware.com/xmlns/xmpp/properties">>, + RawMsgProps = exml_query:subelement_with_name_and_ns(Msg, <<"properties">>, SmackNS), + case RawMsgProps of + #xmlel{children = Children} -> + Props = [convert_prop_child(Child) || Child <- Children], + [{<<"properties">>, maps:from_list(Props)}]; + _ -> + [] + end. + +thread_and_parent(Msg) -> + case exml_query:path(Msg, [{element, <<"thread">>}, cdata]) of + undefined -> []; + Thread -> [{<<"thread">>, Thread} | parent(Msg)] + end. + +parent(Msg) -> + case exml_query:path(Msg, [{element, <<"thread">>}, {attr, <<"parent">>}]) of + undefined -> []; + ThreadParent -> [{<<"parent">>, ThreadParent}] + end. + convert_prop_child(Child)-> Name = exml_query:path(Child, [{element, <<"name">>}, cdata]), Value = exml_query:path(Child, [{element, <<"value">>}, cdata]), {Name, Value}. - -maybe_jid(undefined) -> - undefined; -maybe_jid(JID) -> - jid:from_binary(JID). - -maybe_integer(undefined) -> - undefined; -maybe_integer(Val) -> - binary_to_integer(Val). - -maybe_before_to_us(undefined, Now) -> - Now; -maybe_before_to_us(Timestamp, _) -> - Timestamp * 1000. diff --git a/src/mongoose_client_api/mongoose_client_api_rooms.erl b/src/mongoose_client_api/mongoose_client_api_rooms.erl index fa4773fbfd6..02bc2fc86aa 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms.erl @@ -4,23 +4,28 @@ -export([routes/0]). -behaviour(cowboy_rest). --export([trails/0]). --export([init/2]). --export([content_types_provided/2]). --export([content_types_accepted/2]). --export([is_authorized/2]). --export([allowed_methods/2]). --export([resource_exists/2]). --export([assert_room_id_set/2]). - --export([to_json/2]). --export([from_json/2]). - --ignore_xref([ - from_json/2, to_json/2, trails/0 -]). - --include("mongoose.hrl"). +-export([trails/0, + init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2]). + +%% Used by mongoose_client_api_rooms_* +-export([get_room_jid/3, + get_user_aff/2, + get_room_name/1, + get_room_subject/1]). + +-ignore_xref([from_json/2, to_json/2, trails/0]). + +-import(mongoose_client_api, [parse_body/1, try_handle_request/3, throw_error/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + -include("jlib.hrl"). -spec routes() -> mongoose_http_handler:routes(). @@ -30,166 +35,146 @@ routes() -> trails() -> mongoose_client_api_rooms_doc:trails(). +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. init(Req, Opts) -> mongoose_client_api:init(Req, Opts). +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> mongoose_client_api:is_authorized(Req, State). +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_provided(Req, State) -> {[ {{<<"application">>, <<"json">>, '*'}, to_json} ], Req, State}. +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_accepted(Req, State) -> {[ {{<<"application">>, <<"json">>, '*'}, from_json} ], Req, State}. +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"GET">>, <<"POST">>, <<"PUT">>], Req, State}. -resource_exists(Req, #{jid := #jid{lserver = Server}} = State) -> - RoomIDOrJID = cowboy_req:binding(id, Req), - MUCLightDomain = muc_light_domain(Server), - case RoomIDOrJID of - undefined -> - Method = cowboy_req:method(Req), - case Method of - <<"GET">> -> - {true, Req, State}; - _ -> - {false, Req, State} - end; - _ -> - case validate_room_id(RoomIDOrJID, Server, Req) of - {ok, RoomID} -> - State2 = set_room_id(RoomID, State), - does_room_exist(MUCLightDomain, Req, State2); - _ -> - mongoose_client_api:bad_request(Req, <<"invalid_room_id">>, State) - end - end. +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). -set_room_id(RoomID, State = #{}) -> - State#{room_id => RoomID}. - -does_room_exist(RoomS, Req, #{room_id := RoomU, jid := JID} = State) -> - HostType = mod_muc_light_utils:muc_host_to_host_type(RoomS), - case mod_muc_light_db_backend:get_info(HostType, {RoomU, RoomS}) of - {ok, Config, Users, Version} -> - Room = #{config => Config, - users => Users, - version => Version, - jid => jid:make_noprep(RoomU, RoomS, <<>>)}, - CallerRole = determine_role(jid:to_lus(JID), Users), - {true, Req, State#{room => Room, role_in_room => CallerRole}}; - _ -> - {false, Req, State} - end. +%% @doc Called for a method of type "POST" and "PUT" +-spec from_json(req(), state()) -> {stop, req(), state()}. +from_json(Req, State) -> + F = case cowboy_req:method(Req) of + <<"POST">> -> fun handle_post/2; + <<"PUT">> -> fun handle_put/2 + end, + try_handle_request(Req, State, F). -to_json(Req, #{room := Room} = State) -> - Config = maps:get(config, Room), - Users = maps:get(users, Room), - Resp = #{name => proplists:get_value(roomname, Config), - subject => proplists:get_value(subject, Config), - participants => [user_to_json(U) || U <- Users] - }, - {jiffy:encode(Resp), Req, State}; -to_json(Req, #{jid := #jid{luser = User, lserver = Server}} = State) -> - HostType = mod_muc_light_utils:server_host_to_host_type(Server), - Rooms = mod_muc_light_db_backend:get_user_rooms(HostType, {User, Server}, undefined), - RoomsMap = [get_room_details(RoomUS) || RoomUS <- Rooms], - {jiffy:encode(lists:flatten(RoomsMap)), Req, State}. - -get_room_details({RoomID, RoomS} = RoomUS) -> - HostType = mod_muc_light_utils:muc_host_to_host_type(RoomS), - case mod_muc_light_db_backend:get_config(HostType, RoomUS) of - {ok, Config, _} -> - #{id => RoomID, - name => proplists:get_value(roomname, Config), - subject => proplists:get_value(subject, Config)}; - _ -> - [] - end. +%% Internal functions +handle_get(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + case get_room_jid(Bindings, State, optional) of + undefined -> + {ok, Rooms} = mod_muc_light_api:get_user_rooms(UserJid), + {jiffy:encode(lists:flatmap(fun room_us_to_json/1, Rooms)), Req, State}; + RoomJid -> + case mod_muc_light_api:get_room_info(RoomJid, UserJid) of + {ok, Info} -> + {jiffy:encode(room_info_to_json(Info)), Req, State}; + {room_not_found, Msg} -> + throw_error(not_found, Msg); + {not_room_member, Msg} -> + throw_error(denied, Msg) + end + end. -from_json(Req, State = #{was_replied := true}) -> - {true, Req, State}; -from_json(Req, State) -> - Method = cowboy_req:method(Req), - {ok, Body, Req2} = cowboy_req:read_body(Req), - case mongoose_client_api:json_to_map(Body) of - {ok, #{<<"name">> := N, <<"subject">> := S} = JSONData} when is_binary(N), is_binary(S) -> - handle_request(Method, JSONData, Req2, State); - _ -> - mongoose_client_api:bad_request(Req, <<"Failed to parse parameters">>, State) +handle_post(Req, State = #{jid := UserJid}) -> + MUCLightDomain = muc_light_domain(State), + Args = parse_body(Req), + Name = get_room_name(Args), + Subject = get_room_subject(Args), + {ok, #{jid := RoomJid}} = mod_muc_light_api:create_room(MUCLightDomain, UserJid, Name, Subject), + room_created(Req, State, RoomJid). + +handle_put(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + #jid{luser = RoomId, lserver = MUCLightDomain} = get_room_jid(Bindings, State, required), + Args = parse_body(Req), + Name = get_room_name(Args), + Subject = get_room_subject(Args), + case mod_muc_light_api:create_room(MUCLightDomain, RoomId, UserJid, Name, Subject) of + {ok, #{jid := RoomJid}} -> + room_created(Req, State, RoomJid); + {already_exists, Msg} -> + throw_error(denied, Msg) end. -handle_request(Method, JSONData, Req, State) -> - case handle_request_by_method(Method, JSONData, Req, State) of - {ok, #{jid := RoomJID}} -> - RespBody = #{<<"id">> => RoomJID#jid.luser}, - RespReq = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req), - {true, RespReq, State}; - {Short, Desc} -> - mongoose_client_api:bad_request(Req, format_error(Short, Desc), State) +room_created(Req, State, RoomJid) -> + RespBody = #{<<"id">> => RoomJid#jid.luser}, + Req2 = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req), + Req3 = cowboy_req:reply(201, Req2), + {stop, Req3, State}. + +-spec room_us_to_json(jid:simple_bare_jid()) -> [jiffy:json_value()]. +room_us_to_json({RoomU, RoomS}) -> + #jid{luser = RoomId} = RoomJid = jid:make_noprep(RoomU, RoomS, <<>>), + case mod_muc_light_api:get_room_info(RoomJid) of + {ok, #{name := Name, subject := Subject}} -> + [#{id => RoomId, name => Name, subject => Subject}]; + {room_not_found, _} -> + [] % room was removed after listing rooms, but before this query end. -format_error(Short, Desc) -> - jiffy:encode(#{module => <<"mongoose_client_api_rooms">>, - what => <<"Failed to create room">>, - reason => term_to_bin(Short), - description => term_to_bin(Desc)}). - -term_to_bin(Term) -> - iolist_to_binary(io_lib:format("~p", [Term])). - -handle_request_by_method(<<"POST">>, JSONData, _Req, - #{jid := #jid{lserver = LServer} = UserJID}) -> - #{<<"name">> := RoomName, <<"subject">> := Subject} = JSONData, - MUCServer = muc_light_domain(LServer), - mod_muc_light_api:create_room(MUCServer, UserJID, RoomName, Subject); -handle_request_by_method(<<"PUT">>, JSONData, Req, State) -> - assert_room_id_set(Req, State), - #{jid := #jid{lserver = LServer} = UserJID, room_id := RoomID} = State, - #{<<"name">> := RoomName, <<"subject">> := Subject} = JSONData, - MUCServer = muc_light_domain(LServer), - mod_muc_light_api:create_room(MUCServer, RoomID, UserJID, RoomName, Subject). - -assert_room_id_set(_Req, #{room_id := _} = _State) -> - ok. +-spec room_info_to_json(mod_muc_light_api:room()) -> jiffy:json_value(). +room_info_to_json(#{name := Name, subject := Subject, aff_users := AffUsers}) -> + #{name => Name, subject => Subject, participants => lists:map(fun user_to_json/1, AffUsers)}. user_to_json({UserServer, Role}) -> #{user => jid:to_binary(UserServer), role => Role}. -muc_light_domain(Server) -> - HostType = mod_muc_light_utils:server_host_to_host_type(Server), - mod_muc_light_utils:server_host_to_muc_host(HostType, Server). - -determine_role(US, Users) -> - case lists:keyfind(US, 1, Users) of - false -> none; - {_, Role} -> - Role +get_room_jid(#{id := IdOrJid}, State, _) -> + MUCLightDomain = muc_light_domain(State), + case jid:nodeprep(IdOrJid) of + error -> + case jid:from_binary(IdOrJid) of + error -> + throw_error(bad_request, <<"Invalid room ID">>); + #jid{lserver = MUCLightDomain} = Jid -> + Jid; + #jid{} -> + throw_error(bad_request, <<"Invalid MUC Light domain">>) + end; + RoomId when RoomId =/= <<>> -> + jid:make_noprep(RoomId, MUCLightDomain, <<>>) + end; +get_room_jid(#{}, _State, required) -> throw_error(bad_request, <<"Missing room ID">>); +get_room_jid(#{}, _State, optional) -> undefined. + +get_user_aff(#{jid := UserJid, creds := Creds}, RoomJid) -> + HostType = mongoose_credentials:host_type(Creds), + case mod_muc_light_api:get_room_user_aff(HostType, RoomJid, UserJid) of + {ok, Aff} -> Aff; + {error, room_not_found} -> throw_error(not_found, <<"Room does not exist">>) end. --spec validate_room_id(RoomIDOrJID :: binary(), Server :: binary(), - Req :: cowboy_req:req()) -> - {ok, RoomID :: binary()} | error. -validate_room_id(RoomIDOrJID, Server, Req) -> - MUCLightDomain = muc_light_domain(Server), - case jid:from_binary(RoomIDOrJID) of - #jid{luser = <<>>, lserver = RoomID, lresource = <<>>} -> - {ok, RoomID}; - #jid{luser = RoomID, lserver = MUCLightDomain, lresource = <<>>} -> - {ok, RoomID}; - Other -> - ?LOG_WARNING(#{what => muc_invalid_room_id, - text => <<"REST received room_id field is invalid " - "or of unknown format">>, - server => Server, room => RoomIDOrJID, reason => Other, - req => Req}), - error +muc_light_domain(#{creds := Creds}) -> + HostType = mongoose_credentials:host_type(Creds), + LServer = mongoose_credentials:lserver(Creds), + try + mod_muc_light_utils:server_host_to_muc_host(HostType, LServer) + catch + _:_ -> throw_error(not_found, <<"MUC Light server not found">>) end. + +get_room_name(#{name := Name}) -> Name; +get_room_name(#{}) -> throw_error(bad_request, <<"Missing room name">>). + +get_room_subject(#{subject := Subject}) -> Subject; +get_room_subject(#{}) -> throw_error(bad_request, <<"Missing room subject">>). diff --git a/src/mongoose_client_api/mongoose_client_api_rooms_config.erl b/src/mongoose_client_api/mongoose_client_api_rooms_config.erl index da76ac1a3df..7003db727ec 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_config.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_config.erl @@ -4,18 +4,21 @@ -export([routes/0]). -behaviour(cowboy_rest). --export([trails/0]). --export([init/2]). --export([content_types_provided/2]). --export([content_types_accepted/2]). --export([is_authorized/2]). --export([allowed_methods/2]). --export([resource_exists/2]). - --export([from_json/2]). +-export([trails/0, + init/2, + is_authorized/2, + content_types_accepted/2, + allowed_methods/2, + from_json/2]). -ignore_xref([from_json/2, trails/0]). +-import(mongoose_client_api, [parse_body/1, try_handle_request/3, throw_error/2]). +-import(mongoose_client_api_rooms, [get_room_jid/3, get_room_name/1, get_room_subject/1]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + -spec routes() -> mongoose_http_handler:routes(). routes() -> [{"/rooms/[:id]/config", ?MODULE, #{}}]. @@ -23,50 +26,43 @@ routes() -> trails() -> mongoose_client_api_rooms_config_doc:trails(). +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. init(Req, Opts) -> mongoose_client_api:init(Req, Opts). +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> mongoose_client_api:is_authorized(Req, State). -content_types_provided(Req, State) -> - mongoose_client_api_rooms:content_types_provided(Req, State). - content_types_accepted(Req, State) -> mongoose_client_api_rooms:content_types_accepted(Req, State). allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"PUT">>], Req, State}. -resource_exists(Req, State) -> - mongoose_client_api_rooms:resource_exists(Req, State). - -from_json(Req, State = #{was_replied := true}) -> - {true, Req, State}; +%% @doc Called for a method of type "PUT" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. from_json(Req, State) -> - Method = cowboy_req:method(Req), - {ok, Body, Req2} = cowboy_req:read_body(Req), - case mongoose_client_api:json_to_map(Body) of - {ok, #{<<"name">> := N, <<"subject">> := S} = JSONData} when is_binary(N), is_binary(S) -> - handle_request(Method, JSONData, Req2, State); - _ -> - {false, Req, State} - end. + try_handle_request(Req, State, fun handle_put/2). -handle_request(Method, JSONData, Req, State) -> - case handle_request_by_method(Method, JSONData, Req, State) of +%% Internal functions + +handle_put(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings, State, required), + Args = parse_body(Req), + Name = get_room_name(Args), + Subject = get_room_subject(Args), + Config = #{<<"roomname">> => Name, <<"subject">> => Subject}, + case mod_muc_light_api:change_room_config(RoomJid, UserJid, Config) of {ok, _} -> {true, Req, State}; - {not_allowed, _} -> - mongoose_client_api:forbidden_request(Req, State); - {_, _} -> - {false, Req, State} + {not_room_member, Msg} -> + throw_error(denied, Msg); + {not_allowed, Msg} -> + throw_error(denied, Msg); + {room_not_found, Msg} -> + throw_error(not_found, Msg); + {validation_error, Msg} -> + throw_error(bad_request, Msg) end. - -handle_request_by_method(<<"PUT">>, - #{<<"name">> := RoomName, <<"subject">> := Subject}, - Req, State) -> - mongoose_client_api_rooms:assert_room_id_set(Req, State), - #{jid := UserJID, room := #{jid := RoomJID}} = State, - Config = #{<<"roomname">> => RoomName, <<"subject">> => Subject}, - mod_muc_light_api:change_room_config(RoomJID, UserJID, Config). diff --git a/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl b/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl index 0921c768ca4..a3db7e0945f 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_messages.erl @@ -4,24 +4,24 @@ -export([routes/0]). -behaviour(cowboy_rest). --export([trails/0]). --export([init/2]). --export([content_types_provided/2]). --export([content_types_accepted/2]). --export([is_authorized/2]). --export([allowed_methods/2]). --export([resource_exists/2]). --export([allow_missing_post/2]). - --export([to_json/2]). --export([from_json/2]). +-export([trails/0, + init/2, + is_authorized/2, + content_types_provided/2, + content_types_accepted/2, + allowed_methods/2, + to_json/2, + from_json/2]). + -export([encode/2]). -ignore_xref([from_json/2, to_json/2, trails/0]). --import(mongoose_client_api_messages, [maybe_integer/1]). +-import(mongoose_client_api, [parse_body/1, parse_qs/1, try_handle_request/3, throw_error/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). --include("mongoose.hrl"). -include("jlib.hrl"). -include_lib("exml/include/exml.hrl"). @@ -32,116 +32,107 @@ routes() -> trails() -> mongoose_client_api_rooms_messages_doc:trails(). +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. init(Req, Opts) -> mongoose_client_api:init(Req, Opts). +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> mongoose_client_api:is_authorized(Req, State). +-spec content_types_provided(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_provided(Req, State) -> mongoose_client_api_rooms:content_types_provided(Req, State). +-spec content_types_accepted(req(), state()) -> + {[{{binary(), binary(), '*'}, atom()}], req(), state()}. content_types_accepted(Req, State) -> mongoose_client_api_rooms:content_types_accepted(Req, State). +-spec allowed_methods(req(), state()) -> {[binary()], req(), state()}. allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"GET">>, <<"POST">>], Req, State}. -resource_exists(Req, State) -> - mongoose_client_api_rooms:resource_exists(Req, State). - -allow_missing_post(Req, State) -> - {false, Req, State}. - -to_json(Req, #{role_in_room := none} = State) -> - mongoose_client_api:forbidden_request(Req, State); -to_json(Req, #{jid := UserJID, room := #{jid := RoomJID}} = State) -> - HostType = mod_muc_light_utils:room_jid_to_host_type(RoomJID), - QS = cowboy_req:parse_qs(Req), - PageSize = maybe_integer(proplists:get_value(<<"limit">>, QS, <<"50">>)), - Before = maybe_integer_to_us(proplists:get_value(<<"before">>, QS)), - {ok, Msgs} = mod_muc_light_api:get_room_messages(HostType, RoomJID, UserJID, - PageSize, Before), - JSONData = [make_json_item(Msg) || Msg <- Msgs], - {jiffy:encode(JSONData), Req, State}. - -from_json(Req, #{role_in_room := none} = State) -> - mongoose_client_api:forbidden_request(Req, State); -from_json(Req, #{user := User, jid := JID, room := Room} = State) -> - {ok, Body, Req2} = cowboy_req:read_body(Req), - case mongoose_client_api:json_to_map(Body) of - {ok, JSONData} -> - prepare_message_and_route_to_room(User, JID, Room, State, Req2, JSONData); - _ -> - mongoose_client_api:bad_request(Req2, <<"Request body is not a valid JSON">>, State) +%% @doc Called for a method of type "GET" +-spec to_json(req(), state()) -> {iodata() | stop, req(), state()}. +to_json(Req, State) -> + try_handle_request(Req, State, fun handle_get/2). + +%% @doc Called for a method of type "POST" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. +from_json(Req, State) -> + try_handle_request(Req, State, fun handle_post/2). + +%% Internal functions + +handle_get(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = mongoose_client_api_rooms:get_room_jid(Bindings, State, required), + Args = parse_qs(Req), + Limit = get_limit(Args), + Before = get_before(Args), + case mod_muc_light_api:get_room_messages(RoomJid, UserJid, Limit, Before) of + {ok, Msgs} -> + JSONData = [make_json_item(Msg) || Msg <- Msgs], + {jiffy:encode(JSONData), Req, State}; + {not_room_member, Msg} -> + throw_error(denied, Msg) end. - -prepare_message_and_route_to_room(User, JID, Room, State, Req, JSONData) -> - RoomJID = #jid{lserver = _} = maps:get(jid, Room), +handle_post(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = mongoose_client_api_rooms:get_room_jid(Bindings, State, required), + Args = parse_body(Req), + Children = verify_children(get_body(Args) ++ get_marker(Args) ++ get_markable(Args)), UUID = uuid:uuid_to_string(uuid:get_v4(), binary_standard), - {ok, HostType} = mongoose_domain_api:get_domain_host_type(JID#jid.lserver), - case build_message_from_json(User, RoomJID, UUID, JSONData) of - {ok, Message} -> - Acc = mongoose_acc:new(#{ location => ?LOCATION, - host_type => HostType, - lserver => JID#jid.lserver, - from_jid => JID, - to_jid => RoomJID, - element => Message }), - ejabberd_router:route(JID, RoomJID, Acc, Message), + Attrs = [{<<"id">>, UUID}], + case mod_muc_light_api:send_message(RoomJid, UserJid, Children, Attrs) of + {ok, _} -> Resp = #{id => UUID}, Req3 = cowboy_req:set_resp_body(jiffy:encode(Resp), Req), {true, Req3, State}; - {error, ErrorMsg} -> - Req2 = cowboy_req:set_resp_body(ErrorMsg, Req), - mongoose_client_api:bad_request(Req2, State) + {not_room_member, Msg} -> + throw_error(denied, Msg) end. --spec build_message_from_json(From :: binary(), To :: jid:jid(), ID :: binary(), JSON :: map()) -> - {ok, exml:element()} | {error, ErrorMsg :: binary()}. -build_message_from_json(From, To, ID, JSON) -> - case build_children(JSON) of - {error, _} = Err -> - Err; - [] -> - {error, <<"No valid message elements">>}; - Children -> - Attrs = [{<<"from">>, From}, - {<<"to">>, jid:to_binary(To)}, - {<<"id">>, ID}, - {<<"type">>, <<"groupchat">>}], - {ok, #xmlel{name = <<"message">>, attrs = Attrs, children = Children}} - end. - -build_children(JSON) -> - lists:foldl(fun(_, {error, _} = Err) -> - Err; - (ChildBuilder, Children) -> - ChildBuilder(JSON, Children) - end, [], [fun build_markable/2, fun build_marker/2, fun build_body/2]). - -build_body(#{ <<"body">> := Body }, Children) when is_binary(Body) -> - [#xmlel{ name = <<"body">>, children = [#xmlcdata{ content = Body }] } | Children]; -build_body(#{ <<"body">> := _Body }, _Children) -> - {error, <<"Invalid body, it must be a string">>}; -build_body(_JSON, Children) -> - Children. - -build_marker(#{ <<"chat_marker">> := #{ <<"type">> := Type, <<"id">> := Id } }, Children) +get_limit(#{limit := LimitBin}) -> + try + Limit = binary_to_integer(LimitBin), + true = Limit >= 0 andalso Limit =< 500, + Limit + catch + _:_ -> throw_error(bad_request, <<"Invalid limit">>) + end; +get_limit(#{}) -> 50. + +get_before(#{before := BeforeBin}) -> + try + 1000 * binary_to_integer(BeforeBin) + catch + _:_ -> throw_error(bad_request, <<"Invalid value of 'before'">>) + end; +get_before(#{}) -> undefined. + +get_body(#{body := Body}) when is_binary(Body) -> + [#xmlel{ name = <<"body">>, children = [#xmlcdata{ content = Body }] }]; +get_body(#{body := _}) -> throw_error(bad_request, <<"Invalid message body">>); +get_body(#{}) -> []. + +get_marker(#{chat_marker := #{type := Type, id := Id}}) when Type == <<"received">>; Type == <<"displayed">>; Type == <<"acknowledged">> -> - [#xmlel{ name = Type, attrs = [{<<"xmlns">>, ?NS_CHAT_MARKERS}, {<<"id">>, Id}] } | Children]; -build_marker(#{ <<"chat_marker">> := _Marker }, _Children) -> - {error, <<"Invalid marker, it must be 'received', 'displayed' or 'acknowledged'">>}; -build_marker(_JSON, Children) -> - Children. + [#xmlel{ name = Type, attrs = [{<<"xmlns">>, ?NS_CHAT_MARKERS}, {<<"id">>, Id}] }]; +get_marker(#{chat_marker := _}) -> throw_error(bad_request, <<"Invalid chat marker">>); +get_marker(#{}) -> []. + +get_markable(#{body := _, markable := true}) -> + [#xmlel{ name = <<"markable">>, attrs = [{<<"xmlns">>, ?NS_CHAT_MARKERS}] }]; +get_markable(#{}) -> []. -build_markable(#{ <<"body">> := _Body, <<"markable">> := true }, Children) -> - [#xmlel{ name = <<"markable">>, attrs = [{<<"xmlns">>, ?NS_CHAT_MARKERS}] } | Children]; -build_markable(_JSON, Children) -> - Children. +verify_children([]) -> throw_error(bad_request, <<"No valid message elements">>); +verify_children(Children) -> Children. -spec encode(Packet :: exml:element(), Timestamp :: integer()) -> map(). encode(Packet, Timestamp) -> @@ -199,8 +190,3 @@ add_aff_change_body(Item, #xmlel{attrs = Attrs} = User) -> Item#{type => <<"affiliation">>, affiliation => proplists:get_value(<<"affiliation">>, Attrs), user => exml_query:cdata(User)}. - -maybe_integer_to_us(undefined) -> - undefined; -maybe_integer_to_us(Val) -> - binary_to_integer(Val) * 1000. diff --git a/src/mongoose_client_api/mongoose_client_api_rooms_users.erl b/src/mongoose_client_api/mongoose_client_api_rooms_users.erl index 528e58040cd..7893b127076 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_users.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_users.erl @@ -4,20 +4,22 @@ -export([routes/0]). -behaviour(cowboy_rest). --export([trails/0]). --export([init/2]). --export([content_types_provided/2]). --export([content_types_accepted/2]). --export([is_authorized/2]). --export([allowed_methods/2]). --export([resource_exists/2]). --export([allow_missing_post/2]). - --export([from_json/2]). --export([delete_resource/2]). +-export([trails/0, + init/2, + is_authorized/2, + content_types_accepted/2, + allowed_methods/2, + from_json/2, + delete_resource/2]). -ignore_xref([from_json/2, trails/0]). +-import(mongoose_client_api, [parse_body/1, try_handle_request/3, throw_error/2]). +-import(mongoose_client_api_rooms, [get_room_jid/3, get_user_aff/2]). + +-type req() :: cowboy_req:req(). +-type state() :: map(). + -spec routes() -> mongoose_http_handler:routes(). routes() -> [{"/rooms/:id/users/[:user]", ?MODULE, #{}}]. @@ -25,57 +27,59 @@ routes() -> trails() -> mongoose_client_api_rooms_users_doc:trails(). +-spec init(req(), state()) -> {cowboy_rest, req(), state()}. init(Req, Opts) -> mongoose_client_api:init(Req, Opts). +-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. is_authorized(Req, State) -> mongoose_client_api:is_authorized(Req, State). -content_types_provided(Req, State) -> - mongoose_client_api_rooms:content_types_provided(Req, State). - content_types_accepted(Req, State) -> mongoose_client_api_rooms:content_types_accepted(Req, State). allowed_methods(Req, State) -> {[<<"OPTIONS">>, <<"POST">>, <<"DELETE">>], Req, State}. -resource_exists(Req, State) -> - mongoose_client_api_rooms:resource_exists(Req, State). - -allow_missing_post(Req, State) -> - {false, Req, State}. - -from_json(Req, #{role_in_room := owner, - jid := UserJID, - room := #{jid := RoomJID}} = State) -> - {ok, Body, Req2} = cowboy_req:read_body(Req), - case mongoose_client_api:json_to_map(Body) of - {ok, #{<<"user">> := UserToInvite}} when is_binary(UserToInvite) -> - mod_muc_light_api:change_affiliation(RoomJID, UserJID, - jid:from_binary(UserToInvite), <<"member">>), - {true, Req2, State}; - _ -> - {false, Req, State} - end; +%% @doc Called for a method of type "POST" +-spec from_json(req(), state()) -> {true | stop, req(), state()}. from_json(Req, State) -> - mongoose_client_api:forbidden_request(Req, State). - -delete_resource(Req, #{role_in_room := none} = State) -> - mongoose_client_api:forbidden_request(Req, State); -delete_resource(Req, #{role_in_room := owner} = State) -> - UserToRemove = cowboy_req:binding(user, Req), - remove_user_from_room(UserToRemove, Req, State); -delete_resource(Req, #{user := User} = State) -> - UserToRemove = cowboy_req:binding(user, Req), - case UserToRemove of - User -> - remove_user_from_room(User, Req, State); - _ -> - mongoose_client_api:forbidden_request(Req, State) - end. - -remove_user_from_room(Target, Req, - #{jid := UserJID, room := #{jid := RoomJID}} = State) -> - mod_muc_light_api:change_affiliation(RoomJID, UserJID, jid:from_binary(Target), <<"none">>), + try_handle_request(Req, State, fun handle_post/2). + +%% @doc Called for a method of type "DELETE" +-spec delete_resource(req(), state()) -> {true | stop, req(), state()}. +delete_resource(Req, State) -> + try_handle_request(Req, State, fun handle_delete/2). + +%% Internal functions + +handle_post(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings, State, required), + Args = parse_body(Req), + TargetJid = get_user_jid(Args), + assert_permissions(get_user_aff(State, RoomJid), add, UserJid, TargetJid), + mod_muc_light_api:change_affiliation(RoomJid, UserJid, TargetJid, <<"member">>), {true, Req, State}. + +handle_delete(Req, State = #{jid := UserJid}) -> + Bindings = cowboy_req:bindings(Req), + RoomJid = get_room_jid(Bindings, State, required), + TargetJid = get_user_jid(Bindings), + assert_permissions(get_user_aff(State, RoomJid), remove, UserJid, TargetJid), + mod_muc_light_api:change_affiliation(RoomJid, UserJid, TargetJid, <<"none">>), + {true, Req, State}. + +-spec assert_permissions(mod_muc_light_api:aff(), add | remove, jid:jid(), jid:jid()) -> ok. +assert_permissions(owner, _Op, _UserJid, _TargetJid) -> ok; +assert_permissions(member, remove, UserJid, UserJid) -> ok; +assert_permissions(_Aff, _Op, _UserJid, _TargetJid) -> + throw_error(denied, <<"Operation not permitted for this user">>). + +get_user_jid(#{user := JidBin}) -> + case jid:from_binary(JidBin) of + error -> throw_error(bad_request, <<"Invalid user JID: ", JidBin/binary>>); + Jid -> Jid + end; +get_user_jid(#{}) -> + throw_error(bad_request, <<"Missing JID">>). diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 9bed606a9ba..efa6db151bd 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -28,7 +28,6 @@ register_user/3, remove_user/3, resend_offline_messages_hook/2, - rest_user_send_packet/4, session_cleanup/5, set_vcard/3, unacknowledged_message/2, @@ -384,21 +383,6 @@ resend_offline_messages_hook(Acc, JID) -> HostType = mongoose_acc:host_type(Acc), run_hook_for_host_type(resend_offline_messages_hook, HostType, Acc, [JID]). -%%% @doc The `rest_user_send_packet' hook is called when a user sends -%%% a message using the REST API. --spec rest_user_send_packet(Acc, From, To, Packet) -> Result when - Acc :: mongoose_acc:t(), - From :: jid:jid(), - To :: jid:jid(), - Packet :: exml:element(), - Result :: mongoose_acc:t(). -rest_user_send_packet(Acc, From, To, Packet) -> - Params = #{}, - Args = [From, To, Packet], - ParamsWithLegacyArgs = ejabberd_hooks:add_args(Params, Args), - HostType = mongoose_acc:host_type(Acc), - run_hook_for_host_type(rest_user_send_packet, HostType, Acc, ParamsWithLegacyArgs). - %%% @doc The `session_cleanup' hook is called when sm backend cleans up a user's session. -spec session_cleanup(Server, Acc, User, Resource, SID) -> Result when Server :: jid:server(), diff --git a/src/muc_light/mod_muc_light_api.erl b/src/muc_light/mod_muc_light_api.erl index 83ba0f2cbb9..f3502af5bc4 100644 --- a/src/muc_light/mod_muc_light_api.erl +++ b/src/muc_light/mod_muc_light_api.erl @@ -9,6 +9,7 @@ change_affiliation/4, remove_user_from_room/3, send_message/3, + send_message/4, delete_room/2, delete_room/1, get_room_messages/3, @@ -19,6 +20,7 @@ get_room_info/2, get_room_aff/1, get_room_aff/2, + get_room_user_aff/3, get_blocking_list/1, set_blocking/2 ]). @@ -105,7 +107,7 @@ change_room_config(#jid{luser = RoomID, lserver = MUCServer} = RoomJID, {not_allowed, "Given user does not have permission to change config"}; {error, not_exists} -> ?ROOM_NOT_FOUND_RESULT; - {error, {error, {Key, Reason}}} -> + {error, {Key, Reason}} -> ?VALIDATION_ERROR_RESULT(Key, Reason) end; {error, not_found} -> @@ -130,20 +132,23 @@ remove_user_from_room(RoomJID, SenderJID, RecipientJID) -> -spec send_message(jid:jid(), jid:jid(), binary()) -> {ok | not_room_member | muc_server_not_found, iolist()}. -send_message(#jid{lserver = MUCServer} = RoomJID, SenderJID, Message) -> +send_message(RoomJID, SenderJID, Text) when is_binary(Text) -> + Body = #xmlel{name = <<"body">>, children = [#xmlcdata{content = Text}]}, + send_message(RoomJID, SenderJID, [Body], []). + +-spec send_message(jid:jid(), jid:jid(), [exml:element()], [exml:attr()]) -> + {ok | not_room_member | muc_server_not_found, iolist()}. +send_message(#jid{lserver = MUCServer} = RoomJID, SenderJID, Children, ExtraAttrs) -> case mongoose_domain_api:get_subdomain_host_type(MUCServer) of {ok, HostType} -> - Body = #xmlel{name = <<"body">>, - children = [ #xmlcdata{ content = Message } ] - }, + SenderBare = jid:to_bare(SenderJID), + RoomBare = jid:to_bare(RoomJID), Stanza = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = [ Body ] + attrs = [{<<"type">>, <<"groupchat">>} | ExtraAttrs], + children = Children }, - SenderBare = jid:to_bare(SenderJID), case is_user_room_member(HostType, jid:to_lus(SenderBare), jid:to_lus(RoomJID)) of true -> - RoomBare = jid:to_bare(RoomJID), ejabberd_router:route(SenderBare, RoomBare, Stanza), {ok, "Message sent successfully"}; false -> diff --git a/src/muc_light/mod_muc_light_room_config.erl b/src/muc_light/mod_muc_light_room_config.erl index 36ee132890c..5db3941f992 100644 --- a/src/muc_light/mod_muc_light_room_config.erl +++ b/src/muc_light/mod_muc_light_room_config.erl @@ -83,7 +83,9 @@ from_binary_kv(RawConfig, ConfigSchema, Config) -> end. take_next_kv([{KeyBin, ValBin} | RRawConfig], [{KeyBin, _Default, Key, Type} | RSchema]) -> - {value, RRawConfig, RSchema, {Key, b2value(ValBin, Type)}}; + try {value, RRawConfig, RSchema, {Key, b2value(ValBin, Type)}} + catch _:_ -> {error, {KeyBin, type_error}} + end; take_next_kv(RawConfig, [{_KeyBin, Default, Key, _Type} | RSchema]) -> {default, RawConfig, RSchema, {Key, Default}}; take_next_kv([{KeyBin, _} | _], _) -> @@ -99,11 +101,11 @@ to_binary_kv(Config, ConfigSchema) -> %%==================================================================== -spec b2value(ValBin :: binary(), Type :: value_type()) -> Converted :: value(). -b2value(ValBin, binary) -> ValBin; +b2value(ValBin, binary) when is_binary(ValBin) -> ValBin; b2value(ValBin, integer) -> binary_to_integer(ValBin); b2value(ValBin, float) -> binary_to_float(ValBin). -spec value2b(Val :: value(), Type :: value_type()) -> Converted :: binary(). -value2b(Val, binary) -> Val; +value2b(Val, binary) when is_binary(Val) -> Val; value2b(Val, integer) -> integer_to_binary(Val); value2b(Val, float) -> float_to_binary(Val). diff --git a/src/muc_light/mod_muc_light_utils.erl b/src/muc_light/mod_muc_light_utils.erl index 64c15749a0f..bb99cab80de 100644 --- a/src/muc_light/mod_muc_light_utils.erl +++ b/src/muc_light/mod_muc_light_utils.erl @@ -33,7 +33,6 @@ -export([room_jid_to_host_type/1]). -export([room_jid_to_server_host/1]). -export([muc_host_to_host_type/1]). --export([server_host_to_host_type/1]). -export([server_host_to_muc_host/2]). -export([run_forget_room_hook/1]). @@ -295,14 +294,6 @@ room_jid_to_server_host(#jid{lserver = MucHost}) -> error({room_jid_to_server_host_failed, MucHost, Other}) end. -server_host_to_host_type(LServer) -> - case mongoose_domain_api:get_domain_host_type(LServer) of - {ok, HostType} -> - HostType; - Other -> - error({server_host_to_host_type_failed, LServer, Other}) - end. - muc_host_to_host_type(MucHost) -> case mongoose_domain_api:get_subdomain_host_type(MucHost) of {ok, HostType} ->