From 1cbc9f23c13685e9c95f0ce299c49b538a840313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Wed, 9 Feb 2022 19:59:17 +0100 Subject: [PATCH] WiP --- big_tests/tests/graphql_muc_light_SUITE.erl | 228 +++++++++++++++ big_tests/tests/rest_client_SUITE.erl | 6 +- priv/graphql/schemas/admin/admin_schema.gql | 4 + priv/graphql/schemas/admin/muc_light.gql | 52 ++++ .../admin/mongoose_graphql_admin_mutation.erl | 2 + .../admin/mongoose_graphql_admin_query.erl | 2 + ...goose_graphql_muc_light_admin_mutation.erl | 66 +++++ ...mongoose_graphql_muc_light_admin_query.erl | 16 + src/graphql/mongoose_graphql.erl | 2 + .../mongoose_client_api_rooms_config.erl | 6 +- src/muc_light/mod_muc_light.erl | 4 +- src/muc_light/mod_muc_light_api.erl | 275 ++++++++++++++++++ src/muc_light/mod_muc_light_commands.erl | 217 +++----------- 13 files changed, 691 insertions(+), 189 deletions(-) create mode 100644 big_tests/tests/graphql_muc_light_SUITE.erl create mode 100644 priv/graphql/schemas/admin/muc_light.gql create mode 100644 src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl create mode 100644 src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl create mode 100644 src/muc_light/mod_muc_light_api.erl diff --git a/big_tests/tests/graphql_muc_light_SUITE.erl b/big_tests/tests/graphql_muc_light_SUITE.erl new file mode 100644 index 0000000000..a57d2fffca --- /dev/null +++ b/big_tests/tests/graphql_muc_light_SUITE.erl @@ -0,0 +1,228 @@ +-module(graphql_muc_light_SUITE). + +-compile([export_all, nowarn_export_all]). + +-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). +-import(graphql_helper, [execute/3, execute_auth/2, get_listener_port/1, + get_listener_config/1, get_ok_value/2, get_err_msg/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("jid/include/jid.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +suite() -> + require_rpc_nodes([mim]) ++ escalus:suite(). + +all() -> + [{group, user_muc_light}, + {group, admin_muc_light}]. + +groups() -> + [{user_muc_light, [], user_muc_light_handler()}, + {admin_muc_light, [], admin_muc_light_handler()}]. + +user_muc_light_handler() -> + [mock]. + +admin_muc_light_handler() -> + [admin_create_room, + admin_create_identified_room, + admin_change_room_configuration, + admin_invite_user, + admin_delete_room, + admin_kick_user + ]. + +init_per_suite(Config) -> + Config1 = init_modules(Config), + [{muc_light_host, muc_light_helper:muc_host()} + | escalus:init_per_suite(Config1)]. + +end_per_suite(Config) -> + escalus_fresh:clean(), + dynamic_modules:restore_modules(Config), + escalus:end_per_suite(Config). + +init_modules(Config) -> + HostType = domain_helper:host_type(), + Config1 = dynamic_modules:save_modules(HostType, Config), + Config2 = rest_helper:maybe_enable_mam(mam_helper:backend(), HostType, Config1), + dynamic_modules:ensure_modules(HostType, required_modules(suite)), + Config2. + +required_modules(SuiteOrTC) -> + [{mod_muc_light, common_muc_light_opts() ++ muc_light_opts(SuiteOrTC)}]. + +muc_light_opts(config_can_be_changed_by_all) -> + [{all_can_configure, true}]; +muc_light_opts(suite) -> + []. + +common_muc_light_opts() -> + MucPattern = distributed_helper:subhost_pattern(muc_light_helper:muc_host_pattern()), + [{host, MucPattern}, + {rooms_in_rosters, true}]. + +init_per_group(admin_muc_light, Config) -> + graphql_helper:init_admin_handler(Config); +init_per_group(_GN, Config) -> + Config. + +end_per_group(_GN, Config) -> + Config. + +init_per_testcase(TC, Config) -> + escalus:init_per_testcase(TC, Config). + +end_per_testcase(TC, Config) -> + + escalus:end_per_testcase(TC, Config). + +mock(_Config) -> + ok. + +admin_create_room(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_create_room_story/2). + +admin_create_room_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + AliceBinLower = escalus_utils:jid_to_lower(AliceBin), + Domain = escalus_client:server(Alice), + MucServer = <<"muclight.", Domain/binary>>, + Name = <<"first room">>, + Subject = <<"testing">>, + + Res = execute_auth(admin_create_room_body(Domain, Name, AliceBin, Subject, null), Config), + Path = [data, muc_light, createRoom], + #{<<"jid">> := JID, <<"name">> := Name, <<"subject">> := Subject, + <<"participants">> := Participants} = get_ok_value(Path, Res), + ?assertMatch(#jid{server = MucServer}, jid:from_binary(JID)), + ?assertEqual([#{<<"jid">> => AliceBinLower, <<"affiliance">> => <<"owner">>}], Participants). + % Try to create room with existing nam + %Res2 = execute_auth(admin_create_room_body(Domain, Name, AliceBin, Subject, null), Config), + %?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"already exists">>)). + +admin_create_identified_room(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_create_identified_room_story/2). + +admin_create_identified_room_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + Domain = escalus_client:server(Alice), + MucServer = <<"muclight.", Domain/binary>>, + Name = <<"first room">>, + Subject = <<"testing">>, + Id = <<"my_room">>, + + Res = execute_auth(admin_create_room_body(Domain, Name, AliceBin, Subject, Id), Config), + Path = [data, muc_light, createRoom], + #{<<"jid">> := JID, <<"name">> := Name, <<"subject">> := Subject} = get_ok_value(Path, Res), + ?assertMatch(#jid{user = Id, server = MucServer}, jid:from_binary(JID)), + % Try to create room with existing ID + Res2 = execute_auth(admin_create_room_body(Domain, <<"snd room">>, AliceBin, Subject, Id), Config), + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res2), <<"already exists">>)). + +admin_change_room_configuration(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_change_room_configuration_story/2). + +admin_change_room_configuration_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + Domain = escalus_client:server(Alice), + Name = <<"first room">>, + Subject = <<"testing">>, + Id = atom_to_binary(?FUNCTION_NAME), + %% Create a new room + execute_auth(admin_create_room_body(Domain, Name, AliceBin, Subject, Id), Config), + %% Try to change the room configuration + Name2 = <<"changed room">>, + Subject2 = <<"not testing">>, + Res = execute_auth(admin_change_room_configuration_body(Id, Domain, AliceBin, Name2, Subject2), Config), + Path = [data, muc_light, changeRoomConfiguration], + ?assertMatch(#{<<"name">> := Name2, <<"subject">> := Subject2}, get_ok_value(Path, Res)). + + +admin_invite_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun admin_invite_user_story/3). + +admin_invite_user_story(Config, Alice, Bob) -> + AliceBin = escalus_client:short_jid(Alice), + BobBin = escalus_client:short_jid(Bob), + Domain = escalus_client:server(Alice), + Name = <<"first room">>, + {ok, #{jid := RoomJID}} = create_room(<<>>, Domain, Name, <<>>, jid:from_binary(AliceBin)), + Res = execute_auth(admin_invite_user_body(Domain, Name, AliceBin, BobBin), Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value([data, muc_light, inviteUser], Res), + <<"successfully">>)), + BobName = escalus_utils:jid_to_lower(escalus_client:username(Bob)), + AliceName = escalus_utils:jid_to_lower(escalus_client:username(Alice)), + ExpectedAff = lists:sort([{{AliceName, Domain}, owner}, + {{BobName, Domain}, member}]), + ?assertMatch(ExpectedAff, lists:sort(get_room_aff(RoomJID))). + +admin_delete_room(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_delete_room_story/2). + +admin_delete_room_story(Config, Alice) -> + AliceBin = escalus_client:short_jid(Alice), + Domain = escalus_client:server(Alice), + Name = <<"first room">>, + RoomID = <<"delete_room_id">>, + {ok, #{jid := RoomJID}} = create_room(RoomID, Domain, Name, <<>>, jid:from_binary(AliceBin)), + + Res = execute_auth(admin_delete_room_body(Domain, RoomID), Config), + ?assertNotEqual(nomatch, binary:match(get_ok_value([data, muc_light, deleteRoom], Res), + <<"successfully">>)), + ?assertEqual({error, not_exists}, get_room_info(jid:from_binary(RoomJID))). + + +admin_kick_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], fun admin_kick_user_story/3). + +admin_kick_user_story(Config, Alice, Bob) -> + ok. + + +%% Helpers + +create_room(Id, Domain, Name, Subject, CreatorJID) -> + rpc(mim(), mod_muc_light_api, create_room, [Domain, Id, Name, CreatorJID, Subject]). + +get_room_info(JID) -> + HostType = domain_helper:host_type(), + RoomUS = jid:to_lus(JID), + rpc(mim(), mod_muc_light_db_backend, get_info, [HostType, RoomUS]). + +get_room_aff(JID) -> + {ok, _, Aff, _} = get_room_info(JID), + Aff. + +%% Request bodies + +admin_create_room_body(Domain, Name, Owner, Subject, Id) -> + Query = <<"mutation M1($domain: String!, $name: String!, $owner: JID!, $subject: String!, $id: String) + { muc_light { createRoom(domain: $domain, name: $name, owner: $owner, subject: $subject, id: $id) + { jid name subject participants {jid affiliance} } } }">>, + OpName = <<"M1">>, + Vars = #{<<"domain">> => Domain, <<"name">> => Name, <<"owner">> => Owner, <<"subject">> => Subject, <<"id">> => Id}, + #{query => Query, operationName => OpName, variables => Vars}. + +admin_change_room_configuration_body(Id, Domain, Owner, Name, Subject) -> + Query = <<"mutation M1($id: String!, $domain: String!, $name: String!, $owner: JID!, $subject: String!) + { muc_light { changeRoomConfiguration(id: $id, domain: $domain, name: $name, owner: $owner, subject: $subject) + { jid name subject participants {jid affiliance} } } }">>, + OpName = <<"M1">>, + Vars = #{<<"id">> => Id, <<"domain">> => Domain, <<"name">> => Name, <<"owner">> => Owner, <<"subject">> => Subject}, + #{query => Query, operationName => OpName, variables => Vars}. + +admin_invite_user_body(Domain, Name, Sender, Recipient) -> + Query = <<"mutation M1($domain: String!, $name: String!, $sender: JID!, $recipient: JID!) + { muc_light { inviteUser(domain: $domain, name: $name, sender: $sender, recipient: $recipient) } }">>, + OpName = <<"M1">>, + Vars = #{<<"domain">> => Domain, <<"name">> => Name, <<"sender">> => Sender, <<"recipient">> => Recipient}, + #{query => Query, operationName => OpName, variables => Vars}. + +admin_delete_room_body(Domain, RoomID) -> + Query = <<"mutation M1($domain: String!, $id: String!) + { muc_light { deleteRoom(domain: $domain, id: $id)} }">>, + OpName = <<"M1">>, + Vars = #{<<"domain">> => Domain, <<"id">> => RoomID}, + #{query => Query, operationName => OpName, variables => Vars}. diff --git a/big_tests/tests/rest_client_SUITE.erl b/big_tests/tests/rest_client_SUITE.erl index c19872c30c..262f1490b2 100644 --- a/big_tests/tests/rest_client_SUITE.erl +++ b/big_tests/tests/rest_client_SUITE.erl @@ -39,14 +39,14 @@ all() -> {group, security}]. groups() -> - G = [{messages_with_props, [parallel], message_with_props_test_cases()}, + _G = [{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_config, [], muc_config_cases()}, {roster, [parallel], roster_test_cases()}, - {security, [], security_test_cases()}], - ct_helper:repeat_all_until_all_ok(G). + {security, [], security_test_cases()}]. + %ct_helper:repeat_all_until_all_ok(G). message_test_cases() -> [msg_is_sent_and_delivered_over_xmpp, diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index 4f663fda6e..8cffc62f95 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -18,6 +18,8 @@ type AdminQuery{ session: SessionAdminQuery "Stanza management" stanza: StanzaAdminQuery + "MUC Light room management" + muc_light: MUCLightAdminQuery } """ @@ -33,4 +35,6 @@ type AdminMutation @protected{ session: SessionAdminMutation "Stanza management" stanza: StanzaAdminMutation + "MUC Light room management" + muc_light: MUCLightAdminMutation } diff --git a/priv/graphql/schemas/admin/muc_light.gql b/priv/graphql/schemas/admin/muc_light.gql new file mode 100644 index 0000000000..11b32adfac --- /dev/null +++ b/priv/graphql/schemas/admin/muc_light.gql @@ -0,0 +1,52 @@ +""" +Allow admin to manage Multi-User Chat Light rooms. +""" +type MUCLightAdminMutation @protected{ + "Create a MUC light room under the given XMPP hostname" + createRoom(domain: String!, name: String!, owner: JID!, subject: String!, id: String): Room + "Change configuration of MUC Light room" + changeRoomConfiguration(id: String!, domain: String!, name: String!, owner: JID!, subject: String!): Room + "Invite a user to a MUC Light room" + inviteUser(domain: String!, name: String!, sender: JID!, recipient: JID!): String + "Remove a MUC light room" + deleteRoom(id: String!, domain: String!): String + "Remove a user from a room" + kickUser(domain: String!, name: String!, kicker: JID!, kicked: JID!): String + "Send a message to a MUC Light room" + sendMessageToRoom(domain: String!, name: String!, from: JID!, body: String!): String +} + +""" +Allow admin to get information about Multi-User Chat Light rooms. +""" +type MUCLightAdminQuery @protected{ + "Get users list of MUC Light room" + listRoomUsers(domain: String!, name: String!): String + "Get configuration of the MUC Light room" + getRoomConfig(domain: String!, name: String!): Room + + #readMessages(domain: String!, name: String!) +} + +type RoomPayload{ + message: String! + room: Room +} + +type Room{ + jid: JID + name: String + subject: String + participants: [RoomUser!] +} + +type RoomUser{ + jid: JID + affiliance: String +} + +enum Affiliance{ + OWNER + MEMBER + NONE +} diff --git a/src/graphql/admin/mongoose_graphql_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_admin_mutation.erl index ec07208772..fcb6c974d8 100644 --- a/src/graphql/admin/mongoose_graphql_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_admin_mutation.erl @@ -10,6 +10,8 @@ execute(_Ctx, _Obj, <<"domains">>, _Args) -> {ok, admin}; execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> + {ok, muc_light}; execute(_Ctx, _Obj, <<"session">>, _Opts) -> {ok, session}; execute(_Ctx, _Obj, <<"stanza">>, _) -> diff --git a/src/graphql/admin/mongoose_graphql_admin_query.erl b/src/graphql/admin/mongoose_graphql_admin_query.erl index bb06dc4310..ee71ebbeab 100644 --- a/src/graphql/admin/mongoose_graphql_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_admin_query.erl @@ -10,6 +10,8 @@ execute(_Ctx, _Obj, <<"domains">>, _Args) -> {ok, admin}; execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> + {ok, muc_light}; execute(_Ctx, _Obj, <<"session">>, _Opts) -> {ok, session}; execute(_Ctx, _Obj, <<"stanza">>, _Opts) -> diff --git a/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl new file mode 100644 index 0000000000..43032879ea --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl @@ -0,0 +1,66 @@ +-module(mongoose_graphql_muc_light_admin_mutation). + +-export([execute/4]). + +-ignore_xref([execute/4]). + +-import(mongoose_graphql_helper, [make_error/2, format_result/2]). + +execute(_Ctx, _Obj, <<"createRoom">>, Args) -> + create_room(Args); +execute(_Ctx, _Obj, <<"changeRoomConfiguration">>, Args) -> + change_room_config(Args); +execute(_Ctx, _Obj, <<"inviteUser">>, Args) -> + invite_user(Args); +execute(_Ctx, _Obj, <<"deleteRoom">>, Args) -> + delete_room(Args); +execute(_Ctx, _Obj, <<"kickUser">>, Args) -> + kick_user(Args); +execute(_Ctx, _Obj, <<"sendMessageToRoom">>, Args) -> + send_msg_to_room(Args). + +create_room(#{<<"id">> := null} = Args) -> + create_room(Args#{<<"id">> => <<>>}); +create_room(#{<<"id">> := Id, <<"domain">> := Domain, <<"name">> := RoomName, + <<"owner">> := CreatorJID, <<"subject">> := Subject}) -> + case mod_muc_light_api:create_room(Domain, Id, RoomName, CreatorJID, Subject) of + {ok, Room} -> + {ok, make_room(Room)}; + Err -> + make_error(Err, #{}) + end. + +change_room_config(#{<<"id">> := RoomID, <<"domain">> := Domain, <<"name">> := RoomName, + <<"owner">> := OwnerJID, <<"subject">> := Subject}) -> + case mod_muc_light_api:change_room_config(Domain, RoomID, RoomName, OwnerJID, Subject) of + {ok, Room} -> + {ok, make_room(Room)}; + Err -> + make_error(Err, #{}) + end. + +delete_room(#{<<"domain">> := Domain, <<"id">> := RoomID}) -> + Result = mod_muc_light_api:delete_room(Domain, RoomID), + format_result(Result, #{}). + +invite_user(#{<<"domain">> := Domain, <<"name">> := Name, <<"sender">> := SenderJID, + <<"recipient">> := RecipientJID}) -> + Result = mod_muc_light_api:invite_to_room(Domain, Name, SenderJID, RecipientJID), + format_result(Result, #{}). + +kick_user(#{}) -> + {ok, <<"Not implemented">>}. + +send_msg_to_room(#{}) -> + {ok, <<"Not implemented">>}. + +%% Internal + +-spec make_room(mod_muc_light_api:room()) -> map(). +make_room(#{jid := JID, name := Name, subject := Subject, aff_users := Users}) -> + Participants = lists:map(fun make_ok_user/1, Users), + #{<<"jid">> => JID, <<"name">> => Name, <<"subject">> => Subject, + <<"participants">> => Participants}. + +make_ok_user({JID, Aff}) -> + {ok, #{<<"jid">> => JID, <<"affiliance">> => atom_to_binary(Aff)}}. diff --git a/src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl b/src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl new file mode 100644 index 0000000000..83274ae072 --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl @@ -0,0 +1,16 @@ +-module(mongoose_graphql_muc_light_admin_query). + +-export([execute/4]). + +-ignore_xref([execute/4]). + +execute(_Ctx, _Obj, <<"listRoomUsers">>, Args) -> + list_room_users(Args); +execute(_Ctx, _Obj, <<"getRoomConfig">>, Args) -> + get_room_config(Args). + +list_room_users(#{}) -> + {ok, <<"Not implemented">>}. + +get_room_config(#{}) -> + {ok, <<"Not implemented">>}. diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 38dc017cc0..16887069d4 100644 --- a/src/graphql/mongoose_graphql.erl +++ b/src/graphql/mongoose_graphql.erl @@ -127,6 +127,8 @@ admin_mapping_rules() -> 'StanzaAdminQuery' => mongoose_graphql_stanza_admin_query, 'AccountAdminQuery' => mongoose_graphql_account_admin_query, 'AccountAdminMutation' => mongoose_graphql_account_admin_mutation, + 'MUCLightAdminMutation' => mongoose_graphql_muc_light_admin_mutation, + 'MUCLightAdminQuery' => mongoose_graphql_muc_light_admin_query, 'Domain' => mongoose_graphql_domain, default => mongoose_graphql_default}, interfaces => #{default => mongoose_graphql_default}, 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 18d13559ff..2dad36a894 100644 --- a/src/mongoose_client_api/mongoose_client_api_rooms_config.erl +++ b/src/mongoose_client_api/mongoose_client_api_rooms_config.erl @@ -54,11 +54,11 @@ from_json(Req, State) -> handle_request(Method, JSONData, Req, State) -> case handle_request_by_method(Method, JSONData, Req, State) of - ok -> + {ok, _} -> {true, Req, State}; - {error, internal, not_allowed} -> + {not_allowed, _} -> mongoose_client_api:forbidden_request(Req, State); - {error, internal, _} -> + {_, _} -> {false, Req, State} end. diff --git a/src/muc_light/mod_muc_light.erl b/src/muc_light/mod_muc_light.erl index c828de2e43..2c8cccadcc 100644 --- a/src/muc_light/mod_muc_light.erl +++ b/src/muc_light/mod_muc_light.erl @@ -142,12 +142,12 @@ try_to_create_room(CreatorJid, RoomJID, #create{raw_config = RawConfig} = Creati Error end. --spec change_room_config(UserJid :: jid:jid(), RoomID :: jid:resource(), +-spec change_room_config(UserJid :: jid:jid(), RoomID :: jid:user(), MUCLightDomain :: jid:server(), ConfigReq :: config_req_props(), Acc :: mongoose_acc:t()) -> {ok, jid:jid(), config_req_props()} - | {error, validation_error() | bad_request | not_allowed}. + | {error, validation_error() | bad_request | not_allowed | item_not_found}. change_room_config(UserJid, RoomID, MUCLightDomain, ConfigReq, Acc1) -> RoomJID = jid:make(RoomID, MUCLightDomain, <<>>), {Acc2, AffUsersRes} = get_room_affiliations_from_acc(Acc1, RoomJID), diff --git a/src/muc_light/mod_muc_light_api.erl b/src/muc_light/mod_muc_light_api.erl new file mode 100644 index 0000000000..2035fe03da --- /dev/null +++ b/src/muc_light/mod_muc_light_api.erl @@ -0,0 +1,275 @@ +-module(mod_muc_light_api). + +-export([create_room/5, + invite_to_room/4, + change_room_config/5, + change_affiliation/5, + send_message/4, + delete_room/3, + delete_room/2, + delete_room/1 + ]). +-ignore_xref([delete_room/1]). + +-include("mod_muc_light.hrl"). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +-type create_room_result() :: {ok, room()} | {exist | + max_occupants_reached | + bad_request | + validation_error , iodata()}. + +-type invite_to_room_result() :: {ok | forbidden | not_found, iodata()}. + +-type change_room_config_result() :: {ok, room()} | {wrong_user | + not_allowed | + validation_error | + bad_request, iodata()}. + +-type room() :: #{jid := jid:jid(), + name := binary(), + subject := binary(), + aff_users := aff_users() + }. + +-export_type([room/0, create_room_result/0]). + +-spec create_room(binary(), binary(), binary(), jid:jid(), binary()) -> create_room_result(). +create_room(Domain, RoomId, RoomTitle, CreatorJID, Subject) -> + LServer = jid:nameprep(Domain), + HostType = mod_muc_light_utils:server_host_to_host_type(LServer), + MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), + MUCServiceJID = jid:make(RoomId, MUCLightDomain, <<>>), + Config = make_room_config(RoomTitle, Subject), + case mod_muc_light:try_to_create_room(CreatorJID, MUCServiceJID, Config) of + {ok, RoomJID, #create{aff_users = AffUsers}} -> + {ok, make_room(RoomJID, RoomTitle, Subject, AffUsers)}; + {error, exists} -> + {exist, "Room already exists"}; + {error, max_occupants_reached} -> + {max_occupants_reached, "Max occupants number reached"}; + {error, bad_request} -> + {bad_request, "Bad request"}; + {error, {Key, Reason}} -> + {validation_error, io_lib:format("Validation failed for key: ~p with reason ~p", + [Key, Reason])} + end. + + +-spec invite_to_room(binary(), binary(), jid:jid(), jid:jid()) -> invite_to_room_result(). +invite_to_room(Domain, RoomName, SenderJID, RecipientJID) -> + % FIXME use id instead of roomname because room name is not unique + RecipientBin = jid:to_binary(jid:to_bare(RecipientJID)), + case muc_light_room_name_to_jid_and_aff(SenderJID, RoomName, Domain) of + {ok, R, _Aff} -> + S = jid:to_bare(SenderJID), + Changes = query(?NS_MUC_LIGHT_AFFILIATIONS, + [affiliate(RecipientBin, <<"member">>)]), + ejabberd_router:route(S, R, iq(jid:to_binary(S), jid:to_binary(R), + <<"set">>, [Changes])), + {ok, "User invited successfully"}; + {error, given_user_does_not_occupy_any_room} -> + {forbidden, "Given user does not occupy any room"}; + {error, not_found} -> + {not_found, "Room does not exist"} + end. + +-spec change_room_config(binary(), binary(), binary(), jid:jid(), binary()) -> + change_room_config_result(). +change_room_config(Domain, RoomID, RoomName, UserJID, Subject) -> + LServer = jid:nameprep(Domain), + HostType = mod_muc_light_utils:server_host_to_host_type(LServer), + MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), + UserUS = jid:to_bare(UserJID), + ConfigReq = #config{ raw_config = + [{<<"roomname">>, RoomName}, {<<"subject">>, Subject}]}, + Acc = mongoose_acc:new(#{location => ?LOCATION, lserver => LServer, host_type => HostType}), + case mod_muc_light:change_room_config(UserUS, RoomID, MUCLightDomain, ConfigReq, Acc) of + {ok, RoomJID, _} -> + {ok, make_room(RoomJID, RoomName, Subject, [])}; + {error, item_not_found} -> + {wrong_user, "The given user is not room participant"}; + {error, not_allowed} -> + {not_allowed, "The given user has not permission to change config"}; + {error, {error, {Key, Reason}}} -> + {validation_error, io_lib:format("Validation failed for key: ~p with reason ~p", + [Key, Reason])}; + {error, bad_request} -> + {bad_request, "Bad request"} + end. + +-spec change_affiliation(binary(), binary(), jid:jid(), jid:jid(), binary()) -> ok. +change_affiliation(Domain, RoomID, SenderJID, RecipientJID, Affiliation) -> + RecipientJID2 = jid:to_bare(RecipientJID), + LServer = jid:nameprep(Domain), + HostType = mod_muc_light_utils:server_host_to_host_type(LServer), + MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), + R = jid:make(RoomID, MUCLightDomain, <<>>), + S = jid:to_bare(SenderJID), + Changes = query(?NS_MUC_LIGHT_AFFILIATIONS, + [affiliate(jid:to_binary(RecipientJID2), Affiliation)]), + ejabberd_router:route(S, R, iq(jid:to_binary(S), jid:to_binary(R), + <<"set">>, [Changes])), + ok. + +-spec send_message(binary(), binary(), jid:jid(), binary()) -> {ok | wrong_user, iodata()}. +send_message(Domain, RoomName, SenderJID, Message) -> + Body = #xmlel{name = <<"body">>, + children = [ #xmlcdata{ content = Message } ] + }, + Stanza = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = [ Body ] + }, + S = jid:to_bare(SenderJID), + case get_user_rooms(jid:to_lus(S), Domain) of + [] -> + {wrong_user, "given user does not occupy any room"}; + RoomJIDs when is_list(RoomJIDs) -> + FindFun = find_room_and_user_aff_by_room_name(RoomName, jid:to_lus(S)), + {ok, {RU, RS}, _Aff} = lists:foldl(FindFun, none, RoomJIDs), + true = is_subdomain(RS, Domain), + R = jid:make(RU, RS, <<>>), + ejabberd_router:route(S, R, Stanza), + {ok, "Message send successfully"} + end. + +-spec delete_room(binary(), binary(), jid:jid()) -> + { ok | not_exists | not_allowed | user_without_room, iodata()}. +delete_room(DomainName, RoomName, OwnerJID) -> + %% FIXME use id instead of name + OwnerJID2 = jid:to_bare(OwnerJID), + Res = case muc_light_room_name_to_jid_and_aff(OwnerJID2, RoomName, DomainName) of + {ok, RoomJID, owner} -> + mod_muc_light:delete_room(jid:to_lus(RoomJID)); + {ok, _, _} -> + {error, not_allowed}; + {error, _} = Err -> + Err + end, + format_delete_error_message(Res). + +-spec delete_room(binary(), binary()) -> { ok | not_exists, iodata()}. +delete_room(Domain, RoomID) -> + LServer = jid:nameprep(Domain), + HostType = mod_muc_light_utils:server_host_to_host_type(LServer), + MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), + Res = mod_muc_light:delete_room({RoomID, MUCLightDomain}), + format_delete_error_message(Res). + + +-spec delete_room(jid:jid()) -> { ok | not_exists, iodata()}. +delete_room(RoomJID) -> + Res = mod_muc_light:delete_room(jid:to_lus(RoomJID)), + format_delete_error_message(Res). + + %% Internal + +make_room(JID, Name, Subject, AffUsers) -> + #{jid => JID, name => Name, subject => Subject, aff_users => AffUsers}. + +format_delete_error_message(ok) -> + {ok, "Room deleted successfully!"}; +format_delete_error_message({error, not_allowed}) -> + {not_allowed, "You cannot delete this room"}; +format_delete_error_message({error, not_exists}) -> + {not_exists, "Cannot remove not exisiting room"}; +format_delete_error_message({error, not_found}) -> + {not_exists, "Cannot remove not exisiting room"}; +format_delete_error_message({error, given_user_does_not_occupy_any_room}) -> + {user_without_room, "Given user does not occupy this room"}. + +iq(To, From, Type, Children) -> + UUID = uuid:uuid_to_string(uuid:get_v4(), binary_standard), + #xmlel{name = <<"iq">>, + attrs = [{<<"from">>, From}, + {<<"to">>, To}, + {<<"type">>, Type}, + {<<"id">>, UUID}], + children = Children + }. + +query(NS, Children) when is_binary(NS), is_list(Children) -> + #xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, NS}], + children = Children + }. + +affiliate(JID, Kind) when is_binary(JID), is_binary(Kind) -> + #xmlel{name = <<"user">>, + attrs = [{<<"affiliation">>, Kind}], + children = [ #xmlcdata{ content = JID } ] + }. + + +-spec make_room_config(binary(), binary()) -> create_req_props(). +make_room_config(Name, Subject) -> + #create{raw_config = [{<<"roomname">>, Name}, + {<<"subject">>, Subject}] + }. + +-spec muc_light_room_name_to_jid_and_aff(UserJID :: jid:jid(), + RoomName :: binary(), + Domain :: jid:lserver()) -> + {ok, jid:jid(), aff()} | {error, given_user_does_not_occupy_any_room} | {error, not_found}. +muc_light_room_name_to_jid_and_aff(UserJID, RoomName, Domain) -> + UserUS = jid:to_lus(UserJID), + case get_user_rooms(UserUS, Domain) of + [] -> + {error, given_user_does_not_occupy_any_room}; + RoomUSs when is_list(RoomUSs) -> + FindFun = find_room_and_user_aff_by_room_name(RoomName, UserUS), + case lists:foldl(FindFun, none, RoomUSs) of + {ok, {RU, RS}, UserAff} -> + true = is_subdomain(RS, Domain), + {ok, jid:make(RU, RS, <<>>), UserAff}; + none -> + {error, not_found} + end + end. + +-spec get_user_rooms(UserUS :: jid:simple_bare_jid(), Domain :: jid:lserver()) -> + [jid:simple_bare_jid()]. +get_user_rooms({_, UserS} = UserUS, Domain) -> + HostType = mod_muc_light_utils:server_host_to_host_type(UserS), + mod_muc_light_db_backend:get_user_rooms(HostType, UserUS, Domain). + +-spec get_room_name_and_user_aff(RoomUS :: jid:simple_bare_jid(), + UserUS :: jid:simple_bare_jid()) -> + {ok, RoomName :: binary(), UserAff :: aff()} | {error, not_exists}. +get_room_name_and_user_aff(RoomUS, {_, UserS} = UserUS) -> + HostType = mod_muc_light_utils:server_host_to_host_type(UserS), + case mod_muc_light_db_backend:get_info(HostType, RoomUS) of + {ok, Cfg, Affs, _} -> + {roomname, RoomName} = lists:keyfind(roomname, 1, Cfg), + {_, UserAff} = lists:keyfind(UserUS, 1, Affs), + {ok, RoomName, UserAff}; + Error -> + Error + end. + +-type find_room_acc() :: {ok, RoomUS :: jid:simple_bare_jid(), UserAff :: aff()} | none. + +-spec find_room_and_user_aff_by_room_name(RoomName :: binary(), + UserUS :: jid:simple_bare_jid()) -> + fun((RoomUS :: jid:simple_bare_jid(), find_room_acc()) -> find_room_acc()). +find_room_and_user_aff_by_room_name(RoomName, UserUS) -> + fun (RoomUS, none) -> + case get_room_name_and_user_aff(RoomUS, UserUS) of + {ok, RoomName, UserAff} -> + {ok, RoomUS, UserAff}; + _ -> + none + end; + (_, Acc) when Acc =/= none -> + Acc + end. + +is_subdomain(Child, Parent) -> + %% Example input Child = <<"muclight.localhost">> and Parent = + %% <<"localhost">> + case binary:match(Child, Parent) of + nomatch -> false; + {_, _} -> true + end. diff --git a/src/muc_light/mod_muc_light_commands.erl b/src/muc_light/mod_muc_light_commands.erl index b7f831f8ef..01dc3a4022 100644 --- a/src/muc_light/mod_muc_light_commands.erl +++ b/src/muc_light/mod_muc_light_commands.erl @@ -165,194 +165,49 @@ commands() -> %%-------------------------------------------------------------------- create_unique_room(Domain, RoomName, Creator, Subject) -> - create_room(Domain, <<>>, RoomName, Creator, Subject). + CreatorJID = jid:from_binary(Creator), + case mod_muc_light_api:create_room(Domain, <<>>, RoomName, CreatorJID, Subject) of + {ok, #{jid := JID}} -> jid:to_binary(JID); + Error -> format_response(Error) + end. create_identifiable_room(Domain, Identifier, RoomName, Creator, Subject) -> - create_room(Domain, Identifier, RoomName, Creator, Subject). - -invite_to_room(Domain, RoomName, Sender, Recipient0) -> - Recipient1 = jid:binary_to_bare(Recipient0), - case muc_light_room_name_to_jid_and_aff(jid:from_binary(Sender), RoomName, Domain) of - {ok, R, _Aff} -> - S = jid:binary_to_bare(Sender), - Changes = query(?NS_MUC_LIGHT_AFFILIATIONS, - [affiliate(jid:to_binary(Recipient1), <<"member">>)]), - ejabberd_router:route(S, R, iq(jid:to_binary(S), jid:to_binary(R), - <<"set">>, [Changes])); - {error, given_user_does_not_occupy_any_room} -> - {error, forbidden, "given user does not occupy any room"}; - {error, not_found} -> - {error, not_found, "room does not exist"} + CreatorJID = jid:from_binary(Creator), + case mod_muc_light_api:create_room(Domain, Identifier, RoomName, CreatorJID, Subject) of + {ok, #{jid := JID}} -> jid:to_binary(JID); + Error -> format_response(Error) end. -change_affiliation(Domain, RoomID, Sender, Recipient0, Affiliation) -> - Recipient1 = jid:binary_to_bare(Recipient0), - LServer = jid:nameprep(Domain), - HostType = mod_muc_light_utils:server_host_to_host_type(LServer), - MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), - R = jid:make(RoomID, MUCLightDomain, <<>>), - S = jid:binary_to_bare(Sender), - Changes = query(?NS_MUC_LIGHT_AFFILIATIONS, - [affiliate(jid:to_binary(Recipient1), Affiliation)]), - ejabberd_router:route(S, R, iq(jid:to_binary(S), jid:to_binary(R), - <<"set">>, [Changes])). +invite_to_room(Domain, RoomName, Sender, Recipient) -> + SenderJID = jid:from_binary(Sender), + RecipientJID = jid:from_binary(Recipient), + Result = mod_muc_light_api:invite_to_room(Domain, RoomName, SenderJID, RecipientJID), + format_response(Result). + +change_affiliation(Domain, RoomID, Sender, Recipient, Affiliation) -> + SenderJID = jid:from_binary(Sender), + RecipientJID = jid:from_binary(Recipient), + mod_muc_light_api:change_affiliation(Domain, RoomID, SenderJID, RecipientJID, Affiliation). change_room_config(Domain, RoomID, RoomName, User, Subject) -> - LServer = jid:nameprep(Domain), - HostType = mod_muc_light_utils:server_host_to_host_type(LServer), - MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), - UserUS = jid:binary_to_bare(User), - ConfigReq = #config{ raw_config = - [{<<"roomname">>, RoomName}, {<<"subject">>, Subject}]}, - Acc = mongoose_acc:new(#{location => ?LOCATION, lserver => LServer, host_type => HostType}), - case mod_muc_light:change_room_config(UserUS, RoomID, MUCLightDomain, ConfigReq, Acc) of - {ok, _RoomJID, _} -> - ok; - {error, Reason} -> - {error, internal, Reason} - end. + UserJID = jid:from_binary(User), + Result = mod_muc_light_api:change_room_config(Domain, RoomID, RoomName, UserJID, Subject), + format_response(Result). send_message(Domain, RoomName, Sender, Message) -> - Body = #xmlel{name = <<"body">>, - children = [ #xmlcdata{ content = Message } ] - }, - Stanza = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = [ Body ] - }, - S = jid:binary_to_bare(Sender), - case get_user_rooms(jid:to_lus(S), Domain) of - [] -> - {error, denied, "given user does not occupy any room"}; - RoomJIDs when is_list(RoomJIDs) -> - FindFun = find_room_and_user_aff_by_room_name(RoomName, jid:to_lus(S)), - {ok, {RU, RS}, _Aff} = lists:foldl(FindFun, none, RoomJIDs), - true = is_subdomain(RS, Domain), - R = jid:make(RU, RS, <<>>), - ejabberd_router:route(S, R, Stanza) - end. + SenderJID = jid:from_binary(Sender), + mod_muc_light_api:send_message(Domain, RoomName, SenderJID, Message). --spec delete_room(DomainName :: binary(), RoomName :: binary(), - Owner :: binary()) -> - ok | {error, atom(), term()}. +-spec delete_room(binary(), binary(), binary()) -> ok | {error, atom(), term()}. delete_room(DomainName, RoomName, Owner) -> - OwnerJID = jid:binary_to_bare(Owner), - case muc_light_room_name_to_jid_and_aff(OwnerJID, RoomName, DomainName) of - {ok, RoomJID, owner} -> mod_muc_light:delete_room(jid:to_lus(RoomJID)); - {ok, _, _} -> {error, denied, "you can not delete this room"}; - {error, given_user_does_not_occupy_any_room} -> {error, denied, "given user does not occupy this room"}; - {error, not_found} -> {error, not_found, "room does not exist"} - end. - -%%-------------------------------------------------------------------- -%% Ancillary -%%-------------------------------------------------------------------- - -create_room(Domain, RoomId, RoomTitle, Creator, Subject) -> - LServer = jid:nameprep(Domain), - HostType = mod_muc_light_utils:server_host_to_host_type(LServer), - MUCLightDomain = mod_muc_light_utils:server_host_to_muc_host(HostType, LServer), - CreatorJid = jid:from_binary(Creator), - MUCServiceJID = jid:make(RoomId, MUCLightDomain, <<>>), - Config = make_room_config(RoomTitle, Subject), - case mod_muc_light:try_to_create_room(CreatorJid, MUCServiceJID, Config) of - {ok, RoomJid, _} -> - jid:to_binary(RoomJid); - {error, exists} -> - {error, denied, "Room already exists"}; - {error, Reason} -> - {error, internal, Reason} - end. - -make_room_config(Name, Subject) -> - #create{raw_config = [{<<"roomname">>, Name}, - {<<"subject">>, Subject}] - }. - --spec muc_light_room_name_to_jid_and_aff(UserJID :: jid:jid(), - RoomName :: binary(), - Domain :: jid:lserver()) -> - {ok, jid:jid(), aff()} | {error, given_user_does_not_occupy_any_room} | {error, not_found}. -muc_light_room_name_to_jid_and_aff(UserJID, RoomName, Domain) -> - UserUS = jid:to_lus(UserJID), - case get_user_rooms(UserUS, Domain) of - [] -> - {error, given_user_does_not_occupy_any_room}; - RoomUSs when is_list(RoomUSs) -> - FindFun = find_room_and_user_aff_by_room_name(RoomName, UserUS), - case lists:foldl(FindFun, none, RoomUSs) of - {ok, {RU, RS}, UserAff} -> - true = is_subdomain(RS, Domain), - {ok, jid:make(RU, RS, <<>>), UserAff}; - none -> - {error, not_found} - end - end. - --spec get_user_rooms(UserUS :: jid:simple_bare_jid(), Domain :: jid:lserver()) -> - [jid:simple_bare_jid()]. -get_user_rooms({_, UserS} = UserUS, Domain) -> - HostType = mod_muc_light_utils:server_host_to_host_type(UserS), - mod_muc_light_db_backend:get_user_rooms(HostType, UserUS, Domain). - --spec get_room_name_and_user_aff(RoomUS :: jid:simple_bare_jid(), - UserUS :: jid:simple_bare_jid()) -> - {ok, RoomName :: binary(), UserAff :: aff()} | {error, not_exists}. -get_room_name_and_user_aff(RoomUS, {_, UserS} = UserUS) -> - HostType = mod_muc_light_utils:server_host_to_host_type(UserS), - case mod_muc_light_db_backend:get_info(HostType, RoomUS) of - {ok, Cfg, Affs, _} -> - {roomname, RoomName} = lists:keyfind(roomname, 1, Cfg), - {_, UserAff} = lists:keyfind(UserUS, 1, Affs), - {ok, RoomName, UserAff}; - Error -> - Error - end. - --type find_room_acc() :: {ok, RoomUS :: jid:simple_bare_jid(), UserAff :: aff()} | none. - --spec find_room_and_user_aff_by_room_name(RoomName :: binary(), - UserUS :: jid:simple_bare_jid()) -> - fun((RoomUS :: jid:simple_bare_jid(), find_room_acc()) -> find_room_acc()). -find_room_and_user_aff_by_room_name(RoomName, UserUS) -> - fun (RoomUS, none) -> - case get_room_name_and_user_aff(RoomUS, UserUS) of - {ok, RoomName, UserAff} -> - {ok, RoomUS, UserAff}; - _ -> - none - end; - (_, Acc) when Acc =/= none -> - Acc - end. - -is_subdomain(Child, Parent) -> - %% Example input Child = <<"muclight.localhost">> and Parent = - %% <<"localhost">> - case binary:match(Child, Parent) of - nomatch -> false; - {_, _} -> true - end. - -iq(To, From, Type, Children) -> - UUID = uuid:uuid_to_string(uuid:get_v4(), binary_standard), - #xmlel{name = <<"iq">>, - attrs = [{<<"from">>, From}, - {<<"to">>, To}, - {<<"type">>, Type}, - {<<"id">>, UUID}], - children = Children - }. - -query(NS, Children) when is_binary(NS), is_list(Children) -> - #xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, NS}], - children = Children - }. - -affiliate(JID, Kind) when is_binary(JID), is_binary(Kind) -> - #xmlel{name = <<"user">>, - attrs = [{<<"affiliation">>, Kind}], - children = [ #xmlcdata{ content = JID } ] - }. - + OwnerJID = jid:from_binary(Owner), + Result = mod_muc_light_api:delete_room(DomainName, RoomName, OwnerJID), + format_response(Result). + +format_response({ok, Msg}) -> Msg; +format_response({not_exists, Msg}) -> {error, not_found, Msg}; +format_response({ResStatus, Msg}) when exist =:= ResStatus; + not_allowed =:= ResStatus; + user_without_room =:= ResStatus -> + {error, denied, Msg}; +format_response({_, Reason}) -> {error, internal, Reason}.