--- /dev/null
+--- 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 @@
++%%%----------------------------------------------------------------------
++%%% File : mod_logdb.erl
++%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
++%%% Purpose : Frontend for log user messages to db
++%%% Version : trunk
++%%% Id : $Id$
++%%% 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).
++
++% supervisor
++-export([start_link/2]).
++% gen_mod
++-export([start/2,stop/1]).
++% 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([get_local_identity/5,
++ get_local_features/5,
++ get_local_items/5,
++ adhoc_local_items/4,
++ adhoc_local_commands/4
++% get_sm_identity/5,
++% get_sm_features/5,
++% get_sm_items/5,
++% adhoc_sm_items/4,
++% adhoc_sm_commands/4]).
++ ]).
++% ejabberdctl
++-export([rebuild_stats/3,
++ copy_messages/1, copy_messages_ctl/3, copy_messages_int_tc/1]).
++%
++-export([get_vhost_stats/1, get_vhost_stats_at/2,
++ get_user_stats/2, get_user_messages_at/3,
++ get_dates/1,
++ sort_stats/1,
++ convert_timestamp/1, convert_timestamp_brief/1,
++ get_user_settings/2, set_user_settings/3,
++ user_messages_at_parse_query/4, user_messages_parse_query/3,
++ vhost_messages_parse_query/2, vhost_messages_at_parse_query/4,
++ list_to_bool/1, bool_to_list/1,
++ list_to_string/1, string_to_list/1,
++ get_module_settings/1, set_module_settings/2,
++ purge_old_records/2]).
++
++-include("mod_logdb.hrl").
++-include("ejabberd.hrl").
++-include("jlib.hrl").
++-include("ejabberd_ctl.hrl").
++-include("adhoc.hrl").
++
++-define(PROCNAME, ejabberd_mod_logdb).
++% gen_server call timeout
++-define(CALL_TIMEOUT, 60000).
++
++-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings}).
++
++ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ VHost).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% gen_mod/gen_server callbacks
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% ejabberd starts module
++start(VHost, Opts) ->
++ ChildSpec =
++ {gen_mod:get_module_proc(VHost, ?PROCNAME),
++ {?MODULE, start_link, [VHost, Opts]},
++ permanent,
++ 1000,
++ worker,
++ [?MODULE]},
++ % add child to ejabberd_sup
++ supervisor:start_child(ejabberd_sup, ChildSpec).
++
++% 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], []).
++
++init([VHost, Opts]) ->
++ process_flag(trap_exit, true),
++ DBs = gen_mod:get_opt(dbs, Opts, [{mnesia, []}]),
++ VHostDB = gen_mod:get_opt(vhosts, Opts, [{VHost, mnesia}]),
++ % 10 is default becouse of using in clustered environment
++ PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, 10),
++
++ {value,{_, DBName}} = lists:keysearch(VHost, 1, VHostDB),
++ {value, {DBName, DBOpts}} = lists:keysearch(DBName, 1, DBs),
++
++ ?MYDEBUG("Starting mod_logdb for ~p with ~p backend", [VHost, DBName]),
++
++ 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),
++ 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) ->
++ ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
++
++ %ets:delete(ets_settings_table(VHost)),
++
++ 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(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 110),
++ %ejabberd_hooks:delete(disco_sm_identity, VHost, ?MODULE, get_sm_identity, 110),
++ %ejabberd_hooks:delete(disco_sm_features, VHost, ?MODULE, get_sm_features, 110),
++ %ejabberd_hooks:delete(disco_sm_items, VHost, ?MODULE, get_sm_items, 110),
++ ejabberd_hooks:delete(disco_local_identity, VHost, ?MODULE, get_local_identity, 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),
++
++ ?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),
++ ?MYDEBUG("Unregistered commands for ~p", [VHost]).
++
++stop(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ %gen_server:call(Proc, {cleanup}),
++ %?MYDEBUG("Cleanup in stop finished!!!!", []),
++ %timer:sleep(10000),
++ ok = supervisor:terminate_child(ejabberd_sup, Proc),
++ ok = supervisor:delete_child(ejabberd_sup, Proc).
++
++handle_call({cleanup}, _From, State) ->
++ cleanup(State),
++ ?MYDEBUG("Cleanup finished!!!!!", []),
++ {reply, ok, State};
++handle_call({get_dates}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:get_dates(VHost),
++ {reply, Reply, State};
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% ejabberd_web_admin callbacks
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++handle_call({delete_messages_by_user_at, PMsgs, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, Date),
++ {reply, Reply, State};
++handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:delete_all_messages_by_user_at(User, VHost, Date),
++ {reply, Reply, State};
++handle_call({delete_messages_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:delete_messages_at(VHost, Date),
++ {reply, Reply, State};
++handle_call({get_vhost_stats}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:get_vhost_stats(VHost),
++ {reply, Reply, State};
++handle_call({get_vhost_stats_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:get_vhost_stats_at(VHost, Date),
++ {reply, Reply, State};
++handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:get_user_stats(User, VHost),
++ {reply, Reply, State};
++handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Reply = DBMod:get_user_messages_at(User, VHost, Date),
++ {reply, Reply, State};
++handle_call({get_user_settings, User}, _From, #state{dbmod=_DBMod, vhost=VHost}=State) ->
++ Reply = case ets:match_object(ets_settings_table(VHost),
++ #user_settings{owner_name=User, _='_'}) of
++ [Set] -> Set;
++ _ -> #user_settings{owner_name=User,
++ dolog_default=State#state.dolog_default,
++ dolog_list=[],
++ donotlog_list=[]}
++ end,
++ {reply, Reply, State};
++% TODO: remove User ??
++handle_call({set_user_settings, User, GSet}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ Set = GSet#user_settings{owner_name=User},
++ Reply =
++ case ets:match_object(ets_settings_table(VHost),
++ #user_settings{owner_name=User, _='_'}) of
++ [Set] ->
++ ?MYDEBUG("Settings is equal", []),
++ ok;
++ _ ->
++ case DBMod:set_user_settings(User, VHost, Set) of
++ error ->
++ error;
++ ok ->
++ true = ets:insert(ets_settings_table(VHost), Set),
++ ok
++ end
++ end,
++ {reply, Reply, State};
++handle_call({get_module_settings}, _From, State) ->
++ {reply, State, State};
++handle_call({set_module_settings, #state{purge_older_days=PurgeDays,
++ poll_users_settings=PollSec} = Settings},
++ _From,
++ #state{purgeRef=PurgeRefOld,
++ pollRef=PollRefOld,
++ purge_older_days=PurgeDaysOld,
++ poll_users_settings=PollSecOld} = State) ->
++ PurgeRef = if
++ PurgeDays == never, PurgeDaysOld /= never ->
++ {ok, cancel} = timer:cancel(PurgeRefOld),
++ disabled;
++ is_integer(PurgeDays), PurgeDaysOld == never ->
++ set_purge_timer(PurgeDays);
++ true ->
++ PurgeRefOld
++ end,
++
++ PollRef = if
++ PollSec == PollSecOld ->
++ PollRefOld;
++ PollSec == 0, PollSecOld /= 0 ->
++ {ok, cancel} = timer:cancel(PollRefOld),
++ disabled;
++ is_integer(PollSec), PollSecOld == 0 ->
++ set_poll_timer(PollSec);
++ is_integer(PollSec), PollSecOld /= 0 ->
++ {ok, cancel} = timer:cancel(PollRefOld),
++ set_poll_timer(PollSec)
++ end,
++
++ NewState = State#state{dolog_default=Settings#state.dolog_default,
++ ignore_jids=Settings#state.ignore_jids,
++ groupchat=Settings#state.groupchat,
++ purge_older_days=PurgeDays,
++ poll_users_settings=PollSec,
++ purgeRef=PurgeRef,
++ pollRef=PollRef},
++ {reply, ok, NewState};
++handle_call(Msg, _From, State) ->
++ ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
++ {noreply, State}.
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% end ejabberd_web_admin callbacks
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++
++% ejabberd_hooks call
++handle_cast({addlog, Direction, Owner, Peer, Packet}, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ case filter(Owner, Peer, State) of
++ true ->
++ case catch packet_parse(Owner, Peer, Packet, Direction, State) of
++ ignore ->
++ ok;
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("Failed to parse: ~p", [Reason]);
++ Msg ->
++ DBMod:log_message(VHost, Msg)
++ end;
++ false ->
++ ok
++ 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) ->
++ spawn(?MODULE, copy_messages, [[State, Backend]]),
++ {noreply, State};
++handle_cast({copy_messages, Backend, Date}, State) ->
++ spawn(?MODULE, copy_messages, [[State, Backend, Date]]),
++ {noreply, State};
++handle_cast(Msg, State) ->
++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
++ {noreply, State}.
++
++% return: disabled | timer reference
++set_purge_timer(PurgeDays) ->
++ case PurgeDays of
++ never -> disabled;
++ Days when is_integer(Days) ->
++ {ok, Ref1} = timer:send_interval(timer:hours(24), scheduled_purging),
++ Ref1
++ end.
++
++% return: disabled | timer reference
++set_poll_timer(PollSec) ->
++ if
++ PollSec > 0 ->
++ {ok, Ref2} = timer:send_interval(timer:seconds(PollSec), poll_users_settings),
++ Ref2;
++ % db polling disabled
++ PollSec == 0 ->
++ disabled;
++ true ->
++ {ok, Ref3} = timer:send_interval(timer:seconds(10), poll_users_settings),
++ Ref3
++ end.
++
++% actual starting of logging
++% 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} ->
++ timer:sleep(30000),
++ {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}]),
++ {ok, DoLog} = DBMod:get_users_settings(VHost),
++ ets:insert(ets_settings_table(VHost), DoLog),
++
++ TrefPurge = set_purge_timer(State#state.purge_older_days),
++ TrefPoll = set_poll_timer(State#state.poll_users_settings),
++
++ 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(disco_local_identity, VHost, ?MODULE, get_local_identity, 110),
++ %ejabberd_hooks:add(disco_sm_items, VHost, ?MODULE, get_sm_items, 110),
++ %ejabberd_hooks:add(disco_sm_features, VHost, ?MODULE, get_sm_features, 110),
++ %ejabberd_hooks:add(disco_sm_identity, VHost, ?MODULE, get_sm_identity, 110),
++ ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 110),
++ ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 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),
++
++ ?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),
++ ?MYDEBUG("Registered commands for ~p", [VHost]),
++
++ NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll},
++ {noreply, NewState};
++ Rez ->
++ ?ERROR_MSG("Rez=~p", [Rez]),
++ timer:sleep(30000),
++ {stop, db_connection_failed, State}
++ end;
++% from timer:send_interval/2 (in start handle_info)
++handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = State) ->
++ ?MYDEBUG("Starting scheduled purging of old records for ~p", [VHost]),
++ spawn(?MODULE, purge_old_records, [VHost, integer_to_list(Days)]),
++ {noreply, State};
++% from timer:send_interval/2 (in start handle_info)
++handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) ->
++ {ok, DoLog} = DBMod:get_users_settings(VHost),
++ ?MYDEBUG("DoLog=~p", [DoLog]),
++ true = ets:delete_all_objects(ets_settings_table(VHost)),
++ ets:insert(ets_settings_table(VHost), DoLog),
++ {noreply, State};
++handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
++ {stop, db_connection_dropped, State};
++handle_info({fetch_result, _, _}, State) ->
++ ?MYDEBUG("Got timed out mysql fetch result", []),
++ {noreply, State};
++handle_info(Info, State) ->
++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
++ {noreply, State}.
++
++terminate(db_connection_failed, _State) ->
++ ok;
++terminate(db_connection_dropped, State) ->
++ cleanup(State),
++ ok;
++terminate(_Reason, #state{monref=undefined} = State) ->
++ cleanup(State),
++ ok;
++terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) ->
++ ?INFO_MSG("Reason: ~p", [Reason]),
++ case erlang:is_process_alive(Pid) of
++ true ->
++ erlang:demonitor(MonRef, [flush]),
++ DBMod:stop(VHost);
++ false ->
++ ok
++ end,
++ cleanup(State),
++ ok.
++
++code_change(_OldVsn, State, _Extra) ->
++ {ok, State}.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% ejabberd_hooks callbacks
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% TODO: change to/from to list as sql stores it as list
++send_packet(Owner, Peer, P) ->
++ VHost = Owner#jid.lserver,
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {addlog, to, Owner, Peer, P}).
++
++offline_packet(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}).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% ejabberdctl
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++rebuild_stats(_Val, VHost, ["rebuild_stats"]) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {rebuild_stats}),
++ {stop, ?STATUS_SUCCESS};
++rebuild_stats(Val, _VHost, _Args) ->
++ Val.
++
++copy_messages_ctl(_Val, VHost, ["copy_messages", Backend]) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {copy_messages, Backend}),
++ {stop, ?STATUS_SUCCESS};
++copy_messages_ctl(_Val, VHost, ["copy_messages", Backend, Date]) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:cast(Proc, {copy_messages, Backend, Date}),
++ {stop, ?STATUS_SUCCESS};
++copy_messages_ctl(Val, _VHost, _Args) ->
++ Val.
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% misc operations
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++
++% handle_cast({addlog, E}, _)
++% raw packet -> #msg
++packet_parse(Owner, Peer, Packet, Direction, State) ->
++ case xml:get_subtag(Packet, "body") of
++ false ->
++ ignore;
++ Body_xml ->
++ Message_type =
++ case xml:get_tag_attr_s("type", Packet) of
++ [] -> "normal";
++ MType -> MType
++ end,
++
++ case Message_type of
++ "groupchat" when State#state.groupchat == send, Direction == to ->
++ ok;
++ "groupchat" when State#state.groupchat == send, Direction == from ->
++ throw(ignore);
++ "groupchat" when State#state.groupchat == half ->
++ Rooms = ets:match(muc_online_room, '$1'),
++ 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 ->
++ lists:append(Names, [jlib:jid_to_string({GName, GHost, Nick})])
++ end
++ end, [], Rooms),
++ case lists:member(jlib:jid_to_string(Peer), Ni) of
++ true when Direction == from ->
++ throw(ignore);
++ _ ->
++ ok
++ end;
++ "groupchat" when State#state.groupchat == none ->
++ throw(ignore);
++ _ ->
++ ok
++ end,
++
++ Message_body = xml:get_tag_cdata(Body_xml),
++ Message_subject =
++ case xml:get_subtag(Packet, "subject") of
++ false ->
++ "";
++ Subject_xml ->
++ xml:get_tag_cdata(Subject_xml)
++ end,
++
++ OwnerName = stringprep:tolower(Owner#jid.user),
++ PName = stringprep:tolower(Peer#jid.user),
++ PServer = stringprep:tolower(Peer#jid.server),
++ PResource = Peer#jid.resource,
++
++ #msg{timestamp=get_timestamp(),
++ owner_name=OwnerName,
++ peer_name=PName,
++ peer_server=PServer,
++ peer_resource=PResource,
++ direction=Direction,
++ type=Message_type,
++ subject=Message_subject,
++ body=Message_body}
++ end.
++
++% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages)
++filter(Owner, Peer, State) ->
++ OwnerStr = Owner#jid.luser++"@"++Owner#jid.lserver,
++ OwnerServ = "@"++Owner#jid.lserver,
++ PeerStr = Peer#jid.luser++"@"++Peer#jid.lserver,
++ PeerServ = "@"++Peer#jid.lserver,
++
++ LogTo = case ets:match_object(ets_settings_table(State#state.vhost),
++ #user_settings{owner_name=Owner#jid.luser, _='_'}) of
++ [#user_settings{dolog_default=Default,
++ dolog_list=DLL,
++ donotlog_list=DNLL}] ->
++ A = lists:member(PeerStr, DLL),
++ B = lists:member(PeerStr, DNLL),
++ if
++ A -> true;
++ B -> false;
++ Default == true -> true;
++ Default == false -> false;
++ true -> State#state.dolog_default
++ end;
++ _ -> State#state.dolog_default
++ 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),
++ not lists:member(PeerServ, State#state.ignore_jids),
++ LogTo]).
++
++purge_old_records(VHost, Days) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++
++ Dates = gen_server:call(Proc, {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]+"),
++ 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 ->
++ ?MYDEBUG("Skipping messages at ~p", [Date])
++ end
++ end, Dates).
++
++% called from get_vhost_stats/2, get_user_stats/3
++sort_stats(Stats) ->
++ % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ]
++ CFun = fun({TableName, Count}) ->
++ {ok, [Year, Month, Day]} = regexp:split(TableName, "[^0-9]+"),
++ { calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}), Count }
++ end,
++ % convert to [{63364377601,1}, {63360662401,1}, ... ]
++ CStats = lists:map(CFun, Stats),
++ % sort by date
++ SortedStats = lists:reverse(lists:keysort(1, CStats)),
++ % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list
++ [{mod_logdb:convert_timestamp_brief(TableSec), Count} || {TableSec, Count} <- SortedStats].
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% Date/Time operations
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++% return float seconds elapsed from "zero hour" as list
++get_timestamp() ->
++ {MegaSec, Sec, MicroSec} = now(),
++ [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
++ List.
++
++% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string
++convert_timestamp(Seconds) when is_list(Seconds) ->
++ case string:to_float(Seconds++".0") of
++ {F,_} when is_float(F) -> convert_timestamp(F);
++ _ -> erlang:error(badarg, [Seconds])
++ end;
++convert_timestamp(Seconds) when is_float(Seconds) ->
++ GregSec = trunc(Seconds + 719528*86400),
++ UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
++ {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT),
++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec).
++
++% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string
++convert_timestamp_brief(Seconds) when is_list(Seconds) ->
++ convert_timestamp_brief(list_to_float(Seconds));
++convert_timestamp_brief(Seconds) when is_float(Seconds) ->
++ GregSec = trunc(Seconds + 719528*86400),
++ UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
++ {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT),
++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day);
++convert_timestamp_brief(Seconds) when is_integer(Seconds) ->
++ {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds),
++ integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% DB operations (get)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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).
++
++set_user_settings(User, VHost, Set) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {set_user_settings, User, Set}).
++
++get_module_settings(VHost) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {get_module_settings}).
++
++set_module_settings(VHost, Settings) ->
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {set_module_settings, Settings}).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% Web admin callbacks (delete)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++user_messages_at_parse_query(VHost, Date, Msgs, Query) ->
++ case lists:keysearch("delete", 1, Query) of
++ {value, _} ->
++ PMsgs = lists:filter(
++ fun(Msg) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(Msg#msg.timestamp))),
++ lists:member({"selected", ID}, Query)
++ end, Msgs),
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ gen_server:call(Proc, {delete_messages_by_user_at, PMsgs, Date}, ?CALL_TIMEOUT);
++ false ->
++ nothing
++ end.
++
++user_messages_parse_query(User, VHost, Query) ->
++ Dates = get_dates(VHost),
++ case lists:keysearch("delete", 1, Query) of
++ {value, _} ->
++ PDates = lists:filter(
++ fun(Date) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))),
++ lists:member({"selected", ID}, Query)
++ end, Dates),
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ Rez = lists:foldl(
++ fun(Date, Acc) ->
++ lists:append(Acc,
++ [gen_server:call(Proc,
++ {delete_all_messages_by_user_at, User, Date},
++ ?CALL_TIMEOUT)])
++ end, [], PDates),
++ case lists:member(error, Rez) of
++ true ->
++ error;
++ false ->
++ nothing
++ end;
++ false ->
++ nothing
++ end.
++
++vhost_messages_parse_query(VHost, Query) ->
++ Dates = get_dates(VHost),
++ case lists:keysearch("delete", 1, Query) of
++ {value, _} ->
++ PDates = lists:filter(
++ fun(Date) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(VHost++Date))),
++ lists:member({"selected", ID}, Query)
++ end, Dates),
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ Rez = lists:foldl(fun(Date, Acc) ->
++ lists:append(Acc, [gen_server:call(Proc,
++ {delete_messages_at, Date},
++ ?CALL_TIMEOUT)])
++ end, [], PDates),
++ case lists:member(error, Rez) of
++ true ->
++ error;
++ false ->
++ nothing
++ end;
++ false ->
++ nothing
++ end.
++
++vhost_messages_at_parse_query(VHost, Date, Stats, Query) ->
++ case lists:keysearch("delete", 1, Query) of
++ {value, _} ->
++ PStats = lists:filter(
++ fun({User, _Count}) ->
++ ID = jlib:encode_base64(binary_to_list(term_to_binary(User++VHost))),
++ lists:member({"selected", ID}, Query)
++ end, Stats),
++ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
++ Rez = lists:foldl(fun({User, _Count}, Acc) ->
++ lists:append(Acc, [gen_server:call(Proc,
++ {delete_all_messages_by_user_at,
++ User, Date},
++ ?CALL_TIMEOUT)])
++ end, [], PStats),
++ case lists:member(error, Rez) of
++ true ->
++ error;
++ false ->
++ ok
++ end;
++ false ->
++ nothing
++ end.
++
++copy_messages([#state{vhost=VHost}=State, From]) ->
++ ?INFO_MSG("Going to copy messages from ~p for ~p", [From, VHost]),
++
++ {FromDBName, FromDBOpts} =
++ case lists:keysearch(list_to_atom(From), 1, State#state.dbs) of
++ {value, {FN, FO}} ->
++ {FN, FO};
++ false ->
++ ?ERROR_MSG("Failed to find record for ~p in dbs", [From]),
++ throw(error)
++ end,
++
++ 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),
++
++ lists:foldl(fun(Date, Acc) ->
++ case copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
++ ok ->
++ ?INFO_MSG("Copied messages at ~p (~p/~p)", [Date, Acc, DatesLength]);
++ Value ->
++ ?ERROR_MSG("Failed to copy messages at ~p (~p/~p): ~p", [Date, Acc, DatesLength, Value]),
++ FromDBMod:stop(VHost),
++ throw(error)
++ end,
++ Acc + 1
++ end, 1, Dates),
++ ?INFO_MSG("Copied messages from ~p", [From]),
++ FromDBMod:stop(VHost);
++copy_messages([#state{vhost=VHost}=State, From, Date]) ->
++ {value, {FromDBName, FromDBOpts}} = lists:keysearch(list_to_atom(From), 1, State#state.dbs),
++ FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
++ {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
++ case catch copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
++ {'exit', Reason} ->
++ ?ERROR_MSG("Failed to copy messages at ~p: ~p", [Date, Reason]);
++ ok ->
++ ?INFO_MSG("Copied messages at ~p", [Date]);
++ Value ->
++ ?ERROR_MSG("Failed to copy messages at ~p: ~p", [Date, Value])
++ end,
++ FromDBMod:stop(VHost).
++
++copy_messages_int([FromDBMod, ToDBMod, VHost, Date]) ->
++ ets:new(mod_logdb_temp, [named_table, set, public]),
++ {Time, Value} = timer:tc(?MODULE, copy_messages_int_tc, [[FromDBMod, ToDBMod, VHost, Date]]),
++ ets:delete_all_objects(mod_logdb_temp),
++ ets:delete(mod_logdb_temp),
++ ?INFO_MSG("copy_messages at ~p elapsed ~p sec", [Date, Time/1000000]),
++ Value.
++
++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),
++ ToStats = case mod_logdb:get_vhost_stats_at(VHost, Date) of
++ {ok, Stats} -> Stats;
++ {error, _} -> []
++ end,
++
++ FromStatsS = lists:keysort(1, FromStats),
++ ToStatsS = lists:keysort(1, ToStats),
++
++ StatsLength = length(FromStats),
++
++ CopyFun = if
++ % destination table is empty
++ FromDBMod /= mod_logdb_mnesia_old, ToStats == [] ->
++ fun({User, _Count}, Acc) ->
++ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
++ MAcc =
++ lists:foldl(fun(Msg, MFAcc) ->
++ ok = ToDBMod:log_message(VHost, Msg),
++ MFAcc + 1
++ end, 0, Msgs),
++ NewAcc = Acc + 1,
++ ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
++ %timer:sleep(100),
++ NewAcc
++ end;
++ % destination table is not empty
++ FromDBMod /= mod_logdb_mnesia_old, ToStats /= [] ->
++ fun({User, _Count}, Acc) ->
++ {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date),
++ lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
++ ets:insert(mod_logdb_temp, {Tst});
++ % mysql, pgsql removes final zeros after decimal point
++ (#msg{timestamp=Tst}) when length(Tst) < 16 ->
++ {F, _} = string:to_float(Tst++".0"),
++ [T] = io_lib:format("~.5f", [F]),
++ ets:insert(mod_logdb_temp, {T})
++ end, ToMsgs),
++ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
++ MAcc =
++ lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) ->
++ case ets:member(mod_logdb_temp, ToTimestamp) of
++ false ->
++ ok = ToDBMod:log_message(VHost, Msg),
++ ets:insert(mod_logdb_temp, {ToTimestamp}),
++ MFAcc + 1;
++ true ->
++ MFAcc
++ end
++ end, 0, Msgs),
++ NewAcc = Acc + 1,
++ ets:delete_all_objects(mod_logdb_temp),
++ ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
++ %timer:sleep(100),
++ NewAcc
++ end;
++ % copying from mod_logmnesia
++ true ->
++ fun({User, _Count}, Acc) ->
++ ToStats =
++ case ToDBMod:get_user_messages_at(User, VHost, Date) of
++ {ok, []} ->
++ ok;
++ {ok, ToMsgs} ->
++ lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
++ ets:insert(mod_logdb_temp, {Tst});
++ % mysql, pgsql removes final zeros after decimal point
++ (#msg{timestamp=Tst}) when length(Tst) < 15 ->
++ {F, _} = string:to_float(Tst++".0"),
++ [T] = io_lib:format("~.5f", [F]),
++ ets:insert(mod_logdb_temp, {T})
++ end, ToMsgs);
++ {error, _} ->
++ ok
++ end,
++ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
++
++ MAcc =
++ lists:foldl(
++ fun({msg, TU, TS, TR, FU, FS, FR, Type, Subj, Body, Timest},
++ MFAcc) ->
++ [Timestamp] = if is_float(Timest) == true ->
++ io_lib:format("~.5f", [Timest]);
++ % early versions of mod_logmnesia
++ is_integer(Timest) == true ->
++ io_lib:format("~.5f", [Timest-719528*86400.0]);
++ true ->
++ ?ERROR_MSG("Incorrect timestamp ~p", [Timest]),
++ throw(error)
++ end,
++ case ets:member(mod_logdb_temp, Timestamp) of
++ false ->
++ if
++ % from
++ TS == VHost ->
++ TMsg = #msg{timestamp=Timestamp,
++ owner_name=TU,
++ peer_name=FU, peer_server=FS, peer_resource=FR,
++ direction=from,
++ type=Type,
++ subject=Subj, body=Body},
++ ok = ToDBMod:log_message(VHost, TMsg);
++ true -> ok
++ end,
++ if
++ % to
++ FS == VHost ->
++ FMsg = #msg{timestamp=Timestamp,
++ owner_name=FU,
++ peer_name=TU, peer_server=TS, peer_resource=TR,
++ direction=to,
++ type=Type,
++ subject=Subj, body=Body},
++ ok = ToDBMod:log_message(VHost, FMsg);
++ true -> ok
++ end,
++ ets:insert(mod_logdb_temp, {Timestamp}),
++ MFAcc + 1;
++ true -> % not ets:member
++ MFAcc
++ end % case
++ end, 0, Msgs), % foldl
++ NewAcc = Acc + 1,
++ ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
++ %timer:sleep(100),
++ NewAcc
++ end % fun
++ end, % if FromDBMod /= mod_logdb_mnesia_old
++
++ if
++ FromStats == [] ->
++ ?INFO_MSG("No messages were found at ~p", [Date]);
++ FromStatsS == ToStatsS ->
++ ?INFO_MSG("Stats are equal at ~p", [Date]);
++ FromStatsS /= ToStatsS ->
++ lists:foldl(CopyFun, 0, FromStats),
++ ok = ToDBMod:rebuild_stats_at(VHost, Date)
++ %timer:sleep(1000)
++ end,
++
++ ok.
++
++list_to_bool(Num) ->
++ case lists:member(Num, ["t", "true", "y", "yes", "1"]) of
++ true ->
++ true;
++ false ->
++ case lists:member(Num, ["f", "false", "n", "no", "0"]) of
++ true ->
++ false;
++ false ->
++ error
++ end
++ end.
++
++bool_to_list(true) ->
++ "TRUE";
++bool_to_list(false) ->
++ "FALSE".
++
++list_to_string([]) ->
++ "";
++list_to_string(List) when is_list(List) ->
++ Str = lists:flatmap(fun(Elm) -> Elm ++ "\n" end, List),
++ lists:sublist(Str, length(Str)-1).
++
++string_to_list(null) ->
++ [];
++string_to_list([]) ->
++ [];
++string_to_list(String) ->
++ {ok, List} = regexp:split(String, "\n"),
++ List.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% ad-hoc (copy/pasted from mod_configure.erl)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++-define(ITEMS_RESULT(Allow, LNode, Fallback),
++ case Allow of
++ deny ->
++ Fallback;
++ allow ->
++ case get_local_items(LServer, LNode,
++ jlib:jid_to_string(To), Lang) of
++ {result, Res} ->
++ {result, Res};
++ {error, Error} ->
++ {error, Error}
++ end
++ end).
++
++get_local_items(Acc, From, #jid{lserver = LServer} = To, "", Lang) ->
++ case gen_mod:is_loaded(LServer, mod_adhoc) of
++ false ->
++ Acc;
++ _ ->
++ Items = case Acc of
++ {result, Its} -> Its;
++ empty -> []
++ end,
++ AllowUser = acl:match_rule(LServer, mod_logdb, From),
++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
++ if
++ AllowUser == allow; AllowAdmin == allow ->
++ case get_local_items(LServer, [],
++ jlib:jid_to_string(To), Lang) of
++ {result, Res} ->
++ {result, Items ++ Res};
++ {error, _Error} ->
++ {result, Items}
++ end;
++ true ->
++ {result, Items}
++ end
++ end;
++get_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) ->
++ case gen_mod:is_loaded(LServer, mod_adhoc) of
++ false ->
++ Acc;
++ _ ->
++ LNode = string:tokens(Node, "/"),
++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
++ case LNode of
++ ["mod_logdb"] ->
++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
++ ["mod_logdb_users"] ->
++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
++ ["mod_logdb_users", [$@ | _]] ->
++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
++ ["mod_logdb_users", _User] ->
++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
++ ["mod_logdb_settings"] ->
++ ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
++ _ ->
++ Acc
++ end
++ end.
++
++-define(NODE(Name, Node),
++ {xmlelement, "item",
++ [{"jid", Server},
++ {"name", translate:translate(Lang, Name)},
++ {"node", Node}], []}).
++
++get_local_items(_Host, [], Server, Lang) ->
++ {result,
++ [?NODE("Messages logging engine", "mod_logdb")]
++ };
++get_local_items(_Host, ["mod_logdb"], Server, Lang) ->
++ {result,
++ [?NODE("Messages logging engine users", "mod_logdb_users"),
++ ?NODE("Messages logging engine settings", "mod_logdb_settings")]
++ };
++get_local_items(Host, ["mod_logdb_users"], Server, Lang) ->
++ {result, get_all_vh_users(Host, Server, Lang)};
++get_local_items(_Host, ["mod_logdb_users", [$@ | Diap]], Server, Lang) ->
++ case catch ejabberd_auth:dirty_get_registered_users() of
++ {'EXIT', _Reason} ->
++ ?ERR_INTERNAL_SERVER_ERROR;
++ Users ->
++ SUsers = lists:sort([{S, U} || {U, S} <- Users]),
++ case catch begin
++ {ok, [S1, S2]} = regexp:split(Diap, "-"),
++ N1 = list_to_integer(S1),
++ N2 = list_to_integer(S2),
++ Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
++ lists:map(fun({S, U}) ->
++ ?NODE(U ++ "@" ++ S, "mod_logdb_users/" ++ U ++ "@" ++ S)
++ end, Sub)
++ end of
++ {'EXIT', _Reason} ->
++ ?ERR_NOT_ACCEPTABLE;
++ Res ->
++ {result, Res}
++ end
++ end;
++get_local_items(_Host, ["mod_logdb_users", _User], _Server, _Lang) ->
++ {result, []};
++get_local_items(_Host, ["mod_logdb_settings"], _Server, _Lang) ->
++ {result, []};
++get_local_items(_Host, Item, _Server, _Lang) ->
++ ?MYDEBUG("asked for items in ~p", [Item]),
++ {error, ?ERR_ITEM_NOT_FOUND}.
++
++-define(INFO_RESULT(Allow, Feats),
++ case Allow of
++ deny ->
++ {error, ?ERR_FORBIDDEN};
++ allow ->
++ {result, Feats}
++ end).
++
++get_local_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) ->
++ case gen_mod:is_loaded(LServer, mod_adhoc) of
++ false ->
++ Acc;
++ _ ->
++ LNode = string:tokens(Node, "/"),
++ AllowUser = acl:match_rule(LServer, mod_logdb, From),
++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
++ case LNode of
++ ["mod_logdb"] when AllowUser == allow; AllowAdmin == allow ->
++ ?INFO_RESULT(allow, [?NS_COMMANDS]);
++ ["mod_logdb"] ->
++ ?INFO_RESULT(deny, [?NS_COMMANDS]);
++ ["mod_logdb_users"] ->
++ ?INFO_RESULT(AllowAdmin, []);
++ ["mod_logdb_users", [$@ | _]] ->
++ ?INFO_RESULT(AllowAdmin, []);
++ ["mod_logdb_users", _User] ->
++ ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS]);
++ ["mod_logdb_settings"] ->
++ ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS]);
++ [] ->
++ Acc;
++ _ ->
++ %?MYDEBUG("asked for ~p features: ~p", [LNode, Allow]),
++ Acc
++ end
++ end.
++
++-define(INFO_IDENTITY(Category, Type, Name, Lang),
++ [{xmlelement, "identity",
++ [{"category", Category},
++ {"type", Type},
++ {"name", translate:translate(Lang, Name)}], []}]).
++
++-define(INFO_COMMAND(Name, Lang),
++ ?INFO_IDENTITY("automation", "command-node", Name, Lang)).
++
++get_local_identity(Acc, _From, _To, Node, Lang) ->
++ LNode = string:tokens(Node, "/"),
++ case LNode of
++ ["mod_logdb"] ->
++ ?INFO_COMMAND("Messages logging engine", Lang);
++ ["mod_logdb_users"] ->
++ ?INFO_COMMAND("Messages logging engine users", Lang);
++ ["mod_logdb_users", [$@ | _]] ->
++ Acc;
++ ["mod_logdb_users", User] ->
++ ?INFO_COMMAND(User, Lang);
++ ["mod_logdb_settings"] ->
++ ?INFO_COMMAND("Messages logging engine settings", Lang);
++ [] ->
++ Acc;
++ _ ->
++ Acc
++ end.
++
++%get_sm_items(Acc, From, To, Node, Lang) ->
++% ?MYDEBUG("get_sm_items Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]),
++% Acc.
++
++%get_sm_features(Acc, From, To, Node, Lang) ->
++% ?MYDEBUG("get_sm_features Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]),
++% Acc.
++
++%get_sm_identity(Acc, From, To, Node, Lang) ->
++% ?MYDEBUG("get_sm_identity Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]),
++% Acc.
++
++adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To,
++ Lang) ->
++ Items = case Acc of
++ {result, Its} -> Its;
++ empty -> []
++ end,
++ Nodes = recursively_get_local_items(LServer, "", Server, Lang),
++ Nodes1 = lists:filter(
++ fun(N) ->
++ Nd = xml:get_tag_attr_s("node", N),
++ F = get_local_features([], From, To, Nd, Lang),
++ case F of
++ {result, [?NS_COMMANDS]} ->
++ true;
++ _ ->
++ false
++ end
++ end, Nodes),
++ {result, Items ++ Nodes1}.
++
++recursively_get_local_items(_LServer, "mod_logdb_users", _Server, _Lang) ->
++ [];
++recursively_get_local_items(LServer, Node, Server, Lang) ->
++ LNode = string:tokens(Node, "/"),
++ Items = case get_local_items(LServer, LNode, Server, Lang) of
++ {result, Res} ->
++ Res;
++ {error, _Error} ->
++ []
++ end,
++ Nodes = lists:flatten(
++ lists:map(
++ fun(N) ->
++ S = xml:get_tag_attr_s("jid", N),
++ Nd = xml:get_tag_attr_s("node", N),
++ if (S /= Server) or (Nd == "") ->
++ [];
++ true ->
++ [N, recursively_get_local_items(
++ LServer, Nd, Server, Lang)]
++ end
++ end, Items)),
++ Nodes.
++
++-define(COMMANDS_RESULT(Allow, From, To, Request),
++ case Allow of
++ deny ->
++ {error, ?ERR_FORBIDDEN};
++ allow ->
++ adhoc_local_commands(From, To, Request)
++ end).
++
++adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
++ #adhoc_request{node = Node} = Request) ->
++ LNode = string:tokens(Node, "/"),
++ AllowUser = acl:match_rule(LServer, mod_logdb, From),
++ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
++ case LNode of
++ ["mod_logdb"] when AllowUser == allow; AllowAdmin == allow ->
++ ?COMMANDS_RESULT(allow, From, To, Request);
++ ["mod_logdb_users", _User] when AllowAdmin == allow ->
++ ?COMMANDS_RESULT(allow, From, To, Request);
++ ["mod_logdb_settings"] when AllowAdmin == allow ->
++ ?COMMANDS_RESULT(allow, From, To, Request);
++ _ ->
++ Acc
++ end.
++
++adhoc_local_commands(From, #jid{lserver = LServer} = _To,
++ #adhoc_request{lang = Lang,
++ node = Node,
++ sessionid = SessionID,
++ action = Action,
++ xdata = XData} = Request) ->
++ LNode = string:tokens(Node, "/"),
++ %% If the "action" attribute is not present, it is
++ %% understood as "execute". If there was no <actions/>
++ %% element in the first response (which there isn't in our
++ %% case), "execute" and "complete" are equivalent.
++ ActionIsExecute = lists:member(Action,
++ ["", "execute", "complete"]),
++ if Action == "cancel" ->
++ %% User cancels request
++ adhoc:produce_response(
++ Request,
++ #adhoc_response{status = canceled});
++ XData == false, ActionIsExecute ->
++ %% User requests form
++ case get_form(LServer, LNode, From, Lang) of
++ {result, Form} ->
++ adhoc:produce_response(
++ Request,
++ #adhoc_response{status = executing,
++ elements = Form});
++ {error, Error} ->
++ {error, Error}
++ end;
++ XData /= false, ActionIsExecute ->
++ %% User returns form.
++ case jlib:parse_xdata_submit(XData) of
++ invalid ->
++ {error, ?ERR_BAD_REQUEST};
++ Fields ->
++ case set_form(From, LServer, LNode, Lang, Fields) of
++ {result, _Res} ->
++ adhoc:produce_response(
++ #adhoc_response{lang = Lang,
++ node = Node,
++ sessionid = SessionID,
++ status = completed});
++ {error, Error} ->
++ {error, Error}
++ end
++ end;
++ true ->
++ {error, ?ERR_BAD_REQUEST}
++ end.
++
++-define(LISTLINE(Label, Value),
++ {xmlelement, "option", [{"label", Label}],
++ [{xmlelement, "value", [], [{xmlcdata, Value}]}]}).
++-define(DEFVAL(Value), {xmlelement, "value", [], [{xmlcdata, Value}]}).
++
++get_user_form(LUser, LServer, Lang) ->
++ %From = jlib:jid_to_string(jlib:jid_remove_resource(Jid)),
++ #user_settings{dolog_default=DLD,
++ dolog_list=DLL,
++ donotlog_list=DNLL} = get_user_settings(LUser, LServer),
++ {result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
++ [{xmlelement, "title", [],
++ [{xmlcdata,
++ translate:translate(
++ Lang, "Messages logging engine settings")}]},
++ {xmlelement, "instructions", [],
++ [{xmlcdata,
++ translate:translate(
++ Lang, "Set logging preferences")++ ": " ++ LUser ++ "@" ++ LServer}]},
++ % default to log
++ {xmlelement, "field", [{"type", "list-single"},
++ {"label",
++ translate:translate(Lang, "Default")},
++ {"var", "dolog_default"}],
++ [?DEFVAL(atom_to_list(DLD)),
++ ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"),
++ ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false")
++ ]},
++ % do log list
++ {xmlelement, "field", [{"type", "text-multi"},
++ {"label",
++ translate:translate(
++ Lang, "Log Messages")},
++ {"var", "dolog_list"}],
++ [{xmlelement, "value", [], [{xmlcdata, list_to_string(DLL)}]}]},
++ % do not log list
++ {xmlelement, "field", [{"type", "text-multi"},
++ {"label",
++ translate:translate(
++ Lang, "Do Not Log Messages")},
++ {"var", "donotlog_list"}],
++ [{xmlelement, "value", [], [{xmlcdata, list_to_string(DNLL)}]}]}
++ ]}]}.
++
++get_settings_form(Host, Lang) ->
++ #state{dbmod=DBMod,
++ dbs=DBs,
++ dolog_default=DLD,
++ ignore_jids=IgnoreJids,
++ groupchat=GroupChat,
++ purge_older_days=PurgeDaysT,
++ poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
++
++ Backends = lists:map(fun({Backend, _Opts}) ->
++ ?LISTLINE(atom_to_list(Backend), atom_to_list(Backend))
++ end, DBs),
++ DB = lists:sublist(atom_to_list(DBMod), length(atom_to_list(?MODULE)) + 2, length(atom_to_list(DBMod))),
++ DBsL = lists:append([?DEFVAL(DB)], Backends),
++
++ PurgeDays =
++ case PurgeDaysT of
++ never -> "never";
++ Num when is_integer(Num) -> integer_to_list(Num);
++ _ -> "unknown"
++ end,
++ {result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
++ [{xmlelement, "title", [],
++ [{xmlcdata,
++ translate:translate(
++ Lang, "Messages logging engine settings") ++ " (run-time)"}]},
++ {xmlelement, "instructions", [],
++ [{xmlcdata,
++ translate:translate(
++ Lang, "Set run-time settings")}]},
++ % backends
++ {xmlelement, "field", [{"type", "list-single"},
++ {"label",
++ translate:translate(Lang, "Backend")},
++ {"var", "backend"}],
++ DBsL},
++ % dbs
++ {xmlelement, "field", [{"type", "text-multi"},
++ {"label",
++ translate:translate(
++ Lang, "dbs")},
++ {"var", "dbs"}],
++ [{xmlelement, "value", [], [{xmlcdata, lists:flatten(io_lib:format("~p.",[DBs]))}]}]},
++ % default to log
++ {xmlelement, "field", [{"type", "list-single"},
++ {"label",
++ translate:translate(Lang, "Default")},
++ {"var", "dolog_default"}],
++ [?DEFVAL(atom_to_list(DLD)),
++ ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"),
++ ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false")
++ ]},
++ % groupchat
++ {xmlelement, "field", [{"type", "list-single"},
++ {"label",
++ translate:translate(Lang, "Groupchat messages logging")},
++ {"var", "groupchat"}],
++ [?DEFVAL(atom_to_list(GroupChat)),
++ ?LISTLINE("all", "all"),
++ ?LISTLINE("none", "none"),
++ ?LISTLINE("send", "send"),
++ ?LISTLINE("half", "half")
++ ]},
++ % ignore_jids
++ {xmlelement, "field", [{"type", "text-multi"},
++ {"label",
++ translate:translate(
++ Lang, "Jids/Domains to ignore")},
++ {"var", "ignore_list"}],
++ [{xmlelement, "value", [], [{xmlcdata, list_to_string(IgnoreJids)}]}]},
++ % purge older days
++ {xmlelement, "field", [{"type", "text-single"},
++ {"label",
++ translate:translate(
++ Lang, "Purge messages older than (days)")},
++ {"var", "purge_older_days"}],
++ [{xmlelement, "value", [], [{xmlcdata, PurgeDays}]}]},
++ % poll users settings
++ {xmlelement, "field", [{"type", "text-single"},
++ {"label",
++ translate:translate(
++ Lang, "Poll users settings (seconds)")},
++ {"var", "poll_users_settings"}],
++ [{xmlelement, "value", [], [{xmlcdata, integer_to_list(PollTime)}]}]}
++ ]}]}.
++
++get_form(_Host, ["mod_logdb"], #jid{luser = LUser, lserver = LServer} = _Jid, Lang) ->
++ get_user_form(LUser, LServer, Lang);
++get_form(_Host, ["mod_logdb_users", User], _JidFrom, Lang) ->
++ #jid{luser=LUser, lserver=LServer} = jlib:string_to_jid(User),
++ get_user_form(LUser, LServer, Lang);
++get_form(Host, ["mod_logdb_settings"], _JidFrom, Lang) ->
++ get_settings_form(Host, Lang);
++get_form(_Host, Command, _, _Lang) ->
++ ?MYDEBUG("asked for form ~p", [Command]),
++ {error, ?ERR_SERVICE_UNAVAILABLE}.
++
++check_log_list([Head | Tail]) ->
++ case lists:member($@, Head) of
++ true -> ok;
++ false -> throw(error)
++ end,
++ % this check for Head to be valid jid
++ case jlib:string_to_jid(Head) of
++ error ->
++ throw(error);
++ _ ->
++ check_log_list(Tail)
++ end;
++check_log_list([]) ->
++ ok.
++
++check_ignore_list([Head | Tail]) ->
++ case lists:member($@, Head) of
++ true -> ok;
++ false -> throw(error)
++ end,
++ % this check for Head to be valid jid
++ case jlib:string_to_jid(Head) of
++ error ->
++ % this check for Head to be valid domain "@domain.org"
++ case lists:nth(1, Head) of
++ $@ ->
++ % TODO: this allows spaces and special characters in Head. May be change to nodeprep?
++ case jlib:nameprep(lists:delete($@, Head)) of
++ error -> throw(error);
++ _ -> check_log_list(Tail)
++ end;
++ _ -> throw(error)
++ end;
++ _ ->
++ check_ignore_list(Tail)
++ end;
++check_ignore_list([]) ->
++ ok.
++
++parse_users_settings(XData) ->
++ DLD = case lists:keysearch("dolog_default", 1, XData) of
++ {value, {_, [String]}} when String == "true"; String == "false" ->
++ list_to_bool(String);
++ _ ->
++ throw(bad_request)
++ end,
++ DLL = case lists:keysearch("dolog_list", 1, XData) of
++ false ->
++ throw(bad_request);
++ {value, {_, [[]]}} ->
++ [];
++ {value, {_, List1}} ->
++ case catch check_log_list(List1) of
++ error ->
++ throw(bad_request);
++ ok ->
++ List1
++ end
++ end,
++ DNLL = case lists:keysearch("donotlog_list", 1, XData) of
++ false ->
++ throw(bad_request);
++ {value, {_, [[]]}} ->
++ [];
++ {value, {_, List2}} ->
++ case catch check_log_list(List2) of
++ error ->
++ throw(bad_request);
++ ok ->
++ List2
++ end
++ end,
++ #user_settings{dolog_default=DLD,
++ dolog_list=DLL,
++ donotlog_list=DNLL}.
++
++parse_module_settings(XData) ->
++ DLD = case lists:keysearch("dolog_default", 1, XData) of
++ {value, {_, [Str1]}} when Str1 == "true"; Str1 == "false" ->
++ list_to_bool(Str1);
++ _ ->
++ throw(bad_request)
++ end,
++ GroupChat = case lists:keysearch("groupchat", 1, XData) of
++ {value, {_, [Str2]}} when Str2 == "none";
++ Str2 == "all";
++ Str2 == "send";
++ Str2 == "half" ->
++ list_to_atom(Str2);
++ _ ->
++ throw(bad_request)
++ end,
++ Ignore = case lists:keysearch("ignore_list", 1, XData) of
++ {value, {_, List}} ->
++ case catch check_ignore_list(List) of
++ ok ->
++ List;
++ error ->
++ throw(bad_request)
++ end;
++ _ ->
++ throw(bad_request)
++ end,
++ Purge = case lists:keysearch("purge_older_days", 1, XData) of
++ {value, {_, ["never"]}} ->
++ never;
++ {value, {_, [Str3]}} ->
++ case catch list_to_integer(Str3) of
++ {'EXIT', {badarg, _}} -> throw(bad_request);
++ Int1 -> Int1
++ end;
++ _ ->
++ throw(bad_request)
++ end,
++ Poll = case lists:keysearch("poll_users_settings", 1, XData) of
++ {value, {_, [Str4]}} ->
++ case catch list_to_integer(Str4) of
++ {'EXIT', {badarg, _}} -> throw(bad_request);
++ Int2 -> Int2
++ end;
++ _ ->
++ throw(bad_request)
++ end,
++ #state{dolog_default=DLD,
++ groupchat=GroupChat,
++ ignore_jids=Ignore,
++ purge_older_days=Purge,
++ poll_users_settings=Poll}.
++
++set_form(From, _Host, ["mod_logdb"], _Lang, XData) ->
++ #jid{luser=LUser, lserver=LServer} = From,
++ case catch parse_users_settings(XData) of
++ bad_request ->
++ {error, ?ERR_BAD_REQUEST};
++ UserSettings ->
++ case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
++ ok ->
++ {result, []};
++ error ->
++ {error, ?ERR_INTERNAL_SERVER_ERROR}
++ end
++ end;
++set_form(_From, _Host, ["mod_logdb_users", User], _Lang, XData) ->
++ #jid{luser=LUser, lserver=LServer} = jlib:string_to_jid(User),
++ case catch parse_users_settings(XData) of
++ bad_request -> {error, ?ERR_BAD_REQUEST};
++ UserSettings ->
++ case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
++ ok ->
++ {result, []};
++ error ->
++ {error, ?ERR_INTERNAL_SERVER_ERROR}
++ end
++ end;
++set_form(_From, Host, ["mod_logdb_settings"], _Lang, XData) ->
++ case catch parse_module_settings(XData) of
++ bad_request -> {error, ?ERR_BAD_REQUEST};
++ Settings ->
++ case mod_logdb:set_module_settings(Host, Settings) of
++ ok ->
++ {result, []};
++ error ->
++ {error, ?ERR_INTERNAL_SERVER_ERROR}
++ end
++ end;
++set_form(From, _Host, Node, _Lang, XData) ->
++ User = jlib:jid_to_string(jlib:jid_remove_resource(From)),
++ ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]),
++ {error, ?ERR_SERVICE_UNAVAILABLE}.
++
++%adhoc_sm_items(Acc, From, To, Request) ->
++% ?MYDEBUG("adhoc_sm_items Acc=~p From=~p To=~p Request=~p", [Acc, From, To, Request]),
++% Acc.
++
++%adhoc_sm_commands(Acc, From, To, Request) ->
++% ?MYDEBUG("adhoc_sm_commands Acc=~p From=~p To=~p Request=~p", [Acc, From, To, Request]),
++% Acc.
++
++get_all_vh_users(Host, Server, Lang) ->
++ case catch ejabberd_auth:get_vh_registered_users(Host) of
++ {'EXIT', _Reason} ->
++ [];
++ Users ->
++ SUsers = lists:sort([{S, U} || {U, S} <- Users]),
++ case length(SUsers) of
++ N when N =< 100 ->
++ lists:map(fun({S, U}) ->
++ ?NODE(U ++ "@" ++ S, "mod_logdb_users/" ++ U ++ "@" ++ S)
++ end, SUsers);
++ N ->
++ NParts = trunc(math:sqrt(N * 0.618)) + 1,
++ M = trunc(N / NParts) + 1,
++ lists:map(fun(K) ->
++ L = K + M - 1,
++ Node =
++ "@" ++ integer_to_list(K) ++
++ "-" ++ integer_to_list(L),
++ {FS, FU} = lists:nth(K, SUsers),
++ {LS, LU} =
++ if L < N -> lists:nth(L, SUsers);
++ true -> lists:last(SUsers)
++ end,
++ Name =
++ FU ++ "@" ++ FS ++
++ " -- " ++
++ LU ++ "@" ++ LS,
++ ?NODE(Name, "mod_logdb_users/" ++ Node)
++ 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
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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_server callbacks
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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{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']]}])
++ 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}.
++
++handle_info(Info, State) ->
++ ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
++ {noreply, State}.
++
++terminate(_Reason, _State) ->
++ ok.
++
++code_change(_OldVsn, State, _Extra) ->
++ {ok, State}.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++log_message_int(VHost, #msg{timestamp=Timestamp}=Msg) ->
++ Date = mod_logdb:convert_timestamp_brief(Timestamp),
++
++ 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.
++
++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.
++
++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).
++
++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.
++
++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).
++
++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.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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.
++
++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.
++
++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 @@
++%%%----------------------------------------------------------------------
++%%% File : mod_logdb_mysql.erl
++%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
++%%% Purpose : MySQL backend for mod_logdb
++%%% Version : trunk
++%%% Id : $Id$
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
++
++-module(mod_logdb_mysql).
++-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]).
++
++% gen_server call timeout
++-define(CALL_TIMEOUT, 60000).
++-define(TIMEOUT, 60000).
++-define(INDEX_SIZE, integer_to_list(170)).
++-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}).
++
++% replace "." with "_"
++escape_vhost(VHost) -> lists:map(fun(46) -> 95;
++ (A) -> A
++ end, VHost).
++prefix() ->
++ "`logdb_".
++
++suffix(VHost) ->
++ "_" ++ escape_vhost(VHost) ++ "`".
++
++messages_table(VHost, Date) ->
++ prefix() ++ "messages_" ++ Date ++ suffix(VHost).
++
++stats_table(VHost) ->
++ prefix() ++ "stats" ++ 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).
++
++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
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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_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}
++ 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 =
++ 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};
++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,
++ {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,"\";"],
++ 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) ->
++ 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, _} ->
++ 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}
++ 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;"
++ ],
++ 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
++ 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),"\";"],
++ 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
++ 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,"\";"],
++
++ 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
++ 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]),
++ {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}.
++
++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) ->
++ ok.
++
++code_change(_OldVsn, State, _Extra) ->
++ {ok, State}.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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_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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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, "');"],
++
++ 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]),
++ 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
++ 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
++ end,
++ {updated, _} = sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
++ Res.
++
++
++delete_nonexistent_stats(DBRef, VHost) ->
++ Dates = get_dates_int(DBRef, VHost),
++ STable = stats_table(VHost),
++
++ Temp = lists:flatmap(fun(Date) ->
++ ["\"",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 sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% tables internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++create_stats_table(DBRef, 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
++ end.
++
++create_settings_table(DBRef, 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.
++
++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_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.
++
++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.
++
++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.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% internal ets cache (users, servers, resources)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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.
++
++%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_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.
++
++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.
++
++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.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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, ["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}}}.
++
++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.
++
++sql_query_internal_silent(DBRef, Query) ->
++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
++ get_result(mysql_conn:fetch(DBRef, Query, self(), ?TIMEOUT)).
++
++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 @@
++%%%----------------------------------------------------------------------
++%%% 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
++%%% Version : trunk
++%%% Id : $Id$
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
++
++-module(mod_logdb_mysql5).
++-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]).
++
++% gen_server call timeout
++-define(CALL_TIMEOUT, 60000).
++-define(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}).
++
++% replace "." with "_"
++escape_vhost(VHost) -> lists:map(fun(46) -> 95;
++ (A) -> A
++ end, VHost).
++prefix() ->
++ "`logdb_".
++
++suffix(VHost) ->
++ "_" ++ escape_vhost(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, 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).
++
++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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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_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, [65536, 131072], LogFun) 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),
++ erlang:monitor(process, DBRef),
++ {ok, #state{dbref=DBRef, vhost=VHost}};
++ {error, Reason} ->
++ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
++ {stop, db_connection_failed}
++ end.
++
++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, "');"],
++
++ Reply =
++ 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,
++ {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};
++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,
++ {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 =
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, _} ->
++ ok;
++ {aborted, _} ->
++ 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,"\" ",
++ "ORDER BY count DESC;"
++ ],
++ Reply =
++ case sql_query_internal(DBRef, Query) of
++ {data, Result} ->
++ {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
++ {error, Reason} ->
++ {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};
++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,"\";"],
++ 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}
++ 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;"
++ ],
++ 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
++ 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=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
++ 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
++ 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,";"],
++
++ 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
++ end,
++ {reply, Reply, State};
++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(Msg, State) ->
++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
++ {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) ->
++ ok.
++
++code_change(_OldVsn, State, _Extra) ->
++ {ok, State}.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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_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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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
++ end,
++ {updated, _} = sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
++ Res.
++
++
++delete_nonexistent_stats(DBRef, VHost) ->
++ Dates = get_dates_int(DBRef, VHost),
++ STable = stats_table(VHost),
++
++ Temp = lists:flatmap(fun(Date) ->
++ ["\"",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 sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ error
++ end.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% tables internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++create_stats_table(DBRef, VHost) ->
++ SName = stats_table(VHost),
++ Query = ["CREATE TABLE ",SName," (",
++ "owner_id MEDIUMINT UNSIGNED, ",
++ "at VARCHAR(11), ",
++ "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
++ end.
++
++create_settings_table(DBRef, 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.
++
++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]),
++ ok;
++ {error, _} ->
++ error
++ end.
++
++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]),
++ ok;
++ {error, _} ->
++ error
++ end.
++
++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]),
++ 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]),
++ ok;
++ {error, _} ->
++ error
++ end.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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, ["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}}}.
++
++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.
++
++sql_query_internal_silent(DBRef, Query) ->
++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
++ get_result(mysql_conn:fetch(DBRef, Query, self(), ?TIMEOUT)).
++
++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_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, _, _} = regexp:match(Reason, "#23000"),
++ ?ERROR_MSG("Duplicate key name for ~p", [User]),
++ {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
++ ClID
++ end;
++ {data, [[DBId]]} ->
++ DBId
++ 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;
++
++ 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;
++
++ SET @notable = 0;
++ PREPARE insertmsg FROM @iq;
++ EXECUTE insertmsg;
++ ELSEIF @notable = 0 THEN
++ EXECUTE insertmsg;
++ END IF;
++
++ DEALLOCATE PREPARE insertmsg;
++
++ 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 @@
++%%%----------------------------------------------------------------------
++%%% File : mod_logdb_pgsql.erl
++%%% Author : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
++%%% Purpose : Posgresql backend for mod_logdb
++%%% Version : trunk
++%%% Id : $Id$
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
++
++-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).
++
++% 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]).
++
++% gen_server call timeout
++-define(CALL_TIMEOUT, 60000).
++-define(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, 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).
++
++% 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, "\""]).
++
++stats_table(VHost, Schema) ->
++ prefix(Schema) ++ "stats" ++ 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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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_server callbacks
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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"),
++
++ case catch pgsql:connect(Server, DB, User, Password, Port) 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),
++ erlang:monitor(process, DBRef),
++ {ok, #state{dbref=DBRef, vhost=VHost, schema=Schema}};
++ % 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.
++
++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),
++
++ 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, "');"],
++
++ 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),
++ {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) ->
++ 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);
++ {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),";"]),
++ 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, _} ->
++ 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, count ",
++ "FROM ",SName," ",
++ "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id "
++ "WHERE at='",Date,"';"
++ ],
++ 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) ->
++ 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) ->
++ 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}.
++
++handle_cast(Msg, State) ->
++ ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
++ {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) ->
++ ok.
++
++code_change(_OldVsn, State, _Extra) ->
++ {ok, State}.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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_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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% internals
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++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);
++ {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,"';"],
++
++ {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,
++
++ case sql_transaction_internal(DBRef, Fun) of
++ {atomic, _} ->
++ ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
++ ok;
++ {aborted, _} ->
++ error
++ end.
++
++delete_nonexistent_stats(DBRef, VHost, Schema) ->
++ Dates = get_dates_int(DBRef, VHost),
++ STable = stats_table(VHost, Schema),
++
++ Temp = lists:flatmap(fun(Date) ->
++ ["'",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 sql_query_internal(DBRef, Query) of
++ {updated, _} ->
++ ok;
++ {error, _} ->
++ 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
++ end.
++
++create_settings_table(DBRef, VHost, 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.
++
++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
++ end.
++
++create_servers_table(DBRef, VHost, 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.
++
++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
++ end.
++
++create_internals(DBRef, VHost, Schema) ->
++ case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
++ {updated, _} ->
++ ?MYDEBUG("Created logmessage for ~p", [VHost]),
++ ok;
++ {error, _} ->
++ 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
++ 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;
++
++ 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;
++
++ 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;
++
++ 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;
++
++ 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)';
++
++ viewname := '~s.\"v_' || trim(both '~s.\"' from tablename) || '\"';
++
++ 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 || ''',''' || msubj || ''',''' || mbody || ''',' || mtimestamp || ')';
++ END;
++
++ 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);
++ 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.
++
++% 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}}}.
++
++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.
++
++sql_query_internal_silent(DBRef, Query) ->
++ ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
++ get_result(pgsql:squery(DBRef, Query)).
++
++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}.
++
+--- 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 @@
++%%%----------------------------------------------------------------------
++%%% 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)
++%%% Version : trunk
++%%% Id : $Id$
++%%% Url : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
++%%%----------------------------------------------------------------------
++
++-module(mod_logdb_mnesia_old).
++-author('o.palij@gmail.com').
++-vsn('$Revision$').
++
++-include("ejabberd.hrl").
++-include("jlib.hrl").
++
++-behaviour(gen_logdb).
++
++-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]).
++
++-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).
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% gen_logdb 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
++ end.
++
++stop(_VHost) ->
++ ok.
++
++log_message(_VHost, _Msg) ->
++ error.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% gen_logdb callbacks (maintaince)
++%
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++rebuild_stats(_VHost) ->
++ ok.
++
++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.
++
++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.
++
++get_user_stats(_User, _VHost) ->
++ {error, "does not emplemented"}.
++
++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.
++
++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
++%
++% 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.
+--- 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/
++%%%----------------------------------------------------------------------
++
++-module(gen_logdb).
++-author('o.palij@gmail.com').
++-vsn('$Revision$').
++
++-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"
++ % User = list() -> "admin"
++ {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}
++ ];
++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);
++
++process_admin(Host,
++ #request{us = US,
++ path = ["messages", Date],
++ q = Query,
++ lang = Lang} = Request) when is_list(Host) ->
++ Res = vhost_messages_stats_at(Host, Query, Lang, Date),
++ make_xhtml(Res, Host, Lang);
++
++process_admin(Host,
++ #request{us = US,
++ path = ["user", U, "messages"],
++ q = Query,
++ lang = Lang} = Request) ->
++ Res = user_messages_stats(U, Host, Query, Lang),
++ make_xhtml(Res, Host, Lang);
++
++process_admin(Host,
++ #request{us = US,
++ path = ["user", U, "messages", Date],
++ q = Query,
++ lang = Lang} = Request) ->
++ Res = user_messages_stats_at(U, Host, Query, Lang, Date),
++ make_xhtml(Res, Host, Lang);
++
++process_admin(Host,
+ #request{us = US,
+ path = ["user", U, "roster"],
+ q = Query,
+@@ -1442,6 +1492,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;
++ _ ->
++ 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
++ 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),
++
++ Settings = case gen_mod:is_loaded(Server, mod_logdb) of
++ true ->
++ mod_logdb:get_user_settings(User, Server);
++ false ->
++ []
++ 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,
++
++ ?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.
++
++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.
++
++user_messages_stats(User, Server, Query, Lang) ->
++ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
++ Jid = us_to_list(US),
++
++ Res = case catch mod_logdb:user_messages_parse_query(User, Server, Query) of
++ {'EXIT', Reason} ->
++ ?ERROR_MSG("~p", [Reason]),
++ error;
++ VResult -> VResult
++ end,
++
++ {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]),
++
++ 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.
++
++user_messages_stats_at(User, Server, Query, Lang, Date) ->
++ US = {jlib:nodeprep(User), jlib:nameprep(Server)},
++ Jid = us_to_list(US),
++
++ {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,
++
++ 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),
++
++ % 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,
++
++ % 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)),
++
++ % 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,
++
++ 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)),
++
++ [?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.
++
+ user_roster_parse_query(User, Server, Items, Query, Admin) ->
+ case lists:keysearch("addjid", 1, Query) of
+ {value, _} ->
+@@ -1704,10 +2102,41 @@
+ []}]}}),
+ throw(submitted);
+ false ->
+- ok
+- end
+-
+- end
++ case lists:keysearch(
++ "donotlog" ++ term_to_id(JID), 1, Query) of
++ {value, _} ->
++ Peer = jlib:jid_to_string(JID),
++ Settings = mod_logdb:get_user_settings(User, Server),
++ DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of
++ false -> lists:append(Settings#user_settings.donotlog_list, [Peer]);
++ true -> Settings#user_settings.donotlog_list
++ end,
++ DLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.dolog_list),
++ Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
++ % TODO: check returned value
++ ok = mod_logdb:set_user_settings(User, Server, Sett),
++ throw(nothing);
++ false ->
++ case lists:keysearch(
++ "dolog" ++ 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
++ false -> lists:append(Settings#user_settings.dolog_list, [Peer]);
++ true -> Settings#user_settings.dolog_list
++ end,
++ DNLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.donotlog_list),
++ Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
++ % TODO: check returned value
++ ok = mod_logdb:set_user_settings(User, Server, Sett),
++ throw(nothing);
++ false ->
++ ok
++ end % dolog
++ end % donotlog
++ 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", "Хост"}.
+
++% 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"}.
++{"Logged messages for ", "Gelogde berichten van "}.
++{" at ", " op "}.
++{"No logged messages for ", "Geen gelogde berichten van "}.
++{"Date, Time", "Datum en tijd"}.
++{"Direction: Jid", "Richting: Jabber ID"}.
++{"Subject", "Onderwerp"}.
++{"Body", "Berichtveld"}.
++{"Messages", "Berichten"}.
+
+--- 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", ""}.
+
++% mod_logdb
++{"Users Messages", "Wiadomości użytkownika"}.
++{"Date", "Data"}.
++{"Count", "Liczba"}.
++{"Logged messages for ", "Zapisane wiadomości dla "}.
++{" at ", " o "}.
++{"No logged messages for ", "Brak zapisanych wiadomości dla "}.
++{"Date, Time", "Data, Godzina"}.
++{"Direction: Jid", "Kierunek: Jid"}.
++{"Subject", "Temat"}.
++{"Body", "Treść"}.
++{"Messages","Wiadomości"}.
++{"Filter Selected", "Odfiltruj zaznaczone"}.
++{"Do Not Log Messages", "Nie zapisuj wiadomości"}.
++{"Log Messages", "Zapisuj wiadomości"}.
++{"Messages logging engine", "System zapisywania historii rozmów"}.
++{"Default", "Domyślne"}.
++{"Set logging preferences", "Ustaw preferencje zapisywania"}.
++{"Messages logging engine settings", "Ustawienia systemu logowania"}.
++{"Set run-time settings", "Zapisz ustawienia systemu logowania"}.
++{"Groupchat messages logging", "Zapisywanie rozmów z konferencji"}.
++{"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)"}.