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