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