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