Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL - Implement MUC Light rooms API for admin #3538

Merged
merged 10 commits into from
Mar 2, 2022
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">> => <<>>});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to accept empty binary in the GraphQL query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty string, in this case, means that id is not present and should be generated. It was done this way in the old commands module. I can return an error when id is empty to eliminate the situation that someone passes an empty string by the accident. Should I do it this way?

Copy link
Member

@chrzaszcz chrzaszcz Mar 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the point - the logic for "" is a side-effect of the way it is coded. I think it can stay as it is for now. I think that the policy for empty binaries should be similar as e.g. for empty binary for user name, jid etc.

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