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
484 changes: 484 additions & 0 deletions big_tests/tests/graphql_muc_light_SUITE.erl

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions big_tests/tests/muc_light_http_api_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ create_identifiable_room(Config) ->

invite_to_room(Config) ->
Name = <<"wonderland">>,
Path = path([muc_light_domain(), Name, "participants"]),
Path = path([domain_helper:domain(), Name, "participants"]),
escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate) ->
%% XMPP: Alice creates a room.
Expand All @@ -155,7 +155,7 @@ invite_to_room(Config) ->

send_message_to_room(Config) ->
Name = <<"wonderland">>,
Path = path([muc_light_domain(), Name, "messages"]),
Path = path([domain_helper:domain(), Name, "messages"]),
Text = <<"Hello everyone!">>,
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
Expand Down Expand Up @@ -195,7 +195,7 @@ delete_room_by_non_owner(Config) ->
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"403">>, <<"Forbidden">>},
<<"you can not delete this room">>} =
<<"You cannot delete this room">>} =
check_delete_room(Config, RoomName, RoomName,
Alice, [Bob, Kate], Bob)
end).
Expand All @@ -205,7 +205,7 @@ delete_non_existent_room(Config) ->
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"404">>, _}, <<"room does not exist">>} =
{{<<"404">>, _}, <<"Cannot remove not existing room">>} =
check_delete_room(Config, RoomName, <<"some_non_existent_room">>,
Alice, [Bob, Kate], Alice)
end).
Expand All @@ -215,7 +215,7 @@ delete_room_without_having_a_membership(Config) ->
escalus:fresh_story(Config,
[{alice, 1}, {bob, 1}, {kate, 1}],
fun(Alice, Bob, Kate)->
{{<<"403">>, _}, <<"given user does not occupy this room">>} =
{{<<"403">>, _}, <<"Given user does not occupy any room">>} =
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
check_delete_room(Config, RoomName, RoomName,
Alice, [Bob], Kate)
end).
Expand Down Expand Up @@ -290,7 +290,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([domain_helper:domain(), RoomNameToDelete, ShortJID, "management"]),
rest_helper:delete(admin, Path).


Expand Down
6 changes: 3 additions & 3 deletions doc/rest-api/Administration-backend_swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ paths:
schema:
title: roomJID
type: string
/muc-lights/{XMPPMUCHost}/{roomName}/participants:
/muc-lights/{XMPPHost}/{roomName}/participants:
parameters:
- $ref: '#/parameters/MUCServer'
- $ref: '#/parameters/roomName'
Expand Down Expand Up @@ -504,7 +504,7 @@ paths:
responses:
204:
description: An invite was sent out
/muc-lights/{XMPPMUCHost}/{roomName}/messages:
/muc-lights/{XMPPHost}/{roomName}/messages:
parameters:
- $ref: '#/parameters/MUCServer'
- $ref: '#/parameters/roomName'
Expand Down Expand Up @@ -536,7 +536,7 @@ paths:
responses:
204:
description: Message was sent to the MUC Light room
/muc-lights/{XMPPMUCHost}/{roomName}/{user}/management:
/muc-lights/{XMPPHost}/{roomName}/{user}/management:
parameters:
- $ref: '#/parameters/MUCServer'
- $ref: '#/parameters/roomName'
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
}
48 changes: 48 additions & 0 deletions priv/graphql/schemas/admin/muc_light.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
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
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
"Change configuration of a 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
"Kick a user from a MUC Light room"
kickUser(domain: String!, id: String!, user: 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 the MUC Light room archived messages"
getRoomMessages(domain: String!, id: String!, pageSize: Int!, before: DateTime): StanzasPayload
"Get configuration of the MUC Light room"
getRoomConfig(domain: String!, id: String!): Room
"Get users list of given MUC Light room"
listRoomUsers(domain: String!, id: String!): [RoomUser!]
"Get the list of MUC Light rooms that the user participates in"
listUserRooms(user: JID!): [JID!]
}

type RoomPayload{
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
message: String!
room: Room
}

type Room{
jid: JID
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
name: String
subject: String
participants: [RoomUser!]
}

type RoomUser{
jid: JID
affiliance: String
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
}
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
68 changes: 68 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,68 @@
-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, <<"domain">> := Domain, <<"name">> := RoomName,
<<"owner">> := CreatorJID, <<"subject">> := Subject}) ->
case mod_muc_light_api:create_room(Domain, RoomID, RoomName, CreatorJID, Subject) of
{ok, Room} ->
{ok, make_room(Room)};
Err ->
make_error(Err, #{domain => Domain, roomID => RoomID, creator => CreatorJID})
end.

-spec change_room_config(map()) -> {ok, map()} | {error, resolver_error()}.
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, #{domain => Domain, id => RoomID, owner => OwnerJID})
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
end.

-spec delete_room(map()) -> {ok, binary()} | {error, resolver_error()}.
delete_room(#{<<"domain">> := Domain, <<"id">> := RoomID}) ->
Result = mod_muc_light_api:delete_room(Domain, RoomID),
format_result(Result, #{domain => Domain, id => RoomID}).

-spec invite_user(map()) -> {ok, binary()} | {error, resolver_error()}.
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, #{domain => Domain, name => Name,
sender => sender, recipient => RecipientJID}).
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved

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

-spec send_msg_to_room(map()) -> {ok, binary()} | {error, resolver_error()}.
send_msg_to_room(#{<<"domain">> := Domain, <<"name">> := RoomName, <<"from">> := FromJID,
<<"body">> := Message}) ->
chrzaszcz marked this conversation as resolved.
Show resolved Hide resolved
Result = mod_muc_light_api:send_message(Domain, RoomName, FromJID, Message),
format_result(Result, #{domain => Domain, name => RoomName, from => FromJID}).
63 changes: 63 additions & 0 deletions src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
-module(mongoose_graphql_muc_light_admin_query).

-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, <<"listUserRooms">>, Args) ->
list_user_rooms(Args);
execute(_Ctx, _Obj, <<"listRoomUsers">>, Args) ->
list_room_users(Args);
execute(_Ctx, _Obj, <<"getRoomConfig">>, Args) ->
get_room_config(Args);
execute(_Ctx, _Obj, <<"getRoomMessages">>, Args) ->
get_room_messages(Args).

-spec list_user_rooms(map()) -> {ok, [binary()]} | {error, resolver_error()}.
list_user_rooms(#{<<"user">> := UserJID}) ->
case mod_muc_light_api:get_user_rooms(UserJID) of
{ok, Rooms} ->
{ok, [{ok, R} || R <- Rooms]};
Err ->
make_error(Err, #{user => UserJID})
end.

-spec list_room_users(map()) -> {ok, [map()]} | {error, resolver_error()}.
list_room_users(#{<<"domain">> := Domain, <<"id">> := RoomID}) ->
case mod_muc_light_api:get_room_aff(Domain, RoomID) of
{ok, Affs} ->
{ok, [make_ok_user(A) || A <- Affs]};
Err ->
make_error(Err, #{domain => Domain, id => RoomID})
end.

-spec get_room_config(map()) -> {ok, map()} | {error, resolver_error()}.
get_room_config(#{<<"domain">> := Domain, <<"id">> := RoomID}) ->
case mod_muc_light_api:get_room_info(Domain, RoomID) of
{ok, Room} ->
{ok, make_room(Room)};
Err ->
make_error(Err, #{domain => Domain, id => RoomID})
end.

-spec get_room_messages(map()) -> {ok, map()} | {error, resolver_error()}.
get_room_messages(#{<<"domain">> := Domain, <<"id">> := RoomID,
<<"pageSize">> := PageSize, <<"before">> := Before}) ->
Before2 = null_to_undefined(Before),
case mod_muc_light_api:get_room_messages(Domain, RoomID, PageSize, Before2) of
{ok, Rows} ->
Maps = lists:map(fun mongoose_graphql_stanza_helper:row_to_map/1, Rows),
{ok, #{<<"stanzas">> => Maps, <<"limit">> => null}};
Err ->
make_error(Err, #{domain => Domain, id => RoomID})
end.

%% Helpers

null_to_undefined(null) -> undefined;
null_to_undefined(V) -> V.
10 changes: 1 addition & 9 deletions src/graphql/admin/mongoose_graphql_stanza_admin_query.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,8 @@ get_last_messages2(Caller, Limit, With, Before) ->
Before2 = null_as_undefined(Before), %% Before is in microseconds
Limit2 = min(500, Limit),
Rows = mongoose_stanza_api:lookup_recent_messages(Caller, With2, Before2, Limit2),
Maps = lists:map(fun row_to_map/1, Rows),
Maps = lists:map(fun mongoose_graphql_stanza_helper:row_to_map/1, Rows),
{ok, #{<<"stanzas">> => Maps, <<"limit">> => Limit2}}.

null_as_undefined(null) -> undefined;
null_as_undefined(Value) -> Value.

-spec row_to_map(mod_mam:message_row()) -> {ok, map()}.
row_to_map(#{id := Id, jid := From, packet := Msg}) ->
{Microseconds, _} = mod_mam_utils:decode_compact_uuid(Id),
StanzaID = mod_mam_utils:mess_id_to_external_binary(Id),
Map = #{<<"sender">> => From, <<"timestamp">> => Microseconds,
<<"stanza_id">> => StanzaID, <<"stanza">> => Msg},
{ok, Map}.
2 changes: 2 additions & 0 deletions src/graphql/mongoose_graphql.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
12 changes: 12 additions & 0 deletions src/graphql/mongoose_graphql_muc_light_helper.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-module(mongoose_graphql_muc_light_helper).

-export([make_room/1, make_ok_user/1]).

-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)}}.
11 changes: 11 additions & 0 deletions src/graphql/mongoose_graphql_stanza_helper.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-module(mongoose_graphql_stanza_helper).

-export([row_to_map/1]).

-spec row_to_map(mod_mam:message_row()) -> {ok, map()}.
row_to_map(#{id := Id, jid := From, packet := Msg}) ->
{Microseconds, _} = mod_mam_utils:decode_compact_uuid(Id),
StanzaID = mod_mam_utils:mess_id_to_external_binary(Id),
Map = #{<<"sender">> => From, <<"timestamp">> => Microseconds,
<<"stanza_id">> => StanzaID, <<"stanza">> => Msg},
{ok, Map}.
9 changes: 5 additions & 4 deletions src/mongoose_client_api/mongoose_client_api_rooms_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -67,4 +67,5 @@ handle_request_by_method(<<"PUT">>,
Req, State) ->
mongoose_client_api_rooms:assert_room_id_set(Req, State),
#{user := User, jid := #jid{lserver = Server}, room_id := RoomID} = State,
mod_muc_light_commands:change_room_config(Server, RoomID, Name, User, Subject).
UserJID = jid:from_binary(User),
mod_muc_light_api:change_room_config(Server, RoomID, Name, UserJID, Subject).
Loading