aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAlexey Shchepin <alexey@process-one.net>2021-10-29 05:12:26 +0300
committerAlexey Shchepin <alexey@process-one.net>2021-10-29 05:35:16 +0300
commit5d0e599f1784d7529dcd365ad8c3dd46c1ac85ad (patch)
treebb43112ed91eeaf71b682491243cd9b981613189 /src
parentRemove obsolete cookie preparation in spool dir, it's now stored in HOME (diff)
Support MUC hats (XEP-0317, conversejs/prosody compatible)
Diffstat (limited to 'src')
-rw-r--r--src/mod_muc_room.erl420
1 files changed, 414 insertions, 6 deletions
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index 035e851fd..3f2c655df 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -76,6 +76,12 @@
-define(DEFAULT_MAX_USERS_PRESENCE,1000).
+-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>).
+-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>).
+-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>).
+-define(MAX_HATS_USERS, 100).
+-define(MAX_HATS_PER_USER, 10).
+
%-define(DBGFSM, true).
-ifdef(DBGFSM).
@@ -446,6 +452,8 @@ normal_state({route, <<"">>,
process_iq_mucsub(From, IQ, StateData);
#xcaptcha{} ->
process_iq_captcha(From, IQ, StateData);
+ #adhoc_command{} ->
+ process_iq_adhoc(From, IQ, StateData);
_ ->
Txt = ?T("The feature requested is not "
"supported by the conference"),
@@ -1405,6 +1413,12 @@ is_occupant_or_admin(JID, StateData) ->
_ -> false
end.
+%% Check if the user is an admin or owner.
+-spec is_admin(jid(), state()) -> boolean().
+is_admin(JID, StateData) ->
+ FAffiliation = get_affiliation(JID, StateData),
+ FAffiliation == admin orelse FAffiliation == owner.
+
%% Decide the fate of the message and its sender
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
-spec decide_fate_message(message(), jid(), state()) ->
@@ -1935,7 +1949,7 @@ filter_presence(Presence) ->
XMLNS = xmpp:get_ns(El),
case catch binary:part(XMLNS, 0, size(?NS_MUC)) of
?NS_MUC -> false;
- _ -> true
+ _ -> XMLNS /= ?NS_HATS
end
end, xmpp:get_els(Presence)),
xmpp:set_els(Presence, Els).
@@ -2485,9 +2499,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
Pres = if Presence == undefined -> #presence{};
true -> Presence
end,
- Packet = xmpp:set_subtag(
- Pres, #muc_user{items = [Item],
- status_codes = StatusCodes}),
+ Packet = xmpp:set_subtag(
+ add_presence_hats(NJID, Pres, StateData),
+ #muc_user{items = [Item],
+ status_codes = StatusCodes}),
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
@@ -2536,7 +2551,9 @@ send_existing_presences1(ToJID, StateData) ->
false -> Item0
end,
Packet = xmpp:set_subtag(
- Presence, #muc_user{items = [Item]}),
+ add_presence_hats(
+ FromJID, Presence, StateData),
+ #muc_user{items = [Item]}),
send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),
RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)
end
@@ -3579,7 +3596,8 @@ get_config(Lang, StateData, From) ->
{allow_voice_requests, Config#config.allow_voice_requests},
{allow_subscription, Config#config.allow_subscription},
{voice_request_min_interval, Config#config.voice_request_min_interval},
- {pubsub, Config#config.pubsub}]
+ {pubsub, Config#config.pubsub},
+ {enable_hats, Config#config.enable_hats}]
++
case ejabberd_captcha:is_feature_available() of
true ->
@@ -3667,6 +3685,7 @@ set_config(Opts, Config, ServerHost, Lang) ->
({maxusers, V}, C) -> C#config{max_users = V};
({enablelogging, V}, C) -> C#config{logging = V};
({pubsub, V}, C) -> C#config{pubsub = V};
+ ({enable_hats, V}, C) -> C#config{enable_hats = V};
({lang, L}, C) -> C#config{lang = L};
({captcha_whitelist, Js}, C) ->
LJIDs = [jid:tolower(J) || J <- Js],
@@ -3897,6 +3916,9 @@ set_opts([{Opt, Val} | Opts], StateData) ->
allow_subscription ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
+ enable_hats ->
+ StateData#state{config =
+ (StateData#state.config)#config{enable_hats = Val}};
lang ->
StateData#state{config =
(StateData#state.config)#config{lang = Val}};
@@ -3927,6 +3949,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
end,
StateData#state{subject = Subj};
subject_author -> StateData#state{subject_author = Val};
+ hats_users ->
+ Hats = maps:from_list(
+ lists:map(fun({U, H}) -> {U, maps:from_list(H)} end,
+ Val)),
+ StateData#state{hats_users = Hats};
_ -> StateData
end,
set_opts(Opts, NSD).
@@ -3983,6 +4010,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.vcard),
?MAKE_CONFIG_OPT(#config.vcard_xupdate),
?MAKE_CONFIG_OPT(#config.pubsub),
+ ?MAKE_CONFIG_OPT(#config.enable_hats),
?MAKE_CONFIG_OPT(#config.lang),
{captcha_whitelist,
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
@@ -3990,6 +4018,9 @@ make_opts(StateData) ->
maps:to_list(StateData#state.affiliations)},
{subject, StateData#state.subject},
{subject_author, StateData#state.subject_author},
+ {hats_users,
+ lists:map(fun({U, H}) -> {U, maps:to_list(H)} end,
+ maps:to_list(StateData#state.hats_users))},
{hibernation_time, erlang:system_time(microsecond)},
{subscribers, Subscribers}].
@@ -4080,6 +4111,7 @@ maybe_forget_room(StateData) ->
make_disco_info(_From, StateData) ->
Config = StateData#state.config,
Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
+ ?NS_COMMANDS,
?CONFIG_OPT_TO_FEATURE((Config#config.public),
<<"muc_public">>, <<"muc_hidden">>),
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
@@ -4120,6 +4152,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
Extras = iq_disco_info_extras(Lang, StateData, false),
{result, DiscoInfo#disco_info{xdata = [Extras]}};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-list">>,
+ name = translate:translate(
+ Lang, ?T("Commands"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
sub_els = [#disco_info{node = Node}]},
StateData) ->
try
@@ -4199,6 +4302,46 @@ process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>
{result, #disco_items{}}
end
end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_items{
+ items = [#disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_ADD_CMD,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_REMOVE_CMD,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_LIST_CMD,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = Node}]},
+ StateData)
+ when Node == ?MUC_HAT_ADD_CMD;
+ Node == ?MUC_HAT_REMOVE_CMD;
+ Node == ?MUC_HAT_LIST_CMD ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result, #disco_items{}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) ->
Txt = ?T("Node not found"),
{error, xmpp:err_item_not_found(Txt, Lang)}.
@@ -4441,6 +4584,271 @@ get_mucroom_disco_items(StateData) ->
end, [], StateData#state.nicks),
#disco_items{items = Items}.
+-spec process_iq_adhoc(jid(), iq(), state()) ->
+ {result, adhoc_command()} |
+ {result, adhoc_command(), state()} |
+ {error, stanza_error()}.
+process_iq_adhoc(_From, #iq{type = get}, _StateData) ->
+ {error, xmpp:err_bad_request()};
+process_iq_adhoc(From, #iq{type = set, lang = Lang1,
+ sub_els = [#adhoc_command{} = Request]},
+ StateData) ->
+ % Ad-Hoc Commands are used only for Hats here
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ #adhoc_command{lang = Lang2, node = Node,
+ action = Action, xdata = XData} = Request,
+ Lang = case Lang2 of
+ <<"">> -> Lang1;
+ _ -> Lang2
+ end,
+ case {Node, Action} of
+ {_, cancel} ->
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = canceled, lang = Lang,
+ node = Node})};
+ {?MUC_HAT_ADD_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Add a hat to a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_ADD_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ Title = case xmpp_util:get_xdata_values(
+ <<"hat_title">>, XData) of
+ [] -> <<"">>;
+ [T] -> T
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ case add_hat(JID, URI, Title, StateData) of
+ {ok, NewStateData} ->
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ {error, size_limit} ->
+ Txt = ?T("Hats limit exceeded"),
+ {error, xmpp:err_not_allowed(Txt, Lang)}
+ end;
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_ADD_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_ADD_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_REMOVE_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Remove a hat from a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ NewStateData = del_hat(JID, URI, StateData),
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_REMOVE_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_REMOVE_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_LIST_CMD, execute} ->
+ Hats = get_all_hats(StateData),
+ Items =
+ lists:map(
+ fun({JID, URI, Title}) ->
+ [#xdata_field{
+ var = <<"jid">>,
+ values = [jid:encode(JID)]},
+ #xdata_field{
+ var = <<"hat_title">>,
+ values = [URI]},
+ #xdata_field{
+ var = <<"hat_uri">>,
+ values = [Title]}]
+ end, Hats),
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("List of users with hats")),
+ type = result,
+ reported =
+ [#xdata_field{
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ var = <<"jid">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat URI")),
+ var = <<"hat_uri">>}],
+ items = Items},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = completed,
+ xdata = Form})};
+ {?MUC_HAT_LIST_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ _ ->
+ {error, xmpp:err_item_not_found()}
+ end;
+ _ ->
+ {error, xmpp:err_forbidden()}
+ end.
+
+-spec add_hat(jid(), binary(), binary(), state()) ->
+ {ok, state()} | {error, size_limit}.
+add_hat(JID, URI, Title, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:put(URI, Title, UserHats),
+ USize = maps:size(UserHats2),
+ if
+ USize =< ?MAX_HATS_PER_USER ->
+ Hats2 = maps:put(LJID, UserHats2, Hats),
+ Size = maps:size(Hats2),
+ if
+ Size =< ?MAX_HATS_USERS ->
+ {ok, StateData#state{hats_users = Hats2}};
+ true ->
+ {error, size_limit}
+ end;
+ true ->
+ {error, size_limit}
+ end.
+
+-spec del_hat(jid(), binary(), state()) -> state().
+del_hat(JID, URI, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:remove(URI, UserHats),
+ Hats2 =
+ case maps:size(UserHats2) of
+ 0 ->
+ maps:remove(LJID, Hats);
+ _ ->
+ maps:put(LJID, UserHats2, Hats)
+ end,
+ StateData#state{hats_users = Hats2}.
+
+-spec get_all_hats(state()) -> list({jid(), binary(), binary()}).
+get_all_hats(StateData) ->
+ lists:flatmap(
+ fun({LJID, H}) ->
+ JID = jid:make(LJID),
+ lists:map(fun({URI, Title}) -> {JID, URI, Title} end,
+ maps:to_list(H))
+ end,
+ maps:to_list(StateData#state.hats_users)).
+
+-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}.
+add_presence_hats(JID, Pres, StateData) ->
+ case (StateData#state.config)#config.enable_hats of
+ true ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ case maps:size(UserHats) of
+ 0 -> Pres;
+ _ ->
+ Items =
+ lists:map(fun({URI, Title}) ->
+ #muc_hat{uri = URI, title = Title}
+ end,
+ maps:to_list(UserHats)),
+ xmpp:set_subtag(Pres,
+ #muc_hats{hats = Items})
+ end;
+ false ->
+ Pres
+ end.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support