1 diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg
2 index 9dfd896b..f35ff07a 100644
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"}.
10 +{"Users Messages", "Gebruikersberichten"}.
13 +{"Logged messages for ~s", "Gelogde berichten van ~s"}.
14 +{"Logged messages for ~s at ~s", "Gelogde berichten van ~s op ~s"}.
16 +{"No logged messages for ~s", "Geen gelogde berichten van ~s"}.
17 +{"No logged messages for ~s at ~s", "Geen gelogde berichten van ~s op ~s"}.
18 +{"Date, Time", "Datum en tijd"}.
19 +{"Direction: Jid", "Richting: Jabber ID"}.
20 +{"Subject", "Onderwerp"}.
21 +{"Body", "Berichtveld"}.
22 +{"Messages", "Berichten"}.
23 diff --git a/priv/msgs/pl.msg b/priv/msgs/pl.msg
24 index 6b303cc9..664f1c95 100644
25 --- a/priv/msgs/pl.msg
26 +++ b/priv/msgs/pl.msg
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"}.
32 +{"Users Messages", "Wiadomości użytkownika"}.
35 +{"Logged messages for ~s", "Zapisane wiadomości dla ~s"}.
36 +{"Logged messages for ~s at", "Zapisane wiadomości dla ~s o ~s"}.
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"}.
44 +{"Messages","Wiadomości"}.
45 +{"Filter Selected", "Odfiltruj zaznaczone"}.
46 +{"Do Not Log Messages", "Nie zapisuj wiadomości"}.
47 +{"Log Messages", "Zapisuj wiadomości"}.
48 +{"Messages logging engine", "System zapisywania historii rozmów"}.
49 +{"Default", "Domyślne"}.
50 +{"Set logging preferences", "Ustaw preferencje zapisywania"}.
51 +{"Messages logging engine settings", "Ustawienia systemu logowania"}.
52 +{"Set run-time settings", "Zapisz ustawienia systemu logowania"}.
53 +{"Groupchat messages logging", "Zapisywanie rozmów z konferencji"}.
54 +{"Jids/Domains to ignore", "JID/Domena która ma być ignorowana"}.
55 +{"Purge messages older than (days)", "Usuń wiadomości starsze niż (w dniach)"}.
56 +{"Poll users settings (seconds)", "Czas aktualizacji preferencji użytkowników (sekundy)"}.
57 diff --git a/priv/msgs/ru.msg b/priv/msgs/ru.msg
58 index 05849f51..b87bf9bb 100644
59 --- a/priv/msgs/ru.msg
60 +++ b/priv/msgs/ru.msg
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"}.
66 +{"Users Messages", "Сообщения пользователей"}.
68 +{"Count", "Количество"}.
69 +{"Logged messages for ~s", "Сохранённые cообщения для ~s"}.
70 +{"Logged messages for ~s at ~s", "Сохранённые cообщения для ~s за ~s"}.
72 +{"No logged messages for ~s", "Отсутствуют сообщения для ~s"}.
73 +{"No logged messages for ~s at ~s", "Отсутствуют сообщения для ~s за ~s"}.
74 +{"Date, Time", "Дата, Время"}.
75 +{"Direction: Jid", "Направление: Jid"}.
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)", "Обновлять настройки пользователей через (секунд)"}.
93 +{"Do not drop", "Не удалять"}.
94 +{"Drop messages on user removal", "Удалять сообщения при удалении пользователя"}.
95 diff --git a/priv/msgs/uk.msg b/priv/msgs/uk.msg
96 index a1159b53..4bdab4c5 100644
97 --- a/priv/msgs/uk.msg
98 +++ b/priv/msgs/uk.msg
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"}.
104 +{"Users Messages", "Повідомлення користувачів"}.
106 +{"Count", "Кількість"}.
107 +{"Logged messages for ~s", "Збережені повідомлення для ~s"}.
108 +{"Logged messages for ~s at ~s", "Збережені повідомлення для ~s за ~s"}.
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", "Тема"}.
116 +{"Messages", "Повідомлення"}.
117 +{"Filter Selected", "Відфільтрувати виділені"}.
118 +{"Do Not Log Messages", "Не зберігати повідомлення"}.
119 +{"Log Messages", "Зберігати повідомлення"}.
120 +{"Messages logging engine", "Система збереження повідомлень"}.
121 +{"Default", "За замовчуванням"}.
122 +{"Set logging preferences", "Вкажіть налагоджування збереження повідомлень"}.
123 +{"Messages logging engine users", "Користувачі системи збереження повідомлень"}.
124 +{"Messages logging engine settings", "Налагоджування системи збереження повідомлень"}.
125 +{"Set run-time settings", "Вкажіть поточні налагоджування"}.
126 +{"Groupchat messages logging", "Збереження повідомлень типу groupchat"}.
127 +{"Jids/Domains to ignore", "Ігнорувати наступні jids/домени"}.
128 +{"Purge messages older than (days)", "Видаляти повідомлення старіші ніж (дні)"}.
129 +{"Poll users settings (seconds)", "Оновлювати налагоджування користувачів кожні (секунд)"}.
130 +{"Drop", "Видаляти"}.
131 +{"Do not drop", "Не видаляти"}.
132 +{"Drop messages on user removal", "Видаляти повідомлення під час видалення користувача"}.
133 diff --git a/rebar.config b/rebar.config
134 index 477343c5..db58ab69 100644
138 {eimp, ".*", {git, "https://github.com/processone/eimp", {tag, "1.0.9"}}},
139 {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.26"}}}},
140 {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.27"}}}},
141 - {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
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",
147 {if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3",
148 diff --git a/src/gen_logdb.erl b/src/gen_logdb.erl
150 index 00000000..8bad1129
152 +++ b/src/gen_logdb.erl
154 +%%%----------------------------------------------------------------------
155 +%%% File : gen_logdb.erl
156 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
157 +%%% Purpose : Describes generic behaviour for mod_logdb backends.
158 +%%% Url : https://paleg.github.io/mod_logdb/
159 +%%%----------------------------------------------------------------------
162 +-author('o.palij@gmail.com').
164 +-export([behaviour_info/1]).
166 +behaviour_info(callbacks) ->
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"},
175 + % {db, "logdb"}] | ...
176 + % VHost = list() -> "jabber.example.org"
179 + % called from cleanup/1
180 + % it should logoff database and do cleanup
182 + % Types: VHost = list() -> "jabber.example.org"
185 + % called from handle_call({addlog, _}, _, _)
186 + % it should log messages to database
187 + % log_message(VHost, Msg) -> ok | error
189 + % VHost = list() -> "jabber.example.org"
190 + % Msg = record() -> #msg
193 + % called from ejabberdctl rebuild_stats
194 + % it should rebuild stats table (if used) for vhost
195 + % rebuild_stats(VHost)
197 + % VHost = list() -> "jabber.example.org"
198 + {rebuild_stats, 1},
200 + % it should rebuild stats table (if used) for vhost at Date
201 + % rebuild_stats_at(VHost, Date)
203 + % VHost = list() -> "jabber.example.org"
204 + % Date = list() -> "2007-02-12"
205 + {rebuild_stats_at, 2},
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
211 + % VHost = list() -> "jabber.example.org"
212 + % Msgs = list() -> [ #msg1, msg2, ... ]
213 + % Date = list() -> "2007-02-12"
214 + {delete_messages_by_user_at, 3},
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
220 + % User = list() -> "admin"
221 + % VHost = list() -> "jabber.example.org"
222 + % Date = list() -> "2007-02-12"
223 + {delete_all_messages_by_user_at, 3},
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
229 + % VHost = list() -> "jabber.example.org"
230 + % Date = list() -> "2007-02-12"
231 + {delete_messages_at, 2},
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}, ... ]} |
238 + % VHost = list() -> "jabber.example.org"
239 + % DateN = list() -> "2007-02-12"
240 + % Msgs_countN = number() -> 241
241 + {get_vhost_stats, 1},
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}, ....]} |
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},
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}, ...]} |
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},
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}
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},
275 + % called from many places
276 + % it should return list of dates for vhost
277 + % get_dates(VHost) -> [Date1, Date2, ... ]
279 + % VHost = list() -> "jabber.example.org"
280 + % DateN = list() -> "2007-02-12"
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
287 + % VHost = list() -> "jabber.example.org"
288 + {get_users_settings, 1},
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}
294 + % User = list() -> "admin"
295 + % VHost = list() -> "jabber.example.org"
296 + {get_user_settings, 2},
298 + % called from web admin
299 + % it should set User settings at VHost
300 + % set_user_settings(User, VHost, #user_settings) -> ok | error
302 + % User = list() -> "admin"
303 + % VHost = list() -> "jabber.example.org"
304 + {set_user_settings, 3},
306 + % called from remove_user (ejabberd hook)
307 + % it should remove user messages and settings at VHost
308 + % drop_user(User, VHost) -> ok | error
310 + % User = list() -> "admin"
311 + % VHost = list() -> "jabber.example.org"
314 +behaviour_info(_) ->
316 diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
318 index 00000000..bf0240d1
320 +++ b/src/mod_logdb.erl
322 +%%%----------------------------------------------------------------------
323 +%%% File : mod_logdb.erl
324 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
325 +%%% Purpose : Frontend for log user messages to db
326 +%%% Url : https://paleg.github.io/mod_logdb/
327 +%%%----------------------------------------------------------------------
330 +-author('o.palij@gmail.com').
332 +-behaviour(gen_server).
333 +-behaviour(gen_mod).
336 +-export([start_link/2]).
338 +-export([start/2, stop/1,
340 + depends/2, reload/3]).
342 +-export([code_change/3,
343 + handle_call/3, handle_cast/2, handle_info/2,
344 + init/1, terminate/2]).
346 +-export([send_packet/1, receive_packet/1, offline_message/1, remove_user/2]).
347 +-export([get_local_identity/5,
348 + get_local_features/5,
350 + adhoc_local_items/4,
351 + adhoc_local_commands/4
354 +-export([rebuild_stats/1,
355 + copy_messages/1, copy_messages_ctl/3, copy_messages_int_tc/1]).
357 +-export([get_vhost_stats/1, get_vhost_stats_at/2,
358 + get_user_stats/2, get_user_messages_at/3,
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]).
370 +-export([webadmin_menu/3,
373 + user_parse_query/5]).
375 +-export([vhost_messages_stats/3,
376 + vhost_messages_stats_at/4,
377 + user_messages_stats/4,
378 + user_messages_stats_at/5]).
380 +-include("mod_logdb.hrl").
381 +-include("xmpp.hrl").
382 +-include("mod_roster.hrl").
383 +-include("ejabberd_commands.hrl").
384 +-include("adhoc.hrl").
385 +-include("ejabberd_web_admin.hrl").
386 +-include("ejabberd_http.hrl").
387 +-include("logger.hrl").
389 +-define(PROCNAME, ejabberd_mod_logdb).
390 +% gen_server call timeout
391 +-define(CALL_TIMEOUT, 10000).
393 +-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings, drop_messages_on_user_removal}).
395 +ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ binary_to_list(VHost)).
397 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
399 +% gen_mod/gen_server callbacks
401 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
402 +% ejabberd starts module
403 +start(VHost, Opts) ->
405 + {gen_mod:get_module_proc(VHost, ?PROCNAME),
406 + {?MODULE, start_link, [VHost, Opts]},
411 + % add child to ejabberd_sup
412 + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec).
414 +depends(_Host, _Opts) ->
417 +reload(_Host, _NewOpts, _OldOpts) ->
421 +% supervisor starts gen_server
422 +start_link(VHost, Opts) ->
423 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
424 + {ok, Pid} = gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []),
428 +init([VHost, Opts]) ->
429 + process_flag(trap_exit, true),
430 + DBsRaw = gen_mod:get_opt(dbs, Opts, fun(A) -> A end, [{mnesia, []}]),
431 + DBs = case lists:keysearch(mnesia, 1, DBsRaw) of
432 + false -> lists:append(DBsRaw, [{mnesia,[]}]);
433 + {value, _} -> DBsRaw
435 + VHostDB = gen_mod:get_opt(vhosts, Opts, fun(A) -> A end, [{VHost, mnesia}]),
436 + % 10 is default because of using in clustered environment
437 + PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, fun(A) -> A end, 10),
440 + case lists:keysearch(VHost, 1, VHostDB) of
442 + ?WARNING_MSG("There is no logging backend defined for '~s', switching to mnesia", [VHost]),
444 + {value,{_, DBNameResult}} ->
445 + case lists:keysearch(DBNameResult, 1, DBs) of
447 + ?WARNING_MSG("There is no such logging backend '~s' defined for '~s', switching to mnesia", [DBNameResult, VHost]),
449 + {value, {_, DBOptsResult}} ->
450 + {DBNameResult, DBOptsResult}
454 + ?MYDEBUG("Starting mod_logdb for '~s' with '~s' backend", [VHost, DBName]),
456 + DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)),
458 + {ok, #state{vhost=VHost,
461 + % dbs used for convert messages from one backend to other
463 + dolog_default=gen_mod:get_opt(dolog_default, Opts, fun(A) -> A end, true),
464 + drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts, fun(A) -> A end, true),
465 + ignore_jids=gen_mod:get_opt(ignore_jids, Opts, fun(A) -> A end, []),
466 + groupchat=gen_mod:get_opt(groupchat, Opts, fun(A) -> A end, none),
467 + purge_older_days=gen_mod:get_opt(purge_older_days, Opts, fun(A) -> A end, never),
468 + poll_users_settings=PollUsersSettings}}.
470 +cleanup(#state{vhost=VHost} = _State) ->
471 + ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
473 + %ets:delete(ets_settings_table(VHost)),
475 + ejabberd_hooks:delete(remove_user, VHost, ?MODULE, remove_user, 90),
476 + ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90),
477 + ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
478 + ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_message, 40),
480 + ejabberd_hooks:delete(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
481 + ejabberd_hooks:delete(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
482 + ejabberd_hooks:delete(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
483 + ejabberd_hooks:delete(disco_local_features, VHost, ?MODULE, get_local_features, 50),
484 + ejabberd_hooks:delete(disco_local_items, VHost, ?MODULE, get_local_items, 50),
486 + ejabberd_hooks:delete(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
487 + ejabberd_hooks:delete(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
488 + ejabberd_hooks:delete(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
489 + ejabberd_hooks:delete(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
491 + ?MYDEBUG("Removed hooks for ~p", [VHost]),
493 + ejabberd_commands:unregister_commands(get_commands_spec()),
494 + ?MYDEBUG("Unregistered commands for ~p", [VHost]).
497 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
498 + %gen_server:call(Proc, {cleanup}),
499 + %?MYDEBUG("Cleanup in stop finished!!!!", []),
500 + %timer:sleep(10000),
501 + ok = supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
502 + ok = supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
504 +get_commands_spec() ->
505 + [#ejabberd_commands{name = rebuild_stats, tags = [logdb],
506 + desc = "Rebuild mod_logdb stats for given host",
507 + module = ?MODULE, function = rebuild_stats,
508 + args = [{host, binary}],
509 + result = {res, rescode}},
510 + #ejabberd_commands{name = copy_messages, tags = [logdb],
511 + desc = "Copy logdb messages from given backend to current backend for given host",
512 + module = ?MODULE, function = copy_messages_ctl,
513 + args = [{host, binary}, {backend, binary}, {date, binary}],
514 + result = {res, rescode}}].
516 +mod_opt_type(dbs) ->
517 + fun (A) when is_list(A) -> A end;
518 +mod_opt_type(vhosts) ->
519 + fun (A) when is_list(A) -> A end;
520 +mod_opt_type(poll_users_settings) ->
521 + fun (I) when is_integer(I) -> I end;
522 +mod_opt_type(groupchat) ->
527 +mod_opt_type(dolog_default) ->
528 + fun (B) when is_boolean(B) -> B end;
529 +mod_opt_type(ignore_jids) ->
530 + fun (A) when is_list(A) -> A end;
531 +mod_opt_type(purge_older_days) ->
532 + fun (I) when is_integer(I) -> I end;
534 + [dbs, vhosts, poll_users_settings, groupchat, dolog_default, ignore_jids, purge_older_days].
536 +handle_call({cleanup}, _From, State) ->
538 + ?MYDEBUG("Cleanup finished!!!!!", []),
539 + {reply, ok, State};
540 +handle_call({get_dates}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
541 + Reply = DBMod:get_dates(VHost),
542 + {reply, Reply, State};
543 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
544 +% ejabberd_web_admin callbacks
545 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
546 +handle_call({delete_messages_by_user_at, PMsgs, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
547 + Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, binary_to_list(Date)),
548 + {reply, Reply, State};
549 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
550 + Reply = DBMod:delete_all_messages_by_user_at(binary_to_list(User), VHost, binary_to_list(Date)),
551 + {reply, Reply, State};
552 +handle_call({delete_messages_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
553 + Reply = DBMod:delete_messages_at(VHost, Date),
554 + {reply, Reply, State};
555 +handle_call({get_vhost_stats}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
556 + Reply = DBMod:get_vhost_stats(VHost),
557 + {reply, Reply, State};
558 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
559 + Reply = DBMod:get_vhost_stats_at(VHost, binary_to_list(Date)),
560 + {reply, Reply, State};
561 +handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
562 + Reply = DBMod:get_user_stats(binary_to_list(User), VHost),
563 + {reply, Reply, State};
564 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
565 + Reply = DBMod:get_user_messages_at(binary_to_list(User), VHost, binary_to_list(Date)),
566 + {reply, Reply, State};
567 +handle_call({get_user_settings, User}, _From, #state{dbmod=_DBMod, vhost=VHost}=State) ->
568 + Reply = case ets:match_object(ets_settings_table(VHost),
569 + #user_settings{owner_name=User, _='_'}) of
571 + _ -> #user_settings{owner_name=User,
572 + dolog_default=State#state.dolog_default,
576 + {reply, Reply, State};
577 +% TODO: remove User ??
578 +handle_call({set_user_settings, User, GSet}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
579 + Set = GSet#user_settings{owner_name=User},
581 + case ets:match_object(ets_settings_table(VHost),
582 + #user_settings{owner_name=User, _='_'}) of
586 + case DBMod:set_user_settings(binary_to_list(User), VHost, Set) of
590 + true = ets:insert(ets_settings_table(VHost), Set),
594 + {reply, Reply, State};
595 +handle_call({get_module_settings}, _From, State) ->
596 + {reply, State, State};
597 +handle_call({set_module_settings, #state{purge_older_days=PurgeDays,
598 + poll_users_settings=PollSec} = Settings},
600 + #state{purgeRef=PurgeRefOld,
601 + pollRef=PollRefOld,
602 + purge_older_days=PurgeDaysOld,
603 + poll_users_settings=PollSecOld} = State) ->
605 + PurgeDays == never, PurgeDaysOld /= never ->
606 + {ok, cancel} = timer:cancel(PurgeRefOld),
608 + is_integer(PurgeDays), PurgeDaysOld == never ->
609 + set_purge_timer(PurgeDays);
615 + PollSec == PollSecOld ->
617 + PollSec == 0, PollSecOld /= 0 ->
618 + {ok, cancel} = timer:cancel(PollRefOld),
620 + is_integer(PollSec), PollSecOld == 0 ->
621 + set_poll_timer(PollSec);
622 + is_integer(PollSec), PollSecOld /= 0 ->
623 + {ok, cancel} = timer:cancel(PollRefOld),
624 + set_poll_timer(PollSec)
627 + NewState = State#state{dolog_default=Settings#state.dolog_default,
628 + ignore_jids=Settings#state.ignore_jids,
629 + groupchat=Settings#state.groupchat,
630 + drop_messages_on_user_removal=Settings#state.drop_messages_on_user_removal,
631 + purge_older_days=PurgeDays,
632 + poll_users_settings=PollSec,
635 + {reply, ok, NewState};
636 +handle_call(Msg, _From, State) ->
637 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
639 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
640 +% end ejabberd_web_admin callbacks
641 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
643 +% ejabberd_hooks call
644 +handle_cast({addlog, Direction, Owner, Peer, Packet}, #state{dbmod=DBMod, vhost=VHost}=State) ->
645 + case filter(Owner, Peer, State) of
647 + case catch packet_parse(Owner, Peer, Packet, Direction, State) of
650 + {'EXIT', Reason} ->
651 + ?ERROR_MSG("Failed to parse: ~p", [Reason]);
653 + DBMod:log_message(VHost, Msg)
659 +handle_cast({remove_user, User}, #state{dbmod=DBMod, vhost=VHost}=State) ->
660 + case State#state.drop_messages_on_user_removal of
662 + DBMod:drop_user(binary_to_list(User), VHost),
663 + ?INFO_MSG("Launched ~s@~s removal", [User, VHost]);
665 + ?INFO_MSG("Message removing is disabled. Keeping messages for ~s@~s", [User, VHost])
668 +% ejabberdctl rebuild_stats/3
669 +handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) ->
670 + DBMod:rebuild_stats(VHost),
672 +handle_cast({copy_messages, Backend}, State) ->
673 + spawn(?MODULE, copy_messages, [[State, Backend, []]]),
675 +handle_cast({copy_messages, Backend, Date}, State) ->
676 + spawn(?MODULE, copy_messages, [[State, Backend, [binary_to_list(Date)]]]),
678 +handle_cast(Msg, State) ->
679 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
682 +% return: disabled | timer reference
683 +set_purge_timer(PurgeDays) ->
686 + Days when is_integer(Days) ->
687 + {ok, Ref1} = timer:send_interval(timer:hours(24), scheduled_purging),
691 +% return: disabled | timer reference
692 +set_poll_timer(PollSec) ->
695 + {ok, Ref2} = timer:send_interval(timer:seconds(PollSec), poll_users_settings),
697 + % db polling disabled
701 + {ok, Ref3} = timer:send_interval(timer:seconds(10), poll_users_settings),
705 +% actual starting of logging
706 +% from timer:send_after (in init)
707 +handle_info(start, #state{dbmod=DBMod, vhost=VHost}=State) ->
708 + case DBMod:start(VHost, State#state.dbopts) of
709 + {error,{already_started,_}} ->
710 + ?MYDEBUG("backend module already started - trying to stop it", []),
712 + {stop, already_started, State};
714 + timer:sleep(30000),
715 + ?ERROR_MSG("Failed to start: ~p", [Reason]),
716 + {stop, db_connection_failed, State};
718 + ?INFO_MSG("~p connection established", [DBMod]),
720 + MonRef = erlang:monitor(process, SPid),
722 + ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]),
723 + DoLog = case DBMod:get_users_settings(VHost) of
724 + {ok, Settings} -> [Sett#user_settings{owner_name = iolist_to_binary(Sett#user_settings.owner_name)} || Sett <- Settings];
725 + {error, _Reason} -> []
727 + ets:insert(ets_settings_table(VHost), DoLog),
729 + TrefPurge = set_purge_timer(State#state.purge_older_days),
730 + TrefPoll = set_poll_timer(State#state.poll_users_settings),
732 + ejabberd_hooks:add(remove_user, VHost, ?MODULE, remove_user, 90),
733 + ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90),
734 + ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
735 + ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_message, 40),
737 + ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
738 + ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 50),
739 + ejabberd_hooks:add(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
740 + ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 50),
741 + ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
743 + ejabberd_hooks:add(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
744 + ejabberd_hooks:add(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
745 + ejabberd_hooks:add(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
746 + ejabberd_hooks:add(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
748 + ?MYDEBUG("Added hooks for ~p", [VHost]),
750 + ejabberd_commands:register_commands(get_commands_spec()),
751 + ?MYDEBUG("Registered commands for ~p", [VHost]),
753 + NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll},
754 + {noreply, NewState};
756 + ?ERROR_MSG("Rez=~p", [Rez]),
757 + timer:sleep(30000),
758 + {stop, db_connection_failed, State}
760 +% from timer:send_interval/2 (in start handle_info)
761 +handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = State) ->
762 + ?MYDEBUG("Starting scheduled purging of old records for ~p", [VHost]),
763 + spawn(?MODULE, purge_old_records, [VHost, integer_to_list(Days)]),
765 +% from timer:send_interval/2 (in start handle_info)
766 +handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) ->
767 + {ok, DoLog} = DBMod:get_users_settings(VHost),
768 + ?MYDEBUG("DoLog=~p", [DoLog]),
769 + true = ets:delete_all_objects(ets_settings_table(VHost)),
770 + ets:insert(ets_settings_table(VHost), DoLog),
772 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
773 + {stop, db_connection_dropped, State};
774 +handle_info({fetch_result, _, _}, State) ->
775 + ?MYDEBUG("Got timed out mysql fetch result", []),
777 +handle_info(Info, State) ->
778 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
781 +terminate(db_connection_failed, _State) ->
783 +terminate(db_connection_dropped, State) ->
784 + ?MYDEBUG("Got terminate with db_connection_dropped", []),
787 +terminate(Reason, #state{monref=undefined} = State) ->
788 + ?MYDEBUG("Got terminate with undefined monref.~nReason: ~p", [Reason]),
791 +terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) ->
792 + ?INFO_MSG("Reason: ~p", [Reason]),
793 + case erlang:is_process_alive(Pid) of
795 + erlang:demonitor(MonRef, [flush]),
803 +code_change(_OldVsn, State, _Extra) ->
806 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
808 +% ejabberd_hooks callbacks
810 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
811 +% TODO: change to/from to list as sql stores it as list
812 +send_packet({Pkt, #{jid := Owner} = C2SState}) ->
813 + VHost = Owner#jid.lserver,
814 + Peer = xmpp:get_to(Pkt),
815 + %?MYDEBUG("send_packet. Peer=~p, Owner=~p", [Peer, Owner]),
816 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
817 + gen_server:cast(Proc, {addlog, to, Owner, Peer, Pkt}),
820 +receive_packet({Pkt, #{jid := Owner} = C2SState}) ->
821 + VHost = Owner#jid.lserver,
822 + Peer = xmpp:get_from(Pkt),
823 + %?MYDEBUG("receive_packet. Pkt=~p", [Pkt]),
824 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
825 + gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
828 +offline_message({_Action, #message{from = Peer, to = Owner} = Pkt} = Acc) ->
829 + VHost = Owner#jid.lserver,
830 + %?MYDEBUG("offline_message. Pkt=~p", [Pkt]),
831 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
832 + gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
835 +remove_user(User, Server) ->
836 + LUser = jid:nodeprep(User),
837 + LServer = jid:nameprep(Server),
838 + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME),
839 + gen_server:cast(Proc, {remove_user, LUser}).
841 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
845 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
846 +rebuild_stats(VHost) ->
847 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
848 + gen_server:cast(Proc, {rebuild_stats}),
851 +copy_messages_ctl(VHost, Backend, <<"all">>) ->
852 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
853 + gen_server:cast(Proc, {copy_messages, Backend}),
855 +copy_messages_ctl(VHost, Backend, Date) ->
856 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
857 + gen_server:cast(Proc, {copy_messages, Backend, Date}),
860 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
864 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
866 +% handle_cast({addlog, E}, _)
867 +% raw packet -> #msg
868 +packet_parse(_Owner, _Peer, #message{type = error}, _Direction, _State) ->
870 +packet_parse(_Owner, _Peer, #message{meta = #{sm_copy := true}}, _Direction, _State) ->
872 +packet_parse(_Owner, _Peer, #message{meta = #{from_offline := true}}, _Direction, _State) ->
874 +packet_parse(Owner, Peer, #message{body = Body, subject = Subject, type = Type}, Direction, State) ->
875 + %?MYDEBUG("Owner=~p, Peer=~p, Direction=~p", [Owner, Peer, Direction]),
876 + %?MYDEBUG("Body=~p, Subject=~p, Type=~p", [Body, Subject, Type]),
877 + SubjectText = xmpp:get_text(Subject),
878 + BodyText = xmpp:get_text(Body),
879 + if (SubjectText == <<"">>) and (BodyText == <<"">>) ->
885 + groupchat when State#state.groupchat == send, Direction == to ->
887 + groupchat when State#state.groupchat == send, Direction == from ->
889 + groupchat when State#state.groupchat == none ->
895 + #msg{timestamp = get_timestamp(),
896 + owner_name = stringprep:tolower(Owner#jid.user),
897 + peer_name = stringprep:tolower(Peer#jid.user),
898 + peer_server = stringprep:tolower(Peer#jid.server),
899 + peer_resource = Peer#jid.resource,
900 + direction = Direction,
901 + type = misc:atom_to_binary(Type),
902 + subject = SubjectText,
904 +packet_parse(_, _, _, _, _) ->
907 +% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages)
908 +filter(Owner, Peer, State) ->
909 + OwnerBin = << (Owner#jid.luser)/binary, "@", (Owner#jid.lserver)/binary >>,
910 + OwnerServ = << "@", (Owner#jid.lserver)/binary >>,
911 + PeerBin = << (Peer#jid.luser)/binary, "@", (Peer#jid.lserver)/binary >>,
912 + PeerServ = << "@", (Peer#jid.lserver)/binary >>,
914 + LogTo = case ets:match_object(ets_settings_table(State#state.vhost),
915 + #user_settings{owner_name=Owner#jid.luser, _='_'}) of
916 + [#user_settings{dolog_default=Default,
918 + donotlog_list=DNLL}] ->
920 + A = lists:member(PeerBin, DLL),
921 + B = lists:member(PeerBin, DNLL),
925 + Default == true -> true;
926 + Default == false -> false;
927 + true -> State#state.dolog_default
929 + _ -> State#state.dolog_default
931 + lists:all(fun(O) -> O end,
932 + [not lists:member(OwnerBin, State#state.ignore_jids),
933 + not lists:member(PeerBin, State#state.ignore_jids),
934 + not lists:member(OwnerServ, State#state.ignore_jids),
935 + not lists:member(PeerServ, State#state.ignore_jids),
938 +purge_old_records(VHost, Days) ->
939 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
941 + Dates = ?MODULE:get_dates(VHost),
942 + DateNow = calendar:datetime_to_gregorian_seconds({date(), {0,0,1}}),
943 + DateDiff = list_to_integer(Days)*24*60*60,
944 + ?MYDEBUG("Purging tables older than ~s days", [Days]),
945 + lists:foreach(fun(Date) ->
946 + [Year, Month, Day] = ejabberd_regexp:split(iolist_to_binary(Date), <<"[^0-9]+">>),
947 + DateInSec = calendar:datetime_to_gregorian_seconds({{binary_to_integer(Year), binary_to_integer(Month), binary_to_integer(Day)}, {0,0,1}}),
949 + (DateNow - DateInSec) > DateDiff ->
950 + gen_server:call(Proc, {delete_messages_at, Date});
952 + ?MYDEBUG("Skipping messages at ~p", [Date])
956 +% called from get_vhost_stats/2, get_user_stats/3
957 +sort_stats(Stats) ->
958 + % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ]
959 + CFun = fun({TableName, Count}) ->
960 + [Year, Month, Day] = ejabberd_regexp:split(iolist_to_binary(TableName), <<"[^0-9]+">>),
961 + { calendar:datetime_to_gregorian_seconds({{binary_to_integer(Year), binary_to_integer(Month), binary_to_integer(Day)}, {0,0,1}}), Count }
963 + % convert to [{63364377601,1}, {63360662401,1}, ... ]
964 + CStats = lists:map(CFun, Stats),
966 + SortedStats = lists:reverse(lists:keysort(1, CStats)),
967 + % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list
968 + [{mod_logdb:convert_timestamp_brief(TableSec), Count} || {TableSec, Count} <- SortedStats].
970 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
972 +% Date/Time operations
974 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
975 +% return float seconds elapsed from "zero hour" as list
977 + {MegaSec, Sec, MicroSec} = now(),
978 + [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
981 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string
982 +convert_timestamp(Seconds) when is_list(Seconds) ->
983 + case string:to_float(Seconds++".0") of
984 + {F,_} when is_float(F) -> convert_timestamp(F);
985 + _ -> erlang:error(badarg, [Seconds])
987 +convert_timestamp(Seconds) when is_float(Seconds) ->
988 + GregSec = trunc(Seconds + 719528*86400),
989 + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
990 + {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT),
991 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec).
993 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string
994 +convert_timestamp_brief(Seconds) when is_list(Seconds) ->
995 + convert_timestamp_brief(list_to_float(Seconds));
996 +convert_timestamp_brief(Seconds) when is_float(Seconds) ->
997 + GregSec = trunc(Seconds + 719528*86400),
998 + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
999 + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT),
1000 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day);
1001 +convert_timestamp_brief(Seconds) when is_integer(Seconds) ->
1002 + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds),
1003 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day).
1005 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1007 +% DB operations (get)
1009 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1010 +get_vhost_stats(VHost) ->
1011 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1012 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
1014 +get_vhost_stats_at(VHost, Date) ->
1015 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1016 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
1018 +get_user_stats(User, VHost) ->
1019 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1020 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
1022 +get_user_messages_at(User, VHost, Date) ->
1023 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1024 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
1026 +get_dates(VHost) ->
1027 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1028 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
1030 +get_user_settings(User, VHost) ->
1031 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1032 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
1034 +set_user_settings(User, VHost, Set) ->
1035 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1036 + gen_server:call(Proc, {set_user_settings, User, Set}).
1038 +get_module_settings(VHost) ->
1039 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1040 + gen_server:call(Proc, {get_module_settings}).
1042 +set_module_settings(VHost, Settings) ->
1043 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1044 + gen_server:call(Proc, {set_module_settings, Settings}).
1046 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1048 +% Web admin callbacks (delete)
1050 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1051 +user_messages_at_parse_query(VHost, Date, Msgs, Query) ->
1052 + case lists:keysearch(<<"delete">>, 1, Query) of
1054 + PMsgs = lists:filter(
1056 + ID = misc:encode_base64(term_to_binary(Msg#msg.timestamp)),
1057 + lists:member({<<"selected">>, ID}, Query)
1059 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1060 + gen_server:call(Proc, {delete_messages_by_user_at, PMsgs, Date}, ?CALL_TIMEOUT);
1065 +user_messages_parse_query(User, VHost, Query) ->
1066 + case lists:keysearch(<<"delete">>, 1, Query) of
1068 + Dates = get_dates(VHost),
1069 + PDates = lists:filter(
1071 + ID = misc:encode_base64( << User/binary, (iolist_to_binary(Date))/binary >> ),
1072 + lists:member({<<"selected">>, ID}, Query)
1074 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1075 + Rez = lists:foldl(
1078 + [gen_server:call(Proc,
1079 + {delete_all_messages_by_user_at, User, iolist_to_binary(Date)},
1082 + case lists:member(error, Rez) of
1092 +vhost_messages_parse_query(VHost, Query) ->
1093 + case lists:keysearch(<<"delete">>, 1, Query) of
1095 + Dates = get_dates(VHost),
1096 + PDates = lists:filter(
1098 + ID = misc:encode_base64( << VHost/binary, (iolist_to_binary(Date))/binary >> ),
1099 + lists:member({<<"selected">>, ID}, Query)
1101 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1102 + Rez = lists:foldl(fun(Date, Acc) ->
1103 + lists:append(Acc, [gen_server:call(Proc,
1104 + {delete_messages_at, Date},
1107 + case lists:member(error, Rez) of
1117 +vhost_messages_at_parse_query(VHost, Date, Stats, Query) ->
1118 + case lists:keysearch(<<"delete">>, 1, Query) of
1120 + PStats = lists:filter(
1121 + fun({User, _Count}) ->
1122 + ID = misc:encode_base64( << (iolist_to_binary(User))/binary, VHost/binary >> ),
1123 + lists:member({<<"selected">>, ID}, Query)
1125 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1126 + Rez = lists:foldl(fun({User, _Count}, Acc) ->
1127 + lists:append(Acc, [gen_server:call(Proc,
1128 + {delete_all_messages_by_user_at,
1129 + iolist_to_binary(User), iolist_to_binary(Date)},
1132 + case lists:member(error, Rez) of
1142 +copy_messages([#state{vhost=VHost}=State, From, DatesIn]) ->
1143 + {FromDBName, FromDBOpts} =
1144 + case lists:keysearch(misc:binary_to_atom(From), 1, State#state.dbs) of
1145 + {value, {FN, FO}} ->
1148 + ?ERROR_MSG("Failed to find record for ~p in dbs", [From]),
1152 + FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
1154 + {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
1156 + Dates = case DatesIn of
1157 + [] -> FromDBMod:get_dates(VHost);
1161 + DatesLength = length(Dates),
1163 + catch lists:foldl(fun(Date, Acc) ->
1164 + case catch copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
1166 + ?INFO_MSG("Copied messages at ~p (~p/~p)", [Date, Acc, DatesLength]);
1168 + ?ERROR_MSG("Failed to copy messages at ~p (~p/~p): ~p", [Date, Acc, DatesLength, Value]),
1173 + ?INFO_MSG("copy_messages from ~p finished", [From]),
1174 + FromDBMod:stop(VHost).
1176 +copy_messages_int([FromDBMod, ToDBMod, VHost, Date]) ->
1177 + ets:new(mod_logdb_temp, [named_table, set, public]),
1178 + {Time, Value} = timer:tc(?MODULE, copy_messages_int_tc, [[FromDBMod, ToDBMod, VHost, Date]]),
1179 + ets:delete_all_objects(mod_logdb_temp),
1180 + ets:delete(mod_logdb_temp),
1181 + ?INFO_MSG("copy_messages at ~p elapsed ~p sec", [Date, Time/1000000]),
1184 +copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) ->
1185 + ?INFO_MSG("Going to copy messages from ~p for ~p at ~p", [FromDBMod, VHost, Date]),
1187 + ok = FromDBMod:rebuild_stats_at(VHost, Date),
1188 + catch mod_logdb:rebuild_stats_at(VHost, Date),
1189 + {ok, FromStats} = FromDBMod:get_vhost_stats_at(VHost, Date),
1190 + ToStats = case mod_logdb:get_vhost_stats_at(VHost, iolist_to_binary(Date)) of
1191 + {ok, Stats} -> Stats;
1195 + FromStatsS = lists:keysort(1, FromStats),
1196 + ToStatsS = lists:keysort(1, ToStats),
1198 + StatsLength = length(FromStats),
1201 + % destination table is empty
1203 + fun({User, _Count}, Acc) ->
1204 + {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
1206 + lists:foldl(fun(Msg, MFAcc) ->
1207 + MsgBinary = Msg#msg{owner_name=iolist_to_binary(User),
1208 + peer_name=iolist_to_binary(Msg#msg.peer_name),
1209 + peer_server=iolist_to_binary(Msg#msg.peer_server),
1210 + peer_resource=iolist_to_binary(Msg#msg.peer_resource),
1211 + type=iolist_to_binary(Msg#msg.type),
1212 + subject=iolist_to_binary(Msg#msg.subject),
1213 + body=iolist_to_binary(Msg#msg.body)},
1214 + ok = ToDBMod:log_message(VHost, MsgBinary),
1218 + ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
1219 + %timer:sleep(100),
1222 + % destination table is not empty
1224 + fun({User, _Count}, Acc) ->
1225 + {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date),
1226 + lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
1227 + ets:insert(mod_logdb_temp, {Tst});
1228 + % mysql, pgsql removes final zeros after decimal point
1229 + (#msg{timestamp=Tst}) when length(Tst) < 16 ->
1230 + {F, _} = string:to_float(Tst++".0"),
1231 + [T] = io_lib:format("~.5f", [F]),
1232 + ets:insert(mod_logdb_temp, {T})
1234 + {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
1236 + lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) ->
1237 + case ets:member(mod_logdb_temp, ToTimestamp) of
1239 + MsgBinary = Msg#msg{owner_name=iolist_to_binary(User),
1240 + peer_name=iolist_to_binary(Msg#msg.peer_name),
1241 + peer_server=iolist_to_binary(Msg#msg.peer_server),
1242 + peer_resource=iolist_to_binary(Msg#msg.peer_resource),
1243 + type=iolist_to_binary(Msg#msg.type),
1244 + subject=iolist_to_binary(Msg#msg.subject),
1245 + body=iolist_to_binary(Msg#msg.body)},
1246 + ok = ToDBMod:log_message(VHost, MsgBinary),
1247 + ets:insert(mod_logdb_temp, {ToTimestamp}),
1254 + ets:delete_all_objects(mod_logdb_temp),
1255 + ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
1256 + %timer:sleep(100),
1262 + FromStats == [] ->
1263 + ?INFO_MSG("No messages were found at ~p", [Date]);
1264 + FromStatsS == ToStatsS ->
1265 + ?INFO_MSG("Stats are equal at ~p", [Date]);
1266 + FromStatsS /= ToStatsS ->
1267 + lists:foldl(CopyFun, 0, FromStats),
1268 + ok = ToDBMod:rebuild_stats_at(VHost, Date)
1269 + %timer:sleep(1000)
1274 +list_to_bool(Num) when is_binary(Num) ->
1275 + list_to_bool(binary_to_list(Num));
1276 +list_to_bool(Num) when is_list(Num) ->
1277 + case lists:member(Num, ["t", "true", "y", "yes", "1"]) of
1281 + case lists:member(Num, ["f", "false", "n", "no", "0"]) of
1289 +bool_to_list(true) ->
1291 +bool_to_list(false) ->
1294 +list_to_string([]) ->
1296 +list_to_string(List) when is_list(List) ->
1297 + Str = lists:flatmap(fun(Elm) when is_binary(Elm) ->
1298 + binary_to_list(Elm) ++ "\n";
1299 + (Elm) when is_list(Elm) ->
1302 + lists:sublist(Str, length(Str)-1).
1304 +string_to_list(null) ->
1306 +string_to_list([]) ->
1308 +string_to_list(String) ->
1309 + ejabberd_regexp:split(iolist_to_binary(String), <<"\n">>).
1311 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1313 +% ad-hoc (copy/pasted from mod_configure.erl)
1315 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1316 +-define(ITEMS_RESULT(Allow, LNode, Fallback),
1320 + case get_local_items(LServer, LNode,
1321 + jid:encode(To), Lang) of
1322 + {result, Res} -> {result, Res};
1323 + {error, Error} -> {error, Error}
1327 +get_local_items(Acc, From, #jid{lserver = LServer} = To,
1329 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1332 + Items = case Acc of
1333 + {result, Its} -> Its;
1336 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1337 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1339 + AllowUser == allow; AllowAdmin == allow ->
1340 + case get_local_items(LServer, [],
1341 + jid:encode(To), Lang) of
1343 + {result, Items ++ Res};
1344 + {error, _Error} ->
1351 +get_local_items(Acc, From, #jid{lserver = LServer} = To,
1353 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1356 + LNode = tokenize(Node),
1357 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1358 + Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
1360 + [<<"mod_logdb">>] ->
1361 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1362 + [<<"mod_logdb_users">>] ->
1363 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1364 + [<<"mod_logdb_users">>, <<$@, _/binary>>] ->
1365 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1366 + [<<"mod_logdb_users">>, _User] ->
1367 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1368 + [<<"mod_logdb_settings">>] ->
1369 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1375 +-define(T(Lang, Text), translate:translate(Lang, Text)).
1377 +-define(NODE(Name, Node),
1378 + #disco_item{jid = jid:make(Server),
1380 + name = ?T(Lang, Name)}).
1382 +-define(NS_ADMINX(Sub),
1383 + <<(?NS_ADMIN)/binary, "#", Sub/binary>>).
1385 +tokenize(Node) -> str:tokens(Node, <<"/#">>).
1387 +get_local_items(_Host, [], Server, Lang) ->
1389 + [?NODE(<<"Messages logging engine">>, <<"mod_logdb">>)]
1391 +get_local_items(_Host, [<<"mod_logdb">>], Server, Lang) ->
1393 + [?NODE(<<"Messages logging engine users">>, <<"mod_logdb_users">>),
1394 + ?NODE(<<"Messages logging engine settings">>, <<"mod_logdb_settings">>)]
1396 +get_local_items(Host, [<<"mod_logdb_users">>], Server, _Lang) ->
1397 + {result, get_all_vh_users(Host, Server)};
1398 +get_local_items(Host, [<<"mod_logdb_users">>, <<$@, Diap/binary>>], Server, Lang) ->
1399 + Users = ejabberd_auth:get_vh_registered_users(Host),
1400 + SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1402 + [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>),
1403 + N1 = binary_to_integer(S1),
1404 + N2 = binary_to_integer(S2),
1405 + Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
1406 + {result, lists:map(fun({S, U}) ->
1407 + ?NODE(<< U/binary, "@", S/binary >>,
1408 + << (iolist_to_binary("mod_logdb_users/"))/binary, U/binary, "@", S/binary >>)
1411 + xmpp:err_not_acceptable()
1413 +get_local_items(_Host, [<<"mod_logdb_users">>, _User], _Server, _Lang) ->
1415 +get_local_items(_Host, [<<"mod_logdb_settings">>], _Server, _Lang) ->
1417 +get_local_items(_Host, Item, _Server, _Lang) ->
1418 + ?MYDEBUG("asked for items in ~p", [Item]),
1419 + {error, xmpp:err_item_not_found()}.
1421 +-define(INFO_RESULT(Allow, Feats, Lang),
1423 + deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
1424 + allow -> {result, Feats}
1427 +get_local_features(Acc, From,
1428 + #jid{lserver = LServer} = _To, Node, Lang) ->
1429 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1433 + LNode = tokenize(Node),
1434 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1435 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1437 + [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
1438 + ?INFO_RESULT(allow, [?NS_COMMANDS], Lang);
1439 + [<<"mod_logdb">>] ->
1440 + ?INFO_RESULT(deny, [?NS_COMMANDS], Lang);
1441 + [<<"mod_logdb_users">>] ->
1442 + ?INFO_RESULT(AllowAdmin, [], Lang);
1443 + [<<"mod_logdb_users">>, [$@ | _]] ->
1444 + ?INFO_RESULT(AllowAdmin, [], Lang);
1445 + [<<"mod_logdb_users">>, _User] ->
1446 + ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
1447 + [<<"mod_logdb_settings">>] ->
1448 + ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
1456 +-define(INFO_IDENTITY(Category, Type, Name, Lang),
1457 + [#identity{category = Category, type = Type, name = ?T(Lang, Name)}]).
1459 +-define(INFO_COMMAND(Name, Lang),
1460 + ?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
1463 +get_local_identity(Acc, _From, _To, Node, Lang) ->
1464 + LNode = tokenize(Node),
1466 + [<<"mod_logdb">>] ->
1467 + ?INFO_COMMAND(<<"Messages logging engine">>, Lang);
1468 + [<<"mod_logdb_users">>] ->
1469 + ?INFO_COMMAND(<<"Messages logging engine users">>, Lang);
1470 + [<<"mod_logdb_users">>, User] ->
1471 + ?INFO_COMMAND(User, Lang);
1472 + [<<"mod_logdb_settings">>] ->
1473 + ?INFO_COMMAND(<<"Messages logging engine settings">>, Lang);
1478 +adhoc_local_items(Acc, From,
1479 + #jid{lserver = LServer, server = Server} = To, Lang) ->
1480 + % TODO: case acl:match_rule(LServer, ???, From) of
1481 + Items = case Acc of
1482 + {result, Its} -> Its;
1485 + Nodes = recursively_get_local_items(LServer,
1486 + <<"">>, Server, Lang),
1487 + Nodes1 = lists:filter(
1488 + fun(#disco_item{node = Nd}) ->
1489 + F = get_local_features([], From, To, Nd, Lang),
1491 + {result, [?NS_COMMANDS]} -> true;
1495 + {result, Items ++ Nodes1}.
1497 +recursively_get_local_items(_LServer,
1498 + <<"mod_logdb_users">>, _Server, _Lang) ->
1500 +recursively_get_local_items(LServer,
1501 + Node, Server, Lang) ->
1502 + LNode = tokenize(Node),
1503 + Items = case get_local_items(LServer, LNode,
1505 + {result, Res} -> Res;
1506 + {error, _Error} -> []
1508 + Nodes = lists:flatten(
1510 + fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
1511 + if (S /= Server) or (Nd == <<"">>) ->
1514 + [Item, recursively_get_local_items(
1515 + LServer, Nd, Server, Lang)]
1520 +-define(COMMANDS_RESULT(Allow, From, To, Request),
1523 + {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
1525 + adhoc_local_commands(From, To, Request)
1528 +adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
1529 + #adhoc_command{node = Node, lang = Lang} = Request) ->
1530 + LNode = tokenize(Node),
1531 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1532 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1534 + [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
1535 + ?COMMANDS_RESULT(allow, From, To, Request);
1536 + [<<"mod_logdb_users">>, <<$@, _/binary>>] when AllowAdmin == allow ->
1538 + [<<"mod_logdb_users">>, _User] when AllowAdmin == allow ->
1539 + ?COMMANDS_RESULT(allow, From, To, Request);
1540 + [<<"mod_logdb_settings">>] when AllowAdmin == allow ->
1541 + ?COMMANDS_RESULT(allow, From, To, Request);
1546 +adhoc_local_commands(From, #jid{lserver = LServer} = _To,
1547 + #adhoc_command{lang = Lang,
1551 + xdata = XData} = Request) ->
1552 + LNode = tokenize(Node),
1553 + %% If the "action" attribute is not present, it is
1554 + %% understood as "execute". If there was no <actions/>
1555 + %% element in the first response (which there isn't in our
1556 + %% case), "execute" and "complete" are equivalent.
1557 + ActionIsExecute = Action == execute orelse Action == complete,
1558 + if Action == cancel ->
1559 + %% User cancels request
1560 + #adhoc_command{status = canceled, lang = Lang,
1561 + node = Node, sid = SessionID};
1562 + XData == undefined, ActionIsExecute ->
1563 + %% User requests form
1564 + case get_form(LServer, LNode, Lang) of
1566 + xmpp_util:make_adhoc_response(
1568 + #adhoc_command{status = executing,
1573 + XData /= undefined, ActionIsExecute ->
1574 + %% User returns form.
1575 + case catch set_form(From, LServer, LNode, Lang, XData) of
1577 + xmpp_util:make_adhoc_response(
1579 + #adhoc_command{xdata = Res, status = completed});
1580 + {'EXIT', _} -> {error, xmpp:err_bad_request()};
1581 + {error, Error} -> {error, Error}
1584 + {error, xmpp:err_bad_request(<<"Unexpected action">>, Lang)}
1587 +-define(TVFIELD(Type, Var, Val),
1588 + #xdata_field{type = Type, var = Var, values = [Val]}).
1591 + ?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))).
1593 +get_user_form(LUser, LServer, Lang) ->
1594 + ?MYDEBUG("get_user_form ~p ~p", [LUser, LServer]),
1595 + %From = jid:encode(jid:remove_resource(Jid)),
1596 + #user_settings{dolog_default=DLD,
1598 + donotlog_list=DNLL} = get_user_settings(LUser, LServer),
1601 + type = 'list-single',
1602 + label = ?T(Lang, <<"Default">>),
1603 + var = <<"dolog_default">>,
1604 + values = [misc:atom_to_binary(DLD)],
1605 + options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
1606 + value = <<"true">>},
1607 + #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
1608 + value = <<"false">>}]},
1610 + type = 'text-multi',
1611 + label = ?T(Lang, <<"Log Messages">>),
1612 + var = <<"dolog_list">>,
1615 + type = 'text-multi',
1616 + label = ?T(Lang, <<"Do Not Log Messages">>),
1617 + var = <<"donotlog_list">>,
1621 + title = ?T(Lang, <<"Messages logging engine settings">>),
1623 + instructions = [<< (?T(Lang, <<"Set logging preferences">>))/binary,
1624 + (iolist_to_binary(": "))/binary,
1625 + LUser/binary, "@", LServer/binary >>],
1626 + fields = [?HFIELD()|
1629 +get_settings_form(Host, Lang) ->
1630 + ?MYDEBUG("get_settings_form ~p ~p", [Host, Lang]),
1631 + #state{dbmod=_DBMod,
1633 + dolog_default=DLD,
1634 + ignore_jids=IgnoreJids,
1635 + groupchat=GroupChat,
1636 + purge_older_days=PurgeDaysT,
1637 + drop_messages_on_user_removal=MRemoval,
1638 + poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
1641 + case PurgeDaysT of
1642 + never -> <<"never">>;
1643 + Num when is_integer(Num) -> integer_to_binary(Num);
1644 + _ -> <<"unknown">>
1648 + type = 'list-single',
1649 + label = ?T(Lang, <<"Default">>),
1650 + var = <<"dolog_default">>,
1651 + values = [misc:atom_to_binary(DLD)],
1652 + options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
1653 + value = <<"true">>},
1654 + #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
1655 + value = <<"false">>}]},
1657 + type = 'list-single',
1658 + label = ?T(Lang, <<"Drop messages on user removal">>),
1659 + var = <<"drop_messages_on_user_removal">>,
1660 + values = [misc:atom_to_binary(MRemoval)],
1661 + options = [#xdata_option{label = ?T(Lang, <<"Drop">>),
1662 + value = <<"true">>},
1663 + #xdata_option{label = ?T(Lang, <<"Do not drop">>),
1664 + value = <<"false">>}]},
1666 + type = 'list-single',
1667 + label = ?T(Lang, <<"Groupchat messages logging">>),
1668 + var = <<"groupchat">>,
1669 + values = [misc:atom_to_binary(GroupChat)],
1670 + options = [#xdata_option{label = ?T(Lang, <<"all">>),
1671 + value = <<"all">>},
1672 + #xdata_option{label = ?T(Lang, <<"none">>),
1673 + value = <<"none">>},
1674 + #xdata_option{label = ?T(Lang, <<"send">>),
1675 + value = <<"send">>}]},
1677 + type = 'text-multi',
1678 + label = ?T(Lang, <<"Jids/Domains to ignore">>),
1679 + var = <<"ignore_list">>,
1680 + values = IgnoreJids},
1682 + type = 'text-single',
1683 + label = ?T(Lang, <<"Purge messages older than (days)">>),
1684 + var = <<"purge_older_days">>,
1685 + values = [iolist_to_binary(PurgeDays)]},
1687 + type = 'text-single',
1688 + label = ?T(Lang, <<"Poll users settings (seconds)">>),
1689 + var = <<"poll_users_settings">>,
1690 + values = [integer_to_binary(PollTime)]}
1693 + title = ?T(Lang, <<"Messages logging engine settings (run-time)">>),
1694 + instructions = [?T(Lang, <<"Set run-time settings">>)],
1696 + fields = [?HFIELD()|
1699 +get_form(_Host, [<<"mod_logdb_users">>, User], Lang) ->
1700 + #jid{luser=LUser, lserver=LServer} = jid:decode(User),
1701 + get_user_form(LUser, LServer, Lang);
1702 +get_form(Host, [<<"mod_logdb_settings">>], Lang) ->
1703 + get_settings_form(Host, Lang);
1704 +get_form(_Host, Command, _Lang) ->
1705 + ?MYDEBUG("asked for form ~p", [Command]),
1706 + {error, xmpp:err_service_unavailable()}.
1708 +check_log_list([]) ->
1710 +check_log_list([<<>>]) ->
1712 +check_log_list([Head | Tail]) ->
1713 + case binary:match(Head, <<$@>>) of
1714 + nomatch -> throw(error);
1717 + % this check for Head to be valid jid
1718 + case catch jid:decode(Head) of
1719 + {'EXIT', _Reason} -> throw(error);
1720 + _ -> check_log_list(Tail)
1723 +check_ignore_list([]) ->
1725 +check_ignore_list([<<>>]) ->
1727 +check_ignore_list([<<>> | Tail]) ->
1728 + check_ignore_list(Tail);
1729 +check_ignore_list([Head | Tail]) ->
1730 + case binary:match(Head, <<$@>>) of
1732 + nomatch -> throw(error)
1734 + Jid2Test = case Head of
1735 + << $@, _Rest/binary >> -> << "a", Head/binary >>;
1738 + % this check for Head to be valid jid
1739 + case catch jid:decode(Jid2Test) of
1740 + {'EXIT', _Reason} -> throw(error);
1741 + _ -> check_ignore_list(Tail)
1744 +get_value(Field, XData) -> hd(get_values(Field, XData)).
1746 +get_values(Field, XData) ->
1747 + xmpp_util:get_xdata_values(Field, XData).
1749 +parse_users_settings(XData) ->
1750 + DLD = case get_value(<<"dolog_default">>, XData) of
1751 + ValueDLD when ValueDLD == <<"true">>;
1752 + ValueDLD == <<"false">> ->
1753 + list_to_bool(ValueDLD);
1754 + _ -> throw(bad_request)
1757 + ListDLL = get_values(<<"dolog_list">>, XData),
1758 + DLL = case catch check_log_list(ListDLL) of
1760 + error -> throw(bad_request)
1763 + ListDNLL = get_values(<<"donotlog_list">>, XData),
1764 + DNLL = case catch check_log_list(ListDNLL) of
1766 + error -> throw(bad_request)
1769 + #user_settings{dolog_default=DLD,
1771 + donotlog_list=DNLL}.
1773 +parse_module_settings(XData) ->
1774 + DLD = case get_value(<<"dolog_default">>, XData) of
1775 + ValueDLD when ValueDLD == <<"true">>;
1776 + ValueDLD == <<"false">> ->
1777 + list_to_bool(ValueDLD);
1778 + _ -> throw(bad_request)
1780 + MRemoval = case get_value(<<"drop_messages_on_user_removal">>, XData) of
1781 + ValueMRemoval when ValueMRemoval == <<"true">>;
1782 + ValueMRemoval == <<"false">> ->
1783 + list_to_bool(ValueMRemoval);
1784 + _ -> throw(bad_request)
1786 + GroupChat = case get_value(<<"groupchat">>, XData) of
1787 + ValueGroupChat when ValueGroupChat == <<"none">>;
1788 + ValueGroupChat == <<"all">>;
1789 + ValueGroupChat == <<"send">> ->
1790 + misc:binary_to_atom(ValueGroupChat);
1791 + _ -> throw(bad_request)
1793 + ListIgnore = get_values(<<"ignore_list">>, XData),
1794 + Ignore = case catch check_ignore_list(ListIgnore) of
1796 + error -> throw(bad_request)
1798 + Purge = case get_value(<<"purge_older_days">>, XData) of
1799 + <<"never">> -> never;
1801 + case catch binary_to_integer(ValuePurge) of
1802 + IntValuePurge when is_integer(IntValuePurge) -> IntValuePurge;
1803 + _ -> throw(bad_request)
1806 + Poll = case catch binary_to_integer(get_value(<<"poll_users_settings">>, XData)) of
1807 + IntValuePoll when is_integer(IntValuePoll) -> IntValuePoll;
1808 + _ -> throw(bad_request)
1810 + #state{dolog_default=DLD,
1811 + groupchat=GroupChat,
1812 + ignore_jids=Ignore,
1813 + purge_older_days=Purge,
1814 + drop_messages_on_user_removal=MRemoval,
1815 + poll_users_settings=Poll}.
1817 +set_form(_From, _Host, [<<"mod_logdb_users">>, User], Lang, XData) ->
1818 + #jid{luser=LUser, lserver=LServer} = jid:decode(User),
1819 + Txt = "Parse user settings failed",
1820 + case catch parse_users_settings(XData) of
1822 + ?ERROR_MSG("Failed to set user form: bad_request", []),
1823 + {error, xmpp:err_bad_request(Txt, Lang)};
1824 + {'EXIT', Reason} ->
1825 + ?ERROR_MSG("Failed to set user form ~p", [Reason]),
1826 + {error, xmpp:err_bad_request(Txt, Lang)};
1828 + case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
1830 + {result, undefined};
1832 + {error, xmpp:err_internal_server_error()}
1835 +set_form(_From, Host, [<<"mod_logdb_settings">>], Lang, XData) ->
1836 + Txt = "Parse module settings failed",
1837 + case catch parse_module_settings(XData) of
1839 + ?ERROR_MSG("Failed to set settings form: bad_request", []),
1840 + {error, xmpp:err_bad_request(Txt, Lang)};
1841 + {'EXIT', Reason} ->
1842 + ?ERROR_MSG("Failed to set settings form ~p", [Reason]),
1843 + {error, xmpp:err_bad_request(Txt, Lang)};
1845 + case mod_logdb:set_module_settings(Host, Settings) of
1847 + {result, undefined};
1849 + {error, xmpp:err_internal_server_error()}
1852 +set_form(From, _Host, Node, _Lang, XData) ->
1853 + User = jid:encode(jid:remove_resource(From)),
1854 + ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]),
1855 + {error, xmpp:err_service_unavailable()}.
1857 +get_all_vh_users(Host, Server) ->
1858 + case catch ejabberd_auth:get_vh_registered_users(Host) of
1859 + {'EXIT', _Reason} ->
1862 + SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1863 + case length(SUsers) of
1864 + N when N =< 100 ->
1865 + lists:map(fun({S, U}) ->
1866 + #disco_item{jid = jid:make(Server),
1867 + node = <<"mod_logdb_users/", U/binary, $@, S/binary>>,
1868 + name = << U/binary, "@", S/binary >>}
1871 + NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1,
1872 + M = trunc(N / NParts) + 1,
1873 + lists:map(fun(K) ->
1876 + (integer_to_binary(K))/binary,
1878 + (integer_to_binary(L))/binary
1880 + {FS, FU} = lists:nth(K, SUsers),
1882 + if L < N -> lists:nth(L, SUsers);
1883 + true -> lists:last(SUsers)
1886 + <<FU/binary, "@", FS/binary,
1888 + LU/binary, "@", LS/binary>>,
1889 + #disco_item{jid = jid:make(Host),
1890 + node = <<"mod_logdb_users/", Node/binary>>,
1892 + end, lists:seq(1, N, M))
1896 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1900 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1901 +webadmin_menu(Acc, _Host, Lang) ->
1902 + [{<<"messages">>, ?T(<<"Users Messages">>)} | Acc].
1904 +webadmin_user(Acc, User, Server, Lang) ->
1905 + Sett = get_user_settings(User, Server),
1907 + case Sett#user_settings.dolog_default of
1909 + ?INPUTT(<<"submit">>, <<"dolog">>, <<"Log Messages">>);
1911 + ?INPUTT(<<"submit">>, <<"donotlog">>, <<"Do Not Log Messages">>);
1914 + Acc ++ [?XE(<<"h3">>, [?ACT(<<"messages/">>, <<"Messages">>), ?C(<<" ">>), Log])].
1916 +webadmin_page(_, Host,
1917 + #request{path = [<<"messages">>],
1920 + Res = vhost_messages_stats(Host, Query, Lang),
1922 +webadmin_page(_, Host,
1923 + #request{path = [<<"messages">>, Date],
1926 + Res = vhost_messages_stats_at(Host, Query, Lang, Date),
1928 +webadmin_page(_, Host,
1929 + #request{path = [<<"user">>, U, <<"messages">>],
1932 + Res = user_messages_stats(U, Host, Query, Lang),
1934 +webadmin_page(_, Host,
1935 + #request{path = [<<"user">>, U, <<"messages">>, Date],
1938 + Res = mod_logdb:user_messages_stats_at(U, Host, Query, Lang, Date),
1940 +webadmin_page(Acc, _Host, _R) -> Acc.
1942 +user_parse_query(_, <<"dolog">>, User, Server, _Query) ->
1943 + Sett = get_user_settings(User, Server),
1944 + % TODO: check returned value
1945 + set_user_settings(User, Server, Sett#user_settings{dolog_default=true}),
1947 +user_parse_query(_, <<"donotlog">>, User, Server, _Query) ->
1948 + Sett = get_user_settings(User, Server),
1949 + % TODO: check returned value
1950 + set_user_settings(User, Server, Sett#user_settings{dolog_default=false}),
1952 +user_parse_query(Acc, _Action, _User, _Server, _Query) ->
1955 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1959 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1960 +vhost_messages_stats(Server, Query, Lang) ->
1961 + Res = case catch vhost_messages_parse_query(Server, Query) of
1962 + {'EXIT', Reason} ->
1963 + ?ERROR_MSG("~p", [Reason]),
1965 + VResult -> VResult
1967 + {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]),
1968 + ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]),
1969 + %case get_vhost_stats(Server) of
1971 + {'EXIT', CReason} ->
1972 + ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
1973 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
1974 + {error, GReason} ->
1975 + ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
1976 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
1978 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Server])))];
1980 + Fun = fun({Date, Count}) ->
1981 + DateBin = iolist_to_binary(Date),
1982 + ID = misc:encode_base64( << Server/binary, DateBin/binary >> ),
1984 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
1985 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
1986 + ?XE(<<"td">>, [?AC(DateBin, DateBin)]),
1987 + ?XC(<<"td">>, integer_to_binary(Count))
1991 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s">>), [Server])))] ++
1993 + ok -> [?CT(<<"Submitted">>), ?P];
1994 + error -> [?CT(<<"Bad format">>), ?P];
1997 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2002 + ?XCT(<<"td">>, <<"Date">>),
2003 + ?XCT(<<"td">>, <<"Count">>)
2006 + lists:map(Fun, Dates)
2009 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2013 +vhost_messages_stats_at(Server, Query, Lang, Date) ->
2014 + {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]),
2015 + ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]),
2016 + %case get_vhost_stats_at(Server, Date) of
2018 + {'EXIT', CReason} ->
2019 + ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
2020 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2021 + {error, GReason} ->
2022 + ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
2023 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2025 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Server, Date])))];
2027 + Res = case catch vhost_messages_at_parse_query(Server, Date, Stats, Query) of
2028 + {'EXIT', Reason} ->
2029 + ?ERROR_MSG("~p", [Reason]),
2031 + VResult -> VResult
2033 + Fun = fun({User, Count}) ->
2034 + UserBin = iolist_to_binary(User),
2035 + ID = misc:encode_base64( << UserBin/binary, Server/binary >> ),
2037 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2038 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2039 + ?XE(<<"td">>, [?AC(<< <<"../user/">>/binary, UserBin/binary, <<"/messages/">>/binary, Date/binary >>, UserBin)]),
2040 + ?XC(<<"td">>, integer_to_binary(Count))
2043 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Server, Date])))] ++
2045 + ok -> [?CT(<<"Submitted">>), ?P];
2046 + error -> [?CT(<<"Bad format">>), ?P];
2049 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2054 + ?XCT(<<"td">>, <<"User">>),
2055 + ?XCT(<<"td">>, <<"Count">>)
2058 + lists:map(Fun, Stats)
2061 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2065 +user_messages_stats(User, Server, Query, Lang) ->
2066 + Jid = jid:encode({User, Server, ""}),
2068 + Res = case catch user_messages_parse_query(User, Server, Query) of
2069 + {'EXIT', Reason} ->
2070 + ?ERROR_MSG("~p", [Reason]),
2072 + VResult -> VResult
2075 + {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]),
2076 + ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]),
2079 + {'EXIT', CReason} ->
2080 + ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
2081 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
2082 + {error, GReason} ->
2083 + ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
2084 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
2086 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Jid])))];
2088 + Fun = fun({Date, Count}) ->
2089 + DateBin = iolist_to_binary(Date),
2090 + ID = misc:encode_base64( << User/binary, DateBin/binary >> ),
2092 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2093 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2094 + ?XE(<<"td">>, [?AC(DateBin, DateBin)]),
2095 + ?XC(<<"td">>, iolist_to_binary(integer_to_list(Count)))
2098 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T("Logged messages for ~s"), [Jid])))] ++
2100 + ok -> [?CT(<<"Submitted">>), ?P];
2101 + error -> [?CT(<<"Bad format">>), ?P];
2104 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2109 + ?XCT(<<"td">>, <<"Date">>),
2110 + ?XCT(<<"td">>, <<"Count">>)
2113 + lists:map(Fun, Dates)
2116 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2120 +search_user_nick(User, List) ->
2121 + case lists:keysearch(User, 1, List) of
2122 + {value,{User, []}} ->
2124 + {value,{User, Nick}} ->
2130 +user_messages_stats_at(User, Server, Query, Lang, Date) ->
2131 + Jid = jid:encode({User, Server, ""}),
2133 + {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]),
2134 + ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]),
2136 + {'EXIT', CReason} ->
2137 + ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
2138 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
2139 + {error, GReason} ->
2140 + ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
2141 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
2143 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Jid, Date])))];
2144 + {ok, User_messages} ->
2145 + Res = case catch user_messages_at_parse_query(Server,
2149 + {'EXIT', Reason} ->
2150 + ?ERROR_MSG("~p", [Reason]),
2152 + VResult -> VResult
2155 + UR = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]),
2157 + lists:map(fun(Item) ->
2158 + {jid:encode(Item#roster.jid), Item#roster.name}
2161 + UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) ->
2162 + ToAdd = PName++"@"++PServer,
2163 + case lists:member(ToAdd, List) of
2165 + false -> lists:append([ToAdd], List)
2167 + end, [], User_messages),
2169 + % Users to filter (sublist of UniqUsers)
2170 + CheckedUsers = case lists:keysearch(<<"filter">>, 1, Query) of
2172 + lists:filter(fun(UFUser) ->
2173 + ID = misc:encode_base64(term_to_binary(UFUser)),
2174 + lists:member({<<"selected">>, ID}, Query)
2179 + % UniqUsers in html (noone selected -> everyone selected)
2180 + Users = lists:map(fun(UHUser) ->
2181 + ID = misc:encode_base64(term_to_binary(UHUser)),
2182 + Input = case lists:member(UHUser, CheckedUsers) of
2183 + true -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2184 + false when CheckedUsers == [] -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2185 + false -> [?INPUT(<<"checkbox">>, <<"selected">>, ID)]
2188 + case search_user_nick(UHUser, UserRoster) of
2189 + nothing -> <<"">>;
2190 + N -> iolist_to_binary( " ("++ N ++")" )
2193 + [?XE(<<"td">>, Input),
2194 + ?XC(<<"td">>, iolist_to_binary(UHUser++Nick))])
2195 + end, lists:sort(UniqUsers)),
2196 + % Messages to show (based on Users)
2197 + User_messages_filtered = case CheckedUsers of
2198 + [] -> User_messages;
2199 + _ -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) ->
2200 + lists:member(PName++"@"++PServer, CheckedUsers)
2201 + end, User_messages)
2204 + Msgs_Fun = fun(#msg{timestamp=Timestamp,
2206 + direction=Direction,
2207 + peer_name=PName, peer_server=PServer, peer_resource=PRes,
2210 + Text = case Subject of
2211 + "" -> iolist_to_binary(Body);
2212 + _ -> iolist_to_binary([binary_to_list(?T(<<"Subject">>)) ++ ": " ++ Subject ++ "\n" ++ Body])
2214 + Resource = case PRes of
2220 + case search_user_nick(PName++"@"++PServer, UserRoster) of
2221 + nothing when PServer == Server ->
2223 + nothing when Type == "groupchat", Direction == from ->
2224 + PName++"@"++PServer++Resource;
2226 + PName++"@"++PServer;
2229 + ID = misc:encode_base64(term_to_binary(Timestamp)),
2231 + [?XE(<<"td">>, [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2232 + ?XC(<<"td">>, iolist_to_binary(convert_timestamp(Timestamp))),
2233 + ?XC(<<"td">>, iolist_to_binary(atom_to_list(Direction)++": "++UserNick)),
2234 + ?XE(<<"td">>, [?XC(<<"pre">>, Text)])])
2236 + % Filtered user messages in html
2237 + Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
2239 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Jid, Date])))] ++
2241 + ok -> [?CT(<<"Submitted">>), ?P];
2242 + error -> [?CT(<<"Bad format">>), ?P];
2245 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2249 + ?XCT(<<"td">>, <<"User">>)
2255 + ?INPUTT(<<"submit">>, <<"filter">>, <<"Filter Selected">>)
2261 + ?XCT(<<"td">>, <<"Date, Time">>),
2262 + ?XCT(<<"td">>, <<"Direction: Jid">>),
2263 + ?XCT(<<"td">>, <<"Body">>)
2268 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>),
2273 diff --git a/src/mod_logdb.hrl b/src/mod_logdb.hrl
2274 new file mode 100644
2275 index 00000000..49791f4e
2277 +++ b/src/mod_logdb.hrl
2279 +%%%----------------------------------------------------------------------
2280 +%%% File : mod_logdb.hrl
2281 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2283 +%%% Url : https://paleg.github.io/mod_logdb/
2284 +%%%----------------------------------------------------------------------
2286 +-define(logdb_debug, true).
2288 +-ifdef(logdb_debug).
2289 +-define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n",
2290 + [calendar:local_time(),?MODULE,?LINE]++Args)).
2292 +-define(MYDEBUG(_F,_A),[]).
2295 +-record(msg, {timestamp,
2297 + peer_name, peer_server, peer_resource,
2302 +-record(user_settings, {owner_name,
2305 + donotlog_list=[]}).
2307 +-define(INPUTC(Type, Name, Value),
2308 + ?XA(<<"input">>, [{<<"type">>, Type},
2309 + {<<"name">>, Name},
2310 + {<<"value">>, Value},
2311 + {<<"checked">>, <<"true">>}])).
2312 diff --git a/src/mod_logdb_mnesia.erl b/src/mod_logdb_mnesia.erl
2313 new file mode 100644
2314 index 00000000..a08d5262
2316 +++ b/src/mod_logdb_mnesia.erl
2318 +%%%----------------------------------------------------------------------
2319 +%%% File : mod_logdb_mnesia.erl
2320 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2321 +%%% Purpose : mnesia backend for mod_logdb
2322 +%%% Url : https://paleg.github.io/mod_logdb/
2323 +%%%----------------------------------------------------------------------
2325 +-module(mod_logdb_mnesia).
2326 +-author('o.palij@gmail.com').
2328 +-include("mod_logdb.hrl").
2329 +-include("logger.hrl").
2331 +-behaviour(gen_logdb).
2332 +-behaviour(gen_server).
2335 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2337 +-export([start/2, stop/1]).
2339 +-export([log_message/2,
2341 + rebuild_stats_at/2,
2342 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2343 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2345 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
2348 +-define(PROCNAME, mod_logdb_mnesia).
2349 +-define(CALL_TIMEOUT, 10000).
2351 +-record(state, {vhost}).
2353 +-record(stats, {user, at, count}).
2359 + "_" ++ binary_to_list(VHost).
2361 +stats_table(VHost) ->
2362 + list_to_atom(prefix() ++ "stats" ++ suffix(VHost)).
2364 +table_name(VHost, Date) ->
2365 + list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)).
2367 +settings_table(VHost) ->
2368 + list_to_atom(prefix() ++ "settings" ++ suffix(VHost)).
2370 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2372 +% gen_mod callbacks
2374 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2375 +start(VHost, Opts) ->
2376 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2377 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2380 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2381 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2383 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2385 +% gen_server callbacks
2387 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2388 +init([VHost, _Opts]) ->
2389 + case mnesia:system_info(is_running) of
2391 + ok = create_stats_table(VHost),
2392 + ok = create_settings_table(VHost),
2393 + {ok, #state{vhost=VHost}};
2395 + ?ERROR_MSG("Mnesia not running", []),
2396 + {stop, db_connection_failed};
2398 + ?ERROR_MSG("Mnesia status: ~p", [Status]),
2399 + {stop, db_connection_failed}
2402 +handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) ->
2403 + {reply, log_message_int(VHost, Msg), State};
2404 +handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) ->
2405 + {atomic, ok} = delete_nonexistent_stats(VHost),
2407 + lists:foreach(fun(Date) ->
2408 + rebuild_stats_at_int(VHost, Date)
2409 + end, get_dates_int(VHost)),
2410 + {reply, Reply, State};
2411 +handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2412 + Reply = rebuild_stats_at_int(VHost, Date),
2413 + {reply, Reply, State};
2414 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
2415 + Table = table_name(VHost, Date),
2419 + mnesia:write_lock_table(stats_table(VHost)),
2420 + mnesia:write_lock_table(Table),
2421 + mnesia:delete_object(Table, Msg, write)
2424 + DRez = case mnesia:transaction(Fun) of
2425 + {aborted, Reason} ->
2426 + ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
2432 + case rebuild_stats_at_int(VHost, Date) of
2438 + {reply, Reply, State};
2439 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) ->
2440 + {reply, delete_all_messages_by_user_at_int(User, VHost, Date), State};
2441 +handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) ->
2443 + case mnesia:delete_table(table_name(VHost, Date)) of
2445 + delete_stats_by_vhost_at_int(VHost, Date);
2446 + {aborted, Reason} ->
2447 + ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]),
2450 + {reply, Reply, State};
2451 +handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) ->
2452 + Fun = fun(#stats{at=Date, count=Count}, Stats) ->
2453 + case lists:keysearch(Date, 1, Stats) of
2455 + lists:append(Stats, [{Date, Count}]);
2456 + {value, {_, TempCount}} ->
2457 + lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count})
2461 + case mnesia:transaction(fun() ->
2462 + mnesia:foldl(Fun, [], stats_table(VHost))
2464 + {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)};
2465 + {aborted, Reason} -> {error, Reason}
2467 + {reply, Reply, State};
2468 +handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2470 + Pat = #stats{user='$1', at=Date, count='$2'},
2471 + mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
2474 + case mnesia:transaction(Fun) of
2475 + {atomic, Result} ->
2476 + {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))};
2477 + {aborted, Reason} ->
2480 + {reply, Reply, State};
2481 +handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) ->
2482 + {reply, get_user_stats_int(User, VHost), State};
2483 +handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) ->
2485 + case mnesia:transaction(fun() ->
2486 + Pat = #msg{owner_name=User, _='_'},
2487 + mnesia:select(table_name(VHost, Date),
2488 + [{Pat, [], ['$_']}])
2490 + {atomic, Result} -> {ok, Result};
2491 + {aborted, Reason} ->
2494 + {reply, Reply, State};
2495 +handle_call({get_dates}, _From, #state{vhost=VHost}=State) ->
2496 + {reply, get_dates_int(VHost), State};
2497 +handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) ->
2498 + Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}),
2499 + {reply, {ok, Reply}, State};
2500 +handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) ->
2502 + case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of
2507 + {reply, Reply, State};
2508 +handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) ->
2509 + ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]),
2510 + Reply = mnesia:dirty_write(settings_table(VHost), Set),
2511 + ?MYDEBUG("~p", [Reply]),
2512 + {reply, Reply, State};
2513 +handle_call({drop_user, User}, _From, #state{vhost=VHost}=State) ->
2514 + {ok, Dates} = get_user_stats_int(User, VHost),
2515 + MDResult = lists:map(fun({Date, _}) ->
2516 + delete_all_messages_by_user_at_int(User, VHost, Date)
2518 + SDResult = delete_user_settings_int(User, VHost),
2520 + case lists:all(fun(Result) when Result == ok ->
2522 + (Result) when Result == error ->
2524 + end, lists:append(MDResult, [SDResult])) of
2530 + {reply, Reply, State};
2531 +handle_call({stop}, _From, State) ->
2532 + {stop, normal, ok, State};
2533 +handle_call(Msg, _From, State) ->
2534 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
2537 +handle_cast(Msg, State) ->
2538 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
2541 +handle_info(Info, State) ->
2542 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
2545 +terminate(_Reason, _State) ->
2548 +code_change(_OldVsn, State, _Extra) ->
2551 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2553 +% gen_logdb callbacks
2555 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2556 +log_message(VHost, Msg) ->
2557 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2558 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
2559 +rebuild_stats(VHost) ->
2560 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2561 + gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
2562 +rebuild_stats_at(VHost, Date) ->
2563 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2564 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
2565 +delete_messages_by_user_at(VHost, Msgs, Date) ->
2566 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2567 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
2568 +delete_all_messages_by_user_at(User, VHost, Date) ->
2569 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2570 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
2571 +delete_messages_at(VHost, Date) ->
2572 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2573 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
2574 +get_vhost_stats(VHost) ->
2575 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2576 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
2577 +get_vhost_stats_at(VHost, Date) ->
2578 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2579 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
2580 +get_user_stats(User, VHost) ->
2581 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2582 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
2583 +get_user_messages_at(User, VHost, Date) ->
2584 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2585 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
2586 +get_dates(VHost) ->
2587 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2588 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
2589 +get_user_settings(User, VHost) ->
2590 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2591 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
2592 +get_users_settings(VHost) ->
2593 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2594 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
2595 +set_user_settings(User, VHost, Set) ->
2596 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2597 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
2598 +drop_user(User, VHost) ->
2599 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2600 + gen_server:call(Proc, {drop_user, User}, ?CALL_TIMEOUT).
2602 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2606 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2607 +log_message_int(VHost, #msg{timestamp=Timestamp}=MsgBin) ->
2608 + Date = mod_logdb:convert_timestamp_brief(Timestamp),
2610 + Msg = #msg{timestamp = MsgBin#msg.timestamp,
2611 + owner_name = binary_to_list(MsgBin#msg.owner_name),
2612 + peer_name = binary_to_list(MsgBin#msg.peer_name),
2613 + peer_server = binary_to_list(MsgBin#msg.peer_server),
2614 + peer_resource = binary_to_list(MsgBin#msg.peer_resource),
2615 + direction = MsgBin#msg.direction,
2616 + type = binary_to_list(MsgBin#msg.type),
2617 + subject = binary_to_list(MsgBin#msg.subject),
2618 + body = binary_to_list(MsgBin#msg.body)},
2620 + ATable = table_name(VHost, Date),
2622 + mnesia:write_lock_table(ATable),
2623 + mnesia:write(ATable, Msg, write)
2625 + % log message, increment stats for both users
2626 + case mnesia:transaction(Fun) of
2627 + % if table does not exists - create it and try to log message again
2628 + {aborted,{no_exists, _Table}} ->
2629 + case create_msg_table(VHost, Date) of
2630 + {aborted, CReason} ->
2631 + ?ERROR_MSG("Failed to log message: ~p", [CReason]),
2634 + ?MYDEBUG("Created msg table for ~s at ~s", [VHost, Date]),
2635 + log_message_int(VHost, MsgBin)
2637 + {aborted, TReason} ->
2638 + ?ERROR_MSG("Failed to log message: ~p", [TReason]),
2641 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
2642 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
2643 + increment_user_stats(Msg#msg.owner_name, VHost, Date)
2646 +increment_user_stats(Owner, VHost, Date) ->
2648 + Pat = #stats{user=Owner, at=Date, count='$1'},
2649 + mnesia:write_lock_table(stats_table(VHost)),
2650 + case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of
2652 + mnesia:write(stats_table(VHost),
2653 + #stats{user=Owner,
2658 + mnesia:delete_object(stats_table(VHost),
2659 + #stats{user=Owner,
2661 + count=Stats#stats.count},
2663 + New = Stats#stats{count = Stats#stats.count+1},
2665 + New#stats.count > 0 -> mnesia:write(stats_table(VHost),
2672 + case mnesia:transaction(Fun) of
2673 + {aborted, Reason} ->
2674 + ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]),
2677 + ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]),
2681 +get_dates_int(VHost) ->
2682 + Tables = mnesia:system_info(tables),
2683 + lists:foldl(fun(ATable, Dates) ->
2684 + Table = term_to_binary(ATable),
2685 + case ejabberd_regexp:run( Table, << VHost/binary, <<"$">>/binary >> ) of
2687 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
2688 + {match, [{S, E}]} ->
2689 + lists:append(Dates, [lists:sublist(binary_to_list(Table), S+1, E)]);
2698 +rebuild_stats_at_int(VHost, Date) ->
2699 + Table = table_name(VHost, Date),
2700 + STable = stats_table(VHost),
2701 + CFun = fun(Msg, Stats) ->
2702 + Owner = Msg#msg.owner_name,
2703 + case lists:keysearch(Owner, 1, Stats) of
2704 + {value, {_, Count}} ->
2705 + lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1});
2707 + lists:append(Stats, [{Owner, 1}])
2710 + DFun = fun(#stats{at=SDate} = Stat, _Acc)
2711 + when SDate == Date ->
2712 + mnesia:delete_object(stats_table(VHost), Stat, write);
2713 + (_Stat, _Acc) -> ok
2715 + % TODO: Maybe unregister hooks ?
2716 + case mnesia:transaction(fun() ->
2717 + mnesia:write_lock_table(Table),
2718 + mnesia:write_lock_table(STable),
2719 + % Delete all stats for VHost at Date
2720 + mnesia:foldl(DFun, [], STable),
2721 + % Calc stats for VHost at Date
2722 + case mnesia:foldl(CFun, [], Table) of
2725 + % Write new calc'ed stats
2726 + lists:foreach(fun({Owner, Count}) ->
2727 + WStat = #stats{user=Owner, at=Date, count=Count},
2728 + mnesia:write(stats_table(VHost), WStat, write)
2733 + {aborted, Reason} ->
2734 + ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]),
2738 + {atomic, empty} ->
2739 + {atomic,ok} = mnesia:delete_table(Table),
2740 + ?MYDEBUG("Dropped table at ~p", [Date]),
2744 +delete_nonexistent_stats(VHost) ->
2745 + Dates = get_dates_int(VHost),
2746 + mnesia:transaction(fun() ->
2747 + mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) ->
2748 + case lists:member(Date, Dates) of
2749 + false -> mnesia:delete_object(Stat);
2752 + end, ok, stats_table(VHost))
2755 +delete_stats_by_vhost_at_int(VHost, Date) ->
2756 + StatsDelete = fun(#stats{at=SDate} = Stat, _Acc)
2757 + when SDate == Date ->
2758 + mnesia:delete_object(stats_table(VHost), Stat, write),
2760 + (_Msg, _Acc) -> ok
2762 + case mnesia:transaction(fun() ->
2763 + mnesia:write_lock_table(stats_table(VHost)),
2764 + mnesia:foldl(StatsDelete, ok, stats_table(VHost))
2766 + {aborted, Reason} ->
2767 + ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]),
2768 + rebuild_stats_at_int(VHost, Date);
2770 + ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]),
2774 +get_user_stats_int(User, VHost) ->
2775 + case mnesia:transaction(fun() ->
2776 + Pat = #stats{user=User, at='$1', count='$2'},
2777 + mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
2779 + {atomic, Result} ->
2780 + {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])};
2781 + {aborted, Reason} ->
2785 +delete_all_messages_by_user_at_int(User, VHost, Date) ->
2786 + Table = table_name(VHost, Date),
2787 + MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc)
2788 + when Owner == User ->
2789 + mnesia:delete_object(Table, Msg, write),
2791 + (_Msg, _Acc) -> ok
2793 + DRez = case mnesia:transaction(fun() ->
2794 + mnesia:foldl(MsgDelete, ok, Table)
2796 + {aborted, Reason} ->
2797 + ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]),
2802 + case rebuild_stats_at_int(VHost, Date) of
2809 +delete_user_settings_int(User, VHost) ->
2810 + STable = settings_table(VHost),
2811 + case mnesia:dirty_match_object(STable, #user_settings{owner_name=User, _='_'}) of
2815 + mnesia:dirty_delete_object(STable, UserSettings)
2818 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2822 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2823 +create_stats_table(VHost) ->
2824 + SName = stats_table(VHost),
2825 + case mnesia:create_table(SName,
2826 + [{disc_only_copies, [node()]},
2828 + {attributes, record_info(fields, stats)},
2829 + {record_name, stats}
2832 + ?MYDEBUG("Created stats table for ~p", [VHost]),
2833 + lists:foreach(fun(Date) ->
2834 + rebuild_stats_at_int(VHost, Date)
2835 + end, get_dates_int(VHost)),
2837 + {aborted, {already_exists, _}} ->
2838 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
2840 + {aborted, Reason} ->
2841 + ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
2845 +create_settings_table(VHost) ->
2846 + SName = settings_table(VHost),
2847 + case mnesia:create_table(SName,
2848 + [{disc_copies, [node()]},
2850 + {attributes, record_info(fields, user_settings)},
2851 + {record_name, user_settings}
2854 + ?MYDEBUG("Created settings table for ~p", [VHost]),
2856 + {aborted, {already_exists, _}} ->
2857 + ?MYDEBUG("Settings table for ~p already exists", [VHost]),
2859 + {aborted, Reason} ->
2860 + ?ERROR_MSG("Failed to create settings table: ~p", [Reason]),
2864 +create_msg_table(VHost, Date) ->
2865 + mnesia:create_table(
2866 + table_name(VHost, Date),
2867 + [{disc_only_copies, [node()]},
2869 + {attributes, record_info(fields, msg)},
2870 + {record_name, msg}]).
2871 diff --git a/src/mod_logdb_mysql.erl b/src/mod_logdb_mysql.erl
2872 new file mode 100644
2873 index 00000000..21d65e65
2875 +++ b/src/mod_logdb_mysql.erl
2877 +%%%----------------------------------------------------------------------
2878 +%%% File : mod_logdb_mysql.erl
2879 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2880 +%%% Purpose : MySQL backend for mod_logdb
2881 +%%% Url : https://paleg.github.io/mod_logdb/
2882 +%%%----------------------------------------------------------------------
2884 +-module(mod_logdb_mysql).
2885 +-author('o.palij@gmail.com').
2887 +-include("mod_logdb.hrl").
2888 +-include("logger.hrl").
2890 +-behaviour(gen_logdb).
2891 +-behaviour(gen_server).
2894 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2896 +-export([start/2, stop/1]).
2898 +-export([log_message/2,
2900 + rebuild_stats_at/2,
2901 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2902 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2904 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
2907 +% gen_server call timeout
2908 +-define(CALL_TIMEOUT, 30000).
2909 +-define(MYSQL_TIMEOUT, 60000).
2910 +-define(INDEX_SIZE, integer_to_list(170)).
2911 +-define(PROCNAME, mod_logdb_mysql).
2913 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
2914 + list_to_string/1, string_to_list/1,
2915 + convert_timestamp_brief/1]).
2917 +-record(state, {dbref, vhost, server, port, db, user, password}).
2919 +% replace "." with "_"
2920 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
2922 + end, binary_to_list(VHost)).
2927 + "_" ++ escape_vhost(VHost) ++ "`".
2929 +messages_table(VHost, Date) ->
2930 + prefix() ++ "messages_" ++ Date ++ suffix(VHost).
2932 +stats_table(VHost) ->
2933 + prefix() ++ "stats" ++ suffix(VHost).
2935 +temp_table(VHost) ->
2936 + prefix() ++ "temp" ++ suffix(VHost).
2938 +settings_table(VHost) ->
2939 + prefix() ++ "settings" ++ suffix(VHost).
2941 +users_table(VHost) ->
2942 + prefix() ++ "users" ++ suffix(VHost).
2943 +servers_table(VHost) ->
2944 + prefix() ++ "servers" ++ suffix(VHost).
2945 +resources_table(VHost) ->
2946 + prefix() ++ "resources" ++ suffix(VHost).
2948 +ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ binary_to_list(VHost)).
2949 +ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ binary_to_list(VHost)).
2950 +ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ binary_to_list(VHost)).
2952 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2954 +% gen_mod callbacks
2956 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2957 +start(VHost, Opts) ->
2958 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2959 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2962 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2963 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2965 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2967 +% gen_server callbacks
2969 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2970 +init([VHost, Opts]) ->
2973 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
2974 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
2975 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
2976 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
2977 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
2979 + St = #state{vhost=VHost,
2980 + server=Server, port=Port, db=DB,
2981 + user=User, password=Password},
2983 + case open_mysql_connection(St) of
2985 + State = St#state{dbref=DBRef},
2986 + ok = create_stats_table(State),
2987 + ok = create_settings_table(State),
2988 + ok = create_users_table(State),
2989 + % clear ets cache every ...
2990 + timer:send_interval(timer:hours(12), clear_ets_tables),
2991 + ok = create_servers_table(State),
2992 + ok = create_resources_table(State),
2993 + erlang:monitor(process, DBRef),
2995 + {error, Reason} ->
2996 + ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
2997 + {stop, db_connection_failed}
3000 +open_mysql_connection(#state{server=Server, port=Port, db=DB,
3001 + user=DBUser, password=Password} = _State) ->
3002 + LogFun = fun(debug, _Format, _Argument) ->
3003 + %?MYDEBUG(Format, Argument);
3005 + (error, Format, Argument) ->
3006 + ?ERROR_MSG(Format, Argument);
3007 + (Level, Format, Argument) ->
3008 + ?MYDEBUG("MySQL (~p)~n", [Level]),
3009 + ?MYDEBUG(Format, Argument)
3011 + ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
3012 + p1_mysql_conn:start(binary_to_list(Server), Port,
3013 + binary_to_list(DBUser), binary_to_list(Password),
3014 + binary_to_list(DB), LogFun).
3016 +close_mysql_connection(DBRef) ->
3017 + ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
3018 + catch p1_mysql_conn:stop(DBRef).
3020 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3021 + Date = convert_timestamp_brief(Msg#msg.timestamp),
3023 + Table = messages_table(VHost, Date),
3024 + Owner_id = get_user_id(DBRef, VHost, binary_to_list(Msg#msg.owner_name)),
3025 + Peer_name_id = get_user_id(DBRef, VHost, binary_to_list(Msg#msg.peer_name)),
3026 + Peer_server_id = get_server_id(DBRef, VHost, binary_to_list(Msg#msg.peer_server)),
3027 + Peer_resource_id = get_resource_id(DBRef, VHost, binary_to_list(Msg#msg.peer_resource)),
3029 + Query = ["INSERT INTO ",Table," ",
3032 + "peer_server_id,",
3033 + "peer_resource_id,",
3040 + "('", Owner_id, "',",
3041 + "'", Peer_name_id, "',",
3042 + "'", Peer_server_id, "',",
3043 + "'", Peer_resource_id, "',",
3044 + "'", atom_to_list(Msg#msg.direction), "',",
3045 + "'", binary_to_list(Msg#msg.type), "',",
3046 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
3047 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
3048 + "'", Msg#msg.timestamp, "');"],
3051 + case sql_query_internal_silent(DBRef, Query) of
3053 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
3054 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
3055 + increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date);
3056 + {error, Reason} ->
3057 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S02">>) of
3058 + % Table doesn't exist
3060 + case create_msg_table(DBRef, VHost, Date) of
3064 + {updated, _} = sql_query_internal(DBRef, Query),
3065 + increment_user_stats(DBRef, binary_to_list(Msg#msg.owner_name), Owner_id, VHost, Peer_name_id, Peer_server_id, Date)
3068 + ?ERROR_MSG("Failed to log message: ~p", [Reason]),
3072 + {reply, Reply, State};
3073 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3074 + Reply = rebuild_stats_at_int(DBRef, VHost, Date),
3075 + {reply, Reply, State};
3076 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
3077 + {reply, error, State};
3078 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3079 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
3080 + ["\"",Timestamp,"\"",","]
3083 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3085 + Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
3086 + "WHERE timestamp IN (", Temp1],
3089 + case sql_query_internal(DBRef, Query) of
3091 + ?MYDEBUG("Aff=~p", [Aff]),
3092 + rebuild_stats_at_int(DBRef, VHost, Date);
3096 + {reply, Reply, State};
3097 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3098 + ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
3099 + ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
3100 + {reply, ok, State};
3101 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3103 + case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of
3105 + Query = ["DELETE FROM ",stats_table(VHost)," "
3106 + "WHERE at=\"",Date,"\";"],
3107 + case sql_query_internal(DBRef, Query) of
3116 + {reply, Reply, State};
3117 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3118 + SName = stats_table(VHost),
3119 + Query = ["SELECT at, sum(count) ",
3120 + "FROM ",SName," ",
3122 + "ORDER BY DATE(at) DESC;"
3125 + case sql_query_internal(DBRef, Query) of
3127 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
3128 + {error, Reason} ->
3129 + % TODO: Duplicate error message ?
3132 + {reply, Reply, State};
3133 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3134 + SName = stats_table(VHost),
3135 + Query = ["SELECT username, sum(count) AS allcount ",
3136 + "FROM ",SName," ",
3137 + "JOIN ",users_table(VHost)," ON owner_id=user_id "
3138 + "WHERE at=\"",Date,"\" "
3139 + "GROUP BY username ",
3140 + "ORDER BY allcount DESC;"
3143 + case sql_query_internal(DBRef, Query) of
3145 + {ok, lists:reverse(
3147 + [ {User, list_to_integer(Count)} || [User, Count] <- Result]))};
3148 + {error, Reason} ->
3152 + {reply, Reply, State};
3153 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3154 + {reply, get_user_stats_int(DBRef, User, VHost), State};
3155 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3156 + TName = messages_table(VHost, Date),
3157 + UName = users_table(VHost),
3158 + SName = servers_table(VHost),
3159 + RName = resources_table(VHost),
3160 + Query = ["SELECT users.username,",
3161 + "servers.server,",
3162 + "resources.resource,",
3163 + "messages.direction,"
3165 + "messages.subject,"
3167 + "messages.timestamp "
3168 + "FROM ",TName," AS messages "
3169 + "JOIN ",UName," AS users ON peer_name_id=user_id ",
3170 + "JOIN ",SName," AS servers ON peer_server_id=server_id ",
3171 + "JOIN ",RName," AS resources ON peer_resource_id=resource_id ",
3172 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
3173 + "ORDER BY timestamp ASC;"],
3175 + case sql_query_internal(DBRef, Query) of
3177 + Fun = fun([Peer_name, Peer_server, Peer_resource,
3182 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
3183 + direction=list_to_atom(Direction),
3185 + subject=Subject, body=Body,
3186 + timestamp=Timestamp}
3188 + {ok, lists:map(Fun, Result)};
3189 + {error, Reason} ->
3192 + {reply, Reply, State};
3193 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3194 + SName = stats_table(VHost),
3195 + Query = ["SELECT at ",
3196 + "FROM ",SName," ",
3198 + "ORDER BY DATE(at) DESC;"
3201 + case sql_query_internal(DBRef, Query) of
3203 + [ Date || [Date] <- Result ];
3204 + {error, Reason} ->
3207 + {reply, Reply, State};
3208 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3209 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
3210 + "FROM ",settings_table(VHost)," ",
3211 + "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
3213 + case sql_query_internal(DBRef, Query) of
3215 + {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
3216 + #user_settings{owner_name=Owner,
3217 + dolog_default=list_to_bool(DoLogDef),
3218 + dolog_list=string_to_list(DoLogL),
3219 + donotlog_list=string_to_list(DoNotLogL)
3225 + {reply, Reply, State};
3226 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3227 + Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
3228 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"],
3230 + case sql_query_internal(DBRef, Query) of
3233 + {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
3234 + {ok, #user_settings{owner_name=Owner,
3235 + dolog_default=list_to_bool(DoLogDef),
3236 + dolog_list=string_to_list(DoLogL),
3237 + donotlog_list=string_to_list(DoNotLogL)}};
3241 + {reply, Reply, State};
3242 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
3243 + dolog_list=DoLogL,
3244 + donotlog_list=DoNotLogL}},
3245 + _From, #state{dbref=DBRef, vhost=VHost} = State) ->
3246 + User_id = get_user_id(DBRef, VHost, User),
3248 + Query = ["UPDATE ",settings_table(VHost)," ",
3249 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
3250 + "dolog_list='",list_to_string(DoLogL),"', ",
3251 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
3252 + "WHERE owner_id=\"",User_id,"\";"],
3255 + case sql_query_internal(DBRef, Query) of
3257 + IQuery = ["INSERT INTO ",settings_table(VHost)," ",
3258 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
3260 + "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
3261 + case sql_query_internal_silent(DBRef, IQuery) of
3263 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
3265 + {error, Reason} ->
3266 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
3271 + ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
3276 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
3281 + {reply, Reply, State};
3282 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
3283 + ets:delete(ets_users_table(VHost)),
3284 + ets:delete(ets_servers_table(VHost)),
3285 + ?MYDEBUG("Stoping mysql backend for ~p", [VHost]),
3286 + {stop, normal, ok, State};
3287 +handle_call(Msg, _From, State) ->
3288 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
3291 +handle_cast({rebuild_stats}, State) ->
3292 + rebuild_all_stats_int(State),
3294 +handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
3296 + {ok, DBRef} = open_mysql_connection(State),
3297 + {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
3298 + MDResult = lists:map(fun({Date, _}) ->
3299 + delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
3301 + StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
3302 + SDResult = delete_user_settings_int(DBRef, User, VHost),
3303 + case lists:all(fun(Result) when Result == ok ->
3305 + (Result) when Result == error ->
3307 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
3309 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
3311 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
3313 + close_mysql_connection(DBRef)
3317 +handle_cast(Msg, State) ->
3318 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
3321 +handle_info(clear_ets_tables, State) ->
3322 + ets:delete_all_objects(ets_users_table(State#state.vhost)),
3323 + ets:delete_all_objects(ets_resources_table(State#state.vhost)),
3325 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
3326 + {stop, connection_dropped, State};
3327 +handle_info(Info, State) ->
3328 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
3331 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
3332 + close_mysql_connection(DBRef),
3335 +code_change(_OldVsn, State, _Extra) ->
3338 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3340 +% gen_logdb callbacks
3342 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3343 +log_message(VHost, Msg) ->
3344 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3345 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
3346 +rebuild_stats(VHost) ->
3347 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3348 + gen_server:cast(Proc, {rebuild_stats}).
3349 +rebuild_stats_at(VHost, Date) ->
3350 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3351 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
3352 +delete_messages_by_user_at(VHost, Msgs, Date) ->
3353 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3354 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
3355 +delete_all_messages_by_user_at(User, VHost, Date) ->
3356 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3357 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
3358 +delete_messages_at(VHost, Date) ->
3359 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3360 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
3361 +get_vhost_stats(VHost) ->
3362 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3363 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
3364 +get_vhost_stats_at(VHost, Date) ->
3365 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3366 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
3367 +get_user_stats(User, VHost) ->
3368 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3369 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
3370 +get_user_messages_at(User, VHost, Date) ->
3371 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3372 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
3373 +get_dates(VHost) ->
3374 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3375 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
3376 +get_users_settings(VHost) ->
3377 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3378 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
3379 +get_user_settings(User, VHost) ->
3380 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3381 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
3382 +set_user_settings(User, VHost, Set) ->
3383 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3384 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
3385 +drop_user(User, VHost) ->
3386 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3387 + gen_server:cast(Proc, {drop_user, User}).
3389 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3393 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3394 +increment_user_stats(DBRef, User_name, User_id, VHost, PNameID, PServerID, Date) ->
3395 + SName = stats_table(VHost),
3396 + UQuery = ["UPDATE ",SName," ",
3397 + "SET count=count+1 ",
3398 + "WHERE owner_id=\"",User_id,"\" AND peer_name_id=\"",PNameID,"\" AND peer_server_id=\"",PServerID,"\" AND at=\"",Date,"\";"],
3400 + case sql_query_internal(DBRef, UQuery) of
3402 + IQuery = ["INSERT INTO ",SName," ",
3403 + "(owner_id, peer_name_id, peer_server_id, at, count) ",
3405 + "('",User_id,"', '",PNameID,"', '",PServerID,"', '",Date,"', '1');"],
3406 + case sql_query_internal(DBRef, IQuery) of
3408 + ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]),
3414 + ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]),
3420 +get_dates_int(DBRef, VHost) ->
3421 + case sql_query_internal(DBRef, ["SHOW TABLES"]) of
3423 + Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
3424 + lists:foldl(fun([Table], Dates) ->
3425 + case re:run(Table, Reg) of
3427 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
3428 + {match, [{S, E}]} ->
3429 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
3441 +rebuild_all_stats_int(#state{vhost=VHost}=State) ->
3443 + {ok, DBRef} = open_mysql_connection(State),
3444 + ok = delete_nonexistent_stats(DBRef, VHost),
3445 + case lists:filter(fun(Date) ->
3446 + case catch rebuild_stats_at_int(DBRef, VHost, Date) of
3449 + {'EXIT', _} -> true
3451 + end, get_dates_int(DBRef, VHost)) of
3454 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
3457 + close_mysql_connection(DBRef)
3461 +rebuild_stats_at_int(DBRef, VHost, Date) ->
3462 + TempTable = temp_table(VHost),
3464 + Table = messages_table(VHost, Date),
3465 + STable = stats_table(VHost),
3467 + DQuery = [ "DELETE FROM ",STable," ",
3468 + "WHERE at='",Date,"';"],
3470 + ok = create_temp_table(DBRef, TempTable),
3471 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
3472 + SQuery = ["INSERT INTO ",TempTable," ",
3473 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
3474 + "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
3475 + "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
3476 + case sql_query_internal(DBRef, SQuery) of
3478 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
3480 + {data, [["0"]]} ->
3481 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
3482 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]),
3483 + {updated, _} = sql_query_internal(DBRef, DQuery),
3486 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
3490 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
3491 + {updated, _} = sql_query_internal(DBRef, DQuery),
3492 + SQuery1 = ["INSERT INTO ",STable," ",
3493 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
3494 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
3495 + "FROM ",TempTable,";"],
3496 + case sql_query_internal(DBRef, SQuery1) of
3497 + {updated, _} -> ok;
3498 + {error, _} -> error
3500 + {error, _} -> error
3504 + case catch apply(Fun, []) of
3506 + ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
3510 + {'EXIT', Reason} ->
3511 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
3514 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
3515 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
3519 +delete_nonexistent_stats(DBRef, VHost) ->
3520 + Dates = get_dates_int(DBRef, VHost),
3521 + STable = stats_table(VHost),
3523 + Temp = lists:flatmap(fun(Date) ->
3524 + ["\"",Date,"\"",","]
3531 + % replace last "," with ");"
3532 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3533 + Query = ["DELETE FROM ",STable," ",
3534 + "WHERE at NOT IN (", Temp1],
3535 + case sql_query_internal(DBRef, Query) of
3543 +get_user_stats_int(DBRef, User, VHost) ->
3544 + SName = stats_table(VHost),
3545 + Query = ["SELECT at, sum(count) as allcount ",
3546 + "FROM ",SName," ",
3547 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
3549 + "ORDER BY DATE(at) DESC;"
3551 + case sql_query_internal(DBRef, Query) of
3553 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]};
3554 + {error, Result} ->
3558 +delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
3559 + DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
3560 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3561 + case sql_query_internal(DBRef, DQuery) of
3563 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
3569 +delete_all_stats_by_user_int(DBRef, User, VHost) ->
3570 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
3571 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3572 + case sql_query_internal(DBRef, SQuery) of
3574 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
3576 + {error, _} -> error
3579 +delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
3580 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
3581 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
3582 + "AND at=\"",Date,"\";"],
3583 + case sql_query_internal(DBRef, SQuery) of
3585 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
3587 + {error, _} -> error
3590 +delete_user_settings_int(DBRef, User, VHost) ->
3591 + Query = ["DELETE FROM ",settings_table(VHost)," ",
3592 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3593 + case sql_query_internal(DBRef, Query) of
3595 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
3597 + {error, Reason} ->
3598 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
3602 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3606 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3607 +create_temp_table(DBRef, Name) ->
3608 + Query = ["CREATE TABLE ",Name," (",
3609 + "owner_id MEDIUMINT UNSIGNED, ",
3610 + "peer_name_id MEDIUMINT UNSIGNED, ",
3611 + "peer_server_id MEDIUMINT UNSIGNED, ",
3612 + "at VARCHAR(11), ",
3614 + ") ENGINE=MyISAM CHARACTER SET utf8;"
3616 + case sql_query_internal(DBRef, Query) of
3617 + {updated, _} -> ok;
3618 + {error, _Reason} -> error
3621 +create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
3622 + SName = stats_table(VHost),
3623 + Query = ["CREATE TABLE ",SName," (",
3624 + "owner_id MEDIUMINT UNSIGNED, ",
3625 + "peer_name_id MEDIUMINT UNSIGNED, ",
3626 + "peer_server_id MEDIUMINT UNSIGNED, ",
3627 + "at varchar(20), ",
3628 + "count int(11), ",
3629 + "INDEX(owner_id, peer_name_id, peer_server_id), ",
3631 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3633 + case sql_query_internal_silent(DBRef, Query) of
3635 + ?INFO_MSG("Created stats table for ~p", [VHost]),
3636 + rebuild_all_stats_int(State),
3638 + {error, Reason} ->
3639 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
3641 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
3642 + CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
3643 + case sql_query_internal(DBRef, CheckQuery) of
3644 + {data, Elems} when length(Elems) == 2 ->
3645 + ?MYDEBUG("Stats table structure is ok", []),
3648 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
3649 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
3651 + ?INFO_MSG("Successfully dropped ~p", [SName]);
3653 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
3658 + ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
3663 +create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
3664 + SName = settings_table(VHost),
3665 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3666 + "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
3667 + "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
3668 + "dolog_list TEXT, ",
3669 + "donotlog_list TEXT ",
3670 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3672 + case sql_query_internal(DBRef, Query) of
3674 + ?MYDEBUG("Created settings table for ~p", [VHost]),
3680 +create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
3681 + SName = users_table(VHost),
3682 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3683 + "username TEXT NOT NULL, ",
3684 + "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3685 + "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
3686 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3688 + case sql_query_internal(DBRef, Query) of
3690 + ?MYDEBUG("Created users table for ~p", [VHost]),
3691 + ets:new(ets_users_table(VHost), [named_table, set, public]),
3692 + %update_users_from_db(DBRef, VHost),
3698 +create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
3699 + SName = servers_table(VHost),
3700 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3701 + "server TEXT NOT NULL, ",
3702 + "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3703 + "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
3704 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3706 + case sql_query_internal(DBRef, Query) of
3708 + ?MYDEBUG("Created servers table for ~p", [VHost]),
3709 + ets:new(ets_servers_table(VHost), [named_table, set, public]),
3710 + update_servers_from_db(DBRef, VHost),
3716 +create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
3717 + RName = resources_table(VHost),
3718 + Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
3719 + "resource TEXT NOT NULL, ",
3720 + "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3721 + "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
3722 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3724 + case sql_query_internal(DBRef, Query) of
3726 + ?MYDEBUG("Created resources table for ~p", [VHost]),
3727 + ets:new(ets_resources_table(VHost), [named_table, set, public]),
3733 +create_msg_table(DBRef, VHost, Date) ->
3734 + TName = messages_table(VHost, Date),
3735 + Query = ["CREATE TABLE ",TName," (",
3736 + "owner_id MEDIUMINT UNSIGNED, ",
3737 + "peer_name_id MEDIUMINT UNSIGNED, ",
3738 + "peer_server_id MEDIUMINT UNSIGNED, ",
3739 + "peer_resource_id MEDIUMINT(8) UNSIGNED, ",
3740 + "direction ENUM('to', 'from'), ",
3741 + "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ",
3744 + "timestamp DOUBLE, ",
3745 + "INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ",
3746 + "FULLTEXT (body) "
3747 + ") ENGINE=MyISAM CHARACTER SET utf8;"
3749 + case sql_query_internal(DBRef, Query) of
3750 + {updated, _MySQLRes} ->
3751 + ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
3757 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3759 +% internal ets cache (users, servers, resources)
3761 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3762 +update_servers_from_db(DBRef, VHost) ->
3763 + ?INFO_MSG("Reading servers from db for ~p", [VHost]),
3764 + SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"],
3765 + {data, Result} = sql_query_internal(DBRef, SQuery),
3766 + true = ets:delete_all_objects(ets_servers_table(VHost)),
3767 + true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]).
3769 +%update_users_from_db(DBRef, VHost) ->
3770 +% ?INFO_MSG("Reading users from db for ~p", [VHost]),
3771 +% SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"],
3772 +% {data, Result} = sql_query_internal(DBRef, SQuery),
3773 +% true = ets:delete_all_objects(ets_users_table(VHost)),
3774 +% true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]).
3776 +%get_user_name(DBRef, VHost, User_id) ->
3777 +% case ets:match(ets_users_table(VHost), {'$1', User_id}) of
3778 +% [[User]] -> User;
3779 +% % this can be in clustered environment
3781 +% %update_users_from_db(DBRef, VHost),
3782 +% SQuery = ["SELECT username FROM ",users_table(VHost)," ",
3783 +% "WHERE user_id=\"",User_id,"\";"],
3784 +% {data, [[Name]]} = sql_query_internal(DBRef, SQuery),
3785 +% % cache {user, id} pair
3786 +% ets:insert(ets_users_table(VHost), {Name, User_id}),
3790 +%get_server_name(DBRef, VHost, Server_id) ->
3791 +% case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of
3792 +% [[Server]] -> Server;
3793 + % this can be in clustered environment
3795 +% update_servers_from_db(DBRef, VHost),
3796 +% [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}),
3800 +get_user_id_from_db(DBRef, VHost, User) ->
3801 + SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
3802 + "WHERE username=\"",User,"\";"],
3803 + case sql_query_internal(DBRef, SQuery) of
3804 + % no such user in db
3807 + {data, [[DBId]]} ->
3808 + % cache {user, id} pair
3809 + ets:insert(ets_users_table(VHost), {User, DBId}),
3812 +get_user_id(DBRef, VHost, User) ->
3814 + case ets:match(ets_users_table(VHost), {User, '$1'}) of
3817 + case get_user_id_from_db(DBRef, VHost, User) of
3818 + % no such user in db
3820 + IQuery = ["INSERT INTO ",users_table(VHost)," ",
3821 + "SET username=\"",User,"\";"],
3822 + case sql_query_internal_silent(DBRef, IQuery) of
3824 + {ok, NewId} = get_user_id_from_db(DBRef, VHost, User),
3826 + {error, Reason} ->
3827 + % this can be in clustered environment
3828 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3829 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
3830 + {ok, ClID} = get_user_id_from_db(DBRef, VHost, User),
3836 + [[EtsId]] -> EtsId
3839 +get_server_id(DBRef, VHost, Server) ->
3840 + case ets:match(ets_servers_table(VHost), {Server, '$1'}) of
3842 + IQuery = ["INSERT INTO ",servers_table(VHost)," ",
3843 + "SET server=\"",Server,"\";"],
3844 + case sql_query_internal_silent(DBRef, IQuery) of
3846 + SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ",
3847 + "WHERE server=\"",Server,"\";"],
3848 + {data, [[Id]]} = sql_query_internal(DBRef, SQuery),
3849 + ets:insert(ets_servers_table(VHost), {Server, Id}),
3851 + {error, Reason} ->
3852 + % this can be in clustered environment
3853 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3854 + ?ERROR_MSG("Duplicate key name for ~p", [Server]),
3855 + update_servers_from_db(DBRef, VHost),
3856 + [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}),
3862 +get_resource_id_from_db(DBRef, VHost, Resource) ->
3863 + SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ",
3864 + "WHERE resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
3865 + case sql_query_internal(DBRef, SQuery) of
3866 + % no such resource in db
3869 + {data, [[DBId]]} ->
3870 + % cache {resource, id} pair
3871 + ets:insert(ets_resources_table(VHost), {Resource, DBId}),
3874 +get_resource_id(DBRef, VHost, Resource) ->
3876 + case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of
3879 + case get_resource_id_from_db(DBRef, VHost, Resource) of
3880 + % no such resource in db
3882 + IQuery = ["INSERT INTO ",resources_table(VHost)," ",
3883 + "SET resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
3884 + case sql_query_internal_silent(DBRef, IQuery) of
3886 + {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource),
3888 + {error, Reason} ->
3889 + % this can be in clustered environment
3890 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3891 + ?ERROR_MSG("Duplicate key name for ~s", [Resource]),
3892 + {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource),
3898 + [[EtsId]] -> EtsId
3901 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3905 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3906 +sql_query_internal(DBRef, Query) ->
3907 + case sql_query_internal_silent(DBRef, Query) of
3908 + {error, Reason} ->
3909 + ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
3914 +sql_query_internal_silent(DBRef, Query) ->
3915 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
3916 + get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
3918 +get_result({updated, MySQLRes}) ->
3919 + {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
3920 +get_result({data, MySQLRes}) ->
3921 + {data, p1_mysql:get_result_rows(MySQLRes)};
3922 +get_result({error, "query timed out"}) ->
3923 + {error, "query timed out"};
3924 +get_result({error, MySQLRes}) ->
3925 + Reason = p1_mysql:get_result_reason(MySQLRes),
3927 diff --git a/src/mod_logdb_mysql5.erl b/src/mod_logdb_mysql5.erl
3928 new file mode 100644
3929 index 00000000..c05ab958
3931 +++ b/src/mod_logdb_mysql5.erl
3933 +%%%----------------------------------------------------------------------
3934 +%%% File : mod_logdb_mysql5.erl
3935 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
3936 +%%% Purpose : MySQL 5 backend for mod_logdb
3937 +%%% Url : https://paleg.github.io/mod_logdb/
3938 +%%%----------------------------------------------------------------------
3940 +-module(mod_logdb_mysql5).
3941 +-author('o.palij@gmail.com').
3943 +-include("mod_logdb.hrl").
3944 +-include("logger.hrl").
3946 +-behaviour(gen_logdb).
3947 +-behaviour(gen_server).
3950 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
3952 +-export([start/2, stop/1]).
3954 +-export([log_message/2,
3956 + rebuild_stats_at/2,
3957 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
3958 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
3960 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
3963 +% gen_server call timeout
3964 +-define(CALL_TIMEOUT, 30000).
3965 +-define(MYSQL_TIMEOUT, 60000).
3966 +-define(INDEX_SIZE, integer_to_list(170)).
3967 +-define(PROCNAME, mod_logdb_mysql5).
3969 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
3970 + list_to_string/1, string_to_list/1,
3971 + convert_timestamp_brief/1]).
3973 +-record(state, {dbref, vhost, server, port, db, user, password}).
3975 +% replace "." with "_"
3976 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
3978 + end, binary_to_list(VHost)).
3983 + "_" ++ escape_vhost(VHost) ++ "`".
3985 +messages_table(VHost, Date) ->
3986 + prefix() ++ "messages_" ++ Date ++ suffix(VHost).
3988 +% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
3989 +view_table(VHost, Date) ->
3990 + Table = messages_table(VHost, Date),
3991 + TablewoQ = lists:sublist(Table, 2, length(Table) - 2),
3992 + lists:append(["`v_", TablewoQ, "`"]).
3994 +stats_table(VHost) ->
3995 + prefix() ++ "stats" ++ suffix(VHost).
3997 +temp_table(VHost) ->
3998 + prefix() ++ "temp" ++ suffix(VHost).
4000 +settings_table(VHost) ->
4001 + prefix() ++ "settings" ++ suffix(VHost).
4003 +users_table(VHost) ->
4004 + prefix() ++ "users" ++ suffix(VHost).
4005 +servers_table(VHost) ->
4006 + prefix() ++ "servers" ++ suffix(VHost).
4007 +resources_table(VHost) ->
4008 + prefix() ++ "resources" ++ suffix(VHost).
4010 +logmessage_name(VHost) ->
4011 + prefix() ++ "logmessage" ++ suffix(VHost).
4013 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4015 +% gen_mod callbacks
4017 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4018 +start(VHost, Opts) ->
4019 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4020 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
4023 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4024 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
4026 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4028 +% gen_server callbacks
4030 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4031 +init([VHost, Opts]) ->
4034 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
4035 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
4036 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
4037 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
4038 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
4040 + St = #state{vhost=VHost,
4041 + server=Server, port=Port, db=DB,
4042 + user=User, password=Password},
4044 + case open_mysql_connection(St) of
4046 + State = St#state{dbref=DBRef},
4047 + ok = create_internals(State),
4048 + ok = create_stats_table(State),
4049 + ok = create_settings_table(State),
4050 + ok = create_users_table(State),
4051 + ok = create_servers_table(State),
4052 + ok = create_resources_table(State),
4053 + erlang:monitor(process, DBRef),
4055 + {error, Reason} ->
4056 + ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
4057 + {stop, db_connection_failed}
4060 +open_mysql_connection(#state{server=Server, port=Port, db=DB,
4061 + user=DBUser, password=Password} = _State) ->
4062 + LogFun = fun(debug, _Format, _Argument) ->
4063 + %?MYDEBUG(Format, Argument);
4065 + (error, Format, Argument) ->
4066 + ?ERROR_MSG(Format, Argument);
4067 + (Level, Format, Argument) ->
4068 + ?MYDEBUG("MySQL (~p)~n", [Level]),
4069 + ?MYDEBUG(Format, Argument)
4071 + ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
4072 + p1_mysql_conn:start(binary_to_list(Server), Port,
4073 + binary_to_list(DBUser), binary_to_list(Password),
4074 + binary_to_list(DB), LogFun).
4076 +close_mysql_connection(DBRef) ->
4077 + ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
4078 + catch p1_mysql_conn:stop(DBRef).
4080 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4081 + Reply = rebuild_stats_at_int(DBRef, VHost, Date),
4082 + {reply, Reply, State};
4083 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
4084 + {reply, error, State};
4085 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4086 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
4087 + ["\"",Timestamp,"\"",","]
4090 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4092 + Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
4093 + "WHERE timestamp IN (", Temp1],
4096 + case sql_query_internal(DBRef, Query) of
4098 + ?MYDEBUG("Aff=~p", [Aff]),
4099 + rebuild_stats_at_int(DBRef, VHost, Date);
4103 + {reply, Reply, State};
4104 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4105 + ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
4106 + ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
4107 + {reply, ok, State};
4108 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4110 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]),
4111 + TQuery = ["DELETE FROM ",stats_table(VHost)," "
4112 + "WHERE at=\"",Date,"\";"],
4113 + {updated, _} = sql_query_internal(DBRef, TQuery),
4114 + VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"],
4115 + {updated, _} = sql_query_internal(DBRef, VQuery),
4119 + case catch apply(Fun, []) of
4125 + {reply, Reply, State};
4126 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4127 + SName = stats_table(VHost),
4128 + Query = ["SELECT at, sum(count) ",
4129 + "FROM ",SName," ",
4131 + "ORDER BY DATE(at) DESC;"
4134 + case sql_query_internal(DBRef, Query) of
4136 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
4137 + {error, Reason} ->
4138 + % TODO: Duplicate error message ?
4141 + {reply, Reply, State};
4142 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4143 + SName = stats_table(VHost),
4144 + Query = ["SELECT username, sum(count) as allcount ",
4145 + "FROM ",SName," ",
4146 + "JOIN ",users_table(VHost)," ON owner_id=user_id "
4147 + "WHERE at=\"",Date,"\" ",
4148 + "GROUP BY username ",
4149 + "ORDER BY allcount DESC;"
4152 + case sql_query_internal(DBRef, Query) of
4154 + {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
4155 + {error, Reason} ->
4158 + {reply, Reply, State};
4159 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4160 + {reply, get_user_stats_int(DBRef, User, VHost), State};
4161 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4162 + Query = ["SELECT peer_name,",
4170 + "FROM ",view_table(VHost, Date)," "
4171 + "WHERE owner_name=\"",User,"\";"],
4173 + case sql_query_internal(DBRef, Query) of
4175 + Fun = fun([Peer_name, Peer_server, Peer_resource,
4180 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
4181 + direction=list_to_atom(Direction),
4183 + subject=Subject, body=Body,
4184 + timestamp=Timestamp}
4186 + {ok, lists:map(Fun, Result)};
4187 + {error, Reason} ->
4190 + {reply, Reply, State};
4191 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4192 + SName = stats_table(VHost),
4193 + Query = ["SELECT at ",
4194 + "FROM ",SName," ",
4196 + "ORDER BY DATE(at) DESC;"
4199 + case sql_query_internal(DBRef, Query) of
4201 + [ Date || [Date] <- Result ];
4202 + {error, Reason} ->
4205 + {reply, Reply, State};
4206 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4207 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
4208 + "FROM ",settings_table(VHost)," ",
4209 + "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
4211 + case sql_query_internal(DBRef, Query) of
4213 + {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
4214 + #user_settings{owner_name=Owner,
4215 + dolog_default=list_to_bool(DoLogDef),
4216 + dolog_list=string_to_list(DoLogL),
4217 + donotlog_list=string_to_list(DoNotLogL)
4223 + {reply, Reply, State};
4224 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4225 + Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
4226 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4228 + case sql_query_internal(DBRef, Query) of
4231 + {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
4232 + {ok, #user_settings{owner_name=Owner,
4233 + dolog_default=list_to_bool(DoLogDef),
4234 + dolog_list=string_to_list(DoLogL),
4235 + donotlog_list=string_to_list(DoNotLogL)}};
4239 + {reply, Reply, State};
4240 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
4241 + dolog_list=DoLogL,
4242 + donotlog_list=DoNotLogL}},
4243 + _From, #state{dbref=DBRef, vhost=VHost} = State) ->
4244 + User_id = get_user_id(DBRef, VHost, User),
4245 + Query = ["UPDATE ",settings_table(VHost)," ",
4246 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
4247 + "dolog_list='",list_to_string(DoLogL),"', ",
4248 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
4249 + "WHERE owner_id=",User_id,";"],
4252 + case sql_query_internal(DBRef, Query) of
4254 + IQuery = ["INSERT INTO ",settings_table(VHost)," ",
4255 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
4257 + "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
4258 + case sql_query_internal_silent(DBRef, IQuery) of
4260 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
4262 + {error, Reason} ->
4263 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
4268 + ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
4273 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
4278 + {reply, Reply, State};
4279 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
4280 + ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]),
4281 + {stop, normal, ok, State};
4282 +handle_call(Msg, _From, State) ->
4283 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
4286 +handle_cast({log_message, Msg}, #state{dbref=DBRef, vhost=VHost}=State) ->
4288 + Date = convert_timestamp_brief(Msg#msg.timestamp),
4289 + TableName = messages_table(VHost, Date),
4291 + Query = [ "CALL ",logmessage_name(VHost)," "
4292 + "('", TableName, "',",
4294 + "'", binary_to_list(Msg#msg.owner_name), "',",
4295 + "'", binary_to_list(Msg#msg.peer_name), "',",
4296 + "'", binary_to_list(Msg#msg.peer_server), "',",
4297 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
4298 + "'", atom_to_list(Msg#msg.direction), "',",
4299 + "'", binary_to_list(Msg#msg.type), "',",
4300 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
4301 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
4302 + "'", Msg#msg.timestamp, "');"],
4304 + case sql_query_internal(DBRef, Query) of
4306 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
4307 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
4309 + {error, _Reason} ->
4315 +handle_cast({rebuild_stats}, State) ->
4316 + rebuild_all_stats_int(State),
4318 +handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
4320 + {ok, DBRef} = open_mysql_connection(State),
4321 + {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
4322 + MDResult = lists:map(fun({Date, _}) ->
4323 + delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
4325 + StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
4326 + SDResult = delete_user_settings_int(DBRef, User, VHost),
4327 + case lists:all(fun(Result) when Result == ok ->
4329 + (Result) when Result == error ->
4331 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
4333 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
4335 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
4337 + close_mysql_connection(DBRef)
4341 +handle_cast(Msg, State) ->
4342 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
4345 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
4346 + {stop, connection_dropped, State};
4347 +handle_info(Info, State) ->
4348 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
4351 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
4352 + close_mysql_connection(DBRef),
4355 +code_change(_OldVsn, State, _Extra) ->
4358 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4360 +% gen_logdb callbacks
4362 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4363 +log_message(VHost, Msg) ->
4364 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4365 + gen_server:cast(Proc, {log_message, Msg}).
4366 +rebuild_stats(VHost) ->
4367 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4368 + gen_server:cast(Proc, {rebuild_stats}).
4369 +rebuild_stats_at(VHost, Date) ->
4370 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4371 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
4372 +delete_messages_by_user_at(VHost, Msgs, Date) ->
4373 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4374 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
4375 +delete_all_messages_by_user_at(User, VHost, Date) ->
4376 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4377 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
4378 +delete_messages_at(VHost, Date) ->
4379 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4380 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
4381 +get_vhost_stats(VHost) ->
4382 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4383 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
4384 +get_vhost_stats_at(VHost, Date) ->
4385 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4386 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
4387 +get_user_stats(User, VHost) ->
4388 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4389 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
4390 +get_user_messages_at(User, VHost, Date) ->
4391 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4392 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
4393 +get_dates(VHost) ->
4394 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4395 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
4396 +get_users_settings(VHost) ->
4397 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4398 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
4399 +get_user_settings(User, VHost) ->
4400 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4401 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
4402 +set_user_settings(User, VHost, Set) ->
4403 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4404 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
4405 +drop_user(User, VHost) ->
4406 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4407 + gen_server:cast(Proc, {drop_user, User}).
4409 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4413 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4414 +get_dates_int(DBRef, VHost) ->
4415 + case sql_query_internal(DBRef, ["SHOW TABLES"]) of
4417 + Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
4418 + lists:foldl(fun([Table], Dates) ->
4419 + case re:run(Table, Reg) of
4421 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
4422 + {match, [{S, E}]} ->
4423 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
4435 +rebuild_all_stats_int(#state{vhost=VHost}=State) ->
4437 + {ok, DBRef} = open_mysql_connection(State),
4438 + ok = delete_nonexistent_stats(DBRef, VHost),
4439 + case lists:filter(fun(Date) ->
4440 + case catch rebuild_stats_at_int(DBRef, VHost, Date) of
4443 + {'EXIT', _} -> true
4445 + end, get_dates_int(DBRef, VHost)) of
4448 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
4451 + close_mysql_connection(DBRef)
4455 +rebuild_stats_at_int(DBRef, VHost, Date) ->
4456 + TempTable = temp_table(VHost),
4458 + Table = messages_table(VHost, Date),
4459 + STable = stats_table(VHost),
4461 + DQuery = [ "DELETE FROM ",STable," ",
4462 + "WHERE at='",Date,"';"],
4464 + ok = create_temp_table(DBRef, TempTable),
4465 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
4466 + SQuery = ["INSERT INTO ",TempTable," ",
4467 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
4468 + "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
4469 + "FROM ",Table," WHERE ext is NULL GROUP BY owner_id,peer_name_id,peer_server_id;"],
4470 + case sql_query_internal(DBRef, SQuery) of
4472 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
4474 + {data, [["0"]]} ->
4475 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
4476 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
4477 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"]),
4478 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
4479 + {updated, _} = sql_query_internal(DBRef, DQuery),
4482 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
4486 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
4487 + {updated, _} = sql_query_internal(DBRef, DQuery),
4488 + SQuery1 = ["INSERT INTO ",STable," ",
4489 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
4490 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
4491 + "FROM ",TempTable,";"],
4492 + case sql_query_internal(DBRef, SQuery1) of
4493 + {updated, _} -> ok;
4494 + {error, _} -> error
4496 + {error, _} -> error
4500 + case catch apply(Fun, []) of
4502 + ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
4506 + {'EXIT', Reason} ->
4507 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
4510 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
4511 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
4514 +delete_nonexistent_stats(DBRef, VHost) ->
4515 + Dates = get_dates_int(DBRef, VHost),
4516 + STable = stats_table(VHost),
4518 + Temp = lists:flatmap(fun(Date) ->
4519 + ["\"",Date,"\"",","]
4525 + % replace last "," with ");"
4526 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4527 + Query = ["DELETE FROM ",STable," ",
4528 + "WHERE at NOT IN (", Temp1],
4529 + case sql_query_internal(DBRef, Query) of
4537 +get_user_stats_int(DBRef, User, VHost) ->
4538 + SName = stats_table(VHost),
4539 + UName = users_table(VHost),
4540 + Query = ["SELECT stats.at, sum(stats.count) ",
4541 + "FROM ",UName," AS users ",
4542 + "JOIN ",SName," AS stats ON owner_id=user_id "
4543 + "WHERE users.username=\"",User,"\" ",
4544 + "GROUP BY stats.at "
4545 + "ORDER BY DATE(stats.at) DESC;"
4547 + case sql_query_internal(DBRef, Query) of
4549 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
4550 + {error, Result} ->
4554 +delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
4555 + DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
4556 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4557 + case sql_query_internal(DBRef, DQuery) of
4559 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
4565 +delete_all_stats_by_user_int(DBRef, User, VHost) ->
4566 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
4567 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4568 + case sql_query_internal(DBRef, SQuery) of
4570 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
4572 + {error, _} -> error
4575 +delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
4576 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
4577 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
4578 + "AND at=\"",Date,"\";"],
4579 + case sql_query_internal(DBRef, SQuery) of
4581 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
4583 + {error, _} -> error
4586 +delete_user_settings_int(DBRef, User, VHost) ->
4587 + Query = ["DELETE FROM ",settings_table(VHost)," ",
4588 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4589 + case sql_query_internal(DBRef, Query) of
4591 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
4593 + {error, Reason} ->
4594 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
4598 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4602 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4603 +create_temp_table(DBRef, Name) ->
4604 + Query = ["CREATE TABLE ",Name," (",
4605 + "owner_id MEDIUMINT UNSIGNED, ",
4606 + "peer_name_id MEDIUMINT UNSIGNED, ",
4607 + "peer_server_id MEDIUMINT UNSIGNED, ",
4608 + "at VARCHAR(11), ",
4610 + ") ENGINE=MyISAM CHARACTER SET utf8;"
4612 + case sql_query_internal(DBRef, Query) of
4613 + {updated, _} -> ok;
4614 + {error, _Reason} -> error
4617 +create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
4618 + SName = stats_table(VHost),
4619 + Query = ["CREATE TABLE ",SName," (",
4620 + "owner_id MEDIUMINT UNSIGNED, ",
4621 + "peer_name_id MEDIUMINT UNSIGNED, ",
4622 + "peer_server_id MEDIUMINT UNSIGNED, ",
4623 + "at VARCHAR(11), ",
4624 + "count INT(11), ",
4625 + "ext INTEGER DEFAULT NULL, "
4626 + "INDEX ext_i (ext), "
4627 + "INDEX(owner_id,peer_name_id,peer_server_id), ",
4629 + ") ENGINE=MyISAM CHARACTER SET utf8;"
4631 + case sql_query_internal_silent(DBRef, Query) of
4633 + ?MYDEBUG("Created stats table for ~p", [VHost]),
4634 + rebuild_all_stats_int(State),
4636 + {error, Reason} ->
4637 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
4639 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
4640 + CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
4641 + case sql_query_internal(DBRef, CheckQuery) of
4642 + {data, Elems} when length(Elems) == 2 ->
4643 + ?MYDEBUG("Stats table structure is ok", []),
4646 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
4647 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
4649 + ?INFO_MSG("Successfully dropped ~p", [SName]);
4651 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
4656 + ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
4661 +create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
4662 + SName = settings_table(VHost),
4663 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4664 + "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
4665 + "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
4666 + "dolog_list TEXT, ",
4667 + "donotlog_list TEXT ",
4668 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4670 + case sql_query_internal(DBRef, Query) of
4672 + ?MYDEBUG("Created settings table for ~p", [VHost]),
4678 +create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
4679 + SName = users_table(VHost),
4680 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4681 + "username TEXT NOT NULL, ",
4682 + "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4683 + "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
4684 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4686 + case sql_query_internal(DBRef, Query) of
4688 + ?MYDEBUG("Created users table for ~p", [VHost]),
4694 +create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
4695 + SName = servers_table(VHost),
4696 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4697 + "server TEXT NOT NULL, ",
4698 + "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4699 + "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
4700 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4702 + case sql_query_internal(DBRef, Query) of
4704 + ?MYDEBUG("Created servers table for ~p", [VHost]),
4710 +create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
4711 + RName = resources_table(VHost),
4712 + Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
4713 + "resource TEXT NOT NULL, ",
4714 + "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4715 + "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
4716 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4718 + case sql_query_internal(DBRef, Query) of
4720 + ?MYDEBUG("Created resources table for ~p", [VHost]),
4726 +create_internals(#state{dbref=DBRef, vhost=VHost}) ->
4727 + sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS ",logmessage_name(VHost),";"]),
4728 + case sql_query_internal(DBRef, [get_logmessage(VHost)]) of
4730 + ?MYDEBUG("Created logmessage for ~p", [VHost]),
4736 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4740 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4741 +sql_query_internal(DBRef, Query) ->
4742 + case sql_query_internal_silent(DBRef, Query) of
4743 + {error, Reason} ->
4744 + ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
4749 +sql_query_internal_silent(DBRef, Query) ->
4750 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
4751 + get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
4753 +get_result({updated, MySQLRes}) ->
4754 + {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
4755 +get_result({data, MySQLRes}) ->
4756 + {data, p1_mysql:get_result_rows(MySQLRes)};
4757 +get_result({error, "query timed out"}) ->
4758 + {error, "query timed out"};
4759 +get_result({error, MySQLRes}) ->
4760 + Reason = p1_mysql:get_result_reason(MySQLRes),
4763 +get_user_id(DBRef, VHost, User) ->
4764 + SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
4765 + "WHERE username=\"",User,"\";"],
4766 + case sql_query_internal(DBRef, SQuery) of
4768 + IQuery = ["INSERT INTO ",users_table(VHost)," ",
4769 + "SET username=\"",User,"\";"],
4770 + case sql_query_internal_silent(DBRef, IQuery) of
4772 + {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery),
4774 + {error, Reason} ->
4775 + % this can be in clustered environment
4776 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
4777 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
4778 + {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
4781 + {data, [[DBId]]} ->
4785 +get_logmessage(VHost) ->
4786 + UName = users_table(VHost),
4787 + SName = servers_table(VHost),
4788 + RName = resources_table(VHost),
4789 + StName = stats_table(VHost),
4791 +CREATE PROCEDURE ~s(tablename TEXT, atdate TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(10), msubject TEXT, mbody TEXT, mtimestamp DOUBLE)
4793 + DECLARE ownerID MEDIUMINT UNSIGNED;
4794 + DECLARE peer_nameID MEDIUMINT UNSIGNED;
4795 + DECLARE peer_serverID MEDIUMINT UNSIGNED;
4796 + DECLARE peer_resourceID MEDIUMINT UNSIGNED;
4797 + DECLARE Vmtype VARCHAR(10);
4798 + DECLARE Vmtimestamp DOUBLE;
4799 + DECLARE Vmdirection VARCHAR(4);
4800 + DECLARE Vmbody TEXT;
4801 + DECLARE Vmsubject TEXT;
4804 + DECLARE viewname TEXT;
4805 + DECLARE notable INT;
4806 + DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1;
4809 + SET @ownerID = NULL;
4810 + SET @peer_nameID = NULL;
4811 + SET @peer_serverID = NULL;
4812 + SET @peer_resourceID = NULL;
4814 + SET @Vmtype = mtype;
4815 + SET @Vmtimestamp = mtimestamp;
4816 + SET @Vmdirection = mdirection;
4817 + SET @Vmbody = mbody;
4818 + SET @Vmsubject = msubject;
4820 + SELECT user_id INTO @ownerID FROM ~s WHERE username=owner;
4821 + IF @ownerID IS NULL THEN
4822 + INSERT INTO ~s SET username=owner;
4823 + SET @ownerID = LAST_INSERT_ID();
4826 + SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name;
4827 + IF @peer_nameID IS NULL THEN
4828 + INSERT INTO ~s SET username=peer_name;
4829 + SET @peer_nameID = LAST_INSERT_ID();
4832 + SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server;
4833 + IF @peer_serverID IS NULL THEN
4834 + INSERT INTO ~s SET server=peer_server;
4835 + SET @peer_serverID = LAST_INSERT_ID();
4838 + SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource;
4839 + IF @peer_resourceID IS NULL THEN
4840 + INSERT INTO ~s SET resource=peer_resource;
4841 + SET @peer_resourceID = LAST_INSERT_ID();
4844 + SET @iq = CONCAT(\"INSERT INTO \",tablename,\" (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (@ownerID,@peer_nameID,@peer_serverID,@peer_resourceID,@Vmdirection,@Vmtype,@Vmsubject,@Vmbody,@Vmtimestamp);\");
4845 + PREPARE insertmsg FROM @iq;
4847 + IF @notable = 1 THEN
4848 + SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" (
4849 + owner_id MEDIUMINT UNSIGNED NOT NULL,
4850 + peer_name_id MEDIUMINT UNSIGNED NOT NULL,
4851 + peer_server_id MEDIUMINT UNSIGNED NOT NULL,
4852 + peer_resource_id MEDIUMINT(8) UNSIGNED NOT NULL,
4853 + direction ENUM('to', 'from') NOT NULL,
4854 + type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
4857 + timestamp DOUBLE NOT NULL,
4858 + ext INTEGER DEFAULT NULL,
4859 + INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id),
4860 + INDEX ext_i (ext),
4864 + CHARACTER SET utf8;\");
4865 + PREPARE createtable FROM @cq;
4866 + EXECUTE createtable;
4867 + DEALLOCATE PREPARE createtable;
4869 + SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\");
4870 + SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS
4871 + SELECT owner.username AS owner_name,
4872 + peer.username AS peer_name,
4873 + servers.server AS peer_server,
4874 + resources.resource AS peer_resource,
4875 + messages.direction,
4879 + messages.timestamp
4885 + \", tablename,\" messages
4887 + owner.user_id=messages.owner_id and
4888 + peer.user_id=messages.peer_name_id and
4889 + servers.server_id=messages.peer_server_id and
4890 + resources.resource_id=messages.peer_resource_id
4891 + ORDER BY messages.timestamp;\");
4892 + PREPARE createview FROM @cq;
4893 + EXECUTE createview;
4894 + DEALLOCATE PREPARE createview;
4897 + PREPARE insertmsg FROM @iq;
4898 + EXECUTE insertmsg;
4899 + ELSEIF @notable = 0 THEN
4900 + EXECUTE insertmsg;
4903 + DEALLOCATE PREPARE insertmsg;
4905 + IF @notable = 0 THEN
4906 + UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND peer_name_id=@peer_nameID AND peer_server_id=@peer_serverID AND at=atdate;
4907 + IF ROW_COUNT() = 0 THEN
4908 + INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (@ownerID, @peer_nameID, @peer_serverID, atdate, 1);
4911 +END;", [logmessage_name(VHost),UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
4912 diff --git a/src/mod_logdb_pgsql.erl b/src/mod_logdb_pgsql.erl
4913 new file mode 100644
4914 index 00000000..202c6ed4
4916 +++ b/src/mod_logdb_pgsql.erl
4918 +% {ok, DBRef} = pgsql:connect([{host, "127.0.0.1"}, {database, "logdb"}, {user, "logdb"}, {password, "logdb"}, {port, 5432}, {as_binary, true}]).
4920 +% pgsql:squery(DBRef, "CREATE TABLE test.\"logdb_stats_test\" (owner_id INTEGER, peer_name_id INTEGER, peer_server_id INTEGER, at VARCHAR(20), count integer);" ).
4921 +%%%----------------------------------------------------------------------
4922 +%%% File : mod_logdb_pgsql.erl
4923 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
4924 +%%% Purpose : Posgresql backend for mod_logdb
4925 +%%% Url : https://paleg.github.io/mod_logdb/
4926 +%%%----------------------------------------------------------------------
4928 +-module(mod_logdb_pgsql).
4929 +-author('o.palij@gmail.com').
4931 +-include("mod_logdb.hrl").
4932 +-include("logger.hrl").
4934 +-behaviour(gen_logdb).
4935 +-behaviour(gen_server).
4938 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
4940 +-export([start/2, stop/1]).
4942 +-export([log_message/2,
4944 + rebuild_stats_at/2,
4945 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
4946 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
4948 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
4951 +-export([view_table/3]).
4953 +% gen_server call timeout
4954 +-define(CALL_TIMEOUT, 30000).
4955 +-define(PGSQL_TIMEOUT, 60000).
4956 +-define(PROCNAME, mod_logdb_pgsql).
4958 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
4959 + list_to_string/1, string_to_list/1,
4960 + convert_timestamp_brief/1]).
4962 +-record(state, {dbref, vhost, server, port, db, user, password, schema}).
4964 +% replace "." with "_"
4965 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4967 + end, binary_to_list(VHost)).
4970 + Schema ++ ".\"" ++ "logdb_".
4973 + "_" ++ escape_vhost(VHost) ++ "\"".
4975 +messages_table(VHost, Schema, Date) ->
4976 + prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost).
4978 +view_table(VHost, Schema, Date) ->
4979 + Table = messages_table(VHost, Schema, Date),
4980 + TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3),
4981 + lists:append([Schema, ".\"v_", TablewoS, "\""]).
4983 +stats_table(VHost, Schema) ->
4984 + prefix(Schema) ++ "stats" ++ suffix(VHost).
4986 +temp_table(VHost, Schema) ->
4987 + prefix(Schema) ++ "temp" ++ suffix(VHost).
4989 +settings_table(VHost, Schema) ->
4990 + prefix(Schema) ++ "settings" ++ suffix(VHost).
4992 +users_table(VHost, Schema) ->
4993 + prefix(Schema) ++ "users" ++ suffix(VHost).
4994 +servers_table(VHost, Schema) ->
4995 + prefix(Schema) ++ "servers" ++ suffix(VHost).
4996 +resources_table(VHost, Schema) ->
4997 + prefix(Schema) ++ "resources" ++ suffix(VHost).
4999 +logmessage_name(VHost, Schema) ->
5000 + prefix(Schema) ++ "logmessage" ++ suffix(VHost).
5002 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5004 +% gen_mod callbacks
5006 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5007 +start(VHost, Opts) ->
5008 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5009 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
5012 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5013 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
5015 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5017 +% gen_server callbacks
5019 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5020 +init([VHost, Opts]) ->
5021 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
5022 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"ejabberd_logdb">>),
5023 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
5024 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 5432),
5025 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
5026 + Schema = binary_to_list(gen_mod:get_opt(schema, Opts, fun(A) -> A end, <<"public">>)),
5028 + ?MYDEBUG("Starting pgsql backend for ~s", [VHost]),
5030 + St = #state{vhost=VHost,
5031 + server=Server, port=Port, db=DB,
5032 + user=User, password=Password,
5035 + case open_pgsql_connection(St) of
5037 + State = St#state{dbref=DBRef},
5038 + ok = create_internals(State),
5039 + ok = create_stats_table(State),
5040 + ok = create_settings_table(State),
5041 + ok = create_users_table(State),
5042 + ok = create_servers_table(State),
5043 + ok = create_resources_table(State),
5044 + erlang:monitor(process, DBRef),
5046 + % this does not work
5047 + {error, Reason} ->
5048 + ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]),
5049 + {stop, db_connection_failed};
5050 + % and this too, becouse pgsql_conn do exit() which can not be catched
5052 + ?ERROR_MSG("Rez: ~p~n", [Rez]),
5053 + {stop, db_connection_failed}
5056 +open_pgsql_connection(#state{server=Server, port=Port, db=DB, schema=Schema,
5057 + user=User, password=Password} = _State) ->
5058 + ?INFO_MSG("Opening pgsql connection ~s@~s:~p/~s", [User, Server, Port, DB]),
5059 + {ok, DBRef} = pgsql:connect(Server, DB, User, Password, Port),
5060 + {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]),
5063 +close_pgsql_connection(DBRef) ->
5064 + ?MYDEBUG("Closing ~p pgsql connection", [DBRef]),
5065 + pgsql:terminate(DBRef).
5067 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5068 + Date = convert_timestamp_brief(Msg#msg.timestamp),
5069 + TableName = messages_table(VHost, Schema, Date),
5070 + ViewName = view_table(VHost, Schema, Date),
5072 + Query = [ "SELECT ", logmessage_name(VHost, Schema)," "
5073 + "('", TableName, "',",
5074 + "'", ViewName, "',",
5076 + "'", binary_to_list(Msg#msg.owner_name), "',",
5077 + "'", binary_to_list(Msg#msg.peer_name), "',",
5078 + "'", binary_to_list(Msg#msg.peer_server), "',",
5079 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
5080 + "'", atom_to_list(Msg#msg.direction), "',",
5081 + "'", binary_to_list(Msg#msg.type), "',",
5082 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
5083 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
5084 + "'", Msg#msg.timestamp, "');"],
5086 + case sql_query_internal_silent(DBRef, Query) of
5087 + % TODO: change this
5088 + {data, [{"0"}]} ->
5089 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
5090 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
5092 + {error, _Reason} ->
5095 + {reply, ok, State};
5096 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5097 + Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date),
5098 + {reply, Reply, State};
5099 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
5100 + {reply, error, State};
5101 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5102 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
5103 + ["'",Timestamp,"'",","]
5106 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
5108 + Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
5109 + "WHERE timestamp IN (", Temp1],
5112 + case sql_query_internal(DBRef, Query) of
5114 + rebuild_stats_at_int(DBRef, VHost, Schema, Date);
5118 + {reply, Reply, State};
5119 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5120 + ok = delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date),
5121 + ok = delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date),
5122 + {reply, ok, State};
5123 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5124 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5126 + case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date)," CASCADE;"]) of
5128 + Query = ["DELETE FROM ",stats_table(VHost, Schema)," "
5129 + "WHERE at='",Date,"';"],
5130 + case sql_query_internal(DBRef, Query) of
5139 + {reply, Reply, State};
5140 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5141 + SName = stats_table(VHost, Schema),
5142 + Query = ["SELECT at, sum(count) ",
5143 + "FROM ",SName," ",
5145 + "ORDER BY DATE(at) DESC;"
5148 + case sql_query_internal(DBRef, Query) of
5150 + {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]};
5151 + {error, Reason} ->
5152 + % TODO: Duplicate error message ?
5155 + {reply, Reply, State};
5156 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5157 + SName = stats_table(VHost, Schema),
5158 + Query = ["SELECT username, sum(count) AS allcount ",
5159 + "FROM ",SName," ",
5160 + "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id ",
5161 + "WHERE at='",Date,"' ",
5162 + "GROUP BY username ",
5163 + "ORDER BY allcount DESC;"
5166 + case sql_query_internal(DBRef, Query) of
5168 + RFun = fun({User, Count}) ->
5169 + {User, list_to_integer(Count)}
5171 + {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))};
5172 + {error, Reason} ->
5176 + {reply, Reply, State};
5177 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5178 + {reply, get_user_stats_int(DBRef, Schema, User, VHost), State};
5179 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5180 + Query = ["SELECT peer_name,",
5188 + "FROM ",view_table(VHost, Schema, Date)," "
5189 + "WHERE owner_name='",User,"';"],
5191 + case sql_query_internal(DBRef, Query) of
5193 + Fun = fun({Peer_name, Peer_server, Peer_resource,
5198 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
5199 + direction=list_to_atom(Direction),
5201 + subject=Subject, body=Body,
5202 + timestamp=Timestamp}
5204 + {ok, lists:map(Fun, Recs)};
5205 + {error, Reason} ->
5208 + {reply, Reply, State};
5209 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5210 + SName = stats_table(VHost, Schema),
5211 + Query = ["SELECT at ",
5212 + "FROM ",SName," ",
5214 + "ORDER BY at DESC;"
5217 + case sql_query_internal(DBRef, Query) of
5219 + [ Date || {Date} <- Result ];
5220 + {error, Reason} ->
5223 + {reply, Reply, State};
5224 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5225 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
5226 + "FROM ",settings_table(VHost, Schema)," ",
5227 + "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"],
5229 + case sql_query_internal(DBRef, Query) of
5231 + {ok, [#user_settings{owner_name=Owner,
5232 + dolog_default=list_to_bool(DoLogDef),
5233 + dolog_list=string_to_list(DoLogL),
5234 + donotlog_list=string_to_list(DoNotLogL)
5235 + } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]};
5236 + {error, Reason} ->
5239 + {reply, Reply, State};
5240 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5241 + Query = ["SELECT dolog_default,dolog_list,donotlog_list ",
5242 + "FROM ",settings_table(VHost, Schema)," ",
5243 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5245 + case sql_query_internal_silent(DBRef, Query) of
5248 + {data, [{DoLogDef, DoLogL, DoNotLogL}]} ->
5249 + {ok, #user_settings{owner_name=User,
5250 + dolog_default=list_to_bool(DoLogDef),
5251 + dolog_list=string_to_list(DoLogL),
5252 + donotlog_list=string_to_list(DoNotLogL)}};
5253 + {error, Reason} ->
5254 + ?ERROR_MSG("Failed to get_user_settings for ~s@~s: ~p", [User, VHost, Reason]),
5257 + {reply, Reply, State};
5258 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
5259 + dolog_list=DoLogL,
5260 + donotlog_list=DoNotLogL}},
5261 + _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5262 + User_id = get_user_id(DBRef, VHost, Schema, User),
5263 + Query = ["UPDATE ",settings_table(VHost, Schema)," ",
5264 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
5265 + "dolog_list='",list_to_string(DoLogL),"', ",
5266 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
5267 + "WHERE owner_id=",User_id,";"],
5270 + case sql_query_internal(DBRef, Query) of
5272 + IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ",
5273 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
5275 + "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
5276 + case sql_query_internal(DBRef, IQuery) of
5278 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
5284 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
5289 + {reply, Reply, State};
5290 +handle_call({stop}, _From, State) ->
5291 + ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]),
5292 + {stop, normal, ok, State};
5293 +handle_call(Msg, _From, State) ->
5294 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
5298 +handle_cast({rebuild_stats}, State) ->
5299 + rebuild_all_stats_int(State),
5301 +handle_cast({drop_user, User}, #state{vhost=VHost, schema=Schema}=State) ->
5303 + {ok, DBRef} = open_pgsql_connection(State),
5304 + {ok, Dates} = get_user_stats_int(DBRef, Schema, User, VHost),
5305 + MDResult = lists:map(fun({Date, _}) ->
5306 + delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date)
5308 + StDResult = delete_all_stats_by_user_int(DBRef, Schema, User, VHost),
5309 + SDResult = delete_user_settings_int(DBRef, Schema, User, VHost),
5310 + case lists:all(fun(Result) when Result == ok ->
5312 + (Result) when Result == error ->
5314 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
5316 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
5318 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
5320 + close_pgsql_connection(DBRef)
5324 +handle_cast(Msg, State) ->
5325 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
5328 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
5329 + {stop, connection_dropped, State};
5330 +handle_info(Info, State) ->
5331 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
5334 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
5335 + close_pgsql_connection(DBRef),
5338 +code_change(_OldVsn, State, _Extra) ->
5341 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5343 +% gen_logdb callbacks
5345 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5346 +log_message(VHost, Msg) ->
5347 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5348 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
5349 +rebuild_stats(VHost) ->
5350 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5351 + gen_server:cast(Proc, {rebuild_stats}).
5352 +rebuild_stats_at(VHost, Date) ->
5353 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5354 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
5355 +delete_messages_by_user_at(VHost, Msgs, Date) ->
5356 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5357 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
5358 +delete_all_messages_by_user_at(User, VHost, Date) ->
5359 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5360 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
5361 +delete_messages_at(VHost, Date) ->
5362 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5363 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
5364 +get_vhost_stats(VHost) ->
5365 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5366 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
5367 +get_vhost_stats_at(VHost, Date) ->
5368 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5369 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
5370 +get_user_stats(User, VHost) ->
5371 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5372 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
5373 +get_user_messages_at(User, VHost, Date) ->
5374 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5375 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
5376 +get_dates(VHost) ->
5377 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5378 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
5379 +get_users_settings(VHost) ->
5380 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5381 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
5382 +get_user_settings(User, VHost) ->
5383 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5384 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
5385 +set_user_settings(User, VHost, Set) ->
5386 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5387 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
5388 +drop_user(User, VHost) ->
5389 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5390 + gen_server:cast(Proc, {drop_user, User}).
5392 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5396 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5397 +get_dates_int(DBRef, VHost) ->
5398 + Query = ["SELECT n.nspname as \"Schema\",
5399 + c.relname as \"Name\",
5400 + CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as \"Type\",
5401 + r.rolname as \"Owner\"
5402 + FROM pg_catalog.pg_class c
5403 + JOIN pg_catalog.pg_roles r ON r.oid = c.relowner
5404 + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
5405 + WHERE c.relkind IN ('r','')
5406 + AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
5407 + AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$'
5408 + AND pg_catalog.pg_table_is_visible(c.oid)
5410 + case sql_query_internal(DBRef, Query) of
5412 + lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) ->
5413 + case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
5414 + {match, [{S, E}]} ->
5415 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
5424 +rebuild_all_stats_int(#state{vhost=VHost, schema=Schema}=State) ->
5426 + {ok, DBRef} = open_pgsql_connection(State),
5427 + ok = delete_nonexistent_stats(DBRef, Schema, VHost),
5428 + case lists:filter(fun(Date) ->
5429 + case catch rebuild_stats_at_int(DBRef, VHost, Schema, Date) of
5432 + {'EXIT', _} -> true
5434 + end, get_dates_int(DBRef, VHost)) of
5437 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
5440 + close_pgsql_connection(DBRef)
5444 +rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
5445 + TempTable = temp_table(VHost, Schema),
5448 + Table = messages_table(VHost, Schema, Date),
5449 + STable = stats_table(VHost, Schema),
5451 + DQuery = [ "DELETE FROM ",STable," ",
5452 + "WHERE at='",Date,"';"],
5454 + ok = create_temp_table(DBRef, VHost, Schema),
5455 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]),
5456 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
5457 + SQuery = ["INSERT INTO ",TempTable," ",
5458 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
5459 + "SELECT owner_id,peer_name_id,peer_server_id,'",Date,"'",",count(*) ",
5460 + "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
5461 + case sql_query_internal(DBRef, SQuery) of
5463 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
5465 + {data, [{"0"}]} ->
5466 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5467 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table," CASCADE;"]),
5468 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
5469 + {updated, _} = sql_query_internal(DBRef, DQuery),
5472 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
5476 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
5477 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
5478 + {updated, _} = sql_query_internal(DBRef, DQuery),
5479 + SQuery1 = ["INSERT INTO ",STable," ",
5480 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
5481 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
5482 + "FROM ",TempTable,";"],
5483 + case sql_query_internal(DBRef, SQuery1) of
5484 + {updated, _} -> ok;
5485 + {error, _} -> error
5487 + {error, _} -> error
5491 + case sql_transaction_internal(DBRef, Fun) of
5493 + ?INFO_MSG("Rebuilded stats for ~s at ~s", [VHost, Date]),
5495 + {aborted, Reason} ->
5496 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
5499 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
5502 +delete_nonexistent_stats(DBRef, Schema, VHost) ->
5503 + Dates = get_dates_int(DBRef, VHost),
5504 + STable = stats_table(VHost, Schema),
5506 + Temp = lists:flatmap(fun(Date) ->
5507 + ["'",Date,"'",","]
5514 + % replace last "," with ");"
5515 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
5516 + Query = ["DELETE FROM ",STable," ",
5517 + "WHERE at NOT IN (", Temp1],
5518 + case sql_query_internal(DBRef, Query) of
5526 +get_user_stats_int(DBRef, Schema, User, VHost) ->
5527 + SName = stats_table(VHost, Schema),
5528 + UName = users_table(VHost, Schema),
5529 + Query = ["SELECT stats.at, sum(stats.count) ",
5530 + "FROM ",UName," AS users ",
5531 + "JOIN ",SName," AS stats ON owner_id=user_id "
5532 + "WHERE users.username='",User,"' ",
5533 + "GROUP BY stats.at "
5534 + "ORDER BY DATE(at) DESC;"
5536 + case sql_query_internal(DBRef, Query) of
5538 + {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]};
5539 + {error, Result} ->
5543 +delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
5544 + DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
5545 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5546 + case sql_query_internal(DBRef, DQuery) of
5548 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
5554 +delete_all_stats_by_user_int(DBRef, Schema, User, VHost) ->
5555 + SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
5556 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5557 + case sql_query_internal(DBRef, SQuery) of
5559 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
5561 + {error, _} -> error
5564 +delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
5565 + SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
5566 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ",
5567 + "AND at='",Date,"';"],
5568 + case sql_query_internal(DBRef, SQuery) of
5570 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
5572 + {error, _} -> error
5575 +delete_user_settings_int(DBRef, Schema, User, VHost) ->
5576 + Query = ["DELETE FROM ",settings_table(VHost, Schema)," ",
5577 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5578 + case sql_query_internal(DBRef, Query) of
5580 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
5582 + {error, Reason} ->
5583 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
5587 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5591 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5592 +create_temp_table(DBRef, VHost, Schema) ->
5593 + TName = temp_table(VHost, Schema),
5594 + Query = ["CREATE TABLE ",TName," (",
5595 + "owner_id INTEGER, ",
5596 + "peer_name_id INTEGER, ",
5597 + "peer_server_id INTEGER, ",
5598 + "at VARCHAR(20), ",
5602 + case sql_query_internal(DBRef, Query) of
5603 + {updated, _} -> ok;
5604 + {error, _Reason} -> error
5607 +create_stats_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5608 + SName = stats_table(VHost, Schema),
5612 + Query = ["CREATE TABLE ",SName," (",
5613 + "owner_id INTEGER, ",
5614 + "peer_name_id INTEGER, ",
5615 + "peer_server_id INTEGER, ",
5616 + "at VARCHAR(20), ",
5620 + case sql_query_internal_silent(DBRef, Query) of
5622 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_search_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id, peer_name_id, peer_server_id);"]),
5623 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]),
5625 + {error, Reason} ->
5626 + case lists:keysearch(code, 1, Reason) of
5627 + {value, {code, "42P07"}} ->
5630 + ?ERROR_MSG("Failed to create stats table for ~s: ~p", [VHost, Reason]),
5635 + case sql_transaction_internal(DBRef, Fun) of
5636 + {atomic, created} ->
5637 + ?MYDEBUG("Created stats table for ~s", [VHost]),
5638 + rebuild_all_stats_int(State),
5640 + {atomic, exists} ->
5641 + ?MYDEBUG("Stats table for ~s already exists", [VHost]),
5642 + {match, [{F, L}]} = re:run(SName, "\".*\""),
5643 + QTable = lists:sublist(SName, F+2, L-2),
5644 + OIDQuery = ["SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname='",QTable,"' AND pg_catalog.pg_table_is_visible(c.oid);"],
5645 + {data,[{OID}]} = sql_query_internal(DBRef, OIDQuery),
5646 + CheckQuery = ["SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = '",OID,"' AND a.attnum > 0 AND NOT a.attisdropped AND a.attname ~ '^peer_.*_id$';"],
5647 + case sql_query_internal(DBRef, CheckQuery) of
5648 + {data, Elems} when length(Elems) == 2 ->
5649 + ?MYDEBUG("Stats table structure is ok", []),
5652 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
5653 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
5655 + ?INFO_MSG("Successfully dropped ~p", [SName]);
5657 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
5661 + {error, _} -> error
5664 +create_settings_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5665 + SName = settings_table(VHost, Schema),
5666 + Query = ["CREATE TABLE ",SName," (",
5667 + "owner_id INTEGER PRIMARY KEY, ",
5668 + "dolog_default BOOLEAN, ",
5669 + "dolog_list TEXT DEFAULT '', ",
5670 + "donotlog_list TEXT DEFAULT ''",
5673 + case sql_query_internal_silent(DBRef, Query) of
5675 + ?MYDEBUG("Created settings table for ~s", [VHost]),
5677 + {error, Reason} ->
5678 + case lists:keysearch(code, 1, Reason) of
5679 + {value, {code, "42P07"}} ->
5680 + ?MYDEBUG("Settings table for ~s already exists", [VHost]),
5683 + ?ERROR_MSG("Failed to create settings table for ~s: ~p", [VHost, Reason]),
5688 +create_users_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5689 + SName = users_table(VHost, Schema),
5693 + Query = ["CREATE TABLE ",SName," (",
5694 + "username TEXT UNIQUE, ",
5695 + "user_id SERIAL PRIMARY KEY",
5698 + case sql_query_internal_silent(DBRef, Query) of
5700 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]),
5702 + {error, Reason} ->
5703 + case lists:keysearch(code, 1, Reason) of
5704 + {value, {code, "42P07"}} ->
5707 + ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
5712 + case sql_transaction_internal(DBRef, Fun) of
5713 + {atomic, created} ->
5714 + ?MYDEBUG("Created users table for ~s", [VHost]),
5716 + {atomic, exists} ->
5717 + ?MYDEBUG("Users table for ~s already exists", [VHost]),
5719 + {aborted, _} -> error
5722 +create_servers_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5723 + SName = servers_table(VHost, Schema),
5726 + Query = ["CREATE TABLE ",SName," (",
5727 + "server TEXT UNIQUE, ",
5728 + "server_id SERIAL PRIMARY KEY",
5731 + case sql_query_internal_silent(DBRef, Query) of
5733 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]),
5735 + {error, Reason} ->
5736 + case lists:keysearch(code, 1, Reason) of
5737 + {value, {code, "42P07"}} ->
5740 + ?ERROR_MSG("Failed to create servers table for ~s: ~p", [VHost, Reason]),
5745 + case sql_transaction_internal(DBRef, Fun) of
5746 + {atomic, created} ->
5747 + ?MYDEBUG("Created servers table for ~s", [VHost]),
5749 + {atomic, exists} ->
5750 + ?MYDEBUG("Servers table for ~s already exists", [VHost]),
5752 + {aborted, _} -> error
5755 +create_resources_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5756 + RName = resources_table(VHost, Schema),
5758 + Query = ["CREATE TABLE ",RName," (",
5759 + "resource TEXT UNIQUE, ",
5760 + "resource_id SERIAL PRIMARY KEY",
5763 + case sql_query_internal_silent(DBRef, Query) of
5765 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]),
5767 + {error, Reason} ->
5768 + case lists:keysearch(code, 1, Reason) of
5769 + {value, {code, "42P07"}} ->
5772 + ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
5777 + case sql_transaction_internal(DBRef, Fun) of
5778 + {atomic, created} ->
5779 + ?MYDEBUG("Created resources table for ~s", [VHost]),
5781 + {atomic, exists} ->
5782 + ?MYDEBUG("Resources table for ~s already exists", [VHost]),
5784 + {aborted, _} -> error
5787 +create_internals(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5788 + sql_query_internal(DBRef, ["DROP FUNCTION IF EXISTS ",logmessage_name(VHost,Schema)," (tbname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION);"]),
5789 + case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
5791 + ?MYDEBUG("Created logmessage for ~p", [VHost]),
5793 + {error, Reason} ->
5794 + case lists:keysearch(code, 1, Reason) of
5795 + {value, {code, "42704"}} ->
5796 + ?ERROR_MSG("plpgsql language must be installed into database '~s'. Use CREATE LANGUAGE...", [State#state.db]),
5803 +get_user_id(DBRef, VHost, Schema, User) ->
5804 + SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ",
5805 + "WHERE username='",User,"';"],
5806 + case sql_query_internal(DBRef, SQuery) of
5808 + IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ",
5809 + "VALUES ('",User,"');"],
5810 + case sql_query_internal_silent(DBRef, IQuery) of
5812 + {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery),
5814 + {error, Reason} ->
5815 + % this can be in clustered environment
5816 + {value, {code, "23505"}} = lists:keysearch(code, 1, Reason),
5817 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
5818 + {data, [{ClID}]} = sql_query_internal(DBRef, SQuery),
5821 + {data, [{DBId}]} ->
5825 +get_logmessage(VHost,Schema) ->
5826 + UName = users_table(VHost,Schema),
5827 + SName = servers_table(VHost,Schema),
5828 + RName = resources_table(VHost,Schema),
5829 + StName = stats_table(VHost,Schema),
5830 + io_lib:format("CREATE OR REPLACE FUNCTION ~s (tbname TEXT, vname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION) RETURNS INTEGER AS $$
5833 + peer_nameID INTEGER;
5834 + peer_serverID INTEGER;
5835 + peer_resourceID INTEGER;
5836 + tablename ALIAS for $1;
5837 + viewname ALIAS for $2;
5838 + atdate ALIAS for $3;
5840 + SELECT INTO ownerID user_id FROM ~s WHERE username = owner;
5842 + INSERT INTO ~s (username) VALUES (owner);
5843 + ownerID := lastval();
5846 + SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name;
5848 + INSERT INTO ~s (username) VALUES (peer_name);
5849 + peer_nameID := lastval();
5852 + SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server;
5854 + INSERT INTO ~s (server) VALUES (peer_server);
5855 + peer_serverID := lastval();
5858 + SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource;
5860 + INSERT INTO ~s (resource) VALUES (peer_resource);
5861 + peer_resourceID := lastval();
5865 + EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
5866 + EXCEPTION WHEN undefined_table THEN
5867 + EXECUTE 'CREATE TABLE ' || tablename || ' (' ||
5868 + 'owner_id INTEGER, ' ||
5869 + 'peer_name_id INTEGER, ' ||
5870 + 'peer_server_id INTEGER, ' ||
5871 + 'peer_resource_id INTEGER, ' ||
5872 + 'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' ||
5873 + 'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' ||
5874 + 'subject TEXT, ' ||
5876 + 'timestamp DOUBLE PRECISION)';
5877 + EXECUTE 'CREATE INDEX \"search_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id)';
5879 + EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' ||
5880 + 'SELECT owner.username AS owner_name, ' ||
5881 + 'peer.username AS peer_name, ' ||
5882 + 'servers.server AS peer_server, ' ||
5883 + 'resources.resource AS peer_resource, ' ||
5884 + 'messages.direction, ' ||
5885 + 'messages.type, ' ||
5886 + 'messages.subject, ' ||
5887 + 'messages.body, ' ||
5888 + 'messages.timestamp ' ||
5893 + '~s resources, ' ||
5894 + tablename || ' messages ' ||
5896 + 'owner.user_id=messages.owner_id and ' ||
5897 + 'peer.user_id=messages.peer_name_id and ' ||
5898 + 'servers.server_id=messages.peer_server_id and ' ||
5899 + 'resources.resource_id=messages.peer_resource_id ' ||
5900 + 'ORDER BY messages.timestamp';
5902 + EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
5905 + UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID and peer_name_id=peer_nameID and peer_server_id=peer_serverID;
5907 + INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (ownerID, peer_nameID, peer_serverID, atdate, 1);
5911 +$$ LANGUAGE plpgsql;
5912 +", [logmessage_name(VHost,Schema),UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),UName,UName,SName,RName,StName,StName]).
5914 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5918 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5919 +% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
5920 +sql_transaction_internal(DBRef, Fun) ->
5921 + case sql_query_internal(DBRef, ["BEGIN;"]) of
5923 + case catch Fun() of
5925 + rollback_internal(DBRef, Err);
5926 + {error, _} = Err ->
5927 + rollback_internal(DBRef, Err);
5928 + {'EXIT', _} = Err ->
5929 + rollback_internal(DBRef, Err);
5931 + case sql_query_internal(DBRef, ["COMMIT;"]) of
5932 + {error, _} -> rollback_internal(DBRef, {commit_error});
5935 + {atomic, _} -> Res;
5936 + _ -> {atomic, Res}
5941 + {aborted, {begin_error}}
5944 +% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
5945 +rollback_internal(DBRef, Reason) ->
5946 + Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
5947 + {aborted, {Reason, {rollback_result, Res}}}.
5949 +sql_query_internal(DBRef, Query) ->
5950 + case sql_query_internal_silent(DBRef, Query) of
5951 + {error, undefined, Rez} ->
5952 + ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]),
5953 + {error, undefined};
5955 + ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]),
5960 +sql_query_internal_silent(DBRef, Query) ->
5961 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
5962 + % TODO: use pquery?
5963 + get_result(pgsql:squery(DBRef, Query)).
5965 +get_result({ok, ["CREATE TABLE"]}) ->
5967 +get_result({ok, ["DROP TABLE"]}) ->
5969 +get_result({ok, ["ALTER TABLE"]}) ->
5971 +get_result({ok,["DROP VIEW"]}) ->
5973 +get_result({ok,["DROP FUNCTION"]}) ->
5975 +get_result({ok, ["CREATE INDEX"]}) ->
5977 +get_result({ok, ["CREATE FUNCTION"]}) ->
5979 +get_result({ok, [{[$S, $E, $L, $E, $C, $T, $ | _Rest], _Rows, Recs}]}) ->
5982 + lists:map(fun(Elem) when is_binary(Elem) ->
5983 + binary_to_list(Elem);
5984 + (Elem) when is_list(Elem) ->
5986 + (Elem) when is_integer(Elem) ->
5987 + integer_to_list(Elem);
5988 + (Elem) when is_float(Elem) ->
5989 + float_to_list(Elem);
5990 + (Elem) when is_boolean(Elem) ->
5991 + atom_to_list(Elem);
5993 + ?ERROR_MSG("Unknown element type ~p", [Elem]),
5997 + Res = lists:map(Fun, Recs),
5998 + %{data, [list_to_tuple(Rec) || Rec <- Recs]};
6000 +get_result({ok, ["INSERT " ++ OIDN]}) ->
6001 + [_OID, N] = string:tokens(OIDN, " "),
6002 + {updated, list_to_integer(N)};
6003 +get_result({ok, ["DELETE " ++ N]}) ->
6004 + {updated, list_to_integer(N)};
6005 +get_result({ok, ["UPDATE " ++ N]}) ->
6006 + {updated, list_to_integer(N)};
6007 +get_result({ok, ["BEGIN"]}) ->
6009 +get_result({ok, ["LOCK TABLE"]}) ->
6011 +get_result({ok, ["ROLLBACK"]}) ->
6013 +get_result({ok, ["COMMIT"]}) ->
6015 +get_result({ok, ["SET"]}) ->
6017 +get_result({ok, [{error, Error}]}) ->
6020 + {error, undefined, Rez}.
6022 diff --git a/src/mod_roster.erl b/src/mod_roster.erl
6023 index 38c3a78b..f02f2cd3 100644
6024 --- a/src/mod_roster.erl
6025 +++ b/src/mod_roster.erl
6027 -define(ROSTER_ITEM_CACHE, roster_item_cache).
6028 -define(ROSTER_VERSION_CACHE, roster_version_cache).
6030 +-include("mod_logdb.hrl").
6032 -export_type([subscription/0]).
6034 -callback init(binary(), gen_mod:opts()) -> any().
6035 @@ -911,6 +913,14 @@ user_roster(User, Server, Query, Lang) ->
6037 Items = get_roster(LUser, LServer),
6038 SItems = lists:sort(Items),
6040 + Settings = case gen_mod:is_loaded(Server, mod_logdb) of
6042 + mod_logdb:get_user_settings(User, Server);
6047 FItems = case SItems of
6048 [] -> [?CT(<<"None">>)];
6050 @@ -968,7 +978,33 @@ user_roster(User, Server, Query, Lang) ->
6051 [?INPUTT(<<"submit">>,
6053 (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6056 + case gen_mod:is_loaded(Server, mod_logdb) of
6058 + Peer = jid:encode(R#roster.jid),
6059 + A = lists:member(Peer, Settings#user_settings.dolog_list),
6060 + B = lists:member(Peer, Settings#user_settings.donotlog_list),
6064 + {<<"donotlog">>, <<"Do Not Log Messages">>};
6066 + {<<"dolog">>, <<"Log Messages">>};
6067 + Settings#user_settings.dolog_default == true ->
6068 + {<<"donotlog">>, <<"Do Not Log Messages">>};
6069 + Settings#user_settings.dolog_default == false ->
6070 + {<<"dolog">>, <<"Log Messages">>}
6073 + ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
6074 + [?INPUTT(<<"submit">>,
6076 + (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6085 @@ -1075,9 +1111,42 @@ user_roster_item_parse_query(User, Server, Items,
6086 sub_els = [#roster_query{
6087 items = [RosterItem]}]}),
6093 + case lists:keysearch(
6094 + <<"donotlog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
6096 + Peer = jid:encode(JID),
6097 + Settings = mod_logdb:get_user_settings(User, Server),
6098 + DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of
6099 + false -> lists:append(Settings#user_settings.donotlog_list, [Peer]);
6100 + true -> Settings#user_settings.donotlog_list
6102 + DLL = lists:delete(jid:encode(JID), Settings#user_settings.dolog_list),
6103 + Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
6104 + % TODO: check returned value
6105 + ok = mod_logdb:set_user_settings(User, Server, Sett),
6108 + case lists:keysearch(
6109 + <<"dolog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
6111 + Peer = jid:encode(JID),
6112 + Settings = mod_logdb:get_user_settings(User, Server),
6113 + DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of
6114 + false -> lists:append(Settings#user_settings.dolog_list, [Peer]);
6115 + true -> Settings#user_settings.dolog_list
6117 + DNLL = lists:delete(jid:encode(JID), Settings#user_settings.donotlog_list),
6118 + Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
6119 + % TODO: check returned value
6120 + ok = mod_logdb:set_user_settings(User, Server, Sett),