mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-22 16:20:52 +01:00
full support for XEP-0115 v1.5 (EJAB-1223) (EJAB-1189)
This commit is contained in:
parent
f84a1c88cf
commit
92ec42565e
@ -323,9 +323,9 @@ wait_for_stream({xmlstreamstart, #xmlel{ns = NS} = Opening}, StateData) ->
|
|||||||
[]
|
[]
|
||||||
end,
|
end,
|
||||||
Other_Feats = ejabberd_hooks:run_fold(
|
Other_Feats = ejabberd_hooks:run_fold(
|
||||||
c2s_stream_features,
|
c2s_stream_features,
|
||||||
ServerB,
|
ServerB,
|
||||||
[], []),
|
[], [ServerB]),
|
||||||
send_element(StateData,
|
send_element(StateData,
|
||||||
exmpp_stream:features(
|
exmpp_stream:features(
|
||||||
TLSFeature ++
|
TLSFeature ++
|
||||||
@ -340,17 +340,26 @@ wait_for_stream({xmlstreamstart, #xmlel{ns = NS} = Opening}, StateData) ->
|
|||||||
_ ->
|
_ ->
|
||||||
case StateData#state.resource of
|
case StateData#state.resource of
|
||||||
undefined ->
|
undefined ->
|
||||||
RosterVersioningFeature = ejabberd_hooks:run_fold(roster_get_versioning_feature, ServerB, [], [ServerB]),
|
RosterVersioningFeature =
|
||||||
|
ejabberd_hooks:run_fold(
|
||||||
|
roster_get_versioning_feature,
|
||||||
|
ServerB,
|
||||||
|
[], [ServerB]),
|
||||||
|
Other_Feats = ejabberd_hooks:run_fold(
|
||||||
|
c2s_stream_features,
|
||||||
|
ServerB,
|
||||||
|
[], [ServerB]),
|
||||||
send_element(
|
send_element(
|
||||||
StateData,
|
StateData,
|
||||||
exmpp_stream:features([
|
exmpp_stream:features(
|
||||||
exmpp_server_binding:feature(),
|
[exmpp_server_binding:feature(),
|
||||||
exmpp_server_session:feature()
|
exmpp_server_session:feature()]
|
||||||
| RosterVersioningFeature])),
|
++ RosterVersioningFeature
|
||||||
|
++ Other_Feats)),
|
||||||
fsm_next_state(wait_for_bind,
|
fsm_next_state(wait_for_bind,
|
||||||
StateData#state{
|
StateData#state{
|
||||||
server = ServerB,
|
server = ServerB,
|
||||||
lang = Lang});
|
lang = Lang});
|
||||||
_ ->
|
_ ->
|
||||||
send_element(
|
send_element(
|
||||||
StateData,
|
StateData,
|
||||||
|
@ -155,8 +155,9 @@ init([{SockMod, Socket}, Opts]) ->
|
|||||||
wait_for_stream({xmlstreamstart, Opening}, StateData) ->
|
wait_for_stream({xmlstreamstart, Opening}, StateData) ->
|
||||||
case {exmpp_stream:get_default_ns(Opening),
|
case {exmpp_stream:get_default_ns(Opening),
|
||||||
exmpp_xml:is_ns_declared_here(Opening, ?NS_DIALBACK),
|
exmpp_xml:is_ns_declared_here(Opening, ?NS_DIALBACK),
|
||||||
|
exmpp_stream:get_receiving_entity(Opening),
|
||||||
exmpp_stream:get_version(Opening) == {1, 0}} of
|
exmpp_stream:get_version(Opening) == {1, 0}} of
|
||||||
{?NS_JABBER_SERVER, _, true} when
|
{?NS_JABBER_SERVER, _, Server, true} when
|
||||||
StateData#state.tls and (not StateData#state.authenticated) ->
|
StateData#state.tls and (not StateData#state.authenticated) ->
|
||||||
Opening_Reply = exmpp_stream:opening_reply(Opening,
|
Opening_Reply = exmpp_stream:opening_reply(Opening,
|
||||||
StateData#state.streamid),
|
StateData#state.streamid),
|
||||||
@ -188,17 +189,25 @@ wait_for_stream({xmlstreamstart, Opening}, StateData) ->
|
|||||||
true ->
|
true ->
|
||||||
[exmpp_server_tls:feature()]
|
[exmpp_server_tls:feature()]
|
||||||
end,
|
end,
|
||||||
send_element(StateData, exmpp_stream:features(SASL ++ StartTLS)),
|
Features = SASL ++ StartTLS ++ ejabberd_hooks:run_fold(
|
||||||
|
c2s_stream_features,
|
||||||
|
Server,
|
||||||
|
[], [Server]),
|
||||||
|
send_element(StateData, exmpp_stream:features(Features)),
|
||||||
{next_state, wait_for_feature_request, StateData};
|
{next_state, wait_for_feature_request, StateData};
|
||||||
{?NS_JABBER_SERVER, _, true} when
|
{?NS_JABBER_SERVER, _, Server, true} when
|
||||||
StateData#state.authenticated ->
|
StateData#state.authenticated ->
|
||||||
Opening_Reply = exmpp_stream:opening_reply(Opening,
|
Opening_Reply = exmpp_stream:opening_reply(Opening,
|
||||||
StateData#state.streamid),
|
StateData#state.streamid),
|
||||||
send_element(StateData,
|
send_element(StateData,
|
||||||
exmpp_stream:set_dialback_support(Opening_Reply)),
|
exmpp_stream:set_dialback_support(Opening_Reply)),
|
||||||
send_element(StateData, exmpp_stream:features([])),
|
Features = ejabberd_hooks:run_fold(
|
||||||
|
c2s_stream_features,
|
||||||
|
Server,
|
||||||
|
[], [Server]),
|
||||||
|
send_element(StateData, exmpp_stream:features(Features)),
|
||||||
{next_state, stream_established, StateData};
|
{next_state, stream_established, StateData};
|
||||||
{?NS_JABBER_SERVER, true, _} ->
|
{?NS_JABBER_SERVER, true, _Server, _} ->
|
||||||
Opening_Reply = exmpp_stream:opening_reply(Opening,
|
Opening_Reply = exmpp_stream:opening_reply(Opening,
|
||||||
StateData#state.streamid),
|
StateData#state.streamid),
|
||||||
send_element(StateData,
|
send_element(StateData,
|
||||||
|
161
src/mod_caps.erl
161
src/mod_caps.erl
@ -33,6 +33,10 @@
|
|||||||
-behaviour(gen_mod).
|
-behaviour(gen_mod).
|
||||||
|
|
||||||
-export([read_caps/1,
|
-export([read_caps/1,
|
||||||
|
caps_stream_features/2,
|
||||||
|
disco_features/5,
|
||||||
|
disco_identity/5,
|
||||||
|
disco_info/5,
|
||||||
get_features/1]).
|
get_features/1]).
|
||||||
|
|
||||||
%% gen_mod callbacks
|
%% gen_mod callbacks
|
||||||
@ -146,6 +150,42 @@ user_send_packet(From, To, #xmlel{name = 'presence', attrs = Attrs, children = E
|
|||||||
user_send_packet(_From, _To, _Packet) ->
|
user_send_packet(_From, _To, _Packet) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
caps_stream_features(Acc, MyHost) ->
|
||||||
|
case make_my_disco_hash(MyHost) of
|
||||||
|
"" ->
|
||||||
|
Acc;
|
||||||
|
Hash ->
|
||||||
|
[#xmlel{name = c,
|
||||||
|
ns = ?NS_CAPS,
|
||||||
|
attrs = [?XMLATTR(hash, "sha-1"),
|
||||||
|
?XMLATTR(node, ?EJABBERD_URI),
|
||||||
|
?XMLATTR(ver, Hash)]} | Acc]
|
||||||
|
end.
|
||||||
|
|
||||||
|
disco_features(_Acc, From, To, <<?EJABBERD_URI, $#, _/binary>>, Lang) ->
|
||||||
|
ejabberd_hooks:run_fold(disco_local_features,
|
||||||
|
exmpp_jid:domain(To),
|
||||||
|
empty,
|
||||||
|
[From, To, <<>>, Lang]);
|
||||||
|
disco_features(Acc, _From, _To, _Node, _Lang) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
|
disco_identity(_Acc, From, To, <<?EJABBERD_URI, $#, _/binary>>, Lang) ->
|
||||||
|
ejabberd_hooks:run_fold(disco_local_identity,
|
||||||
|
exmpp_jid:domain(To),
|
||||||
|
[],
|
||||||
|
[From, To, <<>>, Lang]);
|
||||||
|
disco_identity(Acc, _From, _To, _Node, _Lang) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
|
disco_info(_Acc, Host, Module, <<?EJABBERD_URI, $#, _/binary>>, Lang) ->
|
||||||
|
ejabberd_hooks:run_fold(disco_info,
|
||||||
|
list_to_binary(Host),
|
||||||
|
[],
|
||||||
|
[Host, Module, <<>>, Lang]);
|
||||||
|
disco_info(Acc, _Host, _Module, _Node, _Lang) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
@ -158,6 +198,16 @@ init([Host, _Opts]) ->
|
|||||||
mnesia:add_table_copy(caps_features, node(), disc_copies),
|
mnesia:add_table_copy(caps_features, node(), disc_copies),
|
||||||
HostB = list_to_binary(Host),
|
HostB = list_to_binary(Host),
|
||||||
ejabberd_hooks:add(user_send_packet, HostB, ?MODULE, user_send_packet, 75),
|
ejabberd_hooks:add(user_send_packet, HostB, ?MODULE, user_send_packet, 75),
|
||||||
|
ejabberd_hooks:add(c2s_stream_features, HostB,
|
||||||
|
?MODULE, caps_stream_features, 75),
|
||||||
|
ejabberd_hooks:add(s2s_stream_features, HostB,
|
||||||
|
?MODULE, caps_stream_features, 75),
|
||||||
|
ejabberd_hooks:add(disco_local_features, HostB,
|
||||||
|
?MODULE, disco_features, 75),
|
||||||
|
ejabberd_hooks:add(disco_local_identity, HostB,
|
||||||
|
?MODULE, disco_identity, 75),
|
||||||
|
ejabberd_hooks:add(disco_info, HostB,
|
||||||
|
?MODULE, disco_info, 75),
|
||||||
{ok, #state{host = Host}}.
|
{ok, #state{host = Host}}.
|
||||||
|
|
||||||
handle_call(stop, _From, State) ->
|
handle_call(stop, _From, State) ->
|
||||||
@ -174,6 +224,16 @@ handle_info(_Info, State) ->
|
|||||||
terminate(_Reason, State) ->
|
terminate(_Reason, State) ->
|
||||||
HostB = list_to_binary(State#state.host),
|
HostB = list_to_binary(State#state.host),
|
||||||
ejabberd_hooks:delete(user_send_packet, HostB, ?MODULE, user_send_packet, 75),
|
ejabberd_hooks:delete(user_send_packet, HostB, ?MODULE, user_send_packet, 75),
|
||||||
|
ejabberd_hooks:delete(c2s_stream_features, HostB,
|
||||||
|
?MODULE, caps_stream_features, 75),
|
||||||
|
ejabberd_hooks:delete(s2s_stream_features, HostB,
|
||||||
|
?MODULE, caps_stream_features, 75),
|
||||||
|
ejabberd_hooks:delete(disco_local_features, HostB,
|
||||||
|
?MODULE, disco_features, 75),
|
||||||
|
ejabberd_hooks:delete(disco_local_identity, HostB,
|
||||||
|
?MODULE, disco_identity, 75),
|
||||||
|
ejabberd_hooks:delete(disco_info, HostB,
|
||||||
|
?MODULE, disco_info, 75),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
@ -229,3 +289,104 @@ node_to_binary(Node, SubNode) ->
|
|||||||
|
|
||||||
features_to_binary(L) -> [list_to_binary(I) || I <- L].
|
features_to_binary(L) -> [list_to_binary(I) || I <- L].
|
||||||
binary_to_features(L) -> [binary_to_list(I) || I <- L].
|
binary_to_features(L) -> [binary_to_list(I) || I <- L].
|
||||||
|
|
||||||
|
make_my_disco_hash(Host) ->
|
||||||
|
JID = exmpp_jid:make(Host),
|
||||||
|
case {ejabberd_hooks:run_fold(disco_local_features,
|
||||||
|
Host,
|
||||||
|
empty,
|
||||||
|
[JID, JID, <<>>, <<>>]),
|
||||||
|
ejabberd_hooks:run_fold(disco_local_identity,
|
||||||
|
Host,
|
||||||
|
[],
|
||||||
|
[JID, JID, <<>>, <<>>]),
|
||||||
|
ejabberd_hooks:run_fold(disco_info,
|
||||||
|
Host,
|
||||||
|
[],
|
||||||
|
[Host, undefined, <<>>, <<>>])} of
|
||||||
|
{{result, Features}, Identities, Info} ->
|
||||||
|
Feats = lists:map(
|
||||||
|
fun({{Feat, _Host}}) ->
|
||||||
|
#xmlel{name = feature,
|
||||||
|
attrs = [?XMLATTR(var, Feat)]};
|
||||||
|
(Feat) ->
|
||||||
|
#xmlel{name = feature,
|
||||||
|
attrs = [?XMLATTR(var, Feat)]}
|
||||||
|
end, Features),
|
||||||
|
make_disco_hash(Identities ++ Info ++ Feats, sha1);
|
||||||
|
_Err ->
|
||||||
|
""
|
||||||
|
end.
|
||||||
|
|
||||||
|
make_disco_hash(DiscoEls, Algo) when Algo == sha1; Algo == md5 ->
|
||||||
|
Concat = [concat_identities(DiscoEls),
|
||||||
|
concat_features(DiscoEls),
|
||||||
|
concat_info(DiscoEls)],
|
||||||
|
base64:encode_to_string(
|
||||||
|
if Algo == sha1 ->
|
||||||
|
crypto:sha(Concat);
|
||||||
|
Algo == md5 ->
|
||||||
|
crypto:md5(Concat)
|
||||||
|
end).
|
||||||
|
|
||||||
|
concat_features(Els) ->
|
||||||
|
lists:usort(
|
||||||
|
lists:flatmap(
|
||||||
|
fun(#xmlel{name = feature} = El) ->
|
||||||
|
[[exmpp_xml:get_attribute(El, var, <<>>), $<]];
|
||||||
|
(_) ->
|
||||||
|
[]
|
||||||
|
end, Els)).
|
||||||
|
|
||||||
|
concat_identities(Els) ->
|
||||||
|
lists:sort(
|
||||||
|
lists:flatmap(
|
||||||
|
fun(#xmlel{name = identity} = El) ->
|
||||||
|
[[exmpp_xml:get_attribute_as_binary(El, category, <<>>), $/,
|
||||||
|
exmpp_xml:get_attribute_as_binary(El, type, <<>>), $/,
|
||||||
|
exmpp_xml:get_attribute_as_binary(El, lang, <<>>), $/,
|
||||||
|
exmpp_xml:get_attribute_as_binary(El, name, <<>>), $<]];
|
||||||
|
(_) ->
|
||||||
|
[]
|
||||||
|
end, Els)).
|
||||||
|
|
||||||
|
concat_info(Els) ->
|
||||||
|
lists:sort(
|
||||||
|
lists:flatmap(
|
||||||
|
fun(#xmlel{name = x, ns = ?NS_DATA_FORMS, children = Fields} = El) ->
|
||||||
|
case exmpp_xml:get_attribute_as_list(El, 'type', "") of
|
||||||
|
"result" ->
|
||||||
|
[concat_xdata_fields(Fields)];
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
end;
|
||||||
|
(_) ->
|
||||||
|
[]
|
||||||
|
end, Els)).
|
||||||
|
|
||||||
|
concat_xdata_fields(Fields) ->
|
||||||
|
[Form, Res] =
|
||||||
|
lists:foldl(
|
||||||
|
fun(#xmlel{name = field, children = Els} = El,
|
||||||
|
[FormType, VarFields] = Acc) ->
|
||||||
|
case exmpp_xml:get_attribute_as_binary(El, var, <<>>) of
|
||||||
|
<<>> ->
|
||||||
|
Acc;
|
||||||
|
<<"FORM_TYPE">> ->
|
||||||
|
[exmpp_xml:get_path(
|
||||||
|
El, [{element, value}, cdata]), VarFields];
|
||||||
|
Var ->
|
||||||
|
[FormType,
|
||||||
|
[[[Var, $<],
|
||||||
|
lists:sort(
|
||||||
|
lists:flatmap(
|
||||||
|
fun(#xmlel{name = value} = VEl) ->
|
||||||
|
[[exmpp_xml:get_cdata(VEl), $<]];
|
||||||
|
(_) ->
|
||||||
|
[]
|
||||||
|
end, Els))] | VarFields]]
|
||||||
|
end;
|
||||||
|
(_, Acc) ->
|
||||||
|
Acc
|
||||||
|
end, [<<>>, []], Fields),
|
||||||
|
[Form, $<, lists:sort(Res)].
|
||||||
|
@ -164,9 +164,8 @@ process_local_iq_info(From, To, #iq{type = get, payload = SubEl,
|
|||||||
_ -> [?XMLATTR('node', Node)]
|
_ -> [?XMLATTR('node', Node)]
|
||||||
end,
|
end,
|
||||||
Result = #xmlel{ns = ?NS_DISCO_INFO, name = 'query',
|
Result = #xmlel{ns = ?NS_DISCO_INFO, name = 'query',
|
||||||
attrs = ANode,
|
attrs = ANode,
|
||||||
children = Identity ++ Info ++ lists:map(fun feature_to_xml/1,
|
children = Identity ++ Info ++ features_to_xml(Features)},
|
||||||
Features)},
|
|
||||||
exmpp_iq:result(IQ_Rec, Result);
|
exmpp_iq:result(IQ_Rec, Result);
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
exmpp_iq:error(IQ_Rec, Error)
|
exmpp_iq:error(IQ_Rec, Error)
|
||||||
@ -204,19 +203,24 @@ get_local_features(Acc, _From, _To, _Node, _Lang) ->
|
|||||||
{error, 'item-not-found'}
|
{error, 'item-not-found'}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
features_to_xml(FeatureList) ->
|
||||||
feature_to_xml({{Feature, _Host}}) ->
|
%% Avoid duplicating features
|
||||||
feature_to_xml(Feature);
|
[#xmlel{ns = ?NS_DISCO_INFO, name = 'feature',
|
||||||
|
attrs = [?XMLATTR('var', Feat)]} ||
|
||||||
feature_to_xml(Feature) when is_binary(Feature) ->
|
Feat <- lists:usort(
|
||||||
#xmlel{ns = ?NS_DISCO_INFO, name = 'feature', attrs = [
|
lists:map(
|
||||||
?XMLATTR('var', Feature)
|
fun({{Feature, _Host}}) ->
|
||||||
]};
|
feature_to_xml(Feature);
|
||||||
|
(Feature) ->
|
||||||
|
feature_to_xml(Feature)
|
||||||
|
end, FeatureList))].
|
||||||
|
|
||||||
feature_to_xml(Feature) when is_list(Feature) ->
|
feature_to_xml(Feature) when is_list(Feature) ->
|
||||||
feature_to_xml(list_to_binary(Feature));
|
feature_to_xml(list_to_binary(Feature));
|
||||||
feature_to_xml(Feature) when is_atom(Feature) ->
|
feature_to_xml(Feature) when is_atom(Feature) ->
|
||||||
feature_to_xml(atom_to_list(Feature)).
|
feature_to_xml(atom_to_list(Feature));
|
||||||
|
feature_to_xml(Feature) when is_binary(Feature) ->
|
||||||
|
Feature.
|
||||||
|
|
||||||
domain_to_xml({Domain}) ->
|
domain_to_xml({Domain}) ->
|
||||||
domain_to_xml(Domain);
|
domain_to_xml(Domain);
|
||||||
@ -364,9 +368,8 @@ process_sm_iq_info(From, To, #iq{type = get, payload = SubEl,
|
|||||||
_ -> [?XMLATTR('node', Node)]
|
_ -> [?XMLATTR('node', Node)]
|
||||||
end,
|
end,
|
||||||
Result = #xmlel{ns = ?NS_DISCO_INFO, name = 'query',
|
Result = #xmlel{ns = ?NS_DISCO_INFO, name = 'query',
|
||||||
attrs = ANode,
|
attrs = ANode,
|
||||||
children = Identity ++ lists:map(fun feature_to_xml/1,
|
children = Identity ++ features_to_xml(Features)},
|
||||||
Features)},
|
|
||||||
exmpp_iq:result(IQ_Rec, Result);
|
exmpp_iq:result(IQ_Rec, Result);
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
exmpp_iq:error(IQ_Rec, Error)
|
exmpp_iq:error(IQ_Rec, Error)
|
||||||
@ -421,7 +424,11 @@ get_user_resources(JID) ->
|
|||||||
|
|
||||||
%%% Support for: XEP-0157 Contact Addresses for XMPP Services
|
%%% Support for: XEP-0157 Contact Addresses for XMPP Services
|
||||||
|
|
||||||
get_info(Acc, Host, Module, Node, _Lang) when Node == <<>> ->
|
get_info(Acc, Host, Mod, Node, _Lang) when Node == <<>> ->
|
||||||
|
Module = case Mod of
|
||||||
|
undefined -> ?MODULE;
|
||||||
|
_ -> Mod
|
||||||
|
end,
|
||||||
Serverinfo_fields = get_fields_xml(Host, Module),
|
Serverinfo_fields = get_fields_xml(Host, Module),
|
||||||
CData1 = #xmlcdata{cdata = list_to_binary(?NS_SERVERINFO_s)},
|
CData1 = #xmlcdata{cdata = list_to_binary(?NS_SERVERINFO_s)},
|
||||||
Value1 = #xmlel{name = 'value', children = [CData1]},
|
Value1 = #xmlel{name = 'value', children = [CData1]},
|
||||||
@ -437,8 +444,8 @@ get_info(Acc, Host, Module, Node, _Lang) when Node == <<>> ->
|
|||||||
},
|
},
|
||||||
[X | Acc];
|
[X | Acc];
|
||||||
|
|
||||||
get_info(_, _, _, _Node, _) ->
|
get_info(Acc, _, _, _Node, _) ->
|
||||||
[].
|
Acc.
|
||||||
|
|
||||||
get_fields_xml(Host, Module) ->
|
get_fields_xml(Host, Module) ->
|
||||||
Fields = gen_mod:get_module_opt(Host, ?MODULE, server_info, []),
|
Fields = gen_mod:get_module_opt(Host, ?MODULE, server_info, []),
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
-export([start/2,
|
-export([start/2,
|
||||||
stop/1,
|
stop/1,
|
||||||
stream_feature_register/1,
|
stream_feature_register/2,
|
||||||
unauthenticated_iq_register/4,
|
unauthenticated_iq_register/4,
|
||||||
try_register/5,
|
try_register/5,
|
||||||
process_iq/3]).
|
process_iq/3]).
|
||||||
@ -68,7 +68,7 @@ stop(Host) ->
|
|||||||
gen_iq_handler:remove_iq_handler(ejabberd_sm, HostB, ?NS_INBAND_REGISTER).
|
gen_iq_handler:remove_iq_handler(ejabberd_sm, HostB, ?NS_INBAND_REGISTER).
|
||||||
|
|
||||||
|
|
||||||
stream_feature_register(Acc) ->
|
stream_feature_register(Acc, _Host) ->
|
||||||
[#xmlel{ns = ?NS_INBAND_REGISTER_FEAT, name = 'register'} | Acc].
|
[#xmlel{ns = ?NS_INBAND_REGISTER_FEAT, name = 'register'} | Acc].
|
||||||
|
|
||||||
unauthenticated_iq_register(_Acc,
|
unauthenticated_iq_register(_Acc,
|
||||||
|
Loading…
Reference in New Issue
Block a user