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