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