]> git.pld-linux.org Git - packages/ejabberd.git/blame - ejabberd-mod_logdb.patch
- rel 1; logdb works
[packages/ejabberd.git] / ejabberd-mod_logdb.patch
CommitLineData
dd02533f
AM
1From 9a2ed8d2b20ef052b71b065e686cc049d18999ac Mon Sep 17 00:00:00 2001
2From: Oleh Palii <o.palij@gmail.com>
3Date: Sat, 31 Aug 2019 11:04:57 +0300
4Subject: [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
046546ef 29diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg
dd02533f 30index 8009d529ff..fd3d3b0ebb 100644
046546ef
AM
31--- a/priv/msgs/nl.msg
32+++ b/priv/msgs/nl.msg
08278fc0
AM
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."}.
046546ef
AM
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."}.
046546ef 37+% mod_logdb
dd02533f
AM
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"}.
046546ef 43+{" at ", " op "}.
dd02533f
AM
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"}.
046546ef 51diff --git a/priv/msgs/pl.msg b/priv/msgs/pl.msg
dd02533f 52index 2ca75b259c..fffae5742e 100644
046546ef
AM
53--- a/priv/msgs/pl.msg
54+++ b/priv/msgs/pl.msg
08278fc0 55@@ -444,3 +444,29 @@
9682d4d5 56 {"You're not allowed to create nodes","Nie masz uprawnień do tworzenia węzłów"}.
08278fc0
AM
57 {"Your Jabber account was successfully created.","Twoje konto zostało stworzone."}.
58 {"Your Jabber account was successfully deleted.","Twoje konto zostało usunięte."}.
046546ef
AM
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)"}.
85diff --git a/priv/msgs/ru.msg b/priv/msgs/ru.msg
dd02533f 86index f7dff97ea1..42be5d4f15 100644
046546ef
AM
87--- a/priv/msgs/ru.msg
88+++ b/priv/msgs/ru.msg
08278fc0
AM
89@@ -507,3 +507,33 @@
90 {"Your Jabber account was successfully created.","Ваш Jabber-аккаунт был успешно создан."}.
046546ef 91 {"Your Jabber account was successfully deleted.","Ваш Jabber-аккаунт был успешно удален."}.
08278fc0 92 {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ваши запросы на добавление в контакт-лист, а также сообщения к ~s блокируются. Для снятия блокировки перейдите по ссылке ~s"}.
046546ef
AM
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", "Удалять сообщения при удалении пользователя"}.
123diff --git a/priv/msgs/uk.msg b/priv/msgs/uk.msg
dd02533f 124index 0fbc336d51..c0b90047fa 100644
046546ef
AM
125--- a/priv/msgs/uk.msg
126+++ b/priv/msgs/uk.msg
08278fc0
AM
127@@ -349,3 +349,33 @@
128 {"Your contact offline message queue is full. The message has been discarded.","Черга повідомлень, що не були доставлені, переповнена. Повідомлення не було збережено."}.
046546ef
AM
129 {"Your Jabber account was successfully created.","Ваш Jabber-акаунт було успішно створено."}.
130 {"Your Jabber account was successfully deleted.","Ваш Jabber-акаунт було успішно видалено."}.
046546ef
AM
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", "Видаляти повідомлення під час видалення користувача"}.
3f23be8e 161diff --git a/rebar.config b/rebar.config
dd02533f 162index e05fe84e6e..c3b87afd28 100644
3f23be8e
AM
163--- a/rebar.config
164+++ b/rebar.config
dd02533f
AM
165@@ -35,8 +35,8 @@
166 {mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.4"}}},
08278fc0
AM
167 {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.29"}}}},
168 {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.30"}}}},
3f23be8e 169- {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
08278fc0 170- {tag, "1.0.11"}}}},
3f23be8e 171+ {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/paleg/p1_mysql",
dd02533f 172+ {tag, "1.0.11_multi"}}}},
3f23be8e 173 {if_var_true, pgsql, {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql",
08278fc0 174 {tag, "1.1.8"}}}},
3f23be8e 175 {if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3",
046546ef 176diff --git a/src/gen_logdb.erl b/src/gen_logdb.erl
0d78319d 177new file mode 100644
dd02533f 178index 0000000000..8bad112969
0d78319d 179--- /dev/null
046546ef 180+++ b/src/gen_logdb.erl
3f23be8e 181@@ -0,0 +1,162 @@
0d78319d
AM
182+%%%----------------------------------------------------------------------
183+%%% File : gen_logdb.erl
3f23be8e 184+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
0d78319d 185+%%% Purpose : Describes generic behaviour for mod_logdb backends.
3f23be8e 186+%%% Url : https://paleg.github.io/mod_logdb/
0d78319d
AM
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.
046546ef 344diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
0d78319d 345new file mode 100644
dd02533f 346index 0000000000..bf0240d139
0d78319d 347--- /dev/null
046546ef 348+++ b/src/mod_logdb.erl
a815cc6c 349@@ -0,0 +1,1951 @@
f7ce3e3a 350+%%%----------------------------------------------------------------------
351+%%% File : mod_logdb.erl
3f23be8e 352+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
f7ce3e3a 353+%%% Purpose : Frontend for log user messages to db
3f23be8e 354+%%% Url : https://paleg.github.io/mod_logdb/
f7ce3e3a 355+%%%----------------------------------------------------------------------
356+
357+-module(mod_logdb).
358+-author('o.palij@gmail.com').
f7ce3e3a 359+
360+-behaviour(gen_server).
361+-behaviour(gen_mod).
362+
363+% supervisor
364+-export([start_link/2]).
365+% gen_mod
3f23be8e
AM
366+-export([start/2, stop/1,
367+ mod_opt_type/1,
368+ depends/2, reload/3]).
f7ce3e3a 369+% gen_server
3f23be8e
AM
370+-export([code_change/3,
371+ handle_call/3, handle_cast/2, handle_info/2,
372+ init/1, terminate/2]).
f7ce3e3a 373+% hooks
3f23be8e 374+-export([send_packet/1, receive_packet/1, offline_message/1, remove_user/2]).
f7ce3e3a 375+-export([get_local_identity/5,
0d78319d 376+ get_local_features/5,
f7ce3e3a 377+ get_local_items/5,
378+ adhoc_local_items/4,
379+ adhoc_local_commands/4
f7ce3e3a 380+ ]).
381+% ejabberdctl
3f23be8e 382+-export([rebuild_stats/1,
f7ce3e3a 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]).
234c6b10 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]).
f7ce3e3a 407+
408+-include("mod_logdb.hrl").
3f23be8e 409+-include("xmpp.hrl").
234c6b10 410+-include("mod_roster.hrl").
3f23be8e 411+-include("ejabberd_commands.hrl").
f7ce3e3a 412+-include("adhoc.hrl").
046546ef
AM
413+-include("ejabberd_web_admin.hrl").
414+-include("ejabberd_http.hrl").
415+-include("logger.hrl").
dd02533f 416+
f7ce3e3a 417+-define(PROCNAME, ejabberd_mod_logdb).
418+% gen_server call timeout
234c6b10 419+-define(CALL_TIMEOUT, 10000).
f7ce3e3a 420+
234c6b10 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}).
f7ce3e3a 422+
046546ef 423+ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ binary_to_list(VHost)).
f7ce3e3a 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
3f23be8e
AM
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.
f7ce3e3a 448+
449+% supervisor starts gen_server
450+start_link(VHost, Opts) ->
451+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
234c6b10 452+ {ok, Pid} = gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []),
453+ Pid ! start,
454+ {ok, Pid}.
f7ce3e3a 455+
456+init([VHost, Opts]) ->
457+ process_flag(trap_exit, true),
046546ef
AM
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,[]}]);
33af2120 461+ {value, _} -> DBsRaw
046546ef
AM
462+ end,
463+ VHostDB = gen_mod:get_opt(vhosts, Opts, fun(A) -> A end, [{VHost, mnesia}]),
3f23be8e 464+ % 10 is default because of using in clustered environment
046546ef 465+ PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, fun(A) -> A end, 10),
f7ce3e3a 466+
046546ef
AM
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,
f7ce3e3a 481+
33af2120 482+ ?MYDEBUG("Starting mod_logdb for '~s' with '~s' backend", [VHost, DBName]),
f7ce3e3a 483+
484+ DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)),
485+
f7ce3e3a 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,
046546ef
AM
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),
f7ce3e3a 496+ poll_users_settings=PollUsersSettings}}.
497+
26b6b0c9 498+cleanup(#state{vhost=VHost} = _State) ->
f7ce3e3a 499+ ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
500+
501+ %ets:delete(ets_settings_table(VHost)),
502+
234c6b10 503+ ejabberd_hooks:delete(remove_user, VHost, ?MODULE, remove_user, 90),
f7ce3e3a 504+ ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90),
505+ ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
3f23be8e
AM
506+ ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_message, 40),
507+
046546ef
AM
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),
046546ef
AM
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),
f7ce3e3a 513+
234c6b10 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+
f7ce3e3a 519+ ?MYDEBUG("Removed hooks for ~p", [VHost]),
520+
3f23be8e 521+ ejabberd_commands:unregister_commands(get_commands_spec()),
f7ce3e3a 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),
3f23be8e
AM
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}}].
f7ce3e3a 543+
bb18ce72
AM
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+
f7ce3e3a 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) ->
046546ef 575+ Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, binary_to_list(Date)),
f7ce3e3a 576+ {reply, Reply, State};
577+handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
046546ef 578+ Reply = DBMod:delete_all_messages_by_user_at(binary_to_list(User), VHost, binary_to_list(Date)),
f7ce3e3a 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) ->
046546ef 587+ Reply = DBMod:get_vhost_stats_at(VHost, binary_to_list(Date)),
f7ce3e3a 588+ {reply, Reply, State};
589+handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
046546ef 590+ Reply = DBMod:get_user_stats(binary_to_list(User), VHost),
f7ce3e3a 591+ {reply, Reply, State};
592+handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
046546ef 593+ Reply = DBMod:get_user_messages_at(binary_to_list(User), VHost, binary_to_list(Date)),
f7ce3e3a 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] ->
f7ce3e3a 612+ ok;
613+ _ ->
046546ef 614+ case DBMod:set_user_settings(binary_to_list(User), VHost, Set) of
f7ce3e3a 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,
234c6b10 658+ drop_messages_on_user_removal=Settings#state.drop_messages_on_user_removal,
f7ce3e3a 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};
234c6b10 687+handle_cast({remove_user, User}, #state{dbmod=DBMod, vhost=VHost}=State) ->
688+ case State#state.drop_messages_on_user_removal of
689+ true ->
046546ef 690+ DBMod:drop_user(binary_to_list(User), VHost),
234c6b10 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};
f7ce3e3a 696+% ejabberdctl rebuild_stats/3
697+handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) ->
f7ce3e3a 698+ DBMod:rebuild_stats(VHost),
699+ {noreply, State};
700+handle_cast({copy_messages, Backend}, State) ->
3f23be8e 701+ spawn(?MODULE, copy_messages, [[State, Backend, []]]),
f7ce3e3a 702+ {noreply, State};
703+handle_cast({copy_messages, Backend, Date}, State) ->
3f23be8e 704+ spawn(?MODULE, copy_messages, [[State, Backend, [binary_to_list(Date)]]]),
f7ce3e3a 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
234c6b10 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} ->
f7ce3e3a 742+ timer:sleep(30000),
234c6b10 743+ ?ERROR_MSG("Failed to start: ~p", [Reason]),
f7ce3e3a 744+ {stop, db_connection_failed, State};
745+ {ok, SPid} ->
f7ce3e3a 746+ ?INFO_MSG("~p connection established", [DBMod]),
0d78319d 747+
f7ce3e3a 748+ MonRef = erlang:monitor(process, SPid),
749+
750+ ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]),
046546ef
AM
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,
f7ce3e3a 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+
234c6b10 760+ ejabberd_hooks:add(remove_user, VHost, ?MODULE, remove_user, 90),
f7ce3e3a 761+ ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90),
762+ ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
3f23be8e 763+ ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_message, 40),
f7ce3e3a 764+
3f23be8e 765+ ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
046546ef 766+ ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 50),
046546ef 767+ ejabberd_hooks:add(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
3f23be8e 768+ ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 50),
046546ef 769+ ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
f7ce3e3a 770+
234c6b10 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+
f7ce3e3a 776+ ?MYDEBUG("Added hooks for ~p", [VHost]),
777+
3f23be8e 778+ ejabberd_commands:register_commands(get_commands_spec()),
f7ce3e3a 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) ->
234c6b10 812+ ?MYDEBUG("Got terminate with db_connection_dropped", []),
f7ce3e3a 813+ cleanup(State),
814+ ok;
234c6b10 815+terminate(Reason, #state{monref=undefined} = State) ->
816+ ?MYDEBUG("Got terminate with undefined monref.~nReason: ~p", [Reason]),
f7ce3e3a 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+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
bb18ce72 835+%
f7ce3e3a 836+% ejabberd_hooks callbacks
bb18ce72 837+%
f7ce3e3a 838+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
839+% TODO: change to/from to list as sql stores it as list
3f23be8e
AM
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}) ->
f7ce3e3a 849+ VHost = Owner#jid.lserver,
3f23be8e
AM
850+ Peer = xmpp:get_from(Pkt),
851+ %?MYDEBUG("receive_packet. Pkt=~p", [Pkt]),
f7ce3e3a 852+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3f23be8e
AM
853+ gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
854+ {Pkt, C2SState}.
f7ce3e3a 855+
3f23be8e 856+offline_message({_Action, #message{from = Peer, to = Owner} = Pkt} = Acc) ->
f7ce3e3a 857+ VHost = Owner#jid.lserver,
3f23be8e 858+ %?MYDEBUG("offline_message. Pkt=~p", [Pkt]),
f7ce3e3a 859+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3f23be8e
AM
860+ gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
861+ Acc.
f7ce3e3a 862+
234c6b10 863+remove_user(User, Server) ->
3f23be8e
AM
864+ LUser = jid:nodeprep(User),
865+ LServer = jid:nameprep(Server),
234c6b10 866+ Proc = gen_mod:get_module_proc(LServer, ?PROCNAME),
867+ gen_server:cast(Proc, {remove_user, LUser}).
868+
f7ce3e3a 869+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
870+%
871+% ejabberdctl
872+%
873+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3f23be8e 874+rebuild_stats(VHost) ->
f7ce3e3a 875+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
876+ gen_server:cast(Proc, {rebuild_stats}),
3f23be8e 877+ ok.
f7ce3e3a 878+
3f23be8e 879+copy_messages_ctl(VHost, Backend, <<"all">>) ->
f7ce3e3a 880+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
881+ gen_server:cast(Proc, {copy_messages, Backend}),
3f23be8e
AM
882+ ok;
883+copy_messages_ctl(VHost, Backend, Date) ->
f7ce3e3a 884+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
885+ gen_server:cast(Proc, {copy_messages, Backend, Date}),
3f23be8e
AM
886+ ok.
887+
f7ce3e3a 888+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
889+%
890+% misc operations
891+%
892+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
893+
894+% handle_cast({addlog, E}, _)
895+% raw packet -> #msg
3f23be8e
AM
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,
f7ce3e3a 911+
3f23be8e
AM
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,
f7ce3e3a 922+
3f23be8e
AM
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.
f7ce3e3a 934+
935+% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages)
936+filter(Owner, Peer, State) ->
046546ef
AM
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 >>,
f7ce3e3a 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}] ->
046546ef
AM
947+
948+ A = lists:member(PeerBin, DLL),
949+ B = lists:member(PeerBin, DNLL),
f7ce3e3a 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
bb18ce72 958+ end,
0d78319d 959+ lists:all(fun(O) -> O end,
046546ef
AM
960+ [not lists:member(OwnerBin, State#state.ignore_jids),
961+ not lists:member(PeerBin, State#state.ignore_jids),
f7ce3e3a 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+
234c6b10 969+ Dates = ?MODULE:get_dates(VHost),
f7ce3e3a 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) ->
046546ef
AM
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}}),
f7ce3e3a 976+ if
977+ (DateNow - DateInSec) > DateDiff ->
978+ gen_server:call(Proc, {delete_messages_at, Date});
0d78319d 979+ true ->
f7ce3e3a 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}) ->
046546ef
AM
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 }
f7ce3e3a 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(),
dd02533f
AM
1006+ [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
1007+ List.
f7ce3e3a 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) ->
046546ef 1080+ case lists:keysearch(<<"delete">>, 1, Query) of
f7ce3e3a 1081+ {value, _} ->
1082+ PMsgs = lists:filter(
1083+ fun(Msg) ->
3f23be8e 1084+ ID = misc:encode_base64(term_to_binary(Msg#msg.timestamp)),
046546ef 1085+ lists:member({<<"selected">>, ID}, Query)
f7ce3e3a 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) ->
046546ef 1094+ case lists:keysearch(<<"delete">>, 1, Query) of
f7ce3e3a 1095+ {value, _} ->
234c6b10 1096+ Dates = get_dates(VHost),
f7ce3e3a 1097+ PDates = lists:filter(
1098+ fun(Date) ->
3f23be8e 1099+ ID = misc:encode_base64( << User/binary, (iolist_to_binary(Date))/binary >> ),
046546ef 1100+ lists:member({<<"selected">>, ID}, Query)
f7ce3e3a 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,
046546ef 1107+ {delete_all_messages_by_user_at, User, iolist_to_binary(Date)},
f7ce3e3a 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) ->
046546ef 1121+ case lists:keysearch(<<"delete">>, 1, Query) of
f7ce3e3a 1122+ {value, _} ->
234c6b10 1123+ Dates = get_dates(VHost),
f7ce3e3a 1124+ PDates = lists:filter(
1125+ fun(Date) ->
3f23be8e 1126+ ID = misc:encode_base64( << VHost/binary, (iolist_to_binary(Date))/binary >> ),
046546ef 1127+ lists:member({<<"selected">>, ID}, Query)
f7ce3e3a 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) ->
046546ef 1146+ case lists:keysearch(<<"delete">>, 1, Query) of
f7ce3e3a 1147+ {value, _} ->
1148+ PStats = lists:filter(
1149+ fun({User, _Count}) ->
3f23be8e 1150+ ID = misc:encode_base64( << (iolist_to_binary(User))/binary, VHost/binary >> ),
046546ef 1151+ lists:member({<<"selected">>, ID}, Query)
f7ce3e3a 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,
046546ef 1157+ iolist_to_binary(User), iolist_to_binary(Date)},
f7ce3e3a 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+
3f23be8e 1170+copy_messages([#state{vhost=VHost}=State, From, DatesIn]) ->
f7ce3e3a 1171+ {FromDBName, FromDBOpts} =
3f23be8e 1172+ case lists:keysearch(misc:binary_to_atom(From), 1, State#state.dbs) of
f7ce3e3a 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),
0d78319d 1183+
3f23be8e
AM
1184+ Dates = case DatesIn of
1185+ [] -> FromDBMod:get_dates(VHost);
1186+ _ -> DatesIn
1187+ end,
1188+
f7ce3e3a 1189+ DatesLength = length(Dates),
1190+
3f23be8e
AM
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]),
f7ce3e3a 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]),
0d78319d 1214+
3f23be8e 1215+ ok = FromDBMod:rebuild_stats_at(VHost, Date),
f7ce3e3a 1216+ catch mod_logdb:rebuild_stats_at(VHost, Date),
3f23be8e
AM
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
f7ce3e3a 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
3f23be8e
AM
1229+ % destination table is empty
1230+ ToStats == [] ->
f7ce3e3a 1231+ fun({User, _Count}, Acc) ->
3f23be8e 1232+ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
f7ce3e3a 1233+ MAcc =
1234+ lists:foldl(fun(Msg, MFAcc) ->
3f23be8e
AM
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),
f7ce3e3a 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;
3f23be8e
AM
1250+ % destination table is not empty
1251+ true ->
f7ce3e3a 1252+ fun({User, _Count}, Acc) ->
3f23be8e 1253+ {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date),
f7ce3e3a 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),
3f23be8e 1262+ {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
f7ce3e3a 1263+ MAcc =
1264+ lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) ->
1265+ case ets:member(mod_logdb_temp, ToTimestamp) of
1266+ false ->
3f23be8e
AM
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),
f7ce3e3a 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
3f23be8e
AM
1286+ end
1287+ end,
f7ce3e3a 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),
3f23be8e 1296+ ok = ToDBMod:rebuild_stats_at(VHost, Date)
f7ce3e3a 1297+ %timer:sleep(1000)
1298+ end,
1299+
1300+ ok.
1301+
046546ef
AM
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) ->
f7ce3e3a 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) ->
046546ef
AM
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),
f7ce3e3a 1330+ lists:sublist(Str, length(Str)-1).
1331+
1332+string_to_list(null) ->
1333+ [];
1334+string_to_list([]) ->
1335+ [];
1336+string_to_list(String) ->
046546ef 1337+ ejabberd_regexp:split(iolist_to_binary(String), <<"\n">>).
f7ce3e3a 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
3f23be8e 1346+ deny -> Fallback;
f7ce3e3a 1347+ allow ->
1348+ case get_local_items(LServer, LNode,
3f23be8e
AM
1349+ jid:encode(To), Lang) of
1350+ {result, Res} -> {result, Res};
1351+ {error, Error} -> {error, Error}
f7ce3e3a 1352+ end
1353+ end).
1354+
3f23be8e
AM
1355+get_local_items(Acc, From, #jid{lserver = LServer} = To,
1356+ <<"">>, Lang) ->
f7ce3e3a 1357+ case gen_mod:is_loaded(LServer, mod_adhoc) of
3f23be8e 1358+ false -> Acc;
f7ce3e3a 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, [],
3f23be8e 1369+ jid:encode(To), Lang) of
f7ce3e3a 1370+ {result, Res} ->
1371+ {result, Items ++ Res};
1372+ {error, _Error} ->
1373+ {result, Items}
1374+ end;
1375+ true ->
1376+ {result, Items}
1377+ end
1378+ end;
3f23be8e
AM
1379+get_local_items(Acc, From, #jid{lserver = LServer} = To,
1380+ Node, Lang) ->
f7ce3e3a 1381+ case gen_mod:is_loaded(LServer, mod_adhoc) of
3f23be8e 1382+ false -> Acc;
f7ce3e3a 1383+ _ ->
3f23be8e 1384+ LNode = tokenize(Node),
f7ce3e3a 1385+ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
3f23be8e 1386+ Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
f7ce3e3a 1387+ case LNode of
046546ef 1388+ [<<"mod_logdb">>] ->
3f23be8e 1389+ ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
046546ef 1390+ [<<"mod_logdb_users">>] ->
3f23be8e 1391+ ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
046546ef 1392+ [<<"mod_logdb_users">>, <<$@, _/binary>>] ->
3f23be8e 1393+ ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
046546ef 1394+ [<<"mod_logdb_users">>, _User] ->
3f23be8e 1395+ ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
046546ef 1396+ [<<"mod_logdb_settings">>] ->
3f23be8e 1397+ ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
f7ce3e3a 1398+ _ ->
1399+ Acc
1400+ end
1401+ end.
1402+
dd02533f 1403+-define(T(Lang, Text), translate:translate(Lang, Text)).
046546ef 1404+
f7ce3e3a 1405+-define(NODE(Name, Node),
3f23be8e
AM
1406+ #disco_item{jid = jid:make(Server),
1407+ node = Node,
dd02533f 1408+ name = ?T(Lang, Name)}).
3f23be8e
AM
1409+
1410+-define(NS_ADMINX(Sub),
1411+ <<(?NS_ADMIN)/binary, "#", Sub/binary>>).
1412+
1413+tokenize(Node) -> str:tokens(Node, <<"/#">>).
f7ce3e3a 1414+
1415+get_local_items(_Host, [], Server, Lang) ->
1416+ {result,
046546ef 1417+ [?NODE(<<"Messages logging engine">>, <<"mod_logdb">>)]
f7ce3e3a 1418+ };
046546ef 1419+get_local_items(_Host, [<<"mod_logdb">>], Server, Lang) ->
f7ce3e3a 1420+ {result,
046546ef
AM
1421+ [?NODE(<<"Messages logging engine users">>, <<"mod_logdb_users">>),
1422+ ?NODE(<<"Messages logging engine settings">>, <<"mod_logdb_settings">>)]
f7ce3e3a 1423+ };
3f23be8e
AM
1424+get_local_items(Host, [<<"mod_logdb_users">>], Server, _Lang) ->
1425+ {result, get_all_vh_users(Host, Server)};
046546ef 1426+get_local_items(Host, [<<"mod_logdb_users">>, <<$@, Diap/binary>>], Server, Lang) ->
3f23be8e
AM
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()
f7ce3e3a 1440+ end;
046546ef 1441+get_local_items(_Host, [<<"mod_logdb_users">>, _User], _Server, _Lang) ->
f7ce3e3a 1442+ {result, []};
046546ef 1443+get_local_items(_Host, [<<"mod_logdb_settings">>], _Server, _Lang) ->
f7ce3e3a 1444+ {result, []};
1445+get_local_items(_Host, Item, _Server, _Lang) ->
1446+ ?MYDEBUG("asked for items in ~p", [Item]),
3f23be8e 1447+ {error, xmpp:err_item_not_found()}.
f7ce3e3a 1448+
3f23be8e 1449+-define(INFO_RESULT(Allow, Feats, Lang),
f7ce3e3a 1450+ case Allow of
3f23be8e 1451+ deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
046546ef 1452+ allow -> {result, Feats}
f7ce3e3a 1453+ end).
1454+
3f23be8e
AM
1455+get_local_features(Acc, From,
1456+ #jid{lserver = LServer} = _To, Node, Lang) ->
f7ce3e3a 1457+ case gen_mod:is_loaded(LServer, mod_adhoc) of
1458+ false ->
1459+ Acc;
1460+ _ ->
3f23be8e 1461+ LNode = tokenize(Node),
f7ce3e3a 1462+ AllowUser = acl:match_rule(LServer, mod_logdb, From),
1463+ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1464+ case LNode of
046546ef 1465+ [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
3f23be8e 1466+ ?INFO_RESULT(allow, [?NS_COMMANDS], Lang);
046546ef 1467+ [<<"mod_logdb">>] ->
3f23be8e 1468+ ?INFO_RESULT(deny, [?NS_COMMANDS], Lang);
046546ef 1469+ [<<"mod_logdb_users">>] ->
3f23be8e 1470+ ?INFO_RESULT(AllowAdmin, [], Lang);
046546ef 1471+ [<<"mod_logdb_users">>, [$@ | _]] ->
3f23be8e 1472+ ?INFO_RESULT(AllowAdmin, [], Lang);
046546ef 1473+ [<<"mod_logdb_users">>, _User] ->
3f23be8e 1474+ ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
046546ef 1475+ [<<"mod_logdb_settings">>] ->
3f23be8e 1476+ ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
f7ce3e3a 1477+ [] ->
1478+ Acc;
1479+ _ ->
f7ce3e3a 1480+ Acc
1481+ end
1482+ end.
1483+
1484+-define(INFO_IDENTITY(Category, Type, Name, Lang),
dd02533f 1485+ [#identity{category = Category, type = Type, name = ?T(Lang, Name)}]).
f7ce3e3a 1486+
1487+-define(INFO_COMMAND(Name, Lang),
046546ef
AM
1488+ ?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
1489+ Name, Lang)).
f7ce3e3a 1490+
1491+get_local_identity(Acc, _From, _To, Node, Lang) ->
3f23be8e 1492+ LNode = tokenize(Node),
f7ce3e3a 1493+ case LNode of
046546ef
AM
1494+ [<<"mod_logdb">>] ->
1495+ ?INFO_COMMAND(<<"Messages logging engine">>, Lang);
1496+ [<<"mod_logdb_users">>] ->
1497+ ?INFO_COMMAND(<<"Messages logging engine users">>, Lang);
046546ef 1498+ [<<"mod_logdb_users">>, User] ->
f7ce3e3a 1499+ ?INFO_COMMAND(User, Lang);
046546ef
AM
1500+ [<<"mod_logdb_settings">>] ->
1501+ ?INFO_COMMAND(<<"Messages logging engine settings">>, Lang);
f7ce3e3a 1502+ _ ->
1503+ Acc
1504+ end.
1505+
3f23be8e
AM
1506+adhoc_local_items(Acc, From,
1507+ #jid{lserver = LServer, server = Server} = To, Lang) ->
1508+ % TODO: case acl:match_rule(LServer, ???, From) of
f7ce3e3a 1509+ Items = case Acc of
1510+ {result, Its} -> Its;
1511+ empty -> []
1512+ end,
3f23be8e
AM
1513+ Nodes = recursively_get_local_items(LServer,
1514+ <<"">>, Server, Lang),
f7ce3e3a 1515+ Nodes1 = lists:filter(
3f23be8e 1516+ fun(#disco_item{node = Nd}) ->
f7ce3e3a 1517+ F = get_local_features([], From, To, Nd, Lang),
1518+ case F of
3f23be8e
AM
1519+ {result, [?NS_COMMANDS]} -> true;
1520+ _ -> false
f7ce3e3a 1521+ end
1522+ end, Nodes),
1523+ {result, Items ++ Nodes1}.
1524+
3f23be8e
AM
1525+recursively_get_local_items(_LServer,
1526+ <<"mod_logdb_users">>, _Server, _Lang) ->
f7ce3e3a 1527+ [];
3f23be8e
AM
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} -> []
f7ce3e3a 1535+ end,
1536+ Nodes = lists:flatten(
1537+ lists:map(
3f23be8e
AM
1538+ fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
1539+ if (S /= Server) or (Nd == <<"">>) ->
f7ce3e3a 1540+ [];
1541+ true ->
3f23be8e
AM
1542+ [Item, recursively_get_local_items(
1543+ LServer, Nd, Server, Lang)]
f7ce3e3a 1544+ end
1545+ end, Items)),
1546+ Nodes.
1547+
1548+-define(COMMANDS_RESULT(Allow, From, To, Request),
1549+ case Allow of
1550+ deny ->
3f23be8e 1551+ {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
f7ce3e3a 1552+ allow ->
1553+ adhoc_local_commands(From, To, Request)
1554+ end).
1555+
1556+adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
3f23be8e
AM
1557+ #adhoc_command{node = Node, lang = Lang} = Request) ->
1558+ LNode = tokenize(Node),
f7ce3e3a 1559+ AllowUser = acl:match_rule(LServer, mod_logdb, From),
1560+ AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1561+ case LNode of
046546ef 1562+ [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
f7ce3e3a 1563+ ?COMMANDS_RESULT(allow, From, To, Request);
3f23be8e
AM
1564+ [<<"mod_logdb_users">>, <<$@, _/binary>>] when AllowAdmin == allow ->
1565+ Acc;
046546ef 1566+ [<<"mod_logdb_users">>, _User] when AllowAdmin == allow ->
f7ce3e3a 1567+ ?COMMANDS_RESULT(allow, From, To, Request);
046546ef 1568+ [<<"mod_logdb_settings">>] when AllowAdmin == allow ->
f7ce3e3a 1569+ ?COMMANDS_RESULT(allow, From, To, Request);
1570+ _ ->
1571+ Acc
1572+ end.
1573+
1574+adhoc_local_commands(From, #jid{lserver = LServer} = _To,
3f23be8e 1575+ #adhoc_command{lang = Lang,
f7ce3e3a 1576+ node = Node,
3f23be8e 1577+ sid = SessionID,
f7ce3e3a 1578+ action = Action,
1579+ xdata = XData} = Request) ->
3f23be8e 1580+ LNode = tokenize(Node),
f7ce3e3a 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.
3f23be8e
AM
1585+ ActionIsExecute = Action == execute orelse Action == complete,
1586+ if Action == cancel ->
f7ce3e3a 1587+ %% User cancels request
3f23be8e
AM
1588+ #adhoc_command{status = canceled, lang = Lang,
1589+ node = Node, sid = SessionID};
1590+ XData == undefined, ActionIsExecute ->
f7ce3e3a 1591+ %% User requests form
3f23be8e 1592+ case get_form(LServer, LNode, Lang) of
f7ce3e3a 1593+ {result, Form} ->
3f23be8e 1594+ xmpp_util:make_adhoc_response(
f7ce3e3a 1595+ Request,
3f23be8e
AM
1596+ #adhoc_command{status = executing,
1597+ xdata = Form});
f7ce3e3a 1598+ {error, Error} ->
1599+ {error, Error}
1600+ end;
3f23be8e 1601+ XData /= undefined, ActionIsExecute ->
f7ce3e3a 1602+ %% User returns form.
3f23be8e
AM
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}
f7ce3e3a 1610+ end;
046546ef 1611+ true ->
3f23be8e 1612+ {error, xmpp:err_bad_request(<<"Unexpected action">>, Lang)}
f7ce3e3a 1613+ end.
1614+
3f23be8e
AM
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))).
f7ce3e3a 1620+
1621+get_user_form(LUser, LServer, Lang) ->
3f23be8e
AM
1622+ ?MYDEBUG("get_user_form ~p ~p", [LUser, LServer]),
1623+ %From = jid:encode(jid:remove_resource(Jid)),
f7ce3e3a 1624+ #user_settings{dolog_default=DLD,
1625+ dolog_list=DLL,
1626+ donotlog_list=DNLL} = get_user_settings(LUser, LServer),
3f23be8e
AM
1627+ Fs = [
1628+ #xdata_field{
1629+ type = 'list-single',
dd02533f 1630+ label = ?T(Lang, <<"Default">>),
3f23be8e
AM
1631+ var = <<"dolog_default">>,
1632+ values = [misc:atom_to_binary(DLD)],
dd02533f 1633+ options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
3f23be8e 1634+ value = <<"true">>},
dd02533f 1635+ #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
3f23be8e
AM
1636+ value = <<"false">>}]},
1637+ #xdata_field{
1638+ type = 'text-multi',
dd02533f 1639+ label = ?T(Lang, <<"Log Messages">>),
3f23be8e
AM
1640+ var = <<"dolog_list">>,
1641+ values = DLL},
1642+ #xdata_field{
1643+ type = 'text-multi',
dd02533f 1644+ label = ?T(Lang, <<"Do Not Log Messages">>),
3f23be8e
AM
1645+ var = <<"donotlog_list">>,
1646+ values = DNLL}
1647+ ],
1648+ {result, #xdata{
dd02533f 1649+ title = ?T(Lang, <<"Messages logging engine settings">>),
3f23be8e 1650+ type = form,
dd02533f 1651+ instructions = [<< (?T(Lang, <<"Set logging preferences">>))/binary,
3f23be8e
AM
1652+ (iolist_to_binary(": "))/binary,
1653+ LUser/binary, "@", LServer/binary >>],
1654+ fields = [?HFIELD()|
1655+ Fs]}}.
f7ce3e3a 1656+
1657+get_settings_form(Host, Lang) ->
3f23be8e 1658+ ?MYDEBUG("get_settings_form ~p ~p", [Host, Lang]),
046546ef
AM
1659+ #state{dbmod=_DBMod,
1660+ dbs=_DBs,
f7ce3e3a 1661+ dolog_default=DLD,
1662+ ignore_jids=IgnoreJids,
1663+ groupchat=GroupChat,
1664+ purge_older_days=PurgeDaysT,
234c6b10 1665+ drop_messages_on_user_removal=MRemoval,
f7ce3e3a 1666+ poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
1667+
f7ce3e3a 1668+ PurgeDays =
1669+ case PurgeDaysT of
046546ef
AM
1670+ never -> <<"never">>;
1671+ Num when is_integer(Num) -> integer_to_binary(Num);
1672+ _ -> <<"unknown">>
f7ce3e3a 1673+ end,
3f23be8e
AM
1674+ Fs = [
1675+ #xdata_field{
1676+ type = 'list-single',
dd02533f 1677+ label = ?T(Lang, <<"Default">>),
3f23be8e
AM
1678+ var = <<"dolog_default">>,
1679+ values = [misc:atom_to_binary(DLD)],
dd02533f 1680+ options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
3f23be8e 1681+ value = <<"true">>},
dd02533f 1682+ #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
3f23be8e
AM
1683+ value = <<"false">>}]},
1684+ #xdata_field{
1685+ type = 'list-single',
dd02533f 1686+ label = ?T(Lang, <<"Drop messages on user removal">>),
3f23be8e
AM
1687+ var = <<"drop_messages_on_user_removal">>,
1688+ values = [misc:atom_to_binary(MRemoval)],
dd02533f 1689+ options = [#xdata_option{label = ?T(Lang, <<"Drop">>),
3f23be8e 1690+ value = <<"true">>},
dd02533f 1691+ #xdata_option{label = ?T(Lang, <<"Do not drop">>),
3f23be8e
AM
1692+ value = <<"false">>}]},
1693+ #xdata_field{
1694+ type = 'list-single',
dd02533f 1695+ label = ?T(Lang, <<"Groupchat messages logging">>),
3f23be8e
AM
1696+ var = <<"groupchat">>,
1697+ values = [misc:atom_to_binary(GroupChat)],
dd02533f 1698+ options = [#xdata_option{label = ?T(Lang, <<"all">>),
3f23be8e 1699+ value = <<"all">>},
dd02533f 1700+ #xdata_option{label = ?T(Lang, <<"none">>),
3f23be8e 1701+ value = <<"none">>},
dd02533f 1702+ #xdata_option{label = ?T(Lang, <<"send">>),
3f23be8e
AM
1703+ value = <<"send">>}]},
1704+ #xdata_field{
1705+ type = 'text-multi',
dd02533f 1706+ label = ?T(Lang, <<"Jids/Domains to ignore">>),
3f23be8e
AM
1707+ var = <<"ignore_list">>,
1708+ values = IgnoreJids},
1709+ #xdata_field{
1710+ type = 'text-single',
dd02533f 1711+ label = ?T(Lang, <<"Purge messages older than (days)">>),
3f23be8e
AM
1712+ var = <<"purge_older_days">>,
1713+ values = [iolist_to_binary(PurgeDays)]},
1714+ #xdata_field{
1715+ type = 'text-single',
dd02533f 1716+ label = ?T(Lang, <<"Poll users settings (seconds)">>),
3f23be8e
AM
1717+ var = <<"poll_users_settings">>,
1718+ values = [integer_to_binary(PollTime)]}
1719+ ],
1720+ {result, #xdata{
dd02533f
AM
1721+ title = ?T(Lang, <<"Messages logging engine settings (run-time)">>),
1722+ instructions = [?T(Lang, <<"Set run-time settings">>)],
3f23be8e
AM
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),
f7ce3e3a 1729+ get_user_form(LUser, LServer, Lang);
3f23be8e 1730+get_form(Host, [<<"mod_logdb_settings">>], Lang) ->
f7ce3e3a 1731+ get_settings_form(Host, Lang);
3f23be8e 1732+get_form(_Host, Command, _Lang) ->
f7ce3e3a 1733+ ?MYDEBUG("asked for form ~p", [Command]),
3f23be8e 1734+ {error, xmpp:err_service_unavailable()}.
f7ce3e3a 1735+
046546ef
AM
1736+check_log_list([]) ->
1737+ ok;
1738+check_log_list([<<>>]) ->
1739+ ok;
f7ce3e3a 1740+check_log_list([Head | Tail]) ->
046546ef
AM
1741+ case binary:match(Head, <<$@>>) of
1742+ nomatch -> throw(error);
1743+ {_, _} -> ok
f7ce3e3a 1744+ end,
1745+ % this check for Head to be valid jid
3f23be8e
AM
1746+ case catch jid:decode(Head) of
1747+ {'EXIT', _Reason} -> throw(error);
1748+ _ -> check_log_list(Tail)
046546ef 1749+ end.
f7ce3e3a 1750+
046546ef
AM
1751+check_ignore_list([]) ->
1752+ ok;
1753+check_ignore_list([<<>>]) ->
1754+ ok;
1755+check_ignore_list([<<>> | Tail]) ->
1756+ check_ignore_list(Tail);
f7ce3e3a 1757+check_ignore_list([Head | Tail]) ->
046546ef
AM
1758+ case binary:match(Head, <<$@>>) of
1759+ {_, _} -> ok;
1760+ nomatch -> throw(error)
f7ce3e3a 1761+ end,
3f23be8e
AM
1762+ Jid2Test = case Head of
1763+ << $@, _Rest/binary >> -> << "a", Head/binary >>;
1764+ Jid -> Jid
1765+ end,
f7ce3e3a 1766+ % this check for Head to be valid jid
3f23be8e
AM
1767+ case catch jid:decode(Jid2Test) of
1768+ {'EXIT', _Reason} -> throw(error);
1769+ _ -> check_ignore_list(Tail)
046546ef 1770+ end.
f7ce3e3a 1771+
3f23be8e
AM
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+
f7ce3e3a 1777+parse_users_settings(XData) ->
3f23be8e
AM
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)
f7ce3e3a 1783+ end,
3f23be8e
AM
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+
f7ce3e3a 1797+ #user_settings{dolog_default=DLD,
1798+ dolog_list=DLL,
1799+ donotlog_list=DNLL}.
1800+
1801+parse_module_settings(XData) ->
3f23be8e
AM
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)
234c6b10 1807+ end,
3f23be8e
AM
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)
f7ce3e3a 1820+ end,
3f23be8e
AM
1821+ ListIgnore = get_values(<<"ignore_list">>, XData),
1822+ Ignore = case catch check_ignore_list(ListIgnore) of
1823+ ok -> ListIgnore;
1824+ error -> throw(bad_request)
f7ce3e3a 1825+ end,
3f23be8e
AM
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
f7ce3e3a 1833+ end,
3f23be8e
AM
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)
f7ce3e3a 1837+ end,
1838+ #state{dolog_default=DLD,
1839+ groupchat=GroupChat,
1840+ ignore_jids=Ignore,
1841+ purge_older_days=Purge,
234c6b10 1842+ drop_messages_on_user_removal=MRemoval,
f7ce3e3a 1843+ poll_users_settings=Poll}.
1844+
3f23be8e
AM
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",
f7ce3e3a 1848+ case catch parse_users_settings(XData) of
1849+ bad_request ->
3f23be8e
AM
1850+ ?ERROR_MSG("Failed to set user form: bad_request", []),
1851+ {error, xmpp:err_bad_request(Txt, Lang)};
046546ef 1852+ {'EXIT', Reason} ->
3f23be8e
AM
1853+ ?ERROR_MSG("Failed to set user form ~p", [Reason]),
1854+ {error, xmpp:err_bad_request(Txt, Lang)};
f7ce3e3a 1855+ UserSettings ->
1856+ case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
1857+ ok ->
3f23be8e 1858+ {result, undefined};
f7ce3e3a 1859+ error ->
3f23be8e 1860+ {error, xmpp:err_internal_server_error()}
f7ce3e3a 1861+ end
1862+ end;
3f23be8e
AM
1863+set_form(_From, Host, [<<"mod_logdb_settings">>], Lang, XData) ->
1864+ Txt = "Parse module settings failed",
f7ce3e3a 1865+ case catch parse_module_settings(XData) of
3f23be8e
AM
1866+ bad_request ->
1867+ ?ERROR_MSG("Failed to set settings form: bad_request", []),
1868+ {error, xmpp:err_bad_request(Txt, Lang)};
046546ef 1869+ {'EXIT', Reason} ->
3f23be8e
AM
1870+ ?ERROR_MSG("Failed to set settings form ~p", [Reason]),
1871+ {error, xmpp:err_bad_request(Txt, Lang)};
f7ce3e3a 1872+ Settings ->
1873+ case mod_logdb:set_module_settings(Host, Settings) of
1874+ ok ->
3f23be8e 1875+ {result, undefined};
f7ce3e3a 1876+ error ->
3f23be8e 1877+ {error, xmpp:err_internal_server_error()}
f7ce3e3a 1878+ end
1879+ end;
1880+set_form(From, _Host, Node, _Lang, XData) ->
3f23be8e 1881+ User = jid:encode(jid:remove_resource(From)),
f7ce3e3a 1882+ ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]),
3f23be8e 1883+ {error, xmpp:err_service_unavailable()}.
f7ce3e3a 1884+
3f23be8e 1885+get_all_vh_users(Host, Server) ->
f7ce3e3a 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}) ->
3f23be8e
AM
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);
f7ce3e3a 1898+ N ->
3f23be8e 1899+ NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1,
f7ce3e3a 1900+ M = trunc(N / NParts) + 1,
1901+ lists:map(fun(K) ->
1902+ L = K + M - 1,
046546ef 1903+ Node = <<"@",
3f23be8e 1904+ (integer_to_binary(K))/binary,
046546ef 1905+ "-",
3f23be8e 1906+ (integer_to_binary(L))/binary
046546ef 1907+ >>,
f7ce3e3a 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 =
046546ef
AM
1914+ <<FU/binary, "@", FS/binary,
1915+ " -- ",
1916+ LU/binary, "@", LS/binary>>,
3f23be8e
AM
1917+ #disco_item{jid = jid:make(Host),
1918+ node = <<"mod_logdb_users/", Node/binary>>,
1919+ name = Name}
f7ce3e3a 1920+ end, lists:seq(1, N, M))
1921+ end
1922+ end.
f7ce3e3a 1923+
1924+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1925+%
234c6b10 1926+% webadmin hooks
f7ce3e3a 1927+%
1928+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
234c6b10 1929+webadmin_menu(Acc, _Host, Lang) ->
dd02533f 1930+ [{<<"messages">>, ?T(<<"Users Messages">>)} | Acc].
234c6b10 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 ->
046546ef 1937+ ?INPUTT(<<"submit">>, <<"dolog">>, <<"Log Messages">>);
234c6b10 1938+ true ->
046546ef 1939+ ?INPUTT(<<"submit">>, <<"donotlog">>, <<"Do Not Log Messages">>);
234c6b10 1940+ _ -> []
1941+ end,
046546ef 1942+ Acc ++ [?XE(<<"h3">>, [?ACT(<<"messages/">>, <<"Messages">>), ?C(<<" ">>), Log])].
f7ce3e3a 1943+
234c6b10 1944+webadmin_page(_, Host,
046546ef 1945+ #request{path = [<<"messages">>],
234c6b10 1946+ q = Query,
046546ef 1947+ lang = Lang}) ->
234c6b10 1948+ Res = vhost_messages_stats(Host, Query, Lang),
1949+ {stop, Res};
1950+webadmin_page(_, Host,
046546ef 1951+ #request{path = [<<"messages">>, Date],
234c6b10 1952+ q = Query,
046546ef 1953+ lang = Lang}) ->
234c6b10 1954+ Res = vhost_messages_stats_at(Host, Query, Lang, Date),
1955+ {stop, Res};
1956+webadmin_page(_, Host,
046546ef 1957+ #request{path = [<<"user">>, U, <<"messages">>],
234c6b10 1958+ q = Query,
1959+ lang = Lang}) ->
1960+ Res = user_messages_stats(U, Host, Query, Lang),
1961+ {stop, Res};
1962+webadmin_page(_, Host,
046546ef 1963+ #request{path = [<<"user">>, U, <<"messages">>, Date],
234c6b10 1964+ q = Query,
1965+ lang = Lang}) ->
1966+ Res = mod_logdb:user_messages_stats_at(U, Host, Query, Lang, Date),
1967+ {stop, Res};
046546ef 1968+webadmin_page(Acc, _Host, _R) -> Acc.
234c6b10 1969+
046546ef 1970+user_parse_query(_, <<"dolog">>, User, Server, _Query) ->
234c6b10 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};
046546ef 1975+user_parse_query(_, <<"donotlog">>, User, Server, _Query) ->
234c6b10 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.
f7ce3e3a 1982+
1983+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1984+%
234c6b10 1985+% webadmin funcs
f7ce3e3a 1986+%
1987+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
234c6b10 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]),
dd02533f 2001+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
234c6b10 2002+ {error, GReason} ->
2003+ ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
dd02533f 2004+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
234c6b10 2005+ {ok, []} ->
dd02533f 2006+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Server])))];
234c6b10 2007+ {ok, Dates} ->
2008+ Fun = fun({Date, Count}) ->
046546ef 2009+ DateBin = iolist_to_binary(Date),
3f23be8e 2010+ ID = misc:encode_base64( << Server/binary, DateBin/binary >> ),
046546ef
AM
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))
234c6b10 2016+ ])
2017+ end,
046546ef 2018+
dd02533f 2019+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s">>), [Server])))] ++
234c6b10 2020+ case Res of
046546ef
AM
2021+ ok -> [?CT(<<"Submitted">>), ?P];
2022+ error -> [?CT(<<"Bad format">>), ?P];
234c6b10 2023+ nothing -> []
2024+ end ++
046546ef
AM
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">>)
234c6b10 2032+ ])]),
046546ef 2033+ ?XE(<<"tbody">>,
234c6b10 2034+ lists:map(Fun, Dates)
2035+ )]),
2036+ ?BR,
046546ef 2037+ ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
234c6b10 2038+ ])]
f7ce3e3a 2039+ end.
2040+
234c6b10 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]),
dd02533f 2048+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
234c6b10 2049+ {error, GReason} ->
2050+ ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
dd02533f 2051+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
234c6b10 2052+ {ok, []} ->
dd02533f 2053+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Server, Date])))];
046546ef
AM
2054+ {ok, Stats} ->
2055+ Res = case catch vhost_messages_at_parse_query(Server, Date, Stats, Query) of
234c6b10 2056+ {'EXIT', Reason} ->
2057+ ?ERROR_MSG("~p", [Reason]),
2058+ error;
2059+ VResult -> VResult
2060+ end,
2061+ Fun = fun({User, Count}) ->
046546ef 2062+ UserBin = iolist_to_binary(User),
3f23be8e 2063+ ID = misc:encode_base64( << UserBin/binary, Server/binary >> ),
046546ef 2064+ ?XE(<<"tr">>,
bb18ce72 2065+ [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
046546ef
AM
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))
234c6b10 2069+ ])
2070+ end,
dd02533f 2071+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Server, Date])))] ++
234c6b10 2072+ case Res of
046546ef
AM
2073+ ok -> [?CT(<<"Submitted">>), ?P];
2074+ error -> [?CT(<<"Bad format">>), ?P];
234c6b10 2075+ nothing -> []
2076+ end ++
046546ef
AM
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">>)
234c6b10 2084+ ])]),
046546ef
AM
2085+ ?XE(<<"tbody">>,
2086+ lists:map(Fun, Stats)
234c6b10 2087+ )]),
2088+ ?BR,
046546ef 2089+ ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
234c6b10 2090+ ])]
2091+ end.
2092+
2093+user_messages_stats(User, Server, Query, Lang) ->
3f23be8e 2094+ Jid = jid:encode({User, Server, ""}),
234c6b10 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
f7ce3e3a 2101+ end,
234c6b10 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]),
dd02533f 2109+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
234c6b10 2110+ {error, GReason} ->
2111+ ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
dd02533f 2112+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
234c6b10 2113+ {ok, []} ->
dd02533f 2114+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Jid])))];
234c6b10 2115+ {ok, Dates} ->
2116+ Fun = fun({Date, Count}) ->
046546ef 2117+ DateBin = iolist_to_binary(Date),
3f23be8e 2118+ ID = misc:encode_base64( << User/binary, DateBin/binary >> ),
046546ef
AM
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)))
234c6b10 2124+ ])
234c6b10 2125+ end,
046546ef 2126+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T("Logged messages for ~s"), [Jid])))] ++
234c6b10 2127+ case Res of
046546ef
AM
2128+ ok -> [?CT(<<"Submitted">>), ?P];
2129+ error -> [?CT(<<"Bad format">>), ?P];
234c6b10 2130+ nothing -> []
2131+ end ++
046546ef
AM
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">>)
234c6b10 2139+ ])]),
046546ef 2140+ ?XE(<<"tbody">>,
234c6b10 2141+ lists:map(Fun, Dates)
2142+ )]),
2143+ ?BR,
046546ef 2144+ ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
234c6b10 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) ->
3f23be8e 2159+ Jid = jid:encode({User, Server, ""}),
234c6b10 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]),
dd02533f 2166+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
234c6b10 2167+ {error, GReason} ->
2168+ ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
dd02533f 2169+ [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
234c6b10 2170+ {ok, []} ->
dd02533f 2171+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Jid, Date])))];
234c6b10 2172+ {ok, User_messages} ->
2173+ Res = case catch user_messages_at_parse_query(Server,
046546ef
AM
2174+ Date,
2175+ User_messages,
2176+ Query) of
234c6b10 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) ->
3f23be8e 2186+ {jid:encode(Item#roster.jid), Item#roster.name}
046546ef 2187+ end, UR),
234c6b10 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)
046546ef 2198+ CheckedUsers = case lists:keysearch(<<"filter">>, 1, Query) of
234c6b10 2199+ {value, _} ->
2200+ lists:filter(fun(UFUser) ->
3f23be8e 2201+ ID = misc:encode_base64(term_to_binary(UFUser)),
046546ef 2202+ lists:member({<<"selected">>, ID}, Query)
234c6b10 2203+ end, UniqUsers);
2204+ false -> []
2205+ end,
2206+
2207+ % UniqUsers in html (noone selected -> everyone selected)
2208+ Users = lists:map(fun(UHUser) ->
3f23be8e 2209+ ID = misc:encode_base64(term_to_binary(UHUser)),
234c6b10 2210+ Input = case lists:member(UHUser, CheckedUsers) of
046546ef
AM
2211+ true -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2212+ false when CheckedUsers == [] -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2213+ false -> [?INPUT(<<"checkbox">>, <<"selected">>, ID)]
234c6b10 2214+ end,
2215+ Nick =
2216+ case search_user_nick(UHUser, UserRoster) of
046546ef
AM
2217+ nothing -> <<"">>;
2218+ N -> iolist_to_binary( " ("++ N ++")" )
234c6b10 2219+ end,
046546ef
AM
2220+ ?XE(<<"tr">>,
2221+ [?XE(<<"td">>, Input),
2222+ ?XC(<<"td">>, iolist_to_binary(UHUser++Nick))])
234c6b10 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}) ->
046546ef
AM
2238+ Text = case Subject of
2239+ "" -> iolist_to_binary(Body);
dd02533f 2240+ _ -> iolist_to_binary([binary_to_list(?T(<<"Subject">>)) ++ ": " ++ Subject ++ "\n" ++ Body])
046546ef 2241+ end,
234c6b10 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,
3f23be8e 2257+ ID = misc:encode_base64(term_to_binary(Timestamp)),
046546ef
AM
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)])])
234c6b10 2263+ end,
2264+ % Filtered user messages in html
2265+ Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
2266+
dd02533f 2267+ [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Jid, Date])))] ++
234c6b10 2268+ case Res of
046546ef
AM
2269+ ok -> [?CT(<<"Submitted">>), ?P];
2270+ error -> [?CT(<<"Bad format">>), ?P];
234c6b10 2271+ nothing -> []
2272+ end ++
046546ef
AM
2273+ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2274+ [?XE(<<"table">>,
2275+ [?XE(<<"thead">>,
2276+ [?X(<<"td">>),
2277+ ?XCT(<<"td">>, <<"User">>)
234c6b10 2278+ ]
2279+ ),
046546ef 2280+ ?XE(<<"tbody">>,
234c6b10 2281+ Users
2282+ )]),
046546ef 2283+ ?INPUTT(<<"submit">>, <<"filter">>, <<"Filter Selected">>)
234c6b10 2284+ ] ++
046546ef
AM
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">>)
234c6b10 2292+ ])]),
046546ef 2293+ ?XE(<<"tbody">>,
234c6b10 2294+ Msgs
2295+ )]),
046546ef 2296+ ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>),
234c6b10 2297+ ?BR
2298+ ]
2299+ )]
2300+ end.
046546ef 2301diff --git a/src/mod_logdb.hrl b/src/mod_logdb.hrl
0d78319d 2302new file mode 100644
dd02533f 2303index 0000000000..49791f4e69
0d78319d 2304--- /dev/null
046546ef 2305+++ b/src/mod_logdb.hrl
3f23be8e 2306@@ -0,0 +1,33 @@
234c6b10 2307+%%%----------------------------------------------------------------------
2308+%%% File : mod_logdb.hrl
3f23be8e 2309+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
234c6b10 2310+%%% Purpose :
3f23be8e 2311+%%% Url : https://paleg.github.io/mod_logdb/
234c6b10 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),
046546ef
AM
2336+ ?XA(<<"input">>, [{<<"type">>, Type},
2337+ {<<"name">>, Name},
2338+ {<<"value">>, Value},
2339+ {<<"checked">>, <<"true">>}])).
2340diff --git a/src/mod_logdb_mnesia.erl b/src/mod_logdb_mnesia.erl
0d78319d 2341new file mode 100644
dd02533f 2342index 0000000000..a08d5262c2
0d78319d 2343--- /dev/null
046546ef 2344+++ b/src/mod_logdb_mnesia.erl
a815cc6c 2345@@ -0,0 +1,553 @@
234c6b10 2346+%%%----------------------------------------------------------------------
2347+%%% File : mod_logdb_mnesia.erl
3f23be8e 2348+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
234c6b10 2349+%%% Purpose : mnesia backend for mod_logdb
3f23be8e 2350+%%% Url : https://paleg.github.io/mod_logdb/
234c6b10 2351+%%%----------------------------------------------------------------------
2352+
2353+-module(mod_logdb_mnesia).
2354+-author('o.palij@gmail.com').
2355+
2356+-include("mod_logdb.hrl").
046546ef 2357+-include("logger.hrl").
234c6b10 2358+
2359+-behaviour(gen_logdb).
2360+-behaviour(gen_server).
0d78319d 2361+
234c6b10 2362+% gen_server
2363+-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2364+% gen_mod
bb18ce72 2365+-export([start/2, stop/1]).
234c6b10 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]).
0d78319d 2375+
234c6b10 2376+-define(PROCNAME, mod_logdb_mnesia).
2377+-define(CALL_TIMEOUT, 10000).
0d78319d 2378+
234c6b10 2379+-record(state, {vhost}).
2380+
2381+-record(stats, {user, at, count}).
2382+
2383+prefix() ->
2384+ "logdb_".
2385+
2386+suffix(VHost) ->
046546ef 2387+ "_" ++ binary_to_list(VHost).
234c6b10 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)),
f7ce3e3a 2438+ {reply, Reply, State};
234c6b10 2439+handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2440+ Reply = rebuild_stats_at_int(VHost, Date),
0d78319d 2441+ {reply, Reply, State};
234c6b10 2442+handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
f7ce3e3a 2443+ Table = table_name(VHost, Date),
234c6b10 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
f7ce3e3a 2453+ {aborted, Reason} ->
234c6b10 2454+ ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
f7ce3e3a 2455+ error;
2456+ _ ->
2457+ ok
234c6b10 2458+ end,
f7ce3e3a 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};
234c6b10 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};
f7ce3e3a 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) ->
234c6b10 2510+ {reply, get_user_stats_int(User, VHost), State};
f7ce3e3a 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),
234c6b10 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,
f7ce3e3a 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).
234c6b10 2626+drop_user(User, VHost) ->
2627+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2628+ gen_server:call(Proc, {drop_user, User}, ?CALL_TIMEOUT).
f7ce3e3a 2629+
2630+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2631+%
2632+% internals
2633+%
2634+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
046546ef 2635+log_message_int(VHost, #msg{timestamp=Timestamp}=MsgBin) ->
f7ce3e3a 2636+ Date = mod_logdb:convert_timestamp_brief(Timestamp),
2637+
046546ef
AM
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+
f7ce3e3a 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} ->
046546ef
AM
2662+ ?MYDEBUG("Created msg table for ~s at ~s", [VHost, Date]),
2663+ log_message_int(VHost, MsgBin)
f7ce3e3a 2664+ end;
2665+ {aborted, TReason} ->
2666+ ?ERROR_MSG("Failed to log message: ~p", [TReason]),
2667+ error;
2668+ {atomic, _} ->
046546ef
AM
2669+ ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
2670+ [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
f7ce3e3a 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) ->
046546ef
AM
2712+ Table = term_to_binary(ATable),
2713+ case ejabberd_regexp:run( Table, << VHost/binary, <<"$">>/binary >> ) of
0d78319d 2714+ match ->
3f23be8e 2715+ case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
0d78319d 2716+ {match, [{S, E}]} ->
3f23be8e 2717+ lists:append(Dates, [lists:sublist(binary_to_list(Table), S+1, E)]);
f7ce3e3a 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),
046546ef
AM
2747+ % Delete all stats for VHost at Date
2748+ mnesia:foldl(DFun, [], STable),
f7ce3e3a 2749+ % Calc stats for VHost at Date
2750+ case mnesia:foldl(CFun, [], Table) of
2751+ [] -> empty;
2752+ AStats ->
f7ce3e3a 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+
234c6b10 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+
f7ce3e3a 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}]).
046546ef 2899diff --git a/src/mod_logdb_mysql.erl b/src/mod_logdb_mysql.erl
0d78319d 2900new file mode 100644
dd02533f 2901index 0000000000..21d65e6578
0d78319d 2902--- /dev/null
046546ef 2903+++ b/src/mod_logdb_mysql.erl
a815cc6c 2904@@ -0,0 +1,1050 @@
0d78319d
AM
2905+%%%----------------------------------------------------------------------
2906+%%% File : mod_logdb_mysql.erl
3f23be8e 2907+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
0d78319d 2908+%%% Purpose : MySQL backend for mod_logdb
3f23be8e 2909+%%% Url : https://paleg.github.io/mod_logdb/
0d78319d
AM
2910+%%%----------------------------------------------------------------------
2911+
2912+-module(mod_logdb_mysql).
2913+-author('o.palij@gmail.com').
2914+
2915+-include("mod_logdb.hrl").
046546ef 2916+-include("logger.hrl").
0d78319d
AM
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
bb18ce72 2924+-export([start/2, stop/1]).
0d78319d
AM
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
046546ef 2950+ end, binary_to_list(VHost)).
0d78319d
AM
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+
046546ef
AM
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)).
0d78319d
AM
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+
046546ef
AM
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, <<"">>),
0d78319d
AM
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]),
046546ef
AM
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).
0d78319d
AM
3043+
3044+close_mysql_connection(DBRef) ->
3045+ ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
046546ef 3046+ catch p1_mysql_conn:stop(DBRef).
0d78319d
AM
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),
046546ef
AM
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)),
0d78319d
AM
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), "',",
046546ef 3073+ "'", binary_to_list(Msg#msg.type), "',",
bb18ce72
AM
3074+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
3075+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
0d78319d
AM
3076+ "'", Msg#msg.timestamp, "');"],
3077+
3078+ Reply =
3079+ case sql_query_internal_silent(DBRef, Query) of
3080+ {updated, _} ->
046546ef
AM
3081+ ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
3082+ [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
0d78319d
AM
3083+ increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date);
3084+ {error, Reason} ->
046546ef 3085+ case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S02">>) of
0d78319d
AM
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),
046546ef 3093+ increment_user_stats(DBRef, binary_to_list(Msg#msg.owner_name), Owner_id, VHost, Peer_name_id, Peer_server_id, Date)
0d78319d
AM
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],
f7ce3e3a 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) ->
234c6b10 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};
f7ce3e3a 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),
234c6b10 3163+ Query = ["SELECT username, sum(count) AS allcount ",
f7ce3e3a 3164+ "FROM ",SName," ",
3165+ "JOIN ",users_table(VHost)," ON owner_id=user_id "
234c6b10 3166+ "WHERE at=\"",Date,"\" "
3167+ "GROUP BY username ",
3168+ "ORDER BY allcount DESC;"
f7ce3e3a 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) ->
234c6b10 3182+ {reply, get_user_stats_int(DBRef, User, VHost), State};
f7ce3e3a 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;"],
0d78319d 3240+ Reply =
f7ce3e3a 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} ->
046546ef 3294+ case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
f7ce3e3a 3295+ % Already exists
0d78319d 3296+ match ->
f7ce3e3a 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+
234c6b10 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};
f7ce3e3a 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+
234c6b10 3359+terminate(_Reason, #state{dbref=DBRef}=_State) ->
3360+ close_mysql_connection(DBRef),
f7ce3e3a 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),
234c6b10 3376+ gen_server:cast(Proc, {rebuild_stats}).
f7ce3e3a 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).
234c6b10 3413+drop_user(User, VHost) ->
3414+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3415+ gen_server:cast(Proc, {drop_user, User}).
f7ce3e3a 3416+
3417+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3418+%
3419+% internals
3420+%
3421+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
234c6b10 3422+increment_user_stats(DBRef, User_name, User_id, VHost, PNameID, PServerID, Date) ->
f7ce3e3a 3423+ SName = stats_table(VHost),
3424+ UQuery = ["UPDATE ",SName," ",
3425+ "SET count=count+1 ",
234c6b10 3426+ "WHERE owner_id=\"",User_id,"\" AND peer_name_id=\"",PNameID,"\" AND peer_server_id=\"",PServerID,"\" AND at=\"",Date,"\";"],
f7ce3e3a 3427+
3428+ case sql_query_internal(DBRef, UQuery) of
3429+ {updated, 0} ->
3430+ IQuery = ["INSERT INTO ",SName," ",
234c6b10 3431+ "(owner_id, peer_name_id, peer_server_id, at, count) ",
f7ce3e3a 3432+ "VALUES ",
234c6b10 3433+ "('",User_id,"', '",PNameID,"', '",PServerID,"', '",Date,"', '1');"],
f7ce3e3a 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} ->
3f23be8e 3451+ Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
f7ce3e3a 3452+ lists:foldl(fun([Table], Dates) ->
0d78319d 3453+ case re:run(Table, Reg) of
3f23be8e
AM
3454+ {match, _} ->
3455+ case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
0d78319d 3456+ {match, [{S, E}]} ->
3f23be8e 3457+ lists:append(Dates, [lists:sublist(Table, S+1, E)]);
f7ce3e3a 3458+ nomatch ->
3459+ Dates
3460+ end;
234c6b10 3461+ _ ->
f7ce3e3a 3462+ Dates
3463+ end
3464+ end, [], Tables);
3465+ {error, _} ->
3466+ []
3467+ end.
3468+
234c6b10 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).
f7ce3e3a 3488+
234c6b10 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),
f7ce3e3a 3494+
234c6b10 3495+ DQuery = [ "DELETE FROM ",STable," ",
3496+ "WHERE at='",Date,"';"],
f7ce3e3a 3497+
234c6b10 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,
f7ce3e3a 3531+
234c6b10 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.
f7ce3e3a 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+
234c6b10 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.
f7ce3e3a 3570+
234c6b10 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+ ],
f7ce3e3a 3579+ case sql_query_internal(DBRef, Query) of
234c6b10 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
f7ce3e3a 3590+ {updated, _} ->
234c6b10 3591+ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
f7ce3e3a 3592+ ok;
3593+ {error, _} ->
3594+ error
3595+ end.
3596+
234c6b10 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+
f7ce3e3a 3630+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3631+%
3632+% tables internals
3633+%
3634+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
234c6b10 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) ->
f7ce3e3a 3650+ SName = stats_table(VHost),
3651+ Query = ["CREATE TABLE ",SName," (",
3652+ "owner_id MEDIUMINT UNSIGNED, ",
234c6b10 3653+ "peer_name_id MEDIUMINT UNSIGNED, ",
3654+ "peer_server_id MEDIUMINT UNSIGNED, ",
f7ce3e3a 3655+ "at varchar(20), ",
3656+ "count int(11), ",
234c6b10 3657+ "INDEX(owner_id, peer_name_id, peer_server_id), ",
f7ce3e3a 3658+ "INDEX(at)"
3659+ ") ENGINE=InnoDB CHARACTER SET utf8;"
3660+ ],
3661+ case sql_query_internal_silent(DBRef, Query) of
3662+ {updated, _} ->
234c6b10 3663+ ?INFO_MSG("Created stats table for ~p", [VHost]),
3664+ rebuild_all_stats_int(State),
f7ce3e3a 3665+ ok;
3666+ {error, Reason} ->
046546ef 3667+ case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
0d78319d 3668+ match ->
f7ce3e3a 3669+ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
234c6b10 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;
f7ce3e3a 3685+ _ ->
3686+ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
3687+ error
3688+ end
3689+ end.
3690+
234c6b10 3691+create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 3708+create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 3726+create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 3744+create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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, ",
234c6b10 3773+ "INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ",
f7ce3e3a 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
046546ef 3856+ match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
f7ce3e3a 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
046546ef 3881+ match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
f7ce3e3a 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)," ",
bb18ce72 3892+ "WHERE resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
f7ce3e3a 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)," ",
bb18ce72 3911+ "SET resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
f7ce3e3a 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
046546ef
AM
3918+ match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3919+ ?ERROR_MSG("Duplicate key name for ~s", [Resource]),
f7ce3e3a 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+%
0d78319d 3931+% SQL internals
f7ce3e3a 3932+%
3933+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
f7ce3e3a 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)]),
046546ef 3944+ get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
f7ce3e3a 3945+
3946+get_result({updated, MySQLRes}) ->
046546ef 3947+ {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
f7ce3e3a 3948+get_result({data, MySQLRes}) ->
046546ef 3949+ {data, p1_mysql:get_result_rows(MySQLRes)};
f7ce3e3a 3950+get_result({error, "query timed out"}) ->
3951+ {error, "query timed out"};
3952+get_result({error, MySQLRes}) ->
046546ef 3953+ Reason = p1_mysql:get_result_reason(MySQLRes),
f7ce3e3a 3954+ {error, Reason}.
046546ef 3955diff --git a/src/mod_logdb_mysql5.erl b/src/mod_logdb_mysql5.erl
0d78319d 3956new file mode 100644
dd02533f 3957index 0000000000..c05ab958e2
0d78319d 3958--- /dev/null
046546ef 3959+++ b/src/mod_logdb_mysql5.erl
a815cc6c 3960@@ -0,0 +1,979 @@
f7ce3e3a 3961+%%%----------------------------------------------------------------------
3962+%%% File : mod_logdb_mysql5.erl
3f23be8e 3963+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
f7ce3e3a 3964+%%% Purpose : MySQL 5 backend for mod_logdb
3f23be8e 3965+%%% Url : https://paleg.github.io/mod_logdb/
f7ce3e3a 3966+%%%----------------------------------------------------------------------
3967+
3968+-module(mod_logdb_mysql5).
3969+-author('o.palij@gmail.com').
f7ce3e3a 3970+
3971+-include("mod_logdb.hrl").
046546ef 3972+-include("logger.hrl").
f7ce3e3a 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
bb18ce72 3980+-export([start/2, stop/1]).
f7ce3e3a 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,
234c6b10 3988+ get_users_settings/1, get_user_settings/2, set_user_settings/3,
3989+ drop_user/2]).
f7ce3e3a 3990+
3991+% gen_server call timeout
234c6b10 3992+-define(CALL_TIMEOUT, 30000).
3993+-define(MYSQL_TIMEOUT, 60000).
f7ce3e3a 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+
234c6b10 4001+-record(state, {dbref, vhost, server, port, db, user, password}).
f7ce3e3a 4002+
4003+% replace "." with "_"
4004+escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4005+ (A) -> A
046546ef 4006+ end, binary_to_list(VHost)).
f7ce3e3a 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+
234c6b10 4025+temp_table(VHost) ->
4026+ prefix() ++ "temp" ++ suffix(VHost).
4027+
f7ce3e3a 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+
234c6b10 4038+logmessage_name(VHost) ->
4039+ prefix() ++ "logmessage" ++ suffix(VHost).
4040+
f7ce3e3a 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+
046546ef
AM
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, <<"">>),
f7ce3e3a 4067+
234c6b10 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
f7ce3e3a 4073+ {ok, DBRef} ->
234c6b10 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),
f7ce3e3a 4081+ erlang:monitor(process, DBRef),
234c6b10 4082+ {ok, State};
f7ce3e3a 4083+ {error, Reason} ->
4084+ ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
4085+ {stop, db_connection_failed}
4086+ end.
4087+
234c6b10 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,
26b6b0c9 4099+ ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
046546ef
AM
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).
f7ce3e3a 4103+
234c6b10 4104+close_mysql_connection(DBRef) ->
4105+ ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
046546ef 4106+ catch p1_mysql_conn:stop(DBRef).
f7ce3e3a 4107+
f7ce3e3a 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) ->
234c6b10 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};
f7ce3e3a 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),";"],
234c6b10 4143+ {updated, _} = sql_query_internal(DBRef, VQuery),
4144+ ok
f7ce3e3a 4145+ end,
4146+ Reply =
234c6b10 4147+ case catch apply(Fun, []) of
4148+ ok ->
f7ce3e3a 4149+ ok;
234c6b10 4150+ {'EXIT', _} ->
f7ce3e3a 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),
234c6b10 4172+ Query = ["SELECT username, sum(count) as allcount ",
f7ce3e3a 4173+ "FROM ",SName," ",
4174+ "JOIN ",users_table(VHost)," ON owner_id=user_id "
4175+ "WHERE at=\"",Date,"\" ",
234c6b10 4176+ "GROUP BY username ",
4177+ "ORDER BY allcount DESC;"
f7ce3e3a 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) ->
234c6b10 4188+ {reply, get_user_stats_int(DBRef, User, VHost), State};
f7ce3e3a 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;"],
0d78319d 4238+ Reply =
f7ce3e3a 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} ->
046546ef 4291+ case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
f7ce3e3a 4292+ % Already exists
0d78319d 4293+ match ->
f7ce3e3a 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+
234c6b10 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, "',",
046546ef
AM
4322+ "'", binary_to_list(Msg#msg.owner_name), "',",
4323+ "'", binary_to_list(Msg#msg.peer_name), "',",
4324+ "'", binary_to_list(Msg#msg.peer_server), "',",
bb18ce72 4325+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
234c6b10 4326+ "'", atom_to_list(Msg#msg.direction), "',",
046546ef 4327+ "'", binary_to_list(Msg#msg.type), "',",
bb18ce72
AM
4328+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
4329+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
234c6b10 4330+ "'", Msg#msg.timestamp, "');"],
4331+
4332+ case sql_query_internal(DBRef, Query) of
4333+ {updated, _} ->
046546ef
AM
4334+ ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
4335+ [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
234c6b10 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};
f7ce3e3a 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+
234c6b10 4379+terminate(_Reason, #state{dbref=DBRef}=_State) ->
4380+ close_mysql_connection(DBRef),
f7ce3e3a 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),
234c6b10 4393+ gen_server:cast(Proc, {log_message, Msg}).
f7ce3e3a 4394+rebuild_stats(VHost) ->
4395+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
234c6b10 4396+ gen_server:cast(Proc, {rebuild_stats}).
f7ce3e3a 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).
234c6b10 4433+drop_user(User, VHost) ->
4434+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4435+ gen_server:cast(Proc, {drop_user, User}).
f7ce3e3a 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} ->
3f23be8e 4445+ Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
f7ce3e3a 4446+ lists:foldl(fun([Table], Dates) ->
0d78319d 4447+ case re:run(Table, Reg) of
3f23be8e
AM
4448+ {match, _} ->
4449+ case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
0d78319d 4450+ {match, [{S, E}]} ->
3f23be8e 4451+ lists:append(Dates, [lists:sublist(Table, S+1, E)]);
f7ce3e3a 4452+ nomatch ->
4453+ Dates
4454+ end;
234c6b10 4455+ _ ->
f7ce3e3a 4456+ Dates
4457+ end
4458+ end, [], Tables);
4459+ {error, _} ->
4460+ []
4461+ end.
4462+
234c6b10 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).
f7ce3e3a 4482+
234c6b10 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),
f7ce3e3a 4488+
234c6b10 4489+ DQuery = [ "DELETE FROM ",STable," ",
4490+ "WHERE at='",Date,"';"],
f7ce3e3a 4491+
234c6b10 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"]]} ->
234c6b10 4503+ {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
046546ef
AM
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;"]),
234c6b10 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,
f7ce3e3a 4527+
234c6b10 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.
f7ce3e3a 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),
234c6b10 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.
f7ce3e3a 4564+
234c6b10 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.
f7ce3e3a 4602+
234c6b10 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.
f7ce3e3a 4613+
234c6b10 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,"\");"],
f7ce3e3a 4617+ case sql_query_internal(DBRef, Query) of
4618+ {updated, _} ->
234c6b10 4619+ ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
f7ce3e3a 4620+ ok;
234c6b10 4621+ {error, Reason} ->
4622+ ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
f7ce3e3a 4623+ error
4624+ end.
4625+
4626+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4627+%
4628+% tables internals
4629+%
4630+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
234c6b10 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) ->
f7ce3e3a 4646+ SName = stats_table(VHost),
4647+ Query = ["CREATE TABLE ",SName," (",
4648+ "owner_id MEDIUMINT UNSIGNED, ",
234c6b10 4649+ "peer_name_id MEDIUMINT UNSIGNED, ",
4650+ "peer_server_id MEDIUMINT UNSIGNED, ",
f7ce3e3a 4651+ "at VARCHAR(11), ",
4652+ "count INT(11), ",
234c6b10 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;"
f7ce3e3a 4658+ ],
4659+ case sql_query_internal_silent(DBRef, Query) of
4660+ {updated, _} ->
4661+ ?MYDEBUG("Created stats table for ~p", [VHost]),
234c6b10 4662+ rebuild_all_stats_int(State),
f7ce3e3a 4663+ ok;
4664+ {error, Reason} ->
046546ef 4665+ case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
0d78319d 4666+ match ->
f7ce3e3a 4667+ ?MYDEBUG("Stats table for ~p already exists", [VHost]),
234c6b10 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;
f7ce3e3a 4683+ _ ->
4684+ ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
4685+ error
4686+ end
4687+ end.
4688+
234c6b10 4689+create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 4706+create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 4722+create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 4738+create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
f7ce3e3a 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+
234c6b10 4754+create_internals(#state{dbref=DBRef, vhost=VHost}) ->
4755+ sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS ",logmessage_name(VHost),";"]),
f7ce3e3a 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+%
0d78319d 4766+% SQL internals
f7ce3e3a 4767+%
4768+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
f7ce3e3a 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)]),
046546ef 4779+ get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
f7ce3e3a 4780+
4781+get_result({updated, MySQLRes}) ->
046546ef 4782+ {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
f7ce3e3a 4783+get_result({data, MySQLRes}) ->
046546ef 4784+ {data, p1_mysql:get_result_rows(MySQLRes)};
f7ce3e3a 4785+get_result({error, "query timed out"}) ->
4786+ {error, "query timed out"};
4787+get_result({error, MySQLRes}) ->
046546ef 4788+ Reason = p1_mysql:get_result_reason(MySQLRes),
f7ce3e3a 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
046546ef 4804+ match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
f7ce3e3a 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("
234c6b10 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)
f7ce3e3a 4820+BEGIN
0d78319d 4821+ DECLARE ownerID MEDIUMINT UNSIGNED;
f7ce3e3a 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;
0d78319d 4851+ SET @ownerID = LAST_INSERT_ID();
f7ce3e3a 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,\" (
234c6b10 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,
f7ce3e3a 4882+ type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
4883+ subject TEXT,
4884+ body TEXT,
234c6b10 4885+ timestamp DOUBLE NOT NULL,
f7ce3e3a 4886+ ext INTEGER DEFAULT NULL,
234c6b10 4887+ INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id),
f7ce3e3a 4888+ INDEX ext_i (ext),
4889+ FULLTEXT (body)
234c6b10 4890+ ) ENGINE=MyISAM
4891+ PACK_KEYS=1
4892+ CHARACTER SET utf8;\");
f7ce3e3a 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
234c6b10 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;
f7ce3e3a 4935+ IF ROW_COUNT() = 0 THEN
234c6b10 4936+ INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (@ownerID, @peer_nameID, @peer_serverID, atdate, 1);
f7ce3e3a 4937+ END IF;
4938+ END IF;
234c6b10 4939+END;", [logmessage_name(VHost),UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
046546ef 4940diff --git a/src/mod_logdb_pgsql.erl b/src/mod_logdb_pgsql.erl
0d78319d 4941new file mode 100644
dd02533f 4942index 0000000000..202c6ed4a8
0d78319d 4943--- /dev/null
046546ef 4944+++ b/src/mod_logdb_pgsql.erl
a815cc6c 4945@@ -0,0 +1,1104 @@
046546ef
AM
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);" ).
f7ce3e3a 4949+%%%----------------------------------------------------------------------
4950+%%% File : mod_logdb_pgsql.erl
3f23be8e 4951+%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
f7ce3e3a 4952+%%% Purpose : Posgresql backend for mod_logdb
3f23be8e 4953+%%% Url : https://paleg.github.io/mod_logdb/
f7ce3e3a 4954+%%%----------------------------------------------------------------------
4955+
4956+-module(mod_logdb_pgsql).
4957+-author('o.palij@gmail.com').
f7ce3e3a 4958+
4959+-include("mod_logdb.hrl").
046546ef 4960+-include("logger.hrl").
f7ce3e3a 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
bb18ce72 4968+-export([start/2, stop/1]).
f7ce3e3a 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,
234c6b10 4976+ get_users_settings/1, get_user_settings/2, set_user_settings/3,
4977+ drop_user/2]).
4978+
4979+-export([view_table/3]).
f7ce3e3a 4980+
4981+% gen_server call timeout
234c6b10 4982+-define(CALL_TIMEOUT, 30000).
4983+-define(PGSQL_TIMEOUT, 60000).
f7ce3e3a 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+
234c6b10 4990+-record(state, {dbref, vhost, server, port, db, user, password, schema}).
f7ce3e3a 4991+
4992+% replace "." with "_"
4993+escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4994+ (A) -> A
046546ef 4995+ end, binary_to_list(VHost)).
f7ce3e3a 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+
f7ce3e3a 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+
234c6b10 5014+temp_table(VHost, Schema) ->
5015+ prefix(Schema) ++ "temp" ++ suffix(VHost).
5016+
f7ce3e3a 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+
234c6b10 5027+logmessage_name(VHost, Schema) ->
5028+ prefix(Schema) ++ "logmessage" ++ suffix(VHost).
5029+
f7ce3e3a 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]) ->
046546ef
AM
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">>)),
f7ce3e3a 5055+
046546ef 5056+ ?MYDEBUG("Starting pgsql backend for ~s", [VHost]),
234c6b10 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
f7ce3e3a 5064+ {ok, DBRef} ->
234c6b10 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),
f7ce3e3a 5072+ erlang:monitor(process, DBRef),
234c6b10 5073+ {ok, State};
f7ce3e3a 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+
234c6b10 5084+open_pgsql_connection(#state{server=Server, port=Port, db=DB, schema=Schema,
5085+ user=User, password=Password} = _State) ->
26b6b0c9 5086+ ?INFO_MSG("Opening pgsql connection ~s@~s:~p/~s", [User, Server, Port, DB]),
234c6b10 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+
f7ce3e3a 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),
234c6b10 5098+ ViewName = view_table(VHost, Schema, Date),
f7ce3e3a 5099+
234c6b10 5100+ Query = [ "SELECT ", logmessage_name(VHost, Schema)," "
f7ce3e3a 5101+ "('", TableName, "',",
234c6b10 5102+ "'", ViewName, "',",
f7ce3e3a 5103+ "'", Date, "',",
046546ef
AM
5104+ "'", binary_to_list(Msg#msg.owner_name), "',",
5105+ "'", binary_to_list(Msg#msg.peer_name), "',",
5106+ "'", binary_to_list(Msg#msg.peer_server), "',",
bb18ce72 5107+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
234c6b10 5108+ "'", atom_to_list(Msg#msg.direction), "',",
046546ef 5109+ "'", binary_to_list(Msg#msg.type), "',",
bb18ce72
AM
5110+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
5111+ "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
234c6b10 5112+ "'", Msg#msg.timestamp, "');"],
5113+
5114+ case sql_query_internal_silent(DBRef, Query) of
5115+ % TODO: change this
5116+ {data, [{"0"}]} ->
046546ef
AM
5117+ ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
5118+ [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
234c6b10 5119+ ok;
5120+ {error, _Reason} ->
5121+ error
5122+ end,
5123+ {reply, ok, State};
f7ce3e3a 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) ->
234c6b10 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};
f7ce3e3a 5151+handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
f7ce3e3a 5152+ {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5153+ Reply =
234c6b10 5154+ case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date)," CASCADE;"]) of
f7ce3e3a 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),
234c6b10 5186+ Query = ["SELECT username, sum(count) AS allcount ",
f7ce3e3a 5187+ "FROM ",SName," ",
234c6b10 5188+ "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id ",
5189+ "WHERE at='",Date,"' ",
5190+ "GROUP BY username ",
5191+ "ORDER BY allcount DESC;"
f7ce3e3a 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) ->
234c6b10 5206+ {reply, get_user_stats_int(DBRef, Schema, User, VHost), State};
f7ce3e3a 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} ->
046546ef 5282+ ?ERROR_MSG("Failed to get_user_settings for ~s@~s: ~p", [User, VHost, Reason]),
f7ce3e3a 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+
234c6b10 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};
f7ce3e3a 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+
234c6b10 5362+terminate(_Reason, #state{dbref=DBRef}=_State) ->
5363+ close_pgsql_connection(DBRef),
f7ce3e3a 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),
234c6b10 5379+ gen_server:cast(Proc, {rebuild_stats}).
f7ce3e3a 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).
234c6b10 5416+drop_user(User, VHost) ->
5417+ Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5418+ gen_server:cast(Proc, {drop_user, User}).
f7ce3e3a 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) ->
0d78319d
AM
5441+ case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
5442+ {match, [{S, E}]} ->
3f23be8e 5443+ lists:append(Dates, [lists:sublist(Table, S+1, E)]);
f7ce3e3a 5444+ nomatch ->
5445+ Dates
5446+ end
5447+ end, [], Recs);
5448+ {error, _} ->
5449+ []
5450+ end.
5451+
234c6b10 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).
f7ce3e3a 5471+
234c6b10 5472+rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
5473+ TempTable = temp_table(VHost, Schema),
f7ce3e3a 5474+ Fun =
5475+ fun() ->
234c6b10 5476+ Table = messages_table(VHost, Schema, Date),
5477+ STable = stats_table(VHost, Schema),
f7ce3e3a 5478+
5479+ DQuery = [ "DELETE FROM ",STable," ",
5480+ "WHERE at='",Date,"';"],
5481+
234c6b10 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;"],
f7ce3e3a 5489+ case sql_query_internal(DBRef, SQuery) of
5490+ {updated, 0} ->
234c6b10 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;
f7ce3e3a 5515+ {error, _} -> error
5516+ end
234c6b10 5517+ end, % fun
f7ce3e3a 5518+
5519+ case sql_transaction_internal(DBRef, Fun) of
5520+ {atomic, _} ->
046546ef 5521+ ?INFO_MSG("Rebuilded stats for ~s at ~s", [VHost, Date]),
234c6b10 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.
f7ce3e3a 5529+
234c6b10 5530+delete_nonexistent_stats(DBRef, Schema, VHost) ->
f7ce3e3a 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+
234c6b10 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.
f7ce3e3a 5553+
234c6b10 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+ ],
f7ce3e3a 5564+ case sql_query_internal(DBRef, Query) of
234c6b10 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
f7ce3e3a 5575+ {updated, _} ->
234c6b10 5576+ ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
f7ce3e3a 5577+ ok;
5578+ {error, _} ->
5579+ error
5580+ end.
5581+
234c6b10 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+
f7ce3e3a 5615+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5616+%
5617+% tables internals
5618+%
5619+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
234c6b10 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) ->
f7ce3e3a 5636+ SName = stats_table(VHost, Schema),
5637+
5638+ Fun =
5639+ fun() ->
5640+ Query = ["CREATE TABLE ",SName," (",
5641+ "owner_id INTEGER, ",
234c6b10 5642+ "peer_name_id INTEGER, ",
5643+ "peer_server_id INTEGER, ",
f7ce3e3a 5644+ "at VARCHAR(20), ",
5645+ "count integer",
5646+ ");"
5647+ ],
5648+ case sql_query_internal_silent(DBRef, Query) of
5649+ {updated, _} ->
234c6b10 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);"]),
f7ce3e3a 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+ _ ->
046546ef 5658+ ?ERROR_MSG("Failed to create stats table for ~s: ~p", [VHost, Reason]),
f7ce3e3a 5659+ error
5660+ end
5661+ end
5662+ end,
5663+ case sql_transaction_internal(DBRef, Fun) of
5664+ {atomic, created} ->
046546ef 5665+ ?MYDEBUG("Created stats table for ~s", [VHost]),
234c6b10 5666+ rebuild_all_stats_int(State),
5667+ ok;
f7ce3e3a 5668+ {atomic, exists} ->
046546ef 5669+ ?MYDEBUG("Stats table for ~s already exists", [VHost]),
0d78319d 5670+ {match, [{F, L}]} = re:run(SName, "\".*\""),
046546ef 5671+ QTable = lists:sublist(SName, F+2, L-2),
234c6b10 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;
f7ce3e3a 5689+ {error, _} -> error
5690+ end.
5691+
234c6b10 5692+create_settings_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
f7ce3e3a 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, _} ->
046546ef 5703+ ?MYDEBUG("Created settings table for ~s", [VHost]),
f7ce3e3a 5704+ ok;
5705+ {error, Reason} ->
5706+ case lists:keysearch(code, 1, Reason) of
5707+ {value, {code, "42P07"}} ->
046546ef 5708+ ?MYDEBUG("Settings table for ~s already exists", [VHost]),
f7ce3e3a 5709+ ok;
5710+ _ ->
046546ef 5711+ ?ERROR_MSG("Failed to create settings table for ~s: ~p", [VHost, Reason]),
f7ce3e3a 5712+ error
5713+ end
5714+ end.
5715+
234c6b10 5716+create_users_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
f7ce3e3a 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+ _ ->
046546ef 5735+ ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
f7ce3e3a 5736+ error
5737+ end
5738+ end
5739+ end,
5740+ case sql_transaction_internal(DBRef, Fun) of
5741+ {atomic, created} ->
046546ef 5742+ ?MYDEBUG("Created users table for ~s", [VHost]),
f7ce3e3a 5743+ ok;
0d78319d 5744+ {atomic, exists} ->
046546ef 5745+ ?MYDEBUG("Users table for ~s already exists", [VHost]),
f7ce3e3a 5746+ ok;
5747+ {aborted, _} -> error
5748+ end.
5749+
234c6b10 5750+create_servers_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
f7ce3e3a 5751+ SName = servers_table(VHost, Schema),
f7ce3e3a 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+ _ ->
046546ef 5768+ ?ERROR_MSG("Failed to create servers table for ~s: ~p", [VHost, Reason]),
f7ce3e3a 5769+ error
5770+ end
5771+ end
5772+ end,
5773+ case sql_transaction_internal(DBRef, Fun) of
5774+ {atomic, created} ->
046546ef 5775+ ?MYDEBUG("Created servers table for ~s", [VHost]),
f7ce3e3a 5776+ ok;
5777+ {atomic, exists} ->
046546ef 5778+ ?MYDEBUG("Servers table for ~s already exists", [VHost]),
f7ce3e3a 5779+ ok;
5780+ {aborted, _} -> error
5781+ end.
5782+
234c6b10 5783+create_resources_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
f7ce3e3a 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+ _ ->
046546ef 5800+ ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
f7ce3e3a 5801+ error
5802+ end
5803+ end
5804+ end,
5805+ case sql_transaction_internal(DBRef, Fun) of
5806+ {atomic, created} ->
046546ef 5807+ ?MYDEBUG("Created resources table for ~s", [VHost]),
f7ce3e3a 5808+ ok;
5809+ {atomic, exists} ->
046546ef 5810+ ?MYDEBUG("Resources table for ~s already exists", [VHost]),
f7ce3e3a 5811+ ok;
5812+ {aborted, _} -> error
5813+ end.
5814+
26b6b0c9 5815+create_internals(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
234c6b10 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);"]),
f7ce3e3a 5817+ case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
5818+ {updated, _} ->
5819+ ?MYDEBUG("Created logmessage for ~p", [VHost]),
5820+ ok;
26b6b0c9
AM
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
f7ce3e3a 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),
234c6b10 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 $$
f7ce3e3a 5859+DECLARE
5860+ ownerID INTEGER;
5861+ peer_nameID INTEGER;
5862+ peer_serverID INTEGER;
5863+ peer_resourceID INTEGER;
5864+ tablename ALIAS for $1;
234c6b10 5865+ viewname ALIAS for $2;
5866+ atdate ALIAS for $3;
f7ce3e3a 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
234c6b10 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 || ')';
f7ce3e3a 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)';
234c6b10 5905+ EXECUTE 'CREATE INDEX \"search_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id)';
f7ce3e3a 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+
234c6b10 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 || ')';
f7ce3e3a 5931+ END;
5932+
234c6b10 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;
f7ce3e3a 5934+ IF NOT FOUND THEN
234c6b10 5935+ INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (ownerID, peer_nameID, peer_serverID, atdate, 1);
f7ce3e3a 5936+ END IF;
5937+ RETURN 0;
5938+END;
5939+$$ LANGUAGE plpgsql;
234c6b10 5940+", [logmessage_name(VHost,Schema),UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),UName,UName,SName,RName,StName,StName]).
f7ce3e3a 5941+
5942+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5943+%
0d78319d 5944+% SQL internals
f7ce3e3a 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};
0d78319d
AM
5985+ Rez -> Rez
5986+ end.
234c6b10 5987+
0d78319d
AM
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)).
234c6b10 5992+
0d78319d
AM
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};
046546ef 6007+get_result({ok, [{[$S, $E, $L, $E, $C, $T, $ | _Rest], _Rows, Recs}]}) ->
0d78319d
AM
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}.
bb18ce72 6049+
046546ef 6050diff --git a/src/mod_roster.erl b/src/mod_roster.erl
dd02533f 6051index 426589319c..6b51d3c381 100644
046546ef
AM
6052--- a/src/mod_roster.erl
6053+++ b/src/mod_roster.erl
08278fc0 6054@@ -65,6 +65,8 @@
a815cc6c
AM
6055 -define(ROSTER_ITEM_CACHE, roster_item_cache).
6056 -define(ROSTER_VERSION_CACHE, roster_version_cache).
0d78319d 6057
234c6b10 6058+-include("mod_logdb.hrl").
0d78319d 6059+
08278fc0 6060 -type c2s_state() :: ejabberd_c2s:state().
046546ef 6061 -export_type([subscription/0]).
234c6b10 6062
dd02533f 6063@@ -943,6 +945,14 @@ user_roster(User, Server, Query, Lang) ->
046546ef 6064 Query),
234c6b10 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+
046546ef 6075 FItems = case SItems of
dd02533f 6076 [] -> [?CT(?T("None"))];
046546ef 6077 _ ->
dd02533f 6078@@ -1000,7 +1010,33 @@ user_roster(User, Server, Query, Lang) ->
046546ef
AM
6079 [?INPUTT(<<"submit">>,
6080 <<"remove",
6081 (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
08278fc0 6082- ?T("Remove"))])])
dd02533f
AM
6083+ ?T("Remove"))]),
6084+ case gen_mod:is_loaded(Server, mod_logdb) of
046546ef 6085+ true ->
3f23be8e 6086+ Peer = jid:encode(R#roster.jid),
046546ef
AM
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">>,
bb18ce72 6103+ <<Name/binary,
046546ef
AM
6104+ (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6105+ Value)]);
6106+ false ->
6107+ ?X([])
dd02533f 6108+ end
046546ef
AM
6109+ ])
6110 end,
6111 SItems)))])]
6112 end,
dd02533f 6113@@ -1107,9 +1143,42 @@ user_roster_item_parse_query(User, Server, Items,
3f23be8e
AM
6114 sub_els = [#roster_query{
6115 items = [RosterItem]}]}),
046546ef
AM
6116 throw(submitted);
6117- false -> ok
6118- end
6119- end
6120+ false ->
6121+ case lists:keysearch(
bb18ce72 6122+ <<"donotlog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
046546ef 6123+ {value, _} ->
3f23be8e 6124+ Peer = jid:encode(JID),
046546ef
AM
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,
3f23be8e 6130+ DLL = lists:delete(jid:encode(JID), Settings#user_settings.dolog_list),
046546ef
AM
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(
bb18ce72 6137+ <<"dolog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
046546ef 6138+ {value, _} ->
3f23be8e 6139+ Peer = jid:encode(JID),
046546ef
AM
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,
3f23be8e 6145+ DNLL = lists:delete(jid:encode(JID), Settings#user_settings.donotlog_list),
046546ef
AM
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
234c6b10 6155+ end % validate
046546ef
AM
6156 end,
6157 Items),
234c6b10 6158 nothing.
a815cc6c 6159
dd02533f
AM
6160From 5043114bc1a74caa522e8a1569b485ccc1808a79 Mon Sep 17 00:00:00 2001
6161From: Oleh Palii <o.palij@gmail.com>
6162Date: Sat, 31 Aug 2019 15:23:19 +0300
6163Subject: [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
6172diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
6173index 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];
6614diff --git a/src/mod_logdb_mysql.erl b/src/mod_logdb_mysql.erl
6615index 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,
6635diff --git a/src/mod_logdb_mysql5.erl b/src/mod_logdb_mysql5.erl
6636index 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,
6656diff --git a/src/mod_logdb_pgsql.erl b/src/mod_logdb_pgsql.erl
6657index 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
6680From 55274ef5a3deb5979e0d97cdb48768eb472c36ec Mon Sep 17 00:00:00 2001
6681From: Oleh Palii <o.palij@gmail.com>
6682Date: Sat, 31 Aug 2019 22:43:11 +0300
6683Subject: [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
6689diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
6690index 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 1.408687 seconds and 4 git commands to generate.