]> git.pld-linux.org Git - packages/ejabberd.git/blob - ejabberd-mod_logdb.patch
- sever-side message logging support
[packages/ejabberd.git] / ejabberd-mod_logdb.patch
1 --- src/mod_logdb.erl.orig      Tue Dec 11 14:23:19 2007
2 +++ src/mod_logdb.erl   Thu Sep 20 15:26:21 2007
3 @@ -0,0 +1,1656 @@
4 +%%%----------------------------------------------------------------------
5 +%%% File    : mod_logdb.erl
6 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
7 +%%% Purpose : Frontend for log user messages to db
8 +%%% Version : trunk
9 +%%% Id      : $Id$
10 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
11 +%%%----------------------------------------------------------------------
12 +
13 +-module(mod_logdb).
14 +-author('o.palij@gmail.com').
15 +-vsn('$Revision$').
16 +
17 +-behaviour(gen_server).
18 +-behaviour(gen_mod).
19 +
20 +% supervisor
21 +-export([start_link/2]).
22 +% gen_mod
23 +-export([start/2,stop/1]).
24 +% gen_server
25 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
26 +% hooks
27 +-export([send_packet/3, receive_packet/4, offline_packet/3]).
28 +-export([get_local_identity/5,
29 +         get_local_features/5, 
30 +         get_local_items/5,
31 +         adhoc_local_items/4,
32 +         adhoc_local_commands/4
33 +%         get_sm_identity/5,
34 +%         get_sm_features/5,
35 +%         get_sm_items/5,
36 +%         adhoc_sm_items/4,
37 +%         adhoc_sm_commands/4]).
38 +        ]).
39 +% ejabberdctl
40 +-export([rebuild_stats/3,
41 +         copy_messages/1, copy_messages_ctl/3, copy_messages_int_tc/1]).
42 +%
43 +-export([get_vhost_stats/1, get_vhost_stats_at/2,
44 +         get_user_stats/2, get_user_messages_at/3,
45 +         get_dates/1,
46 +         sort_stats/1,
47 +         convert_timestamp/1, convert_timestamp_brief/1,
48 +         get_user_settings/2, set_user_settings/3,
49 +         user_messages_at_parse_query/4, user_messages_parse_query/3,
50 +         vhost_messages_parse_query/2, vhost_messages_at_parse_query/4,
51 +         list_to_bool/1, bool_to_list/1,
52 +         list_to_string/1, string_to_list/1,
53 +         get_module_settings/1, set_module_settings/2,
54 +         purge_old_records/2]).
55 +
56 +-include("mod_logdb.hrl").
57 +-include("ejabberd.hrl").
58 +-include("jlib.hrl").
59 +-include("ejabberd_ctl.hrl").
60 +-include("adhoc.hrl").
61 +
62 +-define(PROCNAME, ejabberd_mod_logdb).
63 +% gen_server call timeout
64 +-define(CALL_TIMEOUT, 60000).
65 +
66 +-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings}).
67 +
68 +ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ VHost).
69 +
70 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
71 +%
72 +% gen_mod/gen_server callbacks
73 +%
74 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
75 +% ejabberd starts module
76 +start(VHost, Opts) ->
77 +    ChildSpec =
78 +        {gen_mod:get_module_proc(VHost, ?PROCNAME),
79 +         {?MODULE, start_link, [VHost, Opts]},
80 +         permanent,
81 +         1000,
82 +         worker,
83 +         [?MODULE]},
84 +    % add child to ejabberd_sup
85 +    supervisor:start_child(ejabberd_sup, ChildSpec).
86 +
87 +% supervisor starts gen_server
88 +start_link(VHost, Opts) ->
89 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
90 +    gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []).
91 +
92 +init([VHost, Opts]) ->
93 +    process_flag(trap_exit, true),
94 +    DBs = gen_mod:get_opt(dbs, Opts, [{mnesia, []}]),
95 +    VHostDB = gen_mod:get_opt(vhosts, Opts, [{VHost, mnesia}]),
96 +    % 10 is default becouse of using in clustered environment
97 +    PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, 10),
98 +
99 +    {value,{_, DBName}} = lists:keysearch(VHost, 1, VHostDB),
100 +    {value, {DBName, DBOpts}} = lists:keysearch(DBName, 1, DBs),
101 +
102 +    ?MYDEBUG("Starting mod_logdb for ~p with ~p backend", [VHost, DBName]),
103 +
104 +    DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)),
105 +
106 +    % actually all work begin on receiving start signal
107 +    timer:send_after(1000, start),
108 +
109 +    {ok, #state{vhost=VHost,
110 +                dbmod=DBMod,
111 +                dbopts=DBOpts,
112 +                % dbs used for convert messages from one backend to other
113 +                dbs=DBs,
114 +                dolog_default=gen_mod:get_opt(dolog_default, Opts, true),
115 +                ignore_jids=gen_mod:get_opt(ignore_jids, Opts, []),
116 +                groupchat=gen_mod:get_opt(groupchat, Opts, none),
117 +                purge_older_days=gen_mod:get_opt(purge_older_days, Opts, never),
118 +                poll_users_settings=PollUsersSettings}}.
119 +
120 +cleanup(#state{vhost=VHost} = State) ->
121 +    ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
122 +
123 +    %ets:delete(ets_settings_table(VHost)),
124 +
125 +    ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90),
126 +    ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
127 +    ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_packet, 10),
128 +    %ejabberd_hooks:delete(adhoc_sm_commands, VHost, ?MODULE, adhoc_sm_commands, 110),
129 +    %ejabberd_hooks:delete(adhoc_sm_items, VHost, ?MODULE, adhoc_sm_items, 110),
130 +    ejabberd_hooks:delete(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 110),
131 +    ejabberd_hooks:delete(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 110),
132 +    %ejabberd_hooks:delete(disco_sm_identity, VHost, ?MODULE, get_sm_identity, 110),
133 +    %ejabberd_hooks:delete(disco_sm_features, VHost, ?MODULE, get_sm_features, 110),
134 +    %ejabberd_hooks:delete(disco_sm_items, VHost, ?MODULE, get_sm_items, 110),
135 +    ejabberd_hooks:delete(disco_local_identity, VHost, ?MODULE, get_local_identity, 110),
136 +    ejabberd_hooks:delete(disco_local_features, VHost, ?MODULE, get_local_features, 110),
137 +    ejabberd_hooks:delete(disco_local_items, VHost, ?MODULE, get_local_items, 110),
138 +
139 +    ?MYDEBUG("Removed hooks for ~p", [VHost]),
140 +
141 +    ejabberd_ctl:unregister_commands(VHost, [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}], ?MODULE, rebuild_stats),
142 +    Supported_backends = lists:flatmap(fun({Backend, _Opts}) ->
143 +                                            [atom_to_list(Backend), " "]
144 +                                       end, State#state.dbs),
145 +    ejabberd_ctl:unregister_commands(
146 +                           VHost,
147 +                           [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }],
148 +                           ?MODULE, copy_messages_ctl),
149 +    ?MYDEBUG("Unregistered commands for ~p", [VHost]).
150 +
151 +stop(VHost) ->
152 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
153 +    %gen_server:call(Proc, {cleanup}),
154 +    %?MYDEBUG("Cleanup in stop finished!!!!", []),
155 +    %timer:sleep(10000),
156 +    ok = supervisor:terminate_child(ejabberd_sup, Proc),
157 +    ok = supervisor:delete_child(ejabberd_sup, Proc).
158 +
159 +handle_call({cleanup}, _From, State) ->
160 +    cleanup(State),
161 +    ?MYDEBUG("Cleanup finished!!!!!", []),
162 +    {reply, ok, State};
163 +handle_call({get_dates}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
164 +    Reply = DBMod:get_dates(VHost),
165 +    {reply, Reply, State};
166 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
167 +% ejabberd_web_admin callbacks
168 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
169 +handle_call({delete_messages_by_user_at, PMsgs, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
170 +    Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, Date),
171 +    {reply, Reply, State};
172 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
173 +    Reply = DBMod:delete_all_messages_by_user_at(User, VHost, Date),
174 +    {reply, Reply, State};
175 +handle_call({delete_messages_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
176 +    Reply = DBMod:delete_messages_at(VHost, Date),
177 +    {reply, Reply, State};
178 +handle_call({get_vhost_stats}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
179 +    Reply = DBMod:get_vhost_stats(VHost),
180 +    {reply, Reply, State};
181 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
182 +    Reply = DBMod:get_vhost_stats_at(VHost, Date),
183 +    {reply, Reply, State};
184 +handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
185 +    Reply = DBMod:get_user_stats(User, VHost),
186 +    {reply, Reply, State};
187 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
188 +    Reply = DBMod:get_user_messages_at(User, VHost, Date),
189 +    {reply, Reply, State};
190 +handle_call({get_user_settings, User}, _From, #state{dbmod=_DBMod, vhost=VHost}=State) ->
191 +    Reply = case ets:match_object(ets_settings_table(VHost),
192 +                                  #user_settings{owner_name=User, _='_'}) of
193 +                 [Set] -> Set;
194 +                 _ -> #user_settings{owner_name=User,
195 +                                     dolog_default=State#state.dolog_default,
196 +                                     dolog_list=[],
197 +                                     donotlog_list=[]}
198 +            end,
199 +    {reply, Reply, State};
200 +% TODO: remove User ??
201 +handle_call({set_user_settings, User, GSet}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
202 +    Set = GSet#user_settings{owner_name=User},
203 +    Reply =
204 +       case ets:match_object(ets_settings_table(VHost),
205 +                             #user_settings{owner_name=User, _='_'}) of
206 +            [Set] ->
207 +                ?MYDEBUG("Settings is equal", []),
208 +                ok;
209 +            _ ->
210 +                case DBMod:set_user_settings(User, VHost, Set) of
211 +                     error ->
212 +                       error;
213 +                     ok ->
214 +                       true = ets:insert(ets_settings_table(VHost), Set),
215 +                       ok
216 +                end
217 +       end,
218 +    {reply, Reply, State};
219 +handle_call({get_module_settings}, _From, State) ->
220 +    {reply, State, State};
221 +handle_call({set_module_settings, #state{purge_older_days=PurgeDays,
222 +                                         poll_users_settings=PollSec} = Settings},
223 +            _From,
224 +            #state{purgeRef=PurgeRefOld,
225 +                   pollRef=PollRefOld,
226 +                   purge_older_days=PurgeDaysOld,
227 +                   poll_users_settings=PollSecOld} = State) ->
228 +    PurgeRef = if
229 +                 PurgeDays == never, PurgeDaysOld /= never  ->
230 +                    {ok, cancel} = timer:cancel(PurgeRefOld),
231 +                    disabled;
232 +                 is_integer(PurgeDays), PurgeDaysOld == never ->
233 +                    set_purge_timer(PurgeDays);
234 +                 true ->
235 +                    PurgeRefOld
236 +               end,
237 +
238 +    PollRef = if
239 +                PollSec == PollSecOld ->
240 +                   PollRefOld;
241 +                PollSec == 0, PollSecOld /= 0 ->
242 +                   {ok, cancel} = timer:cancel(PollRefOld),
243 +                   disabled;
244 +                is_integer(PollSec), PollSecOld == 0 ->
245 +                   set_poll_timer(PollSec);
246 +                is_integer(PollSec), PollSecOld /= 0 ->
247 +                   {ok, cancel} = timer:cancel(PollRefOld),
248 +                   set_poll_timer(PollSec)
249 +              end,
250 +
251 +    NewState = State#state{dolog_default=Settings#state.dolog_default,
252 +                           ignore_jids=Settings#state.ignore_jids,
253 +                           groupchat=Settings#state.groupchat,
254 +                           purge_older_days=PurgeDays,
255 +                           poll_users_settings=PollSec,
256 +                           purgeRef=PurgeRef,
257 +                           pollRef=PollRef},
258 +    {reply, ok, NewState};
259 +handle_call(Msg, _From, State) ->
260 +    ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
261 +    {noreply, State}.
262 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
263 +% end ejabberd_web_admin callbacks
264 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
265 +
266 +% ejabberd_hooks call
267 +handle_cast({addlog, Direction, Owner, Peer, Packet}, #state{dbmod=DBMod, vhost=VHost}=State) ->
268 +    case filter(Owner, Peer, State) of
269 +         true ->
270 +              case catch packet_parse(Owner, Peer, Packet, Direction, State) of
271 +                   ignore ->
272 +                     ok;
273 +                   {'EXIT', Reason} ->
274 +                     ?ERROR_MSG("Failed to parse: ~p", [Reason]);
275 +                   Msg ->
276 +                     DBMod:log_message(VHost, Msg)
277 +              end;
278 +         false ->
279 +              ok
280 +    end,
281 +    {noreply, State};
282 +% ejabberdctl rebuild_stats/3
283 +handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) ->
284 +    % TODO: maybe spawn?
285 +    DBMod:rebuild_stats(VHost),
286 +    {noreply, State};
287 +handle_cast({copy_messages, Backend}, State) ->
288 +    spawn(?MODULE, copy_messages, [[State, Backend]]),
289 +    {noreply, State};
290 +handle_cast({copy_messages, Backend, Date}, State) ->
291 +    spawn(?MODULE, copy_messages, [[State, Backend, Date]]),
292 +    {noreply, State};
293 +handle_cast(Msg, State) ->
294 +    ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
295 +    {noreply, State}.
296 +
297 +% return: disabled | timer reference
298 +set_purge_timer(PurgeDays) ->
299 +    case PurgeDays of
300 +         never -> disabled;
301 +         Days when is_integer(Days) ->
302 +              {ok, Ref1} = timer:send_interval(timer:hours(24), scheduled_purging),
303 +              Ref1
304 +    end.
305 +
306 +% return: disabled | timer reference
307 +set_poll_timer(PollSec) ->
308 +    if
309 +      PollSec > 0 ->
310 +        {ok, Ref2} = timer:send_interval(timer:seconds(PollSec), poll_users_settings),
311 +        Ref2;
312 +      % db polling disabled
313 +      PollSec == 0 ->
314 +        disabled;
315 +      true ->
316 +        {ok, Ref3} = timer:send_interval(timer:seconds(10), poll_users_settings),
317 +        Ref3
318 +    end.
319 +
320 +% actual starting of logging
321 +% from timer:send_after (in init)
322 +handle_info(start, #state{dbmod=DBMod, vhost=VHost}=State) ->
323 +    case DBMod:start(VHost, State#state.dbopts) of
324 +         {error, _Reason} ->
325 +           timer:sleep(30000),
326 +           {stop, db_connection_failed, State};
327 +         {ok, SPid} ->
328 +
329 +           ?INFO_MSG("~p connection established", [DBMod]),
330 +           
331 +           MonRef = erlang:monitor(process, SPid),
332 +
333 +           ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]),
334 +           {ok, DoLog} = DBMod:get_users_settings(VHost),
335 +           ets:insert(ets_settings_table(VHost), DoLog),
336 +
337 +           TrefPurge = set_purge_timer(State#state.purge_older_days),
338 +           TrefPoll = set_poll_timer(State#state.poll_users_settings),
339 +
340 +           ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90),
341 +           ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
342 +           ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_packet, 10),
343 +
344 +           ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 110),
345 +           ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 110),
346 +           ejabberd_hooks:add(disco_local_identity, VHost, ?MODULE, get_local_identity, 110),
347 +           %ejabberd_hooks:add(disco_sm_items, VHost, ?MODULE, get_sm_items, 110),
348 +           %ejabberd_hooks:add(disco_sm_features, VHost, ?MODULE, get_sm_features, 110),
349 +           %ejabberd_hooks:add(disco_sm_identity, VHost, ?MODULE, get_sm_identity, 110),
350 +           ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 110),
351 +           ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 110),
352 +           %ejabberd_hooks:add(adhoc_sm_items, VHost, ?MODULE, adhoc_sm_items, 110),
353 +           %ejabberd_hooks:add(adhoc_sm_commands, VHost, ?MODULE, adhoc_sm_commands, 110),
354 +
355 +           ?MYDEBUG("Added hooks for ~p", [VHost]),
356 +
357 +           ejabberd_ctl:register_commands(
358 +                           VHost,
359 +                           [{"rebuild_stats", "rebuild mod_logdb module stats for vhost"}],
360 +                           ?MODULE, rebuild_stats),
361 +           Supported_backends = lists:flatmap(fun({Backend, _Opts}) ->
362 +                                                  [atom_to_list(Backend), " "]
363 +                                              end, State#state.dbs),
364 +           ejabberd_ctl:register_commands(
365 +                           VHost,
366 +                           [{"copy_messages backend", "copy messages from backend to current backend. backends could be: " ++ Supported_backends }],
367 +                           ?MODULE, copy_messages_ctl),
368 +           ?MYDEBUG("Registered commands for ~p", [VHost]),
369 +
370 +           NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll},
371 +           {noreply, NewState};
372 +        Rez ->
373 +           ?ERROR_MSG("Rez=~p", [Rez]),
374 +           timer:sleep(30000),
375 +           {stop, db_connection_failed, State}
376 +    end;
377 +% from timer:send_interval/2 (in start handle_info)
378 +handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = State) ->
379 +    ?MYDEBUG("Starting scheduled purging of old records for ~p", [VHost]),
380 +    spawn(?MODULE, purge_old_records, [VHost, integer_to_list(Days)]),
381 +    {noreply, State};
382 +% from timer:send_interval/2 (in start handle_info)
383 +handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) ->
384 +    {ok, DoLog} = DBMod:get_users_settings(VHost),
385 +    ?MYDEBUG("DoLog=~p", [DoLog]),
386 +    true = ets:delete_all_objects(ets_settings_table(VHost)),
387 +    ets:insert(ets_settings_table(VHost), DoLog),
388 +    {noreply, State};
389 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
390 +    {stop, db_connection_dropped, State};
391 +handle_info({fetch_result, _, _}, State) ->
392 +    ?MYDEBUG("Got timed out mysql fetch result", []),
393 +    {noreply, State};
394 +handle_info(Info, State) ->
395 +    ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
396 +    {noreply, State}.
397 +
398 +terminate(db_connection_failed, _State) ->
399 +    ok;
400 +terminate(db_connection_dropped, State) ->
401 +    cleanup(State),
402 +    ok;
403 +terminate(_Reason, #state{monref=undefined} = State) ->
404 +    cleanup(State),
405 +    ok;
406 +terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) ->
407 +    ?INFO_MSG("Reason: ~p", [Reason]),
408 +    case erlang:is_process_alive(Pid) of
409 +         true ->
410 +           erlang:demonitor(MonRef, [flush]),
411 +           DBMod:stop(VHost);
412 +         false ->
413 +           ok
414 +    end,
415 +    cleanup(State),
416 +    ok.
417 +
418 +code_change(_OldVsn, State, _Extra) ->
419 +    {ok, State}.
420 +
421 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
422 +%
423 +% ejabberd_hooks callbacks
424 +%
425 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
426 +% TODO: change to/from to list as sql stores it as list
427 +send_packet(Owner, Peer, P) ->
428 +    VHost = Owner#jid.lserver,
429 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
430 +    gen_server:cast(Proc, {addlog, to, Owner, Peer, P}).
431 +
432 +offline_packet(Peer, Owner, P) ->
433 +    VHost = Owner#jid.lserver,
434 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
435 +    gen_server:cast(Proc, {addlog, from, Owner, Peer, P}).
436 +
437 +receive_packet(_JID, Peer, Owner, P) -> 
438 +    VHost = Owner#jid.lserver,
439 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
440 +    gen_server:cast(Proc, {addlog, from, Owner, Peer, P}).
441 +
442 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
443 +%
444 +% ejabberdctl
445 +%
446 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
447 +rebuild_stats(_Val, VHost, ["rebuild_stats"]) ->
448 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
449 +    gen_server:cast(Proc, {rebuild_stats}),
450 +    {stop, ?STATUS_SUCCESS};
451 +rebuild_stats(Val, _VHost, _Args) ->
452 +    Val.
453 +
454 +copy_messages_ctl(_Val, VHost, ["copy_messages", Backend]) ->
455 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
456 +    gen_server:cast(Proc, {copy_messages, Backend}),
457 +    {stop, ?STATUS_SUCCESS};
458 +copy_messages_ctl(_Val, VHost, ["copy_messages", Backend, Date]) ->
459 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
460 +    gen_server:cast(Proc, {copy_messages, Backend, Date}),
461 +    {stop, ?STATUS_SUCCESS};
462 +copy_messages_ctl(Val, _VHost, _Args) ->
463 +    Val.
464 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
465 +%
466 +% misc operations
467 +%
468 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
469 +
470 +% handle_cast({addlog, E}, _)
471 +% raw packet -> #msg
472 +packet_parse(Owner, Peer, Packet, Direction, State) ->
473 +    case xml:get_subtag(Packet, "body") of
474 +         false ->
475 +           ignore;
476 +         Body_xml ->
477 +           Message_type =
478 +              case xml:get_tag_attr_s("type", Packet) of
479 +                   [] -> "normal";
480 +                   MType -> MType
481 +              end,
482 +
483 +           case Message_type of
484 +                "groupchat" when State#state.groupchat == send, Direction == to ->
485 +                   ok;
486 +                "groupchat" when State#state.groupchat == send, Direction == from ->
487 +                   throw(ignore);
488 +                "groupchat" when State#state.groupchat == half ->
489 +                   Rooms = ets:match(muc_online_room, '$1'),
490 +                   Ni=lists:foldl(fun([{muc_online_room, {GName, GHost}, Pid}], Names) ->
491 +                                   case gen_fsm:sync_send_all_state_event(Pid, {get_jid_nick,Owner}) of
492 +                                        [] -> Names;
493 +                                        Nick -> 
494 +                                           lists:append(Names, [jlib:jid_to_string({GName, GHost, Nick})])
495 +                                   end
496 +                                  end, [], Rooms),
497 +                   case lists:member(jlib:jid_to_string(Peer), Ni) of
498 +                        true when Direction == from ->
499 +                          throw(ignore);
500 +                        _ ->
501 +                          ok
502 +                   end;
503 +                "groupchat" when State#state.groupchat == none ->
504 +                   throw(ignore);
505 +                _ ->
506 +                   ok
507 +           end,
508 +
509 +           Message_body = xml:get_tag_cdata(Body_xml),
510 +           Message_subject =
511 +              case xml:get_subtag(Packet, "subject") of
512 +                   false ->
513 +                     "";
514 +                   Subject_xml ->
515 +                     xml:get_tag_cdata(Subject_xml)
516 +              end,
517 +
518 +           OwnerName = stringprep:tolower(Owner#jid.user),
519 +           PName = stringprep:tolower(Peer#jid.user),
520 +           PServer = stringprep:tolower(Peer#jid.server),
521 +           PResource = Peer#jid.resource,
522 +
523 +           #msg{timestamp=get_timestamp(),
524 +                owner_name=OwnerName,
525 +                peer_name=PName,
526 +                peer_server=PServer,
527 +                peer_resource=PResource,
528 +                direction=Direction,
529 +                type=Message_type,
530 +                subject=Message_subject,
531 +                body=Message_body}
532 +    end.
533 +
534 +% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages)
535 +filter(Owner, Peer, State) ->
536 +    OwnerStr = Owner#jid.luser++"@"++Owner#jid.lserver,
537 +    OwnerServ = "@"++Owner#jid.lserver,
538 +    PeerStr = Peer#jid.luser++"@"++Peer#jid.lserver,
539 +    PeerServ = "@"++Peer#jid.lserver,
540 +
541 +    LogTo = case ets:match_object(ets_settings_table(State#state.vhost),
542 +                                  #user_settings{owner_name=Owner#jid.luser, _='_'}) of
543 +                 [#user_settings{dolog_default=Default,
544 +                                 dolog_list=DLL,
545 +                                 donotlog_list=DNLL}] ->
546 +                      A = lists:member(PeerStr, DLL),
547 +                      B = lists:member(PeerStr, DNLL),
548 +                      if
549 +                        A -> true;
550 +                        B -> false;
551 +                        Default == true -> true;
552 +                        Default == false -> false;
553 +                        true -> State#state.dolog_default
554 +                      end;
555 +                 _ -> State#state.dolog_default
556 +           end,
557 +
558 +    lists:all(fun(O) -> O end, 
559 +              [not lists:member(OwnerStr, State#state.ignore_jids),
560 +               not lists:member(PeerStr, State#state.ignore_jids),
561 +               not lists:member(OwnerServ, State#state.ignore_jids),
562 +               not lists:member(PeerServ, State#state.ignore_jids),
563 +               LogTo]).
564 +
565 +purge_old_records(VHost, Days) ->
566 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
567 +
568 +    Dates = gen_server:call(Proc, {get_dates, {VHost}}),
569 +    DateNow = calendar:datetime_to_gregorian_seconds({date(), {0,0,1}}),
570 +    DateDiff = list_to_integer(Days)*24*60*60,
571 +    ?MYDEBUG("Purging tables older than ~s days", [Days]),
572 +    lists:foreach(fun(Date) ->
573 +                    {ok, [Year, Month, Day]} = regexp:split(Date, "[^0-9]+"),
574 +                    DateInSec = calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}),
575 +                    if
576 +                     (DateNow - DateInSec) > DateDiff ->
577 +                        gen_server:call(Proc, {delete_messages_at, Date});
578 +                     true -> 
579 +                        ?MYDEBUG("Skipping messages at ~p", [Date])
580 +                    end
581 +              end, Dates).
582 +
583 +% called from get_vhost_stats/2, get_user_stats/3
584 +sort_stats(Stats) ->
585 +    % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ]
586 +    CFun = fun({TableName, Count}) ->
587 +                 {ok, [Year, Month, Day]} = regexp:split(TableName, "[^0-9]+"),
588 +                 { calendar:datetime_to_gregorian_seconds({{list_to_integer(Year), list_to_integer(Month), list_to_integer(Day)}, {0,0,1}}), Count }
589 +           end,
590 +    % convert to [{63364377601,1}, {63360662401,1}, ... ]
591 +    CStats = lists:map(CFun, Stats),
592 +    % sort by date
593 +    SortedStats = lists:reverse(lists:keysort(1, CStats)),
594 +    % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list
595 +    [{mod_logdb:convert_timestamp_brief(TableSec), Count} || {TableSec, Count} <- SortedStats].
596 +
597 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
598 +%
599 +% Date/Time operations
600 +%
601 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
602 +% return float seconds elapsed from "zero hour" as list
603 +get_timestamp() ->
604 +    {MegaSec, Sec, MicroSec} = now(),
605 +    [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
606 +    List.
607 +
608 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string
609 +convert_timestamp(Seconds) when is_list(Seconds) ->
610 +    case string:to_float(Seconds++".0") of
611 +         {F,_} when is_float(F) -> convert_timestamp(F);
612 +         _ -> erlang:error(badarg, [Seconds])
613 +    end;
614 +convert_timestamp(Seconds) when is_float(Seconds) ->
615 +    GregSec = trunc(Seconds + 719528*86400),
616 +    UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
617 +    {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT),
618 +    integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec).
619 +
620 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string
621 +convert_timestamp_brief(Seconds) when is_list(Seconds) ->
622 +    convert_timestamp_brief(list_to_float(Seconds));
623 +convert_timestamp_brief(Seconds) when is_float(Seconds) ->
624 +    GregSec = trunc(Seconds + 719528*86400),
625 +    UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
626 +    {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT),
627 +    integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day);
628 +convert_timestamp_brief(Seconds) when is_integer(Seconds) ->
629 +    {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds),
630 +    integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day).
631 +
632 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
633 +%
634 +% DB operations (get)
635 +%
636 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
637 +get_vhost_stats(VHost) ->
638 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
639 +    gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
640 +
641 +get_vhost_stats_at(VHost, Date) ->
642 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
643 +    gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
644 +
645 +get_user_stats(User, VHost) ->
646 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
647 +    gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
648 +
649 +get_user_messages_at(User, VHost, Date) ->
650 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
651 +    gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
652 +
653 +get_dates(VHost) ->
654 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
655 +    gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
656 +
657 +get_user_settings(User, VHost) ->
658 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
659 +    gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
660 +
661 +set_user_settings(User, VHost, Set) ->
662 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
663 +    gen_server:call(Proc, {set_user_settings, User, Set}).
664 +
665 +get_module_settings(VHost) ->
666 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
667 +    gen_server:call(Proc, {get_module_settings}).
668 +
669 +set_module_settings(VHost, Settings) ->
670 +    Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
671 +    gen_server:call(Proc, {set_module_settings, Settings}).
672 +
673 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
674 +%
675 +% Web admin callbacks (delete)
676 +%
677 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
678 +user_messages_at_parse_query(VHost, Date, Msgs, Query) ->
679 +    case lists:keysearch("delete", 1, Query) of
680 +         {value, _} ->
681 +             PMsgs = lists:filter(
682 +                              fun(Msg) ->
683 +                                   ID = jlib:encode_base64(binary_to_list(term_to_binary(Msg#msg.timestamp))),
684 +                                   lists:member({"selected", ID}, Query)
685 +                              end, Msgs),
686 +             Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
687 +             gen_server:call(Proc, {delete_messages_by_user_at, PMsgs, Date}, ?CALL_TIMEOUT);
688 +         false ->
689 +             nothing
690 +    end.
691 +
692 +user_messages_parse_query(User, VHost, Query) ->
693 +    Dates = get_dates(VHost),
694 +    case lists:keysearch("delete", 1, Query) of
695 +         {value, _} ->
696 +             PDates = lists:filter(
697 +                              fun(Date) ->
698 +                                   ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))),
699 +                                   lists:member({"selected", ID}, Query)
700 +                              end, Dates),
701 +             Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
702 +             Rez = lists:foldl(
703 +                          fun(Date, Acc) ->
704 +                              lists:append(Acc,
705 +                                           [gen_server:call(Proc,
706 +                                                            {delete_all_messages_by_user_at, User, Date},
707 +                                                            ?CALL_TIMEOUT)])
708 +                          end, [], PDates),
709 +             case lists:member(error, Rez) of
710 +                  true ->
711 +                    error;
712 +                  false ->
713 +                    nothing
714 +             end;
715 +         false ->
716 +             nothing
717 +    end.
718 +
719 +vhost_messages_parse_query(VHost, Query) ->
720 +    Dates = get_dates(VHost),
721 +    case lists:keysearch("delete", 1, Query) of
722 +         {value, _} ->
723 +             PDates = lists:filter(
724 +                              fun(Date) ->
725 +                                   ID = jlib:encode_base64(binary_to_list(term_to_binary(VHost++Date))),
726 +                                   lists:member({"selected", ID}, Query)
727 +                              end, Dates),
728 +             Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
729 +             Rez = lists:foldl(fun(Date, Acc) ->
730 +                                   lists:append(Acc, [gen_server:call(Proc,
731 +                                                                      {delete_messages_at, Date},
732 +                                                                      ?CALL_TIMEOUT)])
733 +                               end, [], PDates),
734 +             case lists:member(error, Rez) of
735 +                  true ->
736 +                    error;
737 +                  false ->
738 +                    nothing
739 +             end;
740 +         false ->
741 +             nothing
742 +    end.
743 +
744 +vhost_messages_at_parse_query(VHost, Date, Stats, Query) ->
745 +    case lists:keysearch("delete", 1, Query) of
746 +         {value, _} ->
747 +             PStats = lists:filter(
748 +                              fun({User, _Count}) ->
749 +                                   ID = jlib:encode_base64(binary_to_list(term_to_binary(User++VHost))),
750 +                                   lists:member({"selected", ID}, Query)
751 +                              end, Stats),
752 +             Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
753 +             Rez = lists:foldl(fun({User, _Count}, Acc) ->
754 +                                   lists:append(Acc, [gen_server:call(Proc,
755 +                                                                      {delete_all_messages_by_user_at,
756 +                                                                       User, Date},
757 +                                                                      ?CALL_TIMEOUT)])
758 +                               end, [], PStats),
759 +             case lists:member(error, Rez) of
760 +                  true ->
761 +                    error;
762 +                  false ->
763 +                    ok
764 +             end;
765 +         false ->
766 +             nothing
767 +    end.
768 +
769 +copy_messages([#state{vhost=VHost}=State, From]) ->
770 +    ?INFO_MSG("Going to copy messages from ~p for ~p", [From, VHost]),
771 +
772 +    {FromDBName, FromDBOpts} =
773 +         case lists:keysearch(list_to_atom(From), 1, State#state.dbs) of
774 +              {value, {FN, FO}} ->
775 +                 {FN, FO};
776 +              false ->
777 +                 ?ERROR_MSG("Failed to find record for ~p in dbs", [From]),
778 +                 throw(error)
779 +         end,
780 +
781 +    FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
782 +
783 +    {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
784
785 +    Dates = FromDBMod:get_dates(VHost),
786 +    DatesLength = length(Dates),
787 +
788 +    lists:foldl(fun(Date, Acc) ->
789 +                   case copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
790 +                        ok ->
791 +                          ?INFO_MSG("Copied messages at ~p (~p/~p)", [Date, Acc, DatesLength]);
792 +                        Value ->
793 +                          ?ERROR_MSG("Failed to copy messages at ~p (~p/~p): ~p", [Date, Acc, DatesLength, Value]),
794 +                          FromDBMod:stop(VHost),
795 +                          throw(error)
796 +                   end,
797 +                   Acc + 1
798 +                  end, 1, Dates),
799 +    ?INFO_MSG("Copied messages from ~p",  [From]),
800 +    FromDBMod:stop(VHost);
801 +copy_messages([#state{vhost=VHost}=State, From, Date]) ->
802 +    {value, {FromDBName, FromDBOpts}} = lists:keysearch(list_to_atom(From), 1, State#state.dbs),
803 +    FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
804 +    {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
805 +    case catch copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
806 +         {'exit', Reason} ->
807 +           ?ERROR_MSG("Failed to copy messages at ~p: ~p", [Date, Reason]);
808 +         ok ->
809 +           ?INFO_MSG("Copied messages at ~p", [Date]);
810 +         Value ->
811 +           ?ERROR_MSG("Failed to copy messages at ~p: ~p", [Date, Value])
812 +    end,
813 +    FromDBMod:stop(VHost).
814 +
815 +copy_messages_int([FromDBMod, ToDBMod, VHost, Date]) ->
816 +    ets:new(mod_logdb_temp, [named_table, set, public]),
817 +    {Time, Value} = timer:tc(?MODULE, copy_messages_int_tc, [[FromDBMod, ToDBMod, VHost, Date]]),
818 +    ets:delete_all_objects(mod_logdb_temp),
819 +    ets:delete(mod_logdb_temp),
820 +    ?INFO_MSG("copy_messages at ~p elapsed ~p sec", [Date, Time/1000000]),
821 +    Value.
822 +
823 +copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) ->
824 +    ?INFO_MSG("Going to copy messages from ~p for ~p at ~p", [FromDBMod, VHost, Date]),
825 +   
826 +    ok = FromDBMod:rebuild_stats_at(VHost, Date),
827 +    catch mod_logdb:rebuild_stats_at(VHost, Date),
828 +    {ok, FromStats} = FromDBMod:get_vhost_stats_at(VHost, Date),
829 +    ToStats = case mod_logdb:get_vhost_stats_at(VHost, Date) of
830 +                   {ok, Stats} -> Stats;
831 +                   {error, _} -> []
832 +              end,
833 +
834 +    FromStatsS = lists:keysort(1, FromStats),
835 +    ToStatsS = lists:keysort(1, ToStats),
836 +
837 +    StatsLength = length(FromStats),
838 +
839 +    CopyFun = if
840 +                                                   % destination table is empty
841 +                FromDBMod /= mod_logdb_mnesia_old, ToStats == [] ->
842 +                    fun({User, _Count}, Acc) ->
843 +                        {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
844 +                        MAcc =
845 +                          lists:foldl(fun(Msg, MFAcc) ->
846 +                                          ok = ToDBMod:log_message(VHost, Msg),
847 +                                          MFAcc + 1
848 +                                      end, 0, Msgs),
849 +                        NewAcc = Acc + 1,
850 +                        ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
851 +                        %timer:sleep(100),
852 +                        NewAcc
853 +                    end;
854 +                                                   % destination table is not empty
855 +                FromDBMod /= mod_logdb_mnesia_old, ToStats /= [] ->
856 +                    fun({User, _Count}, Acc) ->
857 +                        {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date),
858 +                        lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
859 +                                            ets:insert(mod_logdb_temp, {Tst});
860 +                                         % mysql, pgsql removes final zeros after decimal point
861 +                                         (#msg{timestamp=Tst}) when length(Tst) < 16 ->
862 +                                            {F, _} = string:to_float(Tst++".0"),
863 +                                            [T] = io_lib:format("~.5f", [F]),
864 +                                            ets:insert(mod_logdb_temp, {T})
865 +                                      end, ToMsgs),
866 +                        {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
867 +                        MAcc =
868 +                          lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) ->
869 +                                          case ets:member(mod_logdb_temp, ToTimestamp) of
870 +                                               false ->
871 +                                                  ok = ToDBMod:log_message(VHost, Msg),
872 +                                                  ets:insert(mod_logdb_temp, {ToTimestamp}),
873 +                                                  MFAcc + 1;
874 +                                               true ->
875 +                                                  MFAcc
876 +                                          end
877 +                                      end, 0, Msgs),
878 +                        NewAcc = Acc + 1,
879 +                        ets:delete_all_objects(mod_logdb_temp),
880 +                        ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
881 +                        %timer:sleep(100),
882 +                        NewAcc
883 +                    end;
884 +                % copying from mod_logmnesia
885 +                true ->
886 +                    fun({User, _Count}, Acc) ->
887 +                        ToStats =
888 +                           case ToDBMod:get_user_messages_at(User, VHost, Date) of
889 +                                {ok, []} ->
890 +                                  ok;
891 +                                {ok, ToMsgs} ->
892 +                                  lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
893 +                                                     ets:insert(mod_logdb_temp, {Tst});
894 +                                                   % mysql, pgsql removes final zeros after decimal point
895 +                                                   (#msg{timestamp=Tst}) when length(Tst) < 15 ->
896 +                                                     {F, _} = string:to_float(Tst++".0"),
897 +                                                     [T] = io_lib:format("~.5f", [F]),
898 +                                                     ets:insert(mod_logdb_temp, {T})
899 +                                                end, ToMsgs);
900 +                                {error, _} ->
901 +                                  ok
902 +                           end,
903 +                        {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
904 +
905 +                        MAcc =
906 +                          lists:foldl(
907 +                            fun({msg, TU, TS, TR, FU, FS, FR, Type, Subj, Body, Timest},
908 +                                MFAcc) ->
909 +                                  [Timestamp] = if is_float(Timest) == true ->
910 +                                                     io_lib:format("~.5f", [Timest]);
911 +                                                   % early versions of mod_logmnesia
912 +                                                   is_integer(Timest) == true ->
913 +                                                     io_lib:format("~.5f", [Timest-719528*86400.0]);
914 +                                                   true ->
915 +                                                     ?ERROR_MSG("Incorrect timestamp ~p", [Timest]),
916 +                                                     throw(error)
917 +                                                end,
918 +                                  case ets:member(mod_logdb_temp, Timestamp) of
919 +                                       false ->
920 +                                          if
921 +                                           % from
922 +                                           TS == VHost ->
923 +                                             TMsg = #msg{timestamp=Timestamp,
924 +                                                         owner_name=TU,
925 +                                                         peer_name=FU, peer_server=FS, peer_resource=FR,
926 +                                                         direction=from,
927 +                                                         type=Type,
928 +                                                         subject=Subj, body=Body},
929 +                                             ok = ToDBMod:log_message(VHost, TMsg);
930 +                                           true -> ok
931 +                                         end,
932 +                                         if
933 +                                           % to
934 +                                           FS == VHost ->
935 +                                             FMsg = #msg{timestamp=Timestamp,
936 +                                                         owner_name=FU,
937 +                                                         peer_name=TU, peer_server=TS, peer_resource=TR,
938 +                                                         direction=to,
939 +                                                         type=Type,
940 +                                                         subject=Subj, body=Body},
941 +                                             ok = ToDBMod:log_message(VHost, FMsg);
942 +                                           true -> ok
943 +                                         end,
944 +                                         ets:insert(mod_logdb_temp, {Timestamp}),
945 +                                         MFAcc + 1;
946 +                                       true -> % not ets:member
947 +                                          MFAcc
948 +                                   end % case
949 +                          end, 0, Msgs), % foldl
950 +                        NewAcc = Acc + 1,
951 +                        ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
952 +                        %timer:sleep(100),
953 +                        NewAcc
954 +                    end % fun
955 +              end, % if FromDBMod /= mod_logdb_mnesia_old
956 +
957 +    if
958 +      FromStats == [] ->
959 +        ?INFO_MSG("No messages were found at ~p", [Date]);
960 +      FromStatsS == ToStatsS ->
961 +        ?INFO_MSG("Stats are equal at ~p", [Date]);
962 +      FromStatsS /= ToStatsS ->
963 +        lists:foldl(CopyFun, 0, FromStats),
964 +        ok = ToDBMod:rebuild_stats_at(VHost, Date)
965 +        %timer:sleep(1000)
966 +    end,
967 +
968 +    ok.
969 +
970 +list_to_bool(Num) ->
971 +    case lists:member(Num, ["t", "true", "y", "yes", "1"]) of
972 +         true ->
973 +           true;
974 +         false ->
975 +           case lists:member(Num, ["f", "false", "n", "no", "0"]) of
976 +                true ->
977 +                  false;
978 +                false ->
979 +                  error
980 +           end
981 +    end.
982 +
983 +bool_to_list(true) ->
984 +    "TRUE";
985 +bool_to_list(false) ->
986 +    "FALSE".
987 +
988 +list_to_string([]) ->
989 +    "";
990 +list_to_string(List) when is_list(List) ->
991 +    Str = lists:flatmap(fun(Elm) -> Elm ++ "\n" end, List),
992 +    lists:sublist(Str, length(Str)-1).
993 +
994 +string_to_list(null) ->
995 +    [];
996 +string_to_list([]) ->
997 +    [];
998 +string_to_list(String) ->
999 +    {ok, List} = regexp:split(String, "\n"),
1000 +    List.
1001 +
1002 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1003 +%
1004 +% ad-hoc (copy/pasted from mod_configure.erl)
1005 +%
1006 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1007 +-define(ITEMS_RESULT(Allow, LNode, Fallback),
1008 +    case Allow of
1009 +        deny ->
1010 +            Fallback;
1011 +        allow ->
1012 +            case get_local_items(LServer, LNode,
1013 +                                 jlib:jid_to_string(To), Lang) of
1014 +                {result, Res} ->
1015 +                    {result, Res};
1016 +                {error, Error} ->
1017 +                    {error, Error}
1018 +            end
1019 +    end).
1020 +
1021 +get_local_items(Acc, From, #jid{lserver = LServer} = To, "", Lang) ->
1022 +    case gen_mod:is_loaded(LServer, mod_adhoc) of
1023 +        false ->
1024 +            Acc;
1025 +        _ ->
1026 +            Items = case Acc of
1027 +                         {result, Its} -> Its;
1028 +                         empty -> []
1029 +                    end,
1030 +            AllowUser = acl:match_rule(LServer, mod_logdb, From),
1031 +            AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1032 +            if
1033 +              AllowUser == allow; AllowAdmin == allow ->
1034 +                case get_local_items(LServer, [],
1035 +                                     jlib:jid_to_string(To), Lang) of
1036 +                     {result, Res} ->
1037 +                        {result, Items ++ Res};
1038 +                     {error, _Error} ->
1039 +                        {result, Items}
1040 +                end;
1041 +              true ->
1042 +                {result, Items}
1043 +            end
1044 +    end;
1045 +get_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) ->
1046 +    case gen_mod:is_loaded(LServer, mod_adhoc) of
1047 +        false ->
1048 +            Acc;
1049 +        _ ->
1050 +            LNode = string:tokens(Node, "/"),
1051 +            AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1052 +            case LNode of
1053 +                 ["mod_logdb"] ->
1054 +                      ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
1055 +                 ["mod_logdb_users"] ->
1056 +                      ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
1057 +                 ["mod_logdb_users", [$@ | _]] ->
1058 +                      ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
1059 +                 ["mod_logdb_users", _User] ->
1060 +                      ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
1061 +                 ["mod_logdb_settings"] ->
1062 +                      ?ITEMS_RESULT(AllowAdmin, LNode, {error, ?ERR_FORBIDDEN});
1063 +                 _ ->
1064 +                      Acc
1065 +            end
1066 +    end.
1067 +
1068 +-define(NODE(Name, Node),
1069 +        {xmlelement, "item",
1070 +         [{"jid", Server},
1071 +          {"name", translate:translate(Lang, Name)},
1072 +          {"node", Node}], []}).
1073 +
1074 +get_local_items(_Host, [], Server, Lang) ->
1075 +    {result,
1076 +     [?NODE("Messages logging engine", "mod_logdb")]
1077 +    };
1078 +get_local_items(_Host, ["mod_logdb"], Server, Lang) ->
1079 +    {result,
1080 +     [?NODE("Messages logging engine users", "mod_logdb_users"),
1081 +      ?NODE("Messages logging engine settings", "mod_logdb_settings")]
1082 +    };
1083 +get_local_items(Host, ["mod_logdb_users"], Server, Lang) ->
1084 +    {result, get_all_vh_users(Host, Server, Lang)};
1085 +get_local_items(_Host, ["mod_logdb_users", [$@ | Diap]], Server, Lang) ->
1086 +    case catch ejabberd_auth:dirty_get_registered_users() of
1087 +        {'EXIT', _Reason} ->
1088 +            ?ERR_INTERNAL_SERVER_ERROR;
1089 +        Users ->
1090 +            SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1091 +            case catch begin
1092 +                           {ok, [S1, S2]} = regexp:split(Diap, "-"),
1093 +                           N1 = list_to_integer(S1),
1094 +                           N2 = list_to_integer(S2),
1095 +                           Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
1096 +                           lists:map(fun({S, U}) ->
1097 +                                      ?NODE(U ++ "@" ++ S, "mod_logdb_users/" ++ U ++ "@" ++ S)
1098 +                                     end, Sub)
1099 +                       end of
1100 +                {'EXIT', _Reason} ->
1101 +                    ?ERR_NOT_ACCEPTABLE;
1102 +                Res ->
1103 +                    {result, Res}
1104 +            end
1105 +    end;
1106 +get_local_items(_Host, ["mod_logdb_users", _User], _Server, _Lang) ->
1107 +    {result, []};
1108 +get_local_items(_Host, ["mod_logdb_settings"], _Server, _Lang) ->
1109 +    {result, []};
1110 +get_local_items(_Host, Item, _Server, _Lang) ->
1111 +    ?MYDEBUG("asked for items in ~p", [Item]),
1112 +    {error, ?ERR_ITEM_NOT_FOUND}.
1113 +
1114 +-define(INFO_RESULT(Allow, Feats),
1115 +    case Allow of
1116 +        deny ->
1117 +            {error, ?ERR_FORBIDDEN};
1118 +        allow ->
1119 +            {result, Feats}
1120 +    end).
1121 +
1122 +get_local_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) ->
1123 +    case gen_mod:is_loaded(LServer, mod_adhoc) of
1124 +        false ->
1125 +            Acc;
1126 +        _ ->
1127 +            LNode = string:tokens(Node, "/"),
1128 +            AllowUser = acl:match_rule(LServer, mod_logdb, From),
1129 +            AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1130 +            case LNode of
1131 +                 ["mod_logdb"] when AllowUser == allow; AllowAdmin == allow ->
1132 +                    ?INFO_RESULT(allow, [?NS_COMMANDS]);
1133 +                 ["mod_logdb"] ->
1134 +                    ?INFO_RESULT(deny, [?NS_COMMANDS]);
1135 +                 ["mod_logdb_users"] ->
1136 +                    ?INFO_RESULT(AllowAdmin, []);
1137 +                 ["mod_logdb_users", [$@ | _]] ->
1138 +                    ?INFO_RESULT(AllowAdmin, []);
1139 +                 ["mod_logdb_users", _User] ->
1140 +                    ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS]);
1141 +                 ["mod_logdb_settings"] ->
1142 +                    ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS]);
1143 +                 [] ->
1144 +                    Acc;
1145 +                 _ ->
1146 +                    %?MYDEBUG("asked for ~p features: ~p", [LNode, Allow]),
1147 +                    Acc
1148 +            end
1149 +    end.
1150 +
1151 +-define(INFO_IDENTITY(Category, Type, Name, Lang),
1152 +        [{xmlelement, "identity",
1153 +          [{"category", Category},
1154 +           {"type", Type},
1155 +           {"name", translate:translate(Lang, Name)}], []}]).
1156 +
1157 +-define(INFO_COMMAND(Name, Lang),
1158 +        ?INFO_IDENTITY("automation", "command-node", Name, Lang)).
1159 +
1160 +get_local_identity(Acc, _From, _To, Node, Lang) ->
1161 +    LNode = string:tokens(Node, "/"),
1162 +    case LNode of
1163 +         ["mod_logdb"] ->
1164 +            ?INFO_COMMAND("Messages logging engine", Lang);
1165 +         ["mod_logdb_users"] ->
1166 +            ?INFO_COMMAND("Messages logging engine users", Lang);
1167 +         ["mod_logdb_users", [$@ | _]] ->
1168 +            Acc;
1169 +         ["mod_logdb_users", User] ->
1170 +            ?INFO_COMMAND(User, Lang);
1171 +         ["mod_logdb_settings"] ->
1172 +            ?INFO_COMMAND("Messages logging engine settings", Lang);
1173 +         [] ->
1174 +            Acc;
1175 +         _ ->
1176 +            Acc
1177 +    end.
1178 +
1179 +%get_sm_items(Acc, From, To, Node, Lang) ->
1180 +%    ?MYDEBUG("get_sm_items Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]),
1181 +%    Acc.
1182 +
1183 +%get_sm_features(Acc, From, To, Node, Lang) ->
1184 +%    ?MYDEBUG("get_sm_features Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]),
1185 +%    Acc.
1186 +
1187 +%get_sm_identity(Acc, From, To, Node, Lang) ->
1188 +%    ?MYDEBUG("get_sm_identity Acc=~p From=~p To=~p Node=~p Lang=~p", [Acc, From, To, Node, Lang]),
1189 +%    Acc.
1190 +
1191 +adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To,
1192 +                  Lang) ->
1193 +    Items = case Acc of
1194 +                {result, Its} -> Its;
1195 +                empty -> []
1196 +            end,
1197 +    Nodes = recursively_get_local_items(LServer, "", Server, Lang),
1198 +    Nodes1 = lists:filter(
1199 +               fun(N) ->
1200 +                        Nd = xml:get_tag_attr_s("node", N),
1201 +                        F = get_local_features([], From, To, Nd, Lang),
1202 +                        case F of
1203 +                            {result, [?NS_COMMANDS]} ->
1204 +                                true;
1205 +                            _ ->
1206 +                                false
1207 +                        end
1208 +               end, Nodes),
1209 +    {result, Items ++ Nodes1}.
1210 +
1211 +recursively_get_local_items(_LServer, "mod_logdb_users", _Server, _Lang) ->
1212 +    [];
1213 +recursively_get_local_items(LServer, Node, Server, Lang) ->
1214 +    LNode = string:tokens(Node, "/"),
1215 +    Items = case get_local_items(LServer, LNode, Server, Lang) of
1216 +                {result, Res} ->
1217 +                    Res;
1218 +                {error, _Error} ->
1219 +                    []
1220 +            end,
1221 +    Nodes = lists:flatten(
1222 +      lists:map(
1223 +        fun(N) ->
1224 +                S = xml:get_tag_attr_s("jid", N),
1225 +                Nd = xml:get_tag_attr_s("node", N),
1226 +                if (S /= Server) or (Nd == "") ->
1227 +                    [];
1228 +                true ->
1229 +                    [N, recursively_get_local_items(
1230 +                          LServer, Nd, Server, Lang)]
1231 +                end
1232 +        end, Items)),
1233 +    Nodes.
1234 +
1235 +-define(COMMANDS_RESULT(Allow, From, To, Request),
1236 +    case Allow of
1237 +        deny ->
1238 +            {error, ?ERR_FORBIDDEN};
1239 +        allow ->
1240 +            adhoc_local_commands(From, To, Request)
1241 +    end).
1242 +
1243 +adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
1244 +                     #adhoc_request{node = Node} = Request) ->
1245 +    LNode = string:tokens(Node, "/"),
1246 +    AllowUser = acl:match_rule(LServer, mod_logdb, From),
1247 +    AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1248 +    case LNode of
1249 +         ["mod_logdb"] when AllowUser == allow; AllowAdmin == allow ->
1250 +             ?COMMANDS_RESULT(allow, From, To, Request);
1251 +         ["mod_logdb_users", _User] when AllowAdmin == allow ->
1252 +             ?COMMANDS_RESULT(allow, From, To, Request);
1253 +         ["mod_logdb_settings"] when AllowAdmin == allow ->
1254 +             ?COMMANDS_RESULT(allow, From, To, Request);
1255 +         _ ->
1256 +             Acc
1257 +    end.
1258 +
1259 +adhoc_local_commands(From, #jid{lserver = LServer} = _To,
1260 +                     #adhoc_request{lang = Lang,
1261 +                                    node = Node,
1262 +                                    sessionid = SessionID,
1263 +                                    action = Action,
1264 +                                    xdata = XData} = Request) ->
1265 +    LNode = string:tokens(Node, "/"),
1266 +    %% If the "action" attribute is not present, it is
1267 +    %% understood as "execute".  If there was no <actions/>
1268 +    %% element in the first response (which there isn't in our
1269 +    %% case), "execute" and "complete" are equivalent.
1270 +    ActionIsExecute = lists:member(Action,
1271 +                                   ["", "execute", "complete"]),
1272 +    if  Action == "cancel" ->
1273 +            %% User cancels request
1274 +            adhoc:produce_response(
1275 +              Request,
1276 +              #adhoc_response{status = canceled});
1277 +        XData == false, ActionIsExecute ->
1278 +            %% User requests form
1279 +            case get_form(LServer, LNode, From, Lang) of
1280 +                {result, Form} ->
1281 +                    adhoc:produce_response(
1282 +                      Request,
1283 +                      #adhoc_response{status = executing,
1284 +                                      elements = Form});
1285 +                {error, Error} ->
1286 +                    {error, Error}
1287 +            end;
1288 +        XData /= false, ActionIsExecute ->
1289 +            %% User returns form.
1290 +            case jlib:parse_xdata_submit(XData) of
1291 +                invalid ->
1292 +                    {error, ?ERR_BAD_REQUEST};
1293 +                Fields ->
1294 +                    case set_form(From, LServer, LNode, Lang, Fields) of
1295 +                        {result, _Res} ->
1296 +                            adhoc:produce_response(
1297 +                              #adhoc_response{lang = Lang,
1298 +                                              node = Node,
1299 +                                              sessionid = SessionID,
1300 +                                              status = completed});
1301 +                        {error, Error} ->
1302 +                            {error, Error}
1303 +                    end
1304 +            end;
1305 +        true ->
1306 +            {error, ?ERR_BAD_REQUEST}
1307 +    end.
1308 +
1309 +-define(LISTLINE(Label, Value),
1310 +                 {xmlelement, "option", [{"label", Label}],
1311 +                  [{xmlelement, "value", [], [{xmlcdata, Value}]}]}).
1312 +-define(DEFVAL(Value), {xmlelement, "value", [], [{xmlcdata, Value}]}).
1313 +
1314 +get_user_form(LUser, LServer, Lang) ->
1315 +    %From = jlib:jid_to_string(jlib:jid_remove_resource(Jid)),
1316 +    #user_settings{dolog_default=DLD,
1317 +                   dolog_list=DLL,
1318 +                   donotlog_list=DNLL} = get_user_settings(LUser, LServer),
1319 +    {result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
1320 +               [{xmlelement, "title", [],
1321 +                 [{xmlcdata,
1322 +                   translate:translate(
1323 +                     Lang, "Messages logging engine settings")}]},
1324 +                {xmlelement, "instructions", [],
1325 +                 [{xmlcdata,
1326 +                   translate:translate(
1327 +                     Lang, "Set logging preferences")++ ": " ++ LUser ++ "@" ++ LServer}]},
1328 +                % default to log
1329 +                {xmlelement, "field", [{"type", "list-single"},
1330 +                                       {"label",
1331 +                                        translate:translate(Lang, "Default")},
1332 +                                       {"var", "dolog_default"}],
1333 +                 [?DEFVAL(atom_to_list(DLD)),
1334 +                  ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"),
1335 +                  ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false")
1336 +                 ]},
1337 +                % do log list
1338 +                {xmlelement, "field", [{"type", "text-multi"},
1339 +                                       {"label",
1340 +                                        translate:translate(
1341 +                                          Lang, "Log Messages")},
1342 +                                       {"var", "dolog_list"}],
1343 +                 [{xmlelement, "value", [], [{xmlcdata, list_to_string(DLL)}]}]},
1344 +                % do not log list
1345 +                {xmlelement, "field", [{"type", "text-multi"},
1346 +                                       {"label",
1347 +                                        translate:translate(
1348 +                                          Lang, "Do Not Log Messages")},
1349 +                                       {"var", "donotlog_list"}],
1350 +                 [{xmlelement, "value", [], [{xmlcdata, list_to_string(DNLL)}]}]}
1351 +             ]}]}.
1352 +
1353 +get_settings_form(Host, Lang) ->
1354 +    #state{dbmod=DBMod,
1355 +           dbs=DBs,
1356 +           dolog_default=DLD,
1357 +           ignore_jids=IgnoreJids,
1358 +           groupchat=GroupChat,
1359 +           purge_older_days=PurgeDaysT,
1360 +           poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
1361 +
1362 +    Backends = lists:map(fun({Backend, _Opts}) ->
1363 +                             ?LISTLINE(atom_to_list(Backend), atom_to_list(Backend))
1364 +                         end, DBs),
1365 +    DB = lists:sublist(atom_to_list(DBMod), length(atom_to_list(?MODULE)) + 2, length(atom_to_list(DBMod))),
1366 +    DBsL = lists:append([?DEFVAL(DB)], Backends),
1367 +
1368 +    PurgeDays =
1369 +       case PurgeDaysT of
1370 +            never -> "never";
1371 +            Num when is_integer(Num) -> integer_to_list(Num);
1372 +            _ -> "unknown"
1373 +       end,
1374 +    {result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
1375 +               [{xmlelement, "title", [],
1376 +                 [{xmlcdata,
1377 +                   translate:translate(
1378 +                     Lang, "Messages logging engine settings") ++ " (run-time)"}]},
1379 +                {xmlelement, "instructions", [],
1380 +                 [{xmlcdata,
1381 +                   translate:translate(
1382 +                     Lang, "Set run-time settings")}]},
1383 +                % backends
1384 +                {xmlelement, "field", [{"type", "list-single"},
1385 +                                  {"label",
1386 +                                      translate:translate(Lang, "Backend")},
1387 +                                  {"var", "backend"}],
1388 +                 DBsL},
1389 +                % dbs
1390 +                {xmlelement, "field", [{"type", "text-multi"},
1391 +                                       {"label",
1392 +                                        translate:translate(
1393 +                                          Lang, "dbs")},
1394 +                                       {"var", "dbs"}],
1395 +                 [{xmlelement, "value", [], [{xmlcdata, lists:flatten(io_lib:format("~p.",[DBs]))}]}]},
1396 +                % default to log
1397 +                {xmlelement, "field", [{"type", "list-single"},
1398 +                                       {"label",
1399 +                                        translate:translate(Lang, "Default")},
1400 +                                       {"var", "dolog_default"}],
1401 +                 [?DEFVAL(atom_to_list(DLD)),
1402 +                  ?LISTLINE(translate:translate(Lang, "Log Messages"), "true"),
1403 +                  ?LISTLINE(translate:translate(Lang, "Do Not Log Messages"), "false")
1404 +                 ]},
1405 +                % groupchat
1406 +                {xmlelement, "field", [{"type", "list-single"},
1407 +                                       {"label",
1408 +                                        translate:translate(Lang, "Groupchat messages logging")},
1409 +                                       {"var", "groupchat"}],
1410 +                 [?DEFVAL(atom_to_list(GroupChat)),
1411 +                  ?LISTLINE("all", "all"),
1412 +                  ?LISTLINE("none", "none"),
1413 +                  ?LISTLINE("send", "send"),
1414 +                  ?LISTLINE("half", "half")
1415 +                 ]},
1416 +                % ignore_jids
1417 +                {xmlelement, "field", [{"type", "text-multi"},
1418 +                                       {"label",
1419 +                                        translate:translate(
1420 +                                          Lang, "Jids/Domains to ignore")},
1421 +                                       {"var", "ignore_list"}],
1422 +                 [{xmlelement, "value", [], [{xmlcdata, list_to_string(IgnoreJids)}]}]},
1423 +                % purge older days
1424 +                {xmlelement, "field", [{"type", "text-single"},
1425 +                                       {"label",
1426 +                                        translate:translate(
1427 +                                          Lang, "Purge messages older than (days)")},
1428 +                                       {"var", "purge_older_days"}],
1429 +                 [{xmlelement, "value", [], [{xmlcdata, PurgeDays}]}]},
1430 +                % poll users settings
1431 +                {xmlelement, "field", [{"type", "text-single"},
1432 +                                       {"label",
1433 +                                        translate:translate(
1434 +                                          Lang, "Poll users settings (seconds)")},
1435 +                                       {"var", "poll_users_settings"}],
1436 +                 [{xmlelement, "value", [], [{xmlcdata, integer_to_list(PollTime)}]}]}
1437 +             ]}]}.
1438 +
1439 +get_form(_Host, ["mod_logdb"], #jid{luser = LUser, lserver = LServer} = _Jid, Lang) ->
1440 +    get_user_form(LUser, LServer, Lang);
1441 +get_form(_Host, ["mod_logdb_users", User], _JidFrom, Lang) ->
1442 +    #jid{luser=LUser, lserver=LServer} = jlib:string_to_jid(User),
1443 +    get_user_form(LUser, LServer, Lang);
1444 +get_form(Host, ["mod_logdb_settings"], _JidFrom, Lang) ->
1445 +    get_settings_form(Host, Lang);
1446 +get_form(_Host, Command, _, _Lang) ->
1447 +    ?MYDEBUG("asked for form ~p", [Command]),
1448 +    {error, ?ERR_SERVICE_UNAVAILABLE}.
1449 +
1450 +check_log_list([Head | Tail]) ->
1451 +    case lists:member($@, Head) of
1452 +         true -> ok;
1453 +         false -> throw(error)
1454 +    end,
1455 +    % this check for Head to be valid jid
1456 +    case jlib:string_to_jid(Head) of
1457 +         error ->
1458 +            throw(error);
1459 +         _ ->
1460 +            check_log_list(Tail)
1461 +    end;
1462 +check_log_list([]) ->
1463 +    ok.
1464 +
1465 +check_ignore_list([Head | Tail]) ->
1466 +    case lists:member($@, Head) of
1467 +         true -> ok;
1468 +         false -> throw(error)
1469 +    end,
1470 +    % this check for Head to be valid jid
1471 +    case jlib:string_to_jid(Head) of
1472 +         error ->
1473 +            % this check for Head to be valid domain "@domain.org"
1474 +            case lists:nth(1, Head) of
1475 +                 $@ ->
1476 +                    % TODO: this allows spaces and special characters in Head. May be change to nodeprep?
1477 +                    case jlib:nameprep(lists:delete($@, Head)) of
1478 +                         error -> throw(error);
1479 +                         _ -> check_log_list(Tail)
1480 +                    end;
1481 +                 _ -> throw(error)
1482 +            end;
1483 +         _ ->
1484 +            check_ignore_list(Tail)
1485 +    end;
1486 +check_ignore_list([]) ->
1487 +    ok.
1488 +
1489 +parse_users_settings(XData) ->
1490 +    DLD = case lists:keysearch("dolog_default", 1, XData) of
1491 +               {value, {_, [String]}} when String == "true"; String == "false" -> 
1492 +                 list_to_bool(String);
1493 +               _ ->
1494 +                 throw(bad_request)
1495 +          end,
1496 +    DLL = case lists:keysearch("dolog_list", 1, XData) of
1497 +               false ->
1498 +                 throw(bad_request);
1499 +               {value, {_, [[]]}} ->
1500 +                 [];
1501 +               {value, {_, List1}} ->
1502 +                 case catch check_log_list(List1) of
1503 +                      error ->
1504 +                         throw(bad_request);
1505 +                      ok ->
1506 +                         List1
1507 +                 end
1508 +          end,
1509 +    DNLL = case lists:keysearch("donotlog_list", 1, XData) of
1510 +               false ->
1511 +                 throw(bad_request);
1512 +               {value, {_, [[]]}} ->
1513 +                 [];
1514 +               {value, {_, List2}} ->
1515 +                 case catch check_log_list(List2) of
1516 +                      error ->
1517 +                         throw(bad_request);
1518 +                      ok ->
1519 +                         List2
1520 +                 end
1521 +          end,
1522 +    #user_settings{dolog_default=DLD,
1523 +                   dolog_list=DLL,
1524 +                   donotlog_list=DNLL}.
1525 +
1526 +parse_module_settings(XData) ->
1527 +    DLD = case lists:keysearch("dolog_default", 1, XData) of
1528 +               {value, {_, [Str1]}} when Str1 == "true"; Str1 == "false" ->
1529 +                 list_to_bool(Str1);
1530 +               _ ->
1531 +                 throw(bad_request)
1532 +          end,
1533 +    GroupChat = case lists:keysearch("groupchat", 1, XData) of
1534 +                     {value, {_, [Str2]}} when Str2 == "none";
1535 +                                               Str2 == "all";
1536 +                                               Str2 == "send";
1537 +                                               Str2 == "half" ->
1538 +                       list_to_atom(Str2);
1539 +                     _ ->
1540 +                       throw(bad_request)
1541 +                end,
1542 +    Ignore = case lists:keysearch("ignore_list", 1, XData) of
1543 +                  {value, {_, List}} ->
1544 +                    case catch check_ignore_list(List) of
1545 +                         ok ->
1546 +                            List;
1547 +                         error ->
1548 +                            throw(bad_request)
1549 +                    end;
1550 +                  _ ->
1551 +                    throw(bad_request)
1552 +             end,
1553 +    Purge = case lists:keysearch("purge_older_days", 1, XData) of
1554 +                 {value, {_, ["never"]}} ->
1555 +                   never;
1556 +                 {value, {_, [Str3]}} ->
1557 +                   case catch list_to_integer(Str3) of
1558 +                        {'EXIT', {badarg, _}} -> throw(bad_request);
1559 +                        Int1 -> Int1
1560 +                   end;
1561 +                 _ ->
1562 +                   throw(bad_request)
1563 +            end,
1564 +    Poll = case lists:keysearch("poll_users_settings", 1, XData) of
1565 +                {value, {_, [Str4]}} ->
1566 +                  case catch list_to_integer(Str4) of
1567 +                       {'EXIT', {badarg, _}} -> throw(bad_request);
1568 +                       Int2 -> Int2
1569 +                  end;
1570 +                _ ->
1571 +                  throw(bad_request)
1572 +           end,
1573 +    #state{dolog_default=DLD,
1574 +           groupchat=GroupChat,
1575 +           ignore_jids=Ignore,
1576 +           purge_older_days=Purge,
1577 +           poll_users_settings=Poll}.
1578 +
1579 +set_form(From, _Host, ["mod_logdb"], _Lang, XData) ->
1580 +    #jid{luser=LUser, lserver=LServer} = From,
1581 +    case catch parse_users_settings(XData) of
1582 +         bad_request ->
1583 +            {error, ?ERR_BAD_REQUEST};
1584 +         UserSettings ->
1585 +            case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
1586 +                 ok ->
1587 +                    {result, []};
1588 +                 error ->
1589 +                    {error, ?ERR_INTERNAL_SERVER_ERROR}
1590 +            end
1591 +    end;
1592 +set_form(_From, _Host, ["mod_logdb_users", User], _Lang, XData) ->
1593 +    #jid{luser=LUser, lserver=LServer} = jlib:string_to_jid(User),
1594 +    case catch parse_users_settings(XData) of
1595 +         bad_request -> {error, ?ERR_BAD_REQUEST};
1596 +         UserSettings ->
1597 +            case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
1598 +                 ok ->
1599 +                    {result, []};
1600 +                 error ->
1601 +                    {error, ?ERR_INTERNAL_SERVER_ERROR}
1602 +            end
1603 +    end;
1604 +set_form(_From, Host, ["mod_logdb_settings"], _Lang, XData) ->
1605 +    case catch parse_module_settings(XData) of
1606 +         bad_request -> {error, ?ERR_BAD_REQUEST};
1607 +         Settings ->
1608 +            case mod_logdb:set_module_settings(Host, Settings) of
1609 +                 ok ->
1610 +                    {result, []};
1611 +                 error ->
1612 +                    {error, ?ERR_INTERNAL_SERVER_ERROR}
1613 +            end
1614 +    end;
1615 +set_form(From, _Host, Node, _Lang, XData) ->
1616 +    User = jlib:jid_to_string(jlib:jid_remove_resource(From)),
1617 +    ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]),
1618 +    {error, ?ERR_SERVICE_UNAVAILABLE}.
1619 +
1620 +%adhoc_sm_items(Acc, From, To, Request) ->
1621 +%    ?MYDEBUG("adhoc_sm_items Acc=~p From=~p To=~p Request=~p", [Acc, From, To, Request]),
1622 +%    Acc.
1623 +
1624 +%adhoc_sm_commands(Acc, From, To, Request) ->
1625 +%    ?MYDEBUG("adhoc_sm_commands Acc=~p From=~p To=~p Request=~p", [Acc, From, To, Request]),
1626 +%    Acc.
1627 +
1628 +get_all_vh_users(Host, Server, Lang) ->
1629 +    case catch ejabberd_auth:get_vh_registered_users(Host) of
1630 +        {'EXIT', _Reason} ->
1631 +            [];
1632 +        Users ->
1633 +            SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1634 +            case length(SUsers) of
1635 +                N when N =< 100 ->
1636 +                    lists:map(fun({S, U}) ->
1637 +                                      ?NODE(U ++ "@" ++ S, "mod_logdb_users/" ++ U ++ "@" ++ S)
1638 +                              end, SUsers);
1639 +                N ->
1640 +                    NParts = trunc(math:sqrt(N * 0.618)) + 1,
1641 +                    M = trunc(N / NParts) + 1,
1642 +                    lists:map(fun(K) ->
1643 +                                      L = K + M - 1,
1644 +                                      Node =
1645 +                                          "@" ++ integer_to_list(K) ++
1646 +                                          "-" ++ integer_to_list(L),
1647 +                                      {FS, FU} = lists:nth(K, SUsers),
1648 +                                      {LS, LU} =
1649 +                                          if L < N -> lists:nth(L, SUsers);
1650 +                                             true -> lists:last(SUsers)
1651 +                                          end,
1652 +                                      Name =
1653 +                                          FU ++ "@" ++ FS ++
1654 +                                          " -- " ++
1655 +                                          LU ++ "@" ++ LS,
1656 +                                      ?NODE(Name, "mod_logdb_users/" ++ Node)
1657 +                              end, lists:seq(1, N, M))
1658 +            end
1659 +    end.
1660 --- src/mod_logdb.hrl.orig      Tue Dec 11 14:23:19 2007
1661 +++ src/mod_logdb.hrl   Tue Aug  7 16:50:32 2007
1662 @@ -0,0 +1,29 @@
1663 +%%%----------------------------------------------------------------------
1664 +%%% File    : mod_logdb.hrl
1665 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
1666 +%%% Purpose :
1667 +%%% Version : trunk
1668 +%%% Id      : $Id$
1669 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
1670 +%%%----------------------------------------------------------------------
1671 +
1672 +-define(logdb_debug, true).
1673 +
1674 +-ifdef(logdb_debug).
1675 +-define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n",
1676 +                                       [calendar:local_time(),?MODULE,?LINE]++Args)).
1677 +-else.
1678 +-define(MYDEBUG(_F,_A),[]).
1679 +-endif.
1680 +
1681 +-record(msg,   {timestamp,
1682 +                owner_name,
1683 +                peer_name, peer_server, peer_resource,
1684 +                direction,
1685 +                type, subject,
1686 +                body}).
1687 +
1688 +-record(user_settings, {owner_name,
1689 +                        dolog_default,
1690 +                        dolog_list=[],
1691 +                        donotlog_list=[]}).
1692 --- src/mod_logdb_mnesia.erl.orig       Tue Dec 11 14:23:19 2007
1693 +++ src/mod_logdb_mnesia.erl    Wed Aug 22 22:58:11 2007
1694 @@ -0,0 +1,513 @@
1695 +%%%----------------------------------------------------------------------
1696 +%%% File    : mod_logdb_mnesia.erl
1697 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
1698 +%%% Purpose : mnesia backend for mod_logdb
1699 +%%% Version : trunk
1700 +%%% Id      : $Id$
1701 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
1702 +%%%----------------------------------------------------------------------
1703 +
1704 +-module(mod_logdb_mnesia).
1705 +-author('o.palij@gmail.com').
1706 +-vsn('$Revision$').
1707 +
1708 +-include("mod_logdb.hrl").
1709 +-include("ejabberd.hrl").
1710 +-include("jlib.hrl").
1711 +
1712 +-behaviour(gen_logdb).
1713 +-behaviour(gen_server).
1714 +   
1715 +% gen_server
1716 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
1717 +% gen_mod
1718 +-export([start/2, stop/1]).
1719 +% gen_logdb
1720 +-export([log_message/2,
1721 +         rebuild_stats/1,
1722 +         rebuild_stats_at/2,
1723 +         delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
1724 +         get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
1725 +         get_dates/1,
1726 +         get_users_settings/1, get_user_settings/2, set_user_settings/3]).
1727
1728 +-define(PROCNAME, mod_logdb_mnesia).
1729 +-define(CALL_TIMEOUT, 240000).
1730 +  
1731 +-record(state, {vhost}).
1732 +
1733 +-record(stats, {user, at, count}).
1734 +
1735 +prefix() ->
1736 +   "logdb_".
1737 +
1738 +suffix(VHost) ->
1739 +   "_" ++ VHost.
1740 +
1741 +stats_table(VHost) ->
1742 +   list_to_atom(prefix() ++ "stats" ++ suffix(VHost)).
1743 +
1744 +table_name(VHost, Date) ->
1745 +   list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)).
1746 +
1747 +settings_table(VHost) ->
1748 +   list_to_atom(prefix() ++ "settings" ++ suffix(VHost)).
1749 +
1750 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1751 +%
1752 +% gen_mod callbacks
1753 +%
1754 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1755 +start(VHost, Opts) ->
1756 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1757 +   gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
1758 +
1759 +stop(VHost) ->
1760 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1761 +   gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
1762 +
1763 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1764 +%
1765 +% gen_server callbacks
1766 +%
1767 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1768 +init([VHost, _Opts]) ->
1769 +   case mnesia:system_info(is_running) of
1770 +        yes ->
1771 +          ok = create_stats_table(VHost),
1772 +          ok = create_settings_table(VHost),
1773 +          {ok, #state{vhost=VHost}};
1774 +        no ->
1775 +          ?ERROR_MSG("Mnesia not running", []),
1776 +          {stop, db_connection_failed};
1777 +        Status ->
1778 +          ?ERROR_MSG("Mnesia status: ~p", [Status]),
1779 +          {stop, db_connection_failed}
1780 +   end.
1781 +
1782 +handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) ->
1783 +    {reply, log_message_int(VHost, Msg), State};
1784 +handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) ->
1785 +    {atomic, ok} = delete_nonexistent_stats(VHost),
1786 +    Reply =
1787 +      lists:foreach(fun(Date) ->
1788 +                        rebuild_stats_at_int(VHost, Date)
1789 +                    end, get_dates_int(VHost)),
1790 +    {reply, Reply, State};
1791 +handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
1792 +    Reply = rebuild_stats_at_int(VHost, Date),
1793 +    {reply, Reply, State}; 
1794 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
1795 +    Table = table_name(VHost, Date),
1796 +    Fun = fun() ->
1797 +             lists:foreach(
1798 +                fun(Msg) ->
1799 +                    mnesia:write_lock_table(stats_table(VHost)),
1800 +                    mnesia:write_lock_table(Table),
1801 +                    mnesia:delete_object(Table, Msg, write)
1802 +               end, Msgs)
1803 +          end,
1804 +    DRez = case mnesia:transaction(Fun) of
1805 +                {aborted, Reason} ->
1806 +                   ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
1807 +                   error;
1808 +                _ ->
1809 +                   ok
1810 +           end,
1811 +    Reply =
1812 +      case rebuild_stats_at_int(VHost, Date) of
1813 +           error ->
1814 +             error;
1815 +           ok ->
1816 +             DRez
1817 +      end,
1818 +    {reply, Reply, State};
1819 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) ->
1820 +    Table = table_name(VHost, Date),
1821 +    MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc)
1822 +                     when Owner == User ->
1823 +                       mnesia:delete_object(Table, Msg, write),
1824 +                       ok;
1825 +                   (_Msg, _Acc) -> ok
1826 +                end,
1827 +    DRez = case mnesia:transaction(fun() ->
1828 +                                     mnesia:foldl(MsgDelete, ok, Table)
1829 +                                   end) of
1830 +                {aborted, Reason} ->
1831 +                   ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]),
1832 +                   error;
1833 +                _ ->
1834 +                   ok
1835 +    end,
1836 +    Reply =
1837 +      case rebuild_stats_at_int(VHost, Date) of
1838 +           error ->
1839 +             error;
1840 +           ok ->
1841 +             DRez
1842 +      end,
1843 +    {reply, Reply, State};
1844 +handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) ->
1845 +    Reply =
1846 +      case mnesia:delete_table(table_name(VHost, Date)) of
1847 +           {atomic, ok} ->
1848 +              delete_stats_by_vhost_at_int(VHost, Date);
1849 +           {aborted, Reason} ->
1850 +              ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]),
1851 +              error
1852 +      end,
1853 +    {reply, Reply, State};
1854 +handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) ->
1855 +    Fun = fun(#stats{at=Date, count=Count}, Stats) ->
1856 +              case lists:keysearch(Date, 1, Stats) of
1857 +                   false ->
1858 +                      lists:append(Stats, [{Date, Count}]);
1859 +                   {value, {_, TempCount}} ->
1860 +                      lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count})
1861 +              end
1862 +          end,
1863 +    Reply =
1864 +      case mnesia:transaction(fun() ->
1865 +                                   mnesia:foldl(Fun, [], stats_table(VHost))
1866 +                                end) of
1867 +             {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)};
1868 +             {aborted, Reason} -> {error, Reason}
1869 +      end,
1870 +    {reply, Reply, State};
1871 +handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
1872 +    Fun = fun() ->
1873 +             Pat = #stats{user='$1', at=Date, count='$2'},
1874 +             mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
1875 +          end,
1876 +    Reply =
1877 +      case mnesia:transaction(Fun) of
1878 +           {atomic, Result} ->
1879 +                     {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))};
1880 +           {aborted, Reason} ->
1881 +                     {error, Reason}
1882 +      end,
1883 +    {reply, Reply, State};
1884 +handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) ->
1885 +    Reply =
1886 +      case mnesia:transaction(fun() ->
1887 +                                 Pat = #stats{user=User, at='$1', count='$2'},
1888 +                                 mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
1889 +                              end) of
1890 +           {atomic, Result} ->
1891 +                    {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])};
1892 +           {aborted, Reason} ->
1893 +                    {error, Reason}
1894 +      end,
1895 +    {reply, Reply, State};
1896 +handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) ->
1897 +    Reply =
1898 +      case mnesia:transaction(fun() ->
1899 +                                Pat = #msg{owner_name=User, _='_'},
1900 +                                mnesia:select(table_name(VHost, Date),
1901 +                                              [{Pat, [], ['$_']}])
1902 +                        end) of
1903 +           {atomic, Result} -> {ok, Result};
1904 +           {aborted, Reason} ->
1905 +                    {error, Reason}
1906 +      end,
1907 +    {reply, Reply, State};
1908 +handle_call({get_dates}, _From, #state{vhost=VHost}=State) ->
1909 +    {reply, get_dates_int(VHost), State};
1910 +handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) ->
1911 +    Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}),
1912 +    {reply, {ok, Reply}, State};
1913 +handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) ->
1914 +   Reply =
1915 +    case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of
1916 +         [] -> [];
1917 +         [Setting] ->
1918 +            Setting
1919 +    end,
1920 +   {reply, Reply, State};
1921 +handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) ->
1922 +    ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]),
1923 +    Reply = mnesia:dirty_write(settings_table(VHost), Set),
1924 +    {reply, Reply, State};
1925 +handle_call({stop}, _From, State) ->
1926 +   {stop, normal, ok, State};
1927 +handle_call(Msg, _From, State) ->
1928 +    ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
1929 +    {noreply, State}.
1930 +
1931 +handle_cast(Msg, State) ->
1932 +    ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
1933 +    {noreply, State}.
1934 +
1935 +handle_info(Info, State) ->
1936 +    ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
1937 +    {noreply, State}.
1938 +
1939 +terminate(_Reason, _State) ->
1940 +    ok.
1941 +
1942 +code_change(_OldVsn, State, _Extra) ->
1943 +    {ok, State}.
1944 +
1945 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1946 +%
1947 +% gen_logdb callbacks
1948 +%
1949 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1950 +log_message(VHost, Msg) ->
1951 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1952 +   gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
1953 +rebuild_stats(VHost) ->
1954 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1955 +   gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
1956 +rebuild_stats_at(VHost, Date) ->
1957 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1958 +   gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
1959 +delete_messages_by_user_at(VHost, Msgs, Date) ->
1960 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1961 +   gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
1962 +delete_all_messages_by_user_at(User, VHost, Date) ->
1963 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1964 +   gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
1965 +delete_messages_at(VHost, Date) ->
1966 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1967 +   gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
1968 +get_vhost_stats(VHost) ->
1969 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1970 +   gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
1971 +get_vhost_stats_at(VHost, Date) ->
1972 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1973 +   gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
1974 +get_user_stats(User, VHost) ->
1975 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1976 +   gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
1977 +get_user_messages_at(User, VHost, Date) ->
1978 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1979 +   gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
1980 +get_dates(VHost) ->
1981 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1982 +   gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
1983 +get_user_settings(User, VHost) ->
1984 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1985 +   gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
1986 +get_users_settings(VHost) ->
1987 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1988 +   gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
1989 +set_user_settings(User, VHost, Set) ->
1990 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1991 +   gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
1992 +
1993 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1994 +%
1995 +% internals
1996 +%
1997 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1998 +log_message_int(VHost, #msg{timestamp=Timestamp}=Msg) ->
1999 +    Date = mod_logdb:convert_timestamp_brief(Timestamp),
2000 +
2001 +    ATable = table_name(VHost, Date),
2002 +    Fun = fun() ->
2003 +              mnesia:write_lock_table(ATable),
2004 +              mnesia:write(ATable, Msg, write)
2005 +          end,
2006 +    % log message, increment stats for both users
2007 +    case mnesia:transaction(Fun) of
2008 +         % if table does not exists - create it and try to log message again
2009 +         {aborted,{no_exists, _Table}} ->
2010 +             case create_msg_table(VHost, Date) of
2011 +                  {aborted, CReason} ->
2012 +                     ?ERROR_MSG("Failed to log message: ~p", [CReason]),
2013 +                     error;
2014 +                  {atomic, ok} ->
2015 +                     ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
2016 +                     log_message_int(VHost, Msg)
2017 +             end;
2018 +         {aborted, TReason} ->
2019 +             ?ERROR_MSG("Failed to log message: ~p", [TReason]),
2020 +             error;
2021 +         {atomic, _} ->
2022 +             ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
2023 +                                                    Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
2024 +             increment_user_stats(Msg#msg.owner_name, VHost, Date)
2025 +    end.
2026 +
2027 +increment_user_stats(Owner, VHost, Date) ->
2028 +    Fun = fun() ->
2029 +            Pat = #stats{user=Owner, at=Date, count='$1'},
2030 +            mnesia:write_lock_table(stats_table(VHost)),
2031 +            case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of
2032 +                 [] ->
2033 +                    mnesia:write(stats_table(VHost),
2034 +                                 #stats{user=Owner,
2035 +                                        at=Date,
2036 +                                        count=1},
2037 +                                 write);
2038 +                 [Stats] ->
2039 +                    mnesia:delete_object(stats_table(VHost),
2040 +                                         #stats{user=Owner,
2041 +                                                at=Date,
2042 +                                                count=Stats#stats.count},
2043 +                                         write),
2044 +                    New = Stats#stats{count = Stats#stats.count+1},
2045 +                    if
2046 +                      New#stats.count > 0 -> mnesia:write(stats_table(VHost),
2047 +                                                          New,
2048 +                                                          write);
2049 +                      true -> ok
2050 +                    end
2051 +            end
2052 +          end,
2053 +    case mnesia:transaction(Fun) of
2054 +         {aborted, Reason} ->
2055 +             ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]),
2056 +             error;
2057 +         {atomic, _} ->
2058 +             ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]),
2059 +             ok
2060 +    end.
2061 +
2062 +get_dates_int(VHost) ->
2063 +    Tables = mnesia:system_info(tables),
2064 +    lists:foldl(fun(ATable, Dates) ->
2065 +                    Table = atom_to_list(ATable),
2066 +                    case regexp:match(Table, VHost++"$") of
2067 +                         {match, _, _} ->
2068 +                            case regexp:match(Table,"_[0-9]+-[0-9]+-[0-9]+_") of
2069 +                                 {match, S, E} ->
2070 +                                     lists:append(Dates, [lists:sublist(Table,S+1,E-2)]);
2071 +                                 nomatch ->
2072 +                                     Dates
2073 +                            end;
2074 +                         nomatch ->
2075 +                            Dates
2076 +                    end
2077 +                end, [], Tables).
2078 +
2079 +rebuild_stats_at_int(VHost, Date) ->
2080 +    Table = table_name(VHost, Date),
2081 +    STable = stats_table(VHost),
2082 +    CFun = fun(Msg, Stats) ->
2083 +               Owner = Msg#msg.owner_name,
2084 +               case lists:keysearch(Owner, 1, Stats) of
2085 +                    {value, {_, Count}} ->
2086 +                       lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1});
2087 +                    false ->
2088 +                       lists:append(Stats, [{Owner, 1}])
2089 +               end
2090 +           end,
2091 +    DFun = fun(#stats{at=SDate} = Stat, _Acc)
2092 +                when SDate == Date ->
2093 +                 mnesia:delete_object(stats_table(VHost), Stat, write);
2094 +              (_Stat, _Acc) -> ok
2095 +           end,
2096 +    % TODO: Maybe unregister hooks ?
2097 +    case mnesia:transaction(fun() ->
2098 +                               mnesia:write_lock_table(Table),
2099 +                               mnesia:write_lock_table(STable),
2100 +                               % Calc stats for VHost at Date
2101 +                               case mnesia:foldl(CFun, [], Table) of
2102 +                                    [] -> empty;
2103 +                                    AStats ->
2104 +                                      % Delete all stats for VHost at Date
2105 +                                      mnesia:foldl(DFun, [], STable),
2106 +                                      % Write new calc'ed stats
2107 +                                      lists:foreach(fun({Owner, Count}) ->
2108 +                                                        WStat = #stats{user=Owner, at=Date, count=Count},
2109 +                                                        mnesia:write(stats_table(VHost), WStat, write)
2110 +                                                    end, AStats),
2111 +                                      ok
2112 +                               end
2113 +                            end) of
2114 +         {aborted, Reason} ->
2115 +              ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]),
2116 +              error;
2117 +         {atomic, ok} ->
2118 +              ok;
2119 +         {atomic, empty} ->
2120 +              {atomic,ok} = mnesia:delete_table(Table),
2121 +              ?MYDEBUG("Dropped table at ~p", [Date]),
2122 +              ok
2123 +    end.
2124 +
2125 +delete_nonexistent_stats(VHost) ->
2126 +    Dates = get_dates_int(VHost),
2127 +    mnesia:transaction(fun() ->
2128 +                          mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) ->
2129 +                                          case lists:member(Date, Dates) of
2130 +                                               false -> mnesia:delete_object(Stat);
2131 +                                               true -> ok
2132 +                                          end
2133 +                                       end, ok, stats_table(VHost))
2134 +                       end).
2135 +
2136 +delete_stats_by_vhost_at_int(VHost, Date) ->
2137 +    StatsDelete = fun(#stats{at=SDate} = Stat, _Acc)
2138 +                      when SDate == Date ->
2139 +                        mnesia:delete_object(stats_table(VHost), Stat, write),
2140 +                        ok;
2141 +                     (_Msg, _Acc) -> ok
2142 +                  end,
2143 +    case mnesia:transaction(fun() ->
2144 +                             mnesia:write_lock_table(stats_table(VHost)),
2145 +                             mnesia:foldl(StatsDelete, ok, stats_table(VHost))
2146 +                       end) of
2147 +         {aborted, Reason} ->
2148 +            ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]),
2149 +            rebuild_stats_at_int(VHost, Date);
2150 +         _ ->
2151 +            ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]),
2152 +            ok
2153 +    end.
2154 +
2155 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2156 +%
2157 +% tables internals
2158 +%
2159 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2160 +create_stats_table(VHost) ->
2161 +    SName = stats_table(VHost),
2162 +    case mnesia:create_table(SName,
2163 +                             [{disc_only_copies, [node()]},
2164 +                              {type, bag},
2165 +                              {attributes, record_info(fields, stats)},
2166 +                              {record_name, stats}
2167 +                             ]) of
2168 +         {atomic, ok} ->
2169 +             ?MYDEBUG("Created stats table for ~p", [VHost]),
2170 +             lists:foreach(fun(Date) ->
2171 +                    rebuild_stats_at_int(VHost, Date)
2172 +             end, get_dates_int(VHost)),
2173 +             ok;
2174 +         {aborted, {already_exists, _}} ->
2175 +             ?MYDEBUG("Stats table for ~p already exists", [VHost]),
2176 +             ok;
2177 +         {aborted, Reason} ->
2178 +             ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
2179 +             error
2180 +    end.
2181 +
2182 +create_settings_table(VHost) ->
2183 +    SName = settings_table(VHost),
2184 +    case mnesia:create_table(SName,
2185 +                             [{disc_copies, [node()]},
2186 +                              {type, set},
2187 +                              {attributes, record_info(fields, user_settings)},
2188 +                              {record_name, user_settings}
2189 +                             ]) of
2190 +         {atomic, ok} ->
2191 +             ?MYDEBUG("Created settings table for ~p", [VHost]),
2192 +             ok;
2193 +         {aborted, {already_exists, _}} ->
2194 +             ?MYDEBUG("Settings table for ~p already exists", [VHost]),
2195 +             ok;
2196 +         {aborted, Reason} ->
2197 +             ?ERROR_MSG("Failed to create settings table: ~p", [Reason]),
2198 +             error
2199 +    end.
2200 +
2201 +create_msg_table(VHost, Date) ->
2202 +    mnesia:create_table(
2203 +              table_name(VHost, Date),
2204 +              [{disc_only_copies, [node()]},
2205 +               {type, bag},
2206 +               {attributes, record_info(fields, msg)},
2207 +               {record_name, msg}]).
2208 --- src/mod_logdb_mysql.erl.orig        Tue Dec 11 14:23:19 2007
2209 +++ src/mod_logdb_mysql.erl     Sun Nov 18 20:53:55 2007
2210 @@ -0,0 +1,936 @@
2211 +%%%----------------------------------------------------------------------
2212 +%%% File    : mod_logdb_mysql.erl
2213 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
2214 +%%% Purpose : MySQL backend for mod_logdb
2215 +%%% Version : trunk
2216 +%%% Id      : $Id$
2217 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
2218 +%%%----------------------------------------------------------------------
2219 +
2220 +-module(mod_logdb_mysql).
2221 +-author('o.palij@gmail.com').
2222 +-vsn('$Revision$').
2223 +
2224 +-include("mod_logdb.hrl").
2225 +-include("ejabberd.hrl").
2226 +-include("jlib.hrl").
2227 +
2228 +-behaviour(gen_logdb).
2229 +-behaviour(gen_server).
2230 +
2231 +% gen_server
2232 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2233 +% gen_mod
2234 +-export([start/2, stop/1]).
2235 +% gen_logdb
2236 +-export([log_message/2,
2237 +         rebuild_stats/1,
2238 +         rebuild_stats_at/2,
2239 +         delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2240 +         get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2241 +         get_dates/1,
2242 +         get_users_settings/1, get_user_settings/2, set_user_settings/3]).
2243 +
2244 +% gen_server call timeout
2245 +-define(CALL_TIMEOUT, 60000).
2246 +-define(TIMEOUT, 60000).
2247 +-define(INDEX_SIZE, integer_to_list(170)).
2248 +-define(PROCNAME, mod_logdb_mysql).
2249 +
2250 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
2251 +                    list_to_string/1, string_to_list/1,
2252 +                    convert_timestamp_brief/1]).
2253 +
2254 +-record(state, {dbref, vhost}).
2255 +
2256 +% replace "." with "_"
2257 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
2258 +                                    (A) -> A
2259 +                                 end, VHost).
2260 +prefix() ->
2261 +   "`logdb_".
2262 +
2263 +suffix(VHost) ->
2264 +   "_" ++ escape_vhost(VHost) ++ "`".
2265 +
2266 +messages_table(VHost, Date) ->
2267 +   prefix() ++ "messages_" ++ Date ++ suffix(VHost).
2268 +
2269 +stats_table(VHost) ->
2270 +   prefix() ++ "stats" ++ suffix(VHost).
2271 +
2272 +settings_table(VHost) ->
2273 +   prefix() ++ "settings" ++ suffix(VHost).
2274 +
2275 +users_table(VHost) ->
2276 +   prefix() ++ "users" ++ suffix(VHost).
2277 +servers_table(VHost) ->
2278 +   prefix() ++ "servers" ++ suffix(VHost).
2279 +resources_table(VHost) ->
2280 +   prefix() ++ "resources" ++ suffix(VHost).
2281 +
2282 +ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ VHost).
2283 +ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ VHost).
2284 +ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ VHost).
2285 +
2286 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2287 +%
2288 +% gen_mod callbacks
2289 +%
2290 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2291 +start(VHost, Opts) ->
2292 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2293 +   gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2294 +
2295 +stop(VHost) ->
2296 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2297 +   gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2298 +
2299 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2300 +%
2301 +% gen_server callbacks
2302 +%
2303 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2304 +init([VHost, Opts]) ->
2305 +   crypto:start(),
2306 +
2307 +   Server = gen_mod:get_opt(server, Opts, "localhost"),
2308 +   Port = gen_mod:get_opt(port, Opts, 3128),
2309 +   DB = gen_mod:get_opt(db, Opts, "logdb"),
2310 +   User = gen_mod:get_opt(user, Opts, "root"),
2311 +   Password = gen_mod:get_opt(password, Opts, ""),
2312 +
2313 +   LogFun = fun(debug, Format, Argument) ->
2314 +                 ?MYDEBUG(Format, Argument);
2315 +               (error, Format, Argument) ->
2316 +                 ?ERROR_MSG(Format, Argument);
2317 +               (Level, Format, Argument) ->
2318 +                 ?MYDEBUG("MySQL (~p)~n", [Level]),
2319 +                 ?MYDEBUG(Format, Argument)
2320 +            end,
2321 +   case mysql_conn:start(Server, Port, User, Password, DB, LogFun) of
2322 +       {ok, DBRef} ->
2323 +           ok = create_stats_table(DBRef, VHost),
2324 +           ok = create_settings_table(DBRef, VHost),
2325 +           ok = create_users_table(DBRef, VHost),
2326 +           % clear ets cache every ...
2327 +           timer:send_interval(timer:hours(12), clear_ets_tables),
2328 +           ok = create_servers_table(DBRef, VHost),
2329 +           ok = create_resources_table(DBRef, VHost),
2330 +           erlang:monitor(process, DBRef),
2331 +           {ok, #state{dbref=DBRef, vhost=VHost}};
2332 +       {error, Reason} ->
2333 +           ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
2334 +           {stop, db_connection_failed}
2335 +   end.
2336 +
2337 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2338 +    {reply, log_message_int(DBRef, VHost, Msg), State};
2339 +handle_call({rebuild_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2340 +    ok = delete_nonexistent_stats(DBRef, VHost),
2341 +    Reply = 
2342 +      lists:foreach(fun(Date) ->
2343 +                        catch rebuild_stats_at_int(DBRef, VHost, Date)
2344 +                    end, get_dates_int(DBRef, VHost)),
2345 +    {reply, Reply, State};
2346 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2347 +    Reply = rebuild_stats_at_int(DBRef, VHost, Date),
2348 +    {reply, Reply, State};
2349 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
2350 +    {reply, error, State};
2351 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2352 +    Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
2353 +                             ["\"",Timestamp,"\"",","]
2354 +                         end, Msgs),
2355 +
2356 +    Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
2357 +
2358 +    Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
2359 +                             "WHERE timestamp IN (", Temp1],
2360 +
2361 +    Reply =
2362 +      case sql_query_internal(DBRef, Query) of
2363 +           {updated, Aff} ->
2364 +              ?MYDEBUG("Aff=~p", [Aff]),
2365 +              rebuild_stats_at_int(DBRef, VHost, Date);
2366 +           {error, _} ->
2367 +              error
2368 +      end,
2369 +    {reply, Reply, State};
2370 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2371 +    Owner_id = get_user_id(DBRef, VHost, User),
2372 +    DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
2373 +                 "WHERE owner_id=\"",Owner_id,"\";"],
2374 +    Reply =
2375 +      case sql_query_internal(DBRef, DQuery) of
2376 +           {updated, _} ->
2377 +              rebuild_stats_at_int(DBRef, VHost, Date);
2378 +           {error, _} ->
2379 +              error
2380 +      end,
2381 +    {reply, Reply, State};
2382 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2383 +    Reply =
2384 +      case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of
2385 +           {updated, _} ->
2386 +              Query = ["DELETE FROM ",stats_table(VHost)," "
2387 +                          "WHERE at=\"",Date,"\";"],
2388 +              case sql_query_internal(DBRef, Query) of
2389 +                   {updated, _} ->
2390 +                      ok;
2391 +                   {error, _} ->
2392 +                      error
2393 +              end;
2394 +           {error, _} ->
2395 +              error
2396 +      end,
2397 +    {reply, Reply, State};
2398 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2399 +    SName = stats_table(VHost),
2400 +    Query = ["SELECT at, sum(count) ",
2401 +                "FROM ",SName," ",
2402 +                "GROUP BY at ",
2403 +                "ORDER BY DATE(at) DESC;"
2404 +            ],
2405 +    Reply =
2406 +      case sql_query_internal(DBRef, Query) of
2407 +           {data, Result} ->
2408 +              {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
2409 +           {error, Reason} ->
2410 +              % TODO: Duplicate error message ?
2411 +              {error, Reason}
2412 +      end,
2413 +    {reply, Reply, State};
2414 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2415 +    SName = stats_table(VHost),
2416 +    Query = ["SELECT username, count ",
2417 +                "FROM ",SName," ",
2418 +                "JOIN ",users_table(VHost)," ON owner_id=user_id "
2419 +                "WHERE at=\"",Date,"\";"
2420 +            ],
2421 +    Reply =
2422 +      case sql_query_internal(DBRef, Query) of
2423 +           {data, Result} ->
2424 +              {ok, lists:reverse(
2425 +                     lists:keysort(2,
2426 +                                   [ {User, list_to_integer(Count)} || [User, Count] <- Result]))};
2427 +           {error, Reason} ->
2428 +              % TODO:
2429 +              {error, Reason}
2430 +      end,
2431 +    {reply, Reply, State};
2432 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2433 +    SName = stats_table(VHost),
2434 +    Query = ["SELECT at, count ",
2435 +                "FROM ",SName," ",
2436 +                "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
2437 +                "ORDER BY DATE(at) DESC;"
2438 +            ],
2439 +    Reply =
2440 +      case sql_query_internal(DBRef, Query) of
2441 +           {data, Result} ->
2442 +              {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]};
2443 +           {error, Result} ->
2444 +              {error, Result}
2445 +      end,
2446 +    {reply, Reply, State};
2447 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2448 +    TName = messages_table(VHost, Date),
2449 +    UName = users_table(VHost),
2450 +    SName = servers_table(VHost),
2451 +    RName = resources_table(VHost),
2452 +    Query = ["SELECT users.username,",
2453 +                    "servers.server,",
2454 +                    "resources.resource,",
2455 +                    "messages.direction,"
2456 +                    "messages.type,"
2457 +                    "messages.subject,"
2458 +                    "messages.body,"
2459 +                    "messages.timestamp "
2460 +               "FROM ",TName," AS messages "
2461 +                "JOIN ",UName," AS users ON peer_name_id=user_id ",
2462 +                "JOIN ",SName," AS servers ON peer_server_id=server_id ",
2463 +                "JOIN ",RName," AS resources ON peer_resource_id=resource_id ",
2464 +               "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
2465 +               "ORDER BY timestamp ASC;"],
2466 +    Reply =
2467 +      case sql_query_internal(DBRef, Query) of
2468 +           {data, Result} ->
2469 +              Fun = fun([Peer_name, Peer_server, Peer_resource,
2470 +                         Direction,
2471 +                         Type,
2472 +                         Subject, Body,
2473 +                         Timestamp]) ->
2474 +                          #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
2475 +                               direction=list_to_atom(Direction),
2476 +                               type=Type,
2477 +                               subject=Subject, body=Body,
2478 +                               timestamp=Timestamp}
2479 +                    end,
2480 +              {ok, lists:map(Fun, Result)};
2481 +           {error, Reason} ->
2482 +              {error, Reason}
2483 +      end,
2484 +    {reply, Reply, State};
2485 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2486 +    SName = stats_table(VHost),
2487 +    Query = ["SELECT at ",
2488 +                "FROM ",SName," ",
2489 +                "GROUP BY at ",
2490 +                "ORDER BY DATE(at) DESC;"
2491 +            ],
2492 +    Reply =
2493 +       case sql_query_internal(DBRef, Query) of
2494 +            {data, Result} ->
2495 +               [ Date || [Date] <- Result ];
2496 +            {error, Reason} ->
2497 +               {error, Reason}
2498 +       end,
2499 +    {reply, Reply, State};
2500 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2501 +    Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
2502 +                "FROM ",settings_table(VHost)," ",
2503 +             "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
2504 +    Reply = 
2505 +      case sql_query_internal(DBRef, Query) of
2506 +           {data, Result} ->
2507 +              {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
2508 +                                 #user_settings{owner_name=Owner,
2509 +                                                dolog_default=list_to_bool(DoLogDef),
2510 +                                                dolog_list=string_to_list(DoLogL),
2511 +                                                donotlog_list=string_to_list(DoNotLogL)
2512 +                                               }
2513 +                             end, Result)};
2514 +           {error, _} ->
2515 +              error
2516 +      end,
2517 +    {reply, Reply, State};
2518 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
2519 +    Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
2520 +                 "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"],
2521 +    Reply =
2522 +      case sql_query_internal(DBRef, Query) of
2523 +           {data, []} ->
2524 +              {ok, []};
2525 +           {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
2526 +              {ok, #user_settings{owner_name=Owner,
2527 +                                  dolog_default=list_to_bool(DoLogDef),
2528 +                                  dolog_list=string_to_list(DoLogL),
2529 +                                  donotlog_list=string_to_list(DoNotLogL)}};
2530 +           {error, _} ->
2531 +              error
2532 +      end,
2533 +    {reply, Reply, State};
2534 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
2535 +                                                     dolog_list=DoLogL,
2536 +                                                     donotlog_list=DoNotLogL}},
2537 +            _From, #state{dbref=DBRef, vhost=VHost} = State) ->
2538 +    User_id = get_user_id(DBRef, VHost, User),
2539 +
2540 +    Query = ["UPDATE ",settings_table(VHost)," ",
2541 +                "SET dolog_default=",bool_to_list(DoLogDef),", ",
2542 +                    "dolog_list='",list_to_string(DoLogL),"', ",
2543 +                    "donotlog_list='",list_to_string(DoNotLogL),"' ",
2544 +                "WHERE owner_id=\"",User_id,"\";"],
2545 +
2546 +    Reply =
2547 +      case sql_query_internal(DBRef, Query) of
2548 +           {updated, 0} ->
2549 +              IQuery = ["INSERT INTO ",settings_table(VHost)," ",
2550 +                            "(owner_id, dolog_default, dolog_list, donotlog_list) ",
2551 +                            "VALUES ",
2552 +                            "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
2553 +              case sql_query_internal_silent(DBRef, IQuery) of
2554 +                   {updated, _} ->
2555 +                       ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
2556 +                       ok;
2557 +                   {error, Reason} ->
2558 +                       case regexp:match(Reason, "#23000") of
2559 +                            % Already exists
2560 +                            {match, _, _} ->
2561 +                                ok;
2562 +                             _ ->
2563 +                                ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
2564 +                                error
2565 +                       end
2566 +              end;
2567 +           {updated, 1} ->
2568 +              ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
2569 +              ok;
2570 +           {error, _} ->
2571 +              error
2572 +      end,
2573 +    {reply, Reply, State};
2574 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
2575 +   ets:delete(ets_users_table(VHost)),
2576 +   ets:delete(ets_servers_table(VHost)),
2577 +   ?MYDEBUG("Stoping mysql backend for ~p", [VHost]),
2578 +   {stop, normal, ok, State};
2579 +handle_call(Msg, _From, State) ->
2580 +    ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
2581 +    {noreply, State}.
2582 +
2583 +handle_cast(Msg, State) ->
2584 +    ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
2585 +    {noreply, State}.
2586 +
2587 +handle_info(clear_ets_tables, State) ->
2588 +    ets:delete_all_objects(ets_users_table(State#state.vhost)),
2589 +    ets:delete_all_objects(ets_resources_table(State#state.vhost)),
2590 +    {noreply, State};
2591 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
2592 +    {stop, connection_dropped, State};
2593 +handle_info(Info, State) ->
2594 +    ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
2595 +    {noreply, State}.
2596 +
2597 +terminate(_Reason, _State) ->
2598 +    ok.
2599 +
2600 +code_change(_OldVsn, State, _Extra) ->
2601 +    {ok, State}.
2602 +
2603 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2604 +%
2605 +% gen_logdb callbacks
2606 +%
2607 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2608 +log_message(VHost, Msg) ->
2609 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2610 +   gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
2611 +rebuild_stats(VHost) ->
2612 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2613 +   gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
2614 +rebuild_stats_at(VHost, Date) ->
2615 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2616 +   gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
2617 +delete_messages_by_user_at(VHost, Msgs, Date) ->
2618 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2619 +   gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
2620 +delete_all_messages_by_user_at(User, VHost, Date) ->
2621 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2622 +   gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
2623 +delete_messages_at(VHost, Date) ->
2624 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2625 +   gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
2626 +get_vhost_stats(VHost) ->
2627 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2628 +   gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
2629 +get_vhost_stats_at(VHost, Date) ->
2630 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2631 +   gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
2632 +get_user_stats(User, VHost) ->
2633 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2634 +   gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
2635 +get_user_messages_at(User, VHost, Date) ->
2636 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2637 +   gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
2638 +get_dates(VHost) ->
2639 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2640 +   gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
2641 +get_users_settings(VHost) ->
2642 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2643 +   gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
2644 +get_user_settings(User, VHost) ->
2645 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2646 +   gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
2647 +set_user_settings(User, VHost, Set) ->
2648 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2649 +   gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
2650 +
2651 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2652 +%
2653 +% internals
2654 +%
2655 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2656 +log_message_int(DBRef, VHost, Msg) ->
2657 +    Date = convert_timestamp_brief(Msg#msg.timestamp),
2658 +
2659 +    Table = messages_table(VHost, Date),
2660 +    Owner_id = get_user_id(DBRef, VHost, Msg#msg.owner_name),
2661 +    Peer_name_id = get_user_id(DBRef, VHost, Msg#msg.peer_name),
2662 +    Peer_server_id = get_server_id(DBRef, VHost, Msg#msg.peer_server),
2663 +    Peer_resource_id = get_resource_id(DBRef, VHost, Msg#msg.peer_resource),
2664 +
2665 +    Query = ["INSERT INTO ",Table," ",
2666 +                "(owner_id,",
2667 +                 "peer_name_id,",
2668 +                 "peer_server_id,",
2669 +                 "peer_resource_id,",
2670 +                 "direction,",
2671 +                 "type,",
2672 +                 "subject,",
2673 +                 "body,",
2674 +                 "timestamp) ",
2675 +                "VALUES ",
2676 +                "('", Owner_id, "',",
2677 +                  "'", Peer_name_id, "',",
2678 +                  "'", Peer_server_id, "',",
2679 +                  "'", Peer_resource_id, "',",
2680 +                  "'", atom_to_list(Msg#msg.direction), "',",
2681 +                  "'", Msg#msg.type, "',",
2682 +                  "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
2683 +                  "'", ejabberd_odbc:escape(Msg#msg.body), "',",
2684 +                  "'", Msg#msg.timestamp, "');"],
2685 +
2686 +    case sql_query_internal_silent(DBRef, Query) of
2687 +         {updated, _} ->
2688 +            ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
2689 +                                                    Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
2690 +            increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Date);
2691 +         {error, Reason} ->
2692 +            case regexp:match(Reason, "#42S02") of
2693 +                 % Table doesn't exist
2694 +                 {match, _, _} ->
2695 +                   case create_msg_table(DBRef, VHost, Date) of
2696 +                        error ->
2697 +                          error;
2698 +                        ok ->
2699 +                          log_message_int(DBRef, VHost, Msg)
2700 +                   end;
2701 +                 _ ->
2702 +                   ?ERROR_MSG("Failed to log message: ~p", [Reason]),
2703 +                   error
2704 +            end
2705 +    end.
2706 +
2707 +increment_user_stats(DBRef, User_name, User_id, VHost, Date) ->
2708 +    SName = stats_table(VHost),
2709 +    UQuery = ["UPDATE ",SName," ",
2710 +                  "SET count=count+1 ",
2711 +                  "WHERE owner_id=\"",User_id,"\" AND at=\"",Date,"\";"],
2712 +
2713 +    case sql_query_internal(DBRef, UQuery) of
2714 +         {updated, 0} ->
2715 +               IQuery = ["INSERT INTO ",SName," ",
2716 +                             "(owner_id, at, count) ",
2717 +                             "VALUES ",
2718 +                             "('",User_id,"', '",Date,"', '1');"],
2719 +               case sql_query_internal(DBRef, IQuery) of
2720 +                    {updated, _} ->
2721 +                         ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]),
2722 +                         ok;
2723 +                    {error, _} ->
2724 +                         error
2725 +               end;
2726 +         {updated, _} ->
2727 +               ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]),
2728 +               ok;
2729 +         {error, _} ->
2730 +               error
2731 +    end.
2732 +
2733 +get_dates_int(DBRef, VHost) ->
2734 +    case sql_query_internal(DBRef, ["SHOW TABLES"]) of
2735 +         {data, Tables} ->
2736 +            lists:foldl(fun([Table], Dates) ->
2737 +                           % TODO: check prefix()
2738 +                           case regexp:match(Table, escape_vhost(VHost)) of
2739 +                                {match, _, _} ->
2740 +                                   case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of
2741 +                                        {match, S, E} ->
2742 +                                            lists:append(Dates, [lists:sublist(Table,S,E)]);
2743 +                                        nomatch ->
2744 +                                            Dates
2745 +                                   end;
2746 +                               nomatch ->
2747 +                                   Dates
2748 +                           end
2749 +                        end, [], Tables);
2750 +         {error, _} ->
2751 +            []
2752 +     end.
2753 +
2754 +rebuild_stats_at_int(DBRef, VHost, Date) ->
2755 +    Table = messages_table(VHost, Date),
2756 +    STable = stats_table(VHost),
2757 +
2758 +    {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",
2759 +                                                            STable," WRITE;"]),
2760 +    Fun = 
2761 +      fun() ->
2762 +        DQuery = [ "DELETE FROM ",STable," ",
2763 +                      "WHERE at='",Date,"';"],
2764 +
2765 +        {updated, _} = sql_query_internal(DBRef, DQuery),
2766 +
2767 +        SQuery = ["INSERT INTO ",STable," ",
2768 +                   "(owner_id,at,count) ",
2769 +                      "SELECT owner_id,\"",Date,"\"",",count(*) ",
2770 +                         "FROM ",Table," GROUP BY owner_id;"],
2771 +
2772 +        case sql_query_internal(DBRef, SQuery) of
2773 +             {updated, 0} ->
2774 +                 {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
2775 +                 ok;
2776 +             {updated, _} -> ok;
2777 +             {error, _} -> error
2778 +        end
2779 +      end,
2780 +
2781 +
2782 +    Res = case sql_transaction_internal(DBRef, Fun) of
2783 +               {atomic, _} ->
2784 +                   ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
2785 +                   ok;
2786 +               {aborted, _} ->
2787 +                   error
2788 +          end,
2789 +    {updated, _} = sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
2790 +    Res.
2791 +
2792 +
2793 +delete_nonexistent_stats(DBRef, VHost) ->
2794 +    Dates = get_dates_int(DBRef, VHost),
2795 +    STable = stats_table(VHost),
2796 +
2797 +    Temp = lists:flatmap(fun(Date) ->
2798 +                             ["\"",Date,"\"",","]
2799 +                         end, Dates),
2800 +
2801 +    Temp1 = case Temp of
2802 +                 [] ->
2803 +                   ["\"\""];
2804 +                 _ ->
2805 +                   % replace last "," with ");"
2806 +                   lists:append([lists:sublist(Temp, length(Temp)-1), ");"])
2807 +            end,
2808 +
2809 +    Query = ["DELETE FROM ",STable," ",
2810 +                 "WHERE at NOT IN (", Temp1],
2811 +
2812 +    case sql_query_internal(DBRef, Query) of
2813 +         {updated, _} ->
2814 +            ok;
2815 +         {error, _} ->
2816 +            error
2817 +    end.
2818 +
2819 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2820 +%
2821 +% tables internals
2822 +%
2823 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2824 +create_stats_table(DBRef, VHost) ->
2825 +    SName = stats_table(VHost),
2826 +    Query = ["CREATE TABLE ",SName," (",
2827 +                "owner_id MEDIUMINT UNSIGNED, ",
2828 +                "at varchar(20), ",
2829 +                "count int(11), ",
2830 +                "INDEX(owner_id), ",
2831 +                "INDEX(at)"
2832 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
2833 +            ],
2834 +    case sql_query_internal_silent(DBRef, Query) of
2835 +         {updated, _} ->
2836 +            ?MYDEBUG("Created stats table for ~p", [VHost]),
2837 +            lists:foreach(fun(Date) ->
2838 +                            rebuild_stats_at_int(DBRef, VHost, Date)
2839 +                          end, get_dates_int(DBRef, VHost)),
2840 +            ok;
2841 +         {error, Reason} ->
2842 +            case regexp:match(Reason, "#42S01") of
2843 +                 {match, _, _} ->
2844 +                   ?MYDEBUG("Stats table for ~p already exists", [VHost]),
2845 +                   ok;
2846 +                 _ ->
2847 +                   ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
2848 +                   error
2849 +            end
2850 +    end.
2851 +
2852 +create_settings_table(DBRef, VHost) ->
2853 +    SName = settings_table(VHost),
2854 +    Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
2855 +                "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
2856 +                "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
2857 +                "dolog_list TEXT, ",
2858 +                "donotlog_list TEXT ",
2859 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
2860 +            ],
2861 +    case sql_query_internal(DBRef, Query) of
2862 +         {updated, _} ->
2863 +            ?MYDEBUG("Created settings table for ~p", [VHost]),
2864 +            ok;
2865 +         {error, _} ->
2866 +            error
2867 +    end.
2868 +
2869 +create_users_table(DBRef, VHost) ->
2870 +    SName = users_table(VHost),
2871 +    Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
2872 +                "username TEXT NOT NULL, ",
2873 +                "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
2874 +                "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
2875 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
2876 +            ],
2877 +    case sql_query_internal(DBRef, Query) of
2878 +         {updated, _} ->
2879 +            ?MYDEBUG("Created users table for ~p", [VHost]),
2880 +            ets:new(ets_users_table(VHost), [named_table, set, public]),
2881 +            %update_users_from_db(DBRef, VHost),
2882 +            ok;
2883 +         {error, _} ->
2884 +            error
2885 +    end.
2886 +
2887 +create_servers_table(DBRef, VHost) ->
2888 +    SName = servers_table(VHost),
2889 +    Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
2890 +                "server TEXT NOT NULL, ",
2891 +                "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
2892 +                "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
2893 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
2894 +            ],
2895 +    case sql_query_internal(DBRef, Query) of
2896 +         {updated, _} ->
2897 +            ?MYDEBUG("Created servers table for ~p", [VHost]),
2898 +            ets:new(ets_servers_table(VHost), [named_table, set, public]),
2899 +            update_servers_from_db(DBRef, VHost),
2900 +            ok;
2901 +         {error, _} ->
2902 +            error
2903 +    end.
2904 +
2905 +create_resources_table(DBRef, VHost) ->
2906 +    RName = resources_table(VHost),
2907 +    Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
2908 +                "resource TEXT NOT NULL, ",
2909 +                "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
2910 +                "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
2911 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
2912 +            ],
2913 +    case sql_query_internal(DBRef, Query) of
2914 +         {updated, _} ->
2915 +            ?MYDEBUG("Created resources table for ~p", [VHost]),
2916 +            ets:new(ets_resources_table(VHost), [named_table, set, public]),
2917 +            ok;
2918 +         {error, _} ->
2919 +            error
2920 +    end.
2921 +
2922 +create_msg_table(DBRef, VHost, Date) ->
2923 +    TName = messages_table(VHost, Date),
2924 +    Query = ["CREATE TABLE ",TName," (",
2925 +                "owner_id MEDIUMINT UNSIGNED, ",
2926 +                "peer_name_id MEDIUMINT UNSIGNED, ",
2927 +                "peer_server_id MEDIUMINT UNSIGNED, ",
2928 +                "peer_resource_id MEDIUMINT(8) UNSIGNED, ",
2929 +                "direction ENUM('to', 'from'), ",
2930 +                "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ",
2931 +                "subject TEXT, ",
2932 +                "body TEXT, ",
2933 +                "timestamp DOUBLE, ",
2934 +                "INDEX owner_i (owner_id), ",
2935 +                "INDEX peer_i (peer_name_id, peer_server_id), ",
2936 +                "FULLTEXT (body) "
2937 +             ") ENGINE=MyISAM CHARACTER SET utf8;"
2938 +            ],
2939 +    case sql_query_internal(DBRef, Query) of
2940 +         {updated, _MySQLRes} ->
2941 +            ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
2942 +            ok;
2943 +         {error, _} ->
2944 +            error
2945 +    end.
2946 +
2947 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2948 +%
2949 +% internal ets cache (users, servers, resources)
2950 +%
2951 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2952 +update_servers_from_db(DBRef, VHost) ->
2953 +   ?INFO_MSG("Reading servers from db for ~p", [VHost]),
2954 +   SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"],
2955 +   {data, Result} = sql_query_internal(DBRef, SQuery),
2956 +   true = ets:delete_all_objects(ets_servers_table(VHost)),
2957 +   true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]).
2958 +
2959 +%update_users_from_db(DBRef, VHost) ->
2960 +%   ?INFO_MSG("Reading users from db for ~p", [VHost]),
2961 +%   SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"],
2962 +%   {data, Result} = sql_query_internal(DBRef, SQuery),
2963 +%   true = ets:delete_all_objects(ets_users_table(VHost)),
2964 +%   true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]).
2965 +
2966 +%get_user_name(DBRef, VHost, User_id) ->
2967 +%  case ets:match(ets_users_table(VHost), {'$1', User_id}) of
2968 +%       [[User]] -> User;
2969 +%       % this can be in clustered environment
2970 +%       [] ->
2971 +%         %update_users_from_db(DBRef, VHost),
2972 +%         SQuery = ["SELECT username FROM ",users_table(VHost)," ",
2973 +%                             "WHERE user_id=\"",User_id,"\";"],
2974 +%         {data, [[Name]]} = sql_query_internal(DBRef, SQuery),
2975 +%         % cache {user, id} pair
2976 +%         ets:insert(ets_users_table(VHost), {Name, User_id}),
2977 +%         Name
2978 +%  end.
2979 +
2980 +%get_server_name(DBRef, VHost, Server_id) ->
2981 +%  case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of
2982 +%       [[Server]] -> Server;
2983 +       % this can be in clustered environment
2984 +%       [] ->
2985 +%         update_servers_from_db(DBRef, VHost),
2986 +%         [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}),
2987 +%         Server1
2988 +%  end.
2989 +
2990 +get_user_id_from_db(DBRef, VHost, User) ->
2991 +  SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
2992 +               "WHERE username=\"",User,"\";"],
2993 +  case sql_query_internal(DBRef, SQuery) of
2994 +       % no such user in db
2995 +       {data, []} ->
2996 +          {ok, []};
2997 +       {data, [[DBId]]} ->
2998 +          % cache {user, id} pair
2999 +          ets:insert(ets_users_table(VHost), {User, DBId}),
3000 +          {ok, DBId}
3001 +  end.
3002 +get_user_id(DBRef, VHost, User) ->
3003 +  % Look at ets
3004 +  case ets:match(ets_users_table(VHost), {User, '$1'}) of
3005 +       [] ->
3006 +         % Look at db
3007 +         case get_user_id_from_db(DBRef, VHost, User) of
3008 +              % no such user in db
3009 +              {ok, []} ->
3010 +                 IQuery = ["INSERT INTO ",users_table(VHost)," ",
3011 +                              "SET username=\"",User,"\";"],
3012 +                 case sql_query_internal_silent(DBRef, IQuery) of
3013 +                      {updated, _} ->
3014 +                          {ok, NewId} = get_user_id_from_db(DBRef, VHost, User),
3015 +                          NewId;
3016 +                      {error, Reason} ->
3017 +                          % this can be in clustered environment
3018 +                          {match, _, _} = regexp:match(Reason, "#23000"),
3019 +                          ?ERROR_MSG("Duplicate key name for ~p", [User]),
3020 +                          {ok, ClID} = get_user_id_from_db(DBRef, VHost, User),
3021 +                          ClID
3022 +                 end;
3023 +              {ok, DBId} ->
3024 +                 DBId
3025 +         end;
3026 +       [[EtsId]] -> EtsId
3027 +  end.
3028 +
3029 +get_server_id(DBRef, VHost, Server) ->
3030 +  case ets:match(ets_servers_table(VHost), {Server, '$1'}) of
3031 +       [] ->
3032 +        IQuery = ["INSERT INTO ",servers_table(VHost)," ",
3033 +                     "SET server=\"",Server,"\";"],
3034 +        case sql_query_internal_silent(DBRef, IQuery) of
3035 +             {updated, _} ->
3036 +                SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ",
3037 +                             "WHERE server=\"",Server,"\";"],
3038 +                {data, [[Id]]} = sql_query_internal(DBRef, SQuery),
3039 +                ets:insert(ets_servers_table(VHost), {Server, Id}),
3040 +                Id;
3041 +             {error, Reason} ->
3042 +                % this can be in clustered environment
3043 +                {match, _, _} = regexp:match(Reason, "#23000"),
3044 +                ?ERROR_MSG("Duplicate key name for ~p", [Server]),
3045 +                update_servers_from_db(DBRef, VHost),
3046 +                [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}),
3047 +                Id1
3048 +        end;
3049 +       [[Id]] -> Id
3050 +  end.
3051 +
3052 +get_resource_id_from_db(DBRef, VHost, Resource) ->
3053 +  SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ",
3054 +               "WHERE resource=\"",ejabberd_odbc:escape(Resource),"\";"],
3055 +  case sql_query_internal(DBRef, SQuery) of
3056 +       % no such resource in db
3057 +       {data, []} ->
3058 +          {ok, []};
3059 +       {data, [[DBId]]} ->
3060 +          % cache {resource, id} pair
3061 +          ets:insert(ets_resources_table(VHost), {Resource, DBId}),
3062 +          {ok, DBId}
3063 +  end.
3064 +get_resource_id(DBRef, VHost, Resource) ->
3065 +  % Look at ets
3066 +  case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of
3067 +       [] ->
3068 +         % Look at db
3069 +         case get_resource_id_from_db(DBRef, VHost, Resource) of
3070 +              % no such resource in db
3071 +              {ok, []} ->
3072 +                 IQuery = ["INSERT INTO ",resources_table(VHost)," ",
3073 +                              "SET resource=\"",ejabberd_odbc:escape(Resource),"\";"],
3074 +                 case sql_query_internal_silent(DBRef, IQuery) of
3075 +                      {updated, _} ->
3076 +                          {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource),
3077 +                          NewId;
3078 +                      {error, Reason} ->
3079 +                          % this can be in clustered environment
3080 +                          {match, _, _} = regexp:match(Reason, "#23000"),
3081 +                          ?ERROR_MSG("Duplicate key name for ~p", [Resource]),
3082 +                          {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource),
3083 +                          ClID
3084 +                 end;
3085 +              {ok, DBId} ->
3086 +                 DBId
3087 +         end;
3088 +       [[EtsId]] -> EtsId
3089 +  end.
3090 +
3091 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3092 +%
3093 +% SQL internals 
3094 +%
3095 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3096 +% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
3097 +sql_transaction_internal(DBRef, Fun) ->
3098 +    case sql_query_internal(DBRef, ["START TRANSACTION;"]) of
3099 +         {updated, _} ->
3100 +            case catch Fun() of
3101 +                 error = Err ->
3102 +                   rollback_internal(DBRef, Err);
3103 +                 {error, _} = Err ->
3104 +                   rollback_internal(DBRef, Err);
3105 +                 {'EXIT', _} = Err ->
3106 +                   rollback_internal(DBRef, Err);
3107 +                 Res ->
3108 +                   case sql_query_internal(DBRef, ["COMMIT;"]) of
3109 +                        {error, _} -> rollback_internal(DBRef, {commit_error});
3110 +                        {updated, _} ->
3111 +                           case Res of
3112 +                                {atomic, _} -> Res;
3113 +                                _ -> {atomic, Res}
3114 +                           end
3115 +                   end
3116 +            end;
3117 +         {error, _} ->
3118 +            {aborted, {begin_error}}
3119 +    end.
3120 +
3121 +% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
3122 +rollback_internal(DBRef, Reason) ->
3123 +    Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
3124 +    {aborted, {Reason, {rollback_result, Res}}}.
3125 +
3126 +sql_query_internal(DBRef, Query) ->
3127 +    case sql_query_internal_silent(DBRef, Query) of
3128 +         {error, Reason} ->
3129 +            ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
3130 +            {error, Reason};
3131 +         Rez -> Rez
3132 +    end.
3133 +
3134 +sql_query_internal_silent(DBRef, Query) ->
3135 +    ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
3136 +    get_result(mysql_conn:fetch(DBRef, Query, self(), ?TIMEOUT)).
3137 +
3138 +get_result({updated, MySQLRes}) ->
3139 +    {updated, mysql:get_result_affected_rows(MySQLRes)};
3140 +get_result({data, MySQLRes}) ->
3141 +    {data, mysql:get_result_rows(MySQLRes)};
3142 +get_result({error, "query timed out"}) ->
3143 +    {error, "query timed out"};
3144 +get_result({error, MySQLRes}) ->
3145 +    Reason = mysql:get_result_reason(MySQLRes),
3146 +    {error, Reason}.
3147 --- src/mod_logdb_mysql5.erl.orig       Tue Dec 11 14:23:19 2007
3148 +++ src/mod_logdb_mysql5.erl    Tue Dec 11 11:58:33 2007
3149 @@ -0,0 +1,854 @@
3150 +%%%----------------------------------------------------------------------
3151 +%%% File    : mod_logdb_mysql5.erl
3152 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
3153 +%%% Purpose : MySQL 5 backend for mod_logdb
3154 +%%% Version : trunk
3155 +%%% Id      : $Id$
3156 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
3157 +%%%----------------------------------------------------------------------
3158 +
3159 +-module(mod_logdb_mysql5).
3160 +-author('o.palij@gmail.com').
3161 +-vsn('$Revision$').
3162 +
3163 +-include("mod_logdb.hrl").
3164 +-include("ejabberd.hrl").
3165 +-include("jlib.hrl").
3166 +
3167 +-behaviour(gen_logdb).
3168 +-behaviour(gen_server).
3169 +
3170 +% gen_server
3171 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
3172 +% gen_mod
3173 +-export([start/2, stop/1]).
3174 +% gen_logdb
3175 +-export([log_message/2,
3176 +         rebuild_stats/1,
3177 +         rebuild_stats_at/2,
3178 +         delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
3179 +         get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
3180 +         get_dates/1,
3181 +         get_users_settings/1, get_user_settings/2, set_user_settings/3]).
3182 +
3183 +% gen_server call timeout
3184 +-define(CALL_TIMEOUT, 60000).
3185 +-define(TIMEOUT, 60000).
3186 +-define(INDEX_SIZE, integer_to_list(170)).
3187 +-define(PROCNAME, mod_logdb_mysql5).
3188 +
3189 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
3190 +                    list_to_string/1, string_to_list/1,
3191 +                    convert_timestamp_brief/1]).
3192 +
3193 +-record(state, {dbref, vhost}).
3194 +
3195 +% replace "." with "_"
3196 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
3197 +                                    (A) -> A
3198 +                                 end, VHost).
3199 +prefix() ->
3200 +   "`logdb_".
3201 +
3202 +suffix(VHost) ->
3203 +   "_" ++ escape_vhost(VHost) ++ "`".
3204 +
3205 +messages_table(VHost, Date) ->
3206 +   prefix() ++ "messages_" ++ Date ++ suffix(VHost).
3207 +
3208 +% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
3209 +view_table(VHost, Date) ->
3210 +   Table = messages_table(VHost, Date),
3211 +   TablewoQ = lists:sublist(Table, 2, length(Table) - 2),
3212 +   lists:append(["`v_", TablewoQ, "`"]).
3213 +
3214 +stats_table(VHost) ->
3215 +   prefix() ++ "stats" ++ suffix(VHost).
3216 +
3217 +settings_table(VHost) ->
3218 +   prefix() ++ "settings" ++ suffix(VHost).
3219 +
3220 +users_table(VHost) ->
3221 +   prefix() ++ "users" ++ suffix(VHost).
3222 +servers_table(VHost) ->
3223 +   prefix() ++ "servers" ++ suffix(VHost).
3224 +resources_table(VHost) ->
3225 +   prefix() ++ "resources" ++ suffix(VHost).
3226 +
3227 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3228 +%
3229 +% gen_mod callbacks
3230 +%
3231 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3232 +start(VHost, Opts) ->
3233 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3234 +   gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
3235 +
3236 +stop(VHost) ->
3237 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3238 +   gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
3239 +
3240 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3241 +%
3242 +% gen_server callbacks
3243 +%
3244 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3245 +init([VHost, Opts]) ->
3246 +   crypto:start(),
3247 +
3248 +   Server = gen_mod:get_opt(server, Opts, "localhost"),
3249 +   Port = gen_mod:get_opt(port, Opts, 3128),
3250 +   DB = gen_mod:get_opt(db, Opts, "logdb"),
3251 +   User = gen_mod:get_opt(user, Opts, "root"),
3252 +   Password = gen_mod:get_opt(password, Opts, ""),
3253 +
3254 +   LogFun = fun(debug, Format, Argument) ->
3255 +                 ?MYDEBUG(Format, Argument);
3256 +               (error, Format, Argument) ->
3257 +                 ?ERROR_MSG(Format, Argument);
3258 +               (Level, Format, Argument) ->
3259 +                 ?MYDEBUG("MySQL (~p)~n", [Level]),
3260 +                 ?MYDEBUG(Format, Argument)
3261 +            end,
3262 +   case mysql_conn:start(Server, Port, User, Password, DB, [65536, 131072], LogFun) of
3263 +       {ok, DBRef} ->
3264 +           ok = create_internals(DBRef, VHost),
3265 +           ok = create_stats_table(DBRef, VHost),
3266 +           ok = create_settings_table(DBRef, VHost),
3267 +           ok = create_users_table(DBRef, VHost),
3268 +           ok = create_servers_table(DBRef, VHost),
3269 +           ok = create_resources_table(DBRef, VHost),
3270 +           erlang:monitor(process, DBRef),
3271 +           {ok, #state{dbref=DBRef, vhost=VHost}};
3272 +       {error, Reason} ->
3273 +           ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
3274 +           {stop, db_connection_failed}
3275 +   end.
3276 +
3277 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3278 +    Date = convert_timestamp_brief(Msg#msg.timestamp),
3279 +    TableName = messages_table(VHost, Date),
3280 +
3281 +    Query = [ "CALL logmessage "
3282 +                 "('", TableName, "',",
3283 +                  "'", Date, "',",
3284 +                  "'", Msg#msg.owner_name, "',",
3285 +                  "'", Msg#msg.peer_name, "',",
3286 +                  "'", Msg#msg.peer_server, "',",
3287 +                  "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',",
3288 +                  "'", atom_to_list(Msg#msg.direction), "',",
3289 +                  "'", Msg#msg.type, "',",
3290 +                  "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
3291 +                  "'", ejabberd_odbc:escape(Msg#msg.body), "',",
3292 +                  "'", Msg#msg.timestamp, "');"],
3293 +
3294 +    Reply =
3295 +       case sql_query_internal(DBRef, Query) of
3296 +            {updated, _} ->
3297 +               ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
3298 +                                                       Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
3299 +               ok;
3300 +            {error, _Reason} ->
3301 +               error
3302 +       end,
3303 +    {reply, Reply, State};
3304 +handle_call({rebuild_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3305 +    ok = delete_nonexistent_stats(DBRef, VHost),
3306 +    Reply = 
3307 +      lists:foreach(fun(Date) ->
3308 +                        catch rebuild_stats_at_int(DBRef, VHost, Date)
3309 +                    end, get_dates_int(DBRef, VHost)),
3310 +    {reply, Reply, State};
3311 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3312 +    Reply = rebuild_stats_at_int(DBRef, VHost, Date),
3313 +    {reply, Reply, State};
3314 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
3315 +    {reply, error, State};
3316 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3317 +    Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
3318 +                             ["\"",Timestamp,"\"",","]
3319 +                         end, Msgs),
3320 +
3321 +    Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3322 +
3323 +    Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
3324 +                             "WHERE timestamp IN (", Temp1],
3325 +
3326 +    Reply =
3327 +      case sql_query_internal(DBRef, Query) of
3328 +           {updated, Aff} ->
3329 +              ?MYDEBUG("Aff=~p", [Aff]),
3330 +              rebuild_stats_at_int(DBRef, VHost, Date);
3331 +           {error, _} ->
3332 +              error
3333 +      end,
3334 +    {reply, Reply, State};
3335 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3336 +    DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
3337 +                 "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3338 +    Reply =
3339 +      case sql_query_internal(DBRef, DQuery) of
3340 +           {updated, _} ->
3341 +              rebuild_stats_at_int(DBRef, VHost, Date);
3342 +           {error, _} ->
3343 +              error
3344 +      end,
3345 +    {reply, Reply, State};
3346 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3347 +    Fun = fun() ->
3348 +              {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]),
3349 +              TQuery = ["DELETE FROM ",stats_table(VHost)," "
3350 +                           "WHERE at=\"",Date,"\";"],
3351 +              {updated, _} = sql_query_internal(DBRef, TQuery),
3352 +              VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"],
3353 +              {updated, _} = sql_query_internal(DBRef, VQuery)
3354 +          end,
3355 +    Reply =
3356 +      case sql_transaction_internal(DBRef, Fun) of
3357 +           {atomic, _} ->
3358 +              ok;
3359 +           {aborted, _} ->
3360 +              error
3361 +      end,
3362 +    {reply, Reply, State};
3363 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3364 +    SName = stats_table(VHost),
3365 +    Query = ["SELECT at, sum(count) ",
3366 +                "FROM ",SName," ",
3367 +                "GROUP BY at ",
3368 +                "ORDER BY DATE(at) DESC;"
3369 +            ],
3370 +    Reply =
3371 +      case sql_query_internal(DBRef, Query) of
3372 +           {data, Result} ->
3373 +              {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
3374 +           {error, Reason} ->
3375 +              % TODO: Duplicate error message ?
3376 +              {error, Reason}
3377 +      end,
3378 +    {reply, Reply, State};
3379 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3380 +    SName = stats_table(VHost),
3381 +    Query = ["SELECT username, count ",
3382 +                "FROM ",SName," ",
3383 +                "JOIN ",users_table(VHost)," ON owner_id=user_id "
3384 +                "WHERE at=\"",Date,"\" ",
3385 +                "ORDER BY count DESC;"
3386 +            ],
3387 +    Reply =
3388 +      case sql_query_internal(DBRef, Query) of
3389 +           {data, Result} ->
3390 +              {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
3391 +           {error, Reason} ->
3392 +              {error, Reason}
3393 +      end,
3394 +    {reply, Reply, State};
3395 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3396 +    SName = stats_table(VHost),
3397 +    UName = users_table(VHost),
3398 +    Query = ["SELECT stats.at, stats.count ",
3399 +                "FROM ",UName," AS users ",
3400 +                   "JOIN ",SName," AS stats ON owner_id=user_id "
3401 +                "WHERE users.username=\"",User,"\" ",
3402 +                "ORDER BY DATE(at) DESC;"
3403 +            ],
3404 +    Reply =
3405 +      case sql_query_internal(DBRef, Query) of
3406 +           {data, Result} ->
3407 +              {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
3408 +           {error, Result} ->
3409 +              {error, Result}
3410 +      end,
3411 +    {reply, Reply, State};
3412 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3413 +    Query = ["SELECT peer_name,",
3414 +                    "peer_server,",
3415 +                    "peer_resource,",
3416 +                    "direction,"
3417 +                    "type,"
3418 +                    "subject,"
3419 +                    "body,"
3420 +                    "timestamp "
3421 +               "FROM ",view_table(VHost, Date)," "
3422 +               "WHERE owner_name=\"",User,"\";"],
3423 +    Reply =
3424 +      case sql_query_internal(DBRef, Query) of
3425 +           {data, Result} ->
3426 +              Fun = fun([Peer_name, Peer_server, Peer_resource,
3427 +                         Direction,
3428 +                         Type,
3429 +                         Subject, Body,
3430 +                         Timestamp]) ->
3431 +                          #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
3432 +                               direction=list_to_atom(Direction),
3433 +                               type=Type,
3434 +                               subject=Subject, body=Body,
3435 +                               timestamp=Timestamp}
3436 +                    end,
3437 +              {ok, lists:map(Fun, Result)};
3438 +           {error, Reason} ->
3439 +              {error, Reason}
3440 +      end,
3441 +    {reply, Reply, State};
3442 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3443 +    SName = stats_table(VHost),
3444 +    Query = ["SELECT at ",
3445 +                "FROM ",SName," ",
3446 +                "GROUP BY at ",
3447 +                "ORDER BY DATE(at) DESC;"
3448 +            ],
3449 +    Reply =
3450 +       case sql_query_internal(DBRef, Query) of
3451 +            {data, Result} ->
3452 +               [ Date || [Date] <- Result ];
3453 +            {error, Reason} ->
3454 +               {error, Reason}
3455 +       end,
3456 +    {reply, Reply, State};
3457 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3458 +    Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
3459 +                "FROM ",settings_table(VHost)," ",
3460 +             "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
3461 +    Reply = 
3462 +      case sql_query_internal(DBRef, Query) of
3463 +           {data, Result} ->
3464 +              {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
3465 +                                 #user_settings{owner_name=Owner,
3466 +                                                dolog_default=list_to_bool(DoLogDef),
3467 +                                                dolog_list=string_to_list(DoLogL),
3468 +                                                donotlog_list=string_to_list(DoNotLogL)
3469 +                                               }
3470 +                             end, Result)};
3471 +           {error, _} ->
3472 +              error
3473 +      end,
3474 +    {reply, Reply, State};
3475 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3476 +    Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
3477 +                 "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3478 +    Reply =
3479 +      case sql_query_internal(DBRef, Query) of
3480 +           {data, []} ->
3481 +              {ok, []};
3482 +           {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
3483 +              {ok, #user_settings{owner_name=Owner,
3484 +                                  dolog_default=list_to_bool(DoLogDef),
3485 +                                  dolog_list=string_to_list(DoLogL),
3486 +                                  donotlog_list=string_to_list(DoNotLogL)}};
3487 +           {error, _} ->
3488 +              error
3489 +      end,
3490 +    {reply, Reply, State};
3491 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
3492 +                                                     dolog_list=DoLogL,
3493 +                                                     donotlog_list=DoNotLogL}},
3494 +            _From, #state{dbref=DBRef, vhost=VHost} = State) ->
3495 +    User_id = get_user_id(DBRef, VHost, User),
3496 +    Query = ["UPDATE ",settings_table(VHost)," ",
3497 +                "SET dolog_default=",bool_to_list(DoLogDef),", ",
3498 +                    "dolog_list='",list_to_string(DoLogL),"', ",
3499 +                    "donotlog_list='",list_to_string(DoNotLogL),"' ",
3500 +                "WHERE owner_id=",User_id,";"],
3501 +
3502 +    Reply =
3503 +      case sql_query_internal(DBRef, Query) of
3504 +           {updated, 0} ->
3505 +              IQuery = ["INSERT INTO ",settings_table(VHost)," ",
3506 +                            "(owner_id, dolog_default, dolog_list, donotlog_list) ",
3507 +                            "VALUES ",
3508 +                            "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
3509 +              case sql_query_internal_silent(DBRef, IQuery) of
3510 +                   {updated, _} ->
3511 +                       ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
3512 +                       ok;
3513 +                   {error, Reason} ->
3514 +                       case regexp:match(Reason, "#23000") of
3515 +                            % Already exists
3516 +                            {match, _, _} ->
3517 +                                ok;
3518 +                             _ ->
3519 +                                ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
3520 +                                error
3521 +                       end
3522 +              end;
3523 +           {updated, 1} ->
3524 +              ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
3525 +              ok;
3526 +           {error, _} ->
3527 +              error
3528 +      end,
3529 +    {reply, Reply, State};
3530 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
3531 +   ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]),
3532 +   {stop, normal, ok, State};
3533 +handle_call(Msg, _From, State) ->
3534 +    ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
3535 +    {noreply, State}.
3536 +
3537 +handle_cast(Msg, State) ->
3538 +    ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
3539 +    {noreply, State}.
3540 +
3541 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
3542 +    {stop, connection_dropped, State};
3543 +handle_info(Info, State) ->
3544 +    ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
3545 +    {noreply, State}.
3546 +
3547 +terminate(_Reason, _State) ->
3548 +    ok.
3549 +
3550 +code_change(_OldVsn, State, _Extra) ->
3551 +    {ok, State}.
3552 +
3553 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3554 +%
3555 +% gen_logdb callbacks
3556 +%
3557 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3558 +log_message(VHost, Msg) ->
3559 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3560 +   gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
3561 +rebuild_stats(VHost) ->
3562 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3563 +   gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
3564 +rebuild_stats_at(VHost, Date) ->
3565 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3566 +   gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
3567 +delete_messages_by_user_at(VHost, Msgs, Date) ->
3568 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3569 +   gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
3570 +delete_all_messages_by_user_at(User, VHost, Date) ->
3571 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3572 +   gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
3573 +delete_messages_at(VHost, Date) ->
3574 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3575 +   gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
3576 +get_vhost_stats(VHost) ->
3577 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3578 +   gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
3579 +get_vhost_stats_at(VHost, Date) ->
3580 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3581 +   gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
3582 +get_user_stats(User, VHost) ->
3583 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3584 +   gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
3585 +get_user_messages_at(User, VHost, Date) ->
3586 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3587 +   gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
3588 +get_dates(VHost) ->
3589 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3590 +   gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
3591 +get_users_settings(VHost) ->
3592 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3593 +   gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
3594 +get_user_settings(User, VHost) ->
3595 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3596 +   gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
3597 +set_user_settings(User, VHost, Set) ->
3598 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3599 +   gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
3600 +
3601 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3602 +%
3603 +% internals
3604 +%
3605 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3606 +get_dates_int(DBRef, VHost) ->
3607 +    case sql_query_internal(DBRef, ["SHOW TABLES"]) of
3608 +         {data, Tables} ->
3609 +            lists:foldl(fun([Table], Dates) ->
3610 +                           % TODO: check prefix()
3611 +                           case regexp:match(Table, escape_vhost(VHost)) of
3612 +                                {match, _, _} ->
3613 +                                   case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of
3614 +                                        {match, S, E} ->
3615 +                                            lists:append(Dates, [lists:sublist(Table,S,E)]);
3616 +                                        nomatch ->
3617 +                                            Dates
3618 +                                   end;
3619 +                                nomatch ->
3620 +                                   Dates
3621 +                           end
3622 +                        end, [], Tables);
3623 +         {error, _} ->
3624 +            []
3625 +    end.
3626 +
3627 +rebuild_stats_at_int(DBRef, VHost, Date) ->
3628 +    Table = messages_table(VHost, Date),
3629 +    STable = stats_table(VHost),
3630 +
3631 +    {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",
3632 +                                                            STable," WRITE;"]),
3633 +    Fun = 
3634 +      fun() ->
3635 +        DQuery = [ "DELETE FROM ",STable," ",
3636 +                      "WHERE at='",Date,"';"],
3637 +
3638 +        {updated, _} = sql_query_internal(DBRef, DQuery),
3639 +
3640 +        SQuery = ["INSERT INTO ",STable," ",
3641 +                   "(owner_id,at,count) ",
3642 +                      "SELECT owner_id,\"",Date,"\"",",count(*) ",
3643 +                         "FROM ",Table," GROUP BY owner_id;"],
3644 +
3645 +        case sql_query_internal(DBRef, SQuery) of
3646 +             {updated, 0} ->
3647 +                 {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
3648 +                 ok;
3649 +             {updated, _} -> ok;
3650 +             {error, _} -> error
3651 +        end
3652 +      end,
3653 +
3654 +    Res = case sql_transaction_internal(DBRef, Fun) of
3655 +               {atomic, _} ->
3656 +                   ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
3657 +                   ok;
3658 +               {aborted, _} ->
3659 +                   error
3660 +          end,
3661 +    {updated, _} = sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
3662 +    Res.
3663 +
3664 +
3665 +delete_nonexistent_stats(DBRef, VHost) ->
3666 +    Dates = get_dates_int(DBRef, VHost),
3667 +    STable = stats_table(VHost),
3668 +
3669 +    Temp = lists:flatmap(fun(Date) ->
3670 +                             ["\"",Date,"\"",","]
3671 +                         end, Dates),
3672 +
3673 +    Temp1 = case Temp of
3674 +                 [] ->
3675 +                   ["\"\""];
3676 +                 _ ->
3677 +                   % replace last "," with ");"
3678 +                   lists:append([lists:sublist(Temp, length(Temp)-1), ");"])
3679 +            end,
3680 +
3681 +    Query = ["DELETE FROM ",STable," ",
3682 +                 "WHERE at NOT IN (", Temp1],
3683 +
3684 +    case sql_query_internal(DBRef, Query) of
3685 +         {updated, _} ->
3686 +            ok;
3687 +         {error, _} ->
3688 +            error
3689 +    end.
3690 +
3691 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3692 +%
3693 +% tables internals
3694 +%
3695 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3696 +create_stats_table(DBRef, VHost) ->
3697 +    SName = stats_table(VHost),
3698 +    Query = ["CREATE TABLE ",SName," (",
3699 +                "owner_id MEDIUMINT UNSIGNED, ",
3700 +                "at VARCHAR(11), ",
3701 +                "count INT(11), ",
3702 +                "INDEX(owner_id), ",
3703 +                "INDEX(at)"
3704 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
3705 +            ],
3706 +    case sql_query_internal_silent(DBRef, Query) of
3707 +         {updated, _} ->
3708 +            ?MYDEBUG("Created stats table for ~p", [VHost]),
3709 +            lists:foreach(fun(Date) ->
3710 +                            rebuild_stats_at_int(DBRef, VHost, Date)
3711 +                          end, get_dates_int(DBRef, VHost)),
3712 +            ok;
3713 +         {error, Reason} ->
3714 +            case regexp:match(Reason, "#42S01") of
3715 +                 {match, _, _} ->
3716 +                   ?MYDEBUG("Stats table for ~p already exists", [VHost]),
3717 +                   ok;
3718 +                 _ ->
3719 +                   ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
3720 +                   error
3721 +            end
3722 +    end.
3723 +
3724 +create_settings_table(DBRef, VHost) ->
3725 +    SName = settings_table(VHost),
3726 +    Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3727 +                "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
3728 +                "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
3729 +                "dolog_list TEXT, ",
3730 +                "donotlog_list TEXT ",
3731 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
3732 +            ],
3733 +    case sql_query_internal(DBRef, Query) of
3734 +         {updated, _} ->
3735 +            ?MYDEBUG("Created settings table for ~p", [VHost]),
3736 +            ok;
3737 +         {error, _} ->
3738 +            error
3739 +    end.
3740 +
3741 +create_users_table(DBRef, VHost) ->
3742 +    SName = users_table(VHost),
3743 +    Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3744 +                "username TEXT NOT NULL, ",
3745 +                "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3746 +                "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
3747 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
3748 +            ],
3749 +    case sql_query_internal(DBRef, Query) of
3750 +         {updated, _} ->
3751 +            ?MYDEBUG("Created users table for ~p", [VHost]),
3752 +            ok;
3753 +         {error, _} ->
3754 +            error
3755 +    end.
3756 +
3757 +create_servers_table(DBRef, VHost) ->
3758 +    SName = servers_table(VHost),
3759 +    Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3760 +                "server TEXT NOT NULL, ",
3761 +                "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3762 +                "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
3763 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
3764 +            ],
3765 +    case sql_query_internal(DBRef, Query) of
3766 +         {updated, _} ->
3767 +            ?MYDEBUG("Created servers table for ~p", [VHost]),
3768 +            ok;
3769 +         {error, _} ->
3770 +            error
3771 +    end.
3772 +
3773 +create_resources_table(DBRef, VHost) ->
3774 +    RName = resources_table(VHost),
3775 +    Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
3776 +                "resource TEXT NOT NULL, ",
3777 +                "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3778 +                "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
3779 +             ") ENGINE=InnoDB CHARACTER SET utf8;"
3780 +            ],
3781 +    case sql_query_internal(DBRef, Query) of
3782 +         {updated, _} ->
3783 +            ?MYDEBUG("Created resources table for ~p", [VHost]),
3784 +            ok;
3785 +         {error, _} ->
3786 +            error
3787 +    end.
3788 +
3789 +create_internals(DBRef, VHost) ->
3790 +    sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS `logmessage`;"]),
3791 +    case sql_query_internal(DBRef, [get_logmessage(VHost)]) of
3792 +         {updated, _} ->
3793 +            ?MYDEBUG("Created logmessage for ~p", [VHost]),
3794 +            ok;
3795 +         {error, _} ->
3796 +            error
3797 +    end.
3798 +
3799 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3800 +%
3801 +% SQL internals 
3802 +%
3803 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3804 +% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
3805 +sql_transaction_internal(DBRef, Fun) ->
3806 +    case sql_query_internal(DBRef, ["START TRANSACTION;"]) of
3807 +         {updated, _} ->
3808 +            case catch Fun() of
3809 +                 error = Err ->
3810 +                   rollback_internal(DBRef, Err);
3811 +                 {error, _} = Err ->
3812 +                   rollback_internal(DBRef, Err);
3813 +                 {'EXIT', _} = Err ->
3814 +                   rollback_internal(DBRef, Err);
3815 +                 Res ->
3816 +                   case sql_query_internal(DBRef, ["COMMIT;"]) of
3817 +                        {error, _} -> rollback_internal(DBRef, {commit_error});
3818 +                        {updated, _} ->
3819 +                           case Res of
3820 +                                {atomic, _} -> Res;
3821 +                                _ -> {atomic, Res}
3822 +                           end
3823 +                   end
3824 +            end;
3825 +         {error, _} ->
3826 +            {aborted, {begin_error}}
3827 +    end.
3828 +
3829 +% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
3830 +rollback_internal(DBRef, Reason) ->
3831 +    Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
3832 +    {aborted, {Reason, {rollback_result, Res}}}.
3833 +
3834 +sql_query_internal(DBRef, Query) ->
3835 +    case sql_query_internal_silent(DBRef, Query) of
3836 +         {error, Reason} ->
3837 +            ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
3838 +            {error, Reason};
3839 +         Rez -> Rez
3840 +    end.
3841 +
3842 +sql_query_internal_silent(DBRef, Query) ->
3843 +    ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
3844 +    get_result(mysql_conn:fetch(DBRef, Query, self(), ?TIMEOUT)).
3845 +
3846 +get_result({updated, MySQLRes}) ->
3847 +    {updated, mysql:get_result_affected_rows(MySQLRes)};
3848 +get_result({data, MySQLRes}) ->
3849 +    {data, mysql:get_result_rows(MySQLRes)};
3850 +get_result({error, "query timed out"}) ->
3851 +    {error, "query timed out"};
3852 +get_result({error, MySQLRes}) ->
3853 +    Reason = mysql:get_result_reason(MySQLRes),
3854 +    {error, Reason}.
3855 +
3856 +get_user_id(DBRef, VHost, User) ->
3857 +  SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
3858 +               "WHERE username=\"",User,"\";"],
3859 +  case sql_query_internal(DBRef, SQuery) of
3860 +       {data, []} ->
3861 +          IQuery = ["INSERT INTO ",users_table(VHost)," ",
3862 +                       "SET username=\"",User,"\";"],
3863 +          case sql_query_internal_silent(DBRef, IQuery) of
3864 +               {updated, _} ->
3865 +                   {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery),
3866 +                   DBIdNew;
3867 +               {error, Reason} ->
3868 +                   % this can be in clustered environment
3869 +                   {match, _, _} = regexp:match(Reason, "#23000"),
3870 +                   ?ERROR_MSG("Duplicate key name for ~p", [User]),
3871 +                   {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
3872 +                   ClID
3873 +          end;
3874 +       {data, [[DBId]]} ->
3875 +          DBId
3876 +  end.
3877 +
3878 +get_logmessage(VHost) ->
3879 +    UName = users_table(VHost),
3880 +    SName = servers_table(VHost),
3881 +    RName = resources_table(VHost),
3882 +    StName = stats_table(VHost),
3883 +    io_lib:format("
3884 +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)
3885 +BEGIN
3886 +   DECLARE ownerID MEDIUMINT UNSIGNED; 
3887 +   DECLARE peer_nameID MEDIUMINT UNSIGNED;
3888 +   DECLARE peer_serverID MEDIUMINT UNSIGNED;
3889 +   DECLARE peer_resourceID MEDIUMINT UNSIGNED;
3890 +   DECLARE Vmtype VARCHAR(10);
3891 +   DECLARE Vmtimestamp DOUBLE;
3892 +   DECLARE Vmdirection VARCHAR(4);
3893 +   DECLARE Vmbody TEXT;
3894 +   DECLARE Vmsubject TEXT;
3895 +   DECLARE iq TEXT;
3896 +   DECLARE cq TEXT;
3897 +   DECLARE viewname TEXT;
3898 +   DECLARE notable INT;
3899 +   DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1;
3900 +
3901 +   SET @notable = 0;
3902 +   SET @ownerID = NULL;
3903 +   SET @peer_nameID = NULL;
3904 +   SET @peer_serverID = NULL;
3905 +   SET @peer_resourceID = NULL;
3906 +
3907 +   SET @Vmtype = mtype;
3908 +   SET @Vmtimestamp = mtimestamp;
3909 +   SET @Vmdirection = mdirection;
3910 +   SET @Vmbody = mbody;
3911 +   SET @Vmsubject = msubject;
3912 +
3913 +   SELECT user_id INTO @ownerID FROM ~s WHERE username=owner;
3914 +   IF @ownerID IS NULL THEN
3915 +      INSERT INTO ~s SET username=owner;
3916 +      SET @ownerID = LAST_INSERT_ID(); 
3917 +   END IF;
3918 +
3919 +   SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name;
3920 +   IF @peer_nameID IS NULL THEN
3921 +      INSERT INTO ~s SET username=peer_name;
3922 +      SET @peer_nameID = LAST_INSERT_ID();
3923 +   END IF;
3924 +
3925 +   SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server;
3926 +   IF @peer_serverID IS NULL THEN
3927 +      INSERT INTO ~s SET server=peer_server;
3928 +      SET @peer_serverID = LAST_INSERT_ID();
3929 +   END IF;
3930 +
3931 +   SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource;
3932 +   IF @peer_resourceID IS NULL THEN
3933 +      INSERT INTO ~s SET resource=peer_resource;
3934 +      SET @peer_resourceID = LAST_INSERT_ID();
3935 +   END IF;
3936 +
3937 +   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);\");
3938 +   PREPARE insertmsg FROM @iq;
3939 +
3940 +   IF @notable = 1 THEN
3941 +      SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" (
3942 +                          owner_id MEDIUMINT UNSIGNED,
3943 +                          peer_name_id MEDIUMINT UNSIGNED,
3944 +                          peer_server_id MEDIUMINT UNSIGNED,
3945 +                          peer_resource_id MEDIUMINT(8) UNSIGNED,
3946 +                          direction ENUM('to', 'from'),
3947 +                          type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
3948 +                          subject TEXT,
3949 +                          body TEXT,
3950 +                          timestamp DOUBLE,
3951 +                          ext INTEGER DEFAULT NULL,
3952 +                          INDEX owner_i (owner_id),
3953 +                          INDEX peer_i (peer_name_id, peer_server_id),
3954 +                          INDEX ext_i (ext),
3955 +                          FULLTEXT (body)
3956 +                       ) ENGINE=MyISAM CHARACTER SET utf8;\");
3957 +      PREPARE createtable FROM @cq;
3958 +      EXECUTE createtable;
3959 +      DEALLOCATE PREPARE createtable;
3960 +
3961 +      SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\");
3962 +      SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS
3963 +                         SELECT owner.username AS owner_name,
3964 +                                peer.username AS peer_name,
3965 +                                servers.server AS peer_server,
3966 +                                resources.resource AS peer_resource,
3967 +                                messages.direction,
3968 +                                messages.type,
3969 +                                messages.subject,
3970 +                                messages.body,
3971 +                                messages.timestamp
3972 +                         FROM
3973 +                                ~s owner,
3974 +                                ~s peer,
3975 +                                ~s servers,
3976 +                                ~s resources,
3977 +                              \", tablename,\" messages
3978 +                         WHERE
3979 +                                owner.user_id=messages.owner_id and
3980 +                                peer.user_id=messages.peer_name_id and
3981 +                                servers.server_id=messages.peer_server_id and
3982 +                                resources.resource_id=messages.peer_resource_id
3983 +                         ORDER BY messages.timestamp;\");
3984 +      PREPARE createview FROM @cq;
3985 +      EXECUTE createview;
3986 +      DEALLOCATE PREPARE createview;
3987 +
3988 +      SET @notable = 0;
3989 +      PREPARE insertmsg FROM @iq;
3990 +      EXECUTE insertmsg;
3991 +   ELSEIF @notable = 0 THEN
3992 +      EXECUTE insertmsg;
3993 +   END IF;
3994 +
3995 +   DEALLOCATE PREPARE insertmsg;
3996 +
3997 +   IF @notable = 0 THEN
3998 +      UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND at=atdate;
3999 +      IF ROW_COUNT() = 0 THEN
4000 +         INSERT INTO ~s (owner_id, at, count) VALUES (@ownerID, atdate, 1);
4001 +      END IF;
4002 +   END IF;
4003 +END;", [UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
4004 --- src/mod_logdb_pgsql.erl.orig        Tue Dec 11 14:23:19 2007
4005 +++ src/mod_logdb_pgsql.erl     Sun Nov 18 20:53:55 2007
4006 @@ -0,0 +1,911 @@
4007 +%%%----------------------------------------------------------------------
4008 +%%% File    : mod_logdb_pgsql.erl
4009 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
4010 +%%% Purpose : Posgresql backend for mod_logdb
4011 +%%% Version : trunk
4012 +%%% Id      : $Id$
4013 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
4014 +%%%----------------------------------------------------------------------
4015 +
4016 +-module(mod_logdb_pgsql).
4017 +-author('o.palij@gmail.com').
4018 +-vsn('$Revision$').
4019 +
4020 +-include("mod_logdb.hrl").
4021 +-include("ejabberd.hrl").
4022 +-include("jlib.hrl").
4023 +
4024 +-behaviour(gen_logdb).
4025 +-behaviour(gen_server).
4026 +
4027 +% gen_server
4028 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
4029 +% gen_mod
4030 +-export([start/2, stop/1]).
4031 +% gen_logdb
4032 +-export([log_message/2,
4033 +         rebuild_stats/1,
4034 +         rebuild_stats_at/2,
4035 +         delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
4036 +         get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
4037 +         get_dates/1,
4038 +         get_users_settings/1, get_user_settings/2, set_user_settings/3]).
4039 +
4040 +% gen_server call timeout
4041 +-define(CALL_TIMEOUT, 60000).
4042 +-define(TIMEOUT, 60000).
4043 +-define(PROCNAME, mod_logdb_pgsql).
4044 +
4045 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
4046 +                    list_to_string/1, string_to_list/1,
4047 +                    convert_timestamp_brief/1]).
4048 +
4049 +-record(state, {dbref, vhost, schema}).
4050 +
4051 +% replace "." with "_"
4052 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4053 +                                    (A) -> A
4054 +                                 end, VHost).
4055 +
4056 +prefix(Schema) ->
4057 +   Schema ++ ".\"" ++ "logdb_".
4058 +
4059 +suffix(VHost) ->
4060 +   "_" ++ escape_vhost(VHost) ++ "\"".
4061 +
4062 +messages_table(VHost, Schema, Date) ->
4063 +   prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost).
4064 +
4065 +% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
4066 +view_table(VHost, Schema, Date) ->
4067 +   Table = messages_table(VHost, Schema, Date),
4068 +   TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3),
4069 +   lists:append([Schema, ".\"v_", TablewoS, "\""]).
4070 +
4071 +stats_table(VHost, Schema) ->
4072 +   prefix(Schema) ++ "stats" ++ suffix(VHost).
4073 +
4074 +settings_table(VHost, Schema) ->
4075 +   prefix(Schema) ++ "settings" ++ suffix(VHost).
4076 +
4077 +users_table(VHost, Schema) ->
4078 +   prefix(Schema) ++ "users" ++ suffix(VHost).
4079 +servers_table(VHost, Schema) ->
4080 +   prefix(Schema) ++ "servers" ++ suffix(VHost).
4081 +resources_table(VHost, Schema) ->
4082 +   prefix(Schema) ++ "resources" ++ suffix(VHost).
4083 +
4084 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4085 +%
4086 +% gen_mod callbacks
4087 +%
4088 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4089 +start(VHost, Opts) ->
4090 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4091 +   gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
4092 +
4093 +stop(VHost) ->
4094 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4095 +   gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
4096 +
4097 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4098 +%
4099 +% gen_server callbacks
4100 +%
4101 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4102 +init([VHost, Opts]) ->
4103 +   Server = gen_mod:get_opt(server, Opts, "localhost"),
4104 +   DB = gen_mod:get_opt(db, Opts, "ejabberd_logdb"),
4105 +   User = gen_mod:get_opt(user, Opts, "root"),
4106 +   Port = gen_mod:get_opt(port, Opts, 5432),
4107 +   Password = gen_mod:get_opt(password, Opts, ""),
4108 +   Schema = gen_mod:get_opt(schema, Opts, "public"),
4109 +
4110 +   case catch pgsql:connect(Server, DB, User, Password, Port) of
4111 +       {ok, DBRef} ->
4112 +           {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]),
4113 +           ok = create_internals(DBRef, VHost, Schema),
4114 +           ok = create_stats_table(DBRef, VHost, Schema),
4115 +           ok = create_settings_table(DBRef, VHost, Schema),
4116 +           ok = create_users_table(DBRef, VHost, Schema),
4117 +           ok = create_servers_table(DBRef, VHost, Schema),
4118 +           ok = create_resources_table(DBRef, VHost, Schema),
4119 +           erlang:monitor(process, DBRef),
4120 +           {ok, #state{dbref=DBRef, vhost=VHost, schema=Schema}};
4121 +       % this does not work
4122 +       {error, Reason} ->
4123 +           ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]),
4124 +           {stop, db_connection_failed};
4125 +       % and this too, becouse pgsql_conn do exit() which can not be catched
4126 +       {'EXIT', Rez} ->
4127 +           ?ERROR_MSG("Rez: ~p~n", [Rez]),
4128 +           {stop, db_connection_failed}
4129 +   end.
4130 +
4131 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4132 +    Date = convert_timestamp_brief(Msg#msg.timestamp),
4133 +    TableName = messages_table(VHost, Schema, Date),
4134 +
4135 +    Query = [ "SELECT logmessage "
4136 +                 "('", TableName, "',",
4137 +                  "'", Date, "',",
4138 +                  "'", Msg#msg.owner_name, "',",
4139 +                  "'", Msg#msg.peer_name, "',",
4140 +                  "'", Msg#msg.peer_server, "',",
4141 +                  "'", ejabberd_odbc:escape(Msg#msg.peer_resource), "',",
4142 +                  "'", atom_to_list(Msg#msg.direction), "',",
4143 +                  "'", Msg#msg.type, "',",
4144 +                  "'", ejabberd_odbc:escape(Msg#msg.subject), "',",
4145 +                  "'", ejabberd_odbc:escape(Msg#msg.body), "',",
4146 +                  "'", Msg#msg.timestamp, "');"],
4147 +
4148 +    Reply =
4149 +       case sql_query_internal_silent(DBRef, Query) of
4150 +            % TODO: change this
4151 +            {data, [{"0"}]} ->
4152 +                ?MYDEBUG("Logged ok for ~p, peer: ~p", [Msg#msg.owner_name++"@"++VHost,
4153 +                                                        Msg#msg.peer_name++"@"++Msg#msg.peer_server]),
4154 +                ok;
4155 +            {error, _Reason} ->
4156 +                error
4157 +       end,
4158 +    {reply, Reply, State};
4159 +handle_call({rebuild_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4160 +    ok = delete_nonexistent_stats(DBRef, VHost, Schema),
4161 +    Reply =
4162 +      lists:foreach(fun(Date) ->
4163 +                        catch rebuild_stats_at_int(DBRef, VHost, Schema, Date)
4164 +                    end, get_dates_int(DBRef, VHost)),
4165 +    {reply, Reply, State};
4166 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4167 +    Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date),
4168 +    {reply, Reply, State};
4169 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
4170 +    {reply, error, State};
4171 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4172 +    Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
4173 +                             ["'",Timestamp,"'",","]
4174 +                         end, Msgs),
4175 +
4176 +    Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4177 +
4178 +    Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
4179 +                             "WHERE timestamp IN (", Temp1],
4180 +
4181 +    Reply =
4182 +      case sql_query_internal(DBRef, Query) of
4183 +           {updated, _} ->
4184 +              rebuild_stats_at_int(DBRef, VHost, Schema, Date);
4185 +           {error, _} ->
4186 +              error
4187 +      end,
4188 +    {reply, Reply, State};
4189 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4190 +    DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
4191 +                 "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
4192 +    Reply =
4193 +      case sql_query_internal(DBRef, DQuery) of
4194 +           {updated, _} ->
4195 +              rebuild_stats_at_int(DBRef, VHost, Schema, Date);
4196 +           {error, _} ->
4197 +              error
4198 +      end,
4199 +    {reply, Reply, State};
4200 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4201 +    % TODO
4202 +    {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
4203 +    Reply =
4204 +      case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date),";"]) of
4205 +           {updated, _} ->
4206 +              Query = ["DELETE FROM ",stats_table(VHost, Schema)," "
4207 +                          "WHERE at='",Date,"';"],
4208 +              case sql_query_internal(DBRef, Query) of
4209 +                   {updated, _} ->
4210 +                      ok;
4211 +                   {error, _} ->
4212 +                      error
4213 +              end;
4214 +           {error, _} ->
4215 +              error
4216 +      end,
4217 +    {reply, Reply, State};
4218 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4219 +    SName = stats_table(VHost, Schema),
4220 +    Query = ["SELECT at, sum(count) ",
4221 +                "FROM ",SName," ",
4222 +                "GROUP BY at ",
4223 +                "ORDER BY DATE(at) DESC;"
4224 +            ],
4225 +    Reply =
4226 +      case sql_query_internal(DBRef, Query) of
4227 +           {data, Recs} ->
4228 +              {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]};
4229 +           {error, Reason} ->
4230 +              % TODO: Duplicate error message ?
4231 +              {error, Reason}
4232 +      end,
4233 +    {reply, Reply, State};
4234 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4235 +    SName = stats_table(VHost, Schema),
4236 +    Query = ["SELECT username, count ",
4237 +                "FROM ",SName," ",
4238 +                "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id "
4239 +                "WHERE at='",Date,"';"
4240 +            ],
4241 +    Reply =
4242 +      case sql_query_internal(DBRef, Query) of
4243 +           {data, Recs} ->
4244 +              RFun = fun({User, Count}) ->
4245 +                          {User, list_to_integer(Count)}
4246 +                     end,
4247 +              {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))};
4248 +           {error, Reason} ->
4249 +              % TODO:
4250 +              {error, Reason}
4251 +      end,
4252 +    {reply, Reply, State};
4253 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4254 +    SName = stats_table(VHost, Schema),
4255 +    Query = ["SELECT at, count ",
4256 +                "FROM ",SName," ",
4257 +                "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ",
4258 +                "ORDER BY DATE(at) DESC;"
4259 +            ],
4260 +    Reply =
4261 +      case sql_query_internal(DBRef, Query) of
4262 +           {data, Recs} ->
4263 +              {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]};
4264 +           {error, Result} ->
4265 +              {error, Result}
4266 +      end,
4267 +    {reply, Reply, State};
4268 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4269 +    Query = ["SELECT peer_name,",
4270 +                    "peer_server,",
4271 +                    "peer_resource,",
4272 +                    "direction,"
4273 +                    "type,"
4274 +                    "subject,"
4275 +                    "body,"
4276 +                    "timestamp "
4277 +               "FROM ",view_table(VHost, Schema, Date)," "
4278 +               "WHERE owner_name='",User,"';"],
4279 +    Reply =
4280 +      case sql_query_internal(DBRef, Query) of
4281 +           {data, Recs} ->
4282 +              Fun = fun({Peer_name, Peer_server, Peer_resource,
4283 +                         Direction,
4284 +                         Type,
4285 +                         Subject, Body,
4286 +                         Timestamp}) ->
4287 +                          #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
4288 +                               direction=list_to_atom(Direction),
4289 +                               type=Type,
4290 +                               subject=Subject, body=Body,
4291 +                               timestamp=Timestamp}
4292 +                    end,
4293 +              {ok, lists:map(Fun, Recs)};
4294 +           {error, Reason} ->
4295 +              {error, Reason}
4296 +      end,
4297 +    {reply, Reply, State};
4298 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4299 +    SName = stats_table(VHost, Schema),
4300 +    Query = ["SELECT at ",
4301 +                "FROM ",SName," ",
4302 +                "GROUP BY at ",
4303 +                "ORDER BY at DESC;"
4304 +            ],
4305 +    Reply =
4306 +       case sql_query_internal(DBRef, Query) of
4307 +            {data, Result} ->
4308 +               [ Date || {Date} <- Result ];
4309 +            {error, Reason} ->
4310 +               {error, Reason}
4311 +       end,
4312 +    {reply, Reply, State};
4313 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4314 +    Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
4315 +                "FROM ",settings_table(VHost, Schema)," ",
4316 +             "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"],
4317 +    Reply =
4318 +      case sql_query_internal(DBRef, Query) of
4319 +           {data, Recs} ->
4320 +              {ok, [#user_settings{owner_name=Owner,
4321 +                                   dolog_default=list_to_bool(DoLogDef),
4322 +                                   dolog_list=string_to_list(DoLogL),
4323 +                                   donotlog_list=string_to_list(DoNotLogL)
4324 +                                  } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]};
4325 +           {error, Reason} ->
4326 +              {error, Reason}
4327 +      end,
4328 +    {reply, Reply, State};
4329 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4330 +    Query = ["SELECT dolog_default,dolog_list,donotlog_list ",
4331 +                "FROM ",settings_table(VHost, Schema)," ",
4332 +             "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
4333 +    Reply =
4334 +      case sql_query_internal_silent(DBRef, Query) of
4335 +           {data, []} ->
4336 +              {ok, []};
4337 +           {data, [{DoLogDef, DoLogL, DoNotLogL}]} ->
4338 +              {ok, #user_settings{owner_name=User,
4339 +                                  dolog_default=list_to_bool(DoLogDef),
4340 +                                  dolog_list=string_to_list(DoLogL),
4341 +                                  donotlog_list=string_to_list(DoNotLogL)}};
4342 +           {error, Reason} ->
4343 +              ?ERROR_MSG("Failed to get_user_settings for ~p@~p: ~p", [User, VHost, Reason]),
4344 +              error
4345 +      end,
4346 +    {reply, Reply, State};
4347 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
4348 +                                                     dolog_list=DoLogL,
4349 +                                                     donotlog_list=DoNotLogL}},
4350 +            _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
4351 +    User_id = get_user_id(DBRef, VHost, Schema, User),
4352 +    Query = ["UPDATE ",settings_table(VHost, Schema)," ",
4353 +                "SET dolog_default=",bool_to_list(DoLogDef),", ",
4354 +                    "dolog_list='",list_to_string(DoLogL),"', ",
4355 +                    "donotlog_list='",list_to_string(DoNotLogL),"' ",
4356 +                "WHERE owner_id=",User_id,";"],
4357 +
4358 +    Reply =
4359 +      case sql_query_internal(DBRef, Query) of
4360 +           {updated, 0} ->
4361 +              IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ",
4362 +                            "(owner_id, dolog_default, dolog_list, donotlog_list) ",
4363 +                            "VALUES ",
4364 +                            "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
4365 +              case sql_query_internal(DBRef, IQuery) of
4366 +                   {updated, 1} ->
4367 +                       ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
4368 +                       ok;
4369 +                   {error, _} ->
4370 +                       error
4371 +              end;
4372 +           {updated, 1} ->
4373 +              ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
4374 +              ok;
4375 +           {error, _} ->
4376 +              error
4377 +      end,
4378 +    {reply, Reply, State};
4379 +handle_call({stop}, _From, State) ->
4380 +   ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]),
4381 +   {stop, normal, ok, State};
4382 +handle_call(Msg, _From, State) ->
4383 +    ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
4384 +    {noreply, State}.
4385 +
4386 +handle_cast(Msg, State) ->
4387 +    ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
4388 +    {noreply, State}.
4389 +
4390 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
4391 +    {stop, connection_dropped, State};
4392 +handle_info(Info, State) ->
4393 +    ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
4394 +    {noreply, State}.
4395 +
4396 +terminate(_Reason, _State) ->
4397 +    ok.
4398 +
4399 +code_change(_OldVsn, State, _Extra) ->
4400 +    {ok, State}.
4401 +
4402 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4403 +%
4404 +% gen_logdb callbacks
4405 +%
4406 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4407 +log_message(VHost, Msg) ->
4408 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4409 +   gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
4410 +rebuild_stats(VHost) ->
4411 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4412 +   gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
4413 +rebuild_stats_at(VHost, Date) ->
4414 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4415 +   gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
4416 +delete_messages_by_user_at(VHost, Msgs, Date) ->
4417 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4418 +   gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
4419 +delete_all_messages_by_user_at(User, VHost, Date) ->
4420 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4421 +   gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
4422 +delete_messages_at(VHost, Date) ->
4423 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4424 +   gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
4425 +get_vhost_stats(VHost) ->
4426 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4427 +   gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
4428 +get_vhost_stats_at(VHost, Date) ->
4429 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4430 +   gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
4431 +get_user_stats(User, VHost) ->
4432 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4433 +   gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
4434 +get_user_messages_at(User, VHost, Date) ->
4435 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4436 +   gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
4437 +get_dates(VHost) ->
4438 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4439 +   gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
4440 +get_users_settings(VHost) ->
4441 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4442 +   gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
4443 +get_user_settings(User, VHost) ->
4444 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4445 +   gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
4446 +set_user_settings(User, VHost, Set) ->
4447 +   Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4448 +   gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
4449 +
4450 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4451 +%
4452 +% internals
4453 +%
4454 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4455 +get_dates_int(DBRef, VHost) ->
4456 +    Query = ["SELECT n.nspname as \"Schema\",
4457 +                c.relname as \"Name\",
4458 +                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\",
4459 +                r.rolname as \"Owner\"
4460 +              FROM pg_catalog.pg_class c
4461 +                   JOIN pg_catalog.pg_roles r ON r.oid = c.relowner
4462 +                   LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
4463 +              WHERE c.relkind IN ('r','')
4464 +                    AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
4465 +                    AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$'
4466 +                    AND pg_catalog.pg_table_is_visible(c.oid)
4467 +              ORDER BY 1,2;"],
4468 +    case sql_query_internal(DBRef, Query) of
4469 +         {data, Recs} ->
4470 +            lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) ->
4471 +                             case regexp:match(Table,"[0-9]+-[0-9]+-[0-9]+") of
4472 +                                  {match, S, E} ->
4473 +                                      lists:append(Dates, [lists:sublist(Table,S,E)]);
4474 +                                  nomatch ->
4475 +                                      Dates
4476 +                             end
4477 +                        end, [], Recs);
4478 +         {error, _} ->
4479 +            []
4480 +    end.
4481 +
4482 +rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
4483 +    Table = messages_table(VHost, Schema, Date),
4484 +    STable = stats_table(VHost, Schema),
4485 +
4486 +    Fun =
4487 +      fun() ->
4488 +       {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]),
4489 +       {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
4490 +
4491 +       DQuery = [ "DELETE FROM ",STable," ",
4492 +                     "WHERE at='",Date,"';"],
4493 +
4494 +       {updated, _} = sql_query_internal(DBRef, DQuery),
4495 +
4496 +       SQuery = ["INSERT INTO ",STable," ",
4497 +                  "(owner_id,at,count) ",
4498 +                     "SELECT owner_id,'",Date,"'",",count(*) ",
4499 +                        "FROM ",Table," GROUP BY owner_id;"],
4500 +
4501 +       case sql_query_internal(DBRef, SQuery) of
4502 +            {updated, 0} ->
4503 +                {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
4504 +                ok;
4505 +            {updated, _} -> ok;
4506 +            {error, _} -> error
4507 +       end
4508 +      end,
4509 +
4510 +    case sql_transaction_internal(DBRef, Fun) of
4511 +         {atomic, _} ->
4512 +             ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
4513 +             ok;
4514 +         {aborted, _} ->
4515 +             error
4516 +    end.
4517 +
4518 +delete_nonexistent_stats(DBRef, VHost, Schema) ->
4519 +    Dates = get_dates_int(DBRef, VHost),
4520 +    STable = stats_table(VHost, Schema),
4521 +
4522 +    Temp = lists:flatmap(fun(Date) ->
4523 +                             ["'",Date,"'",","]
4524 +                         end, Dates),
4525 +
4526 +    Temp1 = case Temp of
4527 +                 [] ->
4528 +                   ["''"];
4529 +                 _ ->
4530 +                   % replace last "," with ");"
4531 +                   lists:append([lists:sublist(Temp, length(Temp)-1), ");"])
4532 +            end,
4533 +
4534 +    Query = ["DELETE FROM ",STable," ",
4535 +                 "WHERE at NOT IN (", Temp1],
4536 +
4537 +    case sql_query_internal(DBRef, Query) of
4538 +         {updated, _} ->
4539 +            ok;
4540 +         {error, _} ->
4541 +            error
4542 +    end.
4543 +
4544 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4545 +%
4546 +% tables internals
4547 +%
4548 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4549 +create_stats_table(DBRef, VHost, Schema) ->
4550 +    SName = stats_table(VHost, Schema),
4551 +
4552 +    Fun =
4553 +      fun() ->
4554 +        Query = ["CREATE TABLE ",SName," (",
4555 +                    "owner_id INTEGER, ",
4556 +                    "at VARCHAR(20), ",
4557 +                    "count integer",
4558 +                 ");"
4559 +                ],
4560 +        case sql_query_internal_silent(DBRef, Query) of
4561 +             {updated, _} ->
4562 +                {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_owner_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id);"]),
4563 +                {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]),
4564 +                created;
4565 +             {error, Reason} ->
4566 +                case lists:keysearch(code, 1, Reason) of
4567 +                     {value, {code, "42P07"}} ->
4568 +                         exists;
4569 +                     _ ->
4570 +                         ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
4571 +                         error
4572 +                end
4573 +        end
4574 +      end,
4575 +    case sql_transaction_internal(DBRef, Fun) of
4576 +         {atomic, created} ->
4577 +            ?MYDEBUG("Created stats table for ~p", [VHost]),
4578 +            lists:foreach(fun(Date) ->
4579 +                            rebuild_stats_at_int(DBRef, VHost, Schema, Date)
4580 +                          end, get_dates_int(DBRef, VHost));
4581 +         {atomic, exists} ->
4582 +            ?MYDEBUG("Stats table for ~p already exists", [VHost]),
4583 +            ok;
4584 +         {error, _} -> error
4585 +    end.
4586 +
4587 +create_settings_table(DBRef, VHost, Schema) ->
4588 +    SName = settings_table(VHost, Schema),
4589 +    Query = ["CREATE TABLE ",SName," (",
4590 +                "owner_id INTEGER PRIMARY KEY, ",
4591 +                "dolog_default BOOLEAN, ",
4592 +                "dolog_list TEXT DEFAULT '', ",
4593 +                "donotlog_list TEXT DEFAULT ''",
4594 +             ");"
4595 +            ],
4596 +    case sql_query_internal_silent(DBRef, Query) of
4597 +         {updated, _} ->
4598 +            ?MYDEBUG("Created settings table for ~p", [VHost]),
4599 +            ok;
4600 +         {error, Reason} ->
4601 +            case lists:keysearch(code, 1, Reason) of
4602 +                 {value, {code, "42P07"}} ->
4603 +                   ?MYDEBUG("Settings table for ~p already exists", [VHost]),
4604 +                   ok;
4605 +                 _ ->
4606 +                   ?ERROR_MSG("Failed to create settings table for ~p: ~p", [VHost, Reason]),
4607 +                   error
4608 +            end
4609 +    end.
4610 +
4611 +create_users_table(DBRef, VHost, Schema) ->
4612 +    SName = users_table(VHost, Schema),
4613 +
4614 +    Fun =
4615 +      fun() ->
4616 +        Query = ["CREATE TABLE ",SName," (",
4617 +                    "username TEXT UNIQUE, ",
4618 +                    "user_id SERIAL PRIMARY KEY",
4619 +                 ");"
4620 +                ],
4621 +        case sql_query_internal_silent(DBRef, Query) of
4622 +             {updated, _} ->
4623 +                {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]),
4624 +                created;
4625 +             {error, Reason} ->
4626 +                case lists:keysearch(code, 1, Reason) of
4627 +                     {value, {code, "42P07"}} ->
4628 +                       exists;
4629 +                     _ ->
4630 +                       ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]),
4631 +                       error
4632 +                end
4633 +        end
4634 +      end,
4635 +    case sql_transaction_internal(DBRef, Fun) of
4636 +         {atomic, created} ->
4637 +             ?MYDEBUG("Created users table for ~p", [VHost]),
4638 +             ok;
4639 +         {atomic, exists} -> 
4640 +             ?MYDEBUG("Users table for ~p already exists", [VHost]),
4641 +             ok;
4642 +         {aborted, _} -> error
4643 +    end.
4644 +
4645 +create_servers_table(DBRef, VHost, Schema) ->
4646 +    SName = servers_table(VHost, Schema),
4647 +
4648 +    Fun =
4649 +      fun() ->
4650 +        Query = ["CREATE TABLE ",SName," (",
4651 +                    "server TEXT UNIQUE, ",
4652 +                    "server_id SERIAL PRIMARY KEY",
4653 +                 ");"
4654 +                ],
4655 +        case sql_query_internal_silent(DBRef, Query) of
4656 +             {updated, _} ->
4657 +                {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]),
4658 +                created;
4659 +             {error, Reason} ->
4660 +                case lists:keysearch(code, 1, Reason) of
4661 +                     {value, {code, "42P07"}} ->
4662 +                       exists;
4663 +                     _ ->
4664 +                       ?ERROR_MSG("Failed to create servers table for ~p: ~p", [VHost, Reason]),
4665 +                       error
4666 +                end
4667 +        end
4668 +      end,
4669 +    case sql_transaction_internal(DBRef, Fun) of
4670 +         {atomic, created} ->
4671 +            ?MYDEBUG("Created servers table for ~p", [VHost]),
4672 +            ok;
4673 +         {atomic, exists} ->
4674 +            ?MYDEBUG("Servers table for ~p already exists", [VHost]),
4675 +            ok;
4676 +         {aborted, _} -> error
4677 +    end.
4678 +
4679 +create_resources_table(DBRef, VHost, Schema) ->
4680 +    RName = resources_table(VHost, Schema),
4681 +    Fun = fun() ->
4682 +            Query = ["CREATE TABLE ",RName," (",
4683 +                     "resource TEXT UNIQUE, ",
4684 +                     "resource_id SERIAL PRIMARY KEY",
4685 +                     ");"
4686 +                    ],
4687 +            case sql_query_internal_silent(DBRef, Query) of
4688 +                 {updated, _} ->
4689 +                    {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]),
4690 +                    created;
4691 +                 {error, Reason} ->
4692 +                    case lists:keysearch(code, 1, Reason) of
4693 +                         {value, {code, "42P07"}} ->
4694 +                           exists;
4695 +                         _ ->
4696 +                           ?ERROR_MSG("Failed to create users table for ~p: ~p", [VHost, Reason]),
4697 +                           error
4698 +                    end
4699 +            end
4700 +          end,
4701 +    case sql_transaction_internal(DBRef, Fun) of
4702 +         {atomic, created} ->
4703 +             ?MYDEBUG("Created resources table for ~p", [VHost]),
4704 +             ok;
4705 +         {atomic, exists} ->
4706 +             ?MYDEBUG("Resources table for ~p already exists", [VHost]),
4707 +             ok;
4708 +         {aborted, _} -> error
4709 +    end.
4710 +
4711 +create_internals(DBRef, VHost, Schema) ->
4712 +    case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
4713 +         {updated, _} ->
4714 +            ?MYDEBUG("Created logmessage for ~p", [VHost]),
4715 +            ok;
4716 +         {error, _} ->
4717 +            error
4718 +    end.
4719 +
4720 +get_user_id(DBRef, VHost, Schema, User) ->
4721 +    SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ",
4722 +                 "WHERE username='",User,"';"],
4723 +    case sql_query_internal(DBRef, SQuery) of
4724 +         {data, []} ->
4725 +             IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ",
4726 +                          "VALUES ('",User,"');"],
4727 +             case sql_query_internal_silent(DBRef, IQuery) of
4728 +                  {updated, _} ->
4729 +                      {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery),
4730 +                      DBIdNew;
4731 +                  {error, Reason} ->
4732 +                      % this can be in clustered environment
4733 +                      {value, {code, "23505"}} = lists:keysearch(code, 1, Reason),
4734 +                      ?ERROR_MSG("Duplicate key name for ~p", [User]),
4735 +                      {data, [{ClID}]} = sql_query_internal(DBRef, SQuery),
4736 +                      ClID
4737 +             end;
4738 +         {data, [{DBId}]} ->
4739 +            DBId
4740 +    end.
4741 +
4742 +get_logmessage(VHost,Schema) ->
4743 +    UName = users_table(VHost,Schema),
4744 +    SName = servers_table(VHost,Schema),
4745 +    RName = resources_table(VHost,Schema),
4746 +    StName = stats_table(VHost,Schema),
4747 +    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 $$
4748 +DECLARE
4749 +   ownerID INTEGER;
4750 +   peer_nameID INTEGER;
4751 +   peer_serverID INTEGER;
4752 +   peer_resourceID INTEGER;
4753 +   tablename ALIAS for $1;
4754 +   atdate ALIAS for $2;
4755 +   viewname TEXT;
4756 +BEGIN
4757 +   SELECT INTO ownerID user_id FROM ~s WHERE username = owner;
4758 +   IF NOT FOUND THEN
4759 +      INSERT INTO ~s (username) VALUES (owner);
4760 +      ownerID := lastval();
4761 +   END IF;
4762 +
4763 +   SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name;
4764 +   IF NOT FOUND THEN
4765 +      INSERT INTO ~s (username) VALUES (peer_name);
4766 +      peer_nameID := lastval();
4767 +   END IF;
4768 +
4769 +   SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server;
4770 +   IF NOT FOUND THEN
4771 +      INSERT INTO ~s (server) VALUES (peer_server);
4772 +      peer_serverID := lastval();
4773 +   END IF;
4774 +
4775 +   SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource;
4776 +   IF NOT FOUND THEN
4777 +      INSERT INTO ~s (resource) VALUES (peer_resource);
4778 +      peer_resourceID := lastval();
4779 +   END IF;
4780 +
4781 +   BEGIN
4782 +      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 || ')';
4783 +   EXCEPTION WHEN undefined_table THEN
4784 +      EXECUTE 'CREATE TABLE ' || tablename || ' (' ||
4785 +                   'owner_id INTEGER, ' ||
4786 +                   'peer_name_id INTEGER, ' ||
4787 +                   'peer_server_id INTEGER, ' ||
4788 +                   'peer_resource_id INTEGER, ' ||
4789 +                   'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' ||
4790 +                   'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' ||
4791 +                   'subject TEXT, ' ||
4792 +                   'body TEXT, ' ||
4793 +                   'timestamp DOUBLE PRECISION)';
4794 +      EXECUTE 'CREATE INDEX \"owner_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id)';
4795 +      EXECUTE 'CREATE INDEX \"peer_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (peer_server_id, peer_name_id)';
4796 +
4797 +      viewname := '~s.\"v_' || trim(both '~s.\"' from tablename) || '\"';
4798 +
4799 +      EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' ||
4800 +                   'SELECT owner.username AS owner_name, ' ||
4801 +                          'peer.username AS peer_name, ' ||
4802 +                          'servers.server AS peer_server, ' ||
4803 +                          'resources.resource AS peer_resource, ' ||
4804 +                          'messages.direction, ' ||
4805 +                          'messages.type, ' ||
4806 +                          'messages.subject, ' ||
4807 +                          'messages.body, ' ||
4808 +                          'messages.timestamp ' ||
4809 +                   'FROM ' ||
4810 +                          '~s owner, ' ||
4811 +                          '~s peer, ' ||
4812 +                          '~s servers, ' ||
4813 +                          '~s resources, ' ||
4814 +                           tablename || ' messages ' ||
4815 +                   'WHERE ' ||
4816 +                          'owner.user_id=messages.owner_id and ' ||
4817 +                          'peer.user_id=messages.peer_name_id and ' ||
4818 +                          'servers.server_id=messages.peer_server_id and ' ||
4819 +                          'resources.resource_id=messages.peer_resource_id ' ||
4820 +                   'ORDER BY messages.timestamp';
4821 +
4822 +      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 || ')';
4823 +   END;
4824 +
4825 +   UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID;
4826 +   IF NOT FOUND THEN
4827 +      INSERT INTO ~s (owner_id, at, count) VALUES (ownerID, atdate, 1);
4828 +   END IF;
4829 +   RETURN 0;
4830 +END;
4831 +$$ LANGUAGE plpgsql;
4832 +", [UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),Schema,escape_vhost(VHost),Schema,Schema,UName,UName,SName,RName,StName,StName]).
4833 +
4834 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4835 +%
4836 +% SQL internals 
4837 +%
4838 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4839 +% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
4840 +sql_transaction_internal(DBRef, Fun) ->
4841 +    case sql_query_internal(DBRef, ["BEGIN;"]) of
4842 +         {updated, _} ->
4843 +            case catch Fun() of
4844 +                 error = Err ->
4845 +                   rollback_internal(DBRef, Err);
4846 +                 {error, _} = Err ->
4847 +                   rollback_internal(DBRef, Err);
4848 +                 {'EXIT', _} = Err ->
4849 +                   rollback_internal(DBRef, Err);
4850 +                 Res ->
4851 +                   case sql_query_internal(DBRef, ["COMMIT;"]) of
4852 +                        {error, _} -> rollback_internal(DBRef, {commit_error});
4853 +                        {updated, _} ->
4854 +                           case Res of
4855 +                                {atomic, _} -> Res;
4856 +                                _ -> {atomic, Res}
4857 +                           end
4858 +                   end
4859 +            end;
4860 +         {error, _} ->
4861 +            {aborted, {begin_error}}
4862 +    end.
4863 +
4864 +% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
4865 +rollback_internal(DBRef, Reason) ->
4866 +    Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
4867 +    {aborted, {Reason, {rollback_result, Res}}}.
4868 +
4869 +sql_query_internal(DBRef, Query) ->
4870 +    case sql_query_internal_silent(DBRef, Query) of
4871 +         {error, undefined, Rez} ->
4872 +            ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]),
4873 +            {error, undefined};
4874 +         {error, Error} ->
4875 +            ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]),
4876 +            {error, Error};
4877 +         Rez -> Rez
4878 +    end.
4879 +
4880 +sql_query_internal_silent(DBRef, Query) ->
4881 +    ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
4882 +    get_result(pgsql:squery(DBRef, Query)).
4883 +
4884 +get_result({ok, ["CREATE TABLE"]}) ->
4885 +    {updated, 1};
4886 +get_result({ok, ["DROP TABLE"]}) ->
4887 +    {updated, 1};
4888 +get_result({ok,["DROP VIEW"]}) ->
4889 +    {updated, 1};
4890 +get_result({ok, ["CREATE INDEX"]}) ->
4891 +    {updated, 1};
4892 +get_result({ok, ["CREATE FUNCTION"]}) ->
4893 +    {updated, 1};
4894 +get_result({ok, [{"SELECT", _Rows, Recs}]}) ->
4895 +    {data, [list_to_tuple(Rec) || Rec <- Recs]};
4896 +get_result({ok, ["INSERT " ++ OIDN]}) ->
4897 +    [_OID, N] = string:tokens(OIDN, " "),
4898 +    {updated, list_to_integer(N)};
4899 +get_result({ok, ["DELETE " ++ N]}) ->
4900 +    {updated, list_to_integer(N)};
4901 +get_result({ok, ["UPDATE " ++ N]}) ->
4902 +    {updated, list_to_integer(N)};
4903 +get_result({ok, ["BEGIN"]}) ->
4904 +    {updated, 1};
4905 +get_result({ok, ["LOCK TABLE"]}) ->
4906 +    {updated, 1};
4907 +get_result({ok, ["ROLLBACK"]}) ->
4908 +    {updated, 1};
4909 +get_result({ok, ["COMMIT"]}) ->
4910 +    {updated, 1};
4911 +get_result({ok, ["SET"]}) ->
4912 +    {updated, 1};
4913 +get_result({ok, [{error, Error}]}) ->
4914 +    {error, Error};
4915 +get_result(Rez) ->
4916 +    {error, undefined, Rez}.
4917 +
4918 --- src/mod_logdb_mnesia_old.erl.orig   Tue Dec 11 14:23:19 2007
4919 +++ src/mod_logdb_mnesia_old.erl        Wed Aug 22 22:58:11 2007
4920 @@ -0,0 +1,256 @@
4921 +%%%----------------------------------------------------------------------
4922 +%%% File    : mod_logdb_mnesia_old.erl
4923 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
4924 +%%% Purpose : mod_logmnesia backend for mod_logdb (should be used only for copy_tables functionality)
4925 +%%% Version : trunk
4926 +%%% Id      : $Id$
4927 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
4928 +%%%----------------------------------------------------------------------
4929 +
4930 +-module(mod_logdb_mnesia_old).
4931 +-author('o.palij@gmail.com').
4932 +-vsn('$Revision$').
4933 +
4934 +-include("ejabberd.hrl").
4935 +-include("jlib.hrl").
4936 +
4937 +-behaviour(gen_logdb).
4938 +
4939 +-export([start/2, stop/1,
4940 +         log_message/2,
4941 +         rebuild_stats/1,
4942 +         rebuild_stats_at/2,
4943 +         rebuild_stats_at1/2,
4944 +         delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
4945 +         get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
4946 +         get_dates/1,
4947 +         get_users_settings/1, get_user_settings/2, set_user_settings/3]).
4948 +
4949 +-record(stats, {user, server, table, count}).
4950 +-record(msg,   {to_user, to_server, to_resource, from_user, from_server, from_resource, id, type, subject, body, timestamp}).
4951 +
4952 +tables_prefix() -> "messages_".
4953 +% stats_table should not start with tables_prefix(VHost) ! 
4954 +% i.e. lists:prefix(tables_prefix(VHost), atom_to_list(stats_table())) must be /= true
4955 +stats_table() -> list_to_atom("messages-stats").
4956 +% table name as atom from Date
4957 +-define(ATABLE(Date), list_to_atom(tables_prefix() ++ Date)).
4958 +-define(LTABLE(Date), tables_prefix() ++ Date).
4959 +
4960 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4961 +%
4962 +% gen_logdb callbacks
4963 +%
4964 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4965 +start(_Opts, _VHost) ->
4966 +   case mnesia:system_info(is_running) of
4967 +        yes ->
4968 +          ok = create_stats_table(),
4969 +          {ok, ok};
4970 +        no ->
4971 +          ?ERROR_MSG("Mnesia not running", []),
4972 +          error;
4973 +        Status ->
4974 +          ?ERROR_MSG("Mnesia status: ~p", [Status]),
4975 +          error
4976 +   end.
4977 +
4978 +stop(_VHost) ->
4979 +   ok.
4980 +
4981 +log_message(_VHost, _Msg) ->
4982 +   error.
4983 +
4984 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4985 +%
4986 +% gen_logdb callbacks (maintaince)
4987 +%
4988 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4989 +rebuild_stats(_VHost) ->
4990 +     ok.
4991 +
4992 +rebuild_stats_at(VHost, Date) ->
4993 +    Table = ?LTABLE(Date),
4994 +    {Time, Value}=timer:tc(?MODULE, rebuild_stats_at1, [VHost, Table]),
4995 +    ?INFO_MSG("rebuild_stats_at ~p elapsed ~p sec: ~p~n", [Date, Time/1000000, Value]),
4996 +    Value.
4997 +rebuild_stats_at1(VHost, Table) ->
4998 +    CFun = fun(Msg, Stats) ->
4999 +               To = Msg#msg.to_user ++ "@" ++ Msg#msg.to_server,
5000 +               Stats_to = if 
5001 +                            Msg#msg.to_server == VHost ->
5002 +                               case lists:keysearch(To, 1, Stats) of
5003 +                                    {value, {Who_to, Count_to}} ->
5004 +                                       lists:keyreplace(To, 1, Stats, {Who_to, Count_to + 1});
5005 +                                    false ->
5006 +                                        lists:append(Stats, [{To, 1}])
5007 +                               end;
5008 +                            true ->
5009 +                               Stats
5010 +                          end,
5011 +               From = Msg#msg.from_user ++ "@" ++ Msg#msg.from_server,
5012 +               Stats_from = if
5013 +                              Msg#msg.from_server == VHost  ->
5014 +                                 case lists:keysearch(From, 1, Stats_to) of
5015 +                                      {value, {Who_from, Count_from}} ->
5016 +                                         lists:keyreplace(From, 1, Stats_to, {Who_from, Count_from + 1});
5017 +                                      false ->
5018 +                                         lists:append(Stats_to, [{From, 1}])
5019 +                                 end;
5020 +                              true ->
5021 +                                 Stats_to
5022 +                            end,
5023 +               Stats_from
5024 +           end,
5025 +    DFun = fun(#stats{table=STable, server=Server} = Stat, _Acc)
5026 +                when STable == Table, Server == VHost ->
5027 +                 mnesia:delete_object(stats_table(), Stat, write);
5028 +              (_Stat, _Acc) -> ok
5029 +           end,
5030 +    case mnesia:transaction(fun() ->
5031 +                               mnesia:write_lock_table(list_to_atom(Table)),
5032 +                               mnesia:write_lock_table(stats_table()),
5033 +                               % Calc stats for VHost at Date
5034 +                               AStats = mnesia:foldl(CFun, [], list_to_atom(Table)),
5035 +                               % Delete all stats for VHost at Date
5036 +                               mnesia:foldl(DFun, [], stats_table()),
5037 +                               % Write new calc'ed stats
5038 +                               lists:foreach(fun({Who, Count}) ->
5039 +                                                 Jid = jlib:string_to_jid(Who),
5040 +                                                 JUser = Jid#jid.user,
5041 +                                                 WStat = #stats{user=JUser, server=VHost, table=Table, count=Count},
5042 +                                                 mnesia:write(stats_table(), WStat, write)
5043 +                                             end, AStats)
5044 +                            end) of
5045 +         {aborted, Reason} ->
5046 +              ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Table, Reason]),
5047 +              error;
5048 +         {atomic, _} ->
5049 +              ok
5050 +    end.
5051 +
5052 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5053 +%
5054 +% gen_logdb callbacks (delete)
5055 +%
5056 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5057 +delete_messages_by_user_at(_VHost, _Msgs, _Date) ->
5058 +    error.
5059 +
5060 +delete_all_messages_by_user_at(_User, _VHost, _Date) ->
5061 +    error.
5062 +
5063 +delete_messages_at(VHost, Date) ->
5064 +   Table = list_to_atom(tables_prefix() ++ Date),
5065 +
5066 +   DFun = fun(#msg{to_server=To_server, from_server=From_server}=Msg, _Acc)
5067 +                when To_server == VHost; From_server == VHost ->
5068 +                   mnesia:delete_object(Table, Msg, write);
5069 +             (_Msg, _Acc) -> ok
5070 +          end,
5071 +   
5072 +   case mnesia:transaction(fun() ->
5073 +                            mnesia:foldl(DFun, [], Table)
5074 +                           end) of
5075 +        {aborted, Reason} ->
5076 +            ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p: ~p", [VHost, Date, Reason]),
5077 +            error;
5078 +        {atomic, _} ->
5079 +            ok
5080 +   end.
5081 +
5082 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5083 +%
5084 +% gen_logdb callbacks (get)
5085 +%
5086 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5087 +get_vhost_stats(_VHost) ->
5088 +    {error, "does not emplemented"}.
5089 +
5090 +get_vhost_stats_at(VHost, Date) ->
5091 +    Fun = fun() ->
5092 +             Pat = #stats{user='$1', server=VHost, table=tables_prefix()++Date, count = '$2'},
5093 +             mnesia:select(stats_table(), [{Pat, [], [['$1', '$2']]}])
5094 +          end,
5095 +    case mnesia:transaction(Fun) of
5096 +         {atomic, Result} ->
5097 +                   RFun = fun([User, Count]) ->
5098 +                             {User, Count}
5099 +                          end,
5100 +                   {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Result)))};
5101 +         {aborted, Reason} -> {error, Reason}
5102 +    end.
5103 +
5104 +get_user_stats(_User, _VHost) ->
5105 +    {error, "does not emplemented"}.
5106 +
5107 +get_user_messages_at(User, VHost, Date) ->
5108 +    Table_name = tables_prefix() ++ Date,
5109 +    case mnesia:transaction(fun() ->
5110 +                               Pat_to = #msg{to_user=User, to_server=VHost, _='_'},
5111 +                               Pat_from = #msg{from_user=User, from_server=VHost,  _='_'},
5112 +                               mnesia:select(list_to_atom(Table_name),
5113 +                                             [{Pat_to, [], ['$_']},
5114 +                                              {Pat_from, [], ['$_']}])
5115 +                       end) of
5116 +          {atomic, Result} ->
5117 +                   Msgs = lists:map(fun(#msg{to_user=To_user, to_server=To_server, to_resource=To_res,
5118 +                                             from_user=From_user, from_server=From_server, from_resource=From_res,
5119 +                                             type=Type,
5120 +                                             subject=Subj,
5121 +                                             body=Body, timestamp=Timestamp} = _Msg) ->
5122 +                                        Subject = case Subj of
5123 +                                                       "None" -> "";
5124 +                                                       _ -> Subj
5125 +                                                  end,
5126 +                                        {msg, To_user, To_server, To_res, From_user, From_server, From_res, Type, Subject, Body, Timestamp}
5127 +                                    end, Result),
5128 +                   {ok, Msgs};
5129 +          {aborted, Reason} ->
5130 +                   {error, Reason}
5131 +    end.
5132 +
5133 +get_dates(_VHost) ->
5134 +    Tables = mnesia:system_info(tables),
5135 +    MessagesTables =
5136 +        lists:filter(fun(Table) ->
5137 +                         lists:prefix(tables_prefix(), atom_to_list(Table))
5138 +                     end,
5139 +                     Tables),
5140 +    lists:map(fun(Table) ->
5141 +                  lists:sublist(atom_to_list(Table),
5142 +                                length(tables_prefix())+1,
5143 +                                length(atom_to_list(Table)))
5144 +              end,
5145 +              MessagesTables).
5146 +
5147 +get_users_settings(_VHost) ->
5148 +    {ok, []}.
5149 +get_user_settings(_User, _VHost) ->
5150 +    {ok, []}.
5151 +set_user_settings(_User, _VHost, _Set) ->
5152 +    ok.
5153 +
5154 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5155 +%
5156 +% internal 
5157 +%
5158 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5159 +% called from db_logon/2
5160 +create_stats_table() ->
5161 +    SName = stats_table(),
5162 +    case mnesia:create_table(SName,
5163 +                             [{disc_only_copies, [node()]},
5164 +                              {type, bag},
5165 +                              {attributes, record_info(fields, stats)},
5166 +                              {record_name, stats}
5167 +                             ]) of
5168 +         {atomic, ok} ->
5169 +             ?INFO_MSG("Created stats table", []),
5170 +             ok;
5171 +         {aborted, {already_exists, _}} ->
5172 +             ok;
5173 +         {aborted, Reason} ->
5174 +             ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
5175 +             error
5176 +    end.
5177 --- src/gen_logdb.erl.orig      Tue Dec 11 14:23:19 2007
5178 +++ src/gen_logdb.erl   Wed Aug 22 22:58:11 2007
5179 @@ -0,0 +1,158 @@
5180 +%%%----------------------------------------------------------------------
5181 +%%% File    : gen_logdb.erl
5182 +%%% Author  : Oleg Palij (mailto:o.palij@gmail.com xmpp://malik@jabber.te.ua)
5183 +%%% Purpose : Describes generic behaviour for mod_logdb backends.
5184 +%%% Version : trunk
5185 +%%% Id      : $Id$
5186 +%%% Url     : http://www.dp.uz.gov.ua/o.palij/mod_logdb/
5187 +%%%----------------------------------------------------------------------
5188 +
5189 +-module(gen_logdb).
5190 +-author('o.palij@gmail.com').
5191 +-vsn('$Revision$').
5192 +
5193 +-export([behaviour_info/1]).
5194 +
5195 +behaviour_info(callbacks) ->
5196 +   [
5197 +    % called from handle_info(start, _)
5198 +    % it should logon database and return reference to started instance
5199 +    % start(VHost, Opts) -> {ok, SPid} | error
5200 +    %  Options - list of options to connect to db
5201 +    %    Types: Options = list() -> [] |
5202 +    %                              [{user, "logdb"},
5203 +    %                               {pass, "1234"},
5204 +    %                               {db, "logdb"}] | ...
5205 +    %          VHost = list() -> "jabber.example.org"
5206 +    {start, 2},
5207 +
5208 +    % called from cleanup/1
5209 +    % it should logoff database and do cleanup
5210 +    % stop(VHost)
5211 +    %    Types: VHost = list() -> "jabber.example.org"
5212 +    {stop, 1},
5213 +
5214 +    % called from handle_call({addlog, _}, _, _)
5215 +    % it should log messages to database
5216 +    % log_message(VHost, Msg) -> ok | error
5217 +    %    Types:
5218 +    %          VHost = list() -> "jabber.example.org"
5219 +    %          Msg = record() -> #msg
5220 +    {log_message, 2},
5221 +
5222 +    % called from ejabberdctl rebuild_stats
5223 +    % it should rebuild stats table (if used) for vhost
5224 +    % rebuild_stats(VHost)
5225 +    %    Types:
5226 +    %          VHost = list() -> "jabber.example.org"
5227 +    {rebuild_stats, 1},
5228 +
5229 +    % it should rebuild stats table (if used) for vhost at Date
5230 +    % rebuild_stats_at(VHost, Date)
5231 +    %    Types:
5232 +    %          VHost = list() -> "jabber.example.org"
5233 +    %          Date = list() -> "2007-02-12"
5234 +    {rebuild_stats_at, 2},
5235 +
5236 +    % called from user_messages_at_parse_query/5
5237 +    % it should delete selected user messages at date
5238 +    % delete_messages_by_user_at(VHost, Msgs, Date) -> ok | error
5239 +    %    Types:
5240 +    %          VHost = list() -> "jabber.example.org"
5241 +    %          Msgs = list() -> [ #msg1, msg2, ... ]
5242 +    %          Date = list() -> "2007-02-12"
5243 +    {delete_messages_by_user_at, 3},
5244 +
5245 +    % called from user_messages_parse_query/4 | vhost_messages_at_parse_query/4
5246 +    % it should delete all user messages at date
5247 +    % delete_all_messages_by_user_at(User, VHost, Date) -> ok | error
5248 +    %    Types:
5249 +    %          User = list() -> "admin"
5250 +    %          VHost = list() -> "jabber.example.org"
5251 +    %          Date = list() -> "2007-02-12"
5252 +    {delete_all_messages_by_user_at, 3},
5253 +
5254 +    % called from vhost_messages_parse_query/3
5255 +    % it should delete messages for vhost at date and update stats
5256 +    % delete_messages_at(VHost, Date) -> ok | error
5257 +    %    Types:
5258 +    %          VHost = list() -> "jabber.example.org"
5259 +    %          Date = list() -> "2007-02-12"
5260 +    {delete_messages_at, 2},
5261 +
5262 +    % called from ejabberd_web_admin:vhost_messages_stats/3
5263 +    % it should return sorted list of count of messages by dates for vhost
5264 +    % get_vhost_stats(VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ... ]} |
5265 +    %                           {error, Reason}
5266 +    %    Types:
5267 +    %          VHost = list() -> "jabber.example.org"
5268 +    %          DateN = list() -> "2007-02-12"
5269 +    %          Msgs_countN = number() -> 241
5270 +    {get_vhost_stats, 1},
5271 +
5272 +    % called from ejabberd_web_admin:vhost_messages_stats_at/4
5273 +    % it should return sorted list of count of messages by users at date for vhost
5274 +    % get_vhost_stats_at(VHost, Date) -> {ok, [{User1, Msgs_count1}, {User2, Msgs_count2}, ....]} |
5275 +    %                                    {error, Reason}
5276 +    %    Types:
5277 +    %          VHost = list() -> "jabber.example.org"
5278 +    %          Date = list() -> "2007-02-12"
5279 +    %          UserN = list() -> "admin"
5280 +    %          Msgs_countN = number() -> 241
5281 +    {get_vhost_stats_at, 2},
5282 +
5283 +    % called from ejabberd_web_admin:user_messages_stats/4
5284 +    % it should return sorted list of count of messages by date for user at vhost
5285 +    % get_user_stats(User, VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ...]} |
5286 +    %                                {error, Reason}
5287 +    %    Types:
5288 +    %          User = list() -> "admin"
5289 +    %          VHost = list() -> "jabber.example.org"
5290 +    %          DateN = list() -> "2007-02-12"
5291 +    %          Msgs_countN = number() -> 241
5292 +    {get_user_stats, 2},
5293 +
5294 +    % called from ejabberd_web_admin:user_messages_stats_at/5
5295 +    % it should return all user messages at date
5296 +    % get_user_messages_at(User, VHost, Date) -> {ok, Msgs} | {error, Reason}
5297 +    %    Types:
5298 +    %          User = list() -> "admin"
5299 +    %          VHost = list() -> "jabber.example.org"
5300 +    %          Date = list() -> "2007-02-12"
5301 +    %          Msgs = list() -> [ #msg1, msg2, ... ]
5302 +    {get_user_messages_at, 3},
5303 +
5304 +    % called from many places
5305 +    % it should return list of dates for vhost
5306 +    % get_dates(VHost) -> [Date1, Date2, ... ]
5307 +    %    Types:
5308 +    %          VHost = list() -> "jabber.example.org"
5309 +    %          DateN = list() -> "2007-02-12"
5310 +    {get_dates, 1},
5311 +
5312 +    % called from start
5313 +    % it should return list with users settings for VHost in db
5314 +    % get_users_settings(VHost) -> [#user_settings1, #user_settings2, ... ] | error
5315 +    %    Types:
5316 +    %          VHost = list() -> "jabber.example.org"
5317 +    %          User = list() -> "admin"
5318 +    {get_users_settings, 1},
5319 +
5320 +    % called from many places
5321 +    % it should return User settings at VHost from db
5322 +    % get_user_settings(User, VHost) -> error | {ok, #user_settings}
5323 +    %    Types:
5324 +    %          User = list() -> "admin"
5325 +    %          VHost = list() -> "jabber.example.org"
5326 +    {get_user_settings, 2},
5327 +
5328 +    % called from web admin
5329 +    % it should set User settings at VHost
5330 +    % set_user_settings(User, VHost, #user_settings) -> ok | error
5331 +    %    Types:
5332 +    %          User = list() -> "admin"
5333 +    %          VHost = list() -> "jabber.example.org"
5334 +    {set_user_settings, 3}
5335 +   ];
5336 +behaviour_info(_) ->
5337 +   undefined.
5338 --- src/web/ejabberd_web_admin-1.1.4.erl        Tue Dec 11 13:25:24 2007
5339 +++ src/web/ejabberd_web_admin.erl      Fri Jul 27 09:19:48 2007
5340 @@ -21,6 +21,7 @@
5341  -include("ejabberd.hrl").
5342  -include("jlib.hrl").
5343  -include("ejabberd_http.hrl").
5344 +-include("mod_logdb.hrl").
5345  
5346  -define(X(Name), {xmlelement, Name, [], []}).
5347  -define(XA(Name, Attrs), {xmlelement, Name, Attrs, []}).
5348 @@ -46,6 +47,11 @@
5349         ?XA("input", [{"type", Type},
5350                       {"name", Name},
5351                       {"value", Value}])).
5352 +-define(INPUTC(Type, Name, Value),
5353 +        ?XA("input", [{"type", Type},
5354 +                      {"name", Name},
5355 +                      {"value", Value},
5356 +                      {"checked", "true"}])).
5357  -define(INPUTT(Type, Name, Value), ?INPUT(Type, Name, ?T(Value))).
5358  -define(INPUTS(Type, Name, Value, Size),
5359         ?XA("input", [{"type", Type},
5360 @@ -137,6 +143,12 @@
5361                                     [?LI([?ACT(Base ++ "shared-roster/", "Shared Roster")])];
5362                                 false ->
5363                                     []
5364 +                           end ++
5365 +                           case gen_mod:is_loaded(Host, mod_logdb) of
5366 +                               true ->
5367 +                                   [?LI([?ACT(Base ++ "messages/", "Users Messages")])];
5368 +                               false ->
5369 +                                   []
5370                             end
5371                            )]),
5372                   ?XAE("div",
5373 @@ -564,6 +576,12 @@
5374                             [?LI([?ACT(Base ++ "shared-roster/", "Shared Roster")])];
5375                         false ->
5376                             []
5377 +                   end ++
5378 +                   case gen_mod:is_loaded(Host, mod_logdb) of
5379 +                       true ->
5380 +                           [?LI([?ACT(Base ++ "messages/", "Users Messages")])];
5381 +                       false ->
5382 +                           []
5383                     end
5384                    )
5385                ], Host, Lang);
5386 @@ -925,6 +943,38 @@
5387      make_xhtml(Res, Host, Lang);
5388  
5389  process_admin(Host,
5390 +              #request{us = US,
5391 +                       path = ["messages"], 
5392 +                       q = Query, 
5393 +                       lang = Lang} = Request) when is_list(Host) ->
5394 +    Res = vhost_messages_stats(Host, Query, Lang),
5395 +    make_xhtml(Res, Host, Lang);
5396 +
5397 +process_admin(Host,
5398 +              #request{us = US,
5399 +                       path = ["messages", Date],
5400 +                       q = Query,
5401 +                       lang = Lang} = Request) when is_list(Host) ->
5402 +    Res = vhost_messages_stats_at(Host, Query, Lang, Date),
5403 +    make_xhtml(Res, Host, Lang);
5404 +
5405 +process_admin(Host,
5406 +              #request{us = US,
5407 +                       path = ["user", U, "messages"],
5408 +                       q = Query,
5409 +                       lang = Lang} = Request) ->
5410 +    Res = user_messages_stats(U, Host, Query, Lang),
5411 +    make_xhtml(Res, Host, Lang);
5412 +
5413 +process_admin(Host,
5414 +              #request{us = US,
5415 +                       path = ["user", U, "messages", Date],
5416 +                       q = Query,
5417 +                       lang = Lang} = Request) ->
5418 +    Res = user_messages_stats_at(U, Host, Query, Lang, Date),
5419 +    make_xhtml(Res, Host, Lang);
5420 +
5421 +process_admin(Host,
5422               #request{us = US,
5423                        path = ["user", U, "roster"],
5424                        q = Query,
5425 @@ -1442,6 +1492,22 @@
5426               [?XCT("h3", "Password:")] ++ FPassword ++
5427               [?XCT("h3", "Offline Messages:")] ++ FQueueLen ++
5428               [?XE("h3", [?ACT("roster/", "Roster")])] ++
5429 +             case gen_mod:is_loaded(Server, mod_logdb) of
5430 +               true ->
5431 +                   Sett = mod_logdb:get_user_settings(User, Server),
5432 +                   Log =
5433 +                     case Sett#user_settings.dolog_default of
5434 +                          false ->
5435 +                             ?INPUTT("submit", "dolog", "Log Messages");
5436 +                          true -> 
5437 +                             ?INPUTT("submit", "donotlog", "Do Not Log Messages");
5438 +                          _ -> []
5439 +                     end,
5440 +                  [?XE("h3", [?ACT("messages/", "Messages"), ?C(" "), Log])];
5441 +                   %[?INPUT("test", "test", "test"), ?C(" "), Log];
5442 +               false ->
5443 +                  []
5444 +             end ++
5445               [?BR, ?INPUTT("submit", "removeuser", "Remove User")])].
5446  
5447  
5448 @@ -1462,8 +1528,24 @@
5449                 {value, _} ->
5450                     ejabberd_auth:remove_user(User, Server),
5451                     ok;
5452 -               false ->
5453 -                   nothing
5454 +               _ ->
5455 +                   case lists:keysearch("dolog", 1, Query) of
5456 +                         {value, _} ->
5457 +                           Sett = mod_logdb:get_user_settings(User, Server),
5458 +                           % TODO: check returned value
5459 +                           mod_logdb:set_user_settings(User, Server, Sett#user_settings{dolog_default=true}),
5460 +                           nothing;
5461 +                         _ ->
5462 +                           case lists:keysearch("donotlog", 1, Query) of
5463 +                                {value, _} ->
5464 +                                    Sett = mod_logdb:get_user_settings(User, Server),
5465 +                                    % TODO: check returned value
5466 +                                    mod_logdb:set_user_settings(User, Server, Sett#user_settings{dolog_default=false}),
5467 +                                    nothing;
5468 +                                false ->
5469 +                                    nothing
5470 +                           end
5471 +                    end
5472             end
5473      end.
5474  
5475 @@ -1574,6 +1656,14 @@
5476      Res = user_roster_parse_query(User, Server, Items1, Query, Admin),
5477      Items = mnesia:dirty_index_read(roster, US, #roster.us),
5478      SItems = lists:sort(Items),
5479 +
5480 +    Settings = case gen_mod:is_loaded(Server, mod_logdb) of
5481 +         true ->
5482 +             mod_logdb:get_user_settings(User, Server);
5483 +         false ->
5484 +             []
5485 +    end,
5486 +
5487      FItems =
5488         case SItems of
5489             [] ->
5490 @@ -1621,7 +1711,33 @@
5491                                               [?INPUTT("submit",
5492                                                        "remove" ++
5493                                                        term_to_id(R#roster.jid),
5494 -                                                      "Remove")])])
5495 +                                                      "Remove")]),
5496 +                                         case gen_mod:is_loaded(Server, mod_logdb) of
5497 +                                              true ->
5498 +                                                 Peer = jlib:jid_to_string(R#roster.jid),
5499 +                                                 A = lists:member(Peer, Settings#user_settings.dolog_list),
5500 +                                                 B = lists:member(Peer, Settings#user_settings.donotlog_list),
5501 +                                                 {Name, Value} =
5502 +                                                   if
5503 +                                                     A ->
5504 +                                                       {"donotlog", "Do Not Log Messages"};
5505 +                                                     B ->
5506 +                                                       {"dolog", "Log Messages"};
5507 +                                                     Settings#user_settings.dolog_default == true ->
5508 +                                                       {"donotlog", "Do Not Log Messages"};
5509 +                                                     Settings#user_settings.dolog_default == false ->
5510 +                                                       {"dolog", "Log Messages"}
5511 +                                                   end,
5512 +
5513 +                                                 ?XAE("td", [{"class", "valign"}],
5514 +                                                      [?INPUTT("submit",
5515 +                                                               Name ++
5516 +                                                               term_to_id(R#roster.jid),
5517 +                                                               Value)]);
5518 +                                              false ->
5519 +                                                 ?X([])
5520 +                                         end
5521 +                                        ])
5522                             end, SItems))])]
5523         end,
5524      [?XC("h1", ?T("Roster of ") ++ us_to_list(US))] ++
5525 @@ -1637,6 +1753,288 @@
5526                ?INPUTT("submit", "addjid", "Add Jabber ID")
5527               ])].
5528  
5529 +vhost_messages_stats(Server, Query, Lang) ->
5530 +    Res = case catch mod_logdb:vhost_messages_parse_query(Server, Query) of
5531 +                     {'EXIT', Reason} -> 
5532 +                         ?ERROR_MSG("~p", [Reason]),
5533 +                         error;
5534 +                     VResult -> VResult
5535 +          end,
5536 +    {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]),
5537 +    ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]),
5538 +    %case mod_logdb:get_vhost_stats(Server) of
5539 +    case Value of
5540 +         {'EXIT', CReason} ->
5541 +              ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
5542 +              [?XC("h1", ?T("Error occupied while fetching list"))];
5543 +         {error, GReason} ->
5544 +              ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
5545 +              [?XC("h1", ?T("Error occupied while fetching list"))];
5546 +         {ok, []} ->
5547 +              [?XC("h1", ?T("No logged messages for ") ++ Server)];
5548 +         {ok, Dates} ->
5549 +              Fun = fun({Date, Count}) ->
5550 +                         ID = jlib:encode_base64(binary_to_list(term_to_binary(Server++Date))),
5551 +                         ?XE("tr",
5552 +                          [?XE("td", [?INPUT("checkbox", "selected", ID)]),
5553 +                           ?XE("td", [?AC(Date, Date)]),
5554 +                           ?XC("td", integer_to_list(Count))
5555 +                          ])
5556 +                    end,
5557 +              [?XC("h1", ?T("Logged messages for ") ++ Server)] ++
5558 +               case Res of
5559 +                    ok -> [?CT("Submitted"), ?P];
5560 +                    error -> [?CT("Bad format"), ?P];
5561 +                    nothing -> []
5562 +               end ++
5563 +               [?XAE("form", [{"action", ""}, {"method", "post"}],
5564 +                [?XE("table",
5565 +                 [?XE("thead",
5566 +                  [?XE("tr",
5567 +                   [?X("td"),
5568 +                    ?XCT("td", "Date"),
5569 +                    ?XCT("td", "Count")
5570 +                   ])]),
5571 +                  ?XE("tbody",
5572 +                      lists:map(Fun, Dates)
5573 +                     )]),
5574 +                  ?BR,
5575 +                  ?INPUTT("submit", "delete", "Delete Selected")
5576 +                ])]
5577 +   end.
5578 +
5579 +vhost_messages_stats_at(Server, Query, Lang, Date) ->
5580 +   {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]),
5581 +   ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]),
5582 +   %case mod_logdb:get_vhost_stats_at(Server, Date) of
5583 +   case Value of
5584 +        {'EXIT', CReason} ->
5585 +             ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
5586 +             [?XC("h1", ?T("Error occupied while fetching list"))];
5587 +        {error, GReason} ->
5588 +             ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
5589 +             [?XC("h1", ?T("Error occupied while fetching list"))];
5590 +        {ok, []} ->
5591 +             [?XC("h1", ?T("No logged messages for ") ++ Server ++ ?T(" at ") ++ Date)];
5592 +        {ok, Users} ->
5593 +             Res = case catch mod_logdb:vhost_messages_at_parse_query(Server, Date, Users, Query) of
5594 +                        {'EXIT', Reason} ->
5595 +                            ?ERROR_MSG("~p", [Reason]),
5596 +                            error;
5597 +                        VResult -> VResult
5598 +                   end,
5599 +             Fun = fun({User, Count}) ->
5600 +                         ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Server))),
5601 +                         ?XE("tr",
5602 +                          [?XE("td", [?INPUT("checkbox", "selected", ID)]),
5603 +                           ?XE("td", [?AC("../user/"++User++"/messages/"++Date, User)]),
5604 +                           ?XC("td", integer_to_list(Count))
5605 +                          ])
5606 +                   end,
5607 +             [?XC("h1", ?T("Logged messages for ") ++ Server ++ ?T(" at ") ++ Date)] ++
5608 +              case Res of
5609 +                    ok -> [?CT("Submitted"), ?P];
5610 +                    error -> [?CT("Bad format"), ?P];
5611 +                    nothing -> []
5612 +              end ++
5613 +              [?XAE("form", [{"action", ""}, {"method", "post"}],
5614 +                [?XE("table",
5615 +                 [?XE("thead",
5616 +                  [?XE("tr",
5617 +                   [?X("td"),
5618 +                    ?XCT("td", "User"),
5619 +                    ?XCT("td", "Count")
5620 +                   ])]),
5621 +                  ?XE("tbody",
5622 +                      lists:map(Fun, Users)
5623 +                     )]),
5624 +                  ?BR,
5625 +                  ?INPUTT("submit", "delete", "Delete Selected")
5626 +                ])]
5627 +   end.
5628 +
5629 +user_messages_stats(User, Server, Query, Lang) ->
5630 +    US = {jlib:nodeprep(User), jlib:nameprep(Server)},
5631 +    Jid = us_to_list(US),
5632 +
5633 +    Res = case catch mod_logdb:user_messages_parse_query(User, Server, Query) of
5634 +               {'EXIT', Reason} -> 
5635 +                    ?ERROR_MSG("~p", [Reason]),
5636 +                    error;
5637 +               VResult -> VResult
5638 +          end,
5639 +
5640 +   {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]),
5641 +   ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]),
5642 +
5643 +   case Value of
5644 +        {'EXIT', CReason} ->
5645 +            ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
5646 +            [?XC("h1", ?T("Error occupied while fetching days"))];
5647 +        {error, GReason} ->
5648 +            ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
5649 +            [?XC("h1", ?T("Error occupied while fetching days"))];
5650 +        {ok, []} ->
5651 +            [?XC("h1", ?T("No logged messages for ") ++ Jid)];
5652 +        {ok, Dates} ->
5653 +            Fun = fun({Date, Count}) ->
5654 +                      ID = jlib:encode_base64(binary_to_list(term_to_binary(User++Date))),
5655 +                      ?XE("tr",
5656 +                       [?XE("td", [?INPUT("checkbox", "selected", ID)]),
5657 +                        ?XE("td", [?AC(Date, Date)]),
5658 +                        ?XC("td", integer_to_list(Count))
5659 +                       ])
5660 +                       %[?AC(Date, Date ++ " (" ++ integer_to_list(Count) ++ ")"), ?BR]
5661 +                  end,
5662 +            [?XC("h1", ?T("Logged messages for ") ++ Jid)] ++
5663 +             case Res of
5664 +                   ok -> [?CT("Submitted"), ?P];
5665 +                   error -> [?CT("Bad format"), ?P];
5666 +                   nothing -> []
5667 +             end ++
5668 +             [?XAE("form", [{"action", ""}, {"method", "post"}],
5669 +              [?XE("table",
5670 +               [?XE("thead",
5671 +                [?XE("tr",
5672 +                 [?X("td"),
5673 +                  ?XCT("td", "Date"),
5674 +                  ?XCT("td", "Count")
5675 +                 ])]),
5676 +                ?XE("tbody",
5677 +                    lists:map(Fun, Dates)
5678 +                   )]),
5679 +                ?BR,
5680 +                ?INPUTT("submit", "delete", "Delete Selected")
5681 +              ])]
5682 +    end.
5683 +
5684 +user_messages_stats_at(User, Server, Query, Lang, Date) ->
5685 +   US = {jlib:nodeprep(User), jlib:nameprep(Server)},
5686 +   Jid = us_to_list(US),
5687 +
5688 +   {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]),
5689 +   ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]),
5690 +   case Value of
5691 +        {'EXIT', CReason} ->
5692 +           ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
5693 +           [?XC("h1", ?T("Error occupied while fetching messages"))];
5694 +        {error, GReason} ->
5695 +           ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
5696 +           [?XC("h1", ?T("Error occupied while fetching messages"))];
5697 +        {ok, []} ->
5698 +           [?XC("h1", ?T("No logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)];
5699 +        {ok, User_messages} ->
5700 +           Res =  case catch mod_logdb:user_messages_at_parse_query(Server,
5701 +                                                                    Date,
5702 +                                                                    User_messages,
5703 +                                                                    Query) of
5704 +                       {'EXIT', Reason} ->
5705 +                            ?ERROR_MSG("~p", [Reason]),
5706 +                            error;
5707 +                       VResult -> VResult
5708 +                  end,
5709 +
5710 +           UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) ->
5711 +                                 case lists:member(PName++"@"++PServer, List) of
5712 +                                      true -> List;
5713 +                                      false -> lists:append([PName++"@"++PServer], List)
5714 +                                 end
5715 +                               end, [], User_messages),
5716 +
5717 +           % Users to filter (sublist of UniqUsers)
5718 +           CheckedUsers = case lists:keysearch("filter", 1, Query) of
5719 +                           {value, _} ->
5720 +                              lists:filter(fun(UFUser) ->
5721 +                                                ID = jlib:encode_base64(binary_to_list(term_to_binary(UFUser))),
5722 +                                                lists:member({"selected", ID}, Query)
5723 +                                           end, UniqUsers);
5724 +                           false -> []
5725 +                         end,
5726 +
5727 +           % UniqUsers in html (noone selected -> everyone selected)
5728 +           Users = lists:map(fun(UHUser) ->
5729 +                                ID = jlib:encode_base64(binary_to_list(term_to_binary(UHUser))),
5730 +                                Input = case lists:member(UHUser, CheckedUsers) of
5731 +                                         true -> [?INPUTC("checkbox", "selected", ID)];
5732 +                                         false when CheckedUsers == [] -> [?INPUTC("checkbox", "selected", ID)];
5733 +                                         false -> [?INPUT("checkbox", "selected", ID)]
5734 +                                        end,
5735 +                                ?XE("tr",
5736 +                                 [?XE("td", Input),
5737 +                                  ?XC("td", UHUser)])
5738 +                             end, lists:sort(UniqUsers)),
5739 +
5740 +           % Messages to show (based on Users)
5741 +           User_messages_filtered = case CheckedUsers of
5742 +                                         [] -> User_messages;
5743 +                                         _  -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) ->
5744 +                                                  lists:member(PName++"@"++PServer, CheckedUsers)
5745 +                                               end, User_messages)
5746 +                                    end,
5747 +
5748 +           Msgs_Fun = fun(#msg{timestamp=Timestamp,
5749 +                               subject=Subject,
5750 +                               direction=Direction,
5751 +                               peer_name=PName, peer_server=PServer, peer_resource=PRes,
5752 +                               body=Body}) ->
5753 +                      TextRaw = case Subject of
5754 +                                     "" -> Body;
5755 +                                     _ -> [?T("Subject"),": ",Subject,"<br>", Body]
5756 +                                end,
5757 +                      ID = jlib:encode_base64(binary_to_list(term_to_binary(Timestamp))),
5758 +                      % replace \n with <br>
5759 +                      Text = lists:map(fun(10) -> "<br>";
5760 +                                           (A) -> A
5761 +                                        end, TextRaw),
5762 +                      Resource = case PRes of
5763 +                                      [] -> [];
5764 +                                      undefined -> [];
5765 +                                      R -> "/" ++ R
5766 +                                 end,
5767 +                      ?XE("tr",
5768 +                       [?XE("td", [?INPUT("checkbox", "selected", ID)]),
5769 +                        ?XC("td", mod_logdb:convert_timestamp(Timestamp)),
5770 +                        ?XC("td", atom_to_list(Direction)++": "++PName++"@"++PServer++Resource),
5771 +                        ?XC("td", Text)])
5772 +                 end,
5773 +           % Filtered user messages in html
5774 +           Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
5775 +
5776 +           [?XC("h1", ?T("Logged messages for ") ++ Jid ++ ?T(" at ") ++ Date)] ++
5777 +            case Res of
5778 +                 ok -> [?CT("Submitted"), ?P];
5779 +                 error -> [?CT("Bad format"), ?P];
5780 +                 nothing -> []
5781 +            end ++
5782 +            [?XAE("form", [{"action", ""}, {"method", "post"}],
5783 +             [?XE("table",
5784 +                  [?XE("thead",
5785 +                       [?X("td"),
5786 +                        ?XCT("td", "User")
5787 +                       ]
5788 +                      ),
5789 +                   ?XE("tbody",
5790 +                        Users
5791 +                      )]),
5792 +              ?INPUTT("submit", "filter", "Filter Selected")
5793 +             ] ++
5794 +             [?XE("table",
5795 +                  [?XE("thead",
5796 +                       [?XE("tr",
5797 +                        [?X("td"),
5798 +                         ?XCT("td", "Date, Time"),
5799 +                         ?XCT("td", "Direction: Jid"),
5800 +                         ?XCT("td", "Body")
5801 +                        ])]),
5802 +                   ?XE("tbody",
5803 +                        Msgs
5804 +                      )]),
5805 +              ?INPUTT("submit", "delete", "Delete Selected"),
5806 +              ?BR
5807 +             ]
5808 +            )]
5809 +    end.
5810 +
5811  user_roster_parse_query(User, Server, Items, Query, Admin) ->
5812      case lists:keysearch("addjid", 1, Query) of
5813         {value, _} ->
5814 @@ -1704,10 +2102,41 @@
5815                                                 []}]}}),
5816                               throw(submitted);
5817                           false ->
5818 -                             ok
5819 -                     end
5820 -
5821 -             end
5822 +                            case lists:keysearch(
5823 +                                   "donotlog" ++ term_to_id(JID), 1, Query) of
5824 +                                {value, _} ->
5825 +                                     Peer = jlib:jid_to_string(JID),
5826 +                                     Settings = mod_logdb:get_user_settings(User, Server),
5827 +                                     DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of
5828 +                                                 false -> lists:append(Settings#user_settings.donotlog_list, [Peer]);
5829 +                                                 true -> Settings#user_settings.donotlog_list
5830 +                                            end,
5831 +                                     DLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.dolog_list),
5832 +                                     Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
5833 +                                     % TODO: check returned value
5834 +                                     ok = mod_logdb:set_user_settings(User, Server, Sett),
5835 +                                     throw(nothing);
5836 +                                false ->
5837 +                                   case lists:keysearch(
5838 +                                         "dolog" ++ term_to_id(JID), 1, Query) of
5839 +                                      {value, _} ->
5840 +                                          Peer = jlib:jid_to_string(JID),
5841 +                                          Settings = mod_logdb:get_user_settings(User, Server),
5842 +                                          DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of
5843 +                                                     false -> lists:append(Settings#user_settings.dolog_list, [Peer]);
5844 +                                                     true -> Settings#user_settings.dolog_list
5845 +                                                end,
5846 +                                          DNLL = lists:delete(jlib:jid_to_string(JID), Settings#user_settings.donotlog_list),
5847 +                                          Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
5848 +                                          % TODO: check returned value
5849 +                                          ok = mod_logdb:set_user_settings(User, Server, Sett),
5850 +                                          throw(nothing);
5851 +                                      false ->
5852 +                                          ok
5853 +                                  end % dolog
5854 +                            end % donotlog
5855 +                     end % remove
5856 +             end % validate
5857        end, Items),
5858      nothing.
5859  
5860 --- src/mod_muc/mod_muc_room-1.1.4.erl  Tue Dec 11 13:26:10 2007
5861 +++ src/mod_muc/mod_muc_room.erl        Tue Dec 11 14:21:59 2007
5862 @@ -652,6 +652,12 @@
5863                     false
5864             end,
5865      {reply, Reply, StateName, StateData};
5866 +handle_sync_event({get_jid_nick, Jid}, _From, StateName, StateData) ->
5867 +    R = case ?DICT:find(jlib:jid_tolower(Jid), StateData#state.users) of
5868 +             error -> [];
5869 +             {ok, {user, _, Nick, _, _}} -> Nick
5870 +        end,
5871 +    {reply, R, StateName, StateData};
5872  handle_sync_event(_Event, _From, StateName, StateData) ->
5873      Reply = ok,
5874      {reply, Reply, StateName, StateData}.
5875 --- src/msgs/uk-1.1.4.msg       Tue Dec 11 14:15:44 2007
5876 +++ src/msgs/uk.msg     Tue Dec 11 14:23:19 2007
5877 @@ -372,6 +372,32 @@
5878  {"ejabberd virtual hosts", "віртуальні хости ejabberd"}.
5879  {"Host", "Хост"}.
5880  
5881 +% mod_logdb
5882 +{"Users Messages", "Повідомлення користувачів"}.
5883 +{"Date", "Дата"}.
5884 +{"Count", "Кількість"}.
5885 +{"Logged messages for ", "Збережені повідомлення для "}.
5886 +{" at ", " за "}.
5887 +{"No logged messages for ", "Відсутні повідомлення для "}.
5888 +{"Date, Time", "Дата, Час"}.
5889 +{"Direction: Jid", "Напрямок: Jid"}.
5890 +{"Subject", "Тема"}.
5891 +{"Body", "Текст"}.
5892 +{"Messages", "Повідомлення"}.
5893 +{"Filter Selected", "Відфільтрувати виділені"}.
5894 +{"Do Not Log Messages", "Не зберігати повідомлення"}.
5895 +{"Log Messages", "Зберігати повідомлення"}.
5896 +{"Messages logging engine", "Система збереження повідомлень"}.
5897 +{"Default", "За замовчуванням"}.
5898 +{"Set logging preferences", "Вкажіть налагоджування збереження повідомлень"}.
5899 +{"Messages logging engine users", "Користувачі системи збереження повідомлень"}.
5900 +{"Messages logging engine settings", "Налагоджування системи збереження повідомлень"}.
5901 +{"Set run-time settings", "Вкажіть поточні налагоджування"}.
5902 +{"Groupchat messages logging", "Збереження повідомлень типу groupchat"}.
5903 +{"Jids/Domains to ignore", "Ігнорувати наступні jids/домени"}.
5904 +{"Purge messages older than (days)", "Видаляти повідомлення старіші ніж (дні)"}.
5905 +{"Poll users settings (seconds)", "Оновлювати налагоджування користувачів кожні (секунд)"}.
5906 +
5907  % Local Variables:
5908  % mode: erlang
5909  % End:
5910 --- src/msgs/ru-1.1.4.msg       Tue Dec 11 14:15:51 2007
5911 +++ src/msgs/ru.msg     Tue Dec 11 14:23:19 2007
5912 @@ -372,6 +372,32 @@
5913  {"ejabberd virtual hosts", "Виртуальные хосты ejabberd"}.
5914  {"Host", "Хост"}.
5915  
5916 +% mod_logdb.erl
5917 +{"Users Messages", "Сообщения пользователей"}.
5918 +{"Date", "Дата"}.
5919 +{"Count", "Количество"}.
5920 +{"Logged messages for ", "Сохранённые cообщения для "}.
5921 +{" at ", " за "}.
5922 +{"No logged messages for ", "Отсутствуют сообщения для "}.
5923 +{"Date, Time", "Дата, Время"}.
5924 +{"Direction: Jid", "Направление: Jid"}.
5925 +{"Subject", "Тема"}.
5926 +{"Body", "Текст"}.
5927 +{"Messages", "Сообщения"}.
5928 +{"Filter Selected", "Отфильтровать выделенные"}.
5929 +{"Do Not Log Messages", "Не сохранять сообщения"}.
5930 +{"Log Messages", "Сохранять сообщения"}.
5931 +{"Messages logging engine", "Система логирования сообщений"}.
5932 +{"Default", "По умолчанию"}.
5933 +{"Set logging preferences", "Задайте настройки логирования"}.
5934 +{"Messages logging engine users", "Пользователи системы логирования сообщений"}.
5935 +{"Messages logging engine settings", "Настройки системы логирования сообщений"}.
5936 +{"Set run-time settings", "Задайте текущие настройки"}.
5937 +{"Groupchat messages logging", "Логирование сообщений типа groupchat"}.
5938 +{"Jids/Domains to ignore", "Игнорировать следующие jids/домены"}.
5939 +{"Purge messages older than (days)", "Удалять сообщения старее чем (дни)"}.
5940 +{"Poll users settings (seconds)", "Обновлять настройки пользователей через (секунд)"}.
5941 +
5942  % Local Variables:
5943  % mode: erlang
5944  % End:
5945 --- src/msgs/nl-1.1.4.msg       Tue Dec 11 14:15:58 2007
5946 +++ src/msgs/nl.msg     Thu Apr 26 16:04:49 2007
5947 @@ -331,4 +331,15 @@
5948  {"Members:", "Groepsleden:"}.
5949  {"Displayed Groups:", "Weergegeven groepen:"}.
5950  {"Group ", "Groep "}.
5951 +{"Users Messages", "Gebruikersberichten"}.
5952 +{"Date", "Datum"}.
5953 +{"Count", "Aantal"}.
5954 +{"Logged messages for ", "Gelogde berichten van "}.
5955 +{" at ", " op "}.
5956 +{"No logged messages for ", "Geen gelogde berichten van "}.
5957 +{"Date, Time", "Datum en tijd"}.
5958 +{"Direction: Jid", "Richting: Jabber ID"}.
5959 +{"Subject", "Onderwerp"}.
5960 +{"Body", "Berichtveld"}.
5961 +{"Messages", "Berichten"}.
5962  
5963 --- src/msgs/pl-1.1.4.msg       Tue Dec 11 14:16:04 2007
5964 +++ src/msgs/pl.msg     Thu Sep  6 09:52:55 2007
5965 @@ -423,3 +423,27 @@
5966  % ./mod_muc/mod_muc.erl
5967  {"ejabberd MUC module\nCopyright (c) 2003-2006 Alexey Shchepin", ""}.
5968  
5969 +% mod_logdb
5970 +{"Users Messages", "Wiadomości użytkownika"}.
5971 +{"Date", "Data"}.
5972 +{"Count", "Liczba"}.
5973 +{"Logged messages for ", "Zapisane wiadomości dla "}.
5974 +{" at ", " o "}.
5975 +{"No logged messages for ", "Brak zapisanych wiadomości dla "}.
5976 +{"Date, Time", "Data, Godzina"}.
5977 +{"Direction: Jid", "Kierunek: Jid"}.
5978 +{"Subject", "Temat"}.
5979 +{"Body", "Treść"}.
5980 +{"Messages","Wiadomości"}.
5981 +{"Filter Selected", "Odfiltruj zaznaczone"}.
5982 +{"Do Not Log Messages", "Nie zapisuj wiadomości"}.
5983 +{"Log Messages", "Zapisuj wiadomości"}.
5984 +{"Messages logging engine", "System zapisywania historii rozmów"}.
5985 +{"Default", "Domyślne"}.
5986 +{"Set logging preferences", "Ustaw preferencje zapisywania"}.
5987 +{"Messages logging engine settings", "Ustawienia systemu logowania"}.
5988 +{"Set run-time settings", "Zapisz ustawienia systemu logowania"}.
5989 +{"Groupchat messages logging", "Zapisywanie rozmów z konferencji"}.
5990 +{"Jids/Domains to ignore", "JID/Domena która ma być ignorowana"}.
5991 +{"Purge messages older than (days)", "Usuń wiadomości starsze niż (w dniach)"}.
5992 +{"Poll users settings (seconds)", "Czas aktualizacji preferencji użytkowników (sekundy)"}.
This page took 0.460694 seconds and 4 git commands to generate.