--- mod_logmnesia.erl.orig Tue Oct 10 10:07:34 2006 +++ mod_logmnesia.erl Tue Oct 10 08:33:28 2006 @@ -0,0 +1,745 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_logmnesia.erl +%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com) +%%% Purpose : Log user messages to mnesia +%%% Version : 1.3a +%%%---------------------------------------------------------------------- + +%%% DESCRIPTION: +%%% This module sniffs all the user messages send through ejabberd. +%%% It logs messages to a build-in erlang database (mnesia). +%%% Logged messages can be viewed through ejabberd web admin interface. +%%% This module reuses some code from mod_logxml. + +%%% INSTALL: +%%% 1. Download latest patch +%%% 2. Change dir to your ejabberd sources: +%%% # cd /usr/local/src/ejabberd-1.1.2/src +%%% 3. Apply patch +%%% # patch -p0 < patch-src-mod_logmnesia +%%% 4. Rebuild/reinstall ejabberd +%%% 5. Add to ejabberd.cfg, 'modules' section, the basic configuration: +%%% {mod_logmnesia, []}, +%%% 6. Restart ejabberd. + +%%% CONFIGURE: +%%% +%%% ignore_jids: +%%% Ignore messages sent to or from JIDs in the list. +%%% Default value - []. +%%% +%%% groupchat: +%%% This defines how to process groupchat messages. +%%% possible values: +%%% all - to log all groupchat messages +%%% send - to log only sent by user messages +%%% none - to disable logging of groupchat messages (recommended value, +%%% use mod_muc_log for groupchat logging) +%%% Default value - none. +%%% +%%% purge_older_days: +%%% For automatic purging of messages older than specified value. +%%% possible values: +%%% never - do not perform automatic purging of messages +%%% Number - perform daily automatic purging of messages older than Number (in days) +%%% Default value - never. +%%% To keep only "today's" messages set Number to 0. +%%% +%%% ejabberdctl COMMANDS: +%%% purge-old-records Days - it purges all logged messages older than Days, +%%% you can use it in crontab to automatize this process +%%% (or you can use purge_older_days option). +%%% vhost your-vhost-here rebuild-stats - in some rare (I hope :)) cases it is possible that statistics become corrupted, +%%% this command rebuilds statistics for vhost. +%%% +%%% EXAMPLE CONFIGURATION: +%%% +%%% {mod_logmnesia, [ +%%% {ignore_jids, ["bigboss@example.com", "bot@example.org"]}, +%%% {groupchat, none}, +%%% {purge_older_days, 365} +%%% ]}, +%%% +%%% Changelog +%%% 1.3a - 2006-10-10 +%%% * add the appropriate navigation support for eJabberd Web Admin navigation bar (by Nathan Faust) +%%% 1.3 - 2006-10-09 +%%% * added ability to filter messages by interlocutors (while viewing user messages) +%%% * added resource storing in db +%%% WARNING!!! WARNING!!! WARNING!!! WARNING!!! +%%% This version make changes in messages tables structure +%%% (this must not be destructive, but who knows.....), +%%% so if you really need your messages log, please, backup your data before upgrading. +%%% 1.2a - 2006-08-30 +%%% * added nl translation (by Sander Devrieze) +%%% * added ability to delete stats for nonexistent tables while rebuilding stats +%%% 1.2 - 2006-08-28 +%%% * bugfix (improved tables locking while logging message) +%%% * added purge_older_days option +%%% * the time of a message writes now accurate within microseconds +%%% for proper sorting of messages recieved within a second +%%% 1.1b - 2006-08-24 +%%% * bugfix (improved tables locking while rebuild-stats) +%%% 1.1a - 2006-08-23 +%%% * bugfixes +%%% * added ru and uk translations +%%% 1.1 - 2006-08-22 +%%% * added manual selective erasing of messages through web admin +%%% * added groupchat option +%%% * added sort by date in common vhost and user view +%%% * lot of bugfixes +%%% 1.0 - 2006-08-19 +%%% * Initial version +-module(mod_logmnesia). +-author(''). +-vsn(''). + +-behaviour(gen_mod). + +-export([start/2, init/2, stop/1, + send_packet/3, receive_packet/4, + get_user_stats/2, get_user_stats_at/3, + get_vhost_stats/1, get_vhost_stats_at/2, + user_messages_parse_query/4, user_messages_at_parse_query/5, + vhost_messages_parse_query/3, vhost_messages_at_parse_query/4, + convert_timestamp/1, + rebuild_stats/3, purge_old_records/2]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("ejabberd_ctl.hrl"). +-include("mod_logmnesia.hrl"). + +-define(PROCNAME, ejabberd_mod_logmnesia). +-record(options, {ignore_jids, groupchat, purge_older_days}). + +tables_prefix() -> "messages_". +% stats_table should not start with tables_prefix() ! +% i.e. lists:prefix(tables_prefix(), atom_to_list(stats_table())) must be /= true +stats_table() -> list_to_atom("messages-stats"). + +start(Host, Opts) -> + Options = #options{ignore_jids=gen_mod:get_opt(ignore_jids, Opts, []), + groupchat=gen_mod:get_opt(groupchat, Opts, none), + purge_older_days=gen_mod:get_opt(purge_older_days, Opts, never)}, + register(gen_mod:get_module_proc(Host, ?PROCNAME), + spawn(?MODULE, init, [Host, Options])). + +stop(Host) -> + ?MYDEBUG("Stopping ~s", [?MODULE]), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ?MYDEBUG("Removed hooks", []), + ejabberd_ctl:unregister_commands(Host, [{"rebuild-stats", "rebuild logmnesia module stats for vhost"}], ?MODULE, rebuild_stats), + ejabberd_ctl:unregister_commands([{"purge-old-records Days", "purge logged user messaged older than Days"}], ?MODULE, purge_old_records), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! stop, + {wait, Proc}. + +init(Host, Options) -> + AllTables = get_tables_list(), + mnesia:create_table(stats_table(), + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, stats)}, + {record_name, stats} + ]), + convert_tables(AllTables), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, send_packet, 90), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, receive_packet, 90), + ?MYDEBUG("Added hooks", []), + ejabberd_ctl:register_commands( + Host, + [{"rebuild-stats", "rebuild logmnesia module stats for vhost"}], + ?MODULE, rebuild_stats), + ejabberd_ctl:register_commands( + [{"purge-old-records Days", "purge logged user messaged older than Days"}], + ?MODULE, purge_old_records), + loop(Host, AllTables, {0,0,0}, Options). + +convert_tables(Tables) -> + lists:foreach(fun(Table) -> + ATable=list_to_atom(Table), + case mnesia:table_info(ATable, attributes) of + [to_user, to_server, from_user, from_server, id, type, subject, body, timestamp] -> + ?INFO_MSG("Converting ~p", [ATable]), + TrFun = fun(Msg_old) -> + {msg, To_user, To_server, From_user, From_server, Id, Type, Subject, Body, Timestamp} = Msg_old, + #msg{to_user=To_user, to_server=To_server, to_resource=[], from_user=From_user, from_server=From_server, from_resource=[], id=Id, type=Type, subject=Subject, body=Body, timestamp=Timestamp} + end, + case mnesia:transform_table(ATable, TrFun, record_info(fields, msg), msg) of + {aborted, R} -> ?ERROR_MSG("Failed to convert ~p: ~p", [Table, R]); + {atomic, ok} -> ?INFO_MSG("Successfully converted ~p", [Table]) + end; + [to_user, to_server, to_resource, from_user, from_server, from_resource, id, type, subject, body, timestamp] -> + ?MYDEBUG("~p does not need to be converted", [Table]) + end + end, Tables). + +% raw packet -> #msg +packet_parse(E) -> + {_Orientation, From, To, Packet} = E, + + User_to = stringprep:tolower(To#jid.user), + Server_to = stringprep:tolower(To#jid.server), + Resource_to = To#jid.resource, + User_from = stringprep:tolower(From#jid.user), + Server_from = stringprep:tolower(From#jid.server), + Resource_from = From#jid.resource, + + Message_type = xml:get_tag_attr_s("type", Packet), + Message_id = xml:get_tag_attr_s("id", Packet), + Message_subject = case xml:get_subtag(Packet, "subject") of + false -> + "None"; + Subject_xml -> + xml:get_tag_cdata(Subject_xml) + end, + Message_body = case xml:get_subtag(Packet, "body") of + false -> + false; + Body_xml -> + xml:get_tag_cdata(Body_xml) + end, + #msg{to_user=User_to, to_server=Server_to, to_resource=Resource_to, from_user=User_from, from_server=Server_from, from_resource=Resource_from, type=Message_type, id=Message_id, subject=Message_subject, body=Message_body}. + +filter(E, Message, Options) -> + {Orientation, From, To, Packet} = E, + {xmlelement, Stanza_str, _Attrs, _Els} = Packet, + Stanza = list_to_atom(Stanza_str), + + Hosts_all = ejabberd_config:get_global_option(hosts), + {Host_local, Host_remote} = case Orientation of + send -> {From#jid.lserver, To#jid.lserver}; + recv -> {To#jid.lserver, From#jid.lserver} + end, + Direction = case Host_remote of + Host_local -> internal; + _ -> + case lists:member(Host_remote, Hosts_all) of + true -> vhosts; + false -> external + end + end, + Body = case Message#msg.body of + false -> false; + _ -> true + end, + + ToBool = case Message#msg.to_user of + [] -> false; + _ -> true + end, + + OrientationO = case Message#msg.type of + "groupchat" -> + case Options#options.groupchat of + send -> [send]; + all -> [send, recv]; + none -> []; + _ -> [] + end; + _ -> + case Direction of + external -> [send, recv]; + _ -> [send] + end + end, + + lists:all(fun(O) -> O end, + [lists:member(Orientation, OrientationO), + lists:member(Stanza, [iq, message, presence, other]), + lists:member(Direction, [internal, vhosts, external]), + not lists:member(Message#msg.from_user++"@"++Message#msg.from_server, Options#options.ignore_jids), + not lists:member(Message#msg.to_user++"@"++Message#msg.to_server, Options#options.ignore_jids), + Body, + ToBool]). + +loop(Host, AllTables, LastPurgeTime, Options) -> + receive + {addlog, E} -> + %?MYDEBUG("Caught packet=~p", [E]), + %?MYDEBUG("Caught packet!", []), + Message = packet_parse(E), + case filter(E, Message, Options) of + true -> + %?MYDEBUG("Proccessing it", []), + {NewAllTables, NewLastPurgeTime} = do_log(Message, AllTables, LastPurgeTime, Options); + false -> + %?MYDEBUG("Skipping", []), + NewAllTables = AllTables, + NewLastPurgeTime = LastPurgeTime + end, + loop(Host, NewAllTables, NewLastPurgeTime, Options); + stop -> + ?MYDEBUG("Stopped", []), + ok; + _ -> + ?MYDEBUG("Received unknown packet!", []), + loop(Host, AllTables, LastPurgeTime, Options) + end. + +send_packet(FromJID, ToJID, P) -> + Host = FromJID#jid.lserver, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! {addlog, {send, FromJID, ToJID, P}}. + +receive_packet(_JID, From, To, P) -> + Host = To#jid.lserver, + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! {addlog, {recv, From, To, P}}. + +do_log(Msg, AllTables, LastPurgeTime, Options) -> + {Year, Month, Day} = date(), + Table_name = tables_prefix() ++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day), + % purge older tables every new day on first messages to log arrived + NewLastPurgeTime = case Options#options.purge_older_days of + never -> LastPurgeTime; + Days when is_integer(Days) -> + if + {Year, Month, Day} /= LastPurgeTime -> + purge_old_records("", ["purge-old-records", integer_to_list(Days)]), + {Year, Month, Day}; + true -> LastPurgeTime + end; + _ -> LastPurgeTime + end, + % create new table every new day + NewAllTables = case lists:member(Table_name, AllTables) of + true -> + %?MYDEBUG("~s was found at ~p", [Table_name, AllTables]), + log_message(Msg, Table_name), + AllTables; + false -> + %?MYDEBUG("~s was not found at ~p", [Table_name, AllTables]), + case create_msg_table(Table_name) of + { ok, ReturnedTable } -> + ?MYDEBUG("Created ~s table", [ReturnedTable]), + log_message(Msg, ReturnedTable), + lists:append(AllTables, [ReturnedTable]); + { error, Reason } -> + ?ERROR_MSG("Error occupied while creating ~s: ~p", [Table_name, Reason]), + AllTables + end + end, + {NewAllTables, NewLastPurgeTime}. + +log_message(Msg, Table_name) -> + % add timestamp to each #msg + Msg_with_ts = Msg#msg{timestamp=get_timestamp()}, + Fun = fun() -> + mnesia:write_lock_table(list_to_atom(Table_name)), + mnesia:write_lock_table(stats_table()), + case mnesia:write(list_to_atom(Table_name), Msg_with_ts, write) of + ok -> + ?MYDEBUG("Inserted from=~s to=~s ", [Msg#msg.from_user++"@"++Msg#msg.from_server, Msg#msg.to_user++"@"++Msg#msg.to_server]), + case increment_user_stats(Msg#msg.to_user, Msg#msg.to_server, Table_name, 1) of + ok -> ?MYDEBUG("Updated stats for ~s@~s", [Msg#msg.to_user, Msg#msg.to_server]); + ITReason -> ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Msg#msg.to_user, Msg#msg.to_server, ITReason]) + end, + case increment_user_stats(Msg#msg.from_user, Msg#msg.from_server, Table_name, 1) of + ok -> ?MYDEBUG("Updated stats for ~s@~s", [Msg#msg.from_user, Msg#msg.from_server]); + IFReason -> ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Msg#msg.from_user, Msg#msg.from_server, IFReason]) + end; + Reason -> ?ERROR_MSG("Failed to insert: ~p", [Reason]) + end + end, + % log message, increment stats for both users + mnesia:transaction(Fun). + +% It must be called from mnesia:transaction(). +increment_user_stats(User, Server, Table, Count) -> + Pat = #stats{user = User, server = Server, table=Table, count = '$1'}, + case mnesia:select(stats_table(), [{Pat, [], ['$_']}]) of + [] -> + mnesia:write(stats_table(), #stats{user=User, server=Server, table=Table, count=1}, write); + [Stats] -> + New = Stats#stats{count = Stats#stats.count+Count}, + case mnesia:delete_object(stats_table(), #stats{user=User, server=Server, table=Table, count=Stats#stats.count}, write) of + ok when New#stats.count > 0 -> mnesia:write(stats_table(), New, write); + DRes -> DRes + end + end. + +% return float seconds elapsed from "zero hour" +get_timestamp() -> + {MegaSec, Sec, MicroSec} = now(), + MegaSec*1000000 + Sec + MicroSec/1000000. + +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string +convert_timestamp(Seconds) when is_float(Seconds) -> + GregSec = trunc(Seconds + 719528*86400), + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec), + {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT), + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec); +% convert integer gregorian seconds to "%Y-%m-%d %H:%M:%S" string +convert_timestamp(Seconds) when is_integer(Seconds) -> + {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:gregorian_seconds_to_datetime(Seconds), + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec). + +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string +convert_timestamp_brief(Seconds) when is_float(Seconds) -> + GregSec = trunc(Seconds + 719528*86400), + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec), + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT), + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day); +% convert integer gregorian seconds to "%Y-%m-%d" string +convert_timestamp_brief(Seconds) when is_integer(Seconds) -> + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds), + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day). + +get_tables_list() -> + Tables = mnesia:system_info(tables), + lists:map(fun(Table) -> + atom_to_list(Table) + end, + lists:filter(fun(Table) -> + lists:prefix(tables_prefix(), atom_to_list(Table)) + end, + Tables)). + +create_msg_table(Name) -> + case mnesia:create_table( + list_to_atom(Name), + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, msg)}, + {record_name, msg}]) of + {atomic, ok} -> {ok, Name}; + {aborted, Reason} -> {error, Reason} + end. + +% return: {ok, [#msg1, #msg2, ...]} +% {error, Reason} +get_user_stats_at(User, Server, Date) -> + Table_name = tables_prefix() ++ Date, + case mnesia:transaction(fun() -> + Pat_to = #msg{to_user = User, to_server = Server, _ = '_'}, + Pat_from = #msg{from_user = User, from_server = Server, _ = '_'}, + mnesia:select(list_to_atom(Table_name), [{Pat_to, [], ['$_']}, {Pat_from, [], ['$_']}]) + end) of + {atomic, Result} -> {ok, Result}; + {aborted, Reason} -> {error, Reason} + end. + +% return: {ok, [{date1, msgs_count1}, {date2, msgs_count2}, ...]} +% {error, Reason} +get_user_stats(User, Server) -> + case mnesia:transaction(fun() -> + Pat = #stats{user = User, server = Server, table='$1', count = '$2'}, + mnesia:select(stats_table(), [{Pat, [], [['$1', '$2']]}]) + end) of + {atomic, Result} -> + Fun = fun([Table, Count]) -> + {lists:sublist(Table, length(tables_prefix())+1, length(Table)), Count} + end, + {ok, sort_stats(lists:map(Fun, Result))}; + {aborted, Reason} -> {error, Reason} + end. + +% return: {ok, [{"date1", msgs_count1}, {"date2", msgs_count2}, ... ]} +% {error, Reason} +get_vhost_stats(VHost) -> + Fun = fun() -> + Pat = #stats{user='_', server=VHost, table='$1', count = '$2'}, + mnesia:select(stats_table(), [{Pat, [], [['$1', '$2']]}]) + end, + case mnesia:transaction(Fun) of + {atomic, Result} -> + {ok, sort_stats(calc_vhost_stats(Result, [], tables_prefix()))}; + {aborted, Reason} -> {error, Reason} + end. + +calc_vhost_stats([], Rez, _Patt) -> + Rez; +calc_vhost_stats([[RealTable, Count] | Rest], Rez, Patt) -> + TableName = lists:sublist(RealTable, length(Patt)+1, length(RealTable)), + calc_vhost_stats(Rest, case lists:keysearch(TableName, 1, Rez) of + false -> + lists:append(Rez, [{TableName, Count}]); + {value, {_, TempCount}} -> + lists:keyreplace(TableName, 1, Rez, {TableName, TempCount+Count}) + end, Patt). +sort_stats(Stats) -> + % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ] + CFun = fun({TableName, Count}) -> + {ok, [Year, Month, Day]} = regexp:split(TableName, "[^0-9]+"), + { calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}), Count } + end, + % convert to [{63364377601,1}, {63360662401,1}, ... ] + CStats = lists:map(CFun, Stats), + % sort by date + SortedStats = lists:reverse(lists:keysort(1, CStats)), + RFun = fun({TableSec, Count}) -> + {convert_timestamp_brief(TableSec), Count} + end, + % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list + lists:map(RFun, SortedStats). + +% return: {ok, [{user1, msgs_count1}, {user2, msgs_count2}, ....]} +% {error, Reason} +get_vhost_stats_at(VHost, Date) -> + Fun = fun() -> + Pat = #stats{user='$1', server=VHost, table=tables_prefix()++Date, count = '$2'}, + mnesia:select(stats_table(), [{Pat, [], [['$1', '$2']]}]) + end, + case mnesia:transaction(Fun) of + {atomic, Result} -> + RFun = fun([User, Count]) -> + {User, Count} + end, + {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Result)))}; + {aborted, Reason} -> {error, Reason} + end. + +% delete stats for nonexistent tables +delete_nonexistent_stats(AllTables) -> + lists:foreach( + fun(Stat) -> + case lists:member(Stat#stats.table, AllTables) of + false -> + mnesia:dirty_delete_object(stats_table(), Stat); + true -> ok + end + end, mnesia:dirty_match_object(stats_table(), #stats{_='_'})). + +rebuild_stats(_Val, Server, ["rebuild-stats"]) -> + AllTables = get_tables_list(), + delete_nonexistent_stats(AllTables), + lists:foreach(fun(Table) -> + rebuild_stats_at(Server, Table) + end, AllTables), + {stop, ?STATUS_SUCCESS}; +rebuild_stats(Val, _Host, _Args) -> + Val. + +rebuild_stats_at(Server, Table) -> + %?MYDEBUG("Rebuilding stats for ~p at ~s", [Server, Table]), + Fun = fun() -> + mnesia:write_lock_table(stats_table()), + mnesia:write_lock_table(list_to_atom(Table)), + Pat_to = #msg{to_server = Server, _ = '_'}, + Pat_from = #msg{from_server = Server, _ = '_'}, + Result = mnesia:select(list_to_atom(Table), [{Pat_to, [], ['$_']}, {Pat_from, [], ['$_']}]), + update_day_stats(Server, Table, calc_day_stats(Server, Result, [])) + end, + mnesia:transaction(Fun). + +calc_day_stats(_Server, [], Stats) -> + Stats; +calc_day_stats(Server, [Msg | Rest], Stats) -> + To = Msg#msg.to_user ++ "@" ++ Msg#msg.to_server, + From = Msg#msg.from_user ++ "@" ++ Msg#msg.from_server, + Stats_to = if + (Msg#msg.to_server == Server) -> + case lists:keysearch(To, 1, Stats) of + {value, {Who_to, Count_to}} -> + lists:keyreplace(To, 1, Stats, {Who_to, Count_to + 1}); + false -> + lists:append(Stats, [{To, 1}]) + end; + true -> + Stats + end, + Stats_from = if + (Msg#msg.from_server == Server) -> + case lists:keysearch(From, 1, Stats) of + {value, {Who_from, Count_from}} -> + lists:keyreplace(From, 1, Stats_to, {Who_from, Count_from + 1}); + false -> + lists:append(Stats_to, [{From, 1}]) + end; + true -> + Stats_to + end, + calc_day_stats(Server, Rest, Stats_from). + +% It must be called from mnesia:transaction(). +update_day_stats(Server, Table, Stats) -> + TStats = mnesia:select(stats_table(), [{#stats{server=Server, table=Table, _='_'}, [], ['$_']}]), + lists:foreach(fun(TStat) -> + mnesia:delete_object(stats_table(), TStat, write) + end, TStats), + lists:foreach(fun({Who, Count}) -> + Jid = jlib:string_to_jid(Who), + JUser = Jid#jid.user, + JServer = Jid#jid.server, + Stat = #stats{user=JUser, server=JServer, table=Table, count=Count}, + mnesia:write(stats_table(), Stat, write) + end, Stats), + ?MYDEBUG("Updated ~s", [Table]). + +purge_old_records(_Val, ["purge-old-records", Days]) -> + Tables = get_tables_list(), + DateNow = calendar:datetime_to_gregorian_seconds({date(), {0,0,1}}), + DateDiff = list_to_integer(Days)*24*60*60, + ?MYDEBUG("Purging tables older than ~s days", [Days]), + lists:foreach(fun(Table) -> + Date = lists:sublist(Table, length(tables_prefix())+1, length(Table)), + {ok, [Year, Month, Day]} = regexp:split(Date, "[^0-9]+"), + DateInSec = calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}), + if + (DateNow - DateInSec) > DateDiff -> + case mnesia:delete_table(list_to_atom(Table)) of + {atomic, ok} -> ?MYDEBUG("Purged ~p", [Table]); + {aborted, TReason} -> ?MYDEBUG("Failed to purge ~s: ~p", [Table, TReason]) + end, + case mnesia:transaction( + fun() -> + Stats = mnesia:select(stats_table(), [{#stats{table=Table, _='_'}, [], ['$_']}]), + lists:foreach( + fun(Stat) -> + mnesia:delete_object(stats_table(), Stat, write) + end, Stats) + end) of + {atomic, ok} -> ?MYDEBUG("Updated stats",[]); + {aborted, SReason} -> ?MYDEBUG("Failed to update stats: ~p", [SReason]) + end; + true -> ?MYDEBUG("Skipping ~p", [Table]) + end, + {Table, DateInSec} + end, Tables), + {stop, ?STATUS_SUCCESS}; +purge_old_records(Val, _Args) -> + Val. + +user_messages_at_parse_query(User, _Server, Date, Msgs, Query) -> + case lists:keysearch("delete", 1, Query) of + {value, _} -> + Fun = fun() -> + lists:foreach( + fun(Msg) -> + FDate = convert_timestamp(Msg#msg.timestamp), + DJ = case (User == stringprep:tolower(Msg#msg.from_user)) of + true -> + Resource = case Msg#msg.to_resource of + [] -> []; + undefined -> []; + R -> "/" ++ R + end, + #dj{direction="To", jid=Msg#msg.to_user ++ "@" ++ Msg#msg.to_server ++ Resource}; + false -> + Resource = case Msg#msg.from_resource of + [] -> []; + undefined -> []; + R -> "/" ++ R + end, + #dj{direction="From", jid=Msg#msg.from_user ++ "@" ++ Msg#msg.from_server ++ Resource} + end, + Subj = case Msg#msg.subject of + "" -> "None"; + _ -> Msg#msg.subject + end, + Text = Msg#msg.body, + ID = jlib:encode_base64( + binary_to_list(term_to_binary(FDate++DJ#dj.direction++DJ#dj.jid++Subj++Text))), + case lists:member({"selected", ID}, Query) of + true -> + mnesia:write_lock_table(stats_table()), + mnesia:write_lock_table(list_to_atom(tables_prefix()++Date)), + mnesia:delete_object(list_to_atom(tables_prefix()++Date), Msg, write), + increment_user_stats(Msg#msg.to_user, Msg#msg.to_server, tables_prefix()++Date, -1), + increment_user_stats(Msg#msg.from_user, Msg#msg.from_server, tables_prefix()++Date, -1); + false -> + ok + end + end, Msgs) + end, + case mnesia:transaction(Fun) of + {error, _Reason} -> error; + _ -> ok + end; + false -> + nothing + end. + +user_messages_parse_query(User, Server, Tables, Query) -> + case lists:keysearch("delete", 1, Query) of + {value, _} -> + PTables = lists:filter( + fun({Table, _Count}) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Table))), + lists:member({"selected", ID}, Query) + end, Tables), + Fun = fun() -> + lists:foreach(fun({Table, _Count}) -> + {ok, Msgs} = get_user_stats_at(User, Server, Table), + mnesia:write_lock_table(list_to_atom(tables_prefix()++Table)), + lists:foreach(fun(Msg) -> + mnesia:delete_object(list_to_atom(tables_prefix()++Table), Msg, write) + end, Msgs) + end, PTables) + end, + mnesia:transaction(Fun), + lists:foreach(fun({Table, _Count}) -> + rebuild_stats_at(Server, tables_prefix()++Table) + end, PTables), + ok; + false -> + nothing + end. + +vhost_messages_parse_query(Server, Tables, Query) -> + case lists:keysearch("delete", 1, Query) of + {value, _} -> + Fun = fun() -> + lists:foreach( + fun({Table, _Count}) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(Server++Table))), + case lists:member({"selected", ID}, Query) of + true -> + mnesia:write_lock_table(stats_table()), + mnesia:write_lock_table(list_to_atom(tables_prefix()++Table)), + Pat_to = #msg{to_server = Server, _ = '_'}, + Pat_from = #msg{from_server = Server, _ = '_'}, + Msgs = mnesia:select(list_to_atom(tables_prefix()++Table), [{Pat_to, [], ['$_']}, {Pat_from, [], ['$_']}]), + lists:foreach(fun(Msg) -> + mnesia:delete_object(list_to_atom(tables_prefix()++Table), Msg, write) + end, Msgs), + + {ok, Stats} = get_vhost_stats_at(Server, Table), + lists:foreach(fun({User, Count}) -> + mnesia:delete_object(stats_table(), #stats{user=User, server=Server, table=tables_prefix()++Table, count=Count}, write) + end, Stats); + false -> + ok + end + end, Tables) + end, + case mnesia:transaction(Fun) of + {error, _Reason} -> error; + _ -> ok + end; + false -> + nothing + end. + +vhost_messages_at_parse_query(Server, Table, Users, Query) -> + case lists:keysearch("delete", 1, Query) of + {value, _} -> + Fun = fun() -> + lists:foreach( + fun({User, _Count}) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Server))), + case lists:member({"selected", ID}, Query) of + true -> + mnesia:write_lock_table(stats_table()), + mnesia:write_lock_table(list_to_atom(tables_prefix()++Table)), + Pat_to = #msg{to_user = User, to_server = Server, _ = '_'}, + Pat_from = #msg{from_user = User, from_server = Server, _ = '_'}, + Msgs = mnesia:select(list_to_atom(tables_prefix()++Table), [{Pat_to, [], ['$_']}, {Pat_from, [], ['$_']}]), + lists:foreach(fun(Msg) -> + mnesia:delete_object(list_to_atom(tables_prefix()++Table), Msg, write) + end, Msgs); + false -> + ok + end + end, Users) + end, + mnesia:transaction(Fun), + rebuild_stats_at(Server, tables_prefix()++Table), + ok; + false -> + nothing + end. +% TODO: search by body --- mod_logmnesia.hrl.orig Tue Oct 10 10:07:34 2006 +++ mod_logmnesia.hrl Mon Oct 9 15:12:38 2006 @@ -0,0 +1,12 @@ +%-define(logmnesia_debug, true). + +-ifdef(logmnesia_debug). +-define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n", + [calendar:local_time(),?MODULE,?LINE]++Args)). +-else. +-define(MYDEBUG(_F,_A),[]). +-endif. + +-record(msg, {to_user, to_server, to_resource, from_user, from_server, from_resource, id, type, subject, body, timestamp}). +-record(stats, {user, server, table, count}). +-record(dj, {direction, jid}). --- web/ejabberd_web_admin-1.1.2.erl Mon Oct 9 15:22:16 2006 +++ web/ejabberd_web_admin.erl Tue Oct 10 10:07:02 2006 @@ -21,6 +21,7 @@ -include("ejabberd.hrl"). -include("jlib.hrl"). -include("ejabberd_http.hrl"). +-include("mod_logmnesia.hrl"). -define(X(Name), {xmlelement, Name, [], []}). -define(XA(Name, Attrs), {xmlelement, Name, Attrs, []}). @@ -46,6 +47,11 @@ ?XA("input", [{"type", Type}, {"name", Name}, {"value", Value}])). +-define(INPUTC(Type, Name, Value), + ?XA("input", [{"type", Type}, + {"name", Name}, + {"value", Value}, + {"checked", "true"}])). -define(INPUTT(Type, Name, Value), ?INPUT(Type, Name, ?T(Value))). -define(INPUTS(Type, Name, Value, Size), ?XA("input", [{"type", Type}, @@ -137,6 +143,12 @@ [?LI([?ACT(Base ++ "shared-roster/", "Shared Roster")])]; false -> [] + end ++ + case gen_mod:is_loaded(Host, mod_logmnesia) of + true -> + [?LI([?ACT(Base ++ "messages/", "Users Messages")])]; + false -> + [] end )]), ?XAE("div", @@ -564,6 +576,12 @@ [?LI([?ACT(Base ++ "shared-roster/", "Shared Roster")])]; false -> [] + end ++ + case gen_mod:is_loaded(Host, mod_logmnesia) of + true -> + [?LI([?ACT(Base ++ "messages/", "Users Messages")])]; + false -> + [] end ) ], Host, Lang); @@ -925,6 +943,38 @@ make_xhtml(Res, Host, Lang); process_admin(Host, + #request{us = US, + path = ["messages"], + q = Query, + lang = Lang} = Request) when is_list(Host) -> + Res = vhost_messages_stats(Host, Query, Lang), + make_xhtml(Res, Host, Lang); + +process_admin(Host, + #request{us = US, + path = ["messages", Date], + q = Query, + lang = Lang} = Request) when is_list(Host) -> + Res = vhost_messages_stats_at(Host, Query, Lang, Date), + make_xhtml(Res, Host, Lang); + +process_admin(Host, + #request{us = US, + path = ["user", U, "messages"], + q = Query, + lang = Lang} = Request) -> + Res = user_messages_stats(U, Host, Query, Lang), + make_xhtml(Res, Host, Lang); + +process_admin(Host, + #request{us = US, + path = ["user", U, "messages", Date], + q = Query, + lang = Lang} = Request) -> + Res = user_messages_stats_at(U, Host, Query, Lang, Date), + make_xhtml(Res, Host, Lang); + +process_admin(Host, #request{us = US, path = ["user", U, "roster"], q = Query, @@ -1442,6 +1492,12 @@ [?XCT("h3", "Password:")] ++ FPassword ++ [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++ [?XE("h3", [?ACT("roster/", "Roster")])] ++ + case gen_mod:is_loaded(Server, mod_logmnesia) of + true -> + [?XE("h3", [?ACT("messages/", "Messages")])]; + false -> + [] + end ++ [?BR, ?INPUTT("submit", "removeuser", "Remove User")])]. @@ -1636,6 +1692,253 @@ ?INPUT("text", "newjid", ""), ?C(" "), ?INPUTT("submit", "addjid", "Add Jabber ID") ])]. + +vhost_messages_stats(Server, Query, Lang) -> + case mod_logmnesia:get_vhost_stats(Server) of + {ok, []} -> + [?XC("h1", ?T("No logged messages for ") ++ Server)]; + {ok, Tables} -> + Res = mod_logmnesia:vhost_messages_parse_query(Server, Tables, Query), + Fun = fun({Table, Count}) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(Server++Table))), + ?XE("tr", + [?XE("td", [?INPUT("checkbox", "selected", ID)]), + ?XE("td", [?AC(Table, Table)]), + ?XC("td", integer_to_list(Count)) + ]) + end, + [?XC("h1", ?T("Logged messages for ") ++ Server)] ++ + case Res of + ok -> [?CT("Submitted"), ?P]; + error -> [?CT("Bad format"), ?P]; + nothing -> [] + end ++ + [?XAE("form", [{"action", ""}, {"method", "post"}], + [?XE("table", + [?XE("thead", + [?XE("tr", + [?X("td"), + ?XCT("td", "Date"), + ?XCT("td", "Count") + ])]), + ?XE("tbody", + lists:map(Fun, Tables) + )]), + ?BR, + ?INPUTT("submit", "delete", "Delete Selected") + ])]; + {error, _Reason} -> + [?XC("h1", ?T("Error occupied while fetching list"))] + end. + +vhost_messages_stats_at(Server, Query, Lang, Date) -> + case mod_logmnesia:get_vhost_stats_at(Server, Date) of + {ok, []} -> + [?XC("h1", ?T("No logged messages for ") ++ Server ++ ?T(" at ") ++ Date)]; + {ok, Users} -> + Res = mod_logmnesia:vhost_messages_at_parse_query(Server, Date, Users, Query), + Fun = fun({User, Count}) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Server))), + ?XE("tr", + [?XE("td", [?INPUT("checkbox", "selected", ID)]), + ?XE("td", [?AC("../user/"++User++"/messages/"++Date, User)]), + ?XC("td", integer_to_list(Count)) + ]) + end, + [?XC("h1", ?T("Logged messages for ") ++ Server ++ ?T(" at ") ++ Date)] ++ + case Res of + ok -> [?CT("Submitted"), ?P]; + error -> [?CT("Bad format"), ?P]; + nothing -> [] + end ++ + [?XAE("form", [{"action", ""}, {"method", "post"}], + [?XE("table", + [?XE("thead", + [?XE("tr", + [?X("td"), + ?XCT("td", "User"), + ?XCT("td", "Count") + ])]), + ?XE("tbody", + lists:map(Fun, Users) + )]), + ?BR, + ?INPUTT("submit", "delete", "Delete Selected") + ])]; + {error, Reason} -> + [?XC("h1", ?T("Error occupied while fetching list"))] + end. + +user_messages_stats_at(User, Server, Query, Lang, Date) -> + US = {jlib:nodeprep(User), jlib:nameprep(Server)}, + Jid = us_to_list(US), + Result = case mod_logmnesia:get_user_stats_at(User, Server, Date) of + {ok, []} -> + [?XC("h1", ?T("No logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)]; + {ok, User_messages} -> + Res = mod_logmnesia:user_messages_at_parse_query(User, Server, Date, User_messages, Query), + Format_Fun = fun(Elem) -> + Dir_jid = case (User == stringprep:tolower(Elem#msg.from_user)) of + true -> + Resource = case Elem#msg.to_resource of + [] -> []; + undefined -> []; + R -> "/" ++ R + end, + #dj{direction="To", jid=Elem#msg.to_user ++ "@" ++ Elem#msg.to_server ++ Resource}; + false -> + Resource = case Elem#msg.from_resource of + [] -> []; + undefined -> []; + R -> "/" ++ R + end, + #dj{direction="From", jid=Elem#msg.from_user ++ "@" ++ Elem#msg.from_server ++ Resource} + end, + [Elem#msg.timestamp, + Dir_jid, + Elem#msg.type, + Elem#msg.subject, + Elem#msg.body] + end, + % #msg -> [timestamp, #dj, type, subject, body] + User_messages_formatted = lists:map(Format_Fun, User_messages), + % User unique interlocutors + UniqUsers = lists:foldl(fun([_, DJ1, _, _, _], List) -> + case lists:member(DJ1#dj.jid, List) of + true -> List; + false -> lists:append([DJ1#dj.jid], List) + end + end, [], User_messages_formatted), + % Users to filter (sublist of UniqUsers) + CheckedUsers = case lists:keysearch("filter", 1, Query) of + {value, _} -> + lists:filter(fun(User) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(User))), + lists:member({"selected", ID}, Query) + end, UniqUsers); + false -> [] + end, + % UniqUsers in html (noone selected -> everyone selected) + Users = lists:map(fun(User) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(User))), + Input = case lists:member(User, CheckedUsers) of + true -> [?INPUTC("checkbox", "selected", ID)]; + false when CheckedUsers == [] -> [?INPUTC("checkbox", "selected", ID)]; + false -> [?INPUT("checkbox", "selected", ID)] + end, + ?XE("tr", + [?XE("td", Input), + ?XC("td", User)]) + end, lists:sort(UniqUsers)), + % Messages to show (based on Users) + User_messages_filtered = case CheckedUsers of + [] -> User_messages_formatted; + _ -> lists:filter(fun([_, DJ, _, _, _]) -> + lists:member(DJ#dj.jid, CheckedUsers) + end, User_messages_formatted) + end, + Msgs_Fun = fun([Date_time, DJ, _MType, Subject, Text]) -> + %Type = case MType of + % "" -> "None"; + % _ -> MType + % end, + Subj = case Subject of + "" -> "None"; + _ -> Subject + end, + FDate = mod_logmnesia:convert_timestamp(Date_time), + ID = jlib:encode_base64(binary_to_list(term_to_binary(FDate++DJ#dj.direction++DJ#dj.jid++Subj++Text))), + ?XE("tr", + [?XE("td", [?INPUT("checkbox", "selected", ID)]), + ?XC("td", FDate), + ?XC("td", DJ#dj.direction ++ ": " ++ DJ#dj.jid), + %?XC("td", Type), + ?XC("td", Subj), + ?XC("td", Text)]) + end, + % Filtered user messages in html + Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)), + [?XC("h1", ?T("Logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)] ++ + case Res of + ok -> [?CT("Submitted"), ?P]; + error -> [?CT("Bad format"), ?P]; + nothing -> [] + end ++ + [?XAE("form", [{"action", ""}, {"method", "post"}], + [?XE("table", + [?XE("thead", + [?X("td"), + ?XCT("td", "User") + ] + ), + ?XE("tbody", + Users + )]), + ?INPUTT("submit", "filter", "Filter Selected") + ] ++ + [?XE("table", + [?XE("thead", + [?XE("tr", + [?X("td"), + ?XCT("td", "Date, Time"), + ?XCT("td", "Direction: Jid"), + %?XC("td", "Type"), + ?XCT("td", "Subject"), + ?XCT("td", "Body") + ])]), + ?XE("tbody", + Msgs + )]), + ?INPUTT("submit", "delete", "Delete Selected"), + ?BR + ] + )]; + {error, _Reason} -> + [?XC("h1", ?T("Error occupied while fetching messages"))] + end, + Result. + +user_messages_stats(User, Server, Query, Lang) -> + US = {jlib:nodeprep(User), jlib:nameprep(Server)}, + Jid = us_to_list(US), + Result = case mod_logmnesia:get_user_stats(User, Server) of + {ok, []} -> + [?XC("h1", ?T("No logged messages for ") ++ Jid)]; + {ok, Tables} -> + Res = mod_logmnesia:user_messages_parse_query(User, Server, Tables, Query), + Fun = fun({Table, Count}) -> + ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Table))), + ?XE("tr", + [?XE("td", [?INPUT("checkbox", "selected", ID)]), + ?XE("td", [?AC(Table, Table)]), + ?XC("td", integer_to_list(Count)) + ]) + %[?AC(Table, Table ++ " (" ++ integer_to_list(Count) ++ ")"), ?BR] + end, + [?XC("h1", ?T("Logged messages for ") ++ Jid)] ++ + case Res of + ok -> [?CT("Submitted"), ?P]; + error -> [?CT("Bad format"), ?P]; + nothing -> [] + end ++ + [?XAE("form", [{"action", ""}, {"method", "post"}], + [?XE("table", + [?XE("thead", + [?XE("tr", + [?X("td"), + ?XCT("td", "Date"), + ?XCT("td", "Count") + ])]), + ?XE("tbody", + lists:map(Fun, Tables) + )]), + ?BR, + ?INPUTT("submit", "delete", "Delete Selected") + ])]; + {error, _Reason} -> + [?XC("h1", ?T("Error occupied while fetching days"))] + end, + Result. user_roster_parse_query(User, Server, Items, Query, Admin) -> case lists:keysearch("addjid", 1, Query) of --- msgs/uk-1.1.2.msg Mon Oct 9 15:21:55 2006 +++ msgs/uk.msg Tue Oct 10 10:07:34 2006 @@ -371,6 +371,18 @@ {"Virtual Hosts", "Віртуальні хости"}. {"ejabberd virtual hosts", "віртуальні хости ejabberd"}. {"Host", "Хост"}. +{"Users Messages", "Повідомлення користувачів"}. +{"Date", "Дата"}. +{"Count", "Кількість"}. +{"Logged messages for ", "Збережені повідомлення для "}. +{" at ", " за "}. +{"No logged messages for ", "Відсутні повідомлення для "}. +{"Date, Time", "Дата, Час"}. +{"Direction: Jid", "Напрямок: Jid"}. +{"Subject", "Тема"}. +{"Body", "Текст"}. +{"Messages", "Повідомлення"}. +{"Filter Selected", "Відфільтрувати виділені"}. % Local Variables: % mode: erlang --- msgs/ru-1.1.2.msg Mon Oct 9 15:21:55 2006 +++ msgs/ru.msg Tue Oct 10 10:07:34 2006 @@ -371,6 +371,18 @@ {"Virtual Hosts", "Виртуальные хосты"}. {"ejabberd virtual hosts", "Виртуальные хосты ejabberd"}. {"Host", "Хост"}. +{"Users Messages", "Сообщения пользователей"}. +{"Date", "Дата"}. +{"Count", "Количество"}. +{"Logged messages for ", "Сохранённые cообщения для "}. +{" at ", " за "}. +{"No logged messages for ", "Отсутствуют сообщения для "}. +{"Date, Time", "Дата, Время"}. +{"Direction: Jid", "Направление: Jid"}. +{"Subject", "Тема"}. +{"Body", "Текст"}. +{"Messages", "Сообщения"}. +{"Filter Selected", "Отфильтровать выделенные"}. % Local Variables: % mode: erlang --- msgs/nl-1.1.2.msg Mon Oct 9 15:21:55 2006 +++ msgs/nl.msg Mon Oct 9 15:10:06 2006 @@ -331,4 +331,15 @@ {"Members:", "Groepsleden:"}. {"Displayed Groups:", "Weergegeven groepen:"}. {"Group ", "Groep "}. +{"Users Messages", "Gebruikersberichten"}. +{"Date", "Datum"}. +{"Count", "Aantal"}. +{"Logged messages for ", "Gelogde berichten van "}. +{" at ", " op "}. +{"No logged messages for ", "Geen gelogde berichten van "}. +{"Date, Time", "Datum en tijd"}. +{"Direction: Jid", "Richting: Jabber ID"}. +{"Subject", "Onderwerp"}. +{"Body", "Berichtveld"}. +{"Messages", "Berichten"}.