24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-02 21:17:12 +02:00
xmpp.chapril.org-ejabberd/src/mod_mam.erl
Evgeniy Khramtsov 77163c43d2 Simplify the default configuration file
After some discussion with the community it was decided to
clean the configuration file from excessive comments and
explicitly configured default values. Also, mod_mam and
mod_http_upload have been added.

The rationale for this is to have a clean and not bloated
configuration file which doesn't scare away newcomers and
which has all features from the Compliance Suite 2018 (XEP-0387)
enabled by default.

For further configuration an admin is encouraged to read the
documentation at https://docs.ejabberd.im/admin/configuration
2018-07-01 23:57:27 +03:00

1179 lines
38 KiB
Erlang

%%%-------------------------------------------------------------------
%%% File : mod_mam.erl
%%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%% Purpose : Message Archive Management (XEP-0313)
%%% Created : 4 Jul 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2013-2018 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.
%%%
%%%-------------------------------------------------------------------
-module(mod_mam).
-protocol({xep, 313, '0.6.1'}).
-protocol({xep, 334, '0.2'}).
-protocol({xep, 359, '0.5.0'}).
-behaviour(gen_mod).
%% API
-export([start/2, stop/1, reload/3, depends/2]).
-export([sm_receive_packet/1, user_receive_packet/1, user_send_packet/1,
user_send_packet_strip_tag/1, process_iq_v0_2/1, process_iq_v0_3/1,
disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1,
muc_process_iq/2, muc_filter_message/3, message_is_archived/3,
delete_old_messages/2, get_commands_spec/0, msg_to_el/4,
get_room_config/4, set_room_option/3, offline_message/1, export/1,
mod_options/1, remove_mam_for_user_with_peer/3, remove_mam_for_user/2]).
-include("xmpp.hrl").
-include("logger.hrl").
-include("mod_muc_room.hrl").
-include("ejabberd_commands.hrl").
-include("mod_mam.hrl").
-define(DEF_PAGE_SIZE, 50).
-define(MAX_PAGE_SIZE, 250).
-type c2s_state() :: ejabberd_c2s:state().
-callback init(binary(), gen_mod:opts()) -> any().
-callback remove_user(binary(), binary()) -> any().
-callback remove_room(binary(), binary(), binary()) -> any().
-callback delete_old_messages(binary() | global,
erlang:timestamp(),
all | chat | groupchat) -> any().
-callback extended_fields() -> [mam_query:property() | #xdata_field{}].
-callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat,
jid(), binary(), recv | send, integer()) -> ok | any().
-callback write_prefs(binary(), binary(), #archive_prefs{}, binary()) -> ok | any().
-callback get_prefs(binary(), binary()) -> {ok, #archive_prefs{}} | error.
-callback select(binary(), jid(), jid(), mam_query:result(),
#rsm_set{} | undefined, chat | groupchat) ->
{[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()}.
-callback use_cache(binary()) -> boolean().
-callback cache_nodes(binary()) -> [node()].
-callback remove_from_archive(binary(), binary(), jid() | none) -> ok | {error, any()}.
-optional_callbacks([use_cache/1, cache_nodes/1]).
%%%===================================================================
%%% API
%%%===================================================================
start(Host, Opts) ->
case gen_mod:get_opt(db_type, Opts) of
mnesia ->
?WARNING_MSG("Mnesia backend for ~s is not recommended: "
"it's limited to 2GB and often gets corrupted "
"when reaching this limit. SQL backend is "
"recommended. Namely, for small servers SQLite "
"is a preferred choice because it's very easy "
"to configure.", [?MODULE]);
_ ->
ok
end,
Mod = gen_mod:db_mod(Host, Opts, ?MODULE),
Mod:init(Host, Opts),
init_cache(Mod, Host, Opts),
register_iq_handlers(Host),
ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE,
sm_receive_packet, 50),
ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
user_receive_packet, 88),
ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
user_send_packet, 88),
ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
user_send_packet_strip_tag, 500),
ejabberd_hooks:add(offline_message_hook, Host, ?MODULE,
offline_message, 50),
ejabberd_hooks:add(muc_filter_message, Host, ?MODULE,
muc_filter_message, 50),
ejabberd_hooks:add(muc_process_iq, Host, ?MODULE,
muc_process_iq, 50),
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
disco_sm_features, 50),
ejabberd_hooks:add(remove_user, Host, ?MODULE,
remove_user, 50),
ejabberd_hooks:add(remove_room, Host, ?MODULE,
remove_room, 50),
ejabberd_hooks:add(get_room_config, Host, ?MODULE,
get_room_config, 50),
ejabberd_hooks:add(set_room_option, Host, ?MODULE,
set_room_option, 50),
case gen_mod:get_opt(assume_mam_usage, Opts) of
true ->
ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
message_is_archived, 50);
false ->
ok
end,
ejabberd_commands:register_commands(get_commands_spec()),
ok.
use_cache(Mod, Host) ->
case erlang:function_exported(Mod, use_cache, 2) of
true -> Mod:use_cache(Host);
false -> gen_mod:get_module_opt(Host, ?MODULE, use_cache)
end.
cache_nodes(Mod, Host) ->
case erlang:function_exported(Mod, cache_nodes, 1) of
true -> Mod:cache_nodes(Host);
false -> ejabberd_cluster:get_nodes()
end.
init_cache(Mod, Host, Opts) ->
case use_cache(Mod, Host) of
true ->
ets_cache:new(archive_prefs_cache, cache_opts(Opts));
false ->
ets_cache:delete(archive_prefs_cache)
end.
cache_opts(Opts) ->
MaxSize = gen_mod:get_opt(cache_size, Opts),
CacheMissed = gen_mod:get_opt(cache_missed, Opts),
LifeTime = case gen_mod:get_opt(cache_life_time, Opts) of
infinity -> infinity;
I -> timer:seconds(I)
end,
[{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}].
stop(Host) ->
unregister_iq_handlers(Host),
ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE,
sm_receive_packet, 50),
ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
user_receive_packet, 88),
ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
user_send_packet, 88),
ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
user_send_packet_strip_tag, 500),
ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE,
offline_message, 50),
ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE,
muc_filter_message, 50),
ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE,
muc_process_iq, 50),
ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE,
disco_sm_features, 50),
ejabberd_hooks:delete(remove_user, Host, ?MODULE,
remove_user, 50),
ejabberd_hooks:delete(remove_room, Host, ?MODULE,
remove_room, 50),
ejabberd_hooks:delete(get_room_config, Host, ?MODULE,
get_room_config, 50),
ejabberd_hooks:delete(set_room_option, Host, ?MODULE,
set_room_option, 50),
case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage) of
true ->
ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
message_is_archived, 50);
false ->
ok
end,
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
false ->
ejabberd_commands:unregister_commands(get_commands_spec());
true ->
ok
end.
reload(Host, NewOpts, OldOpts) ->
NewMod = gen_mod:db_mod(Host, NewOpts, ?MODULE),
OldMod = gen_mod:db_mod(Host, OldOpts, ?MODULE),
if NewMod /= OldMod ->
NewMod:init(Host, NewOpts);
true ->
ok
end,
init_cache(NewMod, Host, NewOpts),
case gen_mod:is_equal_opt(assume_mam_usage, NewOpts, OldOpts) of
{false, true, _} ->
ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
message_is_archived, 50);
{false, false, _} ->
ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
message_is_archived, 50);
true ->
ok
end.
depends(_Host, _Opts) ->
[].
-spec register_iq_handlers(binary()) -> ok.
register_iq_handlers(Host) ->
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP,
?MODULE, process_iq_v0_2),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP,
?MODULE, process_iq_v0_2),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_0,
?MODULE, process_iq_v0_3),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_0, ?MODULE,
process_iq_v0_3),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_1,
?MODULE, process_iq_v0_3),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_1,
?MODULE, process_iq_v0_3),
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_2,
?MODULE, process_iq_v0_3),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2,
?MODULE, process_iq_v0_3).
-spec unregister_iq_handlers(binary()) -> ok.
unregister_iq_handlers(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_0),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_0),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_1),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1),
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_2),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2).
-spec remove_user(binary(), binary()) -> ok.
remove_user(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:remove_user(LUser, LServer),
case use_cache(Mod, LServer) of
true ->
ets_cache:delete(archive_prefs_cache, {LUser, LServer},
cache_nodes(Mod, LServer));
false ->
ok
end.
-spec remove_room(binary(), binary(), binary()) -> ok.
remove_room(LServer, Name, Host) ->
LName = jid:nodeprep(Name),
LHost = jid:nameprep(Host),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:remove_room(LServer, LName, LHost),
ok.
-spec remove_mam_for_user(binary(), binary()) ->
{ok, binary()} | {error, binary()}.
remove_mam_for_user(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
Mod = gen_mod:db_mod(LServer, ?MODULE),
case Mod:remove_from_archive(LUser, LServer, none) of
ok ->
{ok, <<"MAM archive removed">>};
{error, Bin} when is_binary(Bin) ->
{error, Bin};
{error, _} ->
{error, <<"Db returned error">>}
end.
-spec remove_mam_for_user_with_peer(binary(), binary(), binary()) ->
{ok, binary()} | {error, binary()}.
remove_mam_for_user_with_peer(User, Server, Peer) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
try jid:decode(Peer) of
Jid ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
case Mod:remove_from_archive(LUser, LServer, Jid) of
ok ->
{ok, <<"MAM archive removed">>};
{error, Bin} when is_binary(Bin) ->
{error, Bin};
{error, _} ->
{error, <<"Db returned error">>}
end
catch _:_ ->
{error, <<"Invalid peer JID">>}
end.
-spec get_room_config([muc_roomconfig:property()], mod_muc_room:state(),
jid(), binary()) -> [muc_roomconfig:property()].
get_room_config(Fields, RoomState, _From, _Lang) ->
Config = RoomState#state.config,
Fields ++ [{mam, Config#config.mam}].
-spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary())
-> {pos_integer(), _}.
set_room_option(_Acc, {mam, Val}, _Lang) ->
{#config.mam, Val};
set_room_option(Acc, _Property, _Lang) ->
Acc.
-spec sm_receive_packet(stanza()) -> stanza().
sm_receive_packet(#message{to = #jid{lserver = LServer}} = Pkt) ->
init_stanza_id(Pkt, LServer);
sm_receive_packet(Acc) ->
Acc.
-spec user_receive_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}.
user_receive_packet({#message{from = Peer} = Pkt, #{jid := JID} = C2SState}) ->
LUser = JID#jid.luser,
LServer = JID#jid.lserver,
Pkt1 = case should_archive(Pkt, LServer) of
true ->
case store_msg(Pkt, LUser, LServer, Peer, recv) of
ok ->
mark_stored_msg(Pkt, JID);
_ ->
Pkt
end;
_ ->
Pkt
end,
{Pkt1, C2SState};
user_receive_packet(Acc) ->
Acc.
-spec user_send_packet({stanza(), c2s_state()})
-> {stanza(), c2s_state()}.
user_send_packet({#message{to = Peer} = Pkt, #{jid := JID} = C2SState}) ->
LUser = JID#jid.luser,
LServer = JID#jid.lserver,
Pkt1 = init_stanza_id(Pkt, LServer),
Pkt2 = case should_archive(Pkt1, LServer) of
true ->
case store_msg(xmpp:set_from_to(Pkt1, JID, Peer),
LUser, LServer, Peer, send) of
ok ->
mark_stored_msg(Pkt1, JID);
_ ->
Pkt1
end;
false ->
Pkt1
end,
{Pkt2, C2SState};
user_send_packet(Acc) ->
Acc.
-spec user_send_packet_strip_tag({stanza(), c2s_state()})
-> {stanza(), c2s_state()}.
user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) ->
LServer = JID#jid.lserver,
{strip_my_stanza_id(Pkt, LServer), C2SState};
user_send_packet_strip_tag(Acc) ->
Acc.
-spec offline_message({any(), message()}) -> {any(), message()}.
offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) ->
LUser = To#jid.luser,
LServer = To#jid.lserver,
case should_archive(Pkt, LServer) of
true ->
case store_msg(Pkt, LUser, LServer, Peer, recv) of
ok ->
{archived, mark_stored_msg(Pkt, To)};
_ ->
Acc
end;
false ->
Acc
end.
-spec muc_filter_message(message(), mod_muc_room:state(),
binary()) -> message().
muc_filter_message(#message{from = From} = Pkt,
#state{config = Config, jid = RoomJID} = MUCState,
FromNick) ->
LServer = RoomJID#jid.lserver,
Pkt1 = init_stanza_id(Pkt, LServer),
if Config#config.mam ->
StorePkt = strip_x_jid_tags(Pkt1),
case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of
ok ->
mark_stored_msg(Pkt1, RoomJID);
_ ->
Pkt1
end;
true ->
Pkt1
end;
muc_filter_message(Acc, _MUCState, _FromNick) ->
Acc.
-spec get_stanza_id(stanza()) -> integer().
get_stanza_id(#message{meta = #{stanza_id := ID}}) ->
ID.
-spec init_stanza_id(stanza(), binary()) -> stanza().
init_stanza_id(#message{meta = #{stanza_id := _ID}} = Pkt, _LServer) ->
Pkt;
init_stanza_id(Pkt, LServer) ->
ID = p1_time_compat:system_time(micro_seconds),
Pkt1 = strip_my_stanza_id(Pkt, LServer),
xmpp:put_meta(Pkt1, stanza_id, ID).
-spec set_stanza_id(stanza(), jid(), integer()) -> stanza().
set_stanza_id(Pkt, JID, ID) ->
BareJID = jid:remove_resource(JID),
Archived = #mam_archived{by = BareJID, id = ID},
StanzaID = #stanza_id{by = BareJID, id = ID},
NewEls = [Archived, StanzaID|xmpp:get_els(Pkt)],
xmpp:set_els(Pkt, NewEls).
-spec mark_stored_msg(message(), jid()) -> message().
mark_stored_msg(#message{meta = #{stanza_id := ID}} = Pkt, JID) ->
Pkt1 = set_stanza_id(Pkt, JID, integer_to_binary(ID)),
xmpp:put_meta(Pkt1, mam_archived, true).
% Query archive v0.2
process_iq_v0_2(#iq{from = #jid{lserver = LServer},
to = #jid{lserver = LServer},
type = get, sub_els = [#mam_query{}]} = IQ) ->
process_iq(LServer, IQ, chat);
process_iq_v0_2(IQ) ->
process_iq(IQ).
% Query archive v0.3
process_iq_v0_3(#iq{from = #jid{lserver = LServer},
to = #jid{lserver = LServer},
type = set, sub_els = [#mam_query{}]} = IQ) ->
process_iq(LServer, IQ, chat);
process_iq_v0_3(#iq{from = #jid{lserver = LServer},
to = #jid{lserver = LServer},
type = get, sub_els = [#mam_query{}]} = IQ) ->
process_iq(LServer, IQ);
process_iq_v0_3(IQ) ->
process_iq(IQ).
-spec muc_process_iq(ignore | iq(), mod_muc_room:state()) -> ignore | iq().
muc_process_iq(#iq{type = T, lang = Lang,
from = From,
sub_els = [#mam_query{xmlns = NS}]} = IQ,
MUCState)
when (T == set andalso (NS /= ?NS_MAM_TMP)) orelse
(T == get andalso NS == ?NS_MAM_TMP) ->
case may_enter_room(From, MUCState) of
true ->
LServer = MUCState#state.server_host,
Role = mod_muc_room:get_role(From, MUCState),
process_iq(LServer, IQ, {groupchat, Role, MUCState});
false ->
Text = <<"Only members may query archives of this room">>,
xmpp:make_error(IQ, xmpp:err_forbidden(Text, Lang))
end;
muc_process_iq(#iq{type = get,
sub_els = [#mam_query{xmlns = NS}]} = IQ,
MUCState) when NS /= ?NS_MAM_TMP ->
LServer = MUCState#state.server_host,
process_iq(LServer, IQ);
muc_process_iq(IQ, _MUCState) ->
IQ.
parse_query(#mam_query{xmlns = ?NS_MAM_TMP,
start = Start, 'end' = End,
with = With, withtext = Text}, _Lang) ->
{ok, [{start, Start}, {'end', End},
{with, With}, {withtext, Text}]};
parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) ->
X = xmpp_util:set_xdata_field(
#xdata_field{var = <<"FORM_TYPE">>,
type = hidden, values = [?NS_MAM_1]},
Query#mam_query.xdata),
try mam_query:decode(X#xdata.fields) of
Form -> {ok, Form}
catch _:{mam_query, Why} ->
Txt = mam_query:format_error(Why),
{error, xmpp:err_bad_request(Txt, Lang)}
end;
parse_query(#mam_query{}, _Lang) ->
{ok, []}.
disco_sm_features(empty, From, To, Node, Lang) ->
disco_sm_features({result, []}, From, To, Node, Lang);
disco_sm_features({result, OtherFeatures},
#jid{luser = U, lserver = S},
#jid{luser = U, lserver = S}, <<"">>, _Lang) ->
{result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0 |
OtherFeatures]};
disco_sm_features(Acc, _From, _To, _Node, _Lang) ->
Acc.
-spec message_is_archived(boolean(), c2s_state(), message()) -> boolean().
message_is_archived(true, _C2SState, _Pkt) ->
true;
message_is_archived(false, #{lserver := LServer}, Pkt) ->
case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage) of
true ->
is_archived(Pkt, LServer);
false ->
false
end.
delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>;
TypeBin == <<"groupchat">>;
TypeBin == <<"all">> ->
CurrentTime = p1_time_compat:system_time(micro_seconds),
Diff = Days * 24 * 60 * 60 * 1000000,
TimeStamp = misc:usec_to_now(CurrentTime - Diff),
Type = misc:binary_to_atom(TypeBin),
DBTypes = lists:usort(
lists:map(
fun(Host) ->
case gen_mod:get_module_opt(Host, ?MODULE, db_type) of
sql -> {sql, Host};
Other -> {Other, global}
end
end, ejabberd_config:get_myhosts())),
Results = lists:map(
fun({DBType, ServerHost}) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:delete_old_messages(ServerHost, TimeStamp, Type)
end, DBTypes),
case lists:filter(fun(Res) -> Res /= ok end, Results) of
[] -> ok;
[NotOk|_] -> NotOk
end;
delete_old_messages(_TypeBin, _Days) ->
unsupported_type.
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:export(LServer).
%%%===================================================================
%%% Internal functions
%%%===================================================================
process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
CommonFields = [{with, undefined},
{start, undefined},
{'end', undefined}],
ExtendedFields = Mod:extended_fields(),
Fields = mam_query:encode(CommonFields ++ ExtendedFields),
X = xmpp_util:set_xdata_field(
#xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]},
#xdata{type = form, fields = Fields}),
xmpp:make_iq_result(IQ, #mam_query{xmlns = NS, xdata = X}).
% Preference setting (both v0.2 & v0.3)
process_iq(#iq{type = set, lang = Lang,
sub_els = [#mam_prefs{default = undefined, xmlns = NS}]} = IQ) ->
Why = {missing_attr, <<"default">>, <<"prefs">>, NS},
ErrTxt = xmpp:io_format_error(Why),
xmpp:make_error(IQ, xmpp:err_bad_request(ErrTxt, Lang));
process_iq(#iq{from = #jid{luser = LUser, lserver = LServer},
to = #jid{lserver = LServer},
type = set, lang = Lang,
sub_els = [#mam_prefs{xmlns = NS,
default = Default,
always = Always0,
never = Never0}]} = IQ) ->
Always = lists:usort(get_jids(Always0)),
Never = lists:usort(get_jids(Never0)),
case write_prefs(LUser, LServer, LServer, Default, Always, Never) of
ok ->
NewPrefs = prefs_el(Default, Always, Never, NS),
xmpp:make_iq_result(IQ, NewPrefs);
_Err ->
Txt = <<"Database failure">>,
xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end;
process_iq(#iq{from = #jid{luser = LUser, lserver = LServer},
to = #jid{lserver = LServer},
type = get, sub_els = [#mam_prefs{xmlns = NS}]} = IQ) ->
Prefs = get_prefs(LUser, LServer),
PrefsEl = prefs_el(Prefs#archive_prefs.default,
Prefs#archive_prefs.always,
Prefs#archive_prefs.never,
NS),
xmpp:make_iq_result(IQ, PrefsEl);
process_iq(IQ) ->
xmpp:make_error(IQ, xmpp:err_not_allowed()).
process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang,
sub_els = [SubEl]} = IQ, MsgType) ->
case MsgType of
chat ->
maybe_activate_mam(LUser, LServer);
{groupchat, _Role, _MUCState} ->
ok
end,
case SubEl of
#mam_query{rsm = #rsm_set{index = I}} when is_integer(I) ->
Txt = <<"Unsupported <index/> element">>,
xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang));
#mam_query{rsm = RSM, xmlns = NS} ->
case parse_query(SubEl, Lang) of
{ok, Query} ->
NewRSM = limit_max(RSM, NS),
select_and_send(LServer, Query, NewRSM, IQ, MsgType);
{error, Err} ->
xmpp:make_error(IQ, Err)
end
end.
-spec should_archive(message(), binary()) -> boolean().
should_archive(#message{type = error}, _LServer) ->
false;
should_archive(#message{type = groupchat}, _LServer) ->
false;
should_archive(#message{meta = #{from_offline := true}}, _LServer) ->
false;
should_archive(#message{body = Body, subject = Subject,
type = Type} = Pkt, LServer) ->
case is_archived(Pkt, LServer) of
true ->
false;
false ->
case check_store_hint(Pkt) of
store ->
true;
no_store ->
false;
none when Type == headline ->
false;
none ->
xmpp:get_text(Body) /= <<>> orelse
xmpp:get_text(Subject) /= <<>>
end
end;
should_archive(_, _LServer) ->
false.
-spec strip_my_stanza_id(stanza(), binary()) -> stanza().
strip_my_stanza_id(Pkt, LServer) ->
Els = xmpp:get_els(Pkt),
NewEls = lists:filter(
fun(El) ->
Name = xmpp:get_name(El),
NS = xmpp:get_ns(El),
if (Name == <<"archived">> andalso NS == ?NS_MAM_TMP);
(Name == <<"stanza-id">> andalso NS == ?NS_SID_0) ->
try xmpp:decode(El) of
#mam_archived{by = By} ->
By#jid.lserver /= LServer;
#stanza_id{by = By} ->
By#jid.lserver /= LServer
catch _:{xmpp_codec, _} ->
false
end;
true ->
true
end
end, Els),
xmpp:set_els(Pkt, NewEls).
-spec strip_x_jid_tags(stanza()) -> stanza().
strip_x_jid_tags(Pkt) ->
Els = xmpp:get_els(Pkt),
NewEls = lists:filter(
fun(El) ->
case xmpp:get_name(El) of
<<"x">> ->
NS = xmpp:get_ns(El),
Items = if NS == ?NS_MUC_USER;
NS == ?NS_MUC_ADMIN;
NS == ?NS_MUC_OWNER ->
try xmpp:decode(El) of
#muc_user{items = Is} -> Is;
#muc_admin{items = Is} -> Is;
#muc_owner{items = Is} -> Is
catch _:{xmpp_codec, _} ->
[]
end;
true ->
[]
end,
not lists:any(
fun(#muc_item{jid = JID}) ->
JID /= undefined
end, Items);
_ ->
true
end
end, Els),
xmpp:set_els(Pkt, NewEls).
-spec should_archive_peer(binary(), binary(),
#archive_prefs{}, jid()) -> boolean().
should_archive_peer(LUser, LServer,
#archive_prefs{default = Default,
always = Always,
never = Never},
Peer) ->
LPeer = jid:remove_resource(jid:tolower(Peer)),
case lists:member(LPeer, Always) of
true ->
true;
false ->
case lists:member(LPeer, Never) of
true ->
false;
false ->
case Default of
always -> true;
never -> false;
roster ->
{Sub, _, _} = ejabberd_hooks:run_fold(
roster_get_jid_info,
LServer, {none, none, []},
[LUser, LServer, Peer]),
Sub == both orelse Sub == from orelse Sub == to
end
end
end.
-spec should_archive_muc(message()) -> boolean().
should_archive_muc(#message{type = groupchat,
body = Body, subject = Subj} = Pkt) ->
case check_store_hint(Pkt) of
store ->
true;
no_store ->
false;
none ->
case xmpp:get_text(Body) of
<<"">> ->
case xmpp:get_text(Subj) of
<<"">> ->
false;
_ ->
true
end;
_ ->
true
end
end;
should_archive_muc(_) ->
false.
-spec check_store_hint(message()) -> store | no_store | none.
check_store_hint(Pkt) ->
case has_store_hint(Pkt) of
true ->
store;
false ->
case has_no_store_hint(Pkt) of
true ->
no_store;
false ->
none
end
end.
-spec has_store_hint(message()) -> boolean().
has_store_hint(Message) ->
xmpp:has_subtag(Message, #hint{type = 'store'}).
-spec has_no_store_hint(message()) -> boolean().
has_no_store_hint(Message) ->
xmpp:has_subtag(Message, #hint{type = 'no-store'}) orelse
xmpp:has_subtag(Message, #hint{type = 'no-storage'}) orelse
xmpp:has_subtag(Message, #hint{type = 'no-permanent-store'}) orelse
xmpp:has_subtag(Message, #hint{type = 'no-permanent-storage'}).
-spec is_archived(message(), binary()) -> boolean().
is_archived(Pkt, LServer) ->
case xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}) of
#stanza_id{by = #jid{lserver = LServer}} ->
true;
_ ->
false
end.
-spec may_enter_room(jid(), mod_muc_room:state()) -> boolean().
may_enter_room(From,
#state{config = #config{members_only = false}} = MUCState) ->
mod_muc_room:get_affiliation(From, MUCState) /= outcast;
may_enter_room(From, MUCState) ->
mod_muc_room:is_occupant_or_admin(From, MUCState).
-spec store_msg(message(), binary(), binary(), jid(), send | recv)
-> ok | pass | any().
store_msg(Pkt, LUser, LServer, Peer, Dir) ->
Prefs = get_prefs(LUser, LServer),
case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt} of
{true, #message{meta = #{sm_copy := true}}} ->
ok; % Already stored.
{true, _} ->
case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
[LUser, LServer, Peer, chat, Dir]) of
drop ->
pass;
Pkt1 ->
US = {LUser, LServer},
ID = get_stanza_id(Pkt1),
El = xmpp:encode(Pkt1),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir, ID)
end;
{false, _} ->
pass
end.
-spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary())
-> ok | pass | any().
store_muc(MUCState, Pkt, RoomJID, Peer, Nick) ->
case should_archive_muc(Pkt) of
true ->
{U, S, _} = jid:tolower(RoomJID),
LServer = MUCState#state.server_host,
case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
[U, S, Peer, groupchat, recv]) of
drop ->
pass;
Pkt1 ->
US = {U, S},
ID = get_stanza_id(Pkt1),
El = xmpp:encode(Pkt1),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:store(El, LServer, US, groupchat, Peer, Nick, recv, ID)
end;
false ->
pass
end.
write_prefs(LUser, LServer, Host, Default, Always, Never) ->
Prefs = #archive_prefs{us = {LUser, LServer},
default = Default,
always = Always,
never = Never},
Mod = gen_mod:db_mod(Host, ?MODULE),
case Mod:write_prefs(LUser, LServer, Prefs, Host) of
ok ->
case use_cache(Mod, LServer) of
true ->
ets_cache:delete(archive_prefs_cache, {LUser, LServer},
cache_nodes(Mod, LServer));
false ->
ok
end;
_Err ->
{error, db_failure}
end.
get_prefs(LUser, LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Res = case use_cache(Mod, LServer) of
true ->
ets_cache:lookup(archive_prefs_cache, {LUser, LServer},
fun() -> Mod:get_prefs(LUser, LServer) end);
false ->
Mod:get_prefs(LUser, LServer)
end,
case Res of
{ok, Prefs} ->
Prefs;
error ->
ActivateOpt = gen_mod:get_module_opt(
LServer, ?MODULE,
request_activates_archiving),
case ActivateOpt of
true ->
#archive_prefs{us = {LUser, LServer}, default = never};
false ->
Default = gen_mod:get_module_opt(
LServer, ?MODULE, default),
#archive_prefs{us = {LUser, LServer}, default = Default}
end
end.
prefs_el(Default, Always, Never, NS) ->
#mam_prefs{default = Default,
always = [jid:make(LJ) || LJ <- Always],
never = [jid:make(LJ) || LJ <- Never],
xmlns = NS}.
maybe_activate_mam(LUser, LServer) ->
ActivateOpt = gen_mod:get_module_opt(
LServer, ?MODULE, request_activates_archiving),
case ActivateOpt of
true ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Res = case use_cache(Mod, LServer) of
true ->
ets_cache:lookup(archive_prefs_cache,
{LUser, LServer},
fun() ->
Mod:get_prefs(LUser, LServer)
end);
false ->
Mod:get_prefs(LUser, LServer)
end,
case Res of
{ok, _Prefs} ->
ok;
error ->
Default = gen_mod:get_module_opt(
LServer, ?MODULE, default),
write_prefs(LUser, LServer, LServer, Default, [], [])
end;
false ->
ok
end.
select_and_send(LServer, Query, RSM, #iq{from = From, to = To} = IQ, MsgType) ->
{Msgs, IsComplete, Count} =
case MsgType of
chat ->
select(LServer, From, From, Query, RSM, MsgType);
{groupchat, _Role, _MUCState} ->
select(LServer, From, To, Query, RSM, MsgType)
end,
SortedMsgs = lists:keysort(2, Msgs),
send(SortedMsgs, Count, IsComplete, IQ).
select(_LServer, JidRequestor, JidArchive, Query, RSM,
{groupchat, _Role, #state{config = #config{mam = false},
history = History}} = MsgType) ->
Start = proplists:get_value(start, Query),
End = proplists:get_value('end', Query),
#lqueue{queue = Q} = History,
L = p1_queue:len(Q),
Msgs =
lists:flatmap(
fun({Nick, Pkt, _HaveSubject, Now, _Size}) ->
TS = misc:now_to_usec(Now),
case match_interval(Now, Start, End) and
match_rsm(Now, RSM) of
true ->
case msg_to_el(#archive_msg{
id = integer_to_binary(TS),
type = groupchat,
timestamp = Now,
peer = undefined,
nick = Nick,
packet = Pkt},
MsgType, JidRequestor, JidArchive) of
{ok, Msg} ->
[{integer_to_binary(TS), TS, Msg}];
{error, _} ->
[]
end;
false ->
[]
end
end, p1_queue:to_list(Q)),
case RSM of
#rsm_set{max = Max, before = Before} when is_binary(Before) ->
{NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max),
{NewMsgs, IsComplete, L};
#rsm_set{max = Max} ->
{NewMsgs, IsComplete} = filter_by_max(Msgs, Max),
{NewMsgs, IsComplete, L};
_ ->
{Msgs, true, L}
end;
select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) ->
case might_expose_jid(Query, MsgType) of
true ->
{[], true, 0};
false ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType)
end.
msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick,
peer = Peer, id = ID},
MsgType, JidRequestor, #jid{lserver = LServer} = JidArchive) ->
CodecOpts = ejabberd_config:codec_options(LServer),
try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of
Pkt1 ->
Pkt2 = set_stanza_id(Pkt1, JidArchive, ID),
Pkt3 = maybe_update_from_to(
Pkt2, JidRequestor, JidArchive, Peer, MsgType, Nick),
Delay = #delay{stamp = TS, from = jid:make(LServer)},
{ok, #forwarded{sub_els = [Pkt3], delay = Delay}}
catch _:{xmpp_codec, Why} ->
?ERROR_MSG("Failed to decode raw element ~p from message "
"archive of user ~s: ~s",
[El, jid:encode(JidArchive), xmpp:format_error(Why)]),
{error, invalid_xml}
end.
maybe_update_from_to(#message{sub_els = Els} = Pkt, JidRequestor, JidArchive,
Peer, {groupchat, Role,
#state{config = #config{anonymous = Anon}}},
Nick) ->
ExposeJID = case {Peer, JidRequestor} of
{undefined, _JidRequestor} ->
false;
{{U, S, _R}, #jid{luser = U, lserver = S}} ->
true;
{_Peer, _JidRequestor} when not Anon; Role == moderator ->
true;
{_Peer, _JidRequestor} ->
false
end,
Items = case ExposeJID of
true ->
[#muc_user{items = [#muc_item{jid = Peer}]}];
false ->
[]
end,
Pkt#message{from = jid:replace_resource(JidArchive, Nick),
to = undefined,
sub_els = Items ++ Els};
maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, chat, _Nick) ->
Pkt.
-spec send([{binary(), integer(), xmlel()}],
non_neg_integer(), boolean(), iq()) -> iq() | ignore.
send(Msgs, Count, IsComplete,
#iq{from = From, to = To,
sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) ->
Hint = #hint{type = 'no-store'},
Els = lists:map(
fun({ID, _IDInt, El}) ->
#message{from = To,
to = From,
sub_els = [#mam_result{xmlns = NS,
id = ID,
queryid = QID,
sub_els = [El]}]}
end, Msgs),
RSMOut = make_rsm_out(Msgs, Count),
Result = if NS == ?NS_MAM_TMP ->
#mam_query{xmlns = NS, id = QID, rsm = RSMOut};
true ->
#mam_fin{xmlns = NS, id = QID, rsm = RSMOut,
complete = IsComplete}
end,
if NS /= ?NS_MAM_0 ->
lists:foreach(
fun(El) ->
ejabberd_router:route(El)
end, Els),
xmpp:make_iq_result(IQ, Result);
true ->
ejabberd_router:route(xmpp:make_iq_result(IQ)),
lists:foreach(
fun(El) ->
ejabberd_router:route(El)
end, Els),
ejabberd_router:route(
#message{from = To, to = From, sub_els = [Result, Hint]}),
ignore
end.
-spec make_rsm_out([{binary(), integer(), xmlel()}], non_neg_integer()) -> rsm_set().
make_rsm_out([], Count) ->
#rsm_set{count = Count};
make_rsm_out([{FirstID, _, _}|_] = Msgs, Count) ->
{LastID, _, _} = lists:last(Msgs),
#rsm_set{first = #rsm_first{data = FirstID}, last = LastID, count = Count}.
filter_by_max(Msgs, undefined) ->
{Msgs, true};
filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 ->
{lists:sublist(Msgs, Len), length(Msgs) =< Len};
filter_by_max(_Msgs, _Junk) ->
{[], true}.
-spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined.
limit_max(RSM, ?NS_MAM_TMP) ->
RSM; % XEP-0313 v0.2 doesn't require clients to support RSM.
limit_max(undefined, _NS) ->
#rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) ->
RSM#rsm_set{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE ->
RSM#rsm_set{max = ?MAX_PAGE_SIZE};
limit_max(RSM, _NS) ->
RSM.
match_interval(Now, Start, undefined) ->
Now >= Start;
match_interval(Now, Start, End) ->
(Now >= Start) and (Now =< End).
match_rsm(Now, #rsm_set{'after' = ID}) when is_binary(ID), ID /= <<"">> ->
Now1 = (catch misc:usec_to_now(binary_to_integer(ID))),
Now > Now1;
match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> ->
Now1 = (catch misc:usec_to_now(binary_to_integer(ID))),
Now < Now1;
match_rsm(_Now, _) ->
true.
might_expose_jid(Query,
{groupchat, Role, #state{config = #config{anonymous = true}}})
when Role /= moderator ->
proplists:is_defined(with, Query);
might_expose_jid(_Query, _MsgType) ->
false.
get_jids(undefined) ->
[];
get_jids(Js) ->
[jid:tolower(jid:remove_resource(J)) || J <- Js].
get_commands_spec() ->
[#ejabberd_commands{name = delete_old_mam_messages, tags = [purge],
desc = "Delete MAM messages older than DAYS",
longdesc = "Valid message TYPEs: "
"\"chat\", \"groupchat\", \"all\".",
module = ?MODULE, function = delete_old_messages,
args_desc = ["Type of messages to delete (chat, groupchat, all)",
"Days to keep messages"],
args_example = [<<"all">>, 31],
args = [{type, binary}, {days, integer}],
result = {res, rescode}},
#ejabberd_commands{name = remove_mam_for_user, tags = [mam],
desc = "Remove mam archive for user",
module = ?MODULE, function = remove_mam_for_user,
args = [{user, binary}, {server, binary}],
args_desc = ["Username", "Server"],
args_example = [<<"bob">>, <<"example.com">>],
result = {res, restuple},
result_desc = "Result tuple",
result_example = {ok, <<"MAM archive removed">>}},
#ejabberd_commands{name = remove_mam_for_user_with_peer, tags = [mam],
desc = "Remove mam archive for user with peer",
module = ?MODULE, function = remove_mam_for_user_with_peer,
args = [{user, binary}, {server, binary}, {with, binary}],
args_desc = ["Username", "Server", "Peer"],
args_example = [<<"bob">>, <<"example.com">>, <<"anne@example.com">>],
result = {res, restuple},
result_desc = "Result tuple",
result_example = {ok, <<"MAM archive removed">>}}
].
mod_opt_type(assume_mam_usage) ->
fun (B) when is_boolean(B) -> B end;
mod_opt_type(O) when O == cache_life_time; O == cache_size ->
fun (I) when is_integer(I), I > 0 -> I;
(infinity) -> infinity
end;
mod_opt_type(O) when O == use_cache; O == cache_missed ->
fun (B) when is_boolean(B) -> B end;
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
mod_opt_type(default) ->
fun (always) -> always;
(never) -> never;
(roster) -> roster
end;
mod_opt_type(request_activates_archiving) ->
fun (B) when is_boolean(B) -> B end.
mod_options(Host) ->
[{assume_mam_usage, false},
{default, never},
{request_activates_archiving, false},
{db_type, ejabberd_config:default_db(Host, ?MODULE)},
{use_cache, ejabberd_config:use_cache(Host)},
{cache_size, ejabberd_config:cache_size(Host)},
{cache_missed, ejabberd_config:cache_missed(Host)},
{cache_life_time, ejabberd_config:cache_life_time(Host)}].