2016-04-15 14:11:31 +02:00
|
|
|
%%%-------------------------------------------------------------------
|
2016-12-27 10:44:07 +01:00
|
|
|
%%% File : mod_mam_mnesia.erl
|
|
|
|
%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net>
|
2016-04-15 14:11:31 +02:00
|
|
|
%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
|
2016-12-27 10:44:07 +01:00
|
|
|
%%%
|
|
|
|
%%%
|
2017-01-02 21:41:53 +01:00
|
|
|
%%% ejabberd, Copyright (C) 2002-2017 ProcessOne
|
2016-12-27 10:44:07 +01:00
|
|
|
%%%
|
|
|
|
%%% 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.
|
|
|
|
%%%
|
|
|
|
%%%----------------------------------------------------------------------
|
|
|
|
|
2016-04-15 14:11:31 +02:00
|
|
|
-module(mod_mam_mnesia).
|
|
|
|
|
|
|
|
-behaviour(mod_mam).
|
|
|
|
|
|
|
|
%% API
|
|
|
|
-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3,
|
2016-11-18 11:38:08 +01:00
|
|
|
extended_fields/0, store/7, write_prefs/4, get_prefs/2, select/6]).
|
2016-04-15 14:11:31 +02:00
|
|
|
|
|
|
|
-include_lib("stdlib/include/ms_transform.hrl").
|
2016-07-25 12:50:30 +02:00
|
|
|
-include("xmpp.hrl").
|
2016-05-24 00:08:23 +02:00
|
|
|
-include("logger.hrl").
|
2016-04-15 14:11:31 +02:00
|
|
|
-include("mod_mam.hrl").
|
|
|
|
|
|
|
|
-define(BIN_GREATER_THAN(A, B),
|
|
|
|
((A > B andalso byte_size(A) == byte_size(B))
|
|
|
|
orelse byte_size(A) > byte_size(B))).
|
|
|
|
-define(BIN_LESS_THAN(A, B),
|
|
|
|
((A < B andalso byte_size(A) == byte_size(B))
|
|
|
|
orelse byte_size(A) < byte_size(B))).
|
|
|
|
|
2016-05-24 00:25:52 +02:00
|
|
|
-define(TABLE_SIZE_LIMIT, 2000000000). % A bit less than 2 GiB.
|
|
|
|
|
2016-04-15 14:11:31 +02:00
|
|
|
%%%===================================================================
|
|
|
|
%%% API
|
|
|
|
%%%===================================================================
|
|
|
|
init(_Host, _Opts) ->
|
2016-11-30 11:09:17 +01:00
|
|
|
ejabberd_mnesia:create(?MODULE, archive_msg,
|
2016-04-15 14:11:31 +02:00
|
|
|
[{disc_only_copies, [node()]},
|
|
|
|
{type, bag},
|
|
|
|
{attributes, record_info(fields, archive_msg)}]),
|
2016-11-30 11:09:17 +01:00
|
|
|
ejabberd_mnesia:create(?MODULE, archive_prefs,
|
2016-04-15 14:11:31 +02:00
|
|
|
[{disc_only_copies, [node()]},
|
|
|
|
{attributes, record_info(fields, archive_prefs)}]).
|
|
|
|
|
|
|
|
remove_user(LUser, LServer) ->
|
|
|
|
US = {LUser, LServer},
|
|
|
|
F = fun () ->
|
|
|
|
mnesia:delete({archive_msg, US}),
|
|
|
|
mnesia:delete({archive_prefs, US})
|
|
|
|
end,
|
|
|
|
mnesia:transaction(F).
|
|
|
|
|
|
|
|
remove_room(_LServer, LName, LHost) ->
|
|
|
|
remove_user(LName, LHost).
|
|
|
|
|
|
|
|
delete_old_messages(global, TimeStamp, Type) ->
|
2016-06-21 10:43:19 +02:00
|
|
|
mnesia:change_table_copy_type(archive_msg, node(), disc_copies),
|
|
|
|
Result = delete_old_user_messages(mnesia:dirty_first(archive_msg), TimeStamp, Type),
|
|
|
|
mnesia:change_table_copy_type(archive_msg, node(), disc_only_copies),
|
|
|
|
Result.
|
2016-05-24 00:08:23 +02:00
|
|
|
|
|
|
|
delete_old_user_messages('$end_of_table', _TimeStamp, _Type) ->
|
|
|
|
ok;
|
|
|
|
delete_old_user_messages(User, TimeStamp, Type) ->
|
|
|
|
F = fun() ->
|
|
|
|
Msgs = mnesia:read(archive_msg, User),
|
|
|
|
Keep = lists:filter(
|
|
|
|
fun(#archive_msg{timestamp = MsgTS,
|
|
|
|
type = MsgType}) ->
|
|
|
|
MsgTS >= TimeStamp orelse (Type /= all andalso
|
|
|
|
Type /= MsgType)
|
|
|
|
end, Msgs),
|
|
|
|
if length(Keep) < length(Msgs) ->
|
|
|
|
mnesia:delete({archive_msg, User}),
|
|
|
|
lists:foreach(fun(Msg) -> mnesia:write(Msg) end, Keep);
|
|
|
|
true ->
|
|
|
|
ok
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
case mnesia:transaction(F) of
|
|
|
|
{atomic, ok} ->
|
|
|
|
delete_old_user_messages(mnesia:dirty_next(archive_msg, User),
|
|
|
|
TimeStamp, Type);
|
|
|
|
{aborted, Err} ->
|
|
|
|
?ERROR_MSG("Cannot delete old MAM messages: ~s", [Err]),
|
|
|
|
Err
|
|
|
|
end.
|
2016-04-15 14:11:31 +02:00
|
|
|
|
|
|
|
extended_fields() ->
|
|
|
|
[].
|
|
|
|
|
|
|
|
store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir) ->
|
2016-05-24 00:25:52 +02:00
|
|
|
case {mnesia:table_info(archive_msg, disc_only_copies),
|
|
|
|
mnesia:table_info(archive_msg, memory)} of
|
|
|
|
{[_|_], TableSize} when TableSize > ?TABLE_SIZE_LIMIT ->
|
2016-05-24 07:58:07 +02:00
|
|
|
?ERROR_MSG("MAM archives too large, won't store message for ~s@~s",
|
2016-05-24 00:25:52 +02:00
|
|
|
[LUser, LServer]),
|
|
|
|
{error, overflow};
|
|
|
|
_ ->
|
|
|
|
LPeer = {PUser, PServer, _} = jid:tolower(Peer),
|
|
|
|
TS = p1_time_compat:timestamp(),
|
2016-09-24 22:34:28 +02:00
|
|
|
ID = integer_to_binary(now_to_usec(TS)),
|
2016-05-24 00:25:52 +02:00
|
|
|
F = fun() ->
|
|
|
|
mnesia:write(
|
|
|
|
#archive_msg{us = {LUser, LServer},
|
|
|
|
id = ID,
|
|
|
|
timestamp = TS,
|
|
|
|
peer = LPeer,
|
|
|
|
bare_peer = {PUser, PServer, <<>>},
|
|
|
|
type = Type,
|
|
|
|
nick = Nick,
|
|
|
|
packet = Pkt})
|
|
|
|
end,
|
|
|
|
case mnesia:transaction(F) of
|
|
|
|
{atomic, ok} ->
|
|
|
|
{ok, ID};
|
|
|
|
{aborted, Err} ->
|
|
|
|
?ERROR_MSG("Cannot add message to MAM archive of ~s@~s: ~s",
|
|
|
|
[LUser, LServer, Err]),
|
|
|
|
Err
|
|
|
|
end
|
2016-04-15 14:11:31 +02:00
|
|
|
end.
|
|
|
|
|
|
|
|
write_prefs(_LUser, _LServer, Prefs, _ServerHost) ->
|
|
|
|
mnesia:dirty_write(Prefs).
|
|
|
|
|
|
|
|
get_prefs(LUser, LServer) ->
|
|
|
|
case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of
|
|
|
|
[Prefs] ->
|
|
|
|
{ok, Prefs};
|
|
|
|
_ ->
|
|
|
|
error
|
|
|
|
end.
|
|
|
|
|
|
|
|
select(_LServer, JidRequestor,
|
|
|
|
#jid{luser = LUser, lserver = LServer} = JidArchive,
|
2016-11-18 11:38:08 +01:00
|
|
|
Query, RSM, MsgType) ->
|
|
|
|
Start = proplists:get_value(start, Query),
|
|
|
|
End = proplists:get_value('end', Query),
|
|
|
|
With = proplists:get_value(with, Query),
|
2016-09-13 11:30:05 +02:00
|
|
|
LWith = if With /= undefined -> jid:tolower(With);
|
|
|
|
true -> undefined
|
|
|
|
end,
|
|
|
|
MS = make_matchspec(LUser, LServer, Start, End, LWith),
|
2016-04-15 14:11:31 +02:00
|
|
|
Msgs = mnesia:dirty_select(archive_msg, MS),
|
|
|
|
SortedMsgs = lists:keysort(#archive_msg.timestamp, Msgs),
|
|
|
|
{FilteredMsgs, IsComplete} = filter_by_rsm(SortedMsgs, RSM),
|
|
|
|
Count = length(Msgs),
|
2017-01-20 21:21:06 +01:00
|
|
|
Result = {lists:flatmap(
|
2016-06-29 22:32:59 +02:00
|
|
|
fun(Msg) ->
|
2017-01-20 21:21:06 +01:00
|
|
|
case mod_mam:msg_to_el(
|
|
|
|
Msg, MsgType, JidRequestor, JidArchive) of
|
|
|
|
{ok, El} ->
|
|
|
|
[{Msg#archive_msg.id,
|
|
|
|
binary_to_integer(Msg#archive_msg.id),
|
|
|
|
El}];
|
|
|
|
{error, _} ->
|
|
|
|
[]
|
|
|
|
end
|
2016-06-29 22:32:59 +02:00
|
|
|
end, FilteredMsgs), IsComplete, Count},
|
|
|
|
erlang:garbage_collect(),
|
|
|
|
Result.
|
2016-04-15 14:11:31 +02:00
|
|
|
|
|
|
|
%%%===================================================================
|
|
|
|
%%% Internal functions
|
|
|
|
%%%===================================================================
|
|
|
|
now_to_usec({MSec, Sec, USec}) ->
|
|
|
|
(MSec*1000000 + Sec)*1000000 + USec.
|
|
|
|
|
2016-09-13 11:30:05 +02:00
|
|
|
make_matchspec(LUser, LServer, Start, undefined, With) ->
|
|
|
|
%% List is always greater than a tuple
|
|
|
|
make_matchspec(LUser, LServer, Start, [], With);
|
2016-04-15 14:11:31 +02:00
|
|
|
make_matchspec(LUser, LServer, Start, End, {_, _, <<>>} = With) ->
|
|
|
|
ets:fun2ms(
|
|
|
|
fun(#archive_msg{timestamp = TS,
|
|
|
|
us = US,
|
|
|
|
bare_peer = BPeer} = Msg)
|
|
|
|
when Start =< TS, End >= TS,
|
|
|
|
US == {LUser, LServer},
|
|
|
|
BPeer == With ->
|
|
|
|
Msg
|
|
|
|
end);
|
|
|
|
make_matchspec(LUser, LServer, Start, End, {_, _, _} = With) ->
|
|
|
|
ets:fun2ms(
|
|
|
|
fun(#archive_msg{timestamp = TS,
|
|
|
|
us = US,
|
|
|
|
peer = Peer} = Msg)
|
|
|
|
when Start =< TS, End >= TS,
|
|
|
|
US == {LUser, LServer},
|
|
|
|
Peer == With ->
|
|
|
|
Msg
|
|
|
|
end);
|
2016-07-25 12:50:30 +02:00
|
|
|
make_matchspec(LUser, LServer, Start, End, undefined) ->
|
2016-04-15 14:11:31 +02:00
|
|
|
ets:fun2ms(
|
|
|
|
fun(#archive_msg{timestamp = TS,
|
|
|
|
us = US,
|
|
|
|
peer = Peer} = Msg)
|
|
|
|
when Start =< TS, End >= TS,
|
|
|
|
US == {LUser, LServer} ->
|
|
|
|
Msg
|
|
|
|
end).
|
|
|
|
|
2016-07-25 12:50:30 +02:00
|
|
|
filter_by_rsm(Msgs, undefined) ->
|
2016-04-15 14:11:31 +02:00
|
|
|
{Msgs, true};
|
2016-07-25 12:50:30 +02:00
|
|
|
filter_by_rsm(_Msgs, #rsm_set{max = Max}) when Max < 0 ->
|
2016-04-15 14:11:31 +02:00
|
|
|
{[], true};
|
2016-07-25 12:50:30 +02:00
|
|
|
filter_by_rsm(Msgs, #rsm_set{max = Max, before = Before, 'after' = After}) ->
|
|
|
|
NewMsgs = if is_binary(After), After /= <<"">> ->
|
2016-04-15 14:11:31 +02:00
|
|
|
lists:filter(
|
|
|
|
fun(#archive_msg{id = I}) ->
|
2016-07-25 12:50:30 +02:00
|
|
|
?BIN_GREATER_THAN(I, After)
|
2016-04-15 14:11:31 +02:00
|
|
|
end, Msgs);
|
2016-07-25 12:50:30 +02:00
|
|
|
is_binary(Before), Before /= <<"">> ->
|
2016-04-15 14:11:31 +02:00
|
|
|
lists:foldl(
|
|
|
|
fun(#archive_msg{id = I} = Msg, Acc)
|
2016-07-25 12:50:30 +02:00
|
|
|
when ?BIN_LESS_THAN(I, Before) ->
|
2016-04-15 14:11:31 +02:00
|
|
|
[Msg|Acc];
|
|
|
|
(_, Acc) ->
|
|
|
|
Acc
|
|
|
|
end, [], Msgs);
|
2016-07-25 12:50:30 +02:00
|
|
|
is_binary(Before), Before == <<"">> ->
|
2016-04-15 14:11:31 +02:00
|
|
|
lists:reverse(Msgs);
|
2016-07-25 12:50:30 +02:00
|
|
|
true ->
|
2016-04-15 14:11:31 +02:00
|
|
|
Msgs
|
|
|
|
end,
|
|
|
|
filter_by_max(NewMsgs, Max).
|
|
|
|
|
|
|
|
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}.
|