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

MUC Light ODBC #1093

Merged
merged 11 commits into from
Dec 5, 2016
7 changes: 4 additions & 3 deletions apps/ejabberd/include/mod_muc_light.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@

-type rooms_per_user() :: infinity | non_neg_integer().

-type blocking_who() :: user | room.
-type blocking_what() :: user | room.
-type blocking_action() :: allow | deny.
-type blocking_who() :: ejabberd:simple_bare_jid().
-type blocking_item() :: {
What :: blocking_who(),
What :: blocking_what(),
Action :: blocking_action(),
Who :: ejabberd:simple_bare_jid()
Who :: blocking_who()
}.

-type disco_room_info() :: {RoomUS :: ejabberd:simple_bare_jid(),
Expand Down
39 changes: 39 additions & 0 deletions apps/ejabberd/priv/mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,42 @@ CREATE TABLE offline_message(
packet blob NOT NULL
);
CREATE INDEX i_offline_message USING BTREE ON offline_message(server, username, id);

CREATE TABLE muc_light_rooms(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
luser VARCHAR(250) NOT NULL,
lserver VARCHAR(250) NOT NULL,
version VARCHAR(20) NOT NULL,
PRIMARY KEY (lserver, luser),
UNIQUE KEY k_id USING HASH (id)
);

CREATE INDEX i_muc_light_rooms USING HASH ON muc_light_rooms(id);

CREATE TABLE muc_light_occupants(
room_id BIGINT UNSIGNED NOT NULL REFERENCES muc_light_rooms(id),
luser VARCHAR(250) NOT NULL,
lserver VARCHAR(250) NOT NULL,
aff TINYINT UNSIGNED NOT NULL
);

CREATE INDEX i_muc_light_occupants_id USING HASH ON muc_light_occupants(room_id);
CREATE INDEX i_muc_light_occupants_us USING HASH ON muc_light_occupants(lserver, luser);

CREATE TABLE muc_light_config(
room_id BIGINT UNSIGNED NOT NULL REFERENCES muc_light_rooms(id),
opt VARCHAR(100) NOT NULL,
val VARCHAR(250) NOT NULL
);

CREATE INDEX i_muc_light_config USING HASH ON muc_light_config(room_id);

CREATE TABLE muc_light_blocking(
luser VARCHAR(250) NOT NULL,
lserver VARCHAR(250) NOT NULL,
what TINYINT UNSIGNED NOT NULL,
who VARCHAR(500) NOT NULL
);

CREATE INDEX i_muc_light_blocking USING HASH ON muc_light_blocking(luser, lserver);

35 changes: 35 additions & 0 deletions apps/ejabberd/priv/pg.sql
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,38 @@ CREATE TABLE auth_token(
owner TEXT NOT NULL PRIMARY KEY,
seq_no BIGINT NOT NULL
);

CREATE TABLE muc_light_rooms(
id BIGSERIAL NOT NULL UNIQUE,
luser VARCHAR(250) NOT NULL,
lserver VARCHAR(250) NOT NULL,
version VARCHAR(20) NOT NULL,
PRIMARY KEY (lserver, luser)
);

CREATE TABLE muc_light_occupants(
room_id BIGINT NOT NULL REFERENCES muc_light_rooms(id),
luser VARCHAR(250) NOT NULL,
lserver VARCHAR(250) NOT NULL,
aff SMALLINT NOT NULL
);

CREATE INDEX i_muc_light_occupants_id ON muc_light_occupants (room_id);
CREATE INDEX i_muc_light_occupants_us ON muc_light_occupants (lserver, luser);

CREATE TABLE muc_light_config(
room_id BIGINT NOT NULL REFERENCES muc_light_rooms(id),
opt VARCHAR(100) NOT NULL,
val VARCHAR(250) NOT NULL
);

CREATE INDEX i_muc_light_config ON muc_light_config (room_id);

CREATE TABLE muc_light_blocking(
luser VARCHAR(250) NOT NULL,
lserver VARCHAR(250) NOT NULL,
what SMALLINT NOT NULL,
who VARCHAR(500) NOT NULL
);

CREATE INDEX i_muc_light_blocking ON muc_light_blocking (luser, lserver);
28 changes: 18 additions & 10 deletions apps/ejabberd/src/mod_muc_light.erl
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ start(Host, Opts) ->
ConfigSchema),
set_opt(MyDomain, default_config, DefaultConfig),

set_opt(MyDomain, main_host, Host),

ok.

-spec stop(Host :: ejabberd:server()) -> ok.
Expand Down Expand Up @@ -203,12 +205,17 @@ route(From, To, Packet) ->
DecodedPacket :: mod_muc_light_codec:decode_result(),
OrigPacket :: jlib:xmlel()) -> any().
process_packet(From, To, {ok, {set, #create{} = Create}}, OrigPacket) ->
RoomsPerUser = get_opt(To#jid.lserver, rooms_per_user, ?DEFAULT_ROOMS_PER_USER),
FromUS = jid:to_lus(From),
case RoomsPerUser == infinity orelse length(?BACKEND:get_user_rooms(FromUS)) < RoomsPerUser of
true ->
MayCreate = case get_opt(To#jid.lserver, rooms_per_user, ?DEFAULT_ROOMS_PER_USER) of
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you move the whole case to a dedicated function with meaningful name?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, why not.

infinity ->
true;
RoomsPerUser ->
length(?BACKEND:get_user_rooms(FromUS, To#jid.lserver)) < RoomsPerUser

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 213) as module names.

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 213. Only modules that define callbacks should make dynamic calls.

Copy link
Contributor

Choose a reason for hiding this comment

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

How difficult would it be to implement ?BACKEND:get_user_rooms_count? With count on the backend side?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not difficult at all and actually I was considering it. Let's do it!

end,
if

Choose a reason for hiding this comment

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

According to Elvis:

Replace the 'if' expression on line 215 with a 'case' expression or function clauses.

Choose a reason for hiding this comment

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

According to Elvis:

Replace the 'if' expression on line 215 with a 'case' expression or function clauses.

MayCreate ->
create_room(From, FromUS, To, Create, OrigPacket);
false ->
true ->
?CODEC:encode_error(

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 219) as module names.

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 219. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 219) as module names.

{error, bad_request}, From, To, OrigPacket, fun ejabberd_router:route/3)
end;
Expand Down Expand Up @@ -304,7 +311,7 @@ add_rooms_to_roster(Acc, UserUS) ->
children = [#xmlcdata{ content = RoomVersion }] }]
},
[Item | Acc0]
end, Acc, get_rooms_info(lists:sort(?BACKEND:get_user_rooms(UserUS)))).
end, Acc, get_rooms_info(lists:sort(?BACKEND:get_user_rooms(UserUS, undefined)))).

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 314) as module names.

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 314. Only modules that define callbacks should make dynamic calls.


-spec process_iq_get(Acc :: any(), From :: #jid{}, To :: #jid{},

Choose a reason for hiding this comment

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

According to Elvis:

The spec in line 316 uses a record, please define a type for the record and use that instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Elvis here.

IQ :: #iq{}, ActiveList :: binary()) ->
Expand All @@ -313,7 +320,7 @@ process_iq_get(_Acc, #jid{ lserver = FromS } = From, To, #iq{} = IQ, _ActiveList
MUCHost = gen_mod:get_module_opt_host(FromS, ?MODULE, ?DEFAULT_HOST),
case {?CODEC:decode(From, To, IQ), get_opt(MUCHost, blocking, ?DEFAULT_BLOCKING)} of

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 321. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 321) as module names.

{{ok, {get, #blocking{} = Blocking}}, true} ->
Items = ?BACKEND:get_blocking(jid:to_lus(From)),
Items = ?BACKEND:get_blocking(jid:to_lus(From), MUCHost),

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 323) as module names.

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 323. Only modules that define callbacks should make dynamic calls.

?CODEC:encode({get, Blocking#blocking{ items = Items }}, From, jid:to_lus(To),

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 324. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 324) as module names.

fun(_, _, Packet) -> put(encode_res, Packet) end),
#xmlel{ children = ResponseChildren } = erase(encode_res),
Expand All @@ -336,7 +343,7 @@ process_iq_set(_Acc, #jid{ lserver = FromS } = From, To, #iq{} = IQ) ->
true ->
{stop, {error, ?ERR_BAD_REQUEST}};
false ->
ok = ?BACKEND:set_blocking(jid:to_lus(From), Items),
ok = ?BACKEND:set_blocking(jid:to_lus(From), MUCHost, Items),

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 346. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 346) as module names.

?CODEC:encode(Blocking, From, jid:to_lus(To),

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 347. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 347) as module names.

fun(_, _, Packet) -> put(encode_res, Packet) end),

Choose a reason for hiding this comment

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

According to Elvis:

The expression on line 348 and column 35 is nested beyond the maximum level of 3.

Copy link

Choose a reason for hiding this comment

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

According to Elvis:

The expression on line 349 and column 35 is nested beyond the maximum level of 3.

#xmlel{ children = ResponseChildren } = erase(encode_res),
Expand Down Expand Up @@ -457,7 +464,7 @@ handle_disco_info_get(From, To, DiscoInfo) ->
-spec handle_disco_items_get(From :: jid(), To :: jid(), DiscoItems :: #disco_items{},

Choose a reason for hiding this comment

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

According to Elvis:

The spec in line 464 uses a record, please define a type for the record and use that instead.

OrigPacket :: jlib:xmlel()) -> ok.
handle_disco_items_get(From, To, DiscoItems0, OrigPacket) ->
case catch ?BACKEND:get_user_rooms(jid:to_lus(From)) of
case catch ?BACKEND:get_user_rooms(jid:to_lus(From), To#jid.lserver) of

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 467) as module names.

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 467. Only modules that define callbacks should make dynamic calls.

{error, Error} ->
?ERROR_MSG("Couldn't get room list for user ~p: ~p", [From, Error]),
?CODEC:encode_error({error, internal_server_error}, From, To, OrigPacket,

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 470. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 470) as module names.

Expand Down Expand Up @@ -558,14 +565,15 @@ find_room_pos(_, [], _) -> {error, item_not_found}.
BlockingReq :: {get | set, #blocking{}}) ->
{error, bad_request} | ok.
handle_blocking(From, To, {get, #blocking{} = Blocking}) ->
?CODEC:encode({get, Blocking#blocking{ items = ?BACKEND:get_blocking(jid:to_lus(From)) }},
BlockingItems = ?BACKEND:get_blocking(jid:to_lus(From), To#jid.lserver),

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 568) as module names.

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 568. Only modules that define callbacks should make dynamic calls.

?CODEC:encode({get, Blocking#blocking{ items = BlockingItems }},

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 569. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 569) as module names.

From, jid:to_lus(To), fun ejabberd_router:route/3);
handle_blocking(From, To, {set, #blocking{ items = Items }} = BlockingReq) ->
case lists:any(fun({_, _, {WhoU, WhoS}}) -> WhoU =:= <<>> orelse WhoS =:= <<>> end, Items) of
true ->
{error, bad_request};
false ->
ok = ?BACKEND:set_blocking(jid:to_lus(From), Items),
ok = ?BACKEND:set_blocking(jid:to_lus(From), To#jid.lserver, Items),

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 576. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 576) as module names.

?CODEC:encode(BlockingReq, From, jid:to_lus(To), fun ejabberd_router:route/3),

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 577. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like CODEC on line 577) as module names.

ok
end.
Expand Down
8 changes: 4 additions & 4 deletions apps/ejabberd/src/mod_muc_light_commands.erl
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ send_message(Domain, RoomName, Sender, Message) ->
children = [ Body ]
},
S = jid:binary_to_bare(Sender),
case get_user_rooms(S) of
case get_user_rooms(S, Domain) of
[] ->
{error, given_user_does_not_occupy_any_room};
RoomJIDs when is_list(RoomJIDs) ->
Expand All @@ -169,7 +169,7 @@ make_room_config(Name, Subject) ->
}.

muc_light_room_name_to_jid(Participant, RoomName, Domain) ->
case get_user_rooms(Participant) of
case get_user_rooms(Participant, Domain) of
[] ->
{error, given_user_does_not_occupy_any_room};
RoomJIDs when is_list(RoomJIDs) ->
Expand All @@ -179,8 +179,8 @@ muc_light_room_name_to_jid(Participant, RoomName, Domain) ->
jid:make(RU, RS, <<>>)
end.

get_user_rooms(UserJID) ->
?BACKEND:get_user_rooms(jid:to_lus(UserJID)).
get_user_rooms(UserJID, Domain) ->
?BACKEND:get_user_rooms(jid:to_lus(UserJID), Domain).

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 183. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 183) as module names.


name_of_room_with_jid(RoomJID) ->
case ?BACKEND:get_info(RoomJID) of

Choose a reason for hiding this comment

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

According to Elvis:

Remove the dynamic function call on line 186. Only modules that define callbacks should make dynamic calls.

Choose a reason for hiding this comment

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

According to Elvis:

Don't use macros (like BACKEND on line 186) as module names.

Expand Down
10 changes: 7 additions & 3 deletions apps/ejabberd/src/mod_muc_light_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@

-callback room_exists(RoomUS :: ejabberd:simple_bare_jid()) -> boolean().

-callback get_user_rooms(UserUS :: ejabberd:simple_bare_jid()) ->
-callback get_user_rooms(UserUS :: ejabberd:simple_bare_jid(),
MUCServer :: ejabberd:lserver() | undefined) ->
[RoomUS :: ejabberd:simple_bare_jid()].

-callback remove_user(UserUS :: ejabberd:simple_bare_jid(), Version :: binary()) ->
Expand All @@ -62,13 +63,16 @@

%% ------------------------ Blocking manipulation ------------------------

-callback get_blocking(UserUS :: ejabberd:simple_bare_jid()) -> [blocking_item()].
-callback get_blocking(UserUS :: ejabberd:simple_bare_jid(), MUCServer :: ejabberd:lserver()) ->
[blocking_item()].

-callback get_blocking(UserUS :: ejabberd:simple_bare_jid(),
WhatWhos :: [{blocking_who(), ejabberd:simple_bare_jid()}]) ->
MUCServer :: ejabberd:lserver(),
WhatWhos :: [{blocking_what(), blocking_who()}]) ->
blocking_action().

-callback set_blocking(UserUS :: ejabberd:simple_bare_jid(),
MUCServer :: ejabberd:lserver(),
BlockingItems :: [blocking_item()]) -> ok.

%% ------------------------ Affiliations manipulation ------------------------
Expand Down
35 changes: 20 additions & 15 deletions apps/ejabberd/src/mod_muc_light_db_mnesia.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@
create_room/4,
destroy_room/1,
room_exists/1,
get_user_rooms/1,
get_user_rooms/2,
remove_user/2,

get_config/1,
get_config/2,
set_config/3,
set_config/4,

get_blocking/1,
get_blocking/2,
set_blocking/2,
get_blocking/3,
set_blocking/3,

get_aff_users/1,
modify_aff_users/4,
Expand Down Expand Up @@ -113,9 +113,10 @@ destroy_room(RoomUS) ->
room_exists(RoomUS) ->
mnesia:dirty_read(?ROOM_TAB, RoomUS) =/= [].

-spec get_user_rooms(UserUS :: ejabberd:simple_bare_jid()) ->
-spec get_user_rooms(UserUS :: ejabberd:simple_bare_jid(),
MUCServer :: ejabberd:lserver() | undefined) ->
[RoomUS :: ejabberd:simple_bare_jid()].
get_user_rooms(UserUS) ->
get_user_rooms(UserUS, _MUCHost) ->
UsersRooms = mnesia:dirty_read(?USER_ROOM_TAB, UserUS),
[ UserRoom#?USER_ROOM_TAB.room || UserRoom <- UsersRooms ].

Expand Down Expand Up @@ -164,15 +165,17 @@ set_config(RoomJID, Key, Val, Version) ->

%% ------------------------ Blocking manipulation ------------------------

-spec get_blocking(UserUS :: ejabberd:simple_bare_jid()) -> [blocking_item()].
get_blocking(UserUS) ->
-spec get_blocking(UserUS :: ejabberd:simple_bare_jid(), MUCServer :: ejabberd:lserver()) ->
[blocking_item()].
get_blocking(UserUS, _MUCServer) ->
[ {What, deny, Who}
|| #?BLOCKING_TAB{ item = {What, Who} } <- dirty_get_blocking_raw(UserUS) ].

-spec get_blocking(UserUS :: ejabberd:simple_bare_jid(),
WhatWhos :: [{blocking_who(), ejabberd:simple_bare_jid()}]) ->
MUCServer :: ejabberd:lserver(),
WhatWhos :: [{blocking_what(), ejabberd:simple_bare_jid()}]) ->
blocking_action().
get_blocking(UserUS, WhatWhos) ->
get_blocking(UserUS, _MUCServer, WhatWhos) ->
Blocklist = dirty_get_blocking_raw(UserUS),
case lists:any(
fun(WhatWho) ->
Expand All @@ -182,15 +185,17 @@ get_blocking(UserUS, WhatWhos) ->
false -> allow
end.

-spec set_blocking(UserUS :: ejabberd:simple_bare_jid(), BlockingItems :: [blocking_item()]) -> ok.
set_blocking(_UserUS, []) ->
-spec set_blocking(UserUS :: ejabberd:simple_bare_jid(),
MUCServer :: ejabberd:lserver(),
BlockingItems :: [blocking_item()]) -> ok.
set_blocking(_UserUS, _MUCServer, []) ->
ok;
set_blocking(UserUS, [{What, deny, Who} | RBlockingItems]) ->
set_blocking(UserUS, MUCServer, [{What, deny, Who} | RBlockingItems]) ->
mnesia:dirty_write(#?BLOCKING_TAB{ user = UserUS, item = {What, Who} }),
set_blocking(UserUS, RBlockingItems);
set_blocking(UserUS, [{What, allow, Who} | RBlockingItems]) ->
set_blocking(UserUS, MUCServer, RBlockingItems);
set_blocking(UserUS, MUCServer, [{What, allow, Who} | RBlockingItems]) ->
mnesia:dirty_delete_object(#?BLOCKING_TAB{ user = UserUS, item = {What, Who} }),
set_blocking(UserUS, RBlockingItems).
set_blocking(UserUS, MUCServer, RBlockingItems).

%% ------------------------ Affiliations manipulation ------------------------

Expand Down
Loading