Packages
ejabberd
26.1.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_announce.erl
%%%----------------------------------------------------------------------
%%% 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-2026 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.
%%%
%%%----------------------------------------------------------------------
%%; definitions
%%% 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]).
%% ejabberd_commands
-export([announce_send_all/3,
announce_send_online/3,
get_stored_motd/1,
announce_motd_set_online/3,
announce_motd_update/3,
announce_motd_delete_api/1,
get_commands_spec/0]).
%% WebAdmin
-export([webadmin_menu/3, webadmin_page/3]).
-import(ejabberd_web_admin, [make_command/4, make_command/2]).
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("ejabberd_commands.hrl").
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.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) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
false ->
ejabberd_commands:register_commands(?MODULE, get_commands_spec());
true ->
ok
end,
ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, webadmin_menu, 50),
ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50),
gen_mod:start_child(?MODULE, Host, Opts).
stop(Host) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
false ->
ejabberd_commands:unregister_commands(get_commands_spec());
true ->
ok
end,
ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, webadmin_menu, 50),
ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50),
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
%%====================================================================
%%; -- disco identity
-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.
%%====================================================================
%%; -- disco features
-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.
%%====================================================================
%%; -- disco items
-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.
%%====================================================================
%%; -- announce items
-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
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.
%%====================================================================
%%; -- adhoc form
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")).
%%====================================================================
%%; -- ad-hoc commands implementation
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).
%%====================================================================
%%; Announcing via API commands
%% @format-begin
-spec get_commands_spec() -> [ejabberd_commands()].
get_commands_spec() ->
HostAll = "If HOST is `all`, send to all hosts. ",
BodyNew = "You can use ' \\n ' in the message body to write a newline.",
[#ejabberd_commands{name = announce_send_all,
tags = [announce],
desc = "Send announcement to all users",
longdesc = HostAll ++ BodyNew,
module = ?MODULE,
function = announce_send_all,
note = "added in 25.10",
args = [{host, binary}, {subject, binary}, {body, binary}],
result = {res, rescode}},
#ejabberd_commands{name = announce_send_online,
tags = [announce],
desc = "Send announcement to online users",
longdesc = HostAll ++ BodyNew,
module = ?MODULE,
function = announce_send_online,
note = "added in 25.10",
args = [{host, binary}, {subject, binary}, {body, binary}],
result = {res, rescode}},
#ejabberd_commands{name = announce_motd_get,
tags = [announce],
desc = "Get Message Of The Day",
longdesc = BodyNew,
module = ?MODULE,
function = get_stored_motd,
note = "added in 25.10",
args = [{host, binary}],
result = {motd, {tuple, [{subject, string}, {body, string}]}}},
#ejabberd_commands{name = announce_motd_set_online,
tags = [announce],
desc = "Set Message Of The Day and send to online users",
longdesc = HostAll ++ BodyNew,
module = ?MODULE,
function = announce_motd_set_online,
note = "added in 25.10",
args = [{host, binary}, {subject, binary}, {body, binary}],
result = {res, rescode}},
#ejabberd_commands{name = announce_motd_update,
tags = [announce],
desc = "Update Message Of The Day",
longdesc = HostAll ++ BodyNew,
module = ?MODULE,
function = announce_motd_update,
note = "added in 25.10",
args = [{host, binary}, {subject, binary}, {body, binary}],
result = {res, rescode}},
#ejabberd_commands{name = announce_motd_delete,
tags = [announce],
desc = "Delete Message Of The Day",
longdesc = HostAll,
module = ?MODULE,
function = announce_motd_delete_api,
note = "added in 25.10",
args = [{host, binary}],
result = {res, rescode}}].
announce_send_all(<<"all">>, Subject, Body) ->
Host = hd(ejabberd_option:hosts()),
announce_all_hosts_all(make_packet(Host, Body, Subject));
announce_send_all(Host, Subject, Body) ->
announce_all(make_packet(Host, Body, Subject)).
announce_send_online(<<"all">>, Subject, Body) ->
Host = hd(ejabberd_option:hosts()),
announce_all_hosts_online(make_packet(Host, Body, Subject));
announce_send_online(Host, Subject, Body) ->
announce_online(make_packet(Host, Body, Subject)).
announce_motd_set_online(<<"all">>, Subject, Body) ->
Host = hd(ejabberd_option:hosts()),
announce_all_hosts_motd(make_packet(Host, Body, Subject));
announce_motd_set_online(Host, Subject, Body) ->
announce_motd(make_packet(Host, Body, Subject)).
announce_motd_update(<<"all">>, Subject, Body) ->
Host = hd(ejabberd_option:hosts()),
[{ok, _} | _] = announce_all_hosts_motd_update(make_packet(Host, Body, Subject)),
ok;
announce_motd_update(Host, Subject, Body) ->
{ok, _} = announce_motd_update(make_packet(Host, Body, Subject)),
ok.
announce_motd_delete_api(<<"all">>) ->
Host = hd(ejabberd_option:hosts()),
announce_all_hosts_motd_delete(make_packet(Host));
announce_motd_delete_api(Host) ->
announce_motd_delete(make_packet(Host)).
make_packet(Host) ->
From = To = jid:make(Host),
#message{from = From, to = To}.
make_packet(Host, Body, Subject) ->
From = To = jid:make(Host),
Body2 = binary:replace(Body, <<"\\n">>, <<"\n">>, [global]),
#message{from = From,
to = To,
type = headline,
body = xmpp:mk_text(Body2),
subject = xmpp:mk_text(Subject)}.
%%====================================================================
%%; Announcing via WebAdmin
webadmin_menu(Acc, _Host, Lang) ->
[{<<"announce">>, translate:translate(Lang, ?T("Announcements"))} | Acc].
webadmin_page(_,
Host,
#request{us = _US,
path = [<<"announce">>],
lang = Lang} =
R) ->
PageTitle = translate:translate(Lang, ?T("Announcements")),
Head = ?H1GL(PageTitle, <<"modules/#mod_announce">>, <<"mod_announce">>),
Ann = [?X(<<"hr">>),
?XE(<<"blockquote">>,
[make_command(announce_send_all, R, [{<<"host">>, Host}], []),
make_command(announce_send_online, R, [{<<"host">>, Host}], [])])],
SetMotd =
[make_command(announce_motd_set_online, R, [{<<"host">>, Host}], []),
make_command(announce_motd_update, R, [{<<"host">>, Host}], []),
make_command(announce_motd_delete, R, [{<<"host">>, Host}], [{style, danger}])],
GetMotd = [make_command(announce_motd_get, R, [{<<"host">>, Host}], [])],
Motd =
[?X(<<"hr">>),
?XAC(<<"h2">>, [{<<"id">>, <<"motd">>}], <<"Message Of The Day">>),
?XE(<<"blockquote">>, GetMotd ++ SetMotd)],
{stop, Head ++ Ann ++ Motd};
webadmin_page(Acc, _, _) ->
Acc.
%% @format-end
%%====================================================================
%%; Cache management
-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).
%%====================================================================
%%; ejd2sql callbacks
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).
%%====================================================================
%%; Options and Documentation
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. Equivalent API commands are also available."), "",
?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.")}}]}.
%%% vim: set foldmethod=marker foldmarker=%%;,%%=: