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