Skip to content

Commit

Permalink
Merge pull request #3538 from esl/graphql/muc-light-api
Browse files Browse the repository at this point in the history
GraphQL - Implement MUC Light rooms API for admin
  • Loading branch information
chrzaszcz committed Mar 2, 2022
2 parents a957367 + 198cc90 commit 5dd5d59
Show file tree
Hide file tree
Showing 24 changed files with 1,192 additions and 277 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
{suites, "tests", graphql_SUITE}.
{suites, "tests", graphql_account_SUITE}.
{suites, "tests", graphql_domain_SUITE}.
{suites, "tests", graphql_muc_light_SUITE}.
{suites, "tests", graphql_session_SUITE}.
{suites, "tests", graphql_stanza_SUITE}.
{suites, "tests", inbox_SUITE}.
Expand Down
4 changes: 2 additions & 2 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@

{suites, "tests", graphql_SUITE}.
{suites, "tests", graphql_account_SUITE}.
{suites, "tests", graphql_domain_SUITE}.
{suites, "tests", graphql_muc_light_SUITE}.
{suites, "tests", graphql_session_SUITE}.
{suites, "tests", graphql_stanza_SUITE}.

{suites, "tests", graphql_domain_SUITE}.

{suites, "tests", inbox_SUITE}.

{suites, "tests", inbox_extensions_SUITE}.
Expand Down
508 changes: 508 additions & 0 deletions big_tests/tests/graphql_muc_light_SUITE.erl

Large diffs are not rendered by default.

67 changes: 42 additions & 25 deletions big_tests/tests/muc_light_http_api_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ negative_response() ->
[delete_room_by_non_owner,
delete_non_existent_room,
delete_room_without_having_a_membership,
create_non_unique_room
create_non_unique_room,
create_room_on_non_existing_muc_server
].

%%--------------------------------------------------------------------
Expand Down Expand Up @@ -92,22 +93,23 @@ end_per_testcase(CaseName, Config) ->

create_unique_room(Config) ->
escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
Path = path([domain()]),
MUCLightDomain = muc_light_domain(),
Path = path([MUCLightDomain]),
Name = <<"wonderland">>,
Body = #{ name => Name,
owner => escalus_client:short_jid(Alice),
subject => <<"Lewis Carol">>
},
{{<<"201">>, _}, _} = rest_helper:post(admin, Path, Body),
[Item] = get_disco_rooms(Alice),
MUCLightDomain = muc_light_domain(),
true = is_room_name(Name, Item),
true = is_room_domain(MUCLightDomain, Item)
end).

create_identifiable_room(Config) ->
escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
Path = path([domain()]),
MUCLightDomain = muc_light_domain(),
Path = path([MUCLightDomain]),
RandBits = base16:encode(crypto:strong_rand_bytes(5)),
Name = <<"wonderland">>,
RoomID = <<"just_some_id_", RandBits/binary>>,
Expand All @@ -120,20 +122,19 @@ create_identifiable_room(Config) ->
{{<<"201">>, _}, RoomJID} = rest_helper:putt(admin, Path, Body),
[Item] = get_disco_rooms(Alice),
[RoomIDescaped, MUCLightDomain] = binary:split(RoomJID, <<"@">>),
MUCLightDomain = muc_light_domain(),
true = is_room_name(Name, Item),
true = is_room_domain(MUCLightDomain, Item),
true = is_room_id(RoomIDescaped, Item)
end).

invite_to_room(Config) ->
Name = <<"wonderland">>,
Path = path([muc_light_domain(), Name, "participants"]),
escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate) ->
RoomID = atom_to_binary(?FUNCTION_NAME),
Path = path([muc_light_domain(), RoomID, "participants"]),
%% XMPP: Alice creates a room.
Stt = stanza_create_room(undefined,
[{<<"roomname">>, Name}], [{Kate, member}]),
Stt = stanza_create_room(RoomID,
[{<<"roomname">>, <<"wonderland">>}], [{Kate, member}]),
escalus:send(Alice, Stt),
%% XMPP: Alice recieves a affiliation message to herself and
%% an IQ result when creating the MUC Light room.
Expand All @@ -154,15 +155,15 @@ invite_to_room(Config) ->
end).

send_message_to_room(Config) ->
Name = <<"wonderland">>,
Path = path([muc_light_domain(), Name, "messages"]),
RoomID = atom_to_binary(?FUNCTION_NAME),
Path = path([muc_light_domain(), RoomID, "messages"]),
Text = <<"Hello everyone!">>,
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate) ->
%% XMPP: Alice creates a room.
escalus:send(Alice, stanza_create_room(undefined,
[{<<"roomname">>, Name}], [{Bob, member}, {Kate, member}])),
escalus:send(Alice, stanza_create_room(RoomID,
[{<<"roomname">>, <<"wonderland">>}], [{Bob, member}, {Kate, member}])),
%% XMPP: Alice gets her own affiliation info
escalus:wait_for_stanza(Alice),
%% XMPP: And Alice gets IQ result
Expand All @@ -180,50 +181,55 @@ send_message_to_room(Config) ->
end).

delete_room_by_owner(Config) ->
RoomID = atom_to_binary(?FUNCTION_NAME),
RoomName = <<"wonderland">>,
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"204">>, <<"No Content">>}, <<"">>} =
check_delete_room(Config, RoomName, RoomName,
check_delete_room(Config, RoomName, RoomID, RoomID,
Alice, [Bob, Kate], Alice)
end).

delete_room_by_non_owner(Config) ->
RoomID = atom_to_binary(?FUNCTION_NAME),
RoomName = <<"wonderland">>,
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"403">>, <<"Forbidden">>},
<<"you can not delete this room">>} =
check_delete_room(Config, RoomName, RoomName,
<<"Given user cannot delete this room">>} =
check_delete_room(Config, RoomName, RoomID, RoomID,
Alice, [Bob, Kate], Bob)
end).

delete_non_existent_room(Config) ->
RoomID = atom_to_binary(?FUNCTION_NAME),
RoomName = <<"wonderland">>,
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"404">>, _}, <<"room does not exist">>} =
check_delete_room(Config, RoomName, <<"some_non_existent_room">>,
{{<<"404">>, _}, <<"Cannot remove not existing room">>} =
check_delete_room(Config, RoomName, RoomID,
<<"some_non_existent_room">>,
Alice, [Bob, Kate], Alice)
end).

delete_room_without_having_a_membership(Config) ->
RoomID = atom_to_binary(?FUNCTION_NAME),
RoomName = <<"wonderland">>,
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"403">>, _}, <<"given user does not occupy this room">>} =
check_delete_room(Config, RoomName, RoomName,
{{<<"403">>, _}, <<"Given user does not occupy this room">>} =
check_delete_room(Config, RoomName, RoomID, RoomID,
Alice, [Bob], Kate)
end).


create_non_unique_room(Config) ->
escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
Path = path([domain()]),
Path = path([muc_light_domain()]),
RandBits = base16:encode(crypto:strong_rand_bytes(5)),
Name = <<"wonderland">>,
RoomID = <<"just_some_id_", RandBits/binary>>,
Expand All @@ -237,6 +243,17 @@ create_non_unique_room(Config) ->
ok
end).

create_room_on_non_existing_muc_server(Config) ->
escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
Path = path([domain_helper:domain()]),
Name = <<"wonderland">>,
Body = #{ name => Name,
owner => escalus_client:short_jid(Alice),
subject => <<"Lewis Carol">>
},
{{<<"404">>,<<"Not Found">>}, _} = rest_helper:post(admin, Path, Body)
end).

%%--------------------------------------------------------------------
%% Ancillary (borrowed and adapted from the MUC and MUC Light suites)
%%--------------------------------------------------------------------
Expand Down Expand Up @@ -276,11 +293,11 @@ member_is_affiliated(Stanza, User) ->
Data = exml_query:path(Stanza, [{element, <<"x">>}, {element, <<"user">>}, cdata]),
MemberJID == Data.

check_delete_room(_Config, RoomNameToCreate, RoomNameToDelete, RoomOwner,
check_delete_room(_Config, RoomName, RoomIDToCreate, RoomIDToDelete, RoomOwner,
RoomMembers, UserToExecuteDelete) ->
Members = [{Member, member} || Member <- RoomMembers],
escalus:send(RoomOwner, stanza_create_room(undefined,
[{<<"roomname">>, RoomNameToCreate}],
escalus:send(RoomOwner, stanza_create_room(RoomIDToCreate,
[{<<"roomname">>, RoomName}],
Members)),
%% XMPP RoomOwner gets affiliation and IQ result
Affiliations = [{RoomOwner, owner} | Members],
Expand All @@ -290,7 +307,7 @@ check_delete_room(_Config, RoomNameToCreate, RoomNameToDelete, RoomOwner,
escalus:assert(is_iq_result, CreationResult),
muc_light_helper:verify_aff_bcast(Members, Affiliations),
ShortJID = escalus_client:short_jid(UserToExecuteDelete),
Path = path([muc_light_domain(), RoomNameToDelete, ShortJID, "management"]),
Path = path([muc_light_domain(), RoomIDToDelete, ShortJID, "management"]),
rest_helper:delete(admin, Path).


Expand Down
15 changes: 7 additions & 8 deletions big_tests/tests/rest_client_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,13 @@ all() ->
{group, security}].

groups() ->
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).
[{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()}].

message_test_cases() ->
[msg_is_sent_and_delivered_over_xmpp,
Expand Down
2 changes: 1 addition & 1 deletion doc/rest-api/Administration-backend_swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ paths:
responses:
204:
description: "The operation was successful."
/muc-lights/{XMPPHost}:
/muc-lights/{XMPPMUCHost}:
parameters:
- $ref: '#/parameters/hostName'
post:
Expand Down
4 changes: 4 additions & 0 deletions priv/graphql/schemas/admin/admin_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type AdminQuery{
session: SessionAdminQuery
"Stanza management"
stanza: StanzaAdminQuery
"MUC Light room management"
muc_light: MUCLightAdminQuery
}

"""
Expand All @@ -33,4 +35,6 @@ type AdminMutation @protected{
session: SessionAdminMutation
"Stanza management"
stanza: StanzaAdminMutation
"MUC Light room management"
muc_light: MUCLightAdminMutation
}
43 changes: 43 additions & 0 deletions priv/graphql/schemas/admin/muc_light.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Allow admin to manage Multi-User Chat Light rooms.
"""
type MUCLightAdminMutation @protected{
"Create a MUC light room under the given XMPP hostname"
createRoom(mucDomain: String!, name: String!, owner: JID!, subject: String!, id: String): Room
"Change configuration of a MUC Light room"
changeRoomConfiguration(room: JID!, owner: JID!, name: String!, subject: String!): Room
"Invite a user to a MUC Light room"
inviteUser(room: JID!, sender: JID!, recipient: JID!): String
"Remove a MUC Light room"
deleteRoom(room: JID!): String
"Kick a user from a MUC Light room"
kickUser(room: JID!, user: JID!): String
"Send a message to a MUC Light room"
sendMessageToRoom(room: JID!, from: JID!, body: String!): String
}

"""
Allow admin to get information about Multi-User Chat Light rooms.
"""
type MUCLightAdminQuery @protected{
"Get the MUC Light room archived messages"
getRoomMessages(room: JID!, pageSize: Int!, before: DateTime): StanzasPayload
"Get configuration of the MUC Light room"
getRoomConfig(room: JID!): Room
"Get users list of given MUC Light room"
listRoomUsers(room: JID!): [RoomUser!]
"Get the list of MUC Light rooms that the user participates in"
listUserRooms(user: JID!): [JID!]
}

type Room{
jid: JID!
name: String!
subject: String!
participants: [RoomUser!]!
}

type RoomUser{
jid: JID!
affiliation: Affiliation!
}
5 changes: 5 additions & 0 deletions priv/graphql/schemas/global/muc.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
enum Affiliation{
OWNER
MEMBER
NONE
}
2 changes: 2 additions & 0 deletions src/graphql/admin/mongoose_graphql_admin_mutation.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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">>, _) ->
Expand Down
2 changes: 2 additions & 0 deletions src/graphql/admin/mongoose_graphql_admin_query.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
67 changes: 67 additions & 0 deletions src/graphql/admin/mongoose_graphql_muc_light_admin_mutation.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
-module(mongoose_graphql_muc_light_admin_mutation).

-export([execute/4]).

-ignore_xref([execute/4]).

-include("../mongoose_graphql_types.hrl").

-import(mongoose_graphql_helper, [make_error/2, format_result/2]).
-import(mongoose_graphql_muc_light_helper, [make_room/1, make_ok_user/1]).

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).

-spec create_room(map()) -> {ok, map()} | {error, resolver_error()}.
create_room(#{<<"id">> := null} = Args) ->
create_room(Args#{<<"id">> => <<>>});
create_room(#{<<"id">> := RoomID, <<"mucDomain">> := MUCDomain, <<"name">> := RoomName,
<<"owner">> := CreatorJID, <<"subject">> := Subject}) ->
case mod_muc_light_api:create_room(MUCDomain, RoomID, RoomName, CreatorJID, Subject) of
{ok, Room} ->
{ok, make_room(Room)};
Err ->
make_error(Err, #{mucDomain => MUCDomain, id => RoomID, creator => CreatorJID})
end.

-spec change_room_config(map()) -> {ok, map()} | {error, resolver_error()}.
change_room_config(#{<<"room">> := RoomJID, <<"name">> := RoomName,
<<"owner">> := OwnerJID, <<"subject">> := Subject}) ->
case mod_muc_light_api:change_room_config(RoomJID, OwnerJID, RoomName, Subject) of
{ok, Room} ->
{ok, make_room(Room)};
Err ->
make_error(Err, #{room => jid:to_binary(RoomJID), owner => jid:to_binary(OwnerJID)})
end.

-spec delete_room(map()) -> {ok, binary()} | {error, resolver_error()}.
delete_room(#{<<"room">> := RoomJID}) ->
Result = mod_muc_light_api:delete_room(RoomJID),
format_result(Result, #{room => jid:to_binary(RoomJID)}).

-spec invite_user(map()) -> {ok, binary()} | {error, resolver_error()}.
invite_user(#{<<"room">> := RoomJID, <<"sender">> := SenderJID,
<<"recipient">> := RecipientJID}) ->
Result = mod_muc_light_api:invite_to_room(RoomJID, SenderJID, RecipientJID),
format_result(Result, #{room => jid:to_binary(RoomJID), sender => jid:to_binary(SenderJID),
recipient => jid:to_binary(RecipientJID)}).

-spec kick_user(map()) -> {ok, binary()} | {error, resolver_error()}.
kick_user(#{<<"room">> := RoomJID, <<"user">> := UserJID}) ->
Result = mod_muc_light_api:remove_user_from_room(RoomJID, UserJID, UserJID),
format_result(Result, #{user => UserJID}).

-spec send_msg_to_room(map()) -> {ok, binary()} | {error, resolver_error()}.
send_msg_to_room(#{<<"room">> := RoomJID, <<"from">> := FromJID, <<"body">> := Message}) ->
Result = mod_muc_light_api:send_message(RoomJID, FromJID, Message),
format_result(Result, #{room => jid:to_binary(RoomJID), from => jid:to_binary(FromJID)}).
Loading

0 comments on commit 5dd5d59

Please sign in to comment.