1077 lines
37 KiB
Erlang
1077 lines
37 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : mod_archive.erl
|
|
%%% Author : Olivier Goffart <ogoffar@kde.org>
|
|
%%% Purpose : Message Archiving (JEP-0136)
|
|
%%% Created : 19 Aug 2006
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%% Version 0.0.1 2006-08-19
|
|
%% Version 0.0.2 2006-08-21
|
|
%% Version 0.0.3 2006-08-22
|
|
%% Version 0.0.4 2006-09-10 (RSM JEP-0059 v0.13 JEP-0136 v0.6 with RSM)
|
|
|
|
|
|
%% Options:
|
|
%% save_default -> true | false if messages are stored by default or not
|
|
%% session_duration -> time in secondes before the timeout of a session
|
|
|
|
|
|
-module(mod_archive).
|
|
-author('ogoffart@kde.org').
|
|
|
|
-behaviour(gen_server).
|
|
-behaviour(gen_mod).
|
|
|
|
-export([start_link/2, start/2, stop/1,
|
|
remove_user/2, send_packet/3, receive_packet/4,
|
|
process_iq/3, process_local_iq/3,
|
|
get_disco_features/5]).
|
|
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("jlib.hrl").
|
|
|
|
-record(state, {host, storages, save_default, session_duration}).
|
|
|
|
-define(PROCNAME, ejabberd_mod_archive).
|
|
-define(NS_ARCHIVE,
|
|
"http://www.xmpp.org/extensions/xep-0136.html#ns").
|
|
-define(NS_ARCHIVE_MANAGE,
|
|
"http://www.xmpp.org/extensions/xep-0136.html#ns-manage").
|
|
-define(NS_ARCHIVE_PREF,
|
|
"http://www.xmpp.org/extensions/xep-0136.html#ns-pref").
|
|
-define(NS_ARCHIVE_MANUAL,
|
|
"http://www.xmpp.org/extensions/xep-0136.html#ns-manual").
|
|
-define(INFINITY, calendar:datetime_to_gregorian_seconds({{2038,1,19},{0,0,0}})).
|
|
|
|
-define(MYDEBUG(Format, Args),
|
|
io:format("D(~p:~p:~p) : " ++ Format ++ "~n",
|
|
[calendar:local_time(), ?MODULE, ?LINE] ++ Args)).
|
|
|
|
|
|
%NOTE i was not sure what format to adopt for archive_option. otr_list is unused
|
|
-record(archive_options,
|
|
{us,
|
|
default = unset,
|
|
save_list = [],
|
|
nosave_list = [],
|
|
otr_list = []}).
|
|
|
|
%-record(archive_options, {usj, us, jid, type, value}).
|
|
|
|
-record(archive_message,
|
|
{usjs,
|
|
us,
|
|
jid,
|
|
start,
|
|
message_list = [],
|
|
subject = ""}).
|
|
|
|
-record(msg, {direction, secs, body}).
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
%%--------------------------------------------------------------------
|
|
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
|
|
%% Description: Starts the server
|
|
%%--------------------------------------------------------------------
|
|
start_link(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
|
|
|
|
start(Host, Opts) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
ChildSpec =
|
|
{Proc,
|
|
{?MODULE, start_link, [Host, Opts]},
|
|
temporary,
|
|
1000,
|
|
worker,
|
|
[?MODULE]},
|
|
supervisor:start_child(ejabberd_sup, ChildSpec).
|
|
|
|
stop(Host) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:call(Proc, stop),
|
|
supervisor:delete_child(ejabberd_sup, Proc).
|
|
|
|
%%====================================================================
|
|
%% gen_server callbacks
|
|
%%====================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: init(Args) -> {ok, State} |
|
|
%% {ok, State, Timeout} |
|
|
%% ignore |
|
|
%% {stop, Reason}
|
|
%% Description: Initiates the server
|
|
%%--------------------------------------------------------------------
|
|
init([Host, Opts]) ->
|
|
IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
|
|
SaveDefault = gen_mod:get_opt(save_default, Opts, false),
|
|
SessionDuration = gen_mod:get_opt(session_duration, Opts, 1300),
|
|
mnesia:create_table(archive_options,
|
|
[{disc_copies, [node()]},
|
|
{attributes, record_info(fields, archive_options)}]),
|
|
mnesia:create_table(archive_message,
|
|
[{disc_copies, [node()]},
|
|
{attributes, record_info(fields, archive_message)}]),
|
|
% mnesia:add_table_index(archive_options, us),
|
|
mnesia:add_table_index(archive_message, us),
|
|
ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50),
|
|
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE, ?MODULE, process_iq, IQDisc),
|
|
gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE, ?MODULE, process_local_iq, IQDisc),
|
|
ejabberd_hooks:add(user_send_packet, Host, ?MODULE, send_packet, 90),
|
|
ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, receive_packet, 90),
|
|
ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_disco_features, 99),
|
|
{ok, #state{host = Host,
|
|
storages = [],
|
|
save_default = SaveDefault,
|
|
session_duration = SessionDuration}}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
|
|
%% {reply, Reply, State, Timeout} |
|
|
%% {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, Reply, State} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling call messages
|
|
%%--------------------------------------------------------------------
|
|
handle_call(get_save_default, _From, State) ->
|
|
{reply, State#state.save_default, State};
|
|
handle_call(stop, _From, State) ->
|
|
{stop, normal, ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_cast(Msg, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_cast({addlog, Direction, LUser, LServer, JID, P}, State) ->
|
|
Storages = State#state.storages,
|
|
NewStorages =
|
|
case should_store_jid(LUser, LServer, JID,
|
|
State#state.save_default) of
|
|
false ->
|
|
Storages;
|
|
true ->
|
|
case parse_message(P) of
|
|
ignore ->
|
|
Storages;
|
|
Body ->
|
|
catch do_log(Storages, LUser, LServer, JID,
|
|
Direction, Body,
|
|
State#state.session_duration)
|
|
end
|
|
end,
|
|
{noreply, State#state{storages = NewStorages}};
|
|
handle_cast(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_info(Info, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling all non call/cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_info(_Info, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: terminate(Reason, State) -> void()
|
|
%% Description: This function is called by a gen_server when it is about to
|
|
%% terminate. It should be the opposite of Module:init/1 and do any necessary
|
|
%% cleaning up. When it returns, the gen_server terminates with Reason.
|
|
%% The return value is ignored.
|
|
%%--------------------------------------------------------------------
|
|
terminate(_Reason, State) ->
|
|
Host = State#state.host,
|
|
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_ARCHIVE),
|
|
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ARCHIVE),
|
|
ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, send_packet, 90),
|
|
ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, receive_packet, 90),
|
|
ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_disco_features, 99),
|
|
ok.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
|
|
%% Description: Convert process state when code is changed
|
|
%%--------------------------------------------------------------------
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% Workaround the fact that if the client send <iq type='get'>
|
|
%% it end up like <iq type='get' from='u@h' to = 'u@h'>
|
|
process_iq(From, To, IQ) ->
|
|
#iq{sub_el = SubEl} = IQ,
|
|
#jid{lserver = LServer, luser = LUser} = To,
|
|
#jid{luser = FromUser} = From,
|
|
case {LUser, LServer, lists:member(LServer, ?MYHOSTS)} of
|
|
{FromUser, _, true} ->
|
|
process_local_iq(From, To, IQ);
|
|
{"", _, true} ->
|
|
process_local_iq(From, To, IQ);
|
|
{"", "", _} ->
|
|
process_local_iq(From, To, IQ);
|
|
_ ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}
|
|
end.
|
|
|
|
process_local_iq(From, To, #iq{sub_el = SubEl} = IQ) ->
|
|
case lists:member(From#jid.lserver, ?MYHOSTS) of
|
|
false ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
|
|
true ->
|
|
{xmlelement, Name, _Attrs, _Els} = SubEl,
|
|
case Name of
|
|
"pref" -> process_local_iq_save(From, To, IQ);
|
|
"auto" -> process_local_iq_auto(From, To, IQ);
|
|
%%"otr" -> process_local_iq_otr(From, To, IQ);
|
|
"list" -> process_local_iq_list(From, To, IQ);
|
|
"retrieve" -> process_local_iq_retrieve(From, To, IQ);
|
|
"save" -> process_local_iq_store(From, To, IQ);
|
|
"remove" -> process_local_iq_remove(From, To, IQ);
|
|
_ -> IQ#iq{type = error,
|
|
sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]}
|
|
end
|
|
end.
|
|
|
|
|
|
remove_user(User, Server) ->
|
|
LUser = jlib:nodeprep(User),
|
|
LServer = jlib:nameprep(Server),
|
|
US = {LUser, LServer},
|
|
F = fun() ->
|
|
lists:foreach(
|
|
fun(R) ->
|
|
mnesia:delete_object(R)
|
|
end,
|
|
mnesia:index_read(archive_message, US, #archive_message.us)),
|
|
mnesia:delete({archive_options, US})
|
|
end,
|
|
mnesia:transaction(F).
|
|
|
|
get_disco_features(Acc, _From, _To, "", _Lang) ->
|
|
Features =
|
|
case Acc of
|
|
{result, I} -> I;
|
|
_ -> []
|
|
end,
|
|
{result, Features ++ [?NS_ARCHIVE_MANAGE,
|
|
?NS_ARCHIVE_PREF,
|
|
?NS_ARCHIVE_MANUAL]};
|
|
|
|
get_disco_features(Acc, _From, _To, _Node, _Lang) ->
|
|
Acc.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% 3 Automated archiving
|
|
%%
|
|
|
|
send_packet(From, To, P) ->
|
|
Host = From#jid.lserver,
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:cast(Proc, {addlog, to, From#jid.luser, Host, To, P}).
|
|
|
|
receive_packet(_JID, From, To, P) ->
|
|
Host = To#jid.lserver,
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
gen_server:cast(Proc, {addlog, from, To#jid.luser, Host, From, P}).
|
|
|
|
|
|
% parce a message and return the body string if sicessfull, return ignore if the message should not be stored
|
|
parse_message({xmlelement, "message", _, _} = Packet) ->
|
|
case xml:get_subtag(Packet, "body") of
|
|
false -> ignore;
|
|
Body_xml ->
|
|
case xml:get_tag_attr_s("type", Packet) of
|
|
"groupchat" -> ignore;
|
|
_ -> xml:get_tag_cdata(Body_xml)
|
|
end
|
|
end;
|
|
parse_message(_) -> ignore.
|
|
|
|
% archive the message Body return the list of new Storages
|
|
% Storages: a list of open storages key (usjs)
|
|
% LUser, LServer : the local user's information
|
|
% Jid : the contact's jid
|
|
% Body : the message body
|
|
do_log(Storages, LUser, LServer, Jid, Direction, Body, Session_Duration) ->
|
|
NStorages = smart_find_storage(LUser, LServer, Jid, Storages, get_timestamp() + Session_Duration),
|
|
[{Tm, {_, _, _, Start} = Key} | _] = NStorages,
|
|
Message = #msg{direction=Direction, secs=(Tm-Start), body = Body},
|
|
mnesia:transaction(fun() ->
|
|
NE = case mnesia:read({archive_message, Key}) of
|
|
[] ->
|
|
#archive_message{usjs= Key,
|
|
us = {LUser, LServer},
|
|
jid = jlib:jid_tolower(jlib:jid_remove_resource(Jid)),
|
|
start = Tm,
|
|
message_list = [Message]};
|
|
[E] -> E#archive_message{message_list=lists:append(E#archive_message.message_list, [Message])}
|
|
end,
|
|
mnesia:write(NE)
|
|
end),
|
|
NStorages.
|
|
|
|
%find a storage for Jid and move it on the begin on the storage list, if none are found, a new storage is created, old storages element are removed
|
|
smart_find_storage(LUser, LServer, Jid, [C | Tail], TimeStampLimit) ->
|
|
NGid=jlib:jid_remove_resource(jlib:jid_tolower(Jid)),
|
|
case C of
|
|
{_, {LUser, LServer, NGid, _} = St} ->
|
|
[{get_timestamp(), St} | Tail];
|
|
{Tm, _} ->
|
|
if Tm > TimeStampLimit ->
|
|
smart_find_storage(LUser, LServer, Jid, [], TimeStampLimit);
|
|
true ->
|
|
[UJ | NT] = smart_find_storage(LUser, LServer, Jid, Tail, TimeStampLimit),
|
|
[UJ | [C | NT]]
|
|
end
|
|
end;
|
|
|
|
|
|
smart_find_storage(LUser, LServer, Jid, [], _Limit) ->
|
|
Tm = get_timestamp(),
|
|
[{Tm, {LUser, LServer, jlib:jid_tolower(jlib:jid_remove_resource(Jid)), Tm}}].
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% 3.1 Preferences
|
|
%%
|
|
|
|
process_local_iq_save(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
|
|
Result = case Type of
|
|
set ->
|
|
{xmlelement, _Name, _Attrs, Els} = SubEl,
|
|
process_save_set(From#jid.luser, From#jid.lserver, Els);
|
|
get ->
|
|
process_save_get(From#jid.luser, From#jid.lserver)
|
|
end,
|
|
case Result of
|
|
{result, R} ->
|
|
IQ#iq{type = result, sub_el = [R]};
|
|
ok ->
|
|
broadcast_iq(From, IQ#iq{type = set, sub_el=[SubEl]}),
|
|
IQ#iq{type = result, sub_el = []};
|
|
{error, E} ->
|
|
IQ#iq{type = error, sub_el = [SubEl, E]};
|
|
_ ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
|
|
end.
|
|
|
|
|
|
|
|
|
|
% return {error, xmlelement} or {result, xmlelement}
|
|
process_save_get(LUser, LServer) ->
|
|
case catch mnesia:dirty_read(archive_options, {LUser, LServer}) of
|
|
{'EXIT', _Reason} ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR};
|
|
[] ->
|
|
{result,
|
|
{xmlelement, "pref", [{"xmlns", ?NS_ARCHIVE}],
|
|
default_element(LServer)}};
|
|
[#archive_options{default = Default,
|
|
save_list = SaveList,
|
|
nosave_list = NoSaveList}] ->
|
|
LItems = lists:append(
|
|
lists:map(fun(J) ->
|
|
{xmlelement, "item",
|
|
[{"jid", jlib:jid_to_string(J)},
|
|
{"save","body"}],
|
|
[]}
|
|
end, SaveList),
|
|
lists:map(fun(J) ->
|
|
{xmlelement, "item",
|
|
[{"jid", jlib:jid_to_string(J)},
|
|
{"save","false"}],
|
|
[]}
|
|
end, NoSaveList)),
|
|
DItem = case Default of
|
|
true -> % TODO: <auto/>
|
|
[{xmlelement, "default", [{"save", "body"}], []}];
|
|
false ->
|
|
[{xmlelement, "default", [{"save", "false"}], []}];
|
|
_ ->
|
|
default_element(LServer)
|
|
end,
|
|
{result, {xmlelement, "save", [{"xmlns", ?NS_ARCHIVE}], DItem ++ LItems}}
|
|
end.
|
|
|
|
%return the <default .../> element
|
|
default_element(Host) ->
|
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
|
AutoSave = gen_server:call(Proc, get_save_default),
|
|
SaveAttr = if
|
|
AutoSave -> "true";
|
|
true -> "false"
|
|
end,
|
|
[{xmlelement, "default", [{"save", "false"}, {"otr", "forbid"}], []},
|
|
{xmlelement, "auto", [{"save", SaveAttr}], []}].
|
|
|
|
|
|
% return {error, xmlelement} or {result, xmlelement} or ok
|
|
process_save_set(LUser, LServer, Elms) ->
|
|
F = fun() ->
|
|
NE = case mnesia:read({archive_options, {LUser, LServer}}) of
|
|
[] ->
|
|
#archive_options{us = {LUser, LServer}};
|
|
[E] ->
|
|
E
|
|
end,
|
|
SNE = transaction_parse_save_elem(NE, Elms),
|
|
case SNE of
|
|
{error, _} -> SNE;
|
|
_ -> mnesia:write(SNE)
|
|
end
|
|
end,
|
|
case mnesia:transaction(F) of
|
|
{atomic, {error, _} = Error} ->
|
|
Error;
|
|
{atomic, _} ->
|
|
ok;
|
|
_ ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
% transaction_parse_save_elem(archive_options, ListOfXmlElement) -> #archive_options
|
|
% parse the list of xml element, and modify the given archive_option
|
|
transaction_parse_save_elem(Options, [{xmlelement, "default", Attrs, _} | Tail]) ->
|
|
V = case xml:get_attr_s("save", Attrs) of
|
|
"true" -> true;
|
|
"false" -> false;
|
|
_ -> unset
|
|
end,
|
|
transaction_parse_save_elem(Options#archive_options{default = V}, Tail);
|
|
|
|
transaction_parse_save_elem(Options, [{xmlelement, "item", Attrs, _} | Tail]) ->
|
|
case jlib:string_to_jid(xml:get_attr_s("jid", Attrs)) of
|
|
error -> {error, ?ERR_JID_MALFORMED};
|
|
#jid{luser = LUser, lserver = LServer, lresource = LResource} ->
|
|
JID = {LUser, LServer, LResource},
|
|
case xml:get_attr_s("save", Attrs) of
|
|
"body" ->
|
|
transaction_parse_save_elem(
|
|
Options#archive_options{
|
|
save_list = [JID | lists:delete(JID, Options#archive_options.save_list)],
|
|
nosave_list = lists:delete(JID, Options#archive_options.nosave_list)
|
|
}, Tail);
|
|
"false" ->
|
|
transaction_parse_save_elem(
|
|
Options#archive_options{
|
|
save_list = lists:delete(JID, Options#archive_options.save_list),
|
|
nosave_list = [JID | lists:delete(JID, Options#archive_options.nosave_list)]
|
|
}, Tail);
|
|
_ ->
|
|
transaction_parse_save_elem(
|
|
Options#archive_options{
|
|
save_list = lists:delete(JID, Options#archive_options.save_list),
|
|
nosave_list = lists:delete(JID, Options#archive_options.nosave_list)
|
|
}, Tail)
|
|
end
|
|
end;
|
|
|
|
transaction_parse_save_elem(Options, []) -> Options;
|
|
transaction_parse_save_elem(Options, [_ | Tail]) ->
|
|
transaction_parse_save_elem(Options, Tail).
|
|
|
|
|
|
broadcast_iq(#jid{luser = User, lserver = Server}, IQ) ->
|
|
Fun = fun(Resource) ->
|
|
ejabberd_router:route(
|
|
jlib:make_jid("", Server, ""),
|
|
jlib:make_jid(User, Server, Resource),
|
|
jlib:iq_to_xml(IQ#iq{id="push"}))
|
|
end,
|
|
lists:foreach(Fun, ejabberd_sm:get_user_resources(User,Server)).
|
|
|
|
|
|
|
|
process_local_iq_auto(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
|
|
Result =
|
|
case Type of
|
|
set ->
|
|
{xmlelement, _Name, Attrs, _Els} = SubEl,
|
|
Auto = case xml:get_attr_s("save", Attrs) of
|
|
"true" -> true;
|
|
"false" -> false;
|
|
_ -> unset
|
|
end,
|
|
case Auto of
|
|
unset ->
|
|
{error, ?ERR_BAD_REQUEST};
|
|
_ ->
|
|
LUser = From#jid.luser,
|
|
LServer = From#jid.lserver,
|
|
F = fun() ->
|
|
Opts =
|
|
case mnesia:read({archive_options,
|
|
{LUser, LServer}}) of
|
|
[] ->
|
|
#archive_options{us = {LUser, LServer}};
|
|
[E] ->
|
|
E
|
|
end,
|
|
mnesia:write(Opts#archive_options{
|
|
default = Auto})
|
|
end,
|
|
mnesia:transaction(F),
|
|
{result, []}
|
|
end;
|
|
get ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end,
|
|
case Result of
|
|
{result, R} ->
|
|
IQ#iq{type = result, sub_el = R};
|
|
{error, E} ->
|
|
IQ#iq{type = error, sub_el = [SubEl, E]};
|
|
_ ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% 3.1 Off-the-Record Mode
|
|
%%
|
|
|
|
%TODO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% Utility function
|
|
|
|
% Return true if LUser@LServer should log message for the contact JID
|
|
should_store_jid(LUser, LServer, Jid, Service_Default) ->
|
|
case catch mnesia:dirty_read(archive_options, {LUser, LServer}) of
|
|
[#archive_options{default = Default, save_list = Save_list, nosave_list = Nosave_list}] ->
|
|
Jid_t = jlib:jid_tolower(Jid),
|
|
Jid_b = jlib:jid_remove_resource(Jid_t),
|
|
A = lists:member(Jid_t, Save_list),
|
|
B = lists:member(Jid_t, Nosave_list),
|
|
C = lists:member(Jid_b, Save_list),
|
|
D = lists:member(Jid_b, Nosave_list),
|
|
if A -> true;
|
|
B -> false;
|
|
C -> true;
|
|
D -> false;
|
|
Default == true -> true;
|
|
Default == false -> false;
|
|
true -> Service_Default
|
|
end;
|
|
_ -> Service_Default
|
|
end.
|
|
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% 4. Manual Archiving
|
|
%%
|
|
|
|
|
|
process_local_iq_store(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
|
|
#jid{luser = LUser, lserver = LServer} = From,
|
|
case Type of
|
|
get ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
|
|
set ->
|
|
case parse_store_element (LUser, LServer, SubEl) of
|
|
{error, E} -> IQ#iq{type = error, sub_el = [SubEl, E]};
|
|
Collection ->
|
|
case mnesia:transaction(fun() -> mnesia:write(Collection) end) of
|
|
{atomic, _} ->
|
|
IQ#iq{type = result, sub_el=[]};
|
|
_ ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}
|
|
end
|
|
end
|
|
end.
|
|
|
|
% return a #archive_message StoreElem is an xmlelement, or return {error, E}
|
|
parse_store_element(LUser, LServer,
|
|
{xmlelement, "save", _ChatAttrs, ChatSubEls}) ->
|
|
case xml:remove_cdata(ChatSubEls) of
|
|
[{xmlelement, "chat", Attrs, SubEls}] ->
|
|
case index_from_argument(LUser, LServer, Attrs) of
|
|
{error, E} -> {error, E};
|
|
{LUser, LServer, Jid, Start} = Index ->
|
|
Messages = parse_store_element_sub(SubEls),
|
|
#archive_message{usjs = Index,
|
|
us = {LUser, LServer},
|
|
jid = Jid,
|
|
start = Start,
|
|
subject = xml:get_attr_s("subject", Attrs),
|
|
message_list = Messages}
|
|
end;
|
|
_ ->
|
|
{error, ?ERR_BAD_REQUEST}
|
|
end.
|
|
|
|
% TODO: utc attribute, catch list_to_integer errors
|
|
|
|
parse_store_element_sub([{xmlelement, Dir, _, _} = E | Tail]) ->
|
|
[#msg{direction = list_to_atom(Dir),
|
|
secs = list_to_integer(xml:get_tag_attr_s("secs", E)),
|
|
body = xml:get_tag_cdata(xml:get_subtag(E,"body"))} |
|
|
parse_store_element_sub(Tail)];
|
|
|
|
parse_store_element_sub([]) -> [];
|
|
parse_store_element_sub([_ | Tail]) -> parse_store_element_sub(Tail).
|
|
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% 5. Archive Management
|
|
%%
|
|
|
|
|
|
process_local_iq_list(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
|
|
#jid{luser = LUser, lserver = LServer} = From,
|
|
case Type of
|
|
set ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
|
|
get ->
|
|
{xmlelement, _, Attrs, SubEls} = SubEl,
|
|
RSM = parse_rsm(SubEls),
|
|
?MYDEBUG("RSM Results: ~p ~n", [RSM]),
|
|
Result = case parse_root_argument(Attrs) of
|
|
{error, E} -> {error, E};
|
|
{interval, Start, Stop} ->
|
|
get_list(LUser, LServer, Start, Stop, '_');
|
|
{interval, Start, Stop, Jid} ->
|
|
get_list(LUser, LServer, Start, Stop, Jid);
|
|
{index, Jid, Start} ->
|
|
get_list(LUser, LServer, Start, infinity, Jid);
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end,
|
|
case Result of
|
|
{ok, Items} ->
|
|
FunId = fun(El) -> ?MYDEBUG("FunId ~p ~n", [El]), integer_to_list(element(5,El)) end,
|
|
FunCompare = fun(Id, El) ->
|
|
Id2 = list_to_integer(FunId(El)),
|
|
Id1 = list_to_integer(Id),
|
|
if Id1 == Id2 -> equal;
|
|
Id1 > Id2 -> greater;
|
|
Id1 < Id2 -> smaller
|
|
end
|
|
end,
|
|
case catch execute_rsm(RSM, lists:keysort(5, Items), FunId,FunCompare) of
|
|
{error, R} ->
|
|
IQ#iq{type = error, sub_el = [SubEl, R]};
|
|
{'EXIT', Errr} ->
|
|
?MYDEBUG("INTERNAL ERROR ~p ~n", [Errr]),
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]};
|
|
{RSM_Elem, Items2} ->
|
|
Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
|
Fun = fun(A) ->
|
|
Seconds= A#archive_message.start - Zero,
|
|
Start2 = jlib:now_to_utc_string({Seconds div 1000000, Seconds rem 1000000, 0}),
|
|
Args0 = [{"with", jlib:jid_to_string(A#archive_message.jid)}, {"start", Start2}],
|
|
Args = case A#archive_message.subject of
|
|
"" -> Args0;
|
|
Subject -> [{"subject",Subject} | Args0]
|
|
end,
|
|
{xmlelement, "chat", Args, []}
|
|
end,
|
|
IQ#iq{type = result, sub_el = [{xmlelement, "list", [{"xmlns", ?NS_ARCHIVE}], lists:append(lists:map(Fun, Items2),[RSM_Elem])}]}
|
|
end;
|
|
{error, R} ->
|
|
IQ#iq{type = error, sub_el = [SubEl, R]}
|
|
end
|
|
end.
|
|
|
|
|
|
|
|
|
|
process_local_iq_retrieve(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
|
|
#jid{luser = LUser, lserver = LServer} = From,
|
|
case Type of
|
|
set ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
|
|
get ->
|
|
{xmlelement, _, Attrs, SubEls} = SubEl,
|
|
RSM = parse_rsm(SubEls),
|
|
case index_from_argument(LUser, LServer, Attrs) of
|
|
{error, E} ->
|
|
IQ#iq{type = error, sub_el = [SubEl, E]};
|
|
Index ->
|
|
case retrieve_collection(Index, RSM) of
|
|
{error, Err} ->
|
|
IQ#iq{type = error, sub_el = [SubEl, Err]};
|
|
Store ->
|
|
IQ#iq{type = result, sub_el = [Store]}
|
|
end
|
|
end
|
|
end.
|
|
|
|
|
|
retrieve_collection(Index, RSM) ->
|
|
case get_collection(Index) of
|
|
{error, E} ->
|
|
{error, E};
|
|
A ->
|
|
Zero = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
|
Seconds = A#archive_message.start-Zero,
|
|
Start2 = jlib:now_to_utc_string({Seconds div 1000000, Seconds rem 1000000, 0}),
|
|
Args0 = [{"xmlns", ?NS_ARCHIVE}, {"with", jlib:jid_to_string(A#archive_message.jid)}, {"start", Start2}],
|
|
Args = case A#archive_message.subject of
|
|
"" -> Args0;
|
|
Subject -> [{"subject", Subject} | Args0]
|
|
end,
|
|
case catch execute_rsm(RSM, A#archive_message.message_list, index, index) of
|
|
{error, R} ->
|
|
{error, R};
|
|
{'EXIT', _} ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR};
|
|
{RSM_Elem, Items} ->
|
|
Format_Fun =
|
|
fun(Elem) ->
|
|
{xmlelement, atom_to_list(Elem#msg.direction),
|
|
[{"secs", integer_to_list(Elem#msg.secs)}],
|
|
[{xmlelement, "body", [], [{xmlcdata, Elem#msg.body}]}]}
|
|
end,
|
|
{xmlelement, "chat", Args, lists:append(lists:map(Format_Fun, Items), [RSM_Elem])}
|
|
end
|
|
end.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% 5.3 Removing a Collection
|
|
%%
|
|
|
|
|
|
process_local_iq_remove(From, _To, #iq{type = Type, sub_el = SubEl} = IQ) ->
|
|
#jid{luser = LUser, lserver = LServer} = From,
|
|
case Type of
|
|
get ->
|
|
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
|
|
set ->
|
|
{xmlelement, _, Attrs, _} = SubEl,
|
|
Result = case parse_root_argument(Attrs) of
|
|
{error, E} -> IQ#iq{type = error, sub_el = [SubEl, E]};
|
|
{interval, Start, Stop} -> process_remove_interval(LUser, LServer, Start, Stop, '_');
|
|
{interval, Start, Stop, Jid} -> process_remove_interval(LUser, LServer, Start, Stop, Jid);
|
|
{index, Jid, Start} -> process_remove_index({LUser, LServer, Jid, Start})
|
|
end,
|
|
case Result of
|
|
{error, Ee} -> IQ#iq{type = error, sub_el = [SubEl, Ee]};
|
|
ok -> IQ#iq{type = result, sub_el=[]}
|
|
end
|
|
end.
|
|
|
|
process_remove_index(Index) ->
|
|
case mnesia:transaction(fun() -> mnesia:delete({archive_message, Index}) end) of
|
|
{atomic, _} ->
|
|
ok;
|
|
{aborted, _} ->
|
|
{error, ?ERR_ITEM_NOT_FOUND}
|
|
end.
|
|
|
|
process_remove_interval(LUser, LServer, Start, End, With) ->
|
|
Fun = fun() ->
|
|
Pat = #archive_message{usjs= '_', us = {LUser, LServer}, jid= With,
|
|
start='$1', message_list='_', subject = '_'},
|
|
Guard = [{'>=', '$1', Start},{'<', '$1', End}],
|
|
|
|
lists:foreach(fun(R) -> mnesia:delete_object(R) end,
|
|
mnesia:select(archive_message, [{Pat, Guard, ['$_']}]))
|
|
end,
|
|
|
|
case mnesia:transaction(Fun) of
|
|
{atomic, _} ->
|
|
ok;
|
|
{aborted, _} ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR}
|
|
end.
|
|
|
|
|
|
|
|
% return {ok, [{#archive_message}]} or {error, xmlelement}
|
|
% With is a tuple Jid, or '_'
|
|
get_list(LUser, LServer, Start, End, With) ->
|
|
case mnesia:transaction(fun() ->
|
|
Pat = #archive_message{usjs= '_', us = {LUser, LServer}, jid= With,
|
|
start='$1', message_list='_', subject = '_'},
|
|
Guard = [{'>=', '$1', Start},{'<', '$1', End}],
|
|
mnesia:select(archive_message, [{Pat, Guard, ['$_']}])
|
|
end) of
|
|
{atomic, Result} ->
|
|
{ok, Result};
|
|
{aborted, _} ->
|
|
{error, ?ERRT_INTERNAL_SERVER_ERROR("", "plop")}
|
|
end.
|
|
|
|
|
|
% Index is {LUser, LServer, With, Start}
|
|
get_collection(Index) ->
|
|
case catch mnesia:dirty_read(archive_message, Index) of
|
|
{'EXIT', _Reason} ->
|
|
{error, ?ERR_INTERNAL_SERVER_ERROR};
|
|
[] ->
|
|
{error, ?ERR_ITEM_NOT_FOUND};
|
|
[C] -> C
|
|
end.
|
|
|
|
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Utility
|
|
|
|
|
|
% return either {error, Err} or {LUser, LServer, Jid, Start}
|
|
index_from_argument(LUser, LServer, Attrs) ->
|
|
case parse_root_argument(Attrs) of
|
|
{error, E} -> {error, E};
|
|
{index, Jid, Start} -> {LUser, LServer, Jid, Start};
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end.
|
|
|
|
%parse commons arguments of root elements
|
|
parse_root_argument(Attrs) ->
|
|
case parse_root_argument_aux(Attrs, {undefined, undefined, undefined}) of
|
|
{error, E} -> {error, E};
|
|
{{ok,Jid}, {ok,Start}, undefined} -> {index, Jid, Start};
|
|
{{ok,Jid}, undefined, undefined} -> {interval, 0, ?INFINITY, Jid};
|
|
{{ok,Jid}, {ok,Start}, {ok,Stop}} -> {interval, Start, Stop, Jid};
|
|
{undefined, {ok,Start}, {ok,Stop}} -> {interval, Start, Stop};
|
|
{undefined, undefined, undefined} -> {interval, 0, ?INFINITY};
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end.
|
|
|
|
parse_root_argument_aux([{"with", JidStr} | Tail], {_, AS, AE}) ->
|
|
case jlib:string_to_jid(JidStr) of
|
|
error -> {error, ?ERR_JID_MALFORMED};
|
|
JidS ->
|
|
Jid = jlib:jid_tolower(JidS),
|
|
parse_root_argument_aux(Tail, {{ok, Jid}, AS, AE})
|
|
end;
|
|
parse_root_argument_aux([{"start", Str} | Tail], {AW, _, AE}) ->
|
|
case jlib:datetime_string_to_timestamp(Str) of
|
|
undefined -> {error, ?ERR_BAD_REQUEST};
|
|
No ->
|
|
Val = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)),
|
|
parse_root_argument_aux(Tail, {AW, {ok, Val}, AE})
|
|
end;
|
|
parse_root_argument_aux([{"end", Str} | Tail], {AW, AS, _}) ->
|
|
case jlib:datetime_string_to_timestamp(Str) of
|
|
undefined -> {error, ?ERR_BAD_REQUEST};
|
|
No ->
|
|
Val = calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(No)),
|
|
parse_root_argument_aux(Tail, {AW, AS, {ok, Val}})
|
|
end;
|
|
parse_root_argument_aux([_ | Tail], A) ->
|
|
parse_root_argument_aux(Tail, A);
|
|
parse_root_argument_aux([], A) -> A.
|
|
|
|
|
|
|
|
get_timestamp() ->
|
|
calendar:datetime_to_gregorian_seconds(calendar:universal_time()).
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Result Set Management (JEP-0059)
|
|
%
|
|
% USAGE:
|
|
% RSM = parce_rsm(Xmlelement)
|
|
% case execute_rsm(RSM, List, GetIdFun, IdCompareFun) of
|
|
% {error, E} -> ....;
|
|
% {RSMElement, Items} ->
|
|
% SubElements = lists:append(lists:map(Format_Fun, Items), [RSMElement]),
|
|
% ...;
|
|
% end
|
|
|
|
-define(MY_NS_RSM, "http://jabber.org/protocol/rsm").
|
|
|
|
|
|
|
|
|
|
% {Start, Max, Order} | error | none % | count
|
|
|
|
parse_rsm([A | Tail]) ->
|
|
?MYDEBUG("parse RSM elem ~p ", [A]),
|
|
case A of
|
|
{xmlelement, _, Attrs1, _} ->
|
|
case xml:get_attr_s("xmlns", Attrs1) of
|
|
?MY_NS_RSM ->
|
|
parse_rsm(A);
|
|
HEPO ->
|
|
?MYDEBUG("HEPO ~p ", [HEPO]),
|
|
parse_rsm(Tail)
|
|
end;
|
|
_ ->
|
|
parse_rsm(Tail)
|
|
end;
|
|
parse_rsm([]) ->
|
|
none;
|
|
|
|
% parse_rsm({xmlelement, "count", _, _}) ->
|
|
% count;
|
|
|
|
parse_rsm({xmlelement, "set", _, SubEls}) ->
|
|
parse_rsm_aux(SubEls, {0, infinity, normal});
|
|
|
|
parse_rsm(_) ->
|
|
error.
|
|
|
|
parse_rsm_aux([{xmlelement, "max", _Attrs, Contents} | Tail], Acc) ->
|
|
case catch list_to_integer(xml:get_cdata(Contents)) of
|
|
P when is_integer(P) ->
|
|
case Acc of
|
|
{Start, infinity, Order} ->
|
|
parse_rsm_aux(Tail, {Start, P, Order});
|
|
_ ->
|
|
error
|
|
end;
|
|
HEPO ->
|
|
?MYDEBUG("<max> Not an INTEGER ~p ", [HEPO]),
|
|
error
|
|
end;
|
|
|
|
parse_rsm_aux([{xmlelement, "index", _Attrs, Contents} | Tail], Acc) ->
|
|
case catch list_to_integer(xml:get_cdata(Contents)) of
|
|
P when is_integer(P) ->
|
|
case Acc of
|
|
{0, Max, normal} ->
|
|
parse_rsm_aux(Tail, {P, Max, normal});
|
|
_ ->
|
|
error
|
|
end;
|
|
_ ->
|
|
error
|
|
end;
|
|
|
|
parse_rsm_aux([{xmlelement, "after", _Attrs, Contents} | Tail], Acc) ->
|
|
case Acc of
|
|
{0, Max, normal} ->
|
|
parse_rsm_aux(Tail, {{id, xml:get_cdata(Contents)}, Max, normal});
|
|
_ ->
|
|
error
|
|
end;
|
|
|
|
|
|
parse_rsm_aux([{xmlelement, "before", _Attrs, Contents} | Tail], Acc) ->
|
|
case Acc of
|
|
{0, Max, normal} ->
|
|
case xml:get_cdata(Contents) of
|
|
[] ->
|
|
parse_rsm_aux(Tail, {0, Max, reversed});
|
|
CD ->
|
|
parse_rsm_aux(Tail, {{id, CD}, Max, reversed})
|
|
end;
|
|
_ ->
|
|
error
|
|
end;
|
|
|
|
parse_rsm_aux([_ | Tail], Acc) ->
|
|
parse_rsm_aux(Tail, Acc);
|
|
parse_rsm_aux([], Acc) ->
|
|
Acc.
|
|
|
|
% RSM = {Start, Max, Order}
|
|
% GetId = fun(Elem) -> Id
|
|
% IdCompare = fun(Id, Elem) -> equal | greater | smaller
|
|
%
|
|
% -> {RSMElement, List} | {error, ErrElement}
|
|
|
|
execute_rsm(RSM, List, GetId, IdCompare) ->
|
|
?MYDEBUG("execute_rsm RSM ~p ~n", [RSM]),
|
|
case execute_rsm_aux(RSM, List, IdCompare, 0) of
|
|
none ->
|
|
{{xmlcdata, ""}, List};
|
|
% count ->
|
|
% {{xmlelement, "count", [{"xmlns", ?MY_NS_RSM}], [{xmlcdata, integer_to_list(length(List))}]}, []};
|
|
{error, E} ->
|
|
{error, E};
|
|
{_, []} ->
|
|
{{xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [{xmlelement, "count", [], [{xmlcdata, integer_to_list(length(List))}]}]}, []};
|
|
{Index, L} ->
|
|
case GetId of
|
|
index ->
|
|
{make_rsm(Index, integer_to_list(Index), integer_to_list(Index+length(L)),length(List)), L};
|
|
_ ->
|
|
{make_rsm(Index, GetId(hd(L)), GetId(lists:last(L)),length(List)), L}
|
|
end
|
|
end.
|
|
|
|
|
|
% execute_rsm_aux(count, _List, _, _) ->
|
|
% count;
|
|
|
|
execute_rsm_aux(none, _List, _, _) ->
|
|
none;
|
|
|
|
execute_rsm_aux(error, _List, _, _) ->
|
|
{error, ?ERR_BAD_REQUEST};
|
|
|
|
execute_rsm_aux({S, M, reversed}, List, IdFun, Acc) ->
|
|
{NewFun,NewS} = case IdFun of
|
|
index ->
|
|
{index,
|
|
case S of
|
|
{id, IdentIndex} ->
|
|
integer_to_list(length(List) - list_to_integer(IdentIndex));
|
|
_ -> S
|
|
end};
|
|
_ ->
|
|
{fun(Index, Elem) ->
|
|
case IdFun(Index, Elem) of
|
|
equal -> equal;
|
|
greater -> smaller;
|
|
smaller -> greater;
|
|
O -> O
|
|
end
|
|
end,
|
|
S}
|
|
end,
|
|
{Index, L2} = execute_rsm_aux({NewS,M,normal}, lists:reverse(List), NewFun, 0),
|
|
{Acc + length(List) - Index - length(L2), lists:reverse(L2)};
|
|
|
|
execute_rsm_aux({{id,I}, M, normal}, List, index, Acc) ->
|
|
execute_rsm_aux({list_to_integer(I), M, normal}, List, index, Acc);
|
|
|
|
execute_rsm_aux({{id,I}, M, normal} = RSM, [E | Tail], IdFun, Acc) ->
|
|
case IdFun(I, E) of
|
|
smaller ->
|
|
execute_rsm_aux(RSM, Tail, IdFun, Acc + 1);
|
|
_ ->
|
|
execute_rsm_aux({0, M, normal}, [E | Tail], IdFun, Acc)
|
|
end;
|
|
|
|
execute_rsm_aux({{id,_}, _, normal}, [], _, Acc) ->
|
|
{Acc, []};
|
|
|
|
execute_rsm_aux({0, infinity, normal}, List, _, Acc) ->
|
|
{Acc, List};
|
|
|
|
execute_rsm_aux({_, 0, _}, _, _, Acc) ->
|
|
{Acc, []};
|
|
|
|
execute_rsm_aux({S, M, _}, List, _, Acc) when is_integer(S) and is_integer(M) ->
|
|
?MYDEBUG("execute_rsm_aux sublist ~p ~n", [{S,M,List,Acc}]),
|
|
{Acc + S, lists:sublist(List, S+1,M)}.
|
|
|
|
make_rsm(FirstIndex, FirstId, LastId, Count) ->
|
|
{xmlelement, "set", [{"xmlns", ?MY_NS_RSM}], [
|
|
{xmlelement, "first", [{"index", integer_to_list(FirstIndex)}], [{xmlcdata, FirstId}]},
|
|
{xmlelement, "last", [], [{xmlcdata, LastId}]},
|
|
{xmlelement, "count", [], [{xmlcdata, integer_to_list(Count)}]}]}.
|