---- src/mod_logdb.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb.erl Thu Sep 20 15:26:21 2007
-@@ -0,0 +1,1656 @@
+diff --git src/gen_logdb.erl src/gen_logdb.erl
+new file mode 100644
+index 0000000..06a894b
+--- /dev/null
++++ src/gen_logdb.erl
+@@ -0,0 +1,164 @@
++%%%----------------------------------------------------------------------
++%%% File : gen_logdb.erl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose : Describes generic behaviour for mod_logdb backends.
++%%% Version : trunk
++%%% Id : $Id: gen_logdb.erl 1273 2009-02-05 18:12:57Z malik $
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
++
++-module(gen_logdb).
++-author('o.palij@gmail.com').
++
++-export([behaviour_info/1]).
++
++behaviour_info(callbacks) ->
++ [
++ % called from handle_info(start, _)
++ % it should logon database and return reference to started instance
++ % start(VHost, Opts) -> {ok, SPid} | error
++ % Options - list of options to connect to db
++ % Types: Options = list() -> [] |
++ % [{user, "logdb"},
++ % {pass, "1234"},
++ % {db, "logdb"}] | ...
++ % VHost = list() -> "jabber.example.org"
++ {start, 2},
++
++ % called from cleanup/1
++ % it should logoff database and do cleanup
++ % stop(VHost)
++ % Types: VHost = list() -> "jabber.example.org"
++ {stop, 1},
++
++ % called from handle_call({addlog, _}, _, _)
++ % it should log messages to database
++ % log_message(VHost, Msg) -> ok | error
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % Msg = record() -> #msg
++ {log_message, 2},
++
++ % called from ejabberdctl rebuild_stats
++ % it should rebuild stats table (if used) for vhost
++ % rebuild_stats(VHost)
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ {rebuild_stats, 1},
++
++ % it should rebuild stats table (if used) for vhost at Date
++ % rebuild_stats_at(VHost, Date)
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % Date = list() -> "2007-02-12"
++ {rebuild_stats_at, 2},
++
++ % called from user_messages_at_parse_query/5
++ % it should delete selected user messages at date
++ % delete_messages_by_user_at(VHost, Msgs, Date) -> ok | error
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % Msgs = list() -> [ #msg1, msg2, ... ]
++ % Date = list() -> "2007-02-12"
++ {delete_messages_by_user_at, 3},
++
++ % called from user_messages_parse_query/4 | vhost_messages_at_parse_query/4
++ % it should delete all user messages at date
++ % delete_all_messages_by_user_at(User, VHost, Date) -> ok | error
++ % Types:
++ % User = list() -> "admin"
++ % VHost = list() -> "jabber.example.org"
++ % Date = list() -> "2007-02-12"
++ {delete_all_messages_by_user_at, 3},
++
++ % called from vhost_messages_parse_query/3
++ % it should delete messages for vhost at date and update stats
++ % delete_messages_at(VHost, Date) -> ok | error
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % Date = list() -> "2007-02-12"
++ {delete_messages_at, 2},
++
++ % called from ejabberd_web_admin:vhost_messages_stats/3
++ % it should return sorted list of count of messages by dates for vhost
++ % get_vhost_stats(VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ... ]} |
++ % {error, Reason}
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % DateN = list() -> "2007-02-12"
++ % Msgs_countN = number() -> 241
++ {get_vhost_stats, 1},
++
++ % called from ejabberd_web_admin:vhost_messages_stats_at/4
++ % it should return sorted list of count of messages by users at date for vhost
++ % get_vhost_stats_at(VHost, Date) -> {ok, [{User1, Msgs_count1}, {User2, Msgs_count2}, ....]} |
++ % {error, Reason}
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % Date = list() -> "2007-02-12"
++ % UserN = list() -> "admin"
++ % Msgs_countN = number() -> 241
++ {get_vhost_stats_at, 2},
++
++ % called from ejabberd_web_admin:user_messages_stats/4
++ % it should return sorted list of count of messages by date for user at vhost
++ % get_user_stats(User, VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ...]} |
++ % {error, Reason}
++ % Types:
++ % User = list() -> "admin"
++ % VHost = list() -> "jabber.example.org"
++ % DateN = list() -> "2007-02-12"
++ % Msgs_countN = number() -> 241
++ {get_user_stats, 2},
++
++ % called from ejabberd_web_admin:user_messages_stats_at/5
++ % it should return all user messages at date
++ % get_user_messages_at(User, VHost, Date) -> {ok, Msgs} | {error, Reason}
++ % Types:
++ % User = list() -> "admin"
++ % VHost = list() -> "jabber.example.org"
++ % Date = list() -> "2007-02-12"
++ % Msgs = list() -> [ #msg1, msg2, ... ]
++ {get_user_messages_at, 3},
++
++ % called from many places
++ % it should return list of dates for vhost
++ % get_dates(VHost) -> [Date1, Date2, ... ]
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ % DateN = list() -> "2007-02-12"
++ {get_dates, 1},
++
++ % called from start
++ % it should return list with users settings for VHost in db
++ % get_users_settings(VHost) -> [#user_settings1, #user_settings2, ... ] | error
++ % Types:
++ % VHost = list() -> "jabber.example.org"
++ {get_users_settings, 1},
++
++ % called from many places
++ % it should return User settings at VHost from db
++ % get_user_settings(User, VHost) -> error | {ok, #user_settings}
++ % Types:
++ % User = list() -> "admin"
++ % VHost = list() -> "jabber.example.org"
++ {get_user_settings, 2},
++
++ % called from web admin
++ % it should set User settings at VHost
++ % set_user_settings(User, VHost, #user_settings) -> ok | error
++ % Types:
++ % User = list() -> "admin"
++ % VHost = list() -> "jabber.example.org"
++ {set_user_settings, 3},
++
++ % called from remove_user (ejabberd hook)
++ % it should remove user messages and settings at VHost
++ % drop_user(User, VHost) -> ok | error
++ % Types:
++ % User = list() -> "admin"
++ % VHost = list() -> "jabber.example.org"
++ {drop_user, 2}
++ ];
++behaviour_info(_) ->
++ undefined.
+diff --git src/mod_logdb.erl src/mod_logdb.erl
+new file mode 100644
+index 0000000..7de346f
+--- /dev/null
++++ src/mod_logdb.erl
+@@ -0,0 +1,2087 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_logdb.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
+%%% Purpose : Frontend for log user messages to db
+%%% Version : trunk
-+%%% Id : $Id$
++%%% Id : $Id: mod_logdb.erl 1360 2009-07-30 06:00:14Z malik $
+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
+%%%----------------------------------------------------------------------
+
+-module(mod_logdb).
+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+% gen_server
+-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
+% hooks
-+-export([send_packet/3, receive_packet/4, offline_packet/3]).
++-export([send_packet/3, receive_packet/4, remove_user/2]).
+-export([get_local_identity/5,
-+ get_local_features/5,
++ get_local_features/5,
+ get_local_items/5,
+ adhoc_local_items/4,
+ adhoc_local_commands/4
+ list_to_string/1, string_to_list/1,
+ get_module_settings/1, set_module_settings/2,
+ purge_old_records/2]).
++% webadmin hooks
++-export([webadmin_menu/3,
++ webadmin_user/4,
++ webadmin_page/3,
++ user_parse_query/5]).
++% webadmin queries
++-export([vhost_messages_stats/3,
++ vhost_messages_stats_at/4,
++ user_messages_stats/4,
++ user_messages_stats_at/5]).
+
+-include("mod_logdb.hrl").
+-include("ejabberd.hrl").
++-include("mod_roster.hrl").
+-include("jlib.hrl").
+-include("ejabberd_ctl.hrl").
+-include("adhoc.hrl").
++-include("web/ejabberd_web_admin.hrl").
++-include("web/ejabberd_http.hrl").
+
+-define(PROCNAME, ejabberd_mod_logdb).
+% gen_server call timeout
-+-define(CALL_TIMEOUT, 60000).
++-define(CALL_TIMEOUT, 10000).
+
-+-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings}).
++-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings, drop_messages_on_user_removal}).
+
+ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ VHost).
+
+% supervisor starts gen_server
+start_link(VHost, Opts) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []).
++ {ok, Pid} = gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []),
++ Pid ! start,
++ {ok, Pid}.
+
+init([VHost, Opts]) ->
++ ?MYDEBUG("Starting mod_logdb", []),
+ process_flag(trap_exit, true),
+ DBs = gen_mod:get_opt(dbs, Opts, [{mnesia, []}]),
+ VHostDB = gen_mod:get_opt(vhosts, Opts, [{VHost, mnesia}]),
+
+ DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)),
+
-+ % actually all work begin on receiving start signal
-+ timer:send_after(1000, start),
-+
+ {ok, #state{vhost=VHost,
+ dbmod=DBMod,
+ dbopts=DBOpts,
+ % dbs used for convert messages from one backend to other
+ dbs=DBs,
+ dolog_default=gen_mod:get_opt(dolog_default, Opts, true),
++ drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts, true),
+ 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),
+ poll_users_settings=PollUsersSettings}}.
+
-+cleanup(#state{vhost=VHost} = State) ->
++cleanup(#state{vhost=VHost} = _State) ->
+ ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
+
+ %ets:delete(ets_settings_table(VHost)),
+
++ ejabberd_hooks:delete(remove_user, VHost, ?MODULE, remove_user, 90),
+ ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90),
+ ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
-+ ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_packet, 10),
+ %ejabberd_hooks:delete(adhoc_sm_commands, VHost, ?MODULE, adhoc_sm_commands, 110),
+ %ejabberd_hooks:delete(adhoc_sm_items, VHost, ?MODULE, adhoc_sm_items, 110),
+ ejabberd_hooks:delete(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 110),
+ ejabberd_hooks:delete(disco_local_features, VHost, ?MODULE, get_local_features, 110),
+ ejabberd_hooks:delete(disco_local_items, VHost, ?MODULE, get_local_items, 110),
+
++ ejabberd_hooks:delete(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
++ ejabberd_hooks:delete(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
++ ejabberd_hooks:delete(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
++ ejabberd_hooks:delete(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
++
+ ?MYDEBUG("Removed hooks for ~p", [VHost]),
+
-+ ejabberd_ctl:unregister_commands(VHost, [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ?MODULE, rebuild_stats),
-+ Supported_backends = lists:flatmap(fun({Backend, _Opts}) ->
-+ [atom_to_list(Backend), " "]
-+ end, State#state.dbs),
-+ ejabberd_ctl:unregister_commands(
-+ VHost,
-+ [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }],
-+ ?MODULE, copy_messages_ctl),
++ %ejabberd_ctl:unregister_commands(VHost, [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ?MODULE, rebuild_stats),
++ %Supported_backends = lists:flatmap(fun({Backend, _Opts}) ->
++ % [atom_to_list(Backend), " "]
++ % end, State#state.dbs),
++ %ejabberd_ctl:unregister_commands(
++ % VHost,
++ % [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }],
++ % ?MODULE, copy_messages_ctl),
+ ?MYDEBUG("Unregistered commands for ~p", [VHost]).
+
+stop(VHost) ->
+ NewState = State#state{dolog_default=Settings#state.dolog_default,
+ ignore_jids=Settings#state.ignore_jids,
+ groupchat=Settings#state.groupchat,
++ drop_messages_on_user_removal=Settings#state.drop_messages_on_user_removal,
+ purge_older_days=PurgeDays,
+ poll_users_settings=PollSec,
+ purgeRef=PurgeRef,
+ ok
+ end,
+ {noreply, State};
++handle_cast({remove_user, User}, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ case State#state.drop_messages_on_user_removal of
++ true ->
++ DBMod:drop_user(User, VHost),
++ ?INFO_MSG("Launched ~s@~s removal", [User, VHost]);
++ false ->
++ ?INFO_MSG("Message removing is disabled. Keeping messages for ~s@~s", [User, VHost])
++ end,
++ {noreply, State};
+% ejabberdctl rebuild_stats/3
+handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) ->
-+ % TODO: maybe spawn?
+ DBMod:rebuild_stats(VHost),
+ {noreply, State};
+handle_cast({copy_messages, Backend}, State) ->
+% from timer:send_after (in init)
+handle_info(start, #state{dbmod=DBMod, vhost=VHost}=State) ->
+ case DBMod:start(VHost, State#state.dbopts) of
-+ {error, _Reason} ->
++ {error,{already_started,_}} ->
++ ?MYDEBUG("backend module already started - trying to stop it", []),
++ DBMod:stop(VHost),
++ {stop, already_started, State};
++ {error, Reason} ->
+ timer:sleep(30000),
++ ?ERROR_MSG("Failed to start: ~p", [Reason]),
+ {stop, db_connection_failed, State};
+ {ok, SPid} ->
-+
+ ?INFO_MSG("~p connection established", [DBMod]),
-+
++
+ MonRef = erlang:monitor(process, SPid),
+
+ ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]),
+ TrefPurge = set_purge_timer(State#state.purge_older_days),
+ TrefPoll = set_poll_timer(State#state.poll_users_settings),
+
++ ejabberd_hooks:add(remove_user, VHost, ?MODULE, remove_user, 90),
+ ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90),
+ ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
-+ ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_packet, 10),
+
+ ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 110),
+ ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 110),
+ %ejabberd_hooks:add(adhoc_sm_items, VHost, ?MODULE, adhoc_sm_items, 110),
+ %ejabberd_hooks:add(adhoc_sm_commands, VHost, ?MODULE, adhoc_sm_commands, 110),
+
++ ejabberd_hooks:add(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
++ ejabberd_hooks:add(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
++ ejabberd_hooks:add(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
++ ejabberd_hooks:add(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
++
+ ?MYDEBUG("Added hooks for ~p", [VHost]),
+
-+ ejabberd_ctl:register_commands(
-+ VHost,
-+ [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}],
-+ ?MODULE, rebuild_stats),
-+ Supported_backends = lists:flatmap(fun({Backend, _Opts}) ->
-+ [atom_to_list(Backend), " "]
-+ end, State#state.dbs),
-+ ejabberd_ctl:register_commands(
-+ VHost,
-+ [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }],
-+ ?MODULE, copy_messages_ctl),
++ %ejabberd_ctl:register_commands(
++ % VHost,
++ % [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}],
++ % ?MODULE, rebuild_stats),
++ %Supported_backends = lists:flatmap(fun({Backend, _Opts}) ->
++ % [atom_to_list(Backend), " "]
++ % end, State#state.dbs),
++ %ejabberd_ctl:register_commands(
++ % VHost,
++ % [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }],
++ % ?MODULE, copy_messages_ctl),
+ ?MYDEBUG("Registered commands for ~p", [VHost]),
+
+ NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll},
+terminate(db_connection_failed, _State) ->
+ ok;
+terminate(db_connection_dropped, State) ->
++ ?MYDEBUG("Got terminate with db_connection_dropped", []),
+ cleanup(State),
+ ok;
-+terminate(_Reason, #state{monref=undefined} = State) ->
++terminate(Reason, #state{monref=undefined} = State) ->
++ ?MYDEBUG("Got terminate with undefined monref.~nReason: ~p", [Reason]),
+ cleanup(State),
+ ok;
+terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:cast(Proc, {addlog, to, Owner, Peer, P}).
+
-+offline_packet(Peer, Owner, P) ->
++receive_packet(_JID, Peer, Owner, P) ->
+ VHost = Owner#jid.lserver,
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:cast(Proc, {addlog, from, Owner, Peer, P}).
+
-+receive_packet(_JID, Peer, Owner, P) ->
-+ VHost = Owner#jid.lserver,
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:cast(Proc, {addlog, from, Owner, Peer, P}).
++remove_user(User, Server) ->
++ LUser = jlib:nodeprep(User),
++ LServer = jlib:nameprep(Server),
++ Proc = gen_mod:get_module_proc(LServer, ?PROCNAME),
++ gen_server:cast(Proc, {remove_user, LUser}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+ Ni=lists:foldl(fun([{muc_online_room, {GName, GHost}, Pid}], Names) ->
+ case gen_fsm:sync_send_all_state_event(Pid, {get_jid_nick,Owner}) of
+ [] -> Names;
-+ Nick ->
++ Nick ->
+ lists:append(Names, [jlib:jid_to_string({GName, GHost, Nick})])
+ end
+ end, [], Rooms),
+ _ -> State#state.dolog_default
+ end,
+
-+ lists:all(fun(O) -> O end,
++ lists:all(fun(O) -> O end,
+ [not lists:member(OwnerStr, State#state.ignore_jids),
+ not lists:member(PeerStr, State#state.ignore_jids),
+ not lists:member(OwnerServ, State#state.ignore_jids),
+purge_old_records(VHost, Days) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+
-+ Dates = gen_server:call(Proc, {get_dates, {VHost}}),
++ Dates = ?MODULE:get_dates(VHost),
+ 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(Date) ->
-+ {ok, [Year, Month, Day]} = regexp:split(Date, "[^0-9]+"),
++ [Year, Month, Day] = ejabberd_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 ->
+ gen_server:call(Proc, {delete_messages_at, Date});
-+ true ->
++ true ->
+ ?MYDEBUG("Skipping messages at ~p", [Date])
+ end
+ end, Dates).
+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]+"),
++ [Year, Month, Day] = ejabberd_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}, ... ]
+ end.
+
+user_messages_parse_query(User, VHost, Query) ->
-+ Dates = get_dates(VHost),
+ case lists:keysearch("delete", 1, Query) of
+ {value, _} ->
++ Dates = get_dates(VHost),
+ PDates = lists:filter(
+ fun(Date) ->
+ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))),
+ end.
+
+vhost_messages_parse_query(VHost, Query) ->
-+ Dates = get_dates(VHost),
+ case lists:keysearch("delete", 1, Query) of
+ {value, _} ->
++ Dates = get_dates(VHost),
+ PDates = lists:filter(
+ fun(Date) ->
+ ID = jlib:encode_base64(binary_to_list(term_to_binary(VHost++Date))),
+ FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
+
+ {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
-+
++
+ Dates = FromDBMod:get_dates(VHost),
+ DatesLength = length(Dates),
+
+
+copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) ->
+ ?INFO_MSG("Going to copy messages from ~p for ~p at ~p", [FromDBMod, VHost, Date]),
-+
++
+ ok = FromDBMod:rebuild_stats_at(VHost, Date),
+ catch mod_logdb:rebuild_stats_at(VHost, Date),
+ {ok, FromStats} = FromDBMod:get_vhost_stats_at(VHost, Date),
+string_to_list([]) ->
+ [];
+string_to_list(String) ->
-+ {ok, List} = regexp:split(String, "\n"),
-+ List.
++ ejabberd_regexp:split(String, "\n").
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+ Users ->
+ SUsers = lists:sort([{S, U} || {U, S} <- Users]),
+ case catch begin
-+ {ok, [S1, S2]} = regexp:split(Diap, "-"),
++ [S1, S2] = ejabberd_regexp:split(Diap, "-"),
+ N1 = list_to_integer(S1),
+ N2 = list_to_integer(S2),
+ Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
+ ignore_jids=IgnoreJids,
+ groupchat=GroupChat,
+ purge_older_days=PurgeDaysT,
++ drop_messages_on_user_removal=MRemoval,
+ poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
+
+ Backends = lists:map(fun({Backend, _Opts}) ->
+ ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"),
+ ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false")
+ ]},
++ % drop_messages_on_user_removal
++ {xmlelement, "field", [{"type", "list-single"},
++ {"label",
++ translate:translate(Lang, "Drop messages on user removal")},
++ {"var", "drop_messages_on_user_removal"}],
++ [?DEFVAL(atom_to_list(MRemoval)),
++ ?LISTLINE(translate:translate(Lang, "Drop"), "true"),
++ ?LISTLINE(translate:translate(Lang, "Do not drop"), "false")
++ ]},
+ % groupchat
+ {xmlelement, "field", [{"type", "list-single"},
+ {"label",
+
+parse_users_settings(XData) ->
+ DLD = case lists:keysearch("dolog_default", 1, XData) of
-+ {value, {_, [String]}} when String == "true"; String == "false" ->
++ {value, {_, [String]}} when String == "true"; String == "false" ->
+ list_to_bool(String);
+ _ ->
+ throw(bad_request)
+ _ ->
+ throw(bad_request)
+ end,
++ MRemoval = case lists:keysearch("drop_messages_on_user_removal", 1, XData) of
++ {value, {_, [Str5]}} when Str5 == "true"; Str5 == "false" ->
++ list_to_bool(Str5);
++ _ ->
++ throw(bad_request)
++ end,
+ GroupChat = case lists:keysearch("groupchat", 1, XData) of
+ {value, {_, [Str2]}} when Str2 == "none";
+ Str2 == "all";
+ groupchat=GroupChat,
+ ignore_jids=Ignore,
+ purge_older_days=Purge,
++ drop_messages_on_user_removal=MRemoval,
+ poll_users_settings=Poll}.
+
+set_form(From, _Host, ["mod_logdb"], _Lang, XData) ->
+ end, lists:seq(1, N, M))
+ end
+ end.
---- src/mod_logdb.hrl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb.hrl Tue Aug 7 16:50:32 2007
-@@ -0,0 +1,29 @@
-+%%%----------------------------------------------------------------------
-+%%% File : mod_logdb.hrl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose :
-+%%% Version : trunk
-+%%% Id : $Id$
-+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
-+%%%----------------------------------------------------------------------
-+
-+-define(logdb_debug, true).
-+
-+-ifdef(logdb_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, {timestamp,
-+ owner_name,
-+ peer_name, peer_server, peer_resource,
-+ direction,
-+ type, subject,
-+ body}).
-+
-+-record(user_settings, {owner_name,
-+ dolog_default,
-+ dolog_list=[],
-+ donotlog_list=[]}).
---- src/mod_logdb_mnesia.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb_mnesia.erl Wed Aug 22 22:58:11 2007
-@@ -0,0 +1,513 @@
-+%%%----------------------------------------------------------------------
-+%%% File : mod_logdb_mnesia.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose : mnesia backend for mod_logdb
-+%%% Version : trunk
-+%%% Id : $Id$
-+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
-+%%%----------------------------------------------------------------------
-+
-+-module(mod_logdb_mnesia).
-+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
-+
-+-include("mod_logdb.hrl").
-+-include("ejabberd.hrl").
-+-include("jlib.hrl").
-+
-+-behaviour(gen_logdb).
-+-behaviour(gen_server).
-+
-+% gen_server
-+-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
-+% gen_mod
-+-export([start/2, stop/1]).
-+% gen_logdb
-+-export([log_message/2,
-+ rebuild_stats/1,
-+ rebuild_stats_at/2,
-+ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
-+ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
-+ get_dates/1,
-+ get_users_settings/1, get_user_settings/2, set_user_settings/3]).
-+
-+-define(PROCNAME, mod_logdb_mnesia).
-+-define(CALL_TIMEOUT, 240000).
-+
-+-record(state, {vhost}).
-+
-+-record(stats, {user, at, count}).
-+
-+prefix() ->
-+ "logdb_".
-+
-+suffix(VHost) ->
-+ "_" ++ VHost.
-+
-+stats_table(VHost) ->
-+ list_to_atom(prefix() ++ "stats" ++ suffix(VHost)).
-+
-+table_name(VHost, Date) ->
-+ list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)).
-+
-+settings_table(VHost) ->
-+ list_to_atom(prefix() ++ "settings" ++ suffix(VHost)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% gen_mod callbacks
++% webadmin hooks
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+start(VHost, Opts) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
++webadmin_menu(Acc, _Host, Lang) ->
++ [{"messages", ?T("Users Messages")} | Acc].
++
++webadmin_user(Acc, User, Server, Lang) ->
++ Sett = get_user_settings(User, Server),
++ Log =
++ case Sett#user_settings.dolog_default of
++ false ->
++ ?INPUTT("submit", "dolog", "Log Messages");
++ true ->
++ ?INPUTT("submit", "donotlog", "Do Not Log Messages");
++ _ -> []
++ end,
++ Acc ++ [?XE("h3", [?ACT("messages/", "Messages"), ?C(" "), Log])].
+
-+stop(VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
++webadmin_page(_, Host,
++ #request{path = ["messages"],
++ q = Query,
++ lang = Lang}) when is_list(Host) ->
++ Res = vhost_messages_stats(Host, Query, Lang),
++ {stop, Res};
++webadmin_page(_, Host,
++ #request{path = ["messages", Date],
++ q = Query,
++ lang = Lang}) when is_list(Host) ->
++ Res = vhost_messages_stats_at(Host, Query, Lang, Date),
++ {stop, Res};
++webadmin_page(_, Host,
++ #request{path = ["user", U, "messages"],
++ q = Query,
++ lang = Lang}) ->
++ Res = user_messages_stats(U, Host, Query, Lang),
++ {stop, Res};
++webadmin_page(_, Host,
++ #request{path = ["user", U, "messages", Date],
++ q = Query,
++ lang = Lang}) ->
++ Res = mod_logdb:user_messages_stats_at(U, Host, Query, Lang, Date),
++ {stop, Res};
++webadmin_page(Acc, _, _) -> Acc.
++
++user_parse_query(_, "dolog", User, Server, _Query) ->
++ Sett = get_user_settings(User, Server),
++ % TODO: check returned value
++ set_user_settings(User, Server, Sett#user_settings{dolog_default=true}),
++ {stop, ok};
++user_parse_query(_, "donotlog", User, Server, _Query) ->
++ Sett = get_user_settings(User, Server),
++ % TODO: check returned value
++ set_user_settings(User, Server, Sett#user_settings{dolog_default=false}),
++ {stop, ok};
++user_parse_query(Acc, _Action, _User, _Server, _Query) ->
++ Acc.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% gen_server callbacks
++% webadmin funcs
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+init([VHost, _Opts]) ->
-+ case mnesia:system_info(is_running) of
-+ yes ->
-+ ok = create_stats_table(VHost),
-+ ok = create_settings_table(VHost),
-+ {ok, #state{vhost=VHost}};
-+ no ->
-+ ?ERROR_MSG("Mnesia not running", []),
-+ {stop, db_connection_failed};
-+ Status ->
-+ ?ERROR_MSG("Mnesia status: ~p", [Status]),
-+ {stop, db_connection_failed}
++vhost_messages_stats(Server, Query, Lang) ->
++ Res = case catch vhost_messages_parse_query(Server, Query) of
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("~p", [Reason]),
++ error;
++ VResult -> VResult
++ end,
++ {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]),
++ ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]),
++ %case get_vhost_stats(Server) of
++ case Value of
++ {'EXIT', CReason} ->
++ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
++ [?XC("h1", ?T("Error occupied while fetching list"))];
++ {error, GReason} ->
++ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
++ [?XC("h1", ?T("Error occupied while fetching list"))];
++ {ok, []} ->
++ [?XC("h1", ?T("No logged messages for ") ++ Server)];
++ {ok, Dates} ->
++ Fun = fun({Date, Count}) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(Server++Date))),
++ ?XE("tr",
++ [?XE("td", [?INPUT("checkbox", "selected", ID)]),
++ ?XE("td", [?AC(Date, Date)]),
++ ?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, Dates)
++ )]),
++ ?BR,
++ ?INPUTT("submit", "delete", "Delete Selected")
++ ])]
+ end.
+
-+handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) ->
-+ {reply, log_message_int(VHost, Msg), State};
-+handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) ->
-+ {atomic, ok} = delete_nonexistent_stats(VHost),
-+ Reply =
-+ lists:foreach(fun(Date) ->
-+ rebuild_stats_at_int(VHost, Date)
-+ end, get_dates_int(VHost)),
-+ {reply, Reply, State};
-+handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
-+ Reply = rebuild_stats_at_int(VHost, Date),
-+ {reply, Reply, State};
-+handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
-+ Table = table_name(VHost, Date),
-+ Fun = fun() ->
-+ lists:foreach(
-+ fun(Msg) ->
-+ mnesia:write_lock_table(stats_table(VHost)),
-+ mnesia:write_lock_table(Table),
-+ mnesia:delete_object(Table, Msg, write)
-+ end, Msgs)
-+ end,
-+ DRez = case mnesia:transaction(Fun) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
-+ error;
-+ _ ->
-+ ok
-+ end,
-+ Reply =
-+ case rebuild_stats_at_int(VHost, Date) of
-+ error ->
-+ error;
-+ ok ->
-+ DRez
-+ end,
-+ {reply, Reply, State};
-+handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) ->
-+ Table = table_name(VHost, Date),
-+ MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc)
-+ when Owner == User ->
-+ mnesia:delete_object(Table, Msg, write),
-+ ok;
-+ (_Msg, _Acc) -> ok
-+ end,
-+ DRez = case mnesia:transaction(fun() ->
-+ mnesia:foldl(MsgDelete, ok, Table)
-+ end) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]),
-+ error;
-+ _ ->
-+ ok
-+ end,
-+ Reply =
-+ case rebuild_stats_at_int(VHost, Date) of
-+ error ->
-+ error;
-+ ok ->
-+ DRez
-+ end,
-+ {reply, Reply, State};
-+handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) ->
-+ Reply =
-+ case mnesia:delete_table(table_name(VHost, Date)) of
-+ {atomic, ok} ->
-+ delete_stats_by_vhost_at_int(VHost, Date);
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]),
-+ error
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) ->
-+ Fun = fun(#stats{at=Date, count=Count}, Stats) ->
-+ case lists:keysearch(Date, 1, Stats) of
-+ false ->
-+ lists:append(Stats, [{Date, Count}]);
-+ {value, {_, TempCount}} ->
-+ lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count})
-+ end
-+ end,
-+ Reply =
-+ case mnesia:transaction(fun() ->
-+ mnesia:foldl(Fun, [], stats_table(VHost))
-+ end) of
-+ {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)};
-+ {aborted, Reason} -> {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
-+ Fun = fun() ->
-+ Pat = #stats{user='$1', at=Date, count='$2'},
-+ mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
++vhost_messages_stats_at(Server, Query, Lang, Date) ->
++ {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]),
++ ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]),
++ %case get_vhost_stats_at(Server, Date) of
++ case Value of
++ {'EXIT', CReason} ->
++ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
++ [?XC("h1", ?T("Error occupied while fetching list"))];
++ {error, GReason} ->
++ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
++ [?XC("h1", ?T("Error occupied while fetching list"))];
++ {ok, []} ->
++ [?XC("h1", ?T("No logged messages for ") ++ Server ++ ?T(" at ") ++ Date)];
++ {ok, Users} ->
++ Res = case catch vhost_messages_at_parse_query(Server, Date, Users, Query) of
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("~p", [Reason]),
++ error;
++ VResult -> VResult
++ end,
++ 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")
++ ])]
++ end.
++
++user_messages_stats(User, Server, Query, Lang) ->
++ Jid = jlib:jid_to_string({User, Server, ""}),
++
++ Res = case catch user_messages_parse_query(User, Server, Query) of
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("~p", [Reason]),
++ error;
++ VResult -> VResult
+ end,
-+ Reply =
-+ case mnesia:transaction(Fun) of
-+ {atomic, Result} ->
-+ {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))};
-+ {aborted, Reason} ->
-+ {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) ->
-+ Reply =
-+ case mnesia:transaction(fun() ->
-+ Pat = #stats{user=User, at='$1', count='$2'},
-+ mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
-+ end) of
-+ {atomic, Result} ->
-+ {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])};
-+ {aborted, Reason} ->
-+ {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) ->
-+ Reply =
-+ case mnesia:transaction(fun() ->
-+ Pat = #msg{owner_name=User, _='_'},
-+ mnesia:select(table_name(VHost, Date),
-+ [{Pat, [], ['$_']}])
-+ end) of
-+ {atomic, Result} -> {ok, Result};
-+ {aborted, Reason} ->
-+ {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_dates}, _From, #state{vhost=VHost}=State) ->
-+ {reply, get_dates_int(VHost), State};
-+handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) ->
-+ Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}),
-+ {reply, {ok, Reply}, State};
-+handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) ->
-+ Reply =
-+ case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of
-+ [] -> [];
-+ [Setting] ->
-+ Setting
-+ end,
-+ {reply, Reply, State};
-+handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) ->
-+ ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]),
-+ Reply = mnesia:dirty_write(settings_table(VHost), Set),
-+ {reply, Reply, State};
-+handle_call({stop}, _From, State) ->
-+ {stop, normal, ok, State};
-+handle_call(Msg, _From, State) ->
-+ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
-+ {noreply, State}.
+
-+handle_cast(Msg, State) ->
-+ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
-+ {noreply, State}.
++ {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]),
++ ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]),
+
-+handle_info(Info, State) ->
-+ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
-+ {noreply, State}.
++ case Value of
++ {'EXIT', CReason} ->
++ ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
++ [?XC("h1", ?T("Error occupied while fetching days"))];
++ {error, GReason} ->
++ ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
++ [?XC("h1", ?T("Error occupied while fetching days"))];
++ {ok, []} ->
++ [?XC("h1", ?T("No logged messages for ") ++ Jid)];
++ {ok, Dates} ->
++ Fun = fun({Date, Count}) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))),
++ ?XE("tr",
++ [?XE("td", [?INPUT("checkbox", "selected", ID)]),
++ ?XE("td", [?AC(Date, Date)]),
++ ?XC("td", integer_to_list(Count))
++ ])
++ %[?AC(Date, Date ++ " (" ++ 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, Dates)
++ )]),
++ ?BR,
++ ?INPUTT("submit", "delete", "Delete Selected")
++ ])]
++ end.
+
-+terminate(_Reason, _State) ->
-+ ok.
++search_user_nick(User, List) ->
++ case lists:keysearch(User, 1, List) of
++ {value,{User, []}} ->
++ nothing;
++ {value,{User, Nick}} ->
++ Nick;
++ false ->
++ nothing
++ end.
+
-+code_change(_OldVsn, State, _Extra) ->
-+ {ok, State}.
++user_messages_stats_at(User, Server, Query, Lang, Date) ->
++ Jid = jlib:jid_to_string({User, Server, ""}),
+
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+%
-+% gen_logdb callbacks
-+%
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+log_message(VHost, Msg) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
-+rebuild_stats(VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
-+rebuild_stats_at(VHost, Date) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
-+delete_messages_by_user_at(VHost, Msgs, Date) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
-+delete_all_messages_by_user_at(User, VHost, Date) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
-+delete_messages_at(VHost, Date) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
-+get_vhost_stats(VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
-+get_vhost_stats_at(VHost, Date) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
-+get_user_stats(User, VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
-+get_user_messages_at(User, VHost, Date) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
-+get_dates(VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
-+get_user_settings(User, VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
-+get_users_settings(VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
-+set_user_settings(User, VHost, Set) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
++ {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]),
++ ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]),
++ case Value of
++ {'EXIT', CReason} ->
++ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
++ [?XC("h1", ?T("Error occupied while fetching messages"))];
++ {error, GReason} ->
++ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
++ [?XC("h1", ?T("Error occupied while fetching messages"))];
++ {ok, []} ->
++ [?XC("h1", ?T("No logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)];
++ {ok, User_messages} ->
++ Res = case catch user_messages_at_parse_query(Server,
++ Date,
++ User_messages,
++ Query) of
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("~p", [Reason]),
++ error;
++ VResult -> VResult
++ end,
+
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+%
-+% internals
-+%
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+log_message_int(VHost, #msg{timestamp=Timestamp}=Msg) ->
-+ Date = mod_logdb:convert_timestamp_brief(Timestamp),
++ UR = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]),
++ UserRoster =
++ lists:map(fun(Item) ->
++ {jlib:jid_to_string(Item#roster.jid), Item#roster.name}
++ end, UR),
+
-+ ATable = table_name(VHost, Date),
-+ Fun = fun() ->
-+ mnesia:write_lock_table(ATable),
-+ mnesia:write(ATable, Msg, write)
-+ end,
-+ % log message, increment stats for both users
-+ case mnesia:transaction(Fun) of
-+ % if table does not exists - create it and try to log message again
-+ {aborted,{no_exists, _Table}} ->
-+ case create_msg_table(VHost, Date) of
-+ {aborted, CReason} ->
-+ ?ERROR_MSG("Failed to log message: ~p", [CReason]),
-+ error;
-+ {atomic, ok} ->
-+ ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
-+ log_message_int(VHost, Msg)
-+ end;
-+ {aborted, TReason} ->
-+ ?ERROR_MSG("Failed to log message: ~p", [TReason]),
-+ error;
-+ {atomic, _} ->
-+ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
-+ Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
-+ increment_user_stats(Msg#msg.owner_name, VHost, Date)
-+ end.
++ UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) ->
++ ToAdd = PName++"@"++PServer,
++ case lists:member(ToAdd, List) of
++ true -> List;
++ false -> lists:append([ToAdd], List)
++ end
++ end, [], User_messages),
+
-+increment_user_stats(Owner, VHost, Date) ->
-+ Fun = fun() ->
-+ Pat = #stats{user=Owner, at=Date, count='$1'},
-+ mnesia:write_lock_table(stats_table(VHost)),
-+ case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of
-+ [] ->
-+ mnesia:write(stats_table(VHost),
-+ #stats{user=Owner,
-+ at=Date,
-+ count=1},
-+ write);
-+ [Stats] ->
-+ mnesia:delete_object(stats_table(VHost),
-+ #stats{user=Owner,
-+ at=Date,
-+ count=Stats#stats.count},
-+ write),
-+ New = Stats#stats{count = Stats#stats.count+1},
-+ if
-+ New#stats.count > 0 -> mnesia:write(stats_table(VHost),
-+ New,
-+ write);
-+ true -> ok
-+ end
-+ end
-+ end,
-+ case mnesia:transaction(Fun) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]),
-+ error;
-+ {atomic, _} ->
-+ ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]),
-+ ok
-+ end.
++ % Users to filter (sublist of UniqUsers)
++ CheckedUsers = case lists:keysearch("filter", 1, Query) of
++ {value, _} ->
++ lists:filter(fun(UFUser) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(UFUser))),
++ lists:member({"selected", ID}, Query)
++ end, UniqUsers);
++ false -> []
++ end,
+
-+get_dates_int(VHost) ->
-+ Tables = mnesia:system_info(tables),
-+ lists:foldl(fun(ATable, Dates) ->
-+ Table = atom_to_list(ATable),
-+ case regexp:match(Table, VHost++"$") of
-+ {match, _, _} ->
-+ case regexp:match(Table,"_[0-9]+-[0-9]+-[0-9]+_") of
-+ {match, S, E} ->
-+ lists:append(Dates, [lists:sublist(Table,S+1,E-2)]);
-+ nomatch ->
-+ Dates
-+ end;
-+ nomatch ->
-+ Dates
-+ end
-+ end, [], Tables).
++ % UniqUsers in html (noone selected -> everyone selected)
++ Users = lists:map(fun(UHUser) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(UHUser))),
++ Input = case lists:member(UHUser, CheckedUsers) of
++ true -> [?INPUTC("checkbox", "selected", ID)];
++ false when CheckedUsers == [] -> [?INPUTC("checkbox", "selected", ID)];
++ false -> [?INPUT("checkbox", "selected", ID)]
++ end,
++ Nick =
++ case search_user_nick(UHUser, UserRoster) of
++ nothing -> "";
++ N -> " ("++ N ++")"
++ end,
++ ?XE("tr",
++ [?XE("td", Input),
++ ?XC("td", UHUser++Nick)])
++ end, lists:sort(UniqUsers)),
++ % Messages to show (based on Users)
++ User_messages_filtered = case CheckedUsers of
++ [] -> User_messages;
++ _ -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) ->
++ lists:member(PName++"@"++PServer, CheckedUsers)
++ end, User_messages)
++ end,
+
-+rebuild_stats_at_int(VHost, Date) ->
-+ Table = table_name(VHost, Date),
-+ STable = stats_table(VHost),
-+ CFun = fun(Msg, Stats) ->
-+ Owner = Msg#msg.owner_name,
-+ case lists:keysearch(Owner, 1, Stats) of
-+ {value, {_, Count}} ->
-+ lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1});
-+ false ->
-+ lists:append(Stats, [{Owner, 1}])
-+ end
-+ end,
-+ DFun = fun(#stats{at=SDate} = Stat, _Acc)
-+ when SDate == Date ->
-+ mnesia:delete_object(stats_table(VHost), Stat, write);
-+ (_Stat, _Acc) -> ok
-+ end,
-+ % TODO: Maybe unregister hooks ?
-+ case mnesia:transaction(fun() ->
-+ mnesia:write_lock_table(Table),
-+ mnesia:write_lock_table(STable),
-+ % Calc stats for VHost at Date
-+ case mnesia:foldl(CFun, [], Table) of
-+ [] -> empty;
-+ AStats ->
-+ % Delete all stats for VHost at Date
-+ mnesia:foldl(DFun, [], STable),
-+ % Write new calc'ed stats
-+ lists:foreach(fun({Owner, Count}) ->
-+ WStat = #stats{user=Owner, at=Date, count=Count},
-+ mnesia:write(stats_table(VHost), WStat, write)
-+ end, AStats),
-+ ok
-+ end
-+ end) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]),
-+ error;
-+ {atomic, ok} ->
-+ ok;
-+ {atomic, empty} ->
-+ {atomic,ok} = mnesia:delete_table(Table),
-+ ?MYDEBUG("Dropped table at ~p", [Date]),
-+ ok
++ Msgs_Fun = fun(#msg{timestamp=Timestamp,
++ subject=Subject,
++ direction=Direction,
++ peer_name=PName, peer_server=PServer, peer_resource=PRes,
++ type=Type,
++ body=Body}) ->
++ TextRaw = case Subject of
++ "" -> Body;
++ _ -> [?T("Subject"),": ",Subject,"<br>", Body]
++ end,
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(Timestamp))),
++ % replace \n with <br>
++ Text = lists:map(fun(10) -> "<br>";
++ (A) -> A
++ end, TextRaw),
++ Resource = case PRes of
++ [] -> [];
++ undefined -> [];
++ R -> "/" ++ R
++ end,
++ UserNick =
++ case search_user_nick(PName++"@"++PServer, UserRoster) of
++ nothing when PServer == Server ->
++ PName;
++ nothing when Type == "groupchat", Direction == from ->
++ PName++"@"++PServer++Resource;
++ nothing ->
++ PName++"@"++PServer;
++ N -> N
++ end,
++ ?XE("tr",
++ [?XE("td", [?INPUT("checkbox", "selected", ID)]),
++ ?XC("td", convert_timestamp(Timestamp)),
++ ?XC("td", atom_to_list(Direction)++": "++UserNick),
++ ?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"),
++ ?XCT("td", "Body")
++ ])]),
++ ?XE("tbody",
++ Msgs
++ )]),
++ ?INPUTT("submit", "delete", "Delete Selected"),
++ ?BR
++ ]
++ )]
+ end.
+diff --git src/mod_logdb.hrl src/mod_logdb.hrl
+new file mode 100644
+index 0000000..50db897
+--- /dev/null
++++ src/mod_logdb.hrl
+@@ -0,0 +1,35 @@
++%%%----------------------------------------------------------------------
++%%% File : mod_logdb.hrl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose :
++%%% Version : trunk
++%%% Id : $Id: mod_logdb.hrl 1273 2009-02-05 18:12:57Z malik $
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
+
-+delete_nonexistent_stats(VHost) ->
-+ Dates = get_dates_int(VHost),
-+ mnesia:transaction(fun() ->
-+ mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) ->
-+ case lists:member(Date, Dates) of
-+ false -> mnesia:delete_object(Stat);
-+ true -> ok
-+ end
-+ end, ok, stats_table(VHost))
-+ end).
++-define(logdb_debug, true).
+
-+delete_stats_by_vhost_at_int(VHost, Date) ->
-+ StatsDelete = fun(#stats{at=SDate} = Stat, _Acc)
-+ when SDate == Date ->
-+ mnesia:delete_object(stats_table(VHost), Stat, write),
-+ ok;
-+ (_Msg, _Acc) -> ok
-+ end,
-+ case mnesia:transaction(fun() ->
-+ mnesia:write_lock_table(stats_table(VHost)),
-+ mnesia:foldl(StatsDelete, ok, stats_table(VHost))
-+ end) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]),
-+ rebuild_stats_at_int(VHost, Date);
-+ _ ->
-+ ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]),
-+ ok
-+ end.
++-ifdef(logdb_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.
+
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+%
-+% tables internals
-+%
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+create_stats_table(VHost) ->
-+ SName = stats_table(VHost),
-+ case mnesia:create_table(SName,
-+ [{disc_only_copies, [node()]},
-+ {type, bag},
-+ {attributes, record_info(fields, stats)},
-+ {record_name, stats}
-+ ]) of
-+ {atomic, ok} ->
-+ ?MYDEBUG("Created stats table for ~p", [VHost]),
-+ lists:foreach(fun(Date) ->
-+ rebuild_stats_at_int(VHost, Date)
-+ end, get_dates_int(VHost)),
-+ ok;
-+ {aborted, {already_exists, _}} ->
-+ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
-+ ok;
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
-+ error
-+ end.
++-record(msg, {timestamp,
++ owner_name,
++ peer_name, peer_server, peer_resource,
++ direction,
++ type, subject,
++ body}).
+
-+create_settings_table(VHost) ->
-+ SName = settings_table(VHost),
-+ case mnesia:create_table(SName,
-+ [{disc_copies, [node()]},
-+ {type, set},
-+ {attributes, record_info(fields, user_settings)},
-+ {record_name, user_settings}
-+ ]) of
-+ {atomic, ok} ->
-+ ?MYDEBUG("Created settings table for ~p", [VHost]),
-+ ok;
-+ {aborted, {already_exists, _}} ->
-+ ?MYDEBUG("Settings table for ~p already exists", [VHost]),
-+ ok;
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to create settings table: ~p", [Reason]),
-+ error
-+ end.
++-record(user_settings, {owner_name,
++ dolog_default,
++ dolog_list=[],
++ donotlog_list=[]}).
+
-+create_msg_table(VHost, Date) ->
-+ mnesia:create_table(
-+ table_name(VHost, Date),
-+ [{disc_only_copies, [node()]},
-+ {type, bag},
-+ {attributes, record_info(fields, msg)},
-+ {record_name, msg}]).
---- src/mod_logdb_mysql.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb_mysql.erl Sun Nov 18 20:53:55 2007
-@@ -0,0 +1,936 @@
++-define(INPUTC(Type, Name, Value),
++ ?XA("input", [{"type", Type},
++ {"name", Name},
++ {"value", Value},
++ {"checked", "true"}])).
+diff --git src/mod_logdb_mnesia.erl src/mod_logdb_mnesia.erl
+new file mode 100644
+index 0000000..783aaeb
+--- /dev/null
++++ src/mod_logdb_mnesia.erl
+@@ -0,0 +1,546 @@
+%%%----------------------------------------------------------------------
-+%%% File : mod_logdb_mysql.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose : MySQL backend for mod_logdb
++%%% File : mod_logdb_mnesia.erl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose : mnesia backend for mod_logdb
+%%% Version : trunk
-+%%% Id : $Id$
++%%% Id : $Id: mod_logdb_mnesia.erl 1273 2009-02-05 18:12:57Z malik $
+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
+%%%----------------------------------------------------------------------
+
-+-module(mod_logdb_mysql).
++-module(mod_logdb_mnesia).
+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
+
+-include("mod_logdb.hrl").
+-include("ejabberd.hrl").
+ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
+ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
+ get_dates/1,
-+ get_users_settings/1, get_user_settings/2, set_user_settings/3]).
++ get_users_settings/1, get_user_settings/2, set_user_settings/3,
++ drop_user/2]).
+
-+% gen_server call timeout
-+-define(CALL_TIMEOUT, 60000).
-+-define(TIMEOUT, 60000).
-+-define(INDEX_SIZE, integer_to_list(170)).
-+-define(PROCNAME, mod_logdb_mysql).
++-define(PROCNAME, mod_logdb_mnesia).
++-define(CALL_TIMEOUT, 10000).
+
-+-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
-+ list_to_string/1, string_to_list/1,
-+ convert_timestamp_brief/1]).
++-record(state, {vhost}).
+
-+-record(state, {dbref, vhost}).
++-record(stats, {user, at, count}).
+
-+% replace "." with "_"
-+escape_vhost(VHost) -> lists:map(fun(46) -> 95;
-+ (A) -> A
-+ end, VHost).
+prefix() ->
-+ "`logdb_".
++ "logdb_".
+
+suffix(VHost) ->
-+ "_" ++ escape_vhost(VHost) ++ "`".
-+
-+messages_table(VHost, Date) ->
-+ prefix() ++ "messages_" ++ Date ++ suffix(VHost).
++ "_" ++ VHost.
+
+stats_table(VHost) ->
-+ prefix() ++ "stats" ++ suffix(VHost).
-+
-+settings_table(VHost) ->
-+ prefix() ++ "settings" ++ suffix(VHost).
++ list_to_atom(prefix() ++ "stats" ++ suffix(VHost)).
+
-+users_table(VHost) ->
-+ prefix() ++ "users" ++ suffix(VHost).
-+servers_table(VHost) ->
-+ prefix() ++ "servers" ++ suffix(VHost).
-+resources_table(VHost) ->
-+ prefix() ++ "resources" ++ suffix(VHost).
++table_name(VHost, Date) ->
++ list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)).
+
-+ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ VHost).
-+ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ VHost).
-+ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ VHost).
++settings_table(VHost) ->
++ list_to_atom(prefix() ++ "settings" ++ suffix(VHost)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% gen_server callbacks
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+init([VHost, Opts]) ->
-+ crypto:start(),
-+
-+ Server = gen_mod:get_opt(server, Opts, "localhost"),
-+ Port = gen_mod:get_opt(port, Opts, 3128),
-+ DB = gen_mod:get_opt(db, Opts, "logdb"),
-+ User = gen_mod:get_opt(user, Opts, "root"),
-+ Password = gen_mod:get_opt(password, Opts, ""),
-+
-+ LogFun = fun(debug, Format, Argument) ->
-+ ?MYDEBUG(Format, Argument);
-+ (error, Format, Argument) ->
-+ ?ERROR_MSG(Format, Argument);
-+ (Level, Format, Argument) ->
-+ ?MYDEBUG("MySQL (~p)~n", [Level]),
-+ ?MYDEBUG(Format, Argument)
-+ end,
-+ case mysql_conn:start(Server, Port, User, Password, DB, LogFun) of
-+ {ok, DBRef} ->
-+ ok = create_stats_table(DBRef, VHost),
-+ ok = create_settings_table(DBRef, VHost),
-+ ok = create_users_table(DBRef, VHost),
-+ % clear ets cache every ...
-+ timer:send_interval(timer:hours(12), clear_ets_tables),
-+ ok = create_servers_table(DBRef, VHost),
-+ ok = create_resources_table(DBRef, VHost),
-+ erlang:monitor(process, DBRef),
-+ {ok, #state{dbref=DBRef, vhost=VHost}};
-+ {error, Reason} ->
-+ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
-+ {stop, db_connection_failed}
++init([VHost, _Opts]) ->
++ case mnesia:system_info(is_running) of
++ yes ->
++ ok = create_stats_table(VHost),
++ ok = create_settings_table(VHost),
++ {ok, #state{vhost=VHost}};
++ no ->
++ ?ERROR_MSG("Mnesia not running", []),
++ {stop, db_connection_failed};
++ Status ->
++ ?ERROR_MSG("Mnesia status: ~p", [Status]),
++ {stop, db_connection_failed}
+ end.
+
-+handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ {reply, log_message_int(DBRef, VHost, Msg), State};
-+handle_call({rebuild_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ ok = delete_nonexistent_stats(DBRef, VHost),
-+ Reply =
++handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) ->
++ {reply, log_message_int(VHost, Msg), State};
++handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) ->
++ {atomic, ok} = delete_nonexistent_stats(VHost),
++ Reply =
+ lists:foreach(fun(Date) ->
-+ catch rebuild_stats_at_int(DBRef, VHost, Date)
-+ end, get_dates_int(DBRef, VHost)),
-+ {reply, Reply, State};
-+handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Reply = rebuild_stats_at_int(DBRef, VHost, Date),
++ rebuild_stats_at_int(VHost, Date)
++ end, get_dates_int(VHost)),
+ {reply, Reply, State};
-+handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
-+ {reply, error, State};
-+handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
-+ ["\"",Timestamp,"\"",","]
-+ end, Msgs),
-+
-+ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
-+
-+ Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
-+ "WHERE timestamp IN (", Temp1],
-+
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, Aff} ->
-+ ?MYDEBUG("Aff=~p", [Aff]),
-+ rebuild_stats_at_int(DBRef, VHost, Date);
-+ {error, _} ->
-+ error
-+ end,
++handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
++ Reply = rebuild_stats_at_int(VHost, Date),
+ {reply, Reply, State};
-+handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Owner_id = get_user_id(DBRef, VHost, User),
-+ DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
-+ "WHERE owner_id=\"",Owner_id,"\";"],
++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
++ Table = table_name(VHost, Date),
++ Fun = fun() ->
++ lists:foreach(
++ fun(Msg) ->
++ mnesia:write_lock_table(stats_table(VHost)),
++ mnesia:write_lock_table(Table),
++ mnesia:delete_object(Table, Msg, write)
++ end, Msgs)
++ end,
++ DRez = case mnesia:transaction(Fun) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
++ error;
++ _ ->
++ ok
++ end,
+ Reply =
-+ case sql_query_internal(DBRef, DQuery) of
-+ {updated, _} ->
-+ rebuild_stats_at_int(DBRef, VHost, Date);
-+ {error, _} ->
-+ error
++ case rebuild_stats_at_int(VHost, Date) of
++ error ->
++ error;
++ ok ->
++ DRez
+ end,
+ {reply, Reply, State};
-+handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) ->
++ {reply, delete_all_messages_by_user_at_int(User, VHost, Date), State};
++handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) ->
+ Reply =
-+ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of
-+ {updated, _} ->
-+ Query = ["DELETE FROM ",stats_table(VHost)," "
-+ "WHERE at=\"",Date,"\";"],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ ok;
-+ {error, _} ->
-+ error
-+ end;
-+ {error, _} ->
++ case mnesia:delete_table(table_name(VHost, Date)) of
++ {atomic, ok} ->
++ delete_stats_by_vhost_at_int(VHost, Date);
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]),
+ error
+ end,
+ {reply, Reply, State};
-+handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ SName = stats_table(VHost),
-+ Query = ["SELECT at, sum(count) ",
-+ "FROM ",SName," ",
-+ "GROUP BY at ",
-+ "ORDER BY DATE(at) DESC;"
-+ ],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
-+ {error, Reason} ->
-+ % TODO: Duplicate error message ?
-+ {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ SName = stats_table(VHost),
-+ Query = ["SELECT username, count ",
-+ "FROM ",SName," ",
-+ "JOIN ",users_table(VHost)," ON owner_id=user_id "
-+ "WHERE at=\"",Date,"\";"
-+ ],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ {ok, lists:reverse(
-+ lists:keysort(2,
-+ [ {User, list_to_integer(Count)} || [User, Count] <- Result]))};
-+ {error, Reason} ->
-+ % TODO:
-+ {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ SName = stats_table(VHost),
-+ Query = ["SELECT at, count ",
-+ "FROM ",SName," ",
-+ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
-+ "ORDER BY DATE(at) DESC;"
-+ ],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]};
-+ {error, Result} ->
-+ {error, Result}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ TName = messages_table(VHost, Date),
-+ UName = users_table(VHost),
-+ SName = servers_table(VHost),
-+ RName = resources_table(VHost),
-+ Query = ["SELECT users.username,",
-+ "servers.server,",
-+ "resources.resource,",
-+ "messages.direction,"
-+ "messages.type,"
-+ "messages.subject,"
-+ "messages.body,"
-+ "messages.timestamp "
-+ "FROM ",TName," AS messages "
-+ "JOIN ",UName," AS users ON peer_name_id=user_id ",
-+ "JOIN ",SName," AS servers ON peer_server_id=server_id ",
-+ "JOIN ",RName," AS resources ON peer_resource_id=resource_id ",
-+ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
-+ "ORDER BY timestamp ASC;"],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ Fun = fun([Peer_name, Peer_server, Peer_resource,
-+ Direction,
-+ Type,
-+ Subject, Body,
-+ Timestamp]) ->
-+ #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
-+ direction=list_to_atom(Direction),
-+ type=Type,
-+ subject=Subject, body=Body,
-+ timestamp=Timestamp}
-+ end,
-+ {ok, lists:map(Fun, Result)};
-+ {error, Reason} ->
-+ {error, Reason}
++handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) ->
++ Fun = fun(#stats{at=Date, count=Count}, Stats) ->
++ case lists:keysearch(Date, 1, Stats) of
++ false ->
++ lists:append(Stats, [{Date, Count}]);
++ {value, {_, TempCount}} ->
++ lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count})
++ end
++ end,
++ Reply =
++ case mnesia:transaction(fun() ->
++ mnesia:foldl(Fun, [], stats_table(VHost))
++ end) of
++ {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)};
++ {aborted, Reason} -> {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ SName = stats_table(VHost),
-+ Query = ["SELECT at ",
-+ "FROM ",SName," ",
-+ "GROUP BY at ",
-+ "ORDER BY DATE(at) DESC;"
-+ ],
++handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
++ Fun = fun() ->
++ Pat = #stats{user='$1', at=Date, count='$2'},
++ mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
++ end,
+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ [ Date || [Date] <- Result ];
-+ {error, Reason} ->
-+ {error, Reason}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
-+ "FROM ",settings_table(VHost)," ",
-+ "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
-+ #user_settings{owner_name=Owner,
-+ dolog_default=list_to_bool(DoLogDef),
-+ dolog_list=string_to_list(DoLogL),
-+ donotlog_list=string_to_list(DoNotLogL)
-+ }
-+ end, Result)};
-+ {error, _} ->
-+ error
++ case mnesia:transaction(Fun) of
++ {atomic, Result} ->
++ {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))};
++ {aborted, Reason} ->
++ {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
-+ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"],
++handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) ->
++ {reply, get_user_stats_int(User, VHost), State};
++handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) ->
+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, []} ->
-+ {ok, []};
-+ {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
-+ {ok, #user_settings{owner_name=Owner,
-+ dolog_default=list_to_bool(DoLogDef),
-+ dolog_list=string_to_list(DoLogL),
-+ donotlog_list=string_to_list(DoNotLogL)}};
-+ {error, _} ->
-+ error
++ case mnesia:transaction(fun() ->
++ Pat = #msg{owner_name=User, _='_'},
++ mnesia:select(table_name(VHost, Date),
++ [{Pat, [], ['$_']}])
++ end) of
++ {atomic, Result} -> {ok, Result};
++ {aborted, Reason} ->
++ {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
-+ dolog_list=DoLogL,
-+ donotlog_list=DoNotLogL}},
-+ _From, #state{dbref=DBRef, vhost=VHost} = State) ->
-+ User_id = get_user_id(DBRef, VHost, User),
-+
-+ Query = ["UPDATE ",settings_table(VHost)," ",
-+ "SET dolog_default=",bool_to_list(DoLogDef),", ",
-+ "dolog_list='",list_to_string(DoLogL),"', ",
-+ "donotlog_list='",list_to_string(DoNotLogL),"' ",
-+ "WHERE owner_id=\"",User_id,"\";"],
-+
++handle_call({get_dates}, _From, #state{vhost=VHost}=State) ->
++ {reply, get_dates_int(VHost), State};
++handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) ->
++ Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}),
++ {reply, {ok, Reply}, State};
++handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) ->
++ Reply =
++ case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of
++ [] -> [];
++ [Setting] ->
++ Setting
++ end,
++ {reply, Reply, State};
++handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) ->
++ ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]),
++ Reply = mnesia:dirty_write(settings_table(VHost), Set),
++ ?MYDEBUG("~p", [Reply]),
++ {reply, Reply, State};
++handle_call({drop_user, User}, _From, #state{vhost=VHost}=State) ->
++ {ok, Dates} = get_user_stats_int(User, VHost),
++ MDResult = lists:map(fun({Date, _}) ->
++ delete_all_messages_by_user_at_int(User, VHost, Date)
++ end, Dates),
++ SDResult = delete_user_settings_int(User, VHost),
+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, 0} ->
-+ IQuery = ["INSERT INTO ",settings_table(VHost)," ",
-+ "(owner_id, dolog_default, dolog_list, donotlog_list) ",
-+ "VALUES ",
-+ "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
-+ case sql_query_internal_silent(DBRef, IQuery) of
-+ {updated, _} ->
-+ ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
-+ ok;
-+ {error, Reason} ->
-+ case regexp:match(Reason, "#23000") of
-+ % Already exists
-+ {match, _, _} ->
-+ ok;
-+ _ ->
-+ ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
-+ error
-+ end
-+ end;
-+ {updated, 1} ->
-+ ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
-+ ok;
-+ {error, _} ->
-+ error
++ case lists:all(fun(Result) when Result == ok ->
++ true;
++ (Result) when Result == error ->
++ false
++ end, lists:append(MDResult, [SDResult])) of
++ true ->
++ ok;
++ false ->
++ error
+ end,
+ {reply, Reply, State};
-+handle_call({stop}, _From, #state{vhost=VHost}=State) ->
-+ ets:delete(ets_users_table(VHost)),
-+ ets:delete(ets_servers_table(VHost)),
-+ ?MYDEBUG("Stoping mysql backend for ~p", [VHost]),
++handle_call({stop}, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(Msg, _From, State) ->
+ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
+ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
+ {noreply, State}.
+
-+handle_info(clear_ets_tables, State) ->
-+ ets:delete_all_objects(ets_users_table(State#state.vhost)),
-+ ets:delete_all_objects(ets_resources_table(State#state.vhost)),
-+ {noreply, State};
-+handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
-+ {stop, connection_dropped, State};
+handle_info(Info, State) ->
+ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
+ {noreply, State}.
+get_dates(VHost) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
-+get_users_settings(VHost) ->
-+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
+get_user_settings(User, VHost) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
++get_users_settings(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
+set_user_settings(User, VHost, Set) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
++drop_user(User, VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {drop_user, User}, ?CALL_TIMEOUT).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% internals
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+log_message_int(DBRef, VHost, Msg) ->
-+ Date = convert_timestamp_brief(Msg#msg.timestamp),
-+
-+ Table = messages_table(VHost, Date),
-+ Owner_id = get_user_id(DBRef, VHost, Msg#msg.owner_name),
-+ Peer_name_id = get_user_id(DBRef, VHost, Msg#msg.peer_name),
-+ Peer_server_id = get_server_id(DBRef, VHost, Msg#msg.peer_server),
-+ Peer_resource_id = get_resource_id(DBRef, VHost, Msg#msg.peer_resource),
-+
-+ Query = ["INSERT INTO ",Table," ",
-+ "(owner_id,",
-+ "peer_name_id,",
-+ "peer_server_id,",
-+ "peer_resource_id,",
-+ "direction,",
-+ "type,",
-+ "subject,",
-+ "body,",
-+ "timestamp) ",
-+ "VALUES ",
-+ "('", Owner_id, "',",
-+ "'", Peer_name_id, "',",
-+ "'", Peer_server_id, "',",
-+ "'", Peer_resource_id, "',",
-+ "'", atom_to_list(Msg#msg.direction), "',",
-+ "'", Msg#msg.type, "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.body), "',",
-+ "'", Msg#msg.timestamp, "');"],
++log_message_int(VHost, #msg{timestamp=Timestamp}=Msg) ->
++ Date = mod_logdb:convert_timestamp_brief(Timestamp),
+
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
++ ATable = table_name(VHost, Date),
++ Fun = fun() ->
++ mnesia:write_lock_table(ATable),
++ mnesia:write(ATable, Msg, write)
++ end,
++ % log message, increment stats for both users
++ case mnesia:transaction(Fun) of
++ % if table does not exists - create it and try to log message again
++ {aborted,{no_exists, _Table}} ->
++ case create_msg_table(VHost, Date) of
++ {aborted, CReason} ->
++ ?ERROR_MSG("Failed to log message: ~p", [CReason]),
++ error;
++ {atomic, ok} ->
++ ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
++ log_message_int(VHost, Msg)
++ end;
++ {aborted, TReason} ->
++ ?ERROR_MSG("Failed to log message: ~p", [TReason]),
++ error;
++ {atomic, _} ->
++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
+ Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
-+ increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Date);
-+ {error, Reason} ->
-+ case regexp:match(Reason, "#42S02") of
-+ % Table doesn't exist
-+ {match, _, _} ->
-+ case create_msg_table(DBRef, VHost, Date) of
-+ error ->
-+ error;
-+ ok ->
-+ log_message_int(DBRef, VHost, Msg)
-+ end;
-+ _ ->
-+ ?ERROR_MSG("Failed to log message: ~p", [Reason]),
-+ error
-+ end
-+ end.
-+
-+increment_user_stats(DBRef, User_name, User_id, VHost, Date) ->
-+ SName = stats_table(VHost),
-+ UQuery = ["UPDATE ",SName," ",
-+ "SET count=count+1 ",
-+ "WHERE owner_id=\"",User_id,"\" AND at=\"",Date,"\";"],
-+
-+ case sql_query_internal(DBRef, UQuery) of
-+ {updated, 0} ->
-+ IQuery = ["INSERT INTO ",SName," ",
-+ "(owner_id, at, count) ",
-+ "VALUES ",
-+ "('",User_id,"', '",Date,"', '1');"],
-+ case sql_query_internal(DBRef, IQuery) of
-+ {updated, _} ->
-+ ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]),
-+ ok;
-+ {error, _} ->
-+ error
-+ end;
-+ {updated, _} ->
-+ ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]),
-+ ok;
-+ {error, _} ->
-+ error
++ increment_user_stats(Msg#msg.owner_name, VHost, Date)
+ end.
+
-+get_dates_int(DBRef, VHost) ->
-+ case sql_query_internal(DBRef, ["SHOW TABLES"]) of
-+ {data, Tables} ->
-+ lists:foldl(fun([Table], Dates) ->
-+ % TODO: check prefix()
-+ case regexp:match(Table, escape_vhost(VHost)) of
-+ {match, _, _} ->
-+ case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of
-+ {match, S, E} ->
-+ lists:append(Dates, [lists:sublist(Table,S,E)]);
-+ nomatch ->
-+ Dates
-+ end;
-+ nomatch ->
-+ Dates
-+ end
-+ end, [], Tables);
-+ {error, _} ->
-+ []
-+ end.
-+
-+rebuild_stats_at_int(DBRef, VHost, Date) ->
-+ Table = messages_table(VHost, Date),
-+ STable = stats_table(VHost),
-+
-+ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",
-+ STable," WRITE;"]),
-+ Fun =
-+ fun() ->
-+ DQuery = [ "DELETE FROM ",STable," ",
-+ "WHERE at='",Date,"';"],
-+
-+ {updated, _} = sql_query_internal(DBRef, DQuery),
-+
-+ SQuery = ["INSERT INTO ",STable," ",
-+ "(owner_id,at,count) ",
-+ "SELECT owner_id,\"",Date,"\"",",count(*) ",
-+ "FROM ",Table," GROUP BY owner_id;"],
-+
-+ case sql_query_internal(DBRef, SQuery) of
-+ {updated, 0} ->
-+ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
-+ ok;
-+ {updated, _} -> ok;
-+ {error, _} -> error
-+ end
-+ end,
-+
-+
-+ Res = case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, _} ->
-+ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
-+ ok;
-+ {aborted, _} ->
-+ error
++increment_user_stats(Owner, VHost, Date) ->
++ Fun = fun() ->
++ Pat = #stats{user=Owner, at=Date, count='$1'},
++ mnesia:write_lock_table(stats_table(VHost)),
++ case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of
++ [] ->
++ mnesia:write(stats_table(VHost),
++ #stats{user=Owner,
++ at=Date,
++ count=1},
++ write);
++ [Stats] ->
++ mnesia:delete_object(stats_table(VHost),
++ #stats{user=Owner,
++ at=Date,
++ count=Stats#stats.count},
++ write),
++ New = Stats#stats{count = Stats#stats.count+1},
++ if
++ New#stats.count > 0 -> mnesia:write(stats_table(VHost),
++ New,
++ write);
++ true -> ok
++ end
++ end
+ end,
-+ {updated, _} = sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
-+ Res.
++ case mnesia:transaction(Fun) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]),
++ error;
++ {atomic, _} ->
++ ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]),
++ ok
++ end.
+
++get_dates_int(VHost) ->
++ Tables = mnesia:system_info(tables),
++ lists:foldl(fun(ATable, Dates) ->
++ Table = atom_to_list(ATable),
++ case ejabberd_regexp:run(Table, VHost++"$") of
++ match ->
++ case re:run(Table, "_[0-9]+-[0-9]+-[0-9]+_") of
++ {match, [{S, E}]} ->
++ lists:append(Dates, [lists:sublist(Table,S+1,E-2)]);
++ nomatch ->
++ Dates
++ end;
++ nomatch ->
++ Dates
++ end
++ end, [], Tables).
+
-+delete_nonexistent_stats(DBRef, VHost) ->
-+ Dates = get_dates_int(DBRef, VHost),
++rebuild_stats_at_int(VHost, Date) ->
++ Table = table_name(VHost, Date),
+ STable = stats_table(VHost),
++ CFun = fun(Msg, Stats) ->
++ Owner = Msg#msg.owner_name,
++ case lists:keysearch(Owner, 1, Stats) of
++ {value, {_, Count}} ->
++ lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1});
++ false ->
++ lists:append(Stats, [{Owner, 1}])
++ end
++ end,
++ DFun = fun(#stats{at=SDate} = Stat, _Acc)
++ when SDate == Date ->
++ mnesia:delete_object(stats_table(VHost), Stat, write);
++ (_Stat, _Acc) -> ok
++ end,
++ % TODO: Maybe unregister hooks ?
++ case mnesia:transaction(fun() ->
++ mnesia:write_lock_table(Table),
++ mnesia:write_lock_table(STable),
++ % Calc stats for VHost at Date
++ case mnesia:foldl(CFun, [], Table) of
++ [] -> empty;
++ AStats ->
++ % Delete all stats for VHost at Date
++ mnesia:foldl(DFun, [], STable),
++ % Write new calc'ed stats
++ lists:foreach(fun({Owner, Count}) ->
++ WStat = #stats{user=Owner, at=Date, count=Count},
++ mnesia:write(stats_table(VHost), WStat, write)
++ end, AStats),
++ ok
++ end
++ end) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]),
++ error;
++ {atomic, ok} ->
++ ok;
++ {atomic, empty} ->
++ {atomic,ok} = mnesia:delete_table(Table),
++ ?MYDEBUG("Dropped table at ~p", [Date]),
++ ok
++ end.
+
-+ Temp = lists:flatmap(fun(Date) ->
-+ ["\"",Date,"\"",","]
-+ end, Dates),
++delete_nonexistent_stats(VHost) ->
++ Dates = get_dates_int(VHost),
++ mnesia:transaction(fun() ->
++ mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) ->
++ case lists:member(Date, Dates) of
++ false -> mnesia:delete_object(Stat);
++ true -> ok
++ end
++ end, ok, stats_table(VHost))
++ end).
+
-+ Temp1 = case Temp of
-+ [] ->
-+ ["\"\""];
-+ _ ->
-+ % replace last "," with ");"
-+ lists:append([lists:sublist(Temp, length(Temp)-1), ");"])
-+ end,
++delete_stats_by_vhost_at_int(VHost, Date) ->
++ StatsDelete = fun(#stats{at=SDate} = Stat, _Acc)
++ when SDate == Date ->
++ mnesia:delete_object(stats_table(VHost), Stat, write),
++ ok;
++ (_Msg, _Acc) -> ok
++ end,
++ case mnesia:transaction(fun() ->
++ mnesia:write_lock_table(stats_table(VHost)),
++ mnesia:foldl(StatsDelete, ok, stats_table(VHost))
++ end) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]),
++ rebuild_stats_at_int(VHost, Date);
++ _ ->
++ ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]),
++ ok
++ end.
+
-+ Query = ["DELETE FROM ",STable," ",
-+ "WHERE at NOT IN (", Temp1],
++get_user_stats_int(User, VHost) ->
++ case mnesia:transaction(fun() ->
++ Pat = #stats{user=User, at='$1', count='$2'},
++ mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
++ end) of
++ {atomic, Result} ->
++ {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])};
++ {aborted, Reason} ->
++ {error, Reason}
++ end.
+
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
++delete_all_messages_by_user_at_int(User, VHost, Date) ->
++ Table = table_name(VHost, Date),
++ MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc)
++ when Owner == User ->
++ mnesia:delete_object(Table, Msg, write),
++ ok;
++ (_Msg, _Acc) -> ok
++ end,
++ DRez = case mnesia:transaction(fun() ->
++ mnesia:foldl(MsgDelete, ok, Table)
++ end) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]),
++ error;
++ _ ->
++ ok
++ end,
++ case rebuild_stats_at_int(VHost, Date) of
++ error ->
++ error;
++ ok ->
++ DRez
++ end.
++
++delete_user_settings_int(User, VHost) ->
++ STable = settings_table(VHost),
++ case mnesia:dirty_match_object(STable, #user_settings{owner_name=User, _='_'}) of
++ [] ->
+ ok;
-+ {error, _} ->
-+ error
++ [UserSettings] ->
++ mnesia:dirty_delete_object(STable, UserSettings)
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% tables internals
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+create_stats_table(DBRef, VHost) ->
++create_stats_table(VHost) ->
+ SName = stats_table(VHost),
-+ Query = ["CREATE TABLE ",SName," (",
-+ "owner_id MEDIUMINT UNSIGNED, ",
-+ "at varchar(20), ",
-+ "count int(11), ",
-+ "INDEX(owner_id), ",
-+ "INDEX(at)"
-+ ") ENGINE=InnoDB CHARACTER SET utf8;"
-+ ],
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created stats table for ~p", [VHost]),
-+ lists:foreach(fun(Date) ->
-+ rebuild_stats_at_int(DBRef, VHost, Date)
-+ end, get_dates_int(DBRef, VHost)),
-+ ok;
-+ {error, Reason} ->
-+ case regexp:match(Reason, "#42S01") of
-+ {match, _, _} ->
-+ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
-+ ok;
-+ _ ->
-+ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
-+ error
-+ end
++ case mnesia:create_table(SName,
++ [{disc_only_copies, [node()]},
++ {type, bag},
++ {attributes, record_info(fields, stats)},
++ {record_name, stats}
++ ]) of
++ {atomic, ok} ->
++ ?MYDEBUG("Created stats table for ~p", [VHost]),
++ lists:foreach(fun(Date) ->
++ rebuild_stats_at_int(VHost, Date)
++ end, get_dates_int(VHost)),
++ ok;
++ {aborted, {already_exists, _}} ->
++ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
++ ok;
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
++ error
+ end.
+
-+create_settings_table(DBRef, VHost) ->
++create_settings_table(VHost) ->
+ SName = settings_table(VHost),
-+ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
-+ "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
-+ "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
-+ "dolog_list TEXT, ",
-+ "donotlog_list TEXT ",
-+ ") ENGINE=InnoDB CHARACTER SET utf8;"
-+ ],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created settings table for ~p", [VHost]),
-+ ok;
-+ {error, _} ->
-+ error
++ case mnesia:create_table(SName,
++ [{disc_copies, [node()]},
++ {type, set},
++ {attributes, record_info(fields, user_settings)},
++ {record_name, user_settings}
++ ]) of
++ {atomic, ok} ->
++ ?MYDEBUG("Created settings table for ~p", [VHost]),
++ ok;
++ {aborted, {already_exists, _}} ->
++ ?MYDEBUG("Settings table for ~p already exists", [VHost]),
++ ok;
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to create settings table: ~p", [Reason]),
++ error
+ end.
+
-+create_users_table(DBRef, VHost) ->
-+ SName = users_table(VHost),
-+ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
-+ "username TEXT NOT NULL, ",
-+ "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
-+ "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
-+ ") ENGINE=InnoDB CHARACTER SET utf8;"
-+ ],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created users table for ~p", [VHost]),
-+ ets:new(ets_users_table(VHost), [named_table, set, public]),
-+ %update_users_from_db(DBRef, VHost),
-+ ok;
-+ {error, _} ->
-+ error
-+ end.
++create_msg_table(VHost, Date) ->
++ mnesia:create_table(
++ table_name(VHost, Date),
++ [{disc_only_copies, [node()]},
++ {type, bag},
++ {attributes, record_info(fields, msg)},
++ {record_name, msg}]).
+diff --git src/mod_logdb_mnesia_old.erl src/mod_logdb_mnesia_old.erl
+new file mode 100644
+index 0000000..aef9956
+--- /dev/null
++++ src/mod_logdb_mnesia_old.erl
+@@ -0,0 +1,258 @@
++%%%----------------------------------------------------------------------
++%%% File : mod_logdb_mnesia_old.erl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose : mod_logmnesia backend for mod_logdb (should be used only for copy_tables functionality)
++%%% Version : trunk
++%%% Id : $Id: mod_logdb_mnesia_old.erl 1273 2009-02-05 18:12:57Z malik $
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
+
-+create_servers_table(DBRef, VHost) ->
-+ SName = servers_table(VHost),
-+ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
-+ "server TEXT NOT NULL, ",
-+ "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
-+ "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
-+ ") ENGINE=InnoDB CHARACTER SET utf8;"
-+ ],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created servers table for ~p", [VHost]),
-+ ets:new(ets_servers_table(VHost), [named_table, set, public]),
-+ update_servers_from_db(DBRef, VHost),
-+ ok;
-+ {error, _} ->
-+ error
-+ end.
++-module(mod_logdb_mnesia_old).
++-author('o.palij@gmail.com').
++
++-include("ejabberd.hrl").
++-include("jlib.hrl").
+
-+create_resources_table(DBRef, VHost) ->
-+ RName = resources_table(VHost),
-+ Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
-+ "resource TEXT NOT NULL, ",
-+ "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
-+ "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
-+ ") ENGINE=InnoDB CHARACTER SET utf8;"
-+ ],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created resources table for ~p", [VHost]),
-+ ets:new(ets_resources_table(VHost), [named_table, set, public]),
-+ ok;
-+ {error, _} ->
-+ error
-+ end.
++-behaviour(gen_logdb).
+
-+create_msg_table(DBRef, VHost, Date) ->
-+ TName = messages_table(VHost, Date),
-+ Query = ["CREATE TABLE ",TName," (",
-+ "owner_id MEDIUMINT UNSIGNED, ",
-+ "peer_name_id MEDIUMINT UNSIGNED, ",
-+ "peer_server_id MEDIUMINT UNSIGNED, ",
-+ "peer_resource_id MEDIUMINT(8) UNSIGNED, ",
-+ "direction ENUM('to', 'from'), ",
-+ "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ",
-+ "subject TEXT, ",
-+ "body TEXT, ",
-+ "timestamp DOUBLE, ",
-+ "INDEX owner_i (owner_id), ",
-+ "INDEX peer_i (peer_name_id, peer_server_id), ",
-+ "FULLTEXT (body) "
-+ ") ENGINE=MyISAM CHARACTER SET utf8;"
-+ ],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _MySQLRes} ->
-+ ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
-+ ok;
-+ {error, _} ->
-+ error
-+ end.
++-export([start/2, stop/1,
++ log_message/2,
++ rebuild_stats/1,
++ rebuild_stats_at/2,
++ rebuild_stats_at1/2,
++ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
++ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
++ get_dates/1,
++ get_users_settings/1, get_user_settings/2, set_user_settings/3,
++ drop_user/2]).
++
++-record(stats, {user, server, table, count}).
++-record(msg, {to_user, to_server, to_resource, from_user, from_server, from_resource, id, type, subject, body, timestamp}).
++
++tables_prefix() -> "messages_".
++% stats_table should not start with tables_prefix(VHost) !
++% i.e. lists:prefix(tables_prefix(VHost), atom_to_list(stats_table())) must be /= true
++stats_table() -> list_to_atom("messages-stats").
++% table name as atom from Date
++-define(ATABLE(Date), list_to_atom(tables_prefix() ++ Date)).
++-define(LTABLE(Date), tables_prefix() ++ Date).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% internal ets cache (users, servers, resources)
++% gen_logdb callbacks
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+update_servers_from_db(DBRef, VHost) ->
-+ ?INFO_MSG("Reading servers from db for ~p", [VHost]),
-+ SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"],
-+ {data, Result} = sql_query_internal(DBRef, SQuery),
-+ true = ets:delete_all_objects(ets_servers_table(VHost)),
-+ true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]).
-+
-+%update_users_from_db(DBRef, VHost) ->
-+% ?INFO_MSG("Reading users from db for ~p", [VHost]),
-+% SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"],
-+% {data, Result} = sql_query_internal(DBRef, SQuery),
-+% true = ets:delete_all_objects(ets_users_table(VHost)),
-+% true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]).
-+
-+%get_user_name(DBRef, VHost, User_id) ->
-+% case ets:match(ets_users_table(VHost), {'$1', User_id}) of
-+% [[User]] -> User;
-+% % this can be in clustered environment
-+% [] ->
-+% %update_users_from_db(DBRef, VHost),
-+% SQuery = ["SELECT username FROM ",users_table(VHost)," ",
-+% "WHERE user_id=\"",User_id,"\";"],
-+% {data, [[Name]]} = sql_query_internal(DBRef, SQuery),
-+% % cache {user, id} pair
-+% ets:insert(ets_users_table(VHost), {Name, User_id}),
-+% Name
-+% end.
++start(_Opts, _VHost) ->
++ case mnesia:system_info(is_running) of
++ yes ->
++ ok = create_stats_table(),
++ {ok, ok};
++ no ->
++ ?ERROR_MSG("Mnesia not running", []),
++ error;
++ Status ->
++ ?ERROR_MSG("Mnesia status: ~p", [Status]),
++ error
++ end.
+
-+%get_server_name(DBRef, VHost, Server_id) ->
-+% case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of
-+% [[Server]] -> Server;
-+ % this can be in clustered environment
-+% [] ->
-+% update_servers_from_db(DBRef, VHost),
-+% [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}),
-+% Server1
-+% end.
++stop(_VHost) ->
++ ok.
+
-+get_user_id_from_db(DBRef, VHost, User) ->
-+ SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
-+ "WHERE username=\"",User,"\";"],
-+ case sql_query_internal(DBRef, SQuery) of
-+ % no such user in db
-+ {data, []} ->
-+ {ok, []};
-+ {data, [[DBId]]} ->
-+ % cache {user, id} pair
-+ ets:insert(ets_users_table(VHost), {User, DBId}),
-+ {ok, DBId}
-+ end.
-+get_user_id(DBRef, VHost, User) ->
-+ % Look at ets
-+ case ets:match(ets_users_table(VHost), {User, '$1'}) of
-+ [] ->
-+ % Look at db
-+ case get_user_id_from_db(DBRef, VHost, User) of
-+ % no such user in db
-+ {ok, []} ->
-+ IQuery = ["INSERT INTO ",users_table(VHost)," ",
-+ "SET username=\"",User,"\";"],
-+ case sql_query_internal_silent(DBRef, IQuery) of
-+ {updated, _} ->
-+ {ok, NewId} = get_user_id_from_db(DBRef, VHost, User),
-+ NewId;
-+ {error, Reason} ->
-+ % this can be in clustered environment
-+ {match, _, _} = regexp:match(Reason, "#23000"),
-+ ?ERROR_MSG("Duplicate key name for ~p", [User]),
-+ {ok, ClID} = get_user_id_from_db(DBRef, VHost, User),
-+ ClID
-+ end;
-+ {ok, DBId} ->
-+ DBId
-+ end;
-+ [[EtsId]] -> EtsId
-+ end.
++log_message(_VHost, _Msg) ->
++ error.
+
-+get_server_id(DBRef, VHost, Server) ->
-+ case ets:match(ets_servers_table(VHost), {Server, '$1'}) of
-+ [] ->
-+ IQuery = ["INSERT INTO ",servers_table(VHost)," ",
-+ "SET server=\"",Server,"\";"],
-+ case sql_query_internal_silent(DBRef, IQuery) of
-+ {updated, _} ->
-+ SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ",
-+ "WHERE server=\"",Server,"\";"],
-+ {data, [[Id]]} = sql_query_internal(DBRef, SQuery),
-+ ets:insert(ets_servers_table(VHost), {Server, Id}),
-+ Id;
-+ {error, Reason} ->
-+ % this can be in clustered environment
-+ {match, _, _} = regexp:match(Reason, "#23000"),
-+ ?ERROR_MSG("Duplicate key name for ~p", [Server]),
-+ update_servers_from_db(DBRef, VHost),
-+ [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}),
-+ Id1
-+ end;
-+ [[Id]] -> Id
-+ end.
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% gen_logdb callbacks (maintaince)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++rebuild_stats(_VHost) ->
++ ok.
+
-+get_resource_id_from_db(DBRef, VHost, Resource) ->
-+ SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ",
-+ "WHERE resource=\"",ejabberd_odbc:escape(Resource),"\";"],
-+ case sql_query_internal(DBRef, SQuery) of
-+ % no such resource in db
-+ {data, []} ->
-+ {ok, []};
-+ {data, [[DBId]]} ->
-+ % cache {resource, id} pair
-+ ets:insert(ets_resources_table(VHost), {Resource, DBId}),
-+ {ok, DBId}
-+ end.
-+get_resource_id(DBRef, VHost, Resource) ->
-+ % Look at ets
-+ case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of
-+ [] ->
-+ % Look at db
-+ case get_resource_id_from_db(DBRef, VHost, Resource) of
-+ % no such resource in db
-+ {ok, []} ->
-+ IQuery = ["INSERT INTO ",resources_table(VHost)," ",
-+ "SET resource=\"",ejabberd_odbc:escape(Resource),"\";"],
-+ case sql_query_internal_silent(DBRef, IQuery) of
-+ {updated, _} ->
-+ {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource),
-+ NewId;
-+ {error, Reason} ->
-+ % this can be in clustered environment
-+ {match, _, _} = regexp:match(Reason, "#23000"),
-+ ?ERROR_MSG("Duplicate key name for ~p", [Resource]),
-+ {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource),
-+ ClID
-+ end;
-+ {ok, DBId} ->
-+ DBId
-+ end;
-+ [[EtsId]] -> EtsId
-+ end.
++rebuild_stats_at(VHost, Date) ->
++ Table = ?LTABLE(Date),
++ {Time, Value}=timer:tc(?MODULE, rebuild_stats_at1, [VHost, Table]),
++ ?INFO_MSG("rebuild_stats_at ~p elapsed ~p sec: ~p~n", [Date, Time/1000000, Value]),
++ Value.
++rebuild_stats_at1(VHost, Table) ->
++ CFun = fun(Msg, Stats) ->
++ To = Msg#msg.to_user ++ "@" ++ Msg#msg.to_server,
++ Stats_to = if
++ Msg#msg.to_server == VHost ->
++ 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,
++ From = Msg#msg.from_user ++ "@" ++ Msg#msg.from_server,
++ Stats_from = if
++ Msg#msg.from_server == VHost ->
++ case lists:keysearch(From, 1, Stats_to) 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,
++ Stats_from
++ end,
++ DFun = fun(#stats{table=STable, server=Server} = Stat, _Acc)
++ when STable == Table, Server == VHost ->
++ mnesia:delete_object(stats_table(), Stat, write);
++ (_Stat, _Acc) -> ok
++ end,
++ case mnesia:transaction(fun() ->
++ mnesia:write_lock_table(list_to_atom(Table)),
++ mnesia:write_lock_table(stats_table()),
++ % Calc stats for VHost at Date
++ AStats = mnesia:foldl(CFun, [], list_to_atom(Table)),
++ % Delete all stats for VHost at Date
++ mnesia:foldl(DFun, [], stats_table()),
++ % Write new calc'ed stats
++ lists:foreach(fun({Who, Count}) ->
++ Jid = jlib:string_to_jid(Who),
++ JUser = Jid#jid.user,
++ WStat = #stats{user=JUser, server=VHost, table=Table, count=Count},
++ mnesia:write(stats_table(), WStat, write)
++ end, AStats)
++ end) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Table, Reason]),
++ error;
++ {atomic, _} ->
++ ok
++ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% SQL internals
++% gen_logdb callbacks (delete)
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
-+sql_transaction_internal(DBRef, Fun) ->
-+ case sql_query_internal(DBRef, ["START TRANSACTION;"]) of
-+ {updated, _} ->
-+ case catch Fun() of
-+ error = Err ->
-+ rollback_internal(DBRef, Err);
-+ {error, _} = Err ->
-+ rollback_internal(DBRef, Err);
-+ {'EXIT', _} = Err ->
-+ rollback_internal(DBRef, Err);
-+ Res ->
-+ case sql_query_internal(DBRef, ["COMMIT;"]) of
-+ {error, _} -> rollback_internal(DBRef, {commit_error});
-+ {updated, _} ->
-+ case Res of
-+ {atomic, _} -> Res;
-+ _ -> {atomic, Res}
-+ end
-+ end
-+ end;
-+ {error, _} ->
-+ {aborted, {begin_error}}
++delete_messages_by_user_at(_VHost, _Msgs, _Date) ->
++ error.
++
++delete_all_messages_by_user_at(_User, _VHost, _Date) ->
++ error.
++
++delete_messages_at(VHost, Date) ->
++ Table = list_to_atom(tables_prefix() ++ Date),
++
++ DFun = fun(#msg{to_server=To_server, from_server=From_server}=Msg, _Acc)
++ when To_server == VHost; From_server == VHost ->
++ mnesia:delete_object(Table, Msg, write);
++ (_Msg, _Acc) -> ok
++ end,
++
++ case mnesia:transaction(fun() ->
++ mnesia:foldl(DFun, [], Table)
++ end) of
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p: ~p", [VHost, Date, Reason]),
++ error;
++ {atomic, _} ->
++ ok
++ end.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% gen_logdb callbacks (get)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++get_vhost_stats(_VHost) ->
++ {error, "does not emplemented"}.
++
++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.
+
-+% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
-+rollback_internal(DBRef, Reason) ->
-+ Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
-+ {aborted, {Reason, {rollback_result, Res}}}.
++get_user_stats(_User, _VHost) ->
++ {error, "does not emplemented"}.
+
-+sql_query_internal(DBRef, Query) ->
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {error, Reason} ->
-+ ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
-+ {error, Reason};
-+ Rez -> Rez
++get_user_messages_at(User, VHost, Date) ->
++ Table_name = tables_prefix() ++ Date,
++ case mnesia:transaction(fun() ->
++ Pat_to = #msg{to_user=User, to_server=VHost, _='_'},
++ Pat_from = #msg{from_user=User, from_server=VHost, _='_'},
++ mnesia:select(list_to_atom(Table_name),
++ [{Pat_to, [], ['$_']},
++ {Pat_from, [], ['$_']}])
++ end) of
++ {atomic, Result} ->
++ Msgs = lists:map(fun(#msg{to_user=To_user, to_server=To_server, to_resource=To_res,
++ from_user=From_user, from_server=From_server, from_resource=From_res,
++ type=Type,
++ subject=Subj,
++ body=Body, timestamp=Timestamp} = _Msg) ->
++ Subject = case Subj of
++ "None" -> "";
++ _ -> Subj
++ end,
++ {msg, To_user, To_server, To_res, From_user, From_server, From_res, Type, Subject, Body, Timestamp}
++ end, Result),
++ {ok, Msgs};
++ {aborted, Reason} ->
++ {error, Reason}
+ end.
+
-+sql_query_internal_silent(DBRef, Query) ->
-+ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
-+ get_result(mysql_conn:fetch(DBRef, Query, self(), ?TIMEOUT)).
++get_dates(_VHost) ->
++ Tables = mnesia:system_info(tables),
++ MessagesTables =
++ lists:filter(fun(Table) ->
++ lists:prefix(tables_prefix(), atom_to_list(Table))
++ end,
++ Tables),
++ lists:map(fun(Table) ->
++ lists:sublist(atom_to_list(Table),
++ length(tables_prefix())+1,
++ length(atom_to_list(Table)))
++ end,
++ MessagesTables).
+
-+get_result({updated, MySQLRes}) ->
-+ {updated, mysql:get_result_affected_rows(MySQLRes)};
-+get_result({data, MySQLRes}) ->
-+ {data, mysql:get_result_rows(MySQLRes)};
-+get_result({error, "query timed out"}) ->
-+ {error, "query timed out"};
-+get_result({error, MySQLRes}) ->
-+ Reason = mysql:get_result_reason(MySQLRes),
-+ {error, Reason}.
---- src/mod_logdb_mysql5.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb_mysql5.erl Tue Dec 11 11:58:33 2007
-@@ -0,0 +1,854 @@
++get_users_settings(_VHost) ->
++ {ok, []}.
++get_user_settings(_User, _VHost) ->
++ {ok, []}.
++set_user_settings(_User, _VHost, _Set) ->
++ ok.
++drop_user(_User, _VHost) ->
++ ok.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% internal
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% called from db_logon/2
++create_stats_table() ->
++ SName = stats_table(),
++ case mnesia:create_table(SName,
++ [{disc_only_copies, [node()]},
++ {type, bag},
++ {attributes, record_info(fields, stats)},
++ {record_name, stats}
++ ]) of
++ {atomic, ok} ->
++ ?INFO_MSG("Created stats table", []),
++ ok;
++ {aborted, {already_exists, _}} ->
++ ok;
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
++ error
++ end.
+diff --git src/mod_logdb_mysql.erl src/mod_logdb_mysql.erl
+new file mode 100644
+index 0000000..7c473ce
+--- /dev/null
++++ src/mod_logdb_mysql.erl
+@@ -0,0 +1,1052 @@
+%%%----------------------------------------------------------------------
-+%%% File : mod_logdb_mysql5.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose : MySQL 5 backend for mod_logdb
++%%% File : mod_logdb_mysql.erl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose : MySQL backend for mod_logdb
+%%% Version : trunk
-+%%% Id : $Id$
++%%% Id : $Id: mod_logdb_mysql.erl 1360 2009-07-30 06:00:14Z malik $
+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
+%%%----------------------------------------------------------------------
+
-+-module(mod_logdb_mysql5).
++-module(mod_logdb_mysql).
+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
+
+-include("mod_logdb.hrl").
+-include("ejabberd.hrl").
+ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
+ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
+ get_dates/1,
-+ get_users_settings/1, get_user_settings/2, set_user_settings/3]).
++ get_users_settings/1, get_user_settings/2, set_user_settings/3,
++ drop_user/2]).
+
+% gen_server call timeout
-+-define(CALL_TIMEOUT, 60000).
-+-define(TIMEOUT, 60000).
++-define(CALL_TIMEOUT, 30000).
++-define(MYSQL_TIMEOUT, 60000).
+-define(INDEX_SIZE, integer_to_list(170)).
-+-define(PROCNAME, mod_logdb_mysql5).
++-define(PROCNAME, mod_logdb_mysql).
+
+-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
+ list_to_string/1, string_to_list/1,
+ convert_timestamp_brief/1]).
+
-+-record(state, {dbref, vhost}).
++-record(state, {dbref, vhost, server, port, db, user, password}).
+
+% replace "." with "_"
+escape_vhost(VHost) -> lists:map(fun(46) -> 95;
+messages_table(VHost, Date) ->
+ prefix() ++ "messages_" ++ Date ++ suffix(VHost).
+
-+% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
-+view_table(VHost, Date) ->
-+ Table = messages_table(VHost, Date),
-+ TablewoQ = lists:sublist(Table, 2, length(Table) - 2),
-+ lists:append(["`v_", TablewoQ, "`"]).
-+
+stats_table(VHost) ->
+ prefix() ++ "stats" ++ suffix(VHost).
+
++temp_table(VHost) ->
++ prefix() ++ "temp" ++ suffix(VHost).
++
+settings_table(VHost) ->
+ prefix() ++ "settings" ++ suffix(VHost).
+
+resources_table(VHost) ->
+ prefix() ++ "resources" ++ suffix(VHost).
+
++ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ VHost).
++ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ VHost).
++ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ VHost).
++
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% gen_mod callbacks
+ crypto:start(),
+
+ Server = gen_mod:get_opt(server, Opts, "localhost"),
-+ Port = gen_mod:get_opt(port, Opts, 3128),
++ Port = gen_mod:get_opt(port, Opts, 3306),
+ DB = gen_mod:get_opt(db, Opts, "logdb"),
+ User = gen_mod:get_opt(user, Opts, "root"),
+ Password = gen_mod:get_opt(password, Opts, ""),
+
-+ LogFun = fun(debug, Format, Argument) ->
-+ ?MYDEBUG(Format, Argument);
-+ (error, Format, Argument) ->
-+ ?ERROR_MSG(Format, Argument);
-+ (Level, Format, Argument) ->
-+ ?MYDEBUG("MySQL (~p)~n", [Level]),
-+ ?MYDEBUG(Format, Argument)
-+ end,
-+ case mysql_conn:start(Server, Port, User, Password, DB, [65536, 131072], LogFun) of
++ St = #state{vhost=VHost,
++ server=Server, port=Port, db=DB,
++ user=User, password=Password},
++
++ case open_mysql_connection(St) of
+ {ok, DBRef} ->
-+ ok = create_internals(DBRef, VHost),
-+ ok = create_stats_table(DBRef, VHost),
-+ ok = create_settings_table(DBRef, VHost),
-+ ok = create_users_table(DBRef, VHost),
-+ ok = create_servers_table(DBRef, VHost),
-+ ok = create_resources_table(DBRef, VHost),
++ State = St#state{dbref=DBRef},
++ ok = create_stats_table(State),
++ ok = create_settings_table(State),
++ ok = create_users_table(State),
++ % clear ets cache every ...
++ timer:send_interval(timer:hours(12), clear_ets_tables),
++ ok = create_servers_table(State),
++ ok = create_resources_table(State),
+ erlang:monitor(process, DBRef),
-+ {ok, #state{dbref=DBRef, vhost=VHost}};
++ {ok, State};
+ {error, Reason} ->
+ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
+ {stop, db_connection_failed}
+ end.
+
++open_mysql_connection(#state{server=Server, port=Port, db=DB,
++ user=DBUser, password=Password} = _State) ->
++ LogFun = fun(debug, _Format, _Argument) ->
++ %?MYDEBUG(Format, Argument);
++ ok;
++ (error, Format, Argument) ->
++ ?ERROR_MSG(Format, Argument);
++ (Level, Format, Argument) ->
++ ?MYDEBUG("MySQL (~p)~n", [Level]),
++ ?MYDEBUG(Format, Argument)
++ end,
++ ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
++ mysql_conn:start(Server, Port, DBUser, Password, DB, LogFun).
++
++close_mysql_connection(DBRef) ->
++ ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
++ mysql_conn:stop(DBRef).
++
+handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Date = convert_timestamp_brief(Msg#msg.timestamp),
-+ TableName = messages_table(VHost, Date),
+
-+ Query = [ "CALL logmessage "
-+ "('", TableName, "',",
-+ "'", Date, "',",
-+ "'", Msg#msg.owner_name, "',",
-+ "'", Msg#msg.peer_name, "',",
-+ "'", Msg#msg.peer_server, "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',",
-+ "'", atom_to_list(Msg#msg.direction), "',",
-+ "'", Msg#msg.type, "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.body), "',",
-+ "'", Msg#msg.timestamp, "');"],
++ Table = messages_table(VHost, Date),
++ Owner_id = get_user_id(DBRef, VHost, Msg#msg.owner_name),
++ Peer_name_id = get_user_id(DBRef, VHost, Msg#msg.peer_name),
++ Peer_server_id = get_server_id(DBRef, VHost, Msg#msg.peer_server),
++ Peer_resource_id = get_resource_id(DBRef, VHost, Msg#msg.peer_resource),
++
++ Query = ["INSERT INTO ",Table," ",
++ "(owner_id,",
++ "peer_name_id,",
++ "peer_server_id,",
++ "peer_resource_id,",
++ "direction,",
++ "type,",
++ "subject,",
++ "body,",
++ "timestamp) ",
++ "VALUES ",
++ "('", Owner_id, "',",
++ "'", Peer_name_id, "',",
++ "'", Peer_server_id, "',",
++ "'", Peer_resource_id, "',",
++ "'", atom_to_list(Msg#msg.direction), "',",
++ "'", Msg#msg.type, "',",
++ "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
++ "'", ejabberd_odbc:escape(Msg#msg.body), "',",
++ "'", Msg#msg.timestamp, "');"],
+
+ Reply =
-+ case sql_query_internal(DBRef, Query) of
++ case sql_query_internal_silent(DBRef, Query) of
+ {updated, _} ->
+ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
+ Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
-+ ok;
-+ {error, _Reason} ->
-+ error
++ increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date);
++ {error, Reason} ->
++ case ejabberd_regexp:run(Reason, "#42S02") of
++ % Table doesn't exist
++ match ->
++ case create_msg_table(DBRef, VHost, Date) of
++ error ->
++ error;
++ ok ->
++ {updated, _} = sql_query_internal(DBRef, Query),
++ increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date)
++ end;
++ _ ->
++ ?ERROR_MSG("Failed to log message: ~p", [Reason]),
++ error
++ end
+ end,
+ {reply, Reply, State};
-+handle_call({rebuild_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ ok = delete_nonexistent_stats(DBRef, VHost),
-+ Reply =
-+ lists:foreach(fun(Date) ->
-+ catch rebuild_stats_at_int(DBRef, VHost, Date)
-+ end, get_dates_int(DBRef, VHost)),
-+ {reply, Reply, State};
+handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Reply = rebuild_stats_at_int(DBRef, VHost, Date),
+ {reply, Reply, State};
+ {error, _} ->
+ error
+ end,
-+ {reply, Reply, State};
-+handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
-+ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
-+ Reply =
-+ case sql_query_internal(DBRef, DQuery) of
-+ {updated, _} ->
-+ rebuild_stats_at_int(DBRef, VHost, Date);
-+ {error, _} ->
-+ error
-+ end,
-+ {reply, Reply, State};
-+handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Fun = fun() ->
-+ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]),
-+ TQuery = ["DELETE FROM ",stats_table(VHost)," "
-+ "WHERE at=\"",Date,"\";"],
-+ {updated, _} = sql_query_internal(DBRef, TQuery),
-+ VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"],
-+ {updated, _} = sql_query_internal(DBRef, VQuery)
-+ end,
++ {reply, Reply, State};
++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
++ ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
++ {reply, ok, State};
++handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Reply =
-+ case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, _} ->
-+ ok;
-+ {aborted, _} ->
++ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of
++ {updated, _} ->
++ Query = ["DELETE FROM ",stats_table(VHost)," "
++ "WHERE at=\"",Date,"\";"],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end;
++ {error, _} ->
+ error
+ end,
+ {reply, Reply, State};
+ {reply, Reply, State};
+handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ SName = stats_table(VHost),
-+ Query = ["SELECT username, count ",
++ Query = ["SELECT username, sum(count) AS allcount ",
+ "FROM ",SName," ",
+ "JOIN ",users_table(VHost)," ON owner_id=user_id "
-+ "WHERE at=\"",Date,"\" ",
-+ "ORDER BY count DESC;"
++ "WHERE at=\"",Date,"\" "
++ "GROUP BY username ",
++ "ORDER BY allcount DESC;"
+ ],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
+ {data, Result} ->
-+ {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
++ {ok, lists:reverse(
++ lists:keysort(2,
++ [ {User, list_to_integer(Count)} || [User, Count] <- Result]))};
+ {error, Reason} ->
++ % TODO:
+ {error, Reason}
+ end,
+ {reply, Reply, State};
+handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ SName = stats_table(VHost),
-+ UName = users_table(VHost),
-+ Query = ["SELECT stats.at, stats.count ",
-+ "FROM ",UName," AS users ",
-+ "JOIN ",SName," AS stats ON owner_id=user_id "
-+ "WHERE users.username=\"",User,"\" ",
-+ "ORDER BY DATE(at) DESC;"
-+ ],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Result} ->
-+ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
-+ {error, Result} ->
-+ {error, Result}
-+ end,
-+ {reply, Reply, State};
++ {reply, get_user_stats_int(DBRef, User, VHost), State};
+handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
-+ Query = ["SELECT peer_name,",
-+ "peer_server,",
-+ "peer_resource,",
-+ "direction,"
-+ "type,"
-+ "subject,"
-+ "body,"
-+ "timestamp "
-+ "FROM ",view_table(VHost, Date)," "
-+ "WHERE owner_name=\"",User,"\";"],
++ TName = messages_table(VHost, Date),
++ UName = users_table(VHost),
++ SName = servers_table(VHost),
++ RName = resources_table(VHost),
++ Query = ["SELECT users.username,",
++ "servers.server,",
++ "resources.resource,",
++ "messages.direction,"
++ "messages.type,"
++ "messages.subject,"
++ "messages.body,"
++ "messages.timestamp "
++ "FROM ",TName," AS messages "
++ "JOIN ",UName," AS users ON peer_name_id=user_id ",
++ "JOIN ",SName," AS servers ON peer_server_id=server_id ",
++ "JOIN ",RName," AS resources ON peer_resource_id=resource_id ",
++ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
++ "ORDER BY timestamp ASC;"],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
+ {data, Result} ->
+ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
+ "FROM ",settings_table(VHost)," ",
+ "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
-+ Reply =
++ Reply =
+ case sql_query_internal(DBRef, Query) of
+ {data, Result} ->
+ {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
+ {reply, Reply, State};
+handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
-+ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
+ {data, []} ->
+ donotlog_list=DoNotLogL}},
+ _From, #state{dbref=DBRef, vhost=VHost} = State) ->
+ User_id = get_user_id(DBRef, VHost, User),
++
+ Query = ["UPDATE ",settings_table(VHost)," ",
+ "SET dolog_default=",bool_to_list(DoLogDef),", ",
+ "dolog_list='",list_to_string(DoLogL),"', ",
+ "donotlog_list='",list_to_string(DoNotLogL),"' ",
-+ "WHERE owner_id=",User_id,";"],
++ "WHERE owner_id=\"",User_id,"\";"],
+
+ Reply =
+ case sql_query_internal(DBRef, Query) of
+ IQuery = ["INSERT INTO ",settings_table(VHost)," ",
+ "(owner_id, dolog_default, dolog_list, donotlog_list) ",
+ "VALUES ",
-+ "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
++ "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
+ case sql_query_internal_silent(DBRef, IQuery) of
+ {updated, _} ->
+ ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
+ ok;
+ {error, Reason} ->
-+ case regexp:match(Reason, "#23000") of
++ case ejabberd_regexp:run(Reason, "#23000") of
+ % Already exists
-+ {match, _, _} ->
++ match ->
+ ok;
+ _ ->
+ ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
+ end,
+ {reply, Reply, State};
+handle_call({stop}, _From, #state{vhost=VHost}=State) ->
-+ ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]),
++ ets:delete(ets_users_table(VHost)),
++ ets:delete(ets_servers_table(VHost)),
++ ?MYDEBUG("Stoping mysql backend for ~p", [VHost]),
+ {stop, normal, ok, State};
+handle_call(Msg, _From, State) ->
+ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
+ {noreply, State}.
+
++handle_cast({rebuild_stats}, State) ->
++ rebuild_all_stats_int(State),
++ {noreply, State};
++handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
++ Fun = fun() ->
++ {ok, DBRef} = open_mysql_connection(State),
++ {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
++ MDResult = lists:map(fun({Date, _}) ->
++ delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
++ end, Dates),
++ StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
++ SDResult = delete_user_settings_int(DBRef, User, VHost),
++ case lists:all(fun(Result) when Result == ok ->
++ true;
++ (Result) when Result == error ->
++ false
++ end, lists:append([MDResult, [StDResult], [SDResult]])) of
++ true ->
++ ?INFO_MSG("Removed ~s@~s", [User, VHost]);
++ false ->
++ ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
++ end,
++ close_mysql_connection(DBRef)
++ end,
++ spawn(Fun),
++ {noreply, State};
+handle_cast(Msg, State) ->
+ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
+ {noreply, State}.
+
++handle_info(clear_ets_tables, State) ->
++ ets:delete_all_objects(ets_users_table(State#state.vhost)),
++ ets:delete_all_objects(ets_resources_table(State#state.vhost)),
++ {noreply, State};
+handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
+ {stop, connection_dropped, State};
+handle_info(Info, State) ->
+ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
+ {noreply, State}.
+
-+terminate(_Reason, _State) ->
++terminate(_Reason, #state{dbref=DBRef}=_State) ->
++ close_mysql_connection(DBRef),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
+rebuild_stats(VHost) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
++ gen_server:cast(Proc, {rebuild_stats}).
+rebuild_stats_at(VHost, Date) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
+set_user_settings(User, VHost, Set) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
++drop_user(User, VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {drop_user, User}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% internals
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++increment_user_stats(DBRef, User_name, User_id, VHost, PNameID, PServerID, Date) ->
++ SName = stats_table(VHost),
++ UQuery = ["UPDATE ",SName," ",
++ "SET count=count+1 ",
++ "WHERE owner_id=\"",User_id,"\" AND peer_name_id=\"",PNameID,"\" AND peer_server_id=\"",PServerID,"\" AND at=\"",Date,"\";"],
++
++ case sql_query_internal(DBRef, UQuery) of
++ {updated, 0} ->
++ IQuery = ["INSERT INTO ",SName," ",
++ "(owner_id, peer_name_id, peer_server_id, at, count) ",
++ "VALUES ",
++ "('",User_id,"', '",PNameID,"', '",PServerID,"', '",Date,"', '1');"],
++ case sql_query_internal(DBRef, IQuery) of
++ {updated, _} ->
++ ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]),
++ ok;
++ {error, _} ->
++ error
++ end;
++ {updated, _} ->
++ ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]),
++ ok;
++ {error, _} ->
++ error
++ end.
++
+get_dates_int(DBRef, VHost) ->
+ case sql_query_internal(DBRef, ["SHOW TABLES"]) of
+ {data, Tables} ->
+ lists:foldl(fun([Table], Dates) ->
-+ % TODO: check prefix()
-+ case regexp:match(Table, escape_vhost(VHost)) of
-+ {match, _, _} ->
-+ case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of
-+ {match, S, E} ->
++ Reg = lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
++ case re:run(Table, Reg) of
++ {match, [{1, _}]} ->
++ ?MYDEBUG("matched ~p against ~p", [Table, Reg]),
++ case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
++ {match, [{S, E}]} ->
+ lists:append(Dates, [lists:sublist(Table,S,E)]);
+ nomatch ->
+ Dates
+ end;
-+ nomatch ->
++ _ ->
+ Dates
+ end
+ end, [], Tables);
+ {error, _} ->
+ []
-+ end.
-+
-+rebuild_stats_at_int(DBRef, VHost, Date) ->
-+ Table = messages_table(VHost, Date),
-+ STable = stats_table(VHost),
++ end.
+
-+ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",
-+ STable," WRITE;"]),
-+ Fun =
-+ fun() ->
-+ DQuery = [ "DELETE FROM ",STable," ",
-+ "WHERE at='",Date,"';"],
++rebuild_all_stats_int(#state{vhost=VHost}=State) ->
++ Fun = fun() ->
++ {ok, DBRef} = open_mysql_connection(State),
++ ok = delete_nonexistent_stats(DBRef, VHost),
++ case lists:filter(fun(Date) ->
++ case catch rebuild_stats_at_int(DBRef, VHost, Date) of
++ ok -> false;
++ error -> true;
++ {'EXIT', _} -> true
++ end
++ end, get_dates_int(DBRef, VHost)) of
++ [] -> ok;
++ FTables ->
++ ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
++ error
++ end,
++ close_mysql_connection(DBRef)
++ end,
++ spawn(Fun).
+
-+ {updated, _} = sql_query_internal(DBRef, DQuery),
++rebuild_stats_at_int(DBRef, VHost, Date) ->
++ TempTable = temp_table(VHost),
++ Fun = fun() ->
++ Table = messages_table(VHost, Date),
++ STable = stats_table(VHost),
+
-+ SQuery = ["INSERT INTO ",STable," ",
-+ "(owner_id,at,count) ",
-+ "SELECT owner_id,\"",Date,"\"",",count(*) ",
-+ "FROM ",Table," GROUP BY owner_id;"],
++ DQuery = [ "DELETE FROM ",STable," ",
++ "WHERE at='",Date,"';"],
+
-+ case sql_query_internal(DBRef, SQuery) of
-+ {updated, 0} ->
-+ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
-+ ok;
-+ {updated, _} -> ok;
-+ {error, _} -> error
-+ end
-+ end,
++ ok = create_temp_table(DBRef, TempTable),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
++ SQuery = ["INSERT INTO ",TempTable," ",
++ "(owner_id,peer_name_id,peer_server_id,at,count) ",
++ "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
++ "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, 0} ->
++ Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
++ case Count of
++ {data, [["0"]]} ->
++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]),
++ {updated, _} = sql_query_internal(DBRef, DQuery),
++ ok;
++ _ ->
++ ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
++ error
++ end;
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
++ {updated, _} = sql_query_internal(DBRef, DQuery),
++ SQuery1 = ["INSERT INTO ",STable," ",
++ "(owner_id,peer_name_id,peer_server_id,at,count) ",
++ "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
++ "FROM ",TempTable,";"],
++ case sql_query_internal(DBRef, SQuery1) of
++ {updated, _} -> ok;
++ {error, _} -> error
++ end;
++ {error, _} -> error
++ end
++ end,
+
-+ Res = case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, _} ->
-+ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
-+ ok;
-+ {aborted, _} ->
-+ error
-+ end,
-+ {updated, _} = sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
-+ Res.
++ case catch apply(Fun, []) of
++ ok ->
++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
++ ok;
++ error ->
++ error;
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
++ error
++ end,
++ sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
++ sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
++ ok.
+
+
+delete_nonexistent_stats(DBRef, VHost) ->
+ ["\"",Date,"\"",","]
+ end, Dates),
+
-+ Temp1 = case Temp of
-+ [] ->
-+ ["\"\""];
-+ _ ->
-+ % replace last "," with ");"
-+ lists:append([lists:sublist(Temp, length(Temp)-1), ");"])
-+ end,
-+
-+ Query = ["DELETE FROM ",STable," ",
-+ "WHERE at NOT IN (", Temp1],
++ case Temp of
++ [] ->
++ ok;
++ _ ->
++ % replace last "," with ");"
++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
++ Query = ["DELETE FROM ",STable," ",
++ "WHERE at NOT IN (", Temp1],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end
++ end.
+
++get_user_stats_int(DBRef, User, VHost) ->
++ SName = stats_table(VHost),
++ Query = ["SELECT at, sum(count) as allcount ",
++ "FROM ",SName," ",
++ "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
++ "GROUP BY at "
++ "ORDER BY DATE(at) DESC;"
++ ],
+ case sql_query_internal(DBRef, Query) of
++ {data, Result} ->
++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]};
++ {error, Result} ->
++ {error, Result}
++ end.
++
++delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
++ DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ case sql_query_internal(DBRef, DQuery) of
+ {updated, _} ->
++ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
+ ok;
+ {error, _} ->
+ error
+ end.
+
++delete_all_stats_by_user_int(DBRef, User, VHost) ->
++ SQuery = ["DELETE FROM ",stats_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
++ ok;
++ {error, _} -> error
++ end.
++
++delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
++ SQuery = ["DELETE FROM ",stats_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
++ "AND at=\"",Date,"\";"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
++ ok;
++ {error, _} -> error
++ end.
++
++delete_user_settings_int(DBRef, User, VHost) ->
++ Query = ["DELETE FROM ",settings_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
++ ok;
++ {error, Reason} ->
++ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
++ error
++ end.
++
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+% tables internals
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+create_stats_table(DBRef, VHost) ->
++create_temp_table(DBRef, Name) ->
++ Query = ["CREATE TABLE ",Name," (",
++ "owner_id MEDIUMINT UNSIGNED, ",
++ "peer_name_id MEDIUMINT UNSIGNED, ",
++ "peer_server_id MEDIUMINT UNSIGNED, ",
++ "at VARCHAR(11), ",
++ "count INT(11) ",
++ ") ENGINE=MyISAM CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} -> ok;
++ {error, _Reason} -> error
++ end.
++
++create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
+ SName = stats_table(VHost),
+ Query = ["CREATE TABLE ",SName," (",
+ "owner_id MEDIUMINT UNSIGNED, ",
-+ "at VARCHAR(11), ",
-+ "count INT(11), ",
-+ "INDEX(owner_id), ",
++ "peer_name_id MEDIUMINT UNSIGNED, ",
++ "peer_server_id MEDIUMINT UNSIGNED, ",
++ "at varchar(20), ",
++ "count int(11), ",
++ "INDEX(owner_id, peer_name_id, peer_server_id), ",
+ "INDEX(at)"
+ ") ENGINE=InnoDB CHARACTER SET utf8;"
+ ],
+ case sql_query_internal_silent(DBRef, Query) of
+ {updated, _} ->
-+ ?MYDEBUG("Created stats table for ~p", [VHost]),
-+ lists:foreach(fun(Date) ->
-+ rebuild_stats_at_int(DBRef, VHost, Date)
-+ end, get_dates_int(DBRef, VHost)),
++ ?INFO_MSG("Created stats table for ~p", [VHost]),
++ rebuild_all_stats_int(State),
+ ok;
+ {error, Reason} ->
-+ case regexp:match(Reason, "#42S01") of
-+ {match, _, _} ->
++ case ejabberd_regexp:run(Reason, "#42S01") of
++ match ->
+ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
-+ ok;
++ CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
++ case sql_query_internal(DBRef, CheckQuery) of
++ {data, Elems} when length(Elems) == 2 ->
++ ?MYDEBUG("Stats table structure is ok", []),
++ ok;
++ _ ->
++ ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
++ case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
++ {updated, _} ->
++ ?INFO_MSG("Successfully dropped ~p", [SName]);
++ _ ->
++ ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
++ end,
++ error
++ end;
+ _ ->
+ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
+ error
+ end
+ end.
+
-+create_settings_table(DBRef, VHost) ->
++create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
+ SName = settings_table(VHost),
+ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
+ "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
+ error
+ end.
+
-+create_users_table(DBRef, VHost) ->
++create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
+ SName = users_table(VHost),
+ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
+ "username TEXT NOT NULL, ",
+ case sql_query_internal(DBRef, Query) of
+ {updated, _} ->
+ ?MYDEBUG("Created users table for ~p", [VHost]),
++ ets:new(ets_users_table(VHost), [named_table, set, public]),
++ %update_users_from_db(DBRef, VHost),
+ ok;
+ {error, _} ->
+ error
+ end.
+
-+create_servers_table(DBRef, VHost) ->
++create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
+ SName = servers_table(VHost),
+ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
+ "server TEXT NOT NULL, ",
+ case sql_query_internal(DBRef, Query) of
+ {updated, _} ->
+ ?MYDEBUG("Created servers table for ~p", [VHost]),
++ ets:new(ets_servers_table(VHost), [named_table, set, public]),
++ update_servers_from_db(DBRef, VHost),
+ ok;
+ {error, _} ->
+ error
+ end.
+
-+create_resources_table(DBRef, VHost) ->
++create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
+ RName = resources_table(VHost),
+ Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
+ "resource TEXT NOT NULL, ",
+ case sql_query_internal(DBRef, Query) of
+ {updated, _} ->
+ ?MYDEBUG("Created resources table for ~p", [VHost]),
++ ets:new(ets_resources_table(VHost), [named_table, set, public]),
+ ok;
+ {error, _} ->
+ error
+ end.
+
-+create_internals(DBRef, VHost) ->
-+ sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS `logmessage`;"]),
-+ case sql_query_internal(DBRef, [get_logmessage(VHost)]) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created logmessage for ~p", [VHost]),
++create_msg_table(DBRef, VHost, Date) ->
++ TName = messages_table(VHost, Date),
++ Query = ["CREATE TABLE ",TName," (",
++ "owner_id MEDIUMINT UNSIGNED, ",
++ "peer_name_id MEDIUMINT UNSIGNED, ",
++ "peer_server_id MEDIUMINT UNSIGNED, ",
++ "peer_resource_id MEDIUMINT(8) UNSIGNED, ",
++ "direction ENUM('to', 'from'), ",
++ "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ",
++ "subject TEXT, ",
++ "body TEXT, ",
++ "timestamp DOUBLE, ",
++ "INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ",
++ "FULLTEXT (body) "
++ ") ENGINE=MyISAM CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _MySQLRes} ->
++ ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
+ ok;
+ {error, _} ->
+ error
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% SQL internals
++% internal ets cache (users, servers, resources)
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
-+sql_transaction_internal(DBRef, Fun) ->
-+ case sql_query_internal(DBRef, ["START TRANSACTION;"]) of
-+ {updated, _} ->
-+ case catch Fun() of
-+ error = Err ->
-+ rollback_internal(DBRef, Err);
-+ {error, _} = Err ->
-+ rollback_internal(DBRef, Err);
-+ {'EXIT', _} = Err ->
-+ rollback_internal(DBRef, Err);
-+ Res ->
-+ case sql_query_internal(DBRef, ["COMMIT;"]) of
-+ {error, _} -> rollback_internal(DBRef, {commit_error});
-+ {updated, _} ->
-+ case Res of
-+ {atomic, _} -> Res;
-+ _ -> {atomic, Res}
-+ end
-+ end
-+ end;
-+ {error, _} ->
-+ {aborted, {begin_error}}
-+ end.
-+
-+% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
-+rollback_internal(DBRef, Reason) ->
-+ Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
-+ {aborted, {Reason, {rollback_result, Res}}}.
++update_servers_from_db(DBRef, VHost) ->
++ ?INFO_MSG("Reading servers from db for ~p", [VHost]),
++ SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"],
++ {data, Result} = sql_query_internal(DBRef, SQuery),
++ true = ets:delete_all_objects(ets_servers_table(VHost)),
++ true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]).
+
-+sql_query_internal(DBRef, Query) ->
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {error, Reason} ->
-+ ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
-+ {error, Reason};
-+ Rez -> Rez
-+ end.
++%update_users_from_db(DBRef, VHost) ->
++% ?INFO_MSG("Reading users from db for ~p", [VHost]),
++% SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"],
++% {data, Result} = sql_query_internal(DBRef, SQuery),
++% true = ets:delete_all_objects(ets_users_table(VHost)),
++% true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]).
+
-+sql_query_internal_silent(DBRef, Query) ->
-+ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
-+ get_result(mysql_conn:fetch(DBRef, Query, self(), ?TIMEOUT)).
++%get_user_name(DBRef, VHost, User_id) ->
++% case ets:match(ets_users_table(VHost), {'$1', User_id}) of
++% [[User]] -> User;
++% % this can be in clustered environment
++% [] ->
++% %update_users_from_db(DBRef, VHost),
++% SQuery = ["SELECT username FROM ",users_table(VHost)," ",
++% "WHERE user_id=\"",User_id,"\";"],
++% {data, [[Name]]} = sql_query_internal(DBRef, SQuery),
++% % cache {user, id} pair
++% ets:insert(ets_users_table(VHost), {Name, User_id}),
++% Name
++% end.
+
-+get_result({updated, MySQLRes}) ->
-+ {updated, mysql:get_result_affected_rows(MySQLRes)};
-+get_result({data, MySQLRes}) ->
-+ {data, mysql:get_result_rows(MySQLRes)};
-+get_result({error, "query timed out"}) ->
-+ {error, "query timed out"};
-+get_result({error, MySQLRes}) ->
-+ Reason = mysql:get_result_reason(MySQLRes),
-+ {error, Reason}.
++%get_server_name(DBRef, VHost, Server_id) ->
++% case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of
++% [[Server]] -> Server;
++ % this can be in clustered environment
++% [] ->
++% update_servers_from_db(DBRef, VHost),
++% [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}),
++% Server1
++% end.
+
-+get_user_id(DBRef, VHost, User) ->
++get_user_id_from_db(DBRef, VHost, User) ->
+ SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
+ "WHERE username=\"",User,"\";"],
+ case sql_query_internal(DBRef, SQuery) of
++ % no such user in db
+ {data, []} ->
-+ IQuery = ["INSERT INTO ",users_table(VHost)," ",
-+ "SET username=\"",User,"\";"],
-+ case sql_query_internal_silent(DBRef, IQuery) of
-+ {updated, _} ->
-+ {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery),
-+ DBIdNew;
-+ {error, Reason} ->
-+ % this can be in clustered environment
-+ {match, _, _} = regexp:match(Reason, "#23000"),
-+ ?ERROR_MSG("Duplicate key name for ~p", [User]),
-+ {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
-+ ClID
-+ end;
++ {ok, []};
+ {data, [[DBId]]} ->
-+ DBId
++ % cache {user, id} pair
++ ets:insert(ets_users_table(VHost), {User, DBId}),
++ {ok, DBId}
++ end.
++get_user_id(DBRef, VHost, User) ->
++ % Look at ets
++ case ets:match(ets_users_table(VHost), {User, '$1'}) of
++ [] ->
++ % Look at db
++ case get_user_id_from_db(DBRef, VHost, User) of
++ % no such user in db
++ {ok, []} ->
++ IQuery = ["INSERT INTO ",users_table(VHost)," ",
++ "SET username=\"",User,"\";"],
++ case sql_query_internal_silent(DBRef, IQuery) of
++ {updated, _} ->
++ {ok, NewId} = get_user_id_from_db(DBRef, VHost, User),
++ NewId;
++ {error, Reason} ->
++ % this can be in clustered environment
++ match = ejabberd_regexp:run(Reason, "#23000"),
++ ?ERROR_MSG("Duplicate key name for ~p", [User]),
++ {ok, ClID} = get_user_id_from_db(DBRef, VHost, User),
++ ClID
++ end;
++ {ok, DBId} ->
++ DBId
++ end;
++ [[EtsId]] -> EtsId
+ end.
+
-+get_logmessage(VHost) ->
-+ UName = users_table(VHost),
-+ SName = servers_table(VHost),
-+ RName = resources_table(VHost),
-+ StName = stats_table(VHost),
-+ io_lib:format("
-+CREATE PROCEDURE logmessage(tablename TEXT, atdate TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(10), msubject TEXT, mbody TEXT, mtimestamp DOUBLE)
-+BEGIN
-+ DECLARE ownerID MEDIUMINT UNSIGNED;
-+ DECLARE peer_nameID MEDIUMINT UNSIGNED;
-+ DECLARE peer_serverID MEDIUMINT UNSIGNED;
-+ DECLARE peer_resourceID MEDIUMINT UNSIGNED;
-+ DECLARE Vmtype VARCHAR(10);
-+ DECLARE Vmtimestamp DOUBLE;
-+ DECLARE Vmdirection VARCHAR(4);
-+ DECLARE Vmbody TEXT;
-+ DECLARE Vmsubject TEXT;
-+ DECLARE iq TEXT;
-+ DECLARE cq TEXT;
-+ DECLARE viewname TEXT;
-+ DECLARE notable INT;
-+ DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1;
-+
-+ SET @notable = 0;
-+ SET @ownerID = NULL;
-+ SET @peer_nameID = NULL;
-+ SET @peer_serverID = NULL;
-+ SET @peer_resourceID = NULL;
-+
-+ SET @Vmtype = mtype;
-+ SET @Vmtimestamp = mtimestamp;
-+ SET @Vmdirection = mdirection;
-+ SET @Vmbody = mbody;
-+ SET @Vmsubject = msubject;
-+
-+ SELECT user_id INTO @ownerID FROM ~s WHERE username=owner;
-+ IF @ownerID IS NULL THEN
-+ INSERT INTO ~s SET username=owner;
-+ SET @ownerID = LAST_INSERT_ID();
-+ END IF;
-+
-+ SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name;
-+ IF @peer_nameID IS NULL THEN
-+ INSERT INTO ~s SET username=peer_name;
-+ SET @peer_nameID = LAST_INSERT_ID();
-+ END IF;
-+
-+ SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server;
-+ IF @peer_serverID IS NULL THEN
-+ INSERT INTO ~s SET server=peer_server;
-+ SET @peer_serverID = LAST_INSERT_ID();
-+ END IF;
-+
-+ SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource;
-+ IF @peer_resourceID IS NULL THEN
-+ INSERT INTO ~s SET resource=peer_resource;
-+ SET @peer_resourceID = LAST_INSERT_ID();
-+ END IF;
-+
-+ SET @iq = CONCAT(\"INSERT INTO \",tablename,\" (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (@ownerID,@peer_nameID,@peer_serverID,@peer_resourceID,@Vmdirection,@Vmtype,@Vmsubject,@Vmbody,@Vmtimestamp);\");
-+ PREPARE insertmsg FROM @iq;
-+
-+ IF @notable = 1 THEN
-+ SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" (
-+ owner_id MEDIUMINT UNSIGNED,
-+ peer_name_id MEDIUMINT UNSIGNED,
-+ peer_server_id MEDIUMINT UNSIGNED,
-+ peer_resource_id MEDIUMINT(8) UNSIGNED,
-+ direction ENUM('to', 'from'),
-+ type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
-+ subject TEXT,
-+ body TEXT,
-+ timestamp DOUBLE,
-+ ext INTEGER DEFAULT NULL,
-+ INDEX owner_i (owner_id),
-+ INDEX peer_i (peer_name_id, peer_server_id),
-+ INDEX ext_i (ext),
-+ FULLTEXT (body)
-+ ) ENGINE=MyISAM CHARACTER SET utf8;\");
-+ PREPARE createtable FROM @cq;
-+ EXECUTE createtable;
-+ DEALLOCATE PREPARE createtable;
++get_server_id(DBRef, VHost, Server) ->
++ case ets:match(ets_servers_table(VHost), {Server, '$1'}) of
++ [] ->
++ IQuery = ["INSERT INTO ",servers_table(VHost)," ",
++ "SET server=\"",Server,"\";"],
++ case sql_query_internal_silent(DBRef, IQuery) of
++ {updated, _} ->
++ SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ",
++ "WHERE server=\"",Server,"\";"],
++ {data, [[Id]]} = sql_query_internal(DBRef, SQuery),
++ ets:insert(ets_servers_table(VHost), {Server, Id}),
++ Id;
++ {error, Reason} ->
++ % this can be in clustered environment
++ match = ejabberd_regexp:run(Reason, "#23000"),
++ ?ERROR_MSG("Duplicate key name for ~p", [Server]),
++ update_servers_from_db(DBRef, VHost),
++ [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}),
++ Id1
++ end;
++ [[Id]] -> Id
++ end.
+
-+ SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\");
-+ SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS
-+ SELECT owner.username AS owner_name,
-+ peer.username AS peer_name,
-+ servers.server AS peer_server,
-+ resources.resource AS peer_resource,
-+ messages.direction,
-+ messages.type,
-+ messages.subject,
-+ messages.body,
-+ messages.timestamp
-+ FROM
-+ ~s owner,
-+ ~s peer,
-+ ~s servers,
-+ ~s resources,
-+ \", tablename,\" messages
-+ WHERE
-+ owner.user_id=messages.owner_id and
-+ peer.user_id=messages.peer_name_id and
-+ servers.server_id=messages.peer_server_id and
-+ resources.resource_id=messages.peer_resource_id
-+ ORDER BY messages.timestamp;\");
-+ PREPARE createview FROM @cq;
-+ EXECUTE createview;
-+ DEALLOCATE PREPARE createview;
++get_resource_id_from_db(DBRef, VHost, Resource) ->
++ SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ",
++ "WHERE resource=\"",ejabberd_odbc:escape(Resource),"\";"],
++ case sql_query_internal(DBRef, SQuery) of
++ % no such resource in db
++ {data, []} ->
++ {ok, []};
++ {data, [[DBId]]} ->
++ % cache {resource, id} pair
++ ets:insert(ets_resources_table(VHost), {Resource, DBId}),
++ {ok, DBId}
++ end.
++get_resource_id(DBRef, VHost, Resource) ->
++ % Look at ets
++ case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of
++ [] ->
++ % Look at db
++ case get_resource_id_from_db(DBRef, VHost, Resource) of
++ % no such resource in db
++ {ok, []} ->
++ IQuery = ["INSERT INTO ",resources_table(VHost)," ",
++ "SET resource=\"",ejabberd_odbc:escape(Resource),"\";"],
++ case sql_query_internal_silent(DBRef, IQuery) of
++ {updated, _} ->
++ {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource),
++ NewId;
++ {error, Reason} ->
++ % this can be in clustered environment
++ match = ejabberd_regexp:run(Reason, "#23000"),
++ ?ERROR_MSG("Duplicate key name for ~p", [Resource]),
++ {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource),
++ ClID
++ end;
++ {ok, DBId} ->
++ DBId
++ end;
++ [[EtsId]] -> EtsId
++ end.
+
-+ SET @notable = 0;
-+ PREPARE insertmsg FROM @iq;
-+ EXECUTE insertmsg;
-+ ELSEIF @notable = 0 THEN
-+ EXECUTE insertmsg;
-+ END IF;
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% SQL internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++sql_query_internal(DBRef, Query) ->
++ case sql_query_internal_silent(DBRef, Query) of
++ {error, Reason} ->
++ ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
++ {error, Reason};
++ Rez -> Rez
++ end.
+
-+ DEALLOCATE PREPARE insertmsg;
++sql_query_internal_silent(DBRef, Query) ->
++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
++ get_result(mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
+
-+ IF @notable = 0 THEN
-+ UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND at=atdate;
-+ IF ROW_COUNT() = 0 THEN
-+ INSERT INTO ~s (owner_id, at, count) VALUES (@ownerID, atdate, 1);
-+ END IF;
-+ END IF;
-+END;", [UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
---- src/mod_logdb_pgsql.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb_pgsql.erl Sun Nov 18 20:53:55 2007
-@@ -0,0 +1,911 @@
++get_result({updated, MySQLRes}) ->
++ {updated, mysql:get_result_affected_rows(MySQLRes)};
++get_result({data, MySQLRes}) ->
++ {data, mysql:get_result_rows(MySQLRes)};
++get_result({error, "query timed out"}) ->
++ {error, "query timed out"};
++get_result({error, MySQLRes}) ->
++ Reason = mysql:get_result_reason(MySQLRes),
++ {error, Reason}.
+diff --git src/mod_logdb_mysql5.erl src/mod_logdb_mysql5.erl
+new file mode 100644
+index 0000000..59efc77
+--- /dev/null
++++ src/mod_logdb_mysql5.erl
+@@ -0,0 +1,979 @@
+%%%----------------------------------------------------------------------
-+%%% File : mod_logdb_pgsql.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose : Posgresql backend for mod_logdb
++%%% File : mod_logdb_mysql5.erl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose : MySQL 5 backend for mod_logdb
+%%% Version : trunk
-+%%% Id : $Id$
++%%% Id : $Id: mod_logdb_mysql5.erl 1360 2009-07-30 06:00:14Z malik $
+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
+%%%----------------------------------------------------------------------
+
-+-module(mod_logdb_pgsql).
++-module(mod_logdb_mysql5).
+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
+
+-include("mod_logdb.hrl").
+-include("ejabberd.hrl").
+ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
+ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
+ get_dates/1,
-+ get_users_settings/1, get_user_settings/2, set_user_settings/3]).
++ get_users_settings/1, get_user_settings/2, set_user_settings/3,
++ drop_user/2]).
+
+% gen_server call timeout
-+-define(CALL_TIMEOUT, 60000).
-+-define(TIMEOUT, 60000).
-+-define(PROCNAME, mod_logdb_pgsql).
++-define(CALL_TIMEOUT, 30000).
++-define(MYSQL_TIMEOUT, 60000).
++-define(INDEX_SIZE, integer_to_list(170)).
++-define(PROCNAME, mod_logdb_mysql5).
+
+-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
+ list_to_string/1, string_to_list/1,
+ convert_timestamp_brief/1]).
+
-+-record(state, {dbref, vhost, schema}).
++-record(state, {dbref, vhost, server, port, db, user, password}).
+
+% replace "." with "_"
+escape_vhost(VHost) -> lists:map(fun(46) -> 95;
+ (A) -> A
+ end, VHost).
-+
-+prefix(Schema) ->
-+ Schema ++ ".\"" ++ "logdb_".
++prefix() ->
++ "`logdb_".
+
+suffix(VHost) ->
-+ "_" ++ escape_vhost(VHost) ++ "\"".
++ "_" ++ escape_vhost(VHost) ++ "`".
+
-+messages_table(VHost, Schema, Date) ->
-+ prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost).
++messages_table(VHost, Date) ->
++ prefix() ++ "messages_" ++ Date ++ suffix(VHost).
+
+% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
-+view_table(VHost, Schema, Date) ->
-+ Table = messages_table(VHost, Schema, Date),
-+ TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3),
-+ lists:append([Schema, ".\"v_", TablewoS, "\""]).
++view_table(VHost, Date) ->
++ Table = messages_table(VHost, Date),
++ TablewoQ = lists:sublist(Table, 2, length(Table) - 2),
++ lists:append(["`v_", TablewoQ, "`"]).
+
-+stats_table(VHost, Schema) ->
-+ prefix(Schema) ++ "stats" ++ suffix(VHost).
++stats_table(VHost) ->
++ prefix() ++ "stats" ++ suffix(VHost).
+
-+settings_table(VHost, Schema) ->
-+ prefix(Schema) ++ "settings" ++ suffix(VHost).
++temp_table(VHost) ->
++ prefix() ++ "temp" ++ suffix(VHost).
+
-+users_table(VHost, Schema) ->
-+ prefix(Schema) ++ "users" ++ suffix(VHost).
-+servers_table(VHost, Schema) ->
-+ prefix(Schema) ++ "servers" ++ suffix(VHost).
-+resources_table(VHost, Schema) ->
-+ prefix(Schema) ++ "resources" ++ suffix(VHost).
++settings_table(VHost) ->
++ prefix() ++ "settings" ++ suffix(VHost).
++
++users_table(VHost) ->
++ prefix() ++ "users" ++ suffix(VHost).
++servers_table(VHost) ->
++ prefix() ++ "servers" ++ suffix(VHost).
++resources_table(VHost) ->
++ prefix() ++ "resources" ++ suffix(VHost).
++
++logmessage_name(VHost) ->
++ prefix() ++ "logmessage" ++ suffix(VHost).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+init([VHost, Opts]) ->
++ crypto:start(),
++
+ Server = gen_mod:get_opt(server, Opts, "localhost"),
-+ DB = gen_mod:get_opt(db, Opts, "ejabberd_logdb"),
++ Port = gen_mod:get_opt(port, Opts, 3306),
++ DB = gen_mod:get_opt(db, Opts, "logdb"),
+ User = gen_mod:get_opt(user, Opts, "root"),
-+ Port = gen_mod:get_opt(port, Opts, 5432),
+ Password = gen_mod:get_opt(password, Opts, ""),
-+ Schema = gen_mod:get_opt(schema, Opts, "public"),
+
-+ case catch pgsql:connect(Server, DB, User, Password, Port) of
++ St = #state{vhost=VHost,
++ server=Server, port=Port, db=DB,
++ user=User, password=Password},
++
++ case open_mysql_connection(St) of
+ {ok, DBRef} ->
-+ {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]),
-+ ok = create_internals(DBRef, VHost, Schema),
-+ ok = create_stats_table(DBRef, VHost, Schema),
-+ ok = create_settings_table(DBRef, VHost, Schema),
-+ ok = create_users_table(DBRef, VHost, Schema),
-+ ok = create_servers_table(DBRef, VHost, Schema),
-+ ok = create_resources_table(DBRef, VHost, Schema),
++ State = St#state{dbref=DBRef},
++ ok = create_internals(State),
++ ok = create_stats_table(State),
++ ok = create_settings_table(State),
++ ok = create_users_table(State),
++ ok = create_servers_table(State),
++ ok = create_resources_table(State),
+ erlang:monitor(process, DBRef),
-+ {ok, #state{dbref=DBRef, vhost=VHost, schema=Schema}};
-+ % this does not work
++ {ok, State};
+ {error, Reason} ->
-+ ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]),
-+ {stop, db_connection_failed};
-+ % and this too, becouse pgsql_conn do exit() which can not be catched
-+ {'EXIT', Rez} ->
-+ ?ERROR_MSG("Rez: ~p~n", [Rez]),
++ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
+ {stop, db_connection_failed}
+ end.
+
-+handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ Date = convert_timestamp_brief(Msg#msg.timestamp),
-+ TableName = messages_table(VHost, Schema, Date),
++open_mysql_connection(#state{server=Server, port=Port, db=DB,
++ user=DBUser, password=Password} = _State) ->
++ LogFun = fun(debug, _Format, _Argument) ->
++ %?MYDEBUG(Format, Argument);
++ ok;
++ (error, Format, Argument) ->
++ ?ERROR_MSG(Format, Argument);
++ (Level, Format, Argument) ->
++ ?MYDEBUG("MySQL (~p)~n", [Level]),
++ ?MYDEBUG(Format, Argument)
++ end,
++ ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
++ mysql_conn:start(Server, Port, DBUser, Password, DB, [65536, 131072], LogFun).
+
-+ Query = [ "SELECT logmessage "
-+ "('", TableName, "',",
-+ "'", Date, "',",
-+ "'", Msg#msg.owner_name, "',",
-+ "'", Msg#msg.peer_name, "',",
-+ "'", Msg#msg.peer_server, "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',",
-+ "'", atom_to_list(Msg#msg.direction), "',",
-+ "'", Msg#msg.type, "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
-+ "'", ejabberd_odbc:escape(Msg#msg.body), "',",
-+ "'", Msg#msg.timestamp, "');"],
++close_mysql_connection(DBRef) ->
++ ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
++ mysql_conn:stop(DBRef).
+
-+ Reply =
-+ case sql_query_internal_silent(DBRef, Query) of
-+ % TODO: change this
-+ {data, [{"0"}]} ->
-+ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
-+ Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
-+ ok;
-+ {error, _Reason} ->
-+ error
-+ end,
-+ {reply, Reply, State};
-+handle_call({rebuild_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ ok = delete_nonexistent_stats(DBRef, VHost, Schema),
-+ Reply =
-+ lists:foreach(fun(Date) ->
-+ catch rebuild_stats_at_int(DBRef, VHost, Schema, Date)
-+ end, get_dates_int(DBRef, VHost)),
-+ {reply, Reply, State};
-+handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date),
++handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ Reply = rebuild_stats_at_int(DBRef, VHost, Date),
+ {reply, Reply, State};
+handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
+ {reply, error, State};
-+handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
-+ ["'",Timestamp,"'",","]
++ ["\"",Timestamp,"\"",","]
+ end, Msgs),
+
+ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
+
-+ Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
++ Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
+ "WHERE timestamp IN (", Temp1],
+
+ Reply =
+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ rebuild_stats_at_int(DBRef, VHost, Schema, Date);
-+ {error, _} ->
-+ error
-+ end,
-+ {reply, Reply, State};
-+handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
-+ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
-+ Reply =
-+ case sql_query_internal(DBRef, DQuery) of
-+ {updated, _} ->
-+ rebuild_stats_at_int(DBRef, VHost, Schema, Date);
++ {updated, Aff} ->
++ ?MYDEBUG("Aff=~p", [Aff]),
++ rebuild_stats_at_int(DBRef, VHost, Date);
+ {error, _} ->
+ error
+ end,
+ {reply, Reply, State};
-+handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ % TODO
-+ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
++ ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
++ {reply, ok, State};
++handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ Fun = fun() ->
++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]),
++ TQuery = ["DELETE FROM ",stats_table(VHost)," "
++ "WHERE at=\"",Date,"\";"],
++ {updated, _} = sql_query_internal(DBRef, TQuery),
++ VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"],
++ {updated, _} = sql_query_internal(DBRef, VQuery),
++ ok
++ end,
+ Reply =
-+ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date),";"]) of
-+ {updated, _} ->
-+ Query = ["DELETE FROM ",stats_table(VHost, Schema)," "
-+ "WHERE at='",Date,"';"],
-+ case sql_query_internal(DBRef, Query) of
-+ {updated, _} ->
-+ ok;
-+ {error, _} ->
-+ error
-+ end;
-+ {error, _} ->
++ case catch apply(Fun, []) of
++ ok ->
++ ok;
++ {'EXIT', _} ->
+ error
+ end,
+ {reply, Reply, State};
-+handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ SName = stats_table(VHost, Schema),
++handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ SName = stats_table(VHost),
+ Query = ["SELECT at, sum(count) ",
+ "FROM ",SName," ",
+ "GROUP BY at ",
+ ],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
-+ {data, Recs} ->
-+ {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]};
++ {data, Result} ->
++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
+ {error, Reason} ->
+ % TODO: Duplicate error message ?
+ {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ SName = stats_table(VHost, Schema),
-+ Query = ["SELECT username, count ",
++handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ SName = stats_table(VHost),
++ Query = ["SELECT username, sum(count) as allcount ",
+ "FROM ",SName," ",
-+ "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id "
-+ "WHERE at='",Date,"';"
++ "JOIN ",users_table(VHost)," ON owner_id=user_id "
++ "WHERE at=\"",Date,"\" ",
++ "GROUP BY username ",
++ "ORDER BY allcount DESC;"
+ ],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
-+ {data, Recs} ->
-+ RFun = fun({User, Count}) ->
-+ {User, list_to_integer(Count)}
-+ end,
-+ {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))};
++ {data, Result} ->
++ {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
+ {error, Reason} ->
-+ % TODO:
+ {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ SName = stats_table(VHost, Schema),
-+ Query = ["SELECT at, count ",
-+ "FROM ",SName," ",
-+ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ",
-+ "ORDER BY DATE(at) DESC;"
-+ ],
-+ Reply =
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Recs} ->
-+ {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]};
-+ {error, Result} ->
-+ {error, Result}
-+ end,
-+ {reply, Reply, State};
-+handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ {reply, get_user_stats_int(DBRef, User, VHost), State};
++handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Query = ["SELECT peer_name,",
+ "peer_server,",
+ "peer_resource,",
+ "subject,"
+ "body,"
+ "timestamp "
-+ "FROM ",view_table(VHost, Schema, Date)," "
-+ "WHERE owner_name='",User,"';"],
++ "FROM ",view_table(VHost, Date)," "
++ "WHERE owner_name=\"",User,"\";"],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
-+ {data, Recs} ->
-+ Fun = fun({Peer_name, Peer_server, Peer_resource,
++ {data, Result} ->
++ Fun = fun([Peer_name, Peer_server, Peer_resource,
+ Direction,
+ Type,
+ Subject, Body,
-+ Timestamp}) ->
++ Timestamp]) ->
+ #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
+ direction=list_to_atom(Direction),
+ type=Type,
+ subject=Subject, body=Body,
+ timestamp=Timestamp}
+ end,
-+ {ok, lists:map(Fun, Recs)};
++ {ok, lists:map(Fun, Result)};
+ {error, Reason} ->
+ {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ SName = stats_table(VHost, Schema),
++handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ SName = stats_table(VHost),
+ Query = ["SELECT at ",
+ "FROM ",SName," ",
+ "GROUP BY at ",
-+ "ORDER BY at DESC;"
++ "ORDER BY DATE(at) DESC;"
+ ],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
+ {data, Result} ->
-+ [ Date || {Date} <- Result ];
++ [ Date || [Date] <- Result ];
+ {error, Reason} ->
+ {error, Reason}
+ end,
+ {reply, Reply, State};
-+handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
+ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
-+ "FROM ",settings_table(VHost, Schema)," ",
-+ "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"],
++ "FROM ",settings_table(VHost)," ",
++ "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
+ Reply =
+ case sql_query_internal(DBRef, Query) of
-+ {data, Recs} ->
-+ {ok, [#user_settings{owner_name=Owner,
-+ dolog_default=list_to_bool(DoLogDef),
-+ dolog_list=string_to_list(DoLogL),
-+ donotlog_list=string_to_list(DoNotLogL)
-+ } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]};
-+ {error, Reason} ->
-+ {error, Reason}
++ {data, Result} ->
++ {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
++ #user_settings{owner_name=Owner,
++ dolog_default=list_to_bool(DoLogDef),
++ dolog_list=string_to_list(DoLogL),
++ donotlog_list=string_to_list(DoNotLogL)
++ }
++ end, Result)};
++ {error, _} ->
++ error
+ end,
+ {reply, Reply, State};
-+handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ Query = ["SELECT dolog_default,dolog_list,donotlog_list ",
-+ "FROM ",settings_table(VHost, Schema)," ",
-+ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
++handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
++ Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
+ Reply =
-+ case sql_query_internal_silent(DBRef, Query) of
++ case sql_query_internal(DBRef, Query) of
+ {data, []} ->
+ {ok, []};
-+ {data, [{DoLogDef, DoLogL, DoNotLogL}]} ->
-+ {ok, #user_settings{owner_name=User,
++ {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
++ {ok, #user_settings{owner_name=Owner,
+ dolog_default=list_to_bool(DoLogDef),
+ dolog_list=string_to_list(DoLogL),
+ donotlog_list=string_to_list(DoNotLogL)}};
-+ {error, Reason} ->
-+ ?ERROR_MSG("Failed to get_user_settings for ~p@~p: ~p", [User, VHost, Reason]),
++ {error, _} ->
+ error
+ end,
+ {reply, Reply, State};
+handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
+ dolog_list=DoLogL,
+ donotlog_list=DoNotLogL}},
-+ _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
-+ User_id = get_user_id(DBRef, VHost, Schema, User),
-+ Query = ["UPDATE ",settings_table(VHost, Schema)," ",
++ _From, #state{dbref=DBRef, vhost=VHost} = State) ->
++ User_id = get_user_id(DBRef, VHost, User),
++ Query = ["UPDATE ",settings_table(VHost)," ",
+ "SET dolog_default=",bool_to_list(DoLogDef),", ",
+ "dolog_list='",list_to_string(DoLogL),"', ",
+ "donotlog_list='",list_to_string(DoNotLogL),"' ",
+ Reply =
+ case sql_query_internal(DBRef, Query) of
+ {updated, 0} ->
-+ IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ",
++ IQuery = ["INSERT INTO ",settings_table(VHost)," ",
+ "(owner_id, dolog_default, dolog_list, donotlog_list) ",
+ "VALUES ",
-+ "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
-+ case sql_query_internal(DBRef, IQuery) of
-+ {updated, 1} ->
++ "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
++ case sql_query_internal_silent(DBRef, IQuery) of
++ {updated, _} ->
+ ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
+ ok;
-+ {error, _} ->
-+ error
++ {error, Reason} ->
++ case ejabberd_regexp:run(Reason, "#23000") of
++ % Already exists
++ match ->
++ ok;
++ _ ->
++ ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
++ error
++ end
+ end;
+ {updated, 1} ->
+ ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
+ error
+ end,
+ {reply, Reply, State};
-+handle_call({stop}, _From, State) ->
-+ ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]),
++handle_call({stop}, _From, #state{vhost=VHost}=State) ->
++ ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]),
+ {stop, normal, ok, State};
+handle_call(Msg, _From, State) ->
+ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
+ {noreply, State}.
+
++handle_cast({log_message, Msg}, #state{dbref=DBRef, vhost=VHost}=State) ->
++ Fun = fun() ->
++ Date = convert_timestamp_brief(Msg#msg.timestamp),
++ TableName = messages_table(VHost, Date),
++
++ Query = [ "CALL ",logmessage_name(VHost)," "
++ "('", TableName, "',",
++ "'", Date, "',",
++ "'", Msg#msg.owner_name, "',",
++ "'", Msg#msg.peer_name, "',",
++ "'", Msg#msg.peer_server, "',",
++ "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',",
++ "'", atom_to_list(Msg#msg.direction), "',",
++ "'", Msg#msg.type, "',",
++ "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
++ "'", ejabberd_odbc:escape(Msg#msg.body), "',",
++ "'", Msg#msg.timestamp, "');"],
++
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
++ Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
++ ok;
++ {error, _Reason} ->
++ error
++ end
++ end,
++ spawn(Fun),
++ {noreply, State};
++handle_cast({rebuild_stats}, State) ->
++ rebuild_all_stats_int(State),
++ {noreply, State};
++handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
++ Fun = fun() ->
++ {ok, DBRef} = open_mysql_connection(State),
++ {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
++ MDResult = lists:map(fun({Date, _}) ->
++ delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
++ end, Dates),
++ StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
++ SDResult = delete_user_settings_int(DBRef, User, VHost),
++ case lists:all(fun(Result) when Result == ok ->
++ true;
++ (Result) when Result == error ->
++ false
++ end, lists:append([MDResult, [StDResult], [SDResult]])) of
++ true ->
++ ?INFO_MSG("Removed ~s@~s", [User, VHost]);
++ false ->
++ ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
++ end,
++ close_mysql_connection(DBRef)
++ end,
++ spawn(Fun),
++ {noreply, State};
+handle_cast(Msg, State) ->
+ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
+ {noreply, State}.
+ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
+ {noreply, State}.
+
-+terminate(_Reason, _State) ->
++terminate(_Reason, #state{dbref=DBRef}=_State) ->
++ close_mysql_connection(DBRef),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+log_message(VHost, Msg) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
++ gen_server:cast(Proc, {log_message, Msg}).
+rebuild_stats(VHost) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
-+ gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
++ gen_server:cast(Proc, {rebuild_stats}).
+rebuild_stats_at(VHost, Date) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
+set_user_settings(User, VHost, Set) ->
+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
+ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
++drop_user(User, VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {drop_user, User}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+get_dates_int(DBRef, VHost) ->
-+ Query = ["SELECT n.nspname as \"Schema\",
-+ c.relname as \"Name\",
-+ CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as \"Type\",
-+ r.rolname as \"Owner\"
-+ FROM pg_catalog.pg_class c
-+ JOIN pg_catalog.pg_roles r ON r.oid = c.relowner
-+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
-+ WHERE c.relkind IN ('r','')
-+ AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
-+ AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$'
-+ AND pg_catalog.pg_table_is_visible(c.oid)
-+ ORDER BY 1,2;"],
-+ case sql_query_internal(DBRef, Query) of
-+ {data, Recs} ->
-+ lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) ->
-+ case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of
-+ {match, S, E} ->
-+ lists:append(Dates, [lists:sublist(Table,S,E)]);
-+ nomatch ->
-+ Dates
-+ end
-+ end, [], Recs);
++ case sql_query_internal(DBRef, ["SHOW TABLES"]) of
++ {data, Tables} ->
++ lists:foldl(fun([Table], Dates) ->
++ Reg = lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
++ case re:run(Table, Reg) of
++ {match, [{1, _}]} ->
++ case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
++ {match, [{S, E}]} ->
++ lists:append(Dates, [lists:sublist(Table,S,E)]);
++ nomatch ->
++ Dates
++ end;
++ _ ->
++ Dates
++ end
++ end, [], Tables);
+ {error, _} ->
+ []
+ end.
+
-+rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
-+ Table = messages_table(VHost, Schema, Date),
-+ STable = stats_table(VHost, Schema),
-+
-+ Fun =
-+ fun() ->
-+ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]),
-+ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
-+
-+ DQuery = [ "DELETE FROM ",STable," ",
-+ "WHERE at='",Date,"';"],
++rebuild_all_stats_int(#state{vhost=VHost}=State) ->
++ Fun = fun() ->
++ {ok, DBRef} = open_mysql_connection(State),
++ ok = delete_nonexistent_stats(DBRef, VHost),
++ case lists:filter(fun(Date) ->
++ case catch rebuild_stats_at_int(DBRef, VHost, Date) of
++ ok -> false;
++ error -> true;
++ {'EXIT', _} -> true
++ end
++ end, get_dates_int(DBRef, VHost)) of
++ [] -> ok;
++ FTables ->
++ ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
++ error
++ end,
++ close_mysql_connection(DBRef)
++ end,
++ spawn(Fun).
+
-+ {updated, _} = sql_query_internal(DBRef, DQuery),
++rebuild_stats_at_int(DBRef, VHost, Date) ->
++ TempTable = temp_table(VHost),
++ Fun = fun() ->
++ Table = messages_table(VHost, Date),
++ STable = stats_table(VHost),
+
-+ SQuery = ["INSERT INTO ",STable," ",
-+ "(owner_id,at,count) ",
-+ "SELECT owner_id,'",Date,"'",",count(*) ",
-+ "FROM ",Table," GROUP BY owner_id;"],
++ DQuery = [ "DELETE FROM ",STable," ",
++ "WHERE at='",Date,"';"],
+
-+ case sql_query_internal(DBRef, SQuery) of
-+ {updated, 0} ->
-+ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
-+ ok;
-+ {updated, _} -> ok;
-+ {error, _} -> error
-+ end
-+ end,
++ ok = create_temp_table(DBRef, TempTable),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
++ SQuery = ["INSERT INTO ",TempTable," ",
++ "(owner_id,peer_name_id,peer_server_id,at,count) ",
++ "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
++ "FROM ",Table," WHERE ext is NULL GROUP BY owner_id,peer_name_id,peer_server_id;"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, 0} ->
++ Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
++ case Count of
++ {data, [["0"]]} ->
++ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"]),
++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]),
++ {updated, _} = sql_query_internal(DBRef, DQuery),
++ ok;
++ _ ->
++ ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
++ error
++ end;
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
++ {updated, _} = sql_query_internal(DBRef, DQuery),
++ SQuery1 = ["INSERT INTO ",STable," ",
++ "(owner_id,peer_name_id,peer_server_id,at,count) ",
++ "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
++ "FROM ",TempTable,";"],
++ case sql_query_internal(DBRef, SQuery1) of
++ {updated, _} -> ok;
++ {error, _} -> error
++ end;
++ {error, _} -> error
++ end
++ end,
+
-+ case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, _} ->
-+ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
-+ ok;
-+ {aborted, _} ->
-+ error
-+ end.
++ case catch apply(Fun, []) of
++ ok ->
++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
++ ok;
++ error ->
++ error;
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
++ error
++ end,
++ sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
++ sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
++ ok.
+
-+delete_nonexistent_stats(DBRef, VHost, Schema) ->
++delete_nonexistent_stats(DBRef, VHost) ->
+ Dates = get_dates_int(DBRef, VHost),
-+ STable = stats_table(VHost, Schema),
++ STable = stats_table(VHost),
+
+ Temp = lists:flatmap(fun(Date) ->
-+ ["'",Date,"'",","]
++ ["\"",Date,"\"",","]
+ end, Dates),
++ case Temp of
++ [] ->
++ ok;
++ _ ->
++ % replace last "," with ");"
++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
++ Query = ["DELETE FROM ",STable," ",
++ "WHERE at NOT IN (", Temp1],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end
++ end.
+
-+ Temp1 = case Temp of
-+ [] ->
-+ ["''"];
-+ _ ->
-+ % replace last "," with ");"
-+ lists:append([lists:sublist(Temp, length(Temp)-1), ");"])
-+ end,
++get_user_stats_int(DBRef, User, VHost) ->
++ SName = stats_table(VHost),
++ UName = users_table(VHost),
++ Query = ["SELECT stats.at, sum(stats.count) ",
++ "FROM ",UName," AS users ",
++ "JOIN ",SName," AS stats ON owner_id=user_id "
++ "WHERE users.username=\"",User,"\" ",
++ "GROUP BY stats.at "
++ "ORDER BY DATE(stats.at) DESC;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {data, Result} ->
++ {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
++ {error, Result} ->
++ {error, Result}
++ end.
++
++delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
++ DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ case sql_query_internal(DBRef, DQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
++ ok;
++ {error, _} ->
++ error
++ end.
++
++delete_all_stats_by_user_int(DBRef, User, VHost) ->
++ SQuery = ["DELETE FROM ",stats_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
++ ok;
++ {error, _} -> error
++ end.
+
-+ Query = ["DELETE FROM ",STable," ",
-+ "WHERE at NOT IN (", Temp1],
++delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
++ SQuery = ["DELETE FROM ",stats_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
++ "AND at=\"",Date,"\";"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
++ ok;
++ {error, _} -> error
++ end.
+
++delete_user_settings_int(DBRef, User, VHost) ->
++ Query = ["DELETE FROM ",settings_table(VHost)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
+ case sql_query_internal(DBRef, Query) of
+ {updated, _} ->
++ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
+ ok;
-+ {error, _} ->
++ {error, Reason} ->
++ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
+ error
+ end.
+
+% tables internals
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+create_stats_table(DBRef, VHost, Schema) ->
-+ SName = stats_table(VHost, Schema),
-+
-+ Fun =
-+ fun() ->
-+ Query = ["CREATE TABLE ",SName," (",
-+ "owner_id INTEGER, ",
-+ "at VARCHAR(20), ",
-+ "count integer",
-+ ");"
-+ ],
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_owner_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id);"]),
-+ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]),
-+ created;
-+ {error, Reason} ->
-+ case lists:keysearch(code, 1, Reason) of
-+ {value, {code, "42P07"}} ->
-+ exists;
-+ _ ->
-+ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
-+ error
-+ end
-+ end
-+ end,
-+ case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, created} ->
-+ ?MYDEBUG("Created stats table for ~p", [VHost]),
-+ lists:foreach(fun(Date) ->
-+ rebuild_stats_at_int(DBRef, VHost, Schema, Date)
-+ end, get_dates_int(DBRef, VHost));
-+ {atomic, exists} ->
-+ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
-+ ok;
-+ {error, _} -> error
++create_temp_table(DBRef, Name) ->
++ Query = ["CREATE TABLE ",Name," (",
++ "owner_id MEDIUMINT UNSIGNED, ",
++ "peer_name_id MEDIUMINT UNSIGNED, ",
++ "peer_server_id MEDIUMINT UNSIGNED, ",
++ "at VARCHAR(11), ",
++ "count INT(11) ",
++ ") ENGINE=MyISAM CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} -> ok;
++ {error, _Reason} -> error
+ end.
+
-+create_settings_table(DBRef, VHost, Schema) ->
-+ SName = settings_table(VHost, Schema),
++create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
++ SName = stats_table(VHost),
+ Query = ["CREATE TABLE ",SName," (",
-+ "owner_id INTEGER PRIMARY KEY, ",
-+ "dolog_default BOOLEAN, ",
-+ "dolog_list TEXT DEFAULT '', ",
-+ "donotlog_list TEXT DEFAULT ''",
-+ ");"
++ "owner_id MEDIUMINT UNSIGNED, ",
++ "peer_name_id MEDIUMINT UNSIGNED, ",
++ "peer_server_id MEDIUMINT UNSIGNED, ",
++ "at VARCHAR(11), ",
++ "count INT(11), ",
++ "ext INTEGER DEFAULT NULL, "
++ "INDEX ext_i (ext), "
++ "INDEX(owner_id,peer_name_id,peer_server_id), ",
++ "INDEX(at) ",
++ ") ENGINE=MyISAM CHARACTER SET utf8;"
+ ],
+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ ?MYDEBUG("Created settings table for ~p", [VHost]),
-+ ok;
-+ {error, Reason} ->
-+ case lists:keysearch(code, 1, Reason) of
-+ {value, {code, "42P07"}} ->
-+ ?MYDEBUG("Settings table for ~p already exists", [VHost]),
-+ ok;
-+ _ ->
-+ ?ERROR_MSG("Failed to create settings table for ~p: ~p", [VHost, Reason]),
-+ error
-+ end
-+ end.
-+
-+create_users_table(DBRef, VHost, Schema) ->
-+ SName = users_table(VHost, Schema),
-+
-+ Fun =
-+ fun() ->
-+ Query = ["CREATE TABLE ",SName," (",
-+ "username TEXT UNIQUE, ",
-+ "user_id SERIAL PRIMARY KEY",
-+ ");"
-+ ],
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]),
-+ created;
-+ {error, Reason} ->
-+ case lists:keysearch(code, 1, Reason) of
-+ {value, {code, "42P07"}} ->
-+ exists;
-+ _ ->
-+ ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]),
-+ error
-+ end
-+ end
-+ end,
-+ case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, created} ->
-+ ?MYDEBUG("Created users table for ~p", [VHost]),
-+ ok;
-+ {atomic, exists} ->
-+ ?MYDEBUG("Users table for ~p already exists", [VHost]),
-+ ok;
-+ {aborted, _} -> error
++ {updated, _} ->
++ ?MYDEBUG("Created stats table for ~p", [VHost]),
++ rebuild_all_stats_int(State),
++ ok;
++ {error, Reason} ->
++ case ejabberd_regexp:run(Reason, "#42S01") of
++ match ->
++ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
++ CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
++ case sql_query_internal(DBRef, CheckQuery) of
++ {data, Elems} when length(Elems) == 2 ->
++ ?MYDEBUG("Stats table structure is ok", []),
++ ok;
++ _ ->
++ ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
++ case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
++ {updated, _} ->
++ ?INFO_MSG("Successfully dropped ~p", [SName]);
++ _ ->
++ ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
++ end,
++ error
++ end;
++ _ ->
++ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
++ error
++ end
+ end.
+
-+create_servers_table(DBRef, VHost, Schema) ->
-+ SName = servers_table(VHost, Schema),
++create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
++ SName = settings_table(VHost),
++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
++ "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
++ "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
++ "dolog_list TEXT, ",
++ "donotlog_list TEXT ",
++ ") ENGINE=InnoDB CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?MYDEBUG("Created settings table for ~p", [VHost]),
++ ok;
++ {error, _} ->
++ error
++ end.
+
-+ Fun =
-+ fun() ->
-+ Query = ["CREATE TABLE ",SName," (",
-+ "server TEXT UNIQUE, ",
-+ "server_id SERIAL PRIMARY KEY",
-+ ");"
-+ ],
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]),
-+ created;
-+ {error, Reason} ->
-+ case lists:keysearch(code, 1, Reason) of
-+ {value, {code, "42P07"}} ->
-+ exists;
-+ _ ->
-+ ?ERROR_MSG("Failed to create servers table for ~p: ~p", [VHost, Reason]),
-+ error
-+ end
-+ end
-+ end,
-+ case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, created} ->
-+ ?MYDEBUG("Created servers table for ~p", [VHost]),
++create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
++ SName = users_table(VHost),
++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
++ "username TEXT NOT NULL, ",
++ "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
++ "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
++ ") ENGINE=InnoDB CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?MYDEBUG("Created users table for ~p", [VHost]),
+ ok;
-+ {atomic, exists} ->
-+ ?MYDEBUG("Servers table for ~p already exists", [VHost]),
++ {error, _} ->
++ error
++ end.
++
++create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
++ SName = servers_table(VHost),
++ Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
++ "server TEXT NOT NULL, ",
++ "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
++ "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
++ ") ENGINE=InnoDB CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?MYDEBUG("Created servers table for ~p", [VHost]),
+ ok;
-+ {aborted, _} -> error
++ {error, _} ->
++ error
+ end.
+
-+create_resources_table(DBRef, VHost, Schema) ->
-+ RName = resources_table(VHost, Schema),
-+ Fun = fun() ->
-+ Query = ["CREATE TABLE ",RName," (",
-+ "resource TEXT UNIQUE, ",
-+ "resource_id SERIAL PRIMARY KEY",
-+ ");"
-+ ],
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {updated, _} ->
-+ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]),
-+ created;
-+ {error, Reason} ->
-+ case lists:keysearch(code, 1, Reason) of
-+ {value, {code, "42P07"}} ->
-+ exists;
-+ _ ->
-+ ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]),
-+ error
-+ end
-+ end
-+ end,
-+ case sql_transaction_internal(DBRef, Fun) of
-+ {atomic, created} ->
-+ ?MYDEBUG("Created resources table for ~p", [VHost]),
-+ ok;
-+ {atomic, exists} ->
-+ ?MYDEBUG("Resources table for ~p already exists", [VHost]),
-+ ok;
-+ {aborted, _} -> error
++create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
++ RName = resources_table(VHost),
++ Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
++ "resource TEXT NOT NULL, ",
++ "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
++ "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
++ ") ENGINE=InnoDB CHARACTER SET utf8;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?MYDEBUG("Created resources table for ~p", [VHost]),
++ ok;
++ {error, _} ->
++ error
+ end.
+
-+create_internals(DBRef, VHost, Schema) ->
-+ case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
++create_internals(#state{dbref=DBRef, vhost=VHost}) ->
++ sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS ",logmessage_name(VHost),";"]),
++ case sql_query_internal(DBRef, [get_logmessage(VHost)]) of
+ {updated, _} ->
+ ?MYDEBUG("Created logmessage for ~p", [VHost]),
+ ok;
+ error
+ end.
+
-+get_user_id(DBRef, VHost, Schema, User) ->
-+ SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ",
-+ "WHERE username='",User,"';"],
-+ case sql_query_internal(DBRef, SQuery) of
-+ {data, []} ->
-+ IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ",
-+ "VALUES ('",User,"');"],
-+ case sql_query_internal_silent(DBRef, IQuery) of
-+ {updated, _} ->
-+ {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery),
-+ DBIdNew;
-+ {error, Reason} ->
-+ % this can be in clustered environment
-+ {value, {code, "23505"}} = lists:keysearch(code, 1, Reason),
-+ ?ERROR_MSG("Duplicate key name for ~p", [User]),
-+ {data, [{ClID}]} = sql_query_internal(DBRef, SQuery),
-+ ClID
-+ end;
-+ {data, [{DBId}]} ->
-+ DBId
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% SQL internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++sql_query_internal(DBRef, Query) ->
++ case sql_query_internal_silent(DBRef, Query) of
++ {error, Reason} ->
++ ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
++ {error, Reason};
++ Rez -> Rez
+ end.
+
-+get_logmessage(VHost,Schema) ->
-+ UName = users_table(VHost,Schema),
-+ SName = servers_table(VHost,Schema),
-+ RName = resources_table(VHost,Schema),
-+ StName = stats_table(VHost,Schema),
-+ io_lib:format("CREATE OR REPLACE FUNCTION \"logmessage\" (tbname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION) RETURNS INTEGER AS $$
-+DECLARE
-+ ownerID INTEGER;
-+ peer_nameID INTEGER;
-+ peer_serverID INTEGER;
-+ peer_resourceID INTEGER;
-+ tablename ALIAS for $1;
-+ atdate ALIAS for $2;
-+ viewname TEXT;
-+BEGIN
-+ SELECT INTO ownerID user_id FROM ~s WHERE username = owner;
-+ IF NOT FOUND THEN
-+ INSERT INTO ~s (username) VALUES (owner);
-+ ownerID := lastval();
-+ END IF;
++sql_query_internal_silent(DBRef, Query) ->
++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
++ get_result(mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
+
-+ SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name;
-+ IF NOT FOUND THEN
-+ INSERT INTO ~s (username) VALUES (peer_name);
-+ peer_nameID := lastval();
-+ END IF;
++get_result({updated, MySQLRes}) ->
++ {updated, mysql:get_result_affected_rows(MySQLRes)};
++get_result({data, MySQLRes}) ->
++ {data, mysql:get_result_rows(MySQLRes)};
++get_result({error, "query timed out"}) ->
++ {error, "query timed out"};
++get_result({error, MySQLRes}) ->
++ Reason = mysql:get_result_reason(MySQLRes),
++ {error, Reason}.
+
-+ SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server;
-+ IF NOT FOUND THEN
-+ INSERT INTO ~s (server) VALUES (peer_server);
-+ peer_serverID := lastval();
-+ END IF;
++get_user_id(DBRef, VHost, User) ->
++ SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
++ "WHERE username=\"",User,"\";"],
++ case sql_query_internal(DBRef, SQuery) of
++ {data, []} ->
++ IQuery = ["INSERT INTO ",users_table(VHost)," ",
++ "SET username=\"",User,"\";"],
++ case sql_query_internal_silent(DBRef, IQuery) of
++ {updated, _} ->
++ {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery),
++ DBIdNew;
++ {error, Reason} ->
++ % this can be in clustered environment
++ match = ejabberd_regexp:run(Reason, "#23000"),
++ ?ERROR_MSG("Duplicate key name for ~p", [User]),
++ {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
++ ClID
++ end;
++ {data, [[DBId]]} ->
++ DBId
++ end.
+
-+ SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource;
-+ IF NOT FOUND THEN
-+ INSERT INTO ~s (resource) VALUES (peer_resource);
-+ peer_resourceID := lastval();
-+ END IF;
++get_logmessage(VHost) ->
++ UName = users_table(VHost),
++ SName = servers_table(VHost),
++ RName = resources_table(VHost),
++ StName = stats_table(VHost),
++ io_lib:format("
++CREATE PROCEDURE ~s(tablename TEXT, atdate TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(10), msubject TEXT, mbody TEXT, mtimestamp DOUBLE)
++BEGIN
++ DECLARE ownerID MEDIUMINT UNSIGNED;
++ DECLARE peer_nameID MEDIUMINT UNSIGNED;
++ DECLARE peer_serverID MEDIUMINT UNSIGNED;
++ DECLARE peer_resourceID MEDIUMINT UNSIGNED;
++ DECLARE Vmtype VARCHAR(10);
++ DECLARE Vmtimestamp DOUBLE;
++ DECLARE Vmdirection VARCHAR(4);
++ DECLARE Vmbody TEXT;
++ DECLARE Vmsubject TEXT;
++ DECLARE iq TEXT;
++ DECLARE cq TEXT;
++ DECLARE viewname TEXT;
++ DECLARE notable INT;
++ DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1;
+
-+ BEGIN
-+ EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',''' || msubj || ''',''' || mbody || ''',' || mtimestamp || ')';
-+ EXCEPTION WHEN undefined_table THEN
-+ EXECUTE 'CREATE TABLE ' || tablename || ' (' ||
-+ 'owner_id INTEGER, ' ||
-+ 'peer_name_id INTEGER, ' ||
-+ 'peer_server_id INTEGER, ' ||
-+ 'peer_resource_id INTEGER, ' ||
-+ 'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' ||
-+ 'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' ||
-+ 'subject TEXT, ' ||
-+ 'body TEXT, ' ||
-+ 'timestamp DOUBLE PRECISION)';
-+ EXECUTE 'CREATE INDEX \"owner_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id)';
-+ EXECUTE 'CREATE INDEX \"peer_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (peer_server_id, peer_name_id)';
++ SET @notable = 0;
++ SET @ownerID = NULL;
++ SET @peer_nameID = NULL;
++ SET @peer_serverID = NULL;
++ SET @peer_resourceID = NULL;
++
++ SET @Vmtype = mtype;
++ SET @Vmtimestamp = mtimestamp;
++ SET @Vmdirection = mdirection;
++ SET @Vmbody = mbody;
++ SET @Vmsubject = msubject;
+
-+ viewname := '~s.\"v_' || trim(both '~s.\"' from tablename) || '\"';
++ SELECT user_id INTO @ownerID FROM ~s WHERE username=owner;
++ IF @ownerID IS NULL THEN
++ INSERT INTO ~s SET username=owner;
++ SET @ownerID = LAST_INSERT_ID();
++ END IF;
+
-+ EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' ||
-+ 'SELECT owner.username AS owner_name, ' ||
-+ 'peer.username AS peer_name, ' ||
-+ 'servers.server AS peer_server, ' ||
-+ 'resources.resource AS peer_resource, ' ||
-+ 'messages.direction, ' ||
-+ 'messages.type, ' ||
-+ 'messages.subject, ' ||
-+ 'messages.body, ' ||
-+ 'messages.timestamp ' ||
-+ 'FROM ' ||
-+ '~s owner, ' ||
-+ '~s peer, ' ||
-+ '~s servers, ' ||
-+ '~s resources, ' ||
-+ tablename || ' messages ' ||
-+ 'WHERE ' ||
-+ 'owner.user_id=messages.owner_id and ' ||
-+ 'peer.user_id=messages.peer_name_id and ' ||
-+ 'servers.server_id=messages.peer_server_id and ' ||
-+ 'resources.resource_id=messages.peer_resource_id ' ||
-+ 'ORDER BY messages.timestamp';
++ SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name;
++ IF @peer_nameID IS NULL THEN
++ INSERT INTO ~s SET username=peer_name;
++ SET @peer_nameID = LAST_INSERT_ID();
++ END IF;
+
-+ EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',''' || msubj || ''',''' || mbody || ''',' || mtimestamp || ')';
-+ END;
++ SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server;
++ IF @peer_serverID IS NULL THEN
++ INSERT INTO ~s SET server=peer_server;
++ SET @peer_serverID = LAST_INSERT_ID();
++ END IF;
+
-+ UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID;
-+ IF NOT FOUND THEN
-+ INSERT INTO ~s (owner_id, at, count) VALUES (ownerID, atdate, 1);
++ SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource;
++ IF @peer_resourceID IS NULL THEN
++ INSERT INTO ~s SET resource=peer_resource;
++ SET @peer_resourceID = LAST_INSERT_ID();
+ END IF;
-+ RETURN 0;
-+END;
-+$$ LANGUAGE plpgsql;
-+", [UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),Schema,escape_vhost(VHost),Schema,Schema,UName,UName,SName,RName,StName,StName]).
+
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+%
-+% SQL internals
-+%
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
-+sql_transaction_internal(DBRef, Fun) ->
-+ case sql_query_internal(DBRef, ["BEGIN;"]) of
-+ {updated, _} ->
-+ case catch Fun() of
-+ error = Err ->
-+ rollback_internal(DBRef, Err);
-+ {error, _} = Err ->
-+ rollback_internal(DBRef, Err);
-+ {'EXIT', _} = Err ->
-+ rollback_internal(DBRef, Err);
-+ Res ->
-+ case sql_query_internal(DBRef, ["COMMIT;"]) of
-+ {error, _} -> rollback_internal(DBRef, {commit_error});
-+ {updated, _} ->
-+ case Res of
-+ {atomic, _} -> Res;
-+ _ -> {atomic, Res}
-+ end
-+ end
-+ end;
-+ {error, _} ->
-+ {aborted, {begin_error}}
-+ end.
++ SET @iq = CONCAT(\"INSERT INTO \",tablename,\" (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (@ownerID,@peer_nameID,@peer_serverID,@peer_resourceID,@Vmdirection,@Vmtype,@Vmsubject,@Vmbody,@Vmtimestamp);\");
++ PREPARE insertmsg FROM @iq;
+
-+% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
-+rollback_internal(DBRef, Reason) ->
-+ Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
-+ {aborted, {Reason, {rollback_result, Res}}}.
++ IF @notable = 1 THEN
++ SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" (
++ owner_id MEDIUMINT UNSIGNED NOT NULL,
++ peer_name_id MEDIUMINT UNSIGNED NOT NULL,
++ peer_server_id MEDIUMINT UNSIGNED NOT NULL,
++ peer_resource_id MEDIUMINT(8) UNSIGNED NOT NULL,
++ direction ENUM('to', 'from') NOT NULL,
++ type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
++ subject TEXT,
++ body TEXT,
++ timestamp DOUBLE NOT NULL,
++ ext INTEGER DEFAULT NULL,
++ INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id),
++ INDEX ext_i (ext),
++ FULLTEXT (body)
++ ) ENGINE=MyISAM
++ PACK_KEYS=1
++ CHARACTER SET utf8;\");
++ PREPARE createtable FROM @cq;
++ EXECUTE createtable;
++ DEALLOCATE PREPARE createtable;
+
-+sql_query_internal(DBRef, Query) ->
-+ case sql_query_internal_silent(DBRef, Query) of
-+ {error, undefined, Rez} ->
-+ ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]),
-+ {error, undefined};
-+ {error, Error} ->
-+ ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]),
-+ {error, Error};
-+ Rez -> Rez
-+ end.
++ SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\");
++ SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS
++ SELECT owner.username AS owner_name,
++ peer.username AS peer_name,
++ servers.server AS peer_server,
++ resources.resource AS peer_resource,
++ messages.direction,
++ messages.type,
++ messages.subject,
++ messages.body,
++ messages.timestamp
++ FROM
++ ~s owner,
++ ~s peer,
++ ~s servers,
++ ~s resources,
++ \", tablename,\" messages
++ WHERE
++ owner.user_id=messages.owner_id and
++ peer.user_id=messages.peer_name_id and
++ servers.server_id=messages.peer_server_id and
++ resources.resource_id=messages.peer_resource_id
++ ORDER BY messages.timestamp;\");
++ PREPARE createview FROM @cq;
++ EXECUTE createview;
++ DEALLOCATE PREPARE createview;
+
-+sql_query_internal_silent(DBRef, Query) ->
-+ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
-+ get_result(pgsql:squery(DBRef, Query)).
++ SET @notable = 0;
++ PREPARE insertmsg FROM @iq;
++ EXECUTE insertmsg;
++ ELSEIF @notable = 0 THEN
++ EXECUTE insertmsg;
++ END IF;
+
-+get_result({ok, ["CREATE TABLE"]}) ->
-+ {updated, 1};
-+get_result({ok, ["DROP TABLE"]}) ->
-+ {updated, 1};
-+get_result({ok,["DROP VIEW"]}) ->
-+ {updated, 1};
-+get_result({ok, ["CREATE INDEX"]}) ->
-+ {updated, 1};
-+get_result({ok, ["CREATE FUNCTION"]}) ->
-+ {updated, 1};
-+get_result({ok, [{"SELECT", _Rows, Recs}]}) ->
-+ {data, [list_to_tuple(Rec) || Rec <- Recs]};
-+get_result({ok, ["INSERT " ++ OIDN]}) ->
-+ [_OID, N] = string:tokens(OIDN, " "),
-+ {updated, list_to_integer(N)};
-+get_result({ok, ["DELETE " ++ N]}) ->
-+ {updated, list_to_integer(N)};
-+get_result({ok, ["UPDATE " ++ N]}) ->
-+ {updated, list_to_integer(N)};
-+get_result({ok, ["BEGIN"]}) ->
-+ {updated, 1};
-+get_result({ok, ["LOCK TABLE"]}) ->
-+ {updated, 1};
-+get_result({ok, ["ROLLBACK"]}) ->
-+ {updated, 1};
-+get_result({ok, ["COMMIT"]}) ->
-+ {updated, 1};
-+get_result({ok, ["SET"]}) ->
-+ {updated, 1};
-+get_result({ok, [{error, Error}]}) ->
-+ {error, Error};
-+get_result(Rez) ->
-+ {error, undefined, Rez}.
++ DEALLOCATE PREPARE insertmsg;
+
---- src/mod_logdb_mnesia_old.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/mod_logdb_mnesia_old.erl Wed Aug 22 22:58:11 2007
-@@ -0,0 +1,256 @@
++ IF @notable = 0 THEN
++ UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND peer_name_id=@peer_nameID AND peer_server_id=@peer_serverID AND at=atdate;
++ IF ROW_COUNT() = 0 THEN
++ INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (@ownerID, @peer_nameID, @peer_serverID, atdate, 1);
++ END IF;
++ END IF;
++END;", [logmessage_name(VHost),UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
+diff --git src/mod_logdb_pgsql.erl src/mod_logdb_pgsql.erl
+new file mode 100644
+index 0000000..1227519
+--- /dev/null
++++ src/mod_logdb_pgsql.erl
+@@ -0,0 +1,1104 @@
+%%%----------------------------------------------------------------------
-+%%% File : mod_logdb_mnesia_old.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose : mod_logmnesia backend for mod_logdb (should be used only for copy_tables functionality)
++%%% File : mod_logdb_pgsql.erl
++%%% Author : Oleg Palij (mailto,xmpp:o.palij@gmail.com)
++%%% Purpose : Posgresql backend for mod_logdb
+%%% Version : trunk
-+%%% Id : $Id$
++%%% Id : $Id: mod_logdb_pgsql.erl 1360 2009-07-30 06:00:14Z malik $
+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
+%%%----------------------------------------------------------------------
+
-+-module(mod_logdb_mnesia_old).
++-module(mod_logdb_pgsql).
+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
+
++-include("mod_logdb.hrl").
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+
+-behaviour(gen_logdb).
++-behaviour(gen_server).
+
-+-export([start/2, stop/1,
-+ log_message/2,
++% gen_server
++-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
++% gen_mod
++-export([start/2, stop/1]).
++% gen_logdb
++-export([log_message/2,
+ rebuild_stats/1,
+ rebuild_stats_at/2,
-+ rebuild_stats_at1/2,
+ delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
+ get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
+ get_dates/1,
-+ get_users_settings/1, get_user_settings/2, set_user_settings/3]).
++ get_users_settings/1, get_user_settings/2, set_user_settings/3,
++ drop_user/2]).
+
-+-record(stats, {user, server, table, count}).
-+-record(msg, {to_user, to_server, to_resource, from_user, from_server, from_resource, id, type, subject, body, timestamp}).
++-export([view_table/3]).
+
-+tables_prefix() -> "messages_".
-+% stats_table should not start with tables_prefix(VHost) !
-+% i.e. lists:prefix(tables_prefix(VHost), atom_to_list(stats_table())) must be /= true
-+stats_table() -> list_to_atom("messages-stats").
-+% table name as atom from Date
-+-define(ATABLE(Date), list_to_atom(tables_prefix() ++ Date)).
-+-define(LTABLE(Date), tables_prefix() ++ Date).
++% gen_server call timeout
++-define(CALL_TIMEOUT, 30000).
++-define(PGSQL_TIMEOUT, 60000).
++-define(PROCNAME, mod_logdb_pgsql).
++
++-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
++ list_to_string/1, string_to_list/1,
++ convert_timestamp_brief/1]).
++
++-record(state, {dbref, vhost, server, port, db, user, password, schema}).
++
++% replace "." with "_"
++escape_vhost(VHost) -> lists:map(fun(46) -> 95;
++ (A) -> A
++ end, VHost).
++
++prefix(Schema) ->
++ Schema ++ ".\"" ++ "logdb_".
++
++suffix(VHost) ->
++ "_" ++ escape_vhost(VHost) ++ "\"".
++
++messages_table(VHost, Schema, Date) ->
++ prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost).
++
++view_table(VHost, Schema, Date) ->
++ Table = messages_table(VHost, Schema, Date),
++ TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3),
++ lists:append([Schema, ".\"v_", TablewoS, "\""]).
++
++stats_table(VHost, Schema) ->
++ prefix(Schema) ++ "stats" ++ suffix(VHost).
++
++temp_table(VHost, Schema) ->
++ prefix(Schema) ++ "temp" ++ suffix(VHost).
++
++settings_table(VHost, Schema) ->
++ prefix(Schema) ++ "settings" ++ suffix(VHost).
++
++users_table(VHost, Schema) ->
++ prefix(Schema) ++ "users" ++ suffix(VHost).
++servers_table(VHost, Schema) ->
++ prefix(Schema) ++ "servers" ++ suffix(VHost).
++resources_table(VHost, Schema) ->
++ prefix(Schema) ++ "resources" ++ suffix(VHost).
++
++logmessage_name(VHost, Schema) ->
++ prefix(Schema) ++ "logmessage" ++ suffix(VHost).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% gen_mod callbacks
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++start(VHost, Opts) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
++
++stop(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% gen_logdb callbacks
++% gen_server callbacks
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+start(_Opts, _VHost) ->
-+ case mnesia:system_info(is_running) of
-+ yes ->
-+ ok = create_stats_table(),
-+ {ok, ok};
-+ no ->
-+ ?ERROR_MSG("Mnesia not running", []),
-+ error;
-+ Status ->
-+ ?ERROR_MSG("Mnesia status: ~p", [Status]),
-+ error
++init([VHost, Opts]) ->
++ Server = gen_mod:get_opt(server, Opts, "localhost"),
++ DB = gen_mod:get_opt(db, Opts, "ejabberd_logdb"),
++ User = gen_mod:get_opt(user, Opts, "root"),
++ Port = gen_mod:get_opt(port, Opts, 5432),
++ Password = gen_mod:get_opt(password, Opts, ""),
++ Schema = gen_mod:get_opt(schema, Opts, "public"),
++
++ ?MYDEBUG("Starting pgsql backend for ~p", [VHost]),
++
++ St = #state{vhost=VHost,
++ server=Server, port=Port, db=DB,
++ user=User, password=Password,
++ schema=Schema},
++
++ case open_pgsql_connection(St) of
++ {ok, DBRef} ->
++ State = St#state{dbref=DBRef},
++ ok = create_internals(State),
++ ok = create_stats_table(State),
++ ok = create_settings_table(State),
++ ok = create_users_table(State),
++ ok = create_servers_table(State),
++ ok = create_resources_table(State),
++ erlang:monitor(process, DBRef),
++ {ok, State};
++ % this does not work
++ {error, Reason} ->
++ ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]),
++ {stop, db_connection_failed};
++ % and this too, becouse pgsql_conn do exit() which can not be catched
++ {'EXIT', Rez} ->
++ ?ERROR_MSG("Rez: ~p~n", [Rez]),
++ {stop, db_connection_failed}
+ end.
+
-+stop(_VHost) ->
-+ ok.
++open_pgsql_connection(#state{server=Server, port=Port, db=DB, schema=Schema,
++ user=User, password=Password} = _State) ->
++ ?INFO_MSG("Opening pgsql connection ~s@~s:~p/~s", [User, Server, Port, DB]),
++ {ok, DBRef} = pgsql:connect(Server, DB, User, Password, Port),
++ {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]),
++ {ok, DBRef}.
+
-+log_message(_VHost, _Msg) ->
-+ error.
++close_pgsql_connection(DBRef) ->
++ ?MYDEBUG("Closing ~p pgsql connection", [DBRef]),
++ pgsql:terminate(DBRef).
+
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+%
-+% gen_logdb callbacks (maintaince)
-+%
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+rebuild_stats(_VHost) ->
-+ ok.
++handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ Date = convert_timestamp_brief(Msg#msg.timestamp),
++ TableName = messages_table(VHost, Schema, Date),
++ ViewName = view_table(VHost, Schema, Date),
++
++ Query = [ "SELECT ", logmessage_name(VHost, Schema)," "
++ "('", TableName, "',",
++ "'", ViewName, "',",
++ "'", Date, "',",
++ "'", Msg#msg.owner_name, "',",
++ "'", Msg#msg.peer_name, "',",
++ "'", Msg#msg.peer_server, "',",
++ "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',",
++ "'", atom_to_list(Msg#msg.direction), "',",
++ "'", Msg#msg.type, "',",
++ "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
++ "'", ejabberd_odbc:escape(Msg#msg.body), "',",
++ "'", Msg#msg.timestamp, "');"],
++
++ case sql_query_internal_silent(DBRef, Query) of
++ % TODO: change this
++ {data, [{"0"}]} ->
++ ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
++ Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
++ ok;
++ {error, _Reason} ->
++ error
++ end,
++ {reply, ok, State};
++handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date),
++ {reply, Reply, State};
++handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
++ {reply, error, State};
++handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
++ ["'",Timestamp,"'",","]
++ end, Msgs),
++
++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
++
++ Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
++ "WHERE timestamp IN (", Temp1],
++
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ rebuild_stats_at_int(DBRef, VHost, Schema, Date);
++ {error, _} ->
++ error
++ end,
++ {reply, Reply, State};
++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ ok = delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date),
++ ok = delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date),
++ {reply, ok, State};
++handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
++ Reply =
++ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date)," CASCADE;"]) of
++ {updated, _} ->
++ Query = ["DELETE FROM ",stats_table(VHost, Schema)," "
++ "WHERE at='",Date,"';"],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end;
++ {error, _} ->
++ error
++ end,
++ {reply, Reply, State};
++handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ SName = stats_table(VHost, Schema),
++ Query = ["SELECT at, sum(count) ",
++ "FROM ",SName," ",
++ "GROUP BY at ",
++ "ORDER BY DATE(at) DESC;"
++ ],
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {data, Recs} ->
++ {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]};
++ {error, Reason} ->
++ % TODO: Duplicate error message ?
++ {error, Reason}
++ end,
++ {reply, Reply, State};
++handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ SName = stats_table(VHost, Schema),
++ Query = ["SELECT username, sum(count) AS allcount ",
++ "FROM ",SName," ",
++ "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id ",
++ "WHERE at='",Date,"' ",
++ "GROUP BY username ",
++ "ORDER BY allcount DESC;"
++ ],
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {data, Recs} ->
++ RFun = fun({User, Count}) ->
++ {User, list_to_integer(Count)}
++ end,
++ {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))};
++ {error, Reason} ->
++ % TODO:
++ {error, Reason}
++ end,
++ {reply, Reply, State};
++handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ {reply, get_user_stats_int(DBRef, Schema, User, VHost), State};
++handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ Query = ["SELECT peer_name,",
++ "peer_server,",
++ "peer_resource,",
++ "direction,"
++ "type,"
++ "subject,"
++ "body,"
++ "timestamp "
++ "FROM ",view_table(VHost, Schema, Date)," "
++ "WHERE owner_name='",User,"';"],
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {data, Recs} ->
++ Fun = fun({Peer_name, Peer_server, Peer_resource,
++ Direction,
++ Type,
++ Subject, Body,
++ Timestamp}) ->
++ #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
++ direction=list_to_atom(Direction),
++ type=Type,
++ subject=Subject, body=Body,
++ timestamp=Timestamp}
++ end,
++ {ok, lists:map(Fun, Recs)};
++ {error, Reason} ->
++ {error, Reason}
++ end,
++ {reply, Reply, State};
++handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ SName = stats_table(VHost, Schema),
++ Query = ["SELECT at ",
++ "FROM ",SName," ",
++ "GROUP BY at ",
++ "ORDER BY at DESC;"
++ ],
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {data, Result} ->
++ [ Date || {Date} <- Result ];
++ {error, Reason} ->
++ {error, Reason}
++ end,
++ {reply, Reply, State};
++handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
++ "FROM ",settings_table(VHost, Schema)," ",
++ "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"],
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {data, Recs} ->
++ {ok, [#user_settings{owner_name=Owner,
++ dolog_default=list_to_bool(DoLogDef),
++ dolog_list=string_to_list(DoLogL),
++ donotlog_list=string_to_list(DoNotLogL)
++ } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]};
++ {error, Reason} ->
++ {error, Reason}
++ end,
++ {reply, Reply, State};
++handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ Query = ["SELECT dolog_default,dolog_list,donotlog_list ",
++ "FROM ",settings_table(VHost, Schema)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
++ Reply =
++ case sql_query_internal_silent(DBRef, Query) of
++ {data, []} ->
++ {ok, []};
++ {data, [{DoLogDef, DoLogL, DoNotLogL}]} ->
++ {ok, #user_settings{owner_name=User,
++ dolog_default=list_to_bool(DoLogDef),
++ dolog_list=string_to_list(DoLogL),
++ donotlog_list=string_to_list(DoNotLogL)}};
++ {error, Reason} ->
++ ?ERROR_MSG("Failed to get_user_settings for ~p@~p: ~p", [User, VHost, Reason]),
++ error
++ end,
++ {reply, Reply, State};
++handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
++ dolog_list=DoLogL,
++ donotlog_list=DoNotLogL}},
++ _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ User_id = get_user_id(DBRef, VHost, Schema, User),
++ Query = ["UPDATE ",settings_table(VHost, Schema)," ",
++ "SET dolog_default=",bool_to_list(DoLogDef),", ",
++ "dolog_list='",list_to_string(DoLogL),"', ",
++ "donotlog_list='",list_to_string(DoNotLogL),"' ",
++ "WHERE owner_id=",User_id,";"],
++
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {updated, 0} ->
++ IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ",
++ "(owner_id, dolog_default, dolog_list, donotlog_list) ",
++ "VALUES ",
++ "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
++ case sql_query_internal(DBRef, IQuery) of
++ {updated, 1} ->
++ ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
++ ok;
++ {error, _} ->
++ error
++ end;
++ {updated, 1} ->
++ ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
++ ok;
++ {error, _} ->
++ error
++ end,
++ {reply, Reply, State};
++handle_call({stop}, _From, State) ->
++ ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]),
++ {stop, normal, ok, State};
++handle_call(Msg, _From, State) ->
++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
++ {noreply, State}.
+
-+rebuild_stats_at(VHost, Date) ->
-+ Table = ?LTABLE(Date),
-+ {Time, Value}=timer:tc(?MODULE, rebuild_stats_at1, [VHost, Table]),
-+ ?INFO_MSG("rebuild_stats_at ~p elapsed ~p sec: ~p~n", [Date, Time/1000000, Value]),
-+ Value.
-+rebuild_stats_at1(VHost, Table) ->
-+ CFun = fun(Msg, Stats) ->
-+ To = Msg#msg.to_user ++ "@" ++ Msg#msg.to_server,
-+ Stats_to = if
-+ Msg#msg.to_server == VHost ->
-+ 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,
-+ From = Msg#msg.from_user ++ "@" ++ Msg#msg.from_server,
-+ Stats_from = if
-+ Msg#msg.from_server == VHost ->
-+ case lists:keysearch(From, 1, Stats_to) 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,
-+ Stats_from
-+ end,
-+ DFun = fun(#stats{table=STable, server=Server} = Stat, _Acc)
-+ when STable == Table, Server == VHost ->
-+ mnesia:delete_object(stats_table(), Stat, write);
-+ (_Stat, _Acc) -> ok
-+ end,
-+ case mnesia:transaction(fun() ->
-+ mnesia:write_lock_table(list_to_atom(Table)),
-+ mnesia:write_lock_table(stats_table()),
-+ % Calc stats for VHost at Date
-+ AStats = mnesia:foldl(CFun, [], list_to_atom(Table)),
-+ % Delete all stats for VHost at Date
-+ mnesia:foldl(DFun, [], stats_table()),
-+ % Write new calc'ed stats
-+ lists:foreach(fun({Who, Count}) ->
-+ Jid = jlib:string_to_jid(Who),
-+ JUser = Jid#jid.user,
-+ WStat = #stats{user=JUser, server=VHost, table=Table, count=Count},
-+ mnesia:write(stats_table(), WStat, write)
-+ end, AStats)
-+ end) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Table, Reason]),
-+ error;
-+ {atomic, _} ->
-+ ok
-+ end.
+
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+%
-+% gen_logdb callbacks (delete)
-+%
-+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+delete_messages_by_user_at(_VHost, _Msgs, _Date) ->
-+ error.
++handle_cast({rebuild_stats}, State) ->
++ rebuild_all_stats_int(State),
++ {noreply, State};
++handle_cast({drop_user, User}, #state{vhost=VHost, schema=Schema}=State) ->
++ Fun = fun() ->
++ {ok, DBRef} = open_pgsql_connection(State),
++ {ok, Dates} = get_user_stats_int(DBRef, Schema, User, VHost),
++ MDResult = lists:map(fun({Date, _}) ->
++ delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date)
++ end, Dates),
++ StDResult = delete_all_stats_by_user_int(DBRef, Schema, User, VHost),
++ SDResult = delete_user_settings_int(DBRef, Schema, User, VHost),
++ case lists:all(fun(Result) when Result == ok ->
++ true;
++ (Result) when Result == error ->
++ false
++ end, lists:append([MDResult, [StDResult], [SDResult]])) of
++ true ->
++ ?INFO_MSG("Removed ~s@~s", [User, VHost]);
++ false ->
++ ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
++ end,
++ close_pgsql_connection(DBRef)
++ end,
++ spawn(Fun),
++ {noreply, State};
++handle_cast(Msg, State) ->
++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
++ {noreply, State}.
+
-+delete_all_messages_by_user_at(_User, _VHost, _Date) ->
-+ error.
++handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
++ {stop, connection_dropped, State};
++handle_info(Info, State) ->
++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
++ {noreply, State}.
+
-+delete_messages_at(VHost, Date) ->
-+ Table = list_to_atom(tables_prefix() ++ Date),
++terminate(_Reason, #state{dbref=DBRef}=_State) ->
++ close_pgsql_connection(DBRef),
++ ok.
+
-+ DFun = fun(#msg{to_server=To_server, from_server=From_server}=Msg, _Acc)
-+ when To_server == VHost; From_server == VHost ->
-+ mnesia:delete_object(Table, Msg, write);
-+ (_Msg, _Acc) -> ok
-+ end,
-+
-+ case mnesia:transaction(fun() ->
-+ mnesia:foldl(DFun, [], Table)
-+ end) of
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p: ~p", [VHost, Date, Reason]),
-+ error;
-+ {atomic, _} ->
-+ ok
-+ end.
++code_change(_OldVsn, State, _Extra) ->
++ {ok, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% gen_logdb callbacks (get)
++% gen_logdb callbacks
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+get_vhost_stats(_VHost) ->
-+ {error, "does not emplemented"}.
-+
++log_message(VHost, Msg) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
++rebuild_stats(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {rebuild_stats}).
++rebuild_stats_at(VHost, Date) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
++delete_messages_by_user_at(VHost, Msgs, Date) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
++delete_all_messages_by_user_at(User, VHost, Date) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
++delete_messages_at(VHost, Date) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
++get_vhost_stats(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
+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.
-+
-+get_user_stats(_User, _VHost) ->
-+ {error, "does not emplemented"}.
-+
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
++get_user_stats(User, VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
+get_user_messages_at(User, VHost, Date) ->
-+ Table_name = tables_prefix() ++ Date,
-+ case mnesia:transaction(fun() ->
-+ Pat_to = #msg{to_user=User, to_server=VHost, _='_'},
-+ Pat_from = #msg{from_user=User, from_server=VHost, _='_'},
-+ mnesia:select(list_to_atom(Table_name),
-+ [{Pat_to, [], ['$_']},
-+ {Pat_from, [], ['$_']}])
-+ end) of
-+ {atomic, Result} ->
-+ Msgs = lists:map(fun(#msg{to_user=To_user, to_server=To_server, to_resource=To_res,
-+ from_user=From_user, from_server=From_server, from_resource=From_res,
-+ type=Type,
-+ subject=Subj,
-+ body=Body, timestamp=Timestamp} = _Msg) ->
-+ Subject = case Subj of
-+ "None" -> "";
-+ _ -> Subj
-+ end,
-+ {msg, To_user, To_server, To_res, From_user, From_server, From_res, Type, Subject, Body, Timestamp}
-+ end, Result),
-+ {ok, Msgs};
-+ {aborted, Reason} ->
-+ {error, Reason}
-+ end.
-+
-+get_dates(_VHost) ->
-+ Tables = mnesia:system_info(tables),
-+ MessagesTables =
-+ lists:filter(fun(Table) ->
-+ lists:prefix(tables_prefix(), atom_to_list(Table))
-+ end,
-+ Tables),
-+ lists:map(fun(Table) ->
-+ lists:sublist(atom_to_list(Table),
-+ length(tables_prefix())+1,
-+ length(atom_to_list(Table)))
-+ end,
-+ MessagesTables).
-+
-+get_users_settings(_VHost) ->
-+ {ok, []}.
-+get_user_settings(_User, _VHost) ->
-+ {ok, []}.
-+set_user_settings(_User, _VHost, _Set) ->
-+ ok.
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
++get_dates(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
++get_users_settings(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
++get_user_settings(User, VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
++set_user_settings(User, VHost, Set) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
++drop_user(User, VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {drop_user, User}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%
-+% internal
++% internals
+%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-+% called from db_logon/2
-+create_stats_table() ->
-+ SName = stats_table(),
-+ case mnesia:create_table(SName,
-+ [{disc_only_copies, [node()]},
-+ {type, bag},
-+ {attributes, record_info(fields, stats)},
-+ {record_name, stats}
-+ ]) of
-+ {atomic, ok} ->
-+ ?INFO_MSG("Created stats table", []),
-+ ok;
-+ {aborted, {already_exists, _}} ->
-+ ok;
-+ {aborted, Reason} ->
-+ ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
-+ error
-+ end.
---- src/gen_logdb.erl.orig Tue Dec 11 14:23:19 2007
-+++ src/gen_logdb.erl Wed Aug 22 22:58:11 2007
-@@ -0,0 +1,158 @@
-+%%%----------------------------------------------------------------------
-+%%% File : gen_logdb.erl
-+%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
-+%%% Purpose : Describes generic behaviour for mod_logdb backends.
-+%%% Version : trunk
-+%%% Id : $Id$
-+%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
-+%%%----------------------------------------------------------------------
++get_dates_int(DBRef, VHost) ->
++ Query = ["SELECT n.nspname as \"Schema\",
++ c.relname as \"Name\",
++ CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as \"Type\",
++ r.rolname as \"Owner\"
++ FROM pg_catalog.pg_class c
++ JOIN pg_catalog.pg_roles r ON r.oid = c.relowner
++ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
++ WHERE c.relkind IN ('r','')
++ AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
++ AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$'
++ AND pg_catalog.pg_table_is_visible(c.oid)
++ ORDER BY 1,2;"],
++ case sql_query_internal(DBRef, Query) of
++ {data, Recs} ->
++ lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) ->
++ case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
++ {match, [{S, E}]} ->
++ lists:append(Dates, [lists:sublist(Table,S,E)]);
++ nomatch ->
++ Dates
++ end
++ end, [], Recs);
++ {error, _} ->
++ []
++ end.
+
-+-module(gen_logdb).
-+-author('o.palij@gmail.com').
-+-vsn('$Revision$').
++rebuild_all_stats_int(#state{vhost=VHost, schema=Schema}=State) ->
++ Fun = fun() ->
++ {ok, DBRef} = open_pgsql_connection(State),
++ ok = delete_nonexistent_stats(DBRef, Schema, VHost),
++ case lists:filter(fun(Date) ->
++ case catch rebuild_stats_at_int(DBRef, VHost, Schema, Date) of
++ ok -> false;
++ error -> true;
++ {'EXIT', _} -> true
++ end
++ end, get_dates_int(DBRef, VHost)) of
++ [] -> ok;
++ FTables ->
++ ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
++ error
++ end,
++ close_pgsql_connection(DBRef)
++ end,
++ spawn(Fun).
+
-+-export([behaviour_info/1]).
++rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
++ TempTable = temp_table(VHost, Schema),
++ Fun =
++ fun() ->
++ Table = messages_table(VHost, Schema, Date),
++ STable = stats_table(VHost, Schema),
+
-+behaviour_info(callbacks) ->
-+ [
-+ % called from handle_info(start, _)
-+ % it should logon database and return reference to started instance
-+ % start(VHost, Opts) -> {ok, SPid} | error
-+ % Options - list of options to connect to db
-+ % Types: Options = list() -> [] |
-+ % [{user, "logdb"},
-+ % {pass, "1234"},
-+ % {db, "logdb"}] | ...
-+ % VHost = list() -> "jabber.example.org"
-+ {start, 2},
++ DQuery = [ "DELETE FROM ",STable," ",
++ "WHERE at='",Date,"';"],
+
-+ % called from cleanup/1
-+ % it should logoff database and do cleanup
-+ % stop(VHost)
-+ % Types: VHost = list() -> "jabber.example.org"
-+ {stop, 1},
++ ok = create_temp_table(DBRef, VHost, Schema),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
++ SQuery = ["INSERT INTO ",TempTable," ",
++ "(owner_id,peer_name_id,peer_server_id,at,count) ",
++ "SELECT owner_id,peer_name_id,peer_server_id,'",Date,"'",",count(*) ",
++ "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, 0} ->
++ Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
++ case Count of
++ {data, [{"0"}]} ->
++ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
++ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table," CASCADE;"]),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
++ {updated, _} = sql_query_internal(DBRef, DQuery),
++ ok;
++ _ ->
++ ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
++ error
++ end;
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
++ {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
++ {updated, _} = sql_query_internal(DBRef, DQuery),
++ SQuery1 = ["INSERT INTO ",STable," ",
++ "(owner_id,peer_name_id,peer_server_id,at,count) ",
++ "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
++ "FROM ",TempTable,";"],
++ case sql_query_internal(DBRef, SQuery1) of
++ {updated, _} -> ok;
++ {error, _} -> error
++ end;
++ {error, _} -> error
++ end
++ end, % fun
+
-+ % called from handle_call({addlog, _}, _, _)
-+ % it should log messages to database
-+ % log_message(VHost, Msg) -> ok | error
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % Msg = record() -> #msg
-+ {log_message, 2},
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, _} ->
++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
++ ok;
++ {aborted, Reason} ->
++ ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
++ error
++ end,
++ sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
++ ok.
+
-+ % called from ejabberdctl rebuild_stats
-+ % it should rebuild stats table (if used) for vhost
-+ % rebuild_stats(VHost)
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ {rebuild_stats, 1},
++delete_nonexistent_stats(DBRef, Schema, VHost) ->
++ Dates = get_dates_int(DBRef, VHost),
++ STable = stats_table(VHost, Schema),
+
-+ % it should rebuild stats table (if used) for vhost at Date
-+ % rebuild_stats_at(VHost, Date)
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % Date = list() -> "2007-02-12"
-+ {rebuild_stats_at, 2},
++ Temp = lists:flatmap(fun(Date) ->
++ ["'",Date,"'",","]
++ end, Dates),
+
-+ % called from user_messages_at_parse_query/5
-+ % it should delete selected user messages at date
-+ % delete_messages_by_user_at(VHost, Msgs, Date) -> ok | error
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % Msgs = list() -> [ #msg1, msg2, ... ]
-+ % Date = list() -> "2007-02-12"
-+ {delete_messages_by_user_at, 3},
++ case Temp of
++ [] ->
++ ok;
++ _ ->
++ % replace last "," with ");"
++ Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
++ Query = ["DELETE FROM ",STable," ",
++ "WHERE at NOT IN (", Temp1],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end
++ end.
+
-+ % called from user_messages_parse_query/4 | vhost_messages_at_parse_query/4
-+ % it should delete all user messages at date
-+ % delete_all_messages_by_user_at(User, VHost, Date) -> ok | error
-+ % Types:
-+ % User = list() -> "admin"
-+ % VHost = list() -> "jabber.example.org"
-+ % Date = list() -> "2007-02-12"
-+ {delete_all_messages_by_user_at, 3},
++get_user_stats_int(DBRef, Schema, User, VHost) ->
++ SName = stats_table(VHost, Schema),
++ UName = users_table(VHost, Schema),
++ Query = ["SELECT stats.at, sum(stats.count) ",
++ "FROM ",UName," AS users ",
++ "JOIN ",SName," AS stats ON owner_id=user_id "
++ "WHERE users.username='",User,"' ",
++ "GROUP BY stats.at "
++ "ORDER BY DATE(at) DESC;"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {data, Recs} ->
++ {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]};
++ {error, Result} ->
++ {error, Result}
++ end.
+
-+ % called from vhost_messages_parse_query/3
-+ % it should delete messages for vhost at date and update stats
-+ % delete_messages_at(VHost, Date) -> ok | error
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % Date = list() -> "2007-02-12"
-+ {delete_messages_at, 2},
++delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
++ DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
++ case sql_query_internal(DBRef, DQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
++ ok;
++ {error, _} ->
++ error
++ end.
+
-+ % called from ejabberd_web_admin:vhost_messages_stats/3
-+ % it should return sorted list of count of messages by dates for vhost
-+ % get_vhost_stats(VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ... ]} |
-+ % {error, Reason}
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % DateN = list() -> "2007-02-12"
-+ % Msgs_countN = number() -> 241
-+ {get_vhost_stats, 1},
++delete_all_stats_by_user_int(DBRef, Schema, User, VHost) ->
++ SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
++ ok;
++ {error, _} -> error
++ end.
+
-+ % called from ejabberd_web_admin:vhost_messages_stats_at/4
-+ % it should return sorted list of count of messages by users at date for vhost
-+ % get_vhost_stats_at(VHost, Date) -> {ok, [{User1, Msgs_count1}, {User2, Msgs_count2}, ....]} |
-+ % {error, Reason}
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % Date = list() -> "2007-02-12"
-+ % UserN = list() -> "admin"
-+ % Msgs_countN = number() -> 241
-+ {get_vhost_stats_at, 2},
++delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
++ SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ",
++ "AND at='",Date,"';"],
++ case sql_query_internal(DBRef, SQuery) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
++ ok;
++ {error, _} -> error
++ end.
+
-+ % called from ejabberd_web_admin:user_messages_stats/4
-+ % it should return sorted list of count of messages by date for user at vhost
-+ % get_user_stats(User, VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ...]} |
-+ % {error, Reason}
-+ % Types:
-+ % User = list() -> "admin"
-+ % VHost = list() -> "jabber.example.org"
-+ % DateN = list() -> "2007-02-12"
-+ % Msgs_countN = number() -> 241
-+ {get_user_stats, 2},
++delete_user_settings_int(DBRef, Schema, User, VHost) ->
++ Query = ["DELETE FROM ",settings_table(VHost, Schema)," ",
++ "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
++ ok;
++ {error, Reason} ->
++ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
++ error
++ end.
+
-+ % called from ejabberd_web_admin:user_messages_stats_at/5
-+ % it should return all user messages at date
-+ % get_user_messages_at(User, VHost, Date) -> {ok, Msgs} | {error, Reason}
-+ % Types:
-+ % User = list() -> "admin"
-+ % VHost = list() -> "jabber.example.org"
-+ % Date = list() -> "2007-02-12"
-+ % Msgs = list() -> [ #msg1, msg2, ... ]
-+ {get_user_messages_at, 3},
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% tables internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++create_temp_table(DBRef, VHost, Schema) ->
++ TName = temp_table(VHost, Schema),
++ Query = ["CREATE TABLE ",TName," (",
++ "owner_id INTEGER, ",
++ "peer_name_id INTEGER, ",
++ "peer_server_id INTEGER, ",
++ "at VARCHAR(20), ",
++ "count INTEGER ",
++ ");"
++ ],
++ case sql_query_internal(DBRef, Query) of
++ {updated, _} -> ok;
++ {error, _Reason} -> error
++ end.
+
-+ % called from many places
-+ % it should return list of dates for vhost
-+ % get_dates(VHost) -> [Date1, Date2, ... ]
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % DateN = list() -> "2007-02-12"
-+ {get_dates, 1},
++create_stats_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ SName = stats_table(VHost, Schema),
+
-+ % called from start
-+ % it should return list with users settings for VHost in db
-+ % get_users_settings(VHost) -> [#user_settings1, #user_settings2, ... ] | error
-+ % Types:
-+ % VHost = list() -> "jabber.example.org"
-+ % User = list() -> "admin"
-+ {get_users_settings, 1},
++ Fun =
++ fun() ->
++ Query = ["CREATE TABLE ",SName," (",
++ "owner_id INTEGER, ",
++ "peer_name_id INTEGER, ",
++ "peer_server_id INTEGER, ",
++ "at VARCHAR(20), ",
++ "count integer",
++ ");"
++ ],
++ case sql_query_internal_silent(DBRef, Query) of
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_search_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id, peer_name_id, peer_server_id);"]),
++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]),
++ created;
++ {error, Reason} ->
++ case lists:keysearch(code, 1, Reason) of
++ {value, {code, "42P07"}} ->
++ exists;
++ _ ->
++ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
++ error
++ end
++ end
++ end,
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, created} ->
++ ?MYDEBUG("Created stats table for ~p", [VHost]),
++ rebuild_all_stats_int(State),
++ ok;
++ {atomic, exists} ->
++ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
++ {match, [{F, L}]} = re:run(SName, "\".*\""),
++ QTable = lists:sublist(SName, F+1, L-2),
++ OIDQuery = ["SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname='",QTable,"' AND pg_catalog.pg_table_is_visible(c.oid);"],
++ {data,[{OID}]} = sql_query_internal(DBRef, OIDQuery),
++ CheckQuery = ["SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = '",OID,"' AND a.attnum > 0 AND NOT a.attisdropped AND a.attname ~ '^peer_.*_id$';"],
++ case sql_query_internal(DBRef, CheckQuery) of
++ {data, Elems} when length(Elems) == 2 ->
++ ?MYDEBUG("Stats table structure is ok", []),
++ ok;
++ _ ->
++ ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
++ case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
++ {updated, _} ->
++ ?INFO_MSG("Successfully dropped ~p", [SName]);
++ _ ->
++ ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
++ end,
++ error
++ end;
++ {error, _} -> error
++ end.
+
-+ % called from many places
-+ % it should return User settings at VHost from db
-+ % get_user_settings(User, VHost) -> error | {ok, #user_settings}
-+ % Types:
-+ % User = list() -> "admin"
-+ % VHost = list() -> "jabber.example.org"
-+ {get_user_settings, 2},
++create_settings_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
++ SName = settings_table(VHost, Schema),
++ Query = ["CREATE TABLE ",SName," (",
++ "owner_id INTEGER PRIMARY KEY, ",
++ "dolog_default BOOLEAN, ",
++ "dolog_list TEXT DEFAULT '', ",
++ "donotlog_list TEXT DEFAULT ''",
++ ");"
++ ],
++ case sql_query_internal_silent(DBRef, Query) of
++ {updated, _} ->
++ ?MYDEBUG("Created settings table for ~p", [VHost]),
++ ok;
++ {error, Reason} ->
++ case lists:keysearch(code, 1, Reason) of
++ {value, {code, "42P07"}} ->
++ ?MYDEBUG("Settings table for ~p already exists", [VHost]),
++ ok;
++ _ ->
++ ?ERROR_MSG("Failed to create settings table for ~p: ~p", [VHost, Reason]),
++ error
++ end
++ end.
+
-+ % called from web admin
-+ % it should set User settings at VHost
-+ % set_user_settings(User, VHost, #user_settings) -> ok | error
-+ % Types:
-+ % User = list() -> "admin"
-+ % VHost = list() -> "jabber.example.org"
-+ {set_user_settings, 3}
-+ ];
-+behaviour_info(_) ->
-+ undefined.
---- src/web/ejabberd_web_admin-1.1.4.erl Tue Dec 11 13:25:24 2007
-+++ src/web/ejabberd_web_admin.erl Fri Jul 27 09:19:48 2007
-@@ -21,6 +21,7 @@
- -include("ejabberd.hrl").
- -include("jlib.hrl").
- -include("ejabberd_http.hrl").
-+-include("mod_logdb.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_logdb) 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_logdb) 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);
++create_users_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
++ SName = users_table(VHost, Schema),
+
-+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);
++ Fun =
++ fun() ->
++ Query = ["CREATE TABLE ",SName," (",
++ "username TEXT UNIQUE, ",
++ "user_id SERIAL PRIMARY KEY",
++ ");"
++ ],
++ case sql_query_internal_silent(DBRef, Query) of
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]),
++ created;
++ {error, Reason} ->
++ case lists:keysearch(code, 1, Reason) of
++ {value, {code, "42P07"}} ->
++ exists;
++ _ ->
++ ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]),
++ error
++ end
++ end
++ end,
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, created} ->
++ ?MYDEBUG("Created users table for ~p", [VHost]),
++ ok;
++ {atomic, exists} ->
++ ?MYDEBUG("Users table for ~p already exists", [VHost]),
++ ok;
++ {aborted, _} -> error
++ end.
+
-+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);
++create_servers_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
++ SName = servers_table(VHost, Schema),
++ Fun =
++ fun() ->
++ Query = ["CREATE TABLE ",SName," (",
++ "server TEXT UNIQUE, ",
++ "server_id SERIAL PRIMARY KEY",
++ ");"
++ ],
++ case sql_query_internal_silent(DBRef, Query) of
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]),
++ created;
++ {error, Reason} ->
++ case lists:keysearch(code, 1, Reason) of
++ {value, {code, "42P07"}} ->
++ exists;
++ _ ->
++ ?ERROR_MSG("Failed to create servers table for ~p: ~p", [VHost, Reason]),
++ error
++ end
++ end
++ end,
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, created} ->
++ ?MYDEBUG("Created servers table for ~p", [VHost]),
++ ok;
++ {atomic, exists} ->
++ ?MYDEBUG("Servers table for ~p already exists", [VHost]),
++ ok;
++ {aborted, _} -> error
++ end.
+
-+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,22 @@
- [?XCT("h3", "Password:")] ++ FPassword ++
- [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++
- [?XE("h3", [?ACT("roster/", "Roster")])] ++
-+ case gen_mod:is_loaded(Server, mod_logdb) of
-+ true ->
-+ Sett = mod_logdb:get_user_settings(User, Server),
-+ Log =
-+ case Sett#user_settings.dolog_default of
-+ false ->
-+ ?INPUTT("submit", "dolog", "Log Messages");
-+ true ->
-+ ?INPUTT("submit", "donotlog", "Do Not Log Messages");
-+ _ -> []
-+ end,
-+ [?XE("h3", [?ACT("messages/", "Messages"), ?C(" "), Log])];
-+ %[?INPUT("test", "test", "test"), ?C(" "), Log];
-+ false ->
-+ []
-+ end ++
- [?BR, ?INPUTT("submit", "removeuser", "Remove User")])].
-
-
-@@ -1462,8 +1528,24 @@
- {value, _} ->
- ejabberd_auth:remove_user(User, Server),
- ok;
-- false ->
-- nothing
-+ _ ->
-+ case lists:keysearch("dolog", 1, Query) of
-+ {value, _} ->
-+ Sett = mod_logdb:get_user_settings(User, Server),
-+ % TODO: check returned value
-+ mod_logdb:set_user_settings(User, Server, Sett#user_settings{dolog_default=true}),
-+ nothing;
++create_resources_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
++ RName = resources_table(VHost, Schema),
++ Fun = fun() ->
++ Query = ["CREATE TABLE ",RName," (",
++ "resource TEXT UNIQUE, ",
++ "resource_id SERIAL PRIMARY KEY",
++ ");"
++ ],
++ case sql_query_internal_silent(DBRef, Query) of
++ {updated, _} ->
++ {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]),
++ created;
++ {error, Reason} ->
++ case lists:keysearch(code, 1, Reason) of
++ {value, {code, "42P07"}} ->
++ exists;
+ _ ->
-+ case lists:keysearch("donotlog", 1, Query) of
-+ {value, _} ->
-+ Sett = mod_logdb:get_user_settings(User, Server),
-+ % TODO: check returned value
-+ mod_logdb:set_user_settings(User, Server, Sett#user_settings{dolog_default=false}),
-+ nothing;
-+ false ->
-+ nothing
-+ end
++ ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]),
++ error
+ end
- end
- end.
-
-@@ -1574,6 +1656,14 @@
- Res = user_roster_parse_query(User, Server, Items1, Query, Admin),
- Items = mnesia:dirty_index_read(roster, US, #roster.us),
- SItems = lists:sort(Items),
++ end
++ end,
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, created} ->
++ ?MYDEBUG("Created resources table for ~p", [VHost]),
++ ok;
++ {atomic, exists} ->
++ ?MYDEBUG("Resources table for ~p already exists", [VHost]),
++ ok;
++ {aborted, _} -> error
++ end.
+
-+ Settings = case gen_mod:is_loaded(Server, mod_logdb) of
-+ true ->
-+ mod_logdb:get_user_settings(User, Server);
-+ false ->
-+ []
-+ end,
++create_internals(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
++ sql_query_internal(DBRef, ["DROP FUNCTION IF EXISTS ",logmessage_name(VHost,Schema)," (tbname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION);"]),
++ case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
++ {updated, _} ->
++ ?MYDEBUG("Created logmessage for ~p", [VHost]),
++ ok;
++ {error, Reason} ->
++ case lists:keysearch(code, 1, Reason) of
++ {value, {code, "42704"}} ->
++ ?ERROR_MSG("plpgsql language must be installed into database '~s'. Use CREATE LANGUAGE...", [State#state.db]),
++ error;
++ _ ->
++ error
++ end
++ end.
+
- FItems =
- case SItems of
- [] ->
-@@ -1621,7 +1711,33 @@
- [?INPUTT("submit",
- "remove" ++
- term_to_id(R#roster.jid),
-- "Remove")])])
-+ "Remove")]),
-+ case gen_mod:is_loaded(Server, mod_logdb) of
-+ true ->
-+ Peer = jlib:jid_to_string(R#roster.jid),
-+ A = lists:member(Peer, Settings#user_settings.dolog_list),
-+ B = lists:member(Peer, Settings#user_settings.donotlog_list),
-+ {Name, Value} =
-+ if
-+ A ->
-+ {"donotlog", "Do Not Log Messages"};
-+ B ->
-+ {"dolog", "Log Messages"};
-+ Settings#user_settings.dolog_default == true ->
-+ {"donotlog", "Do Not Log Messages"};
-+ Settings#user_settings.dolog_default == false ->
-+ {"dolog", "Log Messages"}
-+ end,
++get_user_id(DBRef, VHost, Schema, User) ->
++ SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ",
++ "WHERE username='",User,"';"],
++ case sql_query_internal(DBRef, SQuery) of
++ {data, []} ->
++ IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ",
++ "VALUES ('",User,"');"],
++ case sql_query_internal_silent(DBRef, IQuery) of
++ {updated, _} ->
++ {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery),
++ DBIdNew;
++ {error, Reason} ->
++ % this can be in clustered environment
++ {value, {code, "23505"}} = lists:keysearch(code, 1, Reason),
++ ?ERROR_MSG("Duplicate key name for ~p", [User]),
++ {data, [{ClID}]} = sql_query_internal(DBRef, SQuery),
++ ClID
++ end;
++ {data, [{DBId}]} ->
++ DBId
++ end.
+
-+ ?XAE("td", [{"class", "valign"}],
-+ [?INPUTT("submit",
-+ Name ++
-+ term_to_id(R#roster.jid),
-+ Value)]);
-+ false ->
-+ ?X([])
-+ end
-+ ])
- end, SItems))])]
- end,
- [?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++
-@@ -1637,6 +1753,288 @@
- ?INPUTT("submit", "addjid", "Add Jabber ID")
- ])].
-
-+vhost_messages_stats(Server, Query, Lang) ->
-+ Res = case catch mod_logdb:vhost_messages_parse_query(Server, Query) of
-+ {'EXIT', Reason} ->
-+ ?ERROR_MSG("~p", [Reason]),
-+ error;
-+ VResult -> VResult
-+ end,
-+ {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]),
-+ ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]),
-+ %case mod_logdb:get_vhost_stats(Server) of
-+ case Value of
-+ {'EXIT', CReason} ->
-+ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
-+ [?XC("h1", ?T("Error occupied while fetching list"))];
-+ {error, GReason} ->
-+ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
-+ [?XC("h1", ?T("Error occupied while fetching list"))];
-+ {ok, []} ->
-+ [?XC("h1", ?T("No logged messages for ") ++ Server)];
-+ {ok, Dates} ->
-+ Fun = fun({Date, Count}) ->
-+ ID = jlib:encode_base64(binary_to_list(term_to_binary(Server++Date))),
-+ ?XE("tr",
-+ [?XE("td", [?INPUT("checkbox", "selected", ID)]),
-+ ?XE("td", [?AC(Date, Date)]),
-+ ?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, Dates)
-+ )]),
-+ ?BR,
-+ ?INPUTT("submit", "delete", "Delete Selected")
-+ ])]
-+ end.
++get_logmessage(VHost,Schema) ->
++ UName = users_table(VHost,Schema),
++ SName = servers_table(VHost,Schema),
++ RName = resources_table(VHost,Schema),
++ StName = stats_table(VHost,Schema),
++ io_lib:format("CREATE OR REPLACE FUNCTION ~s (tbname TEXT, vname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION) RETURNS INTEGER AS $$
++DECLARE
++ ownerID INTEGER;
++ peer_nameID INTEGER;
++ peer_serverID INTEGER;
++ peer_resourceID INTEGER;
++ tablename ALIAS for $1;
++ viewname ALIAS for $2;
++ atdate ALIAS for $3;
++BEGIN
++ SELECT INTO ownerID user_id FROM ~s WHERE username = owner;
++ IF NOT FOUND THEN
++ INSERT INTO ~s (username) VALUES (owner);
++ ownerID := lastval();
++ END IF;
+
-+vhost_messages_stats_at(Server, Query, Lang, Date) ->
-+ {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]),
-+ ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]),
-+ %case mod_logdb:get_vhost_stats_at(Server, Date) of
-+ case Value of
-+ {'EXIT', CReason} ->
-+ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
-+ [?XC("h1", ?T("Error occupied while fetching list"))];
-+ {error, GReason} ->
-+ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
-+ [?XC("h1", ?T("Error occupied while fetching list"))];
-+ {ok, []} ->
-+ [?XC("h1", ?T("No logged messages for ") ++ Server ++ ?T(" at ") ++ Date)];
-+ {ok, Users} ->
-+ Res = case catch mod_logdb:vhost_messages_at_parse_query(Server, Date, Users, Query) of
-+ {'EXIT', Reason} ->
-+ ?ERROR_MSG("~p", [Reason]),
-+ error;
-+ VResult -> VResult
-+ end,
-+ 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")
-+ ])]
-+ end.
++ SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name;
++ IF NOT FOUND THEN
++ INSERT INTO ~s (username) VALUES (peer_name);
++ peer_nameID := lastval();
++ END IF;
+
-+user_messages_stats(User, Server, Query, Lang) ->
-+ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
-+ Jid = us_to_list(US),
++ SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server;
++ IF NOT FOUND THEN
++ INSERT INTO ~s (server) VALUES (peer_server);
++ peer_serverID := lastval();
++ END IF;
+
-+ Res = case catch mod_logdb:user_messages_parse_query(User, Server, Query) of
-+ {'EXIT', Reason} ->
-+ ?ERROR_MSG("~p", [Reason]),
-+ error;
-+ VResult -> VResult
-+ end,
++ SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource;
++ IF NOT FOUND THEN
++ INSERT INTO ~s (resource) VALUES (peer_resource);
++ peer_resourceID := lastval();
++ END IF;
+
-+ {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]),
-+ ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]),
++ BEGIN
++ EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
++ EXCEPTION WHEN undefined_table THEN
++ EXECUTE 'CREATE TABLE ' || tablename || ' (' ||
++ 'owner_id INTEGER, ' ||
++ 'peer_name_id INTEGER, ' ||
++ 'peer_server_id INTEGER, ' ||
++ 'peer_resource_id INTEGER, ' ||
++ 'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' ||
++ 'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' ||
++ 'subject TEXT, ' ||
++ 'body TEXT, ' ||
++ 'timestamp DOUBLE PRECISION)';
++ EXECUTE 'CREATE INDEX \"search_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id)';
+
-+ case Value of
-+ {'EXIT', CReason} ->
-+ ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
-+ [?XC("h1", ?T("Error occupied while fetching days"))];
-+ {error, GReason} ->
-+ ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
-+ [?XC("h1", ?T("Error occupied while fetching days"))];
-+ {ok, []} ->
-+ [?XC("h1", ?T("No logged messages for ") ++ Jid)];
-+ {ok, Dates} ->
-+ Fun = fun({Date, Count}) ->
-+ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))),
-+ ?XE("tr",
-+ [?XE("td", [?INPUT("checkbox", "selected", ID)]),
-+ ?XE("td", [?AC(Date, Date)]),
-+ ?XC("td", integer_to_list(Count))
-+ ])
-+ %[?AC(Date, Date ++ " (" ++ 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, Dates)
-+ )]),
-+ ?BR,
-+ ?INPUTT("submit", "delete", "Delete Selected")
-+ ])]
++ EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' ||
++ 'SELECT owner.username AS owner_name, ' ||
++ 'peer.username AS peer_name, ' ||
++ 'servers.server AS peer_server, ' ||
++ 'resources.resource AS peer_resource, ' ||
++ 'messages.direction, ' ||
++ 'messages.type, ' ||
++ 'messages.subject, ' ||
++ 'messages.body, ' ||
++ 'messages.timestamp ' ||
++ 'FROM ' ||
++ '~s owner, ' ||
++ '~s peer, ' ||
++ '~s servers, ' ||
++ '~s resources, ' ||
++ tablename || ' messages ' ||
++ 'WHERE ' ||
++ 'owner.user_id=messages.owner_id and ' ||
++ 'peer.user_id=messages.peer_name_id and ' ||
++ 'servers.server_id=messages.peer_server_id and ' ||
++ 'resources.resource_id=messages.peer_resource_id ' ||
++ 'ORDER BY messages.timestamp';
++
++ EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
++ END;
++
++ UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID and peer_name_id=peer_nameID and peer_server_id=peer_serverID;
++ IF NOT FOUND THEN
++ INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (ownerID, peer_nameID, peer_serverID, atdate, 1);
++ END IF;
++ RETURN 0;
++END;
++$$ LANGUAGE plpgsql;
++", [logmessage_name(VHost,Schema),UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),UName,UName,SName,RName,StName,StName]).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% SQL internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
++sql_transaction_internal(DBRef, Fun) ->
++ case sql_query_internal(DBRef, ["BEGIN;"]) of
++ {updated, _} ->
++ case catch Fun() of
++ error = Err ->
++ rollback_internal(DBRef, Err);
++ {error, _} = Err ->
++ rollback_internal(DBRef, Err);
++ {'EXIT', _} = Err ->
++ rollback_internal(DBRef, Err);
++ Res ->
++ case sql_query_internal(DBRef, ["COMMIT;"]) of
++ {error, _} -> rollback_internal(DBRef, {commit_error});
++ {updated, _} ->
++ case Res of
++ {atomic, _} -> Res;
++ _ -> {atomic, Res}
++ end
++ end
++ end;
++ {error, _} ->
++ {aborted, {begin_error}}
+ end.
+
-+user_messages_stats_at(User, Server, Query, Lang, Date) ->
-+ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
-+ Jid = us_to_list(US),
++% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
++rollback_internal(DBRef, Reason) ->
++ Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
++ {aborted, {Reason, {rollback_result, Res}}}.
+
-+ {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]),
-+ ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]),
-+ case Value of
-+ {'EXIT', CReason} ->
-+ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
-+ [?XC("h1", ?T("Error occupied while fetching messages"))];
-+ {error, GReason} ->
-+ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
-+ [?XC("h1", ?T("Error occupied while fetching messages"))];
-+ {ok, []} ->
-+ [?XC("h1", ?T("No logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)];
-+ {ok, User_messages} ->
-+ Res = case catch mod_logdb:user_messages_at_parse_query(Server,
-+ Date,
-+ User_messages,
-+ Query) of
-+ {'EXIT', Reason} ->
-+ ?ERROR_MSG("~p", [Reason]),
-+ error;
-+ VResult -> VResult
-+ end,
++sql_query_internal(DBRef, Query) ->
++ case sql_query_internal_silent(DBRef, Query) of
++ {error, undefined, Rez} ->
++ ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]),
++ {error, undefined};
++ {error, Error} ->
++ ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]),
++ {error, Error};
++ Rez -> Rez
++ end.
+
-+ UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) ->
-+ case lists:member(PName++"@"++PServer, List) of
-+ true -> List;
-+ false -> lists:append([PName++"@"++PServer], List)
-+ end
-+ end, [], User_messages),
++sql_query_internal_silent(DBRef, Query) ->
++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
++ % TODO: use pquery?
++ get_result(pgsql:squery(DBRef, Query)).
+
-+ % Users to filter (sublist of UniqUsers)
-+ CheckedUsers = case lists:keysearch("filter", 1, Query) of
-+ {value, _} ->
-+ lists:filter(fun(UFUser) ->
-+ ID = jlib:encode_base64(binary_to_list(term_to_binary(UFUser))),
-+ lists:member({"selected", ID}, Query)
-+ end, UniqUsers);
-+ false -> []
-+ end,
++get_result({ok, ["CREATE TABLE"]}) ->
++ {updated, 1};
++get_result({ok, ["DROP TABLE"]}) ->
++ {updated, 1};
++get_result({ok, ["ALTER TABLE"]}) ->
++ {updated, 1};
++get_result({ok,["DROP VIEW"]}) ->
++ {updated, 1};
++get_result({ok,["DROP FUNCTION"]}) ->
++ {updated, 1};
++get_result({ok, ["CREATE INDEX"]}) ->
++ {updated, 1};
++get_result({ok, ["CREATE FUNCTION"]}) ->
++ {updated, 1};
++get_result({ok, [{"SELECT", _Rows, Recs}]}) ->
++ Fun = fun(Rec) ->
++ list_to_tuple(
++ lists:map(fun(Elem) when is_binary(Elem) ->
++ binary_to_list(Elem);
++ (Elem) when is_list(Elem) ->
++ Elem;
++ (Elem) when is_integer(Elem) ->
++ integer_to_list(Elem);
++ (Elem) when is_float(Elem) ->
++ float_to_list(Elem);
++ (Elem) when is_boolean(Elem) ->
++ atom_to_list(Elem);
++ (Elem) ->
++ ?ERROR_MSG("Unknown element type ~p", [Elem]),
++ Elem
++ end, Rec))
++ end,
++ Res = lists:map(Fun, Recs),
++ %{data, [list_to_tuple(Rec) || Rec <- Recs]};
++ {data, Res};
++get_result({ok, ["INSERT " ++ OIDN]}) ->
++ [_OID, N] = string:tokens(OIDN, " "),
++ {updated, list_to_integer(N)};
++get_result({ok, ["DELETE " ++ N]}) ->
++ {updated, list_to_integer(N)};
++get_result({ok, ["UPDATE " ++ N]}) ->
++ {updated, list_to_integer(N)};
++get_result({ok, ["BEGIN"]}) ->
++ {updated, 1};
++get_result({ok, ["LOCK TABLE"]}) ->
++ {updated, 1};
++get_result({ok, ["ROLLBACK"]}) ->
++ {updated, 1};
++get_result({ok, ["COMMIT"]}) ->
++ {updated, 1};
++get_result({ok, ["SET"]}) ->
++ {updated, 1};
++get_result({ok, [{error, Error}]}) ->
++ {error, Error};
++get_result(Rez) ->
++ {error, undefined, Rez}.
+
-+ % UniqUsers in html (noone selected -> everyone selected)
-+ Users = lists:map(fun(UHUser) ->
-+ ID = jlib:encode_base64(binary_to_list(term_to_binary(UHUser))),
-+ Input = case lists:member(UHUser, 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", UHUser)])
-+ end, lists:sort(UniqUsers)),
+diff --git src/mod_muc/mod_muc_room.erl src/mod_muc/mod_muc_room.erl
+index 02c83ed..7693b66 100644
+--- src/mod_muc/mod_muc_room.erl
++++ src/mod_muc/mod_muc_room.erl
+@@ -726,6 +726,12 @@ handle_sync_event({change_config, Config}, _From, StateName, StateData) ->
+ {reply, {ok, NSD#state.config}, StateName, NSD};
+ handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) ->
+ {reply, {ok, NewStateData}, StateName, NewStateData};
++handle_sync_event({get_jid_nick, Jid}, _From, StateName, StateData) ->
++ R = case ?DICT:find(jlib:jid_tolower(Jid), StateData#state.users) of
++ error -> [];
++ {ok, {user, _, Nick, _, _}} -> Nick
++ end,
++ {reply, R, StateName, StateData};
+ handle_sync_event(_Event, _From, StateName, StateData) ->
+ Reply = ok,
+ {reply, Reply, StateName, StateData}.
+diff --git src/mod_roster.erl src/mod_roster.erl
+index b15497f..ace8ba7 100644
+--- src/mod_roster.erl
++++ src/mod_roster.erl
+@@ -62,6 +62,8 @@
+ -include("web/ejabberd_http.hrl").
+ -include("web/ejabberd_web_admin.hrl").
+
++-include("mod_logdb.hrl").
+
-+ % Messages to show (based on Users)
-+ User_messages_filtered = case CheckedUsers of
-+ [] -> User_messages;
-+ _ -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) ->
-+ lists:member(PName++"@"++PServer, CheckedUsers)
-+ end, User_messages)
-+ end,
+
+ start(Host, Opts) ->
+ IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
+@@ -1334,6 +1336,14 @@ user_roster(User, Server, Query, Lang) ->
+ Res = user_roster_parse_query(User, Server, Items1, Query),
+ Items = get_roster(LUser, LServer),
+ SItems = lists:sort(Items),
+
-+ Msgs_Fun = fun(#msg{timestamp=Timestamp,
-+ subject=Subject,
-+ direction=Direction,
-+ peer_name=PName, peer_server=PServer, peer_resource=PRes,
-+ body=Body}) ->
-+ TextRaw = case Subject of
-+ "" -> Body;
-+ _ -> [?T("Subject"),": ",Subject,"<br>", Body]
-+ end,
-+ ID = jlib:encode_base64(binary_to_list(term_to_binary(Timestamp))),
-+ % replace \n with <br>
-+ Text = lists:map(fun(10) -> "<br>";
-+ (A) -> A
-+ end, TextRaw),
-+ Resource = case PRes of
-+ [] -> [];
-+ undefined -> [];
-+ R -> "/" ++ R
-+ end,
-+ ?XE("tr",
-+ [?XE("td", [?INPUT("checkbox", "selected", ID)]),
-+ ?XC("td", mod_logdb:convert_timestamp(Timestamp)),
-+ ?XC("td", atom_to_list(Direction)++": "++PName++"@"++PServer++Resource),
-+ ?XC("td", Text)])
-+ end,
-+ % Filtered user messages in html
-+ Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
++ Settings = case gen_mod:is_loaded(Server, mod_logdb) of
++ true ->
++ mod_logdb:get_user_settings(User, Server);
++ false ->
++ []
++ end,
+
-+ [?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"),
-+ ?XCT("td", "Body")
-+ ])]),
-+ ?XE("tbody",
-+ Msgs
-+ )]),
-+ ?INPUTT("submit", "delete", "Delete Selected"),
-+ ?BR
-+ ]
-+ )]
-+ end.
+ FItems =
+ case SItems of
+ [] ->
+@@ -1381,7 +1391,33 @@ user_roster(User, Server, Query, Lang) ->
+ [?INPUTT("submit",
+ "remove" ++
+ ejabberd_web_admin:term_to_id(R#roster.jid),
+- "Remove")])])
++ "Remove")]),
++ case gen_mod:is_loaded(Server, mod_logdb) of
++ true ->
++ Peer = jlib:jid_to_string(R#roster.jid),
++ A = lists:member(Peer, Settings#user_settings.dolog_list),
++ B = lists:member(Peer, Settings#user_settings.donotlog_list),
++ {Name, Value} =
++ if
++ A ->
++ {"donotlog", "Do Not Log Messages"};
++ B ->
++ {"dolog", "Log Messages"};
++ Settings#user_settings.dolog_default == true ->
++ {"donotlog", "Do Not Log Messages"};
++ Settings#user_settings.dolog_default == false ->
++ {"dolog", "Log Messages"}
++ end,
+
- user_roster_parse_query(User, Server, Items, Query, Admin) ->
- case lists:keysearch("addjid", 1, Query) of
- {value, _} ->
-@@ -1704,10 +2102,41 @@
++ ?XAE("td", [{"class", "valign"}],
++ [?INPUTT("submit",
++ Name ++
++ ejabberd_web_admin:term_to_id(R#roster.jid),
++ Value)]);
++ false ->
++ ?X([])
++ end
++ ])
+ end, SItems))])]
+ end,
+ [?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++
+@@ -1481,11 +1517,42 @@ user_roster_item_parse_query(User, Server, Items, Query) ->
+ {"subscription", "remove"}],
[]}]}}),
throw(submitted);
- false ->
+- false ->
- ok
- end
-
- end
++ false ->
+ case lists:keysearch(
-+ "donotlog" ++ term_to_id(JID), 1, Query) of
++ "donotlog" ++ ejabberd_web_admin:term_to_id(JID), 1, Query) of
+ {value, _} ->
+ Peer = jlib:jid_to_string(JID),
+ Settings = mod_logdb:get_user_settings(User, Server),
+ throw(nothing);
+ false ->
+ case lists:keysearch(
-+ "dolog" ++ term_to_id(JID), 1, Query) of
-+ {value, _} ->
++ "dolog" ++ ejabberd_web_admin:term_to_id(JID), 1, Query) of
++ {value, _} ->
+ Peer = jlib:jid_to_string(JID),
+ Settings = mod_logdb:get_user_settings(User, Server),
+ DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of
+ % TODO: check returned value
+ ok = mod_logdb:set_user_settings(User, Server, Sett),
+ throw(nothing);
-+ false ->
-+ ok
-+ end % dolog
++ false ->
++ ok
++ end % dolog
+ end % donotlog
-+ end % remove
-+ end % validate
++ end % remove
++ end % validate
end, Items),
nothing.
---- src/mod_muc/mod_muc_room-1.1.4.erl Tue Dec 11 13:26:10 2007
-+++ src/mod_muc/mod_muc_room.erl Tue Dec 11 14:21:59 2007
-@@ -652,6 +652,12 @@
- false
- end,
- {reply, Reply, StateName, StateData};
-+handle_sync_event({get_jid_nick, Jid}, _From, StateName, StateData) ->
-+ R = case ?DICT:find(jlib:jid_tolower(Jid), StateData#state.users) of
-+ error -> [];
-+ {ok, {user, _, Nick, _, _}} -> Nick
-+ end,
-+ {reply, R, StateName, StateData};
- handle_sync_event(_Event, _From, StateName, StateData) ->
- Reply = ok,
- {reply, Reply, StateName, StateData}.
---- src/msgs/uk-1.1.4.msg Tue Dec 11 14:15:44 2007
-+++ src/msgs/uk.msg Tue Dec 11 14:23:19 2007
-@@ -372,6 +372,32 @@
- {"ejabberd virtual hosts", "віртуальні хости ejabberd"}.
- {"Host", "Хост"}.
-
+diff --git src/msgs/nl.msg src/msgs/nl.msg
+index 70e739f..019b7b4 100644
+--- src/msgs/nl.msg
++++ src/msgs/nl.msg
+@@ -419,3 +419,15 @@
+ {"Your Jabber account was successfully created.","Uw Jabber-account is succesvol gecreeerd."}.
+ {"Your Jabber account was successfully deleted.","Uw Jabber-account is succesvol verwijderd."}.
+ {"Your messages to ~s are being blocked. To unblock them, visit ~s","Uw berichten aan ~s worden geblokkeerd. Om ze te deblokkeren, ga naar ~s"}.
+% mod_logdb
-+{"Users Messages", "Повідомлення користувачів"}.
-+{"Date", "Дата"}.
-+{"Count", "Кількість"}.
-+{"Logged messages for ", "Збережені повідомлення для "}.
-+{" at ", " за "}.
-+{"No logged messages for ", "Відсутні повідомлення для "}.
-+{"Date, Time", "Дата, Час"}.
-+{"Direction: Jid", "Напрямок: Jid"}.
-+{"Subject", "Тема"}.
-+{"Body", "Текст"}.
-+{"Messages", "Повідомлення"}.
-+{"Filter Selected", "Відфільтрувати виділені"}.
-+{"Do Not Log Messages", "Не зберігати повідомлення"}.
-+{"Log Messages", "Зберігати повідомлення"}.
-+{"Messages logging engine", "Система збереження повідомлень"}.
-+{"Default", "За замовчуванням"}.
-+{"Set logging preferences", "Вкажіть налагоджування збереження повідомлень"}.
-+{"Messages logging engine users", "Користувачі системи збереження повідомлень"}.
-+{"Messages logging engine settings", "Налагоджування системи збереження повідомлень"}.
-+{"Set run-time settings", "Вкажіть поточні налагоджування"}.
-+{"Groupchat messages logging", "Збереження повідомлень типу groupchat"}.
-+{"Jids/Domains to ignore", "Ігнорувати наступні jids/домени"}.
-+{"Purge messages older than (days)", "Видаляти повідомлення старіші ніж (дні)"}.
-+{"Poll users settings (seconds)", "Оновлювати налагоджування користувачів кожні (секунд)"}.
-+
- % Local Variables:
- % mode: erlang
- % End:
---- src/msgs/ru-1.1.4.msg Tue Dec 11 14:15:51 2007
-+++ src/msgs/ru.msg Tue Dec 11 14:23:19 2007
-@@ -372,6 +372,32 @@
- {"ejabberd virtual hosts", "Виртуальные хосты ejabberd"}.
- {"Host", "Хост"}.
-
-+% mod_logdb.erl
-+{"Users Messages", "Сообщения пользователей"}.
-+{"Date", "Дата"}.
-+{"Count", "Количество"}.
-+{"Logged messages for ", "Сохранённые cообщения для "}.
-+{" at ", " за "}.
-+{"No logged messages for ", "Отсутствуют сообщения для "}.
-+{"Date, Time", "Дата, Время"}.
-+{"Direction: Jid", "Направление: Jid"}.
-+{"Subject", "Тема"}.
-+{"Body", "Текст"}.
-+{"Messages", "Сообщения"}.
-+{"Filter Selected", "Отфильтровать выделенные"}.
-+{"Do Not Log Messages", "Не сохранять сообщения"}.
-+{"Log Messages", "Сохранять сообщения"}.
-+{"Messages logging engine", "Система логирования сообщений"}.
-+{"Default", "По умолчанию"}.
-+{"Set logging preferences", "Задайте настройки логирования"}.
-+{"Messages logging engine users", "Пользователи системы логирования сообщений"}.
-+{"Messages logging engine settings", "Настройки системы логирования сообщений"}.
-+{"Set run-time settings", "Задайте текущие настройки"}.
-+{"Groupchat messages logging", "Логирование сообщений типа groupchat"}.
-+{"Jids/Domains to ignore", "Игнорировать следующие jids/домены"}.
-+{"Purge messages older than (days)", "Удалять сообщения старее чем (дни)"}.
-+{"Poll users settings (seconds)", "Обновлять настройки пользователей через (секунд)"}.
-+
- % Local Variables:
- % mode: erlang
- % End:
---- src/msgs/nl-1.1.4.msg Tue Dec 11 14:15:58 2007
-+++ src/msgs/nl.msg Thu Apr 26 16:04:49 2007
-@@ -331,4 +331,15 @@
- {"Members:", "Groepsleden:"}.
- {"Displayed Groups:", "Weergegeven groepen:"}.
- {"Group ", "Groep "}.
+{"Users Messages", "Gebruikersberichten"}.
+{"Date", "Datum"}.
+{"Count", "Aantal"}.
+{"Subject", "Onderwerp"}.
+{"Body", "Berichtveld"}.
+{"Messages", "Berichten"}.
-
---- src/msgs/pl-1.1.4.msg Tue Dec 11 14:16:04 2007
-+++ src/msgs/pl.msg Thu Sep 6 09:52:55 2007
-@@ -423,3 +423,27 @@
- % ./mod_muc/mod_muc.erl
- {"ejabberd MUC module\nCopyright (c) 2003-2006 Alexey Shchepin", ""}.
-
+diff --git src/msgs/pl.msg src/msgs/pl.msg
+index 4bc2063..4395f3c 100644
+--- src/msgs/pl.msg
++++ src/msgs/pl.msg
+@@ -419,3 +419,27 @@
+ {"Your Jabber account was successfully created.","Twoje konto zostało stworzone."}.
+ {"Your Jabber account was successfully deleted.","Twoje konto zostało usunięte."}.
+ {"Your messages to ~s are being blocked. To unblock them, visit ~s","Twoje wiadomości do ~s są blokowane. Aby je odblokować, odwiedź ~s"}.
+% mod_logdb
+{"Users Messages", "Wiadomości użytkownika"}.
+{"Date", "Data"}.
+{"Jids/Domains to ignore", "JID/Domena która ma być ignorowana"}.
+{"Purge messages older than (days)", "Usuń wiadomości starsze niż (w dniach)"}.
+{"Poll users settings (seconds)", "Czas aktualizacji preferencji użytkowników (sekundy)"}.
+diff --git src/msgs/ru.msg src/msgs/ru.msg
+index ece7348..99879ec 100644
+--- src/msgs/ru.msg
++++ src/msgs/ru.msg
+@@ -419,3 +419,31 @@
+ {"Your Jabber account was successfully created.","Ваш Jabber-аккаунт был успешно создан."}.
+ {"Your Jabber account was successfully deleted.","Ваш Jabber-аккаунт был успешно удален."}.
+ {"Your messages to ~s are being blocked. To unblock them, visit ~s","Ваши сообщения к ~s блокируются. Для снятия блокировки перейдите по ссылке ~s"}.
++% mod_logdb.erl
++{"Users Messages", "Сообщения пользователей"}.
++{"Date", "Дата"}.
++{"Count", "Количество"}.
++{"Logged messages for ", "Сохранённые cообщения для "}.
++{" at ", " за "}.
++{"No logged messages for ", "Отсутствуют сообщения для "}.
++{"Date, Time", "Дата, Время"}.
++{"Direction: Jid", "Направление: Jid"}.
++{"Subject", "Тема"}.
++{"Body", "Текст"}.
++{"Messages", "Сообщения"}.
++{"Filter Selected", "Отфильтровать выделенные"}.
++{"Do Not Log Messages", "Не сохранять сообщения"}.
++{"Log Messages", "Сохранять сообщения"}.
++{"Messages logging engine", "Система логирования сообщений"}.
++{"Default", "По умолчанию"}.
++{"Set logging preferences", "Задайте настройки логирования"}.
++{"Messages logging engine users", "Пользователи системы логирования сообщений"}.
++{"Messages logging engine settings", "Настройки системы логирования сообщений"}.
++{"Set run-time settings", "Задайте текущие настройки"}.
++{"Groupchat messages logging", "Логирование сообщений типа groupchat"}.
++{"Jids/Domains to ignore", "Игнорировать следующие jids/домены"}.
++{"Purge messages older than (days)", "Удалять сообщения старее чем (дни)"}.
++{"Poll users settings (seconds)", "Обновлять настройки пользователей через (секунд)"}.
++{"Drop", "Удалять"}.
++{"Do not drop", "Не удалять"}.
++{"Drop messages on user removal", "Удалять сообщения при удалении пользователя"}.
+diff --git src/msgs/uk.msg src/msgs/uk.msg
+index 6e21c90..1cdd1ea 100644
+--- src/msgs/uk.msg
++++ src/msgs/uk.msg
+@@ -407,3 +407,31 @@
+ {"Your Jabber account was successfully created.","Ваш Jabber-акаунт було успішно створено."}.
+ {"Your Jabber account was successfully deleted.","Ваш Jabber-акаунт було успішно видалено."}.
+ {"Your messages to ~s are being blocked. To unblock them, visit ~s","Ваші повідомлення до ~s блокуються. Для розблокування відвідайте ~s"}.
++% mod_logdb
++{"Users Messages", "Повідомлення користувачів"}.
++{"Date", "Дата"}.
++{"Count", "Кількість"}.
++{"Logged messages for ", "Збережені повідомлення для "}.
++{" at ", " за "}.
++{"No logged messages for ", "Відсутні повідомлення для "}.
++{"Date, Time", "Дата, Час"}.
++{"Direction: Jid", "Напрямок: Jid"}.
++{"Subject", "Тема"}.
++{"Body", "Текст"}.
++{"Messages", "Повідомлення"}.
++{"Filter Selected", "Відфільтрувати виділені"}.
++{"Do Not Log Messages", "Не зберігати повідомлення"}.
++{"Log Messages", "Зберігати повідомлення"}.
++{"Messages logging engine", "Система збереження повідомлень"}.
++{"Default", "За замовчуванням"}.
++{"Set logging preferences", "Вкажіть налагоджування збереження повідомлень"}.
++{"Messages logging engine users", "Користувачі системи збереження повідомлень"}.
++{"Messages logging engine settings", "Налагоджування системи збереження повідомлень"}.
++{"Set run-time settings", "Вкажіть поточні налагоджування"}.
++{"Groupchat messages logging", "Збереження повідомлень типу groupchat"}.
++{"Jids/Domains to ignore", "Ігнорувати наступні jids/домени"}.
++{"Purge messages older than (days)", "Видаляти повідомлення старіші ніж (дні)"}.
++{"Poll users settings (seconds)", "Оновлювати налагоджування користувачів кожні (секунд)"}.
++{"Drop", "Видаляти"}.
++{"Do not drop", "Не видаляти"}.
++{"Drop messages on user removal", "Видаляти повідомлення під час видалення користувача"}.