%%%---------------------------------------------------------------------- %%% File : mod_archive_sql.erl %%% Author : Olivier Goffart %%% Purpose : Message Archiving using SQL DB (JEP-0136) %%% Created : 19 Aug 2006 by Olivier Goffart %%%---------------------------------------------------------------------- %% 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_sql). -author('ogoffart@kde.org'). -author('alexey@process-one.net'). -behaviour(gen_server). -behaviour(gen_mod). -export([start_link/2, start/2, stop/1, remove_user/2, send_packet/3, receive_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, sessions, save_default, session_duration}). -define(PROCNAME, ejabberd_mod_archive_sql). -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(DICT, dict). -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) -> DBHost = gen_mod:get_opt(db_host, Opts, Host), if DBHost /= Host -> PGChildSpec = {gen_mod:get_module_proc(DBHost, ejabberd_odbc_sup), {ejabberd_odbc_sup, start_link, [DBHost]}, temporary, infinity, supervisor, [ejabberd_odbc_sup]}, supervisor:start_child(ejabberd_sup, PGChildSpec); true -> ok end, 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, 900), 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(offline_message_hook, Host, ?MODULE, receive_packet, 35), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_disco_features, 99), timer:send_interval(1000 * SessionDuration div 2, clean_sessions), {ok, #state{host = Host, sessions = ?DICT:new(), 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, Body}, State) -> Sessions = State#state.sessions, NewSessions = case should_store_jid(LUser, LServer, JID, State#state.save_default) of false -> Sessions; true -> do_log(Sessions, LUser, LServer, JID, Direction, Body, State#state.session_duration) end, {noreply, State#state{sessions = NewSessions}}; 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(clean_sessions, State) -> Sessions = State#state.sessions, Timeout = State#state.session_duration, TS = get_timestamp(), NewSessions = ?DICT:filter(fun(_Key, {_Start, Last}) -> TS - Last =< Timeout end, Sessions), {noreply, State#state{sessions = NewSessions}}; 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(offline_message_hook, Host, ?MODULE, receive_packet, 35), 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 %% it end up like 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_pref(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_save(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}, Username = ejabberd_odbc:escape(LUser), DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), F = fun() -> ejabberd_odbc:sql_query_t( ["delete from archive_collection " "where username = '", Username, "'"]) end, ejabberd_odbc:sql_transaction(DBHost, F), F = fun() -> 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, Packet) -> add_log(to, From#jid.luser, From#jid.lserver, To, Packet). receive_packet(From, To, Packet) -> add_log(from, To#jid.luser, To#jid.lserver, From, Packet). receive_packet(_JID, From, To, Packet) -> receive_packet(From, To, Packet). add_log(Direction, LUser, LServer, JID, Packet) -> case parse_message(Packet) of "" -> ok; Body -> Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), gen_server:cast( Proc, {addlog, Direction, LUser, LServer, JID, Body}) end. % Parse the message and return the body string if successful parse_message({xmlelement, "message", _, _} = Packet) -> case xml:get_tag_attr_s("type", Packet) of Type when Type == ""; Type == "normal"; Type == "chat" -> xml:get_path_s(Packet, [{elem, "body"}, cdata]); _ -> "" end; parse_message(_) -> "". % archive the message Body return new Sessions % Sessions: a dict of open sessions % LUser, LServer : the local user's information % Jid : the contact's jid % Body : the message body do_log(Sessions, LUser, LServer, JID, Direction, Body, SessionDuration) -> DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), {NewSessions, Start, Secs} = find_storage(LUser, LServer, JID, Sessions, SessionDuration), Username = ejabberd_odbc:escape(LUser), LJID = jlib:jid_tolower(JID), SJIDU = ejabberd_odbc:escape(element(1, LJID)), SJIDS = ejabberd_odbc:escape(element(2, LJID)), SJIDR = ejabberd_odbc:escape(element(3, LJID)), SStart = ejabberd_odbc:escape(integer_to_list(Start)), SDirection = case Direction of to -> "true"; from -> "false" end, SSecs = ejabberd_odbc:escape(integer_to_list(Secs)), SBody = ejabberd_odbc:escape(Body), SUTC = "0", SName = "", SJID1 = "", F = fun() -> SSID = case ejabberd_odbc:sql_query_t( ["select sid from archive_collection " "where username = '", Username, "' ", "and jid_u = '", SJIDU, "' ", "and jid_s = '", SJIDS, "' ", "and jid_r = '", SJIDR, "' ", "and start = '", SStart, "'"]) of {selected, ["sid"], [{SID}]} -> ["'", ejabberd_odbc:escape(SID), "'"]; {selected, ["sid"], []} -> SSubject = "", CollVals = ["currval('archive_collection_sid_seq'),", "'", Username, "'," "'", SJIDU, "'," "'", SJIDS, "'," "'", SJIDR, "'," "'", SStart, "'," "'", SSubject, "'"], ejabberd_odbc:sql_query_t( "select nextval('archive_collection_sid_seq')"), ejabberd_odbc:sql_query_t( ["insert into archive_collection(" " sid, username," " jid_u, jid_s, jid_r," " start, subject) " " values (", CollVals, ");"]), "currval('archive_collection_sid_seq')" end, MsgVals = [SSID, "," "'", SDirection, "'," "'", SSecs, "'," "'", SUTC, "'," "'", SName, "'," "'", SJID1, "'," "'", SBody, "'"], ejabberd_odbc:sql_query_t( ["insert into archive_message(" " sid, direction_to, secs, utc," " name, jid, body) " " values (", MsgVals, ");"]) end, ejabberd_odbc:sql_transaction(DBHost, F), NewSessions. find_storage(LUser, LServer, JID, Sessions, Timeout) -> LJID = jlib:jid_tolower(JID), Key = {LUser, LServer, LJID}, case ?DICT:find(Key, Sessions) of error -> TS = get_timestamp(), {?DICT:store(Key, {TS, TS}, Sessions), TS, 0}; {ok, {Start, Last}} -> TS = get_timestamp(), if TS - Last > Timeout -> {?DICT:store(Key, {TS, TS}, Sessions), TS, 0}; true -> {?DICT:store(Key, {Start, TS}, Sessions), Start, TS - Last} end end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% 3.1 Preferences %% process_local_iq_pref(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: [{xmlelement, "default", [{"save", "body"}], []}]; false -> [{xmlelement, "default", [{"save", "false"}], []}]; _ -> default_element(LServer) end, {result, {xmlelement, "save", [{"xmlns", ?NS_ARCHIVE}], DItem ++ LItems}} end. %return the 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 = SaveList, nosave_list = NoSaveList}] -> Jid_t = jlib:jid_tolower(Jid), Jid_b = jlib:jid_remove_resource(Jid_t), A = lists:member(Jid_t, SaveList), B = lists:member(Jid_t, NoSaveList), C = lists:member(Jid_b, SaveList), D = lists:member(Jid_b, NoSaveList), 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_save(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 -> DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), F = fun() -> store_collection(Collection) end, case ejabberd_odbc:sql_transaction(DBHost, F) 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]) when Dir == "from"; Dir == "to" -> [#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). store_collection(Collection) -> {LUser, _LServer, JID, Start} = Collection#archive_message.usjs, LJID = jlib:jid_tolower(JID), Username = ejabberd_odbc:escape(LUser), SJIDU = ejabberd_odbc:escape(element(1, LJID)), SJIDS = ejabberd_odbc:escape(element(2, LJID)), SJIDR = ejabberd_odbc:escape(element(3, LJID)), SStart = ejabberd_odbc:escape(integer_to_list(Start)), SSubject = ejabberd_odbc:escape(Collection#archive_message.subject), CollVals = ["currval('archive_collection_sid_seq'),", "'", Username, "'," "'", SJIDU, "'," "'", SJIDS, "'," "'", SJIDR, "'," "'", SStart, "'," "'", SSubject, "'"], ejabberd_odbc:sql_query_t( ["delete from archive_collection " " where username = '", Username, "' " " and jid_u = '", SJIDU, "' ", " and jid_s = '", SJIDS, "' ", " and jid_r = '", SJIDR, "';"]), ejabberd_odbc:sql_query_t( "select nextval('archive_collection_sid_seq')"), ejabberd_odbc:sql_query_t( ["insert into archive_collection(" " sid, username, jid_u, jid_s, jid_r, start, subject) " " values (", CollVals, ");"]), lists:map( fun(#msg{direction = Direction, secs = Secs, body = Body}) -> SDirection = case Direction of to -> "true"; from -> "false" end, SSecs = ejabberd_odbc:escape(integer_to_list(Secs)), SBody = ejabberd_odbc:escape(Body), SUTC = "0", SName = "", SJID1 = "", MsgVals = ["'", SDirection, "'," "'", SSecs, "'," "'", SUTC, "'," "'", SName, "'," "'", SJID1, "'," "'", SBody, "'"], ejabberd_odbc:sql_query_t( ["insert into archive_message(" " sid, direction_to, secs, utc," " name, jid, body) " " values (currval('archive_collection_sid_seq')," " ", MsgVals, ");"]) end, Collection#archive_message.message_list). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% 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, JID} -> get_list(LUser, LServer, Start, Stop, 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, Jid} -> process_remove_interval( LUser, LServer, Start, Stop, Jid) 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_interval(LUser, LServer, Start, End, With) -> DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), Username = ejabberd_odbc:escape(LUser), WithCond = case With of undefined -> ""; JID -> LJID = jlib:jid_tolower(JID), case LJID of {"", LJIDS, ""} -> SJIDS = ejabberd_odbc:escape(LJIDS), [" and jid_s = '", SJIDS, "'"]; {LJIDU, LJIDS, ""} -> SJIDU = ejabberd_odbc:escape(LJIDU), SJIDS = ejabberd_odbc:escape(LJIDS), [" and jid_u = '", SJIDU, "'", " and jid_s = '", SJIDS, "'"]; {LJIDU, LJIDS, LJIDR} -> SJIDU = ejabberd_odbc:escape(LJIDU), SJIDS = ejabberd_odbc:escape(LJIDS), SJIDR = ejabberd_odbc:escape(LJIDR), [" and jid_u = '", SJIDU, "'", " and jid_s = '", SJIDS, "'", " and jid_r = '", SJIDR, "'"] end end, TimeCond = case {Start, End} of {undefined, undefined} -> ""; {undefined, _} -> SEnd = ejabberd_odbc:escape(integer_to_list(End)), [" and start <= '", SEnd, "'"]; {_, undefined} -> SStart = ejabberd_odbc:escape(integer_to_list(Start)), [" and start = '", SStart, "'"]; _ -> SStart = ejabberd_odbc:escape(integer_to_list(Start)), SEnd = ejabberd_odbc:escape(integer_to_list(End)), [" and start >= '", SStart, "'" " and start <= '", SEnd, "'"] end, F = fun() -> ejabberd_odbc:sql_query_t( ["delete from archive_collection " "where username = '", Username, "' ", TimeCond, WithCond]) end, case ejabberd_odbc:sql_transaction(DBHost, F) 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) -> DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), Username = ejabberd_odbc:escape(LUser), WithCond = case With of undefined -> ""; JID -> LJID = jlib:jid_tolower(JID), case LJID of {"", LJIDS, ""} -> SJIDS = ejabberd_odbc:escape(LJIDS), [" and jid_s = '", SJIDS, "'"]; {LJIDU, LJIDS, ""} -> SJIDU = ejabberd_odbc:escape(LJIDU), SJIDS = ejabberd_odbc:escape(LJIDS), [" and jid_u = '", SJIDU, "'", " and jid_s = '", SJIDS, "'"]; {LJIDU, LJIDS, LJIDR} -> SJIDU = ejabberd_odbc:escape(LJIDU), SJIDS = ejabberd_odbc:escape(LJIDS), SJIDR = ejabberd_odbc:escape(LJIDR), [" and jid_u = '", SJIDU, "'", " and jid_s = '", SJIDS, "'", " and jid_r = '", SJIDR, "'"] end end, StartCond = case Start of undefined -> ""; _ -> SStart = ejabberd_odbc:escape(integer_to_list(Start)), [" and start >= '", SStart, "'"] end, EndCond = case End of undefined -> ""; _ -> SEnd = ejabberd_odbc:escape(integer_to_list(End)), [" and start <= '", SEnd, "'"] end, case catch ejabberd_odbc:sql_query( DBHost, ["select jid_u, jid_s, jid_r, start, subject " "from archive_collection " "where username = '", Username, "' ", StartCond, EndCond, WithCond]) of {selected, ["jid_u", "jid_s", "jid_r", "start", "subject"], Rs} -> Result = lists:map( fun({SJIDU, SJIDS, SJIDR, SStart1, Subject}) -> JID1 = jlib:make_jid(SJIDU, SJIDS, SJIDR), Start1 = list_to_integer(SStart1), Index = {LUser, LServer, JID1, Start1}, #archive_message{ usjs = Index, us = {LUser, LServer}, jid = JID1, start = Start1, subject = Subject, message_list = []} end, Rs), {ok, Result}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} end. % Index is {LUser, LServer, With, Start} get_collection(Index) -> {LUser, LServer, JID, Start} = Index, DBHost = gen_mod:get_module_opt(LServer, ?MODULE, db_host, LServer), LJID = jlib:jid_tolower(JID), Username = ejabberd_odbc:escape(LUser), SJIDU = ejabberd_odbc:escape(element(1, LJID)), SJIDS = ejabberd_odbc:escape(element(2, LJID)), SJIDR = ejabberd_odbc:escape(element(3, LJID)), SStart = ejabberd_odbc:escape(integer_to_list(Start)), case catch ejabberd_odbc:sql_query( DBHost, ["select sid, subject from archive_collection " "where username = '", Username, "' ", "and jid_u = '", SJIDU, "' ", "and jid_s = '", SJIDS, "' ", "and jid_r = '", SJIDR, "' ", "and start = '", SStart, "'"]) of {selected, ["sid", "subject"], [{SID, Subject}]} -> SSID = ejabberd_odbc:escape(SID), case catch ejabberd_odbc:sql_query( DBHost, ["select direction_to, secs, utc, name, jid, body " "from archive_message " "where sid = '", SSID, "' ", "order by ser"]) of {selected, ["direction_to", "secs", "utc", "name", "jid", "body"], Rs} -> Msgs = lists:map( fun({DirectionTo, SSecs, SUTC, SName, SJID, Body}) -> Direction = if DirectionTo == "t" -> to; DirectionTo == "f" -> from end, Secs = list_to_integer(SSecs), #msg{direction = Direction, secs = Secs, body = Body} end, Rs), #archive_message{usjs = Index, us = {LUser, LServer}, jid = JID, start = Start, message_list = Msgs, subject = Subject}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} end; {selected, ["sid", "subject"], []} -> {error, ?ERR_ITEM_NOT_FOUND}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} 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}; {interval, Start, _, JID} when Start /= undefined, JID /= undefined -> {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}; {JID, Start, Stop} -> {interval, Start, Stop, JID}; _ -> {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}; JID -> LJID = jlib:jid_tolower(JID), parse_root_argument_aux(Tail, {LJID, 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, 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, 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(" 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)}]}]}.