Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions big_tests/tests/graphql_muc_light_SUITE.erl

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions priv/graphql/schemas/admin/muc_light.gql
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,15 @@ type MUCLightAdminQuery @protected @use(modules: ["mod_muc_light"]){
"Get the user's list of blocked entities"
getBlockingList(user: JID!): [BlockingItem!]
@protected(type: DOMAIN, args: ["user"]) @use(args: ["user"])
"Get rooms under the given MUC Light domain, paginated by room JID"
listRooms(
mucDomain: DomainName!
"Return only rooms whose JID localpart or name contains this string (case-insensitive)"
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
filter: String
"JID of the last room from the previous page (see MUCLightRoomsPayload.nextCursor)"
after: BareJID
"Maximum number of rooms to return in one page"
limit: PosInt = 50
): MUCLightRoomsPayload
@protected(type: DOMAIN, args: ["mucDomain"]) @use(args: ["mucDomain"])
}
24 changes: 24 additions & 0 deletions priv/graphql/schemas/global/muc_light.gql
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,27 @@ type RoomUser{
"User's affiliation"
affiliation: Affiliation!
}

"MUC Light rooms payload"
type MUCLightRoomsPayload{
"List of room descriptions, ordered by room JID"
rooms: [MUCLightRoomDesc!]
"Total number of rooms in the domain matching the filter"
count: NonNegInt
"JID of the last room in this page; pass it as 'after' to get the next page. Null when there are no more rooms"
nextCursor: BareJID
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
}

"MUC Light room description"
type MUCLightRoomDesc{
"Room's JID"
jid: BareJID!
"Room's display name (may be empty)"
name: String
"Room's subject (may be empty)"
subject: String
"Number of room members"
usersNumber: NonNegInt
"JID of the room's owner; null if no owner can be resolved"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just note that there can be multiple owners if we enable that - so this would just return one of them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point, doc updated to say "one of the room's owners".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering whether we should return all owners here instead. Alternatively, we could remove this field and be consistent with MUCRoomDesc.

I assume that when we list owners, they will likely be displayed alongside all users, so we would need to call another API anyway.

ownerJid: JID
}
30 changes: 28 additions & 2 deletions src/graphql/admin/mongoose_graphql_muc_light_admin_query.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
-import(mongoose_graphql_helper, [make_error/2, format_result/2, null_to_undefined/1]).
-import(mongoose_graphql_muc_light_helper, [make_room/1,
make_ok_user/1,
page_size_or_max_limit/2]).
page_size_or_max_limit/2,
null_to_default/2]).

%% Matches the schema default of the listRooms limit argument; only needed
%% when the client passes an explicit null.
-define(DEFAULT_ROOMS_PAGE_SIZE, 50).

execute(_Ctx, _Obj, <<"listUserRooms">>, Args) ->
list_user_rooms(Args);
Expand All @@ -21,7 +26,9 @@ execute(_Ctx, _Obj, <<"getRoomConfig">>, Args) ->
execute(_Ctx, _Obj, <<"getRoomMessages">>, Args) ->
get_room_messages(Args);
execute(_Ctx, _Obj, <<"getBlockingList">>, Args) ->
get_blocking_list(Args).
get_blocking_list(Args);
execute(_Ctx, _Obj, <<"listRooms">>, Args) ->
list_rooms(Args).

-spec list_user_rooms(map()) -> {ok, [{ok, binary()}]} | {error, resolver_error()}.
list_user_rooms(#{<<"user">> := UserJID}) ->
Expand Down Expand Up @@ -72,3 +79,22 @@ get_blocking_list(#{<<"user">> := UserJID}) ->
Err ->
make_error(Err, #{user => jid:to_binary(UserJID)})
end.

-spec list_rooms(map()) -> {ok, map()} | {error, resolver_error()}.
list_rooms(#{<<"mucDomain">> := MUCDomain, <<"filter">> := Filter,
<<"after">> := After, <<"limit">> := Limit}) ->
Filter2 = null_to_undefined(Filter),
After2 = after_to_luser(null_to_undefined(After)),
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
Limit2 = null_to_default(Limit, ?DEFAULT_ROOMS_PAGE_SIZE),
case mod_muc_light_api:get_rooms(MUCDomain, Filter2, After2, Limit2) of
{ok, {Rooms, Count, NextCursor}} ->
{ok, mongoose_graphql_muc_light_helper:make_rooms_payload(Rooms, Count, NextCursor)};
Err ->
make_error(Err, #{mucDomain => MUCDomain})
end.

after_to_luser(undefined) ->
undefined;
after_to_luser(JID) ->
{LUser, _} = jid:to_lus(JID),
LUser.
25 changes: 24 additions & 1 deletion src/graphql/mongoose_graphql_muc_light_helper.erl
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
-module(mongoose_graphql_muc_light_helper).

-import(mongoose_graphql_helper, [undefined_to_null/1]).

-export([make_room/1, make_ok_user/1, blocking_item_to_map/1, prepare_blocking_items/1,
page_size_or_max_limit/2, null_to_default/2, config_to_map/3]).
page_size_or_max_limit/2, null_to_default/2, config_to_map/3,
make_rooms_payload/3]).

-spec page_size_or_max_limit(null | integer(), integer()) -> integer().
page_size_or_max_limit(null, MaxLimit) ->
Expand Down Expand Up @@ -46,3 +49,23 @@ options_to_map(null) ->
#{};
options_to_map(Options) ->
maps:from_list([{K, V} || #{<<"key">> := K, <<"value">> := V} <- Options]).

-spec make_rooms_payload([mod_muc_light_api:room_desc()], non_neg_integer(),
jid:jid() | undefined) -> map().
make_rooms_payload(RoomDescs, Count, NextCursor) ->
Rooms = [{ok, make_room_desc(D)} || D <- RoomDescs],
#{<<"rooms">> => Rooms,
<<"count">> => Count,
<<"nextCursor">> => next_cursor_value(NextCursor)}.

next_cursor_value(undefined) -> null;
next_cursor_value(JID) -> jid:to_binary(JID).

-spec make_room_desc(mod_muc_light_api:room_desc()) -> map().
make_room_desc(#{jid := JID, name := Name, subject := Subject,
users_number := UsersNumber, owner := Owner}) ->
#{<<"jid">> => jid:to_binary(JID),
<<"name">> => undefined_to_null(Name),
<<"subject">> => undefined_to_null(Subject),
<<"usersNumber">> => UsersNumber,
<<"ownerJid">> => undefined_to_null(Owner)}.
78 changes: 77 additions & 1 deletion src/muc_light/mod_muc_light_api.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
get_room_messages/4,
get_room_messages/5,
get_user_rooms/1,
get_rooms/4,
get_room_info/1,
get_room_info/2,
get_room_aff/1,
Expand All @@ -33,7 +34,17 @@
aff_users := aff_users(),
options := map()}.

-export_type([room/0]).
-type room_desc() :: #{jid := jid:jid(),
name := binary() | undefined,
subject := binary() | undefined,
users_number := non_neg_integer(),
owner := jid:simple_bare_jid() | undefined}.

-type list_rooms_result() :: {Rooms :: [room_desc()],
Count :: non_neg_integer(),
NextCursor :: jid:jid() | undefined}.

-export_type([room/0, room_desc/0, list_rooms_result/0]).

-define(ROOM_DELETED_SUCC_RESULT, {ok, "Room deleted successfully"}).
-define(USER_NOT_ROOM_MEMBER_RESULT, {not_room_member, "Given user does not occupy this room"}).
Expand Down Expand Up @@ -151,6 +162,17 @@ get_room_aff(RoomJID) ->
get_user_rooms(UserJID) ->
fold(#{user => UserJID}, [fun check_user/1, fun do_get_user_rooms/1]).

%% Filter is matched case-insensitively as a substring of the room localpart
%% or the room name; After is the localpart of the last room of the previous
%% page (keyset cursor).
-spec get_rooms(jid:lserver(), binary() | undefined, jid:luser() | undefined,
pos_integer()) ->
{ok, list_rooms_result()} | {muc_server_not_found, iolist()}.
get_rooms(MUCServer, Filter, After, Limit) ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure about changing the name from MUC(Light)Domain to MUCServer. If you look at the analogous argument in create_room/3, the name remains the same.

M = #{muc_server => MUCServer, filter => normalize_filter(Filter),
after_room => After, limit => Limit},
fold(M, [fun check_muc_domain/1, fun do_get_rooms/1]).

-spec get_blocking_list(jid:jid()) -> {ok, [blocking_item()]} | {user_not_found, iolist()}.
get_blocking_list(UserJID) ->
fold(#{user => UserJID}, [fun check_user/1, fun do_get_blocking_list/1]).
Expand All @@ -173,6 +195,17 @@ check_user(M = #{user := UserJID = #jid{lserver = LServer}}) ->
end.

check_muc_domain(M = #{room := #jid{lserver = LServer}}) ->
check_muc_lserver(LServer, M);
check_muc_domain(M = #{muc_server := MUCServer}) ->
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
case jid:nameprep(MUCServer) of
error ->
?MUC_SERVER_NOT_FOUND_RESULT;
LServer ->
%% Store the nameprepped value, as the backends match on it verbatim
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
check_muc_lserver(LServer, M#{muc_server := LServer})
end.

check_muc_lserver(LServer, M) ->
case mongoose_domain_api:get_subdomain_host_type(LServer) of
{ok, HostType} ->
M#{muc_host_type => HostType};
Expand Down Expand Up @@ -345,6 +378,23 @@ do_get_user_rooms(#{user := UserJID, user_host_type := HostType}) ->
MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
{ok, mod_muc_light_db_backend:get_user_rooms(HostType, jid:to_lus(UserJID), MUCServer)}.

do_get_rooms(#{muc_server := MUCServer, muc_host_type := HostType,
filter := Filter, after_room := After, limit := Limit}) ->
%% Fetch one extra room to learn whether a next page exists
{Raw, Count} =
mod_muc_light_db_backend:get_room_descs(HostType, MUCServer, Filter, After, Limit + 1),
Schema = mod_muc_light:config_schema(MUCServer),
Page = [raw_to_room_desc(R, Schema) || R <- lists:sublist(Raw, Limit)],
NextCursor = case length(Raw) > Limit of
true -> maps:get(jid, lists:last(Page));
false -> undefined
end,
{ok, {Page, Count, NextCursor}}.

normalize_filter(undefined) -> undefined;
normalize_filter(<<>>) -> undefined;
normalize_filter(Filter) -> string:lowercase(Filter).

do_get_blocking_list(#{user := UserJID, user_host_type := HostType}) ->
MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
{ok, mod_muc_light_db_backend:get_blocking(HostType, jid:to_lus(UserJID), MUCServer)}.
Expand Down Expand Up @@ -387,6 +437,32 @@ get_aff(UserUS, Affs) ->
false -> none
end.

-spec raw_to_room_desc(mod_muc_light_db_backend:room_desc(),
mod_muc_light_room_config:schema()) -> room_desc().
raw_to_room_desc(#{room := {RoomU, RoomS}, config := Config, aff_users := AffUsers}, Schema) ->
%% DB rows are already prepped; make_noprep cannot fail on a malformed row
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
#{jid => jid:make_noprep(RoomU, RoomS, <<>>),
name => config_field(<<"roomname">>, Config, Schema),
subject => config_field(<<"subject">>, Config, Schema),
users_number => length(AffUsers),
owner => find_owner(AffUsers)}.

%% Config kv() is keyed by the internal schema key, which may differ from the field name
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
-spec config_field(binary(), mod_muc_light_room_config:kv(),
mod_muc_light_room_config:schema()) -> binary() | undefined.
config_field(FieldName, Config, Schema) ->
case lists:keyfind(FieldName, 1, Schema) of
{FieldName, _Default, Key, _Type} -> proplists:get_value(Key, Config, undefined);
false -> undefined
end.

-spec find_owner(aff_users()) -> jid:simple_bare_jid() | undefined.
find_owner(AffUsers) ->
case lists:keyfind(owner, 2, AffUsers) of
{OwnerUS, owner} -> OwnerUS;
false -> undefined
end.

make_room(JID, #config{ raw_config = Options}, AffUsers) ->
make_room(JID, Options, AffUsers);
make_room(JID, Options, AffUsers) when is_list(Options) ->
Expand Down
28 changes: 27 additions & 1 deletion src/muc_light/mod_muc_light_db_backend.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@
-type remove_user_return() :: [{RoomUS :: jid:simple_bare_jid(),
modify_aff_users_return()}].

-type room_desc() :: #{room := jid:simple_bare_jid(),
config := mod_muc_light_room_config:kv(),
aff_users := aff_users()}.

-export_type([modify_aff_users_return/0,
remove_user_return/0]).
remove_user_return/0,
room_desc/0]).

%% API
-export([start/2]).
Expand All @@ -42,6 +47,7 @@
-export([get_aff_users/2]).
-export([modify_aff_users/5]).
-export([get_info/2]).
-export([get_room_descs/5]).

%% For tests
-export([force_clear/1]).
Expand Down Expand Up @@ -135,6 +141,19 @@
{ok, mod_muc_light_room_config:kv(), aff_users(), Version :: binary()}
| {error, not_exists}.

%% ------------------------ Listing rooms in a domain ------------------------
%% Returns a page of at most Limit rooms sorted by the room localpart,
%% positioned strictly after the After cursor (undefined starts from the
%% beginning), together with the total number of rooms matching the filter.
%% Filter is a lowercase substring matched case-insensitively against the room
%% localpart and the room name; undefined matches every room.
-callback get_room_descs(HostType :: mongooseim:host_type(),
MUCServer :: jid:lserver(),
Filter :: binary() | undefined,
After :: jid:luser() | undefined,
Limit :: pos_integer()) ->
{[room_desc()], Count :: non_neg_integer()}.

%% ------------------------ API for tests ------------------------
-callback force_clear() -> ok.

Expand Down Expand Up @@ -275,6 +294,13 @@ get_info(HostType, RoomUS) ->
Args = [HostType, RoomUS],
mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).

-spec get_room_descs(mongooseim:host_type(), jid:lserver(), binary() | undefined,
jid:luser() | undefined, pos_integer()) ->
{[room_desc()], Count :: non_neg_integer()}.
get_room_descs(HostType, MUCServer, Filter, After, Limit) ->
Args = [HostType, MUCServer, Filter, After, Limit],
mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args).

-spec force_clear(HostType :: mongooseim:host_type()) -> ok.
force_clear(HostType) ->
mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, []).
44 changes: 43 additions & 1 deletion src/muc_light/mod_muc_light_db_mnesia.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@
set_blocking/4,
get_aff_users/2,
modify_aff_users/5,
get_info/2
get_info/2,
get_room_descs/5
]).

%% Extra API for testing
-export([force_clear/0]).
-ignore_xref([force_clear/0]).

-include("mod_muc_light.hrl").
-include_lib("stdlib/include/ms_transform.hrl").

-record(muc_light_room, {
room :: jid:simple_bare_jid(),
Expand Down Expand Up @@ -232,6 +234,46 @@ get_info(_HostType, RoomUS) ->
{ok, Config, AffUsers, Version}
end.

-spec get_room_descs(mongooseim:host_type(), jid:lserver(), binary() | undefined,
jid:luser() | undefined, pos_integer()) ->
{[mod_muc_light_db_backend:room_desc()], non_neg_integer()}.
get_room_descs(_HostType, MUCServer, Filter, After, Limit) ->
MS = ets:fun2ms(fun(#muc_light_room{room = {_, RoomS}} = Room)
when RoomS =:= MUCServer -> Room end),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you should be able to encode the After condition in this MS as well to avoid fetching all rooms at once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Did the change, but we still need to do the second dirty_select for the count of all rooms, not sure if this version is better.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's keep it as it is, especially since RDBMS is the recommended backen. As a note, mod_muc takes a similar approach to the first implementation and fetches everything.

Rooms = mnesia:dirty_select(muc_light_room, MS),
Schema = mod_muc_light:config_schema(MUCServer),
Matching = lists:keysort(#muc_light_room.room,
[R || R <- Rooms, room_matches_filter(R, Filter, Schema)]),
Page = lists:sublist(drop_up_to_cursor(Matching, After), Limit),
Descs = [#{room => RoomUS, config => Config, aff_users => AffUsers}
|| #muc_light_room{room = RoomUS, config = Config, aff_users = AffUsers} <- Page],
{Descs, length(Matching)}.

drop_up_to_cursor(Rooms, undefined) ->
Rooms;
drop_up_to_cursor(Rooms, After) ->
lists:dropwhile(fun(#muc_light_room{room = {RoomU, _}}) -> RoomU =< After end, Rooms).

room_matches_filter(_Room, undefined, _Schema) ->
true;
room_matches_filter(#muc_light_room{room = {RoomU, _}, config = Config}, Filter, Schema) ->
binary:match(RoomU, Filter) =/= nomatch
orelse room_name_matches(Config, Filter, Schema).

%% Config is keyed by the internal schema key, which may differ from the field name
Comment thread
kamilwaz marked this conversation as resolved.
Outdated
room_name_matches(Config, Filter, Schema) ->
case lists:keyfind(<<"roomname">>, 1, Schema) of
{_FieldName, _Default, Key, _Type} ->
case proplists:get_value(Key, Config) of
Name when is_binary(Name) ->
binary:match(string:lowercase(Name), Filter) =/= nomatch;
_ ->
false
end;
false ->
false
end.

%%====================================================================
%% API for tests
%%====================================================================
Expand Down
Loading