%%%---------------------------------------------------------------------- %%% File : mod_log_chat.erl %%% Author : Jérôme Sautret %%% Purpose : Log 2 ways chat messages in files %%% Id : $Id: mod_log_chat.erl 412 2007-11-15 10:10:09Z mremond $ %%%---------------------------------------------------------------------- -module(mod_log_chat). -author('jerome.sautret@process-one.net'). -behaviour(gen_mod). -export([start/2, init/1, stop/1, log_packet_send/4, log_packet_receive/5]). -ifndef(LAGER). -define(LAGER, 1). -endif. -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). -define(PROCNAME, ?MODULE). -define(DEFAULT_PATH, "."). -define(DEFAULT_FORMAT, text). -record(config, {path=?DEFAULT_PATH, format=?DEFAULT_FORMAT}). start(Host, Opts) -> ?DEBUG(" ~p ~p~n", [Host, Opts]), case gen_mod:get_opt(host_config, Opts, fun(L) when is_list(L) -> L end, []) of [] -> start_vh(Host, Opts); HostConfig -> start_vhs(Host, HostConfig) end. start_vhs(_, []) -> ok; start_vhs(Host, [{Host, Opts}| Tail]) -> ?DEBUG("start_vhs ~p ~p~n", [Host, [{Host, Opts}| Tail]]), start_vh(Host, Opts), start_vhs(Host, Tail); start_vhs(Host, [{_VHost, _Opts}| Tail]) -> ?DEBUG("start_vhs ~p ~p~n", [Host, [{_VHost, _Opts}| Tail]]), start_vhs(Host, Tail). start_vh(Host, Opts) -> Path = gen_mod:get_opt(path, Opts, fun iolist_to_binary/1, ?DEFAULT_PATH), Format = gen_mod:get_opt(format, Opts, fun(A) when is_atom(A) -> A end, ?DEFAULT_FORMAT), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, log_packet_send, 55), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, log_packet_receive, 55), register(gen_mod:get_module_proc(Host, ?PROCNAME), spawn(?MODULE, init, [#config{path=Path, format=Format}])). init(Config)-> ?DEBUG("Starting ~p with config ~p~n", [?MODULE, Config]), loop(Config). loop(Config) -> receive {call, Caller, get_config} -> Caller ! {config, Config}, loop(Config); stop -> exit(normal) end. stop(Host) -> ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, log_packet_send, 55), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, log_packet_receive, 55), gen_mod:get_module_proc(Host, ?PROCNAME) ! stop, ok. log_packet_send(Packet, _C2SState, From, To) -> log_packet(From, To, Packet, From#jid.lserver), Packet. log_packet_receive(Packet, _C2SState, _JID, From, To) when From#jid.lserver == To#jid.lserver-> Packet; % only log at send time if the message is local to the server log_packet_receive(Packet, _C2SState, _JID, From, To) -> log_packet(From, To, Packet, To#jid.lserver), Packet. log_packet(From, To, Packet = #xmlel{name = <<"message">>, attrs = Attrs}, Host) -> case xml:get_attr_s(<<"type">>, Attrs) of <<"groupchat">> -> %% mod_muc_log already does it ?DEBUG("dropping groupchat: ~s", [xml:element_to_binary(Packet)]), ok; <<"error">> -> %% we don't log errors ?DEBUG("dropping error: ~s", [xml:element_to_binary(Packet)]), ok; _ -> write_packet(From, To, Packet, Host) end; log_packet(_From, _To, _Packet, _Host) -> ok. write_packet(From, To, Packet, Host) -> gen_mod:get_module_proc(Host, ?PROCNAME) ! {call, self(), get_config}, Config = receive {config, Result} -> Result end, Format = Config#config.format, {Subject, Body} = {case xml:get_subtag(Packet, <<"subject">>) of false -> ""; SubjEl -> escape(Format, xml:get_tag_cdata(SubjEl)) end, escape(Format, xml:get_path_s(Packet, [{elem, <<"body">>}, cdata]))}, case Subject == [] andalso Body == [] of true -> %% don't log empty messages ?DEBUG("not logging empty message from ~s",[jlib:jid_to_string(From)]), ok; false -> Path = Config#config.path, FromJid = [From#jid.luser, "@", From#jid.lserver], ToJid = [To#jid.luser, "@", To#jid.lserver], {FilenameTemplate, DateString, Header, MessageType} = case calendar:local_time() of {{Y, M, D}, {H, Min, S}} -> SortedJid = lists:sort([FromJid, ToJid]), Title = io_lib:format(template(Format, title), [FromJid, ToJid, Y, M, D]), {lists:flatten(io_lib:format("~s/~~p-~~2.2.0w-~~2.2.0w ~s - ~s~s", [Path | SortedJid]++[template(Format, extension)])), io_lib:format(template(Format, date), [Y, M, D, H, Min, S]), io_lib:format(template(Format, header), lists:duplicate(count(template(Format, header), "~s"), Title) ), case hd(SortedJid) of FromJid -> message1; ToJid -> message2 end } end, ?DEBUG("FilenameTemplate ~p~n",[FilenameTemplate]), Filename = io_lib:format(FilenameTemplate, [Y, M, D]), ?DEBUG("logging message from ~s into ~s~n",[jlib:jid_to_string(From), Filename]), File = case file:read_file_info(Filename) of {ok, _} -> open_logfile(Filename); {error, enoent} -> close_previous_logfile(FilenameTemplate, Format, {Y, M, D}), NewFile = open_logfile(Filename), io:format(NewFile, Header, []), NewFile end, MessageText = case Subject == [] of true -> Body; false -> io_lib:format(template(Format, subject), [Subject])++Body end, ?DEBUG("MessageTemplate ~s~n",[template(Format, MessageType)]), ?DEBUG("Data: ~s ~s ~s ~s ~s ~s ~n",[DateString, FromJid, From#jid.lresource, ToJid, To#jid.lresource, MessageText]), io:format(File, lists:flatten(template(Format, MessageType)), [DateString, FromJid, From#jid.lresource, ToJid, To#jid.lresource, MessageText]), file:close(File) end. open_logfile(Filename) -> case file:open(Filename, [append]) of {ok, File} -> File; {error, Reason} -> ?ERROR_MSG("Cannot write into file ~s: ~p~n", [Filename, Reason]) end. close_previous_logfile(FilenameTemplate, Format, Date) -> Yesterday = calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date) - 1), Filename = io_lib:format(FilenameTemplate, tuple_to_list(Yesterday)), case file:read_file_info(Filename) of {ok, _} -> File = open_logfile(Filename), io:format(File, template(Format, footer), []), file:close(File); {error, enoent} -> ok end. escape(html, <<$<, Text/binary>>, Acc) -> escape(html,Text,<>); escape(html, <<$&, Text/binary>>, Acc) -> escape(html,Text,<>); escape(html, <>, Acc) -> escape(html,Text,<>); escape(html, <<>>, Acc) -> Acc. escape(html, Text) -> escape(html,Text,<<>>); escape(text, Text) -> Text; escape(_, "") -> "". % return the number of occurence of Word in String count(String, Word) -> case string:str(String, Word) of 0 -> 0; N -> 1+count(string:substr(String, N+length(Word)), Word) end. template(text, extension) -> ".log"; template(text, title) -> "Messages log between ~s and ~s on ~p-~2.2.0w-~2.2.0w"; template(text, header) -> "~s~n-----------------------------------------------------------------------~n"; template(text, subject) -> "Subject: ~s~n"; template(text, message) -> "~~s ~~s/~~s -> ~~s/~~s~n~s~~s~n"; template(text, message1) -> io_lib:format(template(text, message), ["> "]); template(text, message2) -> io_lib:format(template(text, message), ["< "]); template(text, date) -> "~p-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w"; template(text, footer) -> "---- End ----~n"; template(html, extension) -> ".html"; template(html, title) -> template(text, title); template(html, header) -> "~n"++ "~s"++ css()++ "~n

~s

~n"; template(html, subject) -> "
Subject: ~s
"; template(html, message) -> "
~~s ~~s/~~s -> ~~s/~~s~~n~~s
~~n"; template(html, message1) -> io_lib:format(template(html, message), [1]); template(html, message2) -> io_lib:format(template(html, message), [2]); template(html, date) -> "~p-~2.2.0w-~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w"; template(html, footer) -> "". css() -> "~n".