1
0
Fork 0
mirror of https://github.com/processone/ejabberd synced 2025-10-02 17:29:27 +02:00

Update implementation of XEP-0317 Hats to version 0.3.1 (#4380)

This commit is contained in:
Badlop 2025-09-08 19:07:30 +02:00
parent c3a24ffdf8
commit 2b7285e0b2
9 changed files with 511 additions and 302 deletions

View file

@ -578,10 +578,10 @@
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0317.html"/>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:version>0.3.1</xmpp:version>
<xmpp:since>21.12</xmpp:since>
<xmpp:status>complete</xmpp:status>
<xmpp:note>mod_muc_room, 0.2.0 since 25.03</xmpp:note>
<xmpp:note>mod_muc_room, 0.3.1 since 25.xx</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>

View file

@ -126,7 +126,8 @@
history = #lqueue{} :: lqueue(),
subject = [] :: [text()],
subject_author = {<<"">>, #jid{}} :: {binary(), jid()},
hats_users = #{} :: #{ljid() => #{binary() => binary()}},
hats_defs = #{} :: #{binary() => {binary(), binary()}},
hats_users = #{} :: #{ljid() => [binary()]},
just_created = erlang:system_time(microsecond) :: true | integer(),
activity = treap:empty() :: treap:treap(),
room_shaper = none :: ejabberd_shaper:shaper(),

View file

@ -129,7 +129,7 @@ defmodule Ejabberd.MixProject do
{:p1_utils, "~> 1.0"},
{:pkix, "~> 1.0"},
{:stringprep, ">= 1.0.26"},
{:xmpp, ">= 1.11.1"},
{:xmpp, git: "https://github.com/processone/xmpp", ref: "e9d901ea84fd3910ad32b715853397eb1155b41c", override: true},
{:yconf, git: "https://github.com/processone/yconf", ref: "95692795a8a8d950ba560e5b07e6b80660557259", override: true}]
++ cond_deps()
end

View file

@ -34,6 +34,6 @@
"stringprep": {:hex, :stringprep, "1.0.33", "22f42866b4f6f3c238ea2b9cb6241791184ddedbab55e94a025511f46325f3ca", [:rebar3], [{:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "96f8b30bc50887f605b33b46bca1d248c19a879319b8c482790e3b4da5da98c0"},
"stun": {:hex, :stun, "1.2.21", "735855314ad22cb7816b88597d2f5ca22e24aa5e4d6010a0ef3affb33ceed6a5", [:rebar3], [{:fast_tls, "1.1.25", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.28", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "3d7fe8efb9d05b240a6aa9a6bf8b8b7bff2d802895d170443c588987dc1e12d9"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"xmpp": {:hex, :xmpp, "1.11.1", "60181e7d3e8e48aa3b23b2792075cda37e2e507ec152490b866e61e5320cb1da", [:rebar3], [{:ezlib, "~> 1.0.12", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "~> 1.1.19", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "~> 1.1.51", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "~> 1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "~> 1.0.29", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "a5c933df904ab3cec15425da334e410ce84ec3ae7b81efe069e5db368a7b3716"},
"xmpp": {:git, "https://github.com/processone/xmpp", "e9d901ea84fd3910ad32b715853397eb1155b41c", [ref: "e9d901ea84fd3910ad32b715853397eb1155b41c"]},
"yconf": {:git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259", [ref: "95692795a8a8d950ba560e5b07e6b80660557259"]},
}

View file

@ -77,7 +77,7 @@
{stringprep, "~> 1.0.33", {git, "https://github.com/processone/stringprep", {tag, "1.0.33"}}},
{if_var_true, stun,
{stun, "~> 1.2.21", {git, "https://github.com/processone/stun", {tag, "1.2.21"}}}},
{xmpp, "~> 1.11.1", {git, "https://github.com/processone/xmpp", {tag, "1.11.1"}}},
{xmpp, ".*", {git, "https://github.com/processone/xmpp", "e9d901ea84fd3910ad32b715853397eb1155b41c"}},
{yconf, ".*", {git, "https://github.com/processone/yconf", "95692795a8a8d950ba560e5b07e6b80660557259"}}
]}.

View file

@ -24,7 +24,10 @@
{<<"stringprep">>,{pkg,<<"stringprep">>,<<"1.0.33">>},0},
{<<"stun">>,{pkg,<<"stun">>,<<"1.2.21">>},0},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.1">>},1},
{<<"xmpp">>,{pkg,<<"xmpp">>,<<"1.11.1">>},0},
{<<"xmpp">>,
{git,"https://github.com/processone/xmpp",
{ref,"e9d901ea84fd3910ad32b715853397eb1155b41c"}},
0},
{<<"yconf">>,
{git,"https://github.com/processone/yconf",
{ref,"95692795a8a8d950ba560e5b07e6b80660557259"}},
@ -55,8 +58,7 @@
{<<"sqlite3">>, <<"E819DEFD280145C328457D7AF897D2E45E8E5270E18812EE30B607C99CDD21AF">>},
{<<"stringprep">>, <<"22F42866B4F6F3C238EA2B9CB6241791184DDEDBAB55E94A025511F46325F3CA">>},
{<<"stun">>, <<"735855314AD22CB7816B88597D2F5CA22E24AA5E4D6010A0EF3AFFB33CEED6A5">>},
{<<"unicode_util_compat">>, <<"A48703A25C170EEDADCA83B11E88985AF08D35F37C6F664D6DCFB106A97782FC">>},
{<<"xmpp">>, <<"60181E7D3E8E48AA3B23B2792075CDA37E2E507EC152490B866E61E5320CB1DA">>}]},
{<<"unicode_util_compat">>, <<"A48703A25C170EEDADCA83B11E88985AF08D35F37C6F664D6DCFB106A97782FC">>}]},
{pkg_hash_ext,[
{<<"base64url">>, <<"F9B3ADD4731A02A9B0410398B475B33E7566A695365237A6BDEE1BB447719F5C">>},
{<<"cache_tab">>, <<"4258009EB050B22AABE0C848E230BBA58401A6895C58C2FF74DFB635E3C35900">>},
@ -82,6 +84,5 @@
{<<"sqlite3">>, <<"3C0BA4E13322C2AD49DE4E2DDD28311366ADDE54BEAE8DBA9D9E3888F69D2857">>},
{<<"stringprep">>, <<"96F8B30BC50887F605B33B46BCA1D248C19A879319B8C482790E3B4DA5DA98C0">>},
{<<"stun">>, <<"3D7FE8EFB9D05B240A6AA9A6BF8B8B7BFF2D802895D170443C588987DC1E12D9">>},
{<<"unicode_util_compat">>, <<"B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642">>},
{<<"xmpp">>, <<"A5C933DF904AB3CEC15425DA334E410CE84EC3AE7B81EFE069E5DB368A7B3716">>}]}
{<<"unicode_util_compat">>, <<"B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642">>}]}
].

View file

@ -1766,7 +1766,7 @@ mod_doc() ->
"The default value is an empty string.")}},
{enable_hats,
#{value => "true | false",
note => "improved in 25.03",
note => "improved in 25.xx",
desc =>
?T("Allow extended roles as defined in XEP-0317 Hats. "
"Check the _`../../tutorials/muc-hats.md|MUC Hats`_ tutorial. "

View file

@ -429,11 +429,15 @@ need_transform({muc_room, {N, H}, _})
?INFO_MSG("Mnesia table 'muc_room' will be converted to binary", []),
true;
need_transform({muc_room, {_N, _H}, Opts}) ->
case lists:keymember(allow_private_messages, 1, Opts) of
true ->
case {lists:keymember(allow_private_messages, 1, Opts),
lists:keymember(hats_defs, 1, Opts)} of
{true, _} ->
?INFO_MSG("Mnesia table 'muc_room' will be converted to allowpm", []),
true;
false ->
{false, false} ->
?INFO_MSG("Mnesia table 'muc_room' will be converted to Hats 0.3.0", []),
true;
{false, true} ->
false
end;
@ -459,7 +463,33 @@ transform(#muc_room{opts = Opts} = R) ->
_ ->
Opts
end,
R#muc_room{opts = Opts2};
Opts4 =
case lists:keyfind(hats_defs, 1, Opts2) of
false ->
{hats_users, HatsUsers} = lists:keyfind(hats_users, 1, Opts2),
{HatsDefs, HatsUsers2} =
lists:foldl(fun({Jid, UriTitleList}, {Defs, Assigns}) ->
Defs2 =
lists:foldl(fun({Uri, Title}, AccDef) ->
maps:put(Uri, {Title, <<"">>}, AccDef)
end,
Defs,
UriTitleList),
Assigns2 =
maps:put(Jid,
[Uri || {Uri, _Title} <- UriTitleList],
Assigns),
{Defs2, Assigns2}
end,
{maps:new(), maps:new()},
HatsUsers),
Opts3 =
lists:keyreplace(hats_users, 1, Opts2, {hats_users, maps:to_list(HatsUsers2)}),
[{hats_defs, maps:to_list(HatsDefs)} | Opts3];
{_, _} ->
Opts2
end,
R#muc_room{opts = Opts4};
transform(#muc_registered{us_host = {{U, S}, H}, nick = Nick} = R) ->
R#muc_registered{us_host = {{iolist_to_binary(U), iolist_to_binary(S)},
iolist_to_binary(H)},

View file

@ -27,7 +27,7 @@
-author('alexey@process-one.net').
-protocol({xep, 317, '0.2.0', '21.12', "complete", "0.2.0 since 25.03"}).
-protocol({xep, 317, '0.3.1', '21.12', "complete", "0.3.1 since 25.xx"}).
-protocol({xep, 410, '1.1.0', '18.12', "complete", ""}).
-behaviour(p1_fsm).
@ -79,9 +79,14 @@
-define(MAX_USERS_DEFAULT_LIST,
[5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
-define(MUC_HAT_ADD_CMD, <<"urn:xmpp:hats:commands:don">>).
-define(MUC_HAT_REMOVE_CMD, <<"urn:xmpp:hats:commands:doff">>).
-define(MUC_HAT_LIST_CMD, <<"urn:xmpp:hats:commands:dlist">>).
-define(MUC_HAT_CREATE_CMD, <<"urn:xmpp:hats:commands:create">>).
-define(MUC_HAT_DESTROY_CMD, <<"urn:xmpp:hats:commands:destroy">>).
-define(MUC_HAT_LISTHATS_CMD, <<"urn:xmpp:hats:commands:list">>).
-define(MUC_HAT_ASSIGN_CMD, <<"urn:xmpp:hats:commands:assign">>).
-define(MUC_HAT_UNASSIGN_CMD, <<"urn:xmpp:hats:commands:unassign">>).
-define(MUC_HAT_LISTUSERS_CMD,<<"urn:xmpp:hats:commands:list-assigned">>).
-define(MAX_HATS_USERS, 100).
-define(MAX_HATS_PER_USER, 10).
-define(CLEAN_ROOM_TIMEOUT, 30000).
@ -4258,11 +4263,10 @@ set_opts2([{Opt, Val} | Opts], StateData) ->
StateData#state{subject_author = Val};
subject_author when is_binary(Val) -> % ejabberd 23.04 or older
StateData#state{subject_author = {Val, #jid{}}};
hats_defs ->
StateData#state{hats_defs = maps:from_list(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#state{hats_users = maps:from_list(Val)};
hibernation_time -> StateData;
Other ->
?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]),
@ -4343,9 +4347,8 @@ make_opts(StateData, Hibernation) ->
{roles, maps:to_list(StateData#state.roles)},
{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))},
{hats_defs, maps:to_list(StateData#state.hats_defs)},
{hats_users, maps:to_list(StateData#state.hats_users)},
{hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end},
{subscribers, Subscribers}].
@ -4461,6 +4464,10 @@ make_disco_info(From, StateData) ->
true -> [?NS_MUCSUB];
false -> []
end
++ case Config#config.enable_hats of
true -> [?NS_HATS];
false -> []
end
++ case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of
true ->
[?NS_OCCUPANT_ID];
@ -4490,6 +4497,7 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
DiscoInfo = make_disco_info(From, StateData),
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) ->
@ -4507,9 +4515,25 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
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) ->
sub_els = [#disco_info{node = Node}]},
StateData)
when Node == ?MUC_HAT_CREATE_CMD;
Node == ?MUC_HAT_DESTROY_CMD;
Node == ?MUC_HAT_LISTHATS_CMD;
Node == ?MUC_HAT_ASSIGN_CMD;
Node == ?MUC_HAT_UNASSIGN_CMD;
Node == ?MUC_HAT_LISTUSERS_CMD ->
NodeName = case Node of
?MUC_HAT_CREATE_CMD -> ?T("Create a Hat");
?MUC_HAT_DESTROY_CMD -> ?T("Destroy a Hat");
?MUC_HAT_LISTHATS_CMD -> ?T("List Hats");
?MUC_HAT_ASSIGN_CMD -> ?T("Assign a Hat to a User");
?MUC_HAT_UNASSIGN_CMD -> ?T("Remove a Hat from a User");
?MUC_HAT_LISTUSERS_CMD -> ?T("List Users and their Hats")
end,
case (StateData#state.config)#config.enable_hats andalso
is_admin(From, StateData)
of
@ -4519,48 +4543,13 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
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"))}],
Lang, NodeName)}],
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) ->
@ -4623,8 +4612,15 @@ iq_disco_info_extras(Lang, StateData, Static) ->
StateData#state.server_host,
Fs5,
[StateData]),
Fs7 = case (StateData#state.config)#config.enable_hats of
true ->
HatsHash = get_hats_hash(StateData),
[{'hats#hash', [HatsHash]} | Fs6];
false ->
Fs6
end,
#xdata{type = result,
fields = muc_roominfo:encode(Fs6, Lang)}.
fields = muc_roominfo:encode(Fs7, Lang)}.
-spec process_iq_disco_items(jid(), iq(), state()) ->
{error, stanza_error()} | {result, disco_items()}.
@ -4657,15 +4653,27 @@ process_iq_disco_items(From, #iq{type = get, lang = Lang,
{result,
#disco_items{
items = [#disco_item{jid = StateData#state.jid,
node = ?MUC_HAT_ADD_CMD,
node = ?MUC_HAT_CREATE_CMD,
name = translate:translate(
Lang, ?T("Add a hat to a user"))},
Lang, ?T("Create a hat"))},
#disco_item{jid = StateData#state.jid,
node = ?MUC_HAT_REMOVE_CMD,
node = ?MUC_HAT_DESTROY_CMD,
name = translate:translate(
Lang, ?T("Destroy a hat"))},
#disco_item{jid = StateData#state.jid,
node = ?MUC_HAT_LISTHATS_CMD,
name = translate:translate(
Lang, ?T("List hats"))},
#disco_item{jid = StateData#state.jid,
node = ?MUC_HAT_ASSIGN_CMD,
name = translate:translate(
Lang, ?T("Assign a hat to a user"))},
#disco_item{jid = StateData#state.jid,
node = ?MUC_HAT_UNASSIGN_CMD,
name = translate:translate(
Lang, ?T("Remove a hat from a user"))},
#disco_item{jid = StateData#state.jid,
node = ?MUC_HAT_LIST_CMD,
node = ?MUC_HAT_LISTUSERS_CMD,
name = translate:translate(
Lang, ?T("List users with hats"))}]}};
false ->
@ -4675,9 +4683,12 @@ process_iq_disco_items(From, #iq{type = get, lang = Lang,
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 ->
when Node == ?MUC_HAT_CREATE_CMD;
Node == ?MUC_HAT_DESTROY_CMD;
Node == ?MUC_HAT_LISTHATS_CMD;
Node == ?MUC_HAT_ASSIGN_CMD;
Node == ?MUC_HAT_UNASSIGN_CMD;
Node == ?MUC_HAT_LISTUSERS_CMD ->
case (StateData#state.config)#config.enable_hats andalso
is_admin(From, StateData)
of
@ -4944,270 +4955,436 @@ get_mucroom_disco_items(StateData) ->
end, [], StateData#state.nicks),
#disco_items{items = Items}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Hats
%% @format-begin
-spec process_iq_adhoc(jid(), iq(), state()) ->
{result, adhoc_command()} |
{result, adhoc_command(), state()} |
{error, stanza_error()}.
{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) ->
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
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,
#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">>}
]},
xmpp_util:make_adhoc_response(Request,
#adhoc_command{status = canceled,
lang = Lang,
node = Node})};
{Node, execute}
when Node == ?MUC_HAT_CREATE_CMD;
Node == ?MUC_HAT_DESTROY_CMD;
Node == ?MUC_HAT_LISTHATS_CMD;
Node == ?MUC_HAT_ASSIGN_CMD;
Node == ?MUC_HAT_UNASSIGN_CMD;
Node == ?MUC_HAT_LISTUSERS_CMD ->
{Status, Form} = process_iq_adhoc_hats(Node, StateData, Lang),
{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),
xmpp_util:make_adhoc_response(Request,
#adhoc_command{status = Status, xdata = Form})};
{Node, complete}
when XData /= undefined andalso Node == ?MUC_HAT_CREATE_CMD;
Node == ?MUC_HAT_DESTROY_CMD;
Node == ?MUC_HAT_ASSIGN_CMD;
Node == ?MUC_HAT_UNASSIGN_CMD ->
case process_iq_adhoc_hats_complete(Node, XData, StateData, Lang) of
{ok, NewStateData} ->
{result,
xmpp_util:make_adhoc_response(
Request,
#adhoc_command{status = completed}),
xmpp_util:make_adhoc_response(Request,
#adhoc_command{status = completed}),
NewStateData};
true ->
{error, XmlElement} ->
{error, XmlElement};
error ->
{error, xmpp:err_bad_request()}
end;
{?MUC_HAT_REMOVE_CMD, complete} ->
{Node, complete}
when Node == ?MUC_HAT_CREATE_CMD;
Node == ?MUC_HAT_DESTROY_CMD;
Node == ?MUC_HAT_ASSIGN_CMD;
Node == ?MUC_HAT_UNASSIGN_CMD ->
{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, _} ->
{Node, _}
when Node == ?MUC_HAT_CREATE_CMD;
Node == ?MUC_HAT_DESTROY_CMD;
Node == ?MUC_HAT_LISTHATS_CMD;
Node == ?MUC_HAT_ASSIGN_CMD;
Node == ?MUC_HAT_UNASSIGN_CMD;
Node == ?MUC_HAT_LISTUSERS_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()}
_ ->
{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.
process_iq_adhoc_hats(?MUC_HAT_LISTHATS_CMD, StateData, Lang) ->
Hats = get_defined_hats(StateData),
Items =
lists:map(fun({URI, Title, Hue}) ->
[#xdata_field{var = <<"hats#uri">>, values = [URI]},
#xdata_field{var = <<"hats#title">>, values = [Title]},
#xdata_field{var = <<"hats#hue">>, values = [Hue]}]
end,
Hats),
Form =
#xdata{title = translate:translate(Lang, ?T("Hats List")),
type = result,
reported =
[#xdata_field{label = translate:translate(Lang, ?T("Hat URI")),
var = <<"hats#uri">>},
#xdata_field{label = translate:translate(Lang, ?T("Hat Title")),
var = <<"hats#title">>},
#xdata_field{label = translate:translate(Lang, ?T("Hat Hue")),
var = <<"hats#hue">>}],
items = Items},
{completed, Form};
process_iq_adhoc_hats(?MUC_HAT_CREATE_CMD, _StateData, Lang) ->
Form =
#xdata{title = translate:translate(Lang, ?T("Create a hat")),
type = form,
fields =
[#xdata_field{type = 'text-single',
label = translate:translate(Lang, ?T("Hat URI")),
required = true,
var = <<"hats#uri">>},
#xdata_field{type = 'text-single',
label = translate:translate(Lang, ?T("Hat Title")),
required = true,
var = <<"hats#title">>},
#xdata_field{type = 'text-single',
label = translate:translate(Lang, ?T("Hat Hue")),
var = <<"hats#hue">>}]},
{executing, Form};
process_iq_adhoc_hats(?MUC_HAT_DESTROY_CMD, _StateData, Lang) ->
Form =
#xdata{title = translate:translate(Lang, ?T("Destroy a hat")),
type = form,
fields =
[#xdata_field{type = 'text-single',
label = translate:translate(Lang, ?T("Hat URI")),
required = true,
var = <<"hat">>}]},
{executing, Form};
process_iq_adhoc_hats(?MUC_HAT_ASSIGN_CMD, StateData, Lang) ->
Hats = get_defined_hats(StateData),
Options =
[#xdata_option{label = Title, value = Uri}
|| {Uri, Title, _Hue} <- lists:keysort(2, Hats)],
Form =
#xdata{title = translate:translate(Lang, ?T("Assign a hat to a user")),
type = form,
fields =
[#xdata_field{type = 'jid-single',
label = translate:translate(Lang, ?T("Jabber ID")),
required = true,
var = <<"hats#jid">>},
#xdata_field{type = 'list-single',
label = translate:translate(Lang, ?T("The role")),
var = <<"hat">>,
options = Options}]},
{executing, Form};
process_iq_adhoc_hats(?MUC_HAT_UNASSIGN_CMD, StateData, Lang) ->
Hats = get_defined_hats(StateData),
Options =
[#xdata_option{label = Title, value = Uri}
|| {Uri, Title, _Hue} <- lists:keysort(2, Hats)],
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 = <<"hats#jid">>},
#xdata_field{type = 'list-single',
label = translate:translate(Lang, ?T("The role")),
var = <<"hat">>,
options = Options}]},
{executing, Form};
process_iq_adhoc_hats(?MUC_HAT_LISTUSERS_CMD, StateData, Lang) ->
Hats = get_assigned_hats(StateData),
Items =
lists:map(fun({JID, URI}) ->
{URI, Title, Hue} = get_hat_details(URI, StateData),
[#xdata_field{var = <<"hats#jid">>, values = [jid:encode(JID)]},
#xdata_field{var = <<"hats#uri">>, values = [URI]},
#xdata_field{var = <<"hats#title">>, values = [Title]},
#xdata_field{var = <<"hats#hue">>, values = [Hue]}]
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 = <<"hats#jid">>},
#xdata_field{label = translate:translate(Lang, ?T("Hat URI")),
var = <<"hats#uri">>},
#xdata_field{label = translate:translate(Lang, ?T("Hat Title")),
var = <<"hats#title">>},
#xdata_field{label = translate:translate(Lang, ?T("Hat Hue")),
var = <<"hats#hue">>}],
items = Items},
{completed, Form};
process_iq_adhoc_hats(_, _, _) ->
{executing, aaa}.
-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)
process_iq_adhoc_hats_complete(?MUC_HAT_CREATE_CMD, XData, StateData, _Lang) ->
URI = try
hd(xmpp_util:get_xdata_values(<<"hats#uri">>, XData))
catch
_:_ ->
error
end,
Title =
case xmpp_util:get_xdata_values(<<"hats#title">>, XData) of
[] ->
<<"">>;
[T] ->
T
end,
StateData#state{hats_users = Hats2}.
Hue = try
hd(xmpp_util:get_xdata_values(<<"hats#hue">>, XData))
catch
_:_ ->
error
end,
if (Title /= error) and (URI /= error) ->
{ok, AffectedJids, NewStateData} = create_hat(URI, Title, Hue, StateData),
store_room(NewStateData),
broadcast_hats_change(NewStateData),
[send_update_presence(AJid, NewStateData, StateData) || AJid <- AffectedJids],
{ok, NewStateData};
true ->
error
end;
process_iq_adhoc_hats_complete(?MUC_HAT_DESTROY_CMD, XData, StateData, _Lang) ->
URI = try
hd(xmpp_util:get_xdata_values(<<"hat">>, XData))
catch
_:_ ->
error
end,
if URI /= error ->
{ok, AffectedJids, NewStateData} = destroy_hat(URI, StateData),
store_room(NewStateData),
broadcast_hats_change(NewStateData),
[send_update_presence(AJid, NewStateData, StateData) || AJid <- AffectedJids],
{ok, NewStateData};
true ->
error
end;
process_iq_adhoc_hats_complete(?MUC_HAT_ASSIGN_CMD, XData, StateData, Lang) ->
JID = try
jid:decode(hd(xmpp_util:get_xdata_values(<<"hats#jid">>, XData)))
catch
_:_ ->
error
end,
URI = try
hd(xmpp_util:get_xdata_values(<<"hat">>, XData))
catch
_:_ ->
error
end,
if (JID /= error) and (URI /= error) ->
case assign_hat(JID, URI, StateData) of
{ok, NewStateData} ->
store_room(NewStateData),
send_update_presence(JID, NewStateData, StateData),
{ok, NewStateData};
{error, size_limit} ->
Txt = ?T("Hats limit exceeded"),
{error, xmpp:err_not_allowed(Txt, Lang)}
end;
true ->
error
end;
process_iq_adhoc_hats_complete(?MUC_HAT_UNASSIGN_CMD, XData, StateData, _Lang) ->
JID = try
jid:decode(hd(xmpp_util:get_xdata_values(<<"hats#jid">>, XData)))
catch
_:_ ->
error
end,
URI = try
hd(xmpp_util:get_xdata_values(<<"hat">>, XData))
catch
_:_ ->
error
end,
if (JID /= error) and (URI /= error) ->
{ok, NewStateData} = unassign_hat(JID, URI, StateData),
store_room(NewStateData),
send_update_presence(JID, NewStateData, StateData),
{ok, NewStateData};
true ->
error
end.
-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)).
%% TODO +++ clean
create_hat(URI, Title, Hue, #state{hats_defs = Hats, hats_users = Users} = StateData) ->
Hats2 = maps:put(URI, {Title, Hue}, Hats),
IsUpdate =
case maps:find(URI, Hats) of
{ok, {OldTitle, OldHue}} ->
(OldTitle /= Title) or (OldHue /= Hue);
error ->
false
end,
AffectedJids =
case IsUpdate of
true ->
maps:fold(fun(Jid, AssignedHatsUris, ChangedAcc) ->
case lists:member(URI, AssignedHatsUris) of
false ->
ChangedAcc;
true ->
[Jid | ChangedAcc]
end
end,
[],
Users);
false ->
[]
end,
{ok, AffectedJids, StateData#state{hats_defs = Hats2}}.
destroy_hat(URI, #state{hats_defs = Hats, hats_users = Users} = StateData) ->
Hats2 = maps:remove(URI, Hats),
{AffectedJids, Users2} =
maps:fold(fun(Jid, AssignedHatsUris, {ChangedAcc, UsersAcc}) ->
case AssignedHatsUris -- [URI] of
[] ->
{ChangedAcc, UsersAcc};
AssignedHatsUris2 ->
{[Jid | ChangedAcc], maps:put(Jid, AssignedHatsUris2, UsersAcc)}
end
end,
{[], maps:new()},
Users),
{ok, AffectedJids, StateData#state{hats_defs = Hats2, hats_users = Users2}}.
broadcast_hats_change(StateData) ->
Codes = [104],
Message =
#message{type = groupchat,
id = p1_rand:get_string(),
sub_els = [#muc_user{status_codes = Codes}]},
send_wrapped_multiple(StateData#state.jid,
get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_CONFIG, StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData).
-spec assign_hat(jid(), binary(), state()) -> {ok, state()} | {error, size_limit}.
assign_hat(JID, URI, StateData) ->
Hats = StateData#state.hats_users,
LJID =
jid:remove_resource(
jid:tolower(JID)),
UserHats = maps:get(LJID, Hats, []),
UserHats2 = lists:umerge([URI], UserHats),
USize = length(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 unassign_hat(jid(), binary(), state()) -> {ok, state()} | {error, size_limit}.
unassign_hat(JID, URI, StateData) ->
Hats = StateData#state.hats_users,
LJID =
jid:remove_resource(
jid:tolower(JID)),
UserHats = maps:get(LJID, Hats, []),
UserHats2 = lists:delete(URI, UserHats),
Hats2 = maps:put(LJID, UserHats2, Hats),
{ok, StateData#state{hats_users = Hats2}}.
-spec get_defined_hats(state()) -> [{binary(), binary(), binary()}].
get_defined_hats(StateData) ->
lists:map(fun({Uri, {Title, Hue}}) -> {Uri, Title, Hue} end,
maps:to_list(StateData#state.hats_defs)).
-spec get_assigned_hats(state()) -> [{jid(), binary()}].
get_assigned_hats(StateData) ->
lists:flatmap(fun({LJID, H}) ->
JID = jid:make(LJID),
lists:map(fun(URI) -> {JID, URI} end, H)
end,
maps:to_list(StateData#state.hats_users)).
get_hats_hash(StateData) ->
str:sha(
misc:term_to_base64(get_assigned_hats(StateData))).
get_hat_details(Uri, StateData) ->
lists:keyfind(Uri, 1, get_defined_hats(StateData)).
-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}.
add_presence_hats(JID, Pres, StateData) ->
case (StateData#state.config)#config.enable_hats of
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;
LJID =
jid:remove_resource(
jid:tolower(JID)),
UserHats = maps:get(LJID, Hats, []),
case length(UserHats) of
0 ->
Pres;
_ ->
Items =
lists:map(fun({URI, Title}) ->
#muc_hat{uri = URI, title = Title}
lists:map(fun(URI) ->
{URI, Title, Hue} = get_hat_details(URI, StateData),
#muc_hat{uri = URI,
title = Title,
hue = Hue}
end,
maps:to_list(UserHats)),
xmpp:set_subtag(Pres,
#muc_hats{hats = Items})
UserHats),
xmpp:set_subtag(Pres, #muc_hats{hats = Items})
end;
false ->
Pres
end.
%% @format-end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec process_iq_moderate(jid(), iq(), binary(), binary() | undefined, state()) ->
{result, undefined, state()} |