From 2b7285e0b2423fb54f54cd41342d02658e178c57 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 8 Sep 2025 19:07:30 +0200 Subject: [PATCH] Update implementation of XEP-0317 Hats to version 0.3.1 (#4380) --- ejabberd.doap | 4 +- include/mod_muc_room.hrl | 3 +- mix.exs | 2 +- mix.lock | 2 +- rebar.config | 2 +- rebar.lock | 11 +- src/mod_muc.erl | 2 +- src/mod_muc_mnesia.erl | 38 +- src/mod_muc_room.erl | 749 ++++++++++++++++++++++++--------------- 9 files changed, 511 insertions(+), 302 deletions(-) diff --git a/ejabberd.doap b/ejabberd.doap index b48ad5892..bfd3f5636 100644 --- a/ejabberd.doap +++ b/ejabberd.doap @@ -578,10 +578,10 @@ - 0.2.0 + 0.3.1 21.12 complete - mod_muc_room, 0.2.0 since 25.03 + mod_muc_room, 0.3.1 since 25.xx diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index 949e910da..5f81fe026 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -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(), diff --git a/mix.exs b/mix.exs index 5b5be84f5..fcb3ac39e 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index 9ad9a9e33..3eb53bce9 100644 --- a/mix.lock +++ b/mix.lock @@ -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"]}, } diff --git a/rebar.config b/rebar.config index 1d0f9a8db..3d85402e3 100644 --- a/rebar.config +++ b/rebar.config @@ -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"}} ]}. diff --git a/rebar.lock b/rebar.lock index 2547b4371..d69fcec1e 100644 --- a/rebar.lock +++ b/rebar.lock @@ -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">>}]} ]. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index ffb8c25d7..592d18fae 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -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. " diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl index 65c37a7ab..02ecb3ce8 100644 --- a/src/mod_muc_mnesia.erl +++ b/src/mod_muc_mnesia.erl @@ -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)}, diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 85c1d2d69..64c91e4c0 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -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()} |