%%%----------------------------------------------------------------------
%%% File    : mod_announce.erl
%%% Author  : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Manage announce messages
%%% Created : 11 Aug 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------

%%% Implements a small subset of XEP-0133: Service Administration
%%% Version 1.1 (2005-08-19)

-module(mod_announce).
-author('alexey@process-one.net').

-behaviour(gen_server).
-behaviour(gen_mod).

-export([start/2, stop/1, reload/3, export/1, import_info/0,
	 import_start/2, import/5, announce/1, send_motd/1, disco_identity/5,
	 disco_features/5, disco_items/5, depends/2,
	 send_announcement_to_all/3, announce_commands/4, mod_doc/0,
	 announce_items/4, mod_opt_type/1, mod_options/1, clean_cache/1]).
-export([init/1, handle_call/3, handle_cast/2,
	 handle_info/2, terminate/2, code_change/3]).
-export([announce_all/1,
	 announce_all_hosts_all/1,
	 announce_online/1,
	 announce_all_hosts_online/1,
	 announce_motd/1,
	 announce_all_hosts_motd/1,
	 announce_motd_update/1,
	 announce_all_hosts_motd_update/1,
	 announce_motd_delete/1,
	 announce_all_hosts_motd_delete/1]).

-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("mod_announce.hrl").
-include("translate.hrl").

-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), binary(), [binary()]) -> ok.
-callback set_motd_users(binary(), [{binary(), binary(), binary()}]) -> ok | {error, any()}.
-callback set_motd(binary(), xmlel()) -> ok | {error, any()}.
-callback delete_motd(binary()) -> ok | {error, any()}.
-callback get_motd(binary()) -> {ok, xmlel()} | error | {error, any()}.
-callback is_motd_user(binary(), binary()) -> {ok, boolean()} | {error, any()}.
-callback set_motd_user(binary(), binary()) -> ok | {error, any()}.
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> [node()].

-optional_callbacks([use_cache/1, cache_nodes/1]).

-record(state, {host :: binary()}).

-define(NS_ADMINL(Sub), [<<"http:">>, <<"jabber.org">>, <<"protocol">>,
                         <<"admin">>, <<Sub>>]).
-define(MOTD_CACHE, motd_cache).

tokenize(Node) -> str:tokens(Node, <<"/#">>).

%%====================================================================
%% gen_mod callbacks
%%====================================================================
start(Host, Opts) ->
    gen_mod:start_child(?MODULE, Host, Opts).

stop(Host) ->
    gen_mod:stop_child(?MODULE, Host).

reload(Host, NewOpts, OldOpts) ->
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
    if NewMod /= OldMod ->
	    NewMod:init(Host, NewOpts);
       true ->
	    ok
    end,
    init_cache(NewMod, Host, NewOpts).

depends(_Host, _Opts) ->
    [{mod_adhoc, hard}].

%%====================================================================
%% gen_server callbacks
%%====================================================================
init([Host|_]) ->
    process_flag(trap_exit, true),
    Opts = gen_mod:get_module_opts(Host, ?MODULE),
    Mod = gen_mod:db_mod(Opts, ?MODULE),
    Mod:init(Host, Opts),
    init_cache(Mod, Host, Opts),
    ejabberd_hooks:add(local_send_to_resource_hook, Host,
		       ?MODULE, announce, 50),
    ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 50),
    ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50),
    ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 50),
    ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, announce_items, 50),
    ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, announce_commands, 50),
    ejabberd_hooks:add(c2s_self_presence, Host,
		       ?MODULE, send_motd, 50),
    {ok, #state{host = Host}}.

handle_call(Request, From, State) ->
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
    {noreply, State}.

handle_cast({F, #message{from = From, to = To} = Pkt}, State) when is_atom(F) ->
    LServer = To#jid.lserver,
    Host = case F of
	       announce_all -> LServer;
	       announce_all_hosts_all -> global;
	       announce_online -> LServer;
	       announce_all_hosts_online -> global;
	       announce_motd -> LServer;
	       announce_all_hosts_motd -> global;
	       announce_motd_update -> LServer;
	       announce_all_hosts_motd_update -> global;
	       announce_motd_delete -> LServer;
	       announce_all_hosts_motd_delete -> global
	   end,
    Access = get_access(Host),
    case acl:match_rule(Host, Access, From) of
	deny ->
	    route_forbidden_error(Pkt);
	allow ->
	    ?MODULE:F(Pkt)
    end,
    {noreply, State};
handle_cast(Msg, State) ->
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
    {noreply, State}.


handle_info(Info, State) ->
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
    {noreply, State}.

terminate(_Reason, #state{host = Host}) ->
    ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, announce_commands, 50),
    ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, announce_items, 50),
    ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 50),
    ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50),
    ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 50),
    ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, announce, 50),
    ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, send_motd, 50).

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% Announcing via messages to a custom resource
-spec announce(stanza()) -> ok | stop.
announce(#message{to = #jid{luser = <<>>} = To} = Packet) ->
    Proc = gen_mod:get_module_proc(To#jid.lserver, ?MODULE),
    Res = case To#jid.lresource of
	      <<"announce/all">> ->
		  gen_server:cast(Proc, {announce_all, Packet});
	      <<"announce/all-hosts/all">> ->
		  gen_server:cast(Proc, {announce_all_hosts_all, Packet});
	      <<"announce/online">> ->
		  gen_server:cast(Proc, {announce_online, Packet});
	      <<"announce/all-hosts/online">> ->
		  gen_server:cast(Proc, {announce_all_hosts_online, Packet});
	      <<"announce/motd">> ->
		  gen_server:cast(Proc, {announce_motd, Packet});
	      <<"announce/all-hosts/motd">> ->
		  gen_server:cast(Proc, {announce_all_hosts_motd, Packet});
	      <<"announce/motd/update">> ->
		  gen_server:cast(Proc, {announce_motd_update, Packet});
	      <<"announce/all-hosts/motd/update">> ->
		  gen_server:cast(Proc, {announce_all_hosts_motd_update, Packet});
	      <<"announce/motd/delete">> ->
		  gen_server:cast(Proc, {announce_motd_delete, Packet});
	      <<"announce/all-hosts/motd/delete">> ->
		  gen_server:cast(Proc, {announce_all_hosts_motd_delete, Packet});
	      _ ->
		  undefined
	  end,
    case Res of
	ok -> stop;
	_ -> ok
    end;
announce(_Packet) ->
    ok.

%%-------------------------------------------------------------------------
%% Announcing via ad-hoc commands
-define(INFO_COMMAND(Lang, Node),
	[#identity{category = <<"automation">>,
		   type = <<"command-node">>,
		   name = get_title(Lang, Node)}]).

disco_identity(Acc, _From, _To, Node, Lang) ->
    LNode = tokenize(Node),
    case LNode of
	?NS_ADMINL("announce") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("announce-allhosts") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("announce-all") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("announce-all-allhosts") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("set-motd") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("set-motd-allhosts") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("edit-motd") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("edit-motd-allhosts") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("delete-motd") ->
	    ?INFO_COMMAND(Lang, Node);
	?NS_ADMINL("delete-motd-allhosts") ->
	    ?INFO_COMMAND(Lang, Node);
	_ ->
	    Acc
    end.

%%-------------------------------------------------------------------------

-define(INFO_RESULT(Allow, Feats, Lang),
	case Allow of
	    deny ->
		{error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
	    allow ->
		{result, Feats}
	end).

disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
	false ->
	    Acc;
	_ ->
	    Access1 = get_access(LServer),
	    Access2 = get_access(global),
	    case {acl:match_rule(LServer, Access1, From),
		  acl:match_rule(global, Access2, From)} of
		{deny, deny} ->
		    Txt = ?T("Access denied by service policy"),
		    {error, xmpp:err_forbidden(Txt, Lang)};
		_ ->
		    {result, []}
	    end
    end;

disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
	false ->
	    Acc;
	_ ->
	    Access = get_access(LServer),
	    Allow = acl:match_rule(LServer, Access, From),
	    AccessGlobal = get_access(global),
	    AllowGlobal = acl:match_rule(global, AccessGlobal, From),
	    case Node of
		?NS_ADMIN_ANNOUNCE ->
		    ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
		?NS_ADMIN_ANNOUNCE_ALL ->
		    ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
		?NS_ADMIN_SET_MOTD ->
		    ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
		?NS_ADMIN_EDIT_MOTD ->
		    ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
		?NS_ADMIN_DELETE_MOTD ->
		    ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
		?NS_ADMIN_ANNOUNCE_ALLHOSTS ->
		    ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang);
		?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS ->
		    ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang);
		?NS_ADMIN_SET_MOTD_ALLHOSTS ->
		    ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang);
		?NS_ADMIN_EDIT_MOTD_ALLHOSTS ->
		    ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang);
		?NS_ADMIN_DELETE_MOTD_ALLHOSTS ->
		    ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang);
		_ ->
		    Acc
	    end
    end.

%%-------------------------------------------------------------------------
-define(NODE_TO_ITEM(Lang, Server, Node),
	#disco_item{jid = jid:make(Server),
		    node = Node,
		    name = get_title(Lang, Node)}).

-define(ITEMS_RESULT(Allow, Items, Lang),
	case Allow of
	    deny ->
		{error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
	    allow ->
		{result, Items}
	end).

disco_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, <<"">>, Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
	false ->
	    Acc;
	_ ->
	    Access1 = get_access(LServer),
	    Access2 = get_access(global),
	    case {acl:match_rule(LServer, Access1, From),
		  acl:match_rule(global, Access2, From)} of
		{deny, deny} ->
		    Acc;
		_ ->
		    Items = case Acc of
				{result, I} -> I;
				_ -> []
			    end,
		    Nodes = [?NODE_TO_ITEM(Lang, Server, <<"announce">>)],
		    {result, Items ++ Nodes}
	    end
    end;

disco_items(Acc, From, #jid{lserver = LServer} = To, <<"announce">>, Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
	false ->
	    Acc;
	_ ->
	    announce_items(Acc, From, To, Lang)
    end;

disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
	false ->
	    Acc;
	_ ->
	    Access = get_access(LServer),
	    Allow = acl:match_rule(LServer, Access, From),
	    AccessGlobal = get_access(global),
	    AllowGlobal = acl:match_rule(global, AccessGlobal, From),
	    case Node of
		?NS_ADMIN_ANNOUNCE ->
		    ?ITEMS_RESULT(Allow, [], Lang);
		?NS_ADMIN_ANNOUNCE_ALL ->
		    ?ITEMS_RESULT(Allow, [], Lang);
		?NS_ADMIN_SET_MOTD ->
		    ?ITEMS_RESULT(Allow, [], Lang);
		?NS_ADMIN_EDIT_MOTD ->
		    ?ITEMS_RESULT(Allow, [], Lang);
		?NS_ADMIN_DELETE_MOTD ->
		    ?ITEMS_RESULT(Allow, [], Lang);
		?NS_ADMIN_ANNOUNCE_ALLHOSTS ->
		    ?ITEMS_RESULT(AllowGlobal, [], Lang);
		?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS ->
		    ?ITEMS_RESULT(AllowGlobal, [], Lang);
		?NS_ADMIN_SET_MOTD_ALLHOSTS ->
		    ?ITEMS_RESULT(AllowGlobal, [], Lang);
		?NS_ADMIN_EDIT_MOTD_ALLHOSTS ->
		    ?ITEMS_RESULT(AllowGlobal, [], Lang);
		?NS_ADMIN_DELETE_MOTD_ALLHOSTS ->
		    ?ITEMS_RESULT(AllowGlobal, [], Lang);
		_ ->
		    Acc
	    end
    end.

%%-------------------------------------------------------------------------
-spec announce_items(empty | {error, stanza_error()} | {result, [disco_item()]},
			jid(), jid(), binary()) -> {error, stanza_error()} |
						   {result, [disco_item()]} |
						   empty.
announce_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, Lang) ->
    Access1 = get_access(LServer),
    Nodes1 = case acl:match_rule(LServer, Access1, From) of
		 allow ->
		     [?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE_ALL),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_SET_MOTD),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_EDIT_MOTD),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_DELETE_MOTD)];
		 deny ->
		     []
	     end,
    Access2 = get_access(global),
    Nodes2 = case acl:match_rule(global, Access2, From) of
		 allow ->
		     [?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE_ALLHOSTS),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_SET_MOTD_ALLHOSTS),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_EDIT_MOTD_ALLHOSTS),
		      ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_DELETE_MOTD_ALLHOSTS)];
		 deny ->
		     []
	     end,
    case {Nodes1, Nodes2} of
	{[], []} ->
	    Acc;
	_ ->
	    Items = case Acc of
			{result, I} -> I;
			_ -> []
		    end,
	    {result, Items ++ Nodes1 ++ Nodes2}
    end.

%%-------------------------------------------------------------------------

commands_result(Allow, From, To, Request) ->
    case Allow of
	deny ->
	    Lang = Request#adhoc_command.lang,
	    {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
	allow ->
	    announce_commands(From, To, Request)
    end.

-spec announce_commands(empty | adhoc_command(), jid(), jid(), adhoc_command()) ->
			       adhoc_command() | {error, stanza_error()}.
announce_commands(Acc, From, #jid{lserver = LServer} = To,
		  #adhoc_command{node = Node} = Request) ->
    LNode = tokenize(Node),
    F = fun() ->
		Access = get_access(global),
		Allow = acl:match_rule(global, Access, From),
		commands_result(Allow, From, To, Request)
	end,
    R = case LNode of
	    ?NS_ADMINL("announce-allhosts") -> F();
	    ?NS_ADMINL("announce-all-allhosts") -> F();
	    ?NS_ADMINL("set-motd-allhosts") -> F();
	    ?NS_ADMINL("edit-motd-allhosts") -> F();
	    ?NS_ADMINL("delete-motd-allhosts") -> F();
	    _ ->
		Access = get_access(LServer),
		Allow = acl:match_rule(LServer, Access, From),
		case LNode of
		    ?NS_ADMINL("announce") ->
			commands_result(Allow, From, To, Request);
		    ?NS_ADMINL("announce-all") ->
			commands_result(Allow, From, To, Request);
		    ?NS_ADMINL("set-motd") ->
			commands_result(Allow, From, To, Request);
		    ?NS_ADMINL("edit-motd") ->
			commands_result(Allow, From, To, Request);
		    ?NS_ADMINL("delete-motd") ->
			commands_result(Allow, From, To, Request);
		    _ ->
			unknown
		end
	end,
    case R of
	unknown -> Acc;
	_ -> {stop, R}
    end.

%%-------------------------------------------------------------------------

announce_commands(From, To,
		  #adhoc_command{lang = Lang,
				 node = Node,
				 sid = SID,
				 xdata = XData,
				 action = Action} = Request) ->
    if Action == cancel ->
	    %% User cancels request
	    #adhoc_command{status = canceled, lang = Lang, node = Node,
			   sid = SID};
       XData == undefined andalso Action == execute ->
	    %% User requests form
	    Form = generate_adhoc_form(Lang, Node, To#jid.lserver),
	    xmpp_util:make_adhoc_response(
	      #adhoc_command{status = executing, lang = Lang, node = Node,
			     sid = SID, xdata = Form});
       XData /= undefined andalso (Action == execute orelse Action == complete) ->
	    case handle_adhoc_form(From, To, Request) of
		ok ->
		    #adhoc_command{lang = Lang, node = Node, sid = SID,
				   status = completed};
		{error, _} = Err ->
		    Err
	    end;
       true ->
	    Txt = ?T("Unexpected action"),
	    {error, xmpp:err_bad_request(Txt, Lang)}
    end.

vvaluel(Val) ->
    case Val of
        <<>> -> [];
        _ -> [Val]
    end.

generate_adhoc_form(Lang, Node, ServerHost) ->
    LNode = tokenize(Node),
    {OldSubject, OldBody} = if (LNode == ?NS_ADMINL("edit-motd"))
			       or (LNode == ?NS_ADMINL("edit-motd-allhosts")) ->
				    get_stored_motd(ServerHost);
			       true ->
				    {<<>>, <<>>}
			    end,
    Fs = if (LNode == ?NS_ADMINL("delete-motd"))
	    or (LNode == ?NS_ADMINL("delete-motd-allhosts")) ->
		 [#xdata_field{type = boolean,
			       var = <<"confirm">>,
			       label = translate:translate(
					 Lang, ?T("Really delete message of the day?")),
			       values = [<<"true">>]}];
	    true ->
		 [#xdata_field{type = 'text-single',
			       var = <<"subject">>,
			       label = translate:translate(Lang, ?T("Subject")),
			       values = vvaluel(OldSubject)},
		  #xdata_field{type = 'text-multi',
			       var = <<"body">>,
			       label = translate:translate(Lang, ?T("Message body")),
			       values = vvaluel(OldBody)}]
	 end,
    #xdata{type = form,
	   title = get_title(Lang, Node),
	   fields = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>,
				  values = [?NS_ADMIN]}|Fs]}.

join_lines([]) ->
    <<>>;
join_lines(Lines) ->
    join_lines(Lines, []).
join_lines([Line|Lines], Acc) ->
    join_lines(Lines, [<<"\n">>,Line|Acc]);
join_lines([], Acc) ->
    %% Remove last newline
    iolist_to_binary(lists:reverse(tl(Acc))).

handle_adhoc_form(From, #jid{lserver = LServer} = To,
		  #adhoc_command{lang = Lang, node = Node,
				 xdata = XData}) ->
    Confirm = case xmpp_util:get_xdata_values(<<"confirm">>, XData) of
		  [<<"true">>] -> true;
		  [<<"1">>] -> true;
		  _ -> false
	      end,
    Subject = join_lines(xmpp_util:get_xdata_values(<<"subject">>, XData)),
    Body = join_lines(xmpp_util:get_xdata_values(<<"body">>, XData)),
    Packet = #message{from = From,
		      to = To,
		      type = headline,
		      body = xmpp:mk_text(Body),
		      subject = xmpp:mk_text(Subject)},
    Proc = gen_mod:get_module_proc(LServer, ?MODULE),
    case {Node, Body} of
	{?NS_ADMIN_DELETE_MOTD, _} ->
	    if	Confirm ->
		    gen_server:cast(Proc, {announce_motd_delete, Packet});
		true ->
		    ok
	    end;
	{?NS_ADMIN_DELETE_MOTD_ALLHOSTS, _} ->
	    if	Confirm ->
		    gen_server:cast(Proc, {announce_all_hosts_motd_delete, Packet});
		true ->
		    ok
	    end;
	{_, <<>>} ->
	    %% An announce message with no body is definitely an operator error.
	    %% Throw an error and give him/her a chance to send message again.
	    {error, xmpp:err_not_acceptable(
		      ?T("No body provided for announce message"), Lang)};
	%% Now send the packet to ?MODULE.
	%% We don't use direct announce_* functions because it
	%% leads to large delay in response and <iq/> queries processing
	{?NS_ADMIN_ANNOUNCE, _} ->
	    gen_server:cast(Proc, {announce_online, Packet});
	{?NS_ADMIN_ANNOUNCE_ALLHOSTS, _} ->
	    gen_server:cast(Proc, {announce_all_hosts_online, Packet});
	{?NS_ADMIN_ANNOUNCE_ALL, _} ->
	    gen_server:cast(Proc, {announce_all, Packet});
	{?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS, _} ->
	    gen_server:cast(Proc, {announce_all_hosts_all, Packet});
	{?NS_ADMIN_SET_MOTD, _} ->
	    gen_server:cast(Proc, {announce_motd, Packet});
	{?NS_ADMIN_SET_MOTD_ALLHOSTS, _} ->
	    gen_server:cast(Proc, {announce_all_hosts_motd, Packet});
	{?NS_ADMIN_EDIT_MOTD, _} ->
	    gen_server:cast(Proc, {announce_motd_update, Packet});
	{?NS_ADMIN_EDIT_MOTD_ALLHOSTS, _} ->
	    gen_server:cast(Proc, {announce_all_hosts_motd_update, Packet});
	Junk ->
	    %% This can't happen, as we haven't registered any other
	    %% command nodes.
	    ?ERROR_MSG("Unexpected node/body = ~p", [Junk]),
	    {error, xmpp:err_internal_server_error()}
    end.

get_title(Lang, <<"announce">>) ->
    translate:translate(Lang, ?T("Announcements"));
get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALL) ->
    translate:translate(Lang, ?T("Send announcement to all users"));
get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS) ->
    translate:translate(Lang, ?T("Send announcement to all users on all hosts"));
get_title(Lang, ?NS_ADMIN_ANNOUNCE) ->
    translate:translate(Lang, ?T("Send announcement to all online users"));
get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALLHOSTS) ->
    translate:translate(Lang, ?T("Send announcement to all online users on all hosts"));
get_title(Lang, ?NS_ADMIN_SET_MOTD) ->
    translate:translate(Lang, ?T("Set message of the day and send to online users"));
get_title(Lang, ?NS_ADMIN_SET_MOTD_ALLHOSTS) ->
    translate:translate(Lang, ?T("Set message of the day on all hosts and send to online users"));
get_title(Lang, ?NS_ADMIN_EDIT_MOTD) ->
    translate:translate(Lang, ?T("Update message of the day (don't send)"));
get_title(Lang, ?NS_ADMIN_EDIT_MOTD_ALLHOSTS) ->
    translate:translate(Lang, ?T("Update message of the day on all hosts (don't send)"));
get_title(Lang, ?NS_ADMIN_DELETE_MOTD) ->
    translate:translate(Lang, ?T("Delete message of the day"));
get_title(Lang, ?NS_ADMIN_DELETE_MOTD_ALLHOSTS) ->
    translate:translate(Lang, ?T("Delete message of the day on all hosts")).

%%-------------------------------------------------------------------------

announce_all(#message{to = To} = Packet) ->
    Local = jid:make(To#jid.server),
    lists:foreach(
      fun({User, Server}) ->
	      Dest = jid:make(User, Server),
	      ejabberd_router:route(
		xmpp:set_from_to(add_store_hint(Packet), Local, Dest))
      end, ejabberd_auth:get_users(To#jid.lserver)).

announce_all_hosts_all(#message{to = To} = Packet) ->
    Local = jid:make(To#jid.server),
    lists:foreach(
      fun({User, Server}) ->
	      Dest = jid:make(User, Server),
	      ejabberd_router:route(
		xmpp:set_from_to(add_store_hint(Packet), Local, Dest))
      end, ejabberd_auth:get_users()).

announce_online(#message{to = To} = Packet) ->
    announce_online1(ejabberd_sm:get_vh_session_list(To#jid.lserver),
		     To#jid.server, Packet).

announce_all_hosts_online(#message{to = To} = Packet) ->
    announce_online1(ejabberd_sm:dirty_get_sessions_list(),
		     To#jid.server, Packet).

announce_online1(Sessions, Server, Packet) ->
    Local = jid:make(Server),
    lists:foreach(
      fun({U, S, R}) ->
	      Dest = jid:make(U, S, R),
	      ejabberd_router:route(xmpp:set_from_to(Packet, Local, Dest))
      end, Sessions).

announce_motd(#message{to = To} = Packet) ->
    announce_motd(To#jid.lserver, Packet).

announce_all_hosts_motd(Packet) ->
    Hosts = ejabberd_option:hosts(),
    [announce_motd(Host, Packet) || Host <- Hosts].

announce_motd(Host, Packet) ->
    LServer = jid:nameprep(Host),
    announce_motd_update(LServer, Packet),
    Sessions = ejabberd_sm:get_vh_session_list(LServer),
    announce_online1(Sessions, LServer, Packet),
    Mod = gen_mod:db_mod(LServer, ?MODULE),
    Mod:set_motd_users(LServer, Sessions).

announce_motd_update(#message{to = To} = Packet) ->
    announce_motd_update(To#jid.lserver, Packet).

announce_all_hosts_motd_update(Packet) ->
    Hosts = ejabberd_option:hosts(),
    [announce_motd_update(Host, Packet) || Host <- Hosts].

announce_motd_update(LServer, Packet) ->
    Mod = gen_mod:db_mod(LServer, ?MODULE),
    delete_motd(Mod, LServer),
    set_motd(Mod, LServer, xmpp:encode(Packet)).

announce_motd_delete(#message{to = To}) ->
    LServer = To#jid.lserver,
    Mod = gen_mod:db_mod(LServer, ?MODULE),
    delete_motd(Mod, LServer).

announce_all_hosts_motd_delete(_Packet) ->
    lists:foreach(
      fun(Host) ->
	      Mod = gen_mod:db_mod(Host, ?MODULE),
	      delete_motd(Mod, Host)
      end, ejabberd_option:hosts()).

-spec send_motd({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}.
send_motd({_, #{pres_last := _}} = Acc) ->
    %% This is just a presence update, nothing to do
    Acc;
send_motd({#presence{type = available},
	   #{jid := #jid{luser = LUser, lserver = LServer} = JID}} = Acc)
  when LUser /= <<>> ->
    Mod = gen_mod:db_mod(LServer, ?MODULE),
    case get_motd(Mod, LServer) of
	{ok, Packet} ->
	    CodecOpts = ejabberd_config:codec_options(),
	    try xmpp:decode(Packet, ?NS_CLIENT, CodecOpts) of
		Msg ->
		    case is_motd_user(Mod, LUser, LServer) of
			false ->
			    Local = jid:make(LServer),
			    ejabberd_router:route(
			      xmpp:set_from_to(Msg, Local, JID)),
			    set_motd_user(Mod, LUser, LServer);
			true ->
			    ok
		    end
	    catch _:{xmpp_codec, Why} ->
		    ?ERROR_MSG("Failed to decode motd packet ~p: ~ts",
			       [Packet, xmpp:format_error(Why)])
	    end;
	_ ->
	    ok
    end,
    Acc;
send_motd(Acc) ->
    Acc.

-spec get_motd(module(), binary()) -> {ok, xmlel()} | error | {error, any()}.
get_motd(Mod, LServer) ->
    case use_cache(Mod, LServer) of
	true ->
	    ets_cache:lookup(
	      ?MOTD_CACHE, {<<"">>, LServer},
	      fun() -> Mod:get_motd(LServer) end);
	false ->
	    Mod:get_motd(LServer)
    end.

-spec set_motd(module(), binary(), xmlel()) -> any().
set_motd(Mod, LServer, XML) ->
    case use_cache(Mod, LServer) of
	true ->
	    ets_cache:update(
	      ?MOTD_CACHE, {<<"">>, LServer}, {ok, XML},
	      fun() -> Mod:set_motd(LServer, XML) end,
	      cache_nodes(Mod, LServer));
	false ->
	    Mod:set_motd(LServer, XML)
    end.

-spec is_motd_user(module(), binary(), binary()) -> boolean().
is_motd_user(Mod, LUser, LServer) ->
    Res = case use_cache(Mod, LServer) of
	      true ->
		  ets_cache:lookup(
		    ?MOTD_CACHE, {LUser, LServer},
		    fun() -> Mod:is_motd_user(LUser, LServer) end);
	      false ->
		  Mod:is_motd_user(LUser, LServer)
	  end,
    case Res of
	{ok, Bool} -> Bool;
	_ -> false
    end.

-spec set_motd_user(module(), binary(), binary()) -> any().
set_motd_user(Mod, LUser, LServer) ->
    case use_cache(Mod, LServer) of
	true ->
	    ets_cache:update(
	      ?MOTD_CACHE, {LUser, LServer}, {ok, true},
	      fun() -> Mod:set_motd_user(LUser, LServer) end,
	      cache_nodes(Mod, LServer));
	false ->
	    Mod:set_motd_user(LUser, LServer)
    end.

-spec delete_motd(module(), binary()) -> ok | {error, any()}.
delete_motd(Mod, LServer) ->
    case Mod:delete_motd(LServer) of
	ok ->
	    case use_cache(Mod, LServer) of
		true ->
		    ejabberd_cluster:eval_everywhere(
		      ?MODULE, clean_cache, [LServer]);
		false ->
		    ok
	    end;
	Err ->
	    Err
    end.

get_stored_motd(LServer) ->
    Mod = gen_mod:db_mod(LServer, ?MODULE),
    case get_motd(Mod, LServer) of
        {ok, Packet} ->
	    CodecOpts = ejabberd_config:codec_options(),
	    try xmpp:decode(Packet, ?NS_CLIENT, CodecOpts) of
		#message{body = Body, subject = Subject} ->
		    {xmpp:get_text(Subject), xmpp:get_text(Body)}
	    catch _:{xmpp_codec, Why} ->
		    ?ERROR_MSG("Failed to decode motd packet ~p: ~ts",
			       [Packet, xmpp:format_error(Why)])
	    end;
        _ ->
            {<<>>, <<>>}
    end.

%% This function is similar to others, but doesn't perform any ACL verification
send_announcement_to_all(Host, SubjectS, BodyS) ->
    Packet = #message{type = headline,
		      body = xmpp:mk_text(BodyS),
		      subject = xmpp:mk_text(SubjectS)},
    Sessions = ejabberd_sm:dirty_get_sessions_list(),
    Local = jid:make(Host),
    lists:foreach(
      fun({U, S, R}) ->
	      Dest = jid:make(U, S, R),
	      ejabberd_router:route(
		xmpp:set_from_to(add_store_hint(Packet), Local, Dest))
      end, Sessions).

-spec get_access(global | binary()) -> atom().

get_access(Host) ->
    mod_announce_opt:access(Host).

-spec add_store_hint(stanza()) -> stanza().
add_store_hint(El) ->
    xmpp:set_subtag(El, #hint{type = store}).

-spec route_forbidden_error(stanza()) -> ok.
route_forbidden_error(Packet) ->
    Lang = xmpp:get_lang(Packet),
    Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang),
    ejabberd_router:route_error(Packet, Err).

-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
init_cache(Mod, Host, Opts) ->
    case use_cache(Mod, Host) of
	true ->
	    CacheOpts = cache_opts(Opts),
	    ets_cache:new(?MOTD_CACHE, CacheOpts);
	false ->
	    ets_cache:delete(?MOTD_CACHE)
    end.

-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
cache_opts(Opts) ->
    MaxSize = mod_announce_opt:cache_size(Opts),
    CacheMissed = mod_announce_opt:cache_missed(Opts),
    LifeTime = mod_announce_opt:cache_life_time(Opts),
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].

-spec use_cache(module(), binary()) -> boolean().
use_cache(Mod, Host) ->
    case erlang:function_exported(Mod, use_cache, 1) of
	true -> Mod:use_cache(Host);
	false -> mod_announce_opt:use_cache(Host)
    end.

-spec cache_nodes(module(), binary()) -> [node()].
cache_nodes(Mod, Host) ->
    case erlang:function_exported(Mod, cache_nodes, 1) of
	true -> Mod:cache_nodes(Host);
	false -> ejabberd_cluster:get_nodes()
    end.

-spec clean_cache(binary()) -> non_neg_integer().
clean_cache(LServer) ->
    ets_cache:filter(
      ?MOTD_CACHE,
      fun({_, S}, _) -> S /= LServer end).

%%-------------------------------------------------------------------------
export(LServer) ->
    Mod = gen_mod:db_mod(LServer, ?MODULE),
    Mod:export(LServer).

import_info() ->
    [{<<"motd">>, 3}].

import_start(LServer, DBType) ->
    Mod = gen_mod:db_mod(DBType, ?MODULE),
    Mod:init(LServer, []).

import(LServer, {sql, _}, DBType, Tab, List) ->
    Mod = gen_mod:db_mod(DBType, ?MODULE),
    Mod:import(LServer, Tab, List).

mod_opt_type(access) ->
    econf:acl();
mod_opt_type(db_type) ->
    econf:db_type(?MODULE);
mod_opt_type(use_cache) ->
    econf:bool();
mod_opt_type(cache_size) ->
    econf:pos_int(infinity);
mod_opt_type(cache_missed) ->
    econf:bool();
mod_opt_type(cache_life_time) ->
    econf:timeout(second, infinity).

mod_options(Host) ->
    [{access, none},
     {db_type, ejabberd_config:default_db(Host, ?MODULE)},
     {use_cache, ejabberd_option:use_cache(Host)},
     {cache_size, ejabberd_option:cache_size(Host)},
     {cache_missed, ejabberd_option:cache_missed(Host)},
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].

mod_doc() ->
    #{desc =>
          [?T("This module enables configured users to broadcast "
              "announcements and to set the message of the day (MOTD). "
              "Configured users can perform these actions with an XMPP "
              "client either using Ad-Hoc Commands or sending messages "
              "to specific JIDs."), "",
	   ?T("NOTE: This module can be resource intensive on large "
	      "deployments as it may broadcast a lot of messages. This module "
	      "should be disabled for instances of ejabberd with hundreds of "
	      "thousands users."), "",
           ?T("To send announcements using "
              "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands], "
              "this module requires _`mod_adhoc`_ (to execute the commands), "
              "and recommends _`mod_disco`_ (to discover the commands)."), "",
           ?T("To send announcements by sending messages to specific JIDs, these are the destination JIDs:"), "",
           "- 'example.org/announce/all':",
           ?T("Send the message to all registered users in that vhost. If the user is "
              "online and connected to several resources, only the resource "
              "with the highest priority will receive the message. "
              "If the registered user is not connected, the message is "
              "stored offline in assumption that offline storage (see _`mod_offline`_) "
              "is enabled."),
           "- 'example.org/announce/online':",
           ?T("Send the message to all connected users. If the user is "
              "online and connected to several resources, all resources will "
              "receive the message."),
           "- 'example.org/announce/motd':",
           ?T("Set the message of the day (MOTD) that is sent "
              "to users when they login. Also sends the message to all "
              "connected users (similar to 'announce/online')."),
           "- 'example.org/announce/motd/update':",
           ?T("Set the message of the day (MOTD) that is sent to users "
              "when they login. This does not send the message to any currently connected user."),
           "- 'example.org/announce/motd/delete':",
           ?T("Remove the existing message of the day (MOTD) by sending a message to this JID."), "",
           ?T("There are similar destination JIDs to apply to all virtual hosts in ejabberd:"), "",
           "- 'example.org/announce/all-hosts/all': send to all registered accounts",
           "- 'example.org/announce/all-hosts/online': send to online sessions",
           "- 'example.org/announce/all-hosts/motd': set MOTD and send to online",
           "- 'example.org/announce/all-hosts/motd/update': update MOTD",
           "- 'example.org/announce/all-hosts/motd/delete': delete MOTD"],
      opts =>
          [{access,
            #{value => ?T("AccessName"),
              desc =>
                  ?T("This option specifies who is allowed to send announcements "
                     "and to set the message of the day. The default value is 'none' "
                     "(i.e. nobody is able to send such messages).")}},
           {db_type,
            #{value => "mnesia | sql",
              desc =>
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
           {use_cache,
            #{value => "true | false",
              desc =>
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
           {cache_size,
            #{value => "pos_integer() | infinity",
              desc =>
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
           {cache_missed,
            #{value => "true | false",
              desc =>
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
           {cache_life_time,
            #{value => "timeout()",
              desc =>
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
