1 diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg
2 index 8bb1c0eb..22c83f20 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 03fbd3d0..89d09f34 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 7acab78f..18af522a 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 568ac092..3a324ed1 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 aef3a017..b35db36f 100644
138 {eimp, ".*", {git, "https://github.com/processone/eimp", {tag, "1.0.5"}}},
139 {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.21"}}}},
140 {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.23"}}}},
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..d5983820
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("ejabberd.hrl").
382 +-include("xmpp.hrl").
383 +-include("mod_roster.hrl").
384 +-include("ejabberd_commands.hrl").
385 +-include("adhoc.hrl").
386 +-include("ejabberd_web_admin.hrl").
387 +-include("ejabberd_http.hrl").
388 +-include("logger.hrl").
390 +-define(PROCNAME, ejabberd_mod_logdb).
391 +% gen_server call timeout
392 +-define(CALL_TIMEOUT, 10000).
394 +-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings, drop_messages_on_user_removal}).
396 +ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ binary_to_list(VHost)).
398 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
400 +% gen_mod/gen_server callbacks
402 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
403 +% ejabberd starts module
404 +start(VHost, Opts) ->
406 + {gen_mod:get_module_proc(VHost, ?PROCNAME),
407 + {?MODULE, start_link, [VHost, Opts]},
412 + % add child to ejabberd_sup
413 + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec).
415 +depends(_Host, _Opts) ->
418 +reload(_Host, _NewOpts, _OldOpts) ->
422 +% supervisor starts gen_server
423 +start_link(VHost, Opts) ->
424 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
425 + {ok, Pid} = gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []),
429 +init([VHost, Opts]) ->
430 + process_flag(trap_exit, true),
431 + DBsRaw = gen_mod:get_opt(dbs, Opts, fun(A) -> A end, [{mnesia, []}]),
432 + DBs = case lists:keysearch(mnesia, 1, DBsRaw) of
433 + false -> lists:append(DBsRaw, [{mnesia,[]}]);
434 + {value, _} -> DBsRaw
436 + VHostDB = gen_mod:get_opt(vhosts, Opts, fun(A) -> A end, [{VHost, mnesia}]),
437 + % 10 is default because of using in clustered environment
438 + PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, fun(A) -> A end, 10),
441 + case lists:keysearch(VHost, 1, VHostDB) of
443 + ?WARNING_MSG("There is no logging backend defined for '~s', switching to mnesia", [VHost]),
445 + {value,{_, DBNameResult}} ->
446 + case lists:keysearch(DBNameResult, 1, DBs) of
448 + ?WARNING_MSG("There is no such logging backend '~s' defined for '~s', switching to mnesia", [DBNameResult, VHost]),
450 + {value, {_, DBOptsResult}} ->
451 + {DBNameResult, DBOptsResult}
455 + ?MYDEBUG("Starting mod_logdb for '~s' with '~s' backend", [VHost, DBName]),
457 + DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)),
459 + {ok, #state{vhost=VHost,
462 + % dbs used for convert messages from one backend to other
464 + dolog_default=gen_mod:get_opt(dolog_default, Opts, fun(A) -> A end, true),
465 + drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts, fun(A) -> A end, true),
466 + ignore_jids=gen_mod:get_opt(ignore_jids, Opts, fun(A) -> A end, []),
467 + groupchat=gen_mod:get_opt(groupchat, Opts, fun(A) -> A end, none),
468 + purge_older_days=gen_mod:get_opt(purge_older_days, Opts, fun(A) -> A end, never),
469 + poll_users_settings=PollUsersSettings}}.
471 +cleanup(#state{vhost=VHost} = _State) ->
472 + ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
474 + %ets:delete(ets_settings_table(VHost)),
476 + ejabberd_hooks:delete(remove_user, VHost, ?MODULE, remove_user, 90),
477 + ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90),
478 + ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
479 + ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_message, 40),
481 + ejabberd_hooks:delete(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
482 + ejabberd_hooks:delete(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
483 + ejabberd_hooks:delete(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
484 + ejabberd_hooks:delete(disco_local_features, VHost, ?MODULE, get_local_features, 50),
485 + ejabberd_hooks:delete(disco_local_items, VHost, ?MODULE, get_local_items, 50),
487 + ejabberd_hooks:delete(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
488 + ejabberd_hooks:delete(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
489 + ejabberd_hooks:delete(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
490 + ejabberd_hooks:delete(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
492 + ?MYDEBUG("Removed hooks for ~p", [VHost]),
494 + ejabberd_commands:unregister_commands(get_commands_spec()),
495 + ?MYDEBUG("Unregistered commands for ~p", [VHost]).
498 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
499 + %gen_server:call(Proc, {cleanup}),
500 + %?MYDEBUG("Cleanup in stop finished!!!!", []),
501 + %timer:sleep(10000),
502 + ok = supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
503 + ok = supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
505 +get_commands_spec() ->
506 + [#ejabberd_commands{name = rebuild_stats, tags = [logdb],
507 + desc = "Rebuild mod_logdb stats for given host",
508 + module = ?MODULE, function = rebuild_stats,
509 + args = [{host, binary}],
510 + result = {res, rescode}},
511 + #ejabberd_commands{name = copy_messages, tags = [logdb],
512 + desc = "Copy logdb messages from given backend to current backend for given host",
513 + module = ?MODULE, function = copy_messages_ctl,
514 + args = [{host, binary}, {backend, binary}, {date, binary}],
515 + result = {res, rescode}}].
517 +mod_opt_type(dbs) ->
518 + fun (A) when is_list(A) -> A end;
519 +mod_opt_type(vhosts) ->
520 + fun (A) when is_list(A) -> A end;
521 +mod_opt_type(poll_users_settings) ->
522 + fun (I) when is_integer(I) -> I end;
523 +mod_opt_type(groupchat) ->
528 +mod_opt_type(dolog_default) ->
529 + fun (B) when is_boolean(B) -> B end;
530 +mod_opt_type(ignore_jids) ->
531 + fun (A) when is_list(A) -> A end;
532 +mod_opt_type(purge_older_days) ->
533 + fun (I) when is_integer(I) -> I end;
535 + [dbs, vhosts, poll_users_settings, groupchat, dolog_default, ignore_jids, purge_older_days].
537 +handle_call({cleanup}, _From, State) ->
539 + ?MYDEBUG("Cleanup finished!!!!!", []),
540 + {reply, ok, State};
541 +handle_call({get_dates}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
542 + Reply = DBMod:get_dates(VHost),
543 + {reply, Reply, State};
544 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
545 +% ejabberd_web_admin callbacks
546 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
547 +handle_call({delete_messages_by_user_at, PMsgs, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
548 + Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, binary_to_list(Date)),
549 + {reply, Reply, State};
550 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
551 + Reply = DBMod:delete_all_messages_by_user_at(binary_to_list(User), VHost, binary_to_list(Date)),
552 + {reply, Reply, State};
553 +handle_call({delete_messages_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
554 + Reply = DBMod:delete_messages_at(VHost, Date),
555 + {reply, Reply, State};
556 +handle_call({get_vhost_stats}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
557 + Reply = DBMod:get_vhost_stats(VHost),
558 + {reply, Reply, State};
559 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
560 + Reply = DBMod:get_vhost_stats_at(VHost, binary_to_list(Date)),
561 + {reply, Reply, State};
562 +handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
563 + Reply = DBMod:get_user_stats(binary_to_list(User), VHost),
564 + {reply, Reply, State};
565 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
566 + Reply = DBMod:get_user_messages_at(binary_to_list(User), VHost, binary_to_list(Date)),
567 + {reply, Reply, State};
568 +handle_call({get_user_settings, User}, _From, #state{dbmod=_DBMod, vhost=VHost}=State) ->
569 + Reply = case ets:match_object(ets_settings_table(VHost),
570 + #user_settings{owner_name=User, _='_'}) of
572 + _ -> #user_settings{owner_name=User,
573 + dolog_default=State#state.dolog_default,
577 + {reply, Reply, State};
578 +% TODO: remove User ??
579 +handle_call({set_user_settings, User, GSet}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
580 + Set = GSet#user_settings{owner_name=User},
582 + case ets:match_object(ets_settings_table(VHost),
583 + #user_settings{owner_name=User, _='_'}) of
587 + case DBMod:set_user_settings(binary_to_list(User), VHost, Set) of
591 + true = ets:insert(ets_settings_table(VHost), Set),
595 + {reply, Reply, State};
596 +handle_call({get_module_settings}, _From, State) ->
597 + {reply, State, State};
598 +handle_call({set_module_settings, #state{purge_older_days=PurgeDays,
599 + poll_users_settings=PollSec} = Settings},
601 + #state{purgeRef=PurgeRefOld,
602 + pollRef=PollRefOld,
603 + purge_older_days=PurgeDaysOld,
604 + poll_users_settings=PollSecOld} = State) ->
606 + PurgeDays == never, PurgeDaysOld /= never ->
607 + {ok, cancel} = timer:cancel(PurgeRefOld),
609 + is_integer(PurgeDays), PurgeDaysOld == never ->
610 + set_purge_timer(PurgeDays);
616 + PollSec == PollSecOld ->
618 + PollSec == 0, PollSecOld /= 0 ->
619 + {ok, cancel} = timer:cancel(PollRefOld),
621 + is_integer(PollSec), PollSecOld == 0 ->
622 + set_poll_timer(PollSec);
623 + is_integer(PollSec), PollSecOld /= 0 ->
624 + {ok, cancel} = timer:cancel(PollRefOld),
625 + set_poll_timer(PollSec)
628 + NewState = State#state{dolog_default=Settings#state.dolog_default,
629 + ignore_jids=Settings#state.ignore_jids,
630 + groupchat=Settings#state.groupchat,
631 + drop_messages_on_user_removal=Settings#state.drop_messages_on_user_removal,
632 + purge_older_days=PurgeDays,
633 + poll_users_settings=PollSec,
636 + {reply, ok, NewState};
637 +handle_call(Msg, _From, State) ->
638 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
640 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
641 +% end ejabberd_web_admin callbacks
642 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
644 +% ejabberd_hooks call
645 +handle_cast({addlog, Direction, Owner, Peer, Packet}, #state{dbmod=DBMod, vhost=VHost}=State) ->
646 + case filter(Owner, Peer, State) of
648 + case catch packet_parse(Owner, Peer, Packet, Direction, State) of
651 + {'EXIT', Reason} ->
652 + ?ERROR_MSG("Failed to parse: ~p", [Reason]);
654 + DBMod:log_message(VHost, Msg)
660 +handle_cast({remove_user, User}, #state{dbmod=DBMod, vhost=VHost}=State) ->
661 + case State#state.drop_messages_on_user_removal of
663 + DBMod:drop_user(binary_to_list(User), VHost),
664 + ?INFO_MSG("Launched ~s@~s removal", [User, VHost]);
666 + ?INFO_MSG("Message removing is disabled. Keeping messages for ~s@~s", [User, VHost])
669 +% ejabberdctl rebuild_stats/3
670 +handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) ->
671 + DBMod:rebuild_stats(VHost),
673 +handle_cast({copy_messages, Backend}, State) ->
674 + spawn(?MODULE, copy_messages, [[State, Backend, []]]),
676 +handle_cast({copy_messages, Backend, Date}, State) ->
677 + spawn(?MODULE, copy_messages, [[State, Backend, [binary_to_list(Date)]]]),
679 +handle_cast(Msg, State) ->
680 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
683 +% return: disabled | timer reference
684 +set_purge_timer(PurgeDays) ->
687 + Days when is_integer(Days) ->
688 + {ok, Ref1} = timer:send_interval(timer:hours(24), scheduled_purging),
692 +% return: disabled | timer reference
693 +set_poll_timer(PollSec) ->
696 + {ok, Ref2} = timer:send_interval(timer:seconds(PollSec), poll_users_settings),
698 + % db polling disabled
702 + {ok, Ref3} = timer:send_interval(timer:seconds(10), poll_users_settings),
706 +% actual starting of logging
707 +% from timer:send_after (in init)
708 +handle_info(start, #state{dbmod=DBMod, vhost=VHost}=State) ->
709 + case DBMod:start(VHost, State#state.dbopts) of
710 + {error,{already_started,_}} ->
711 + ?MYDEBUG("backend module already started - trying to stop it", []),
713 + {stop, already_started, State};
715 + timer:sleep(30000),
716 + ?ERROR_MSG("Failed to start: ~p", [Reason]),
717 + {stop, db_connection_failed, State};
719 + ?INFO_MSG("~p connection established", [DBMod]),
721 + MonRef = erlang:monitor(process, SPid),
723 + ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]),
724 + DoLog = case DBMod:get_users_settings(VHost) of
725 + {ok, Settings} -> [Sett#user_settings{owner_name = iolist_to_binary(Sett#user_settings.owner_name)} || Sett <- Settings];
726 + {error, _Reason} -> []
728 + ets:insert(ets_settings_table(VHost), DoLog),
730 + TrefPurge = set_purge_timer(State#state.purge_older_days),
731 + TrefPoll = set_poll_timer(State#state.poll_users_settings),
733 + ejabberd_hooks:add(remove_user, VHost, ?MODULE, remove_user, 90),
734 + ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90),
735 + ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
736 + ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_message, 40),
738 + ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
739 + ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 50),
740 + ejabberd_hooks:add(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
741 + ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 50),
742 + ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
744 + ejabberd_hooks:add(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
745 + ejabberd_hooks:add(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
746 + ejabberd_hooks:add(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
747 + ejabberd_hooks:add(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
749 + ?MYDEBUG("Added hooks for ~p", [VHost]),
751 + ejabberd_commands:register_commands(get_commands_spec()),
752 + ?MYDEBUG("Registered commands for ~p", [VHost]),
754 + NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll},
755 + {noreply, NewState};
757 + ?ERROR_MSG("Rez=~p", [Rez]),
758 + timer:sleep(30000),
759 + {stop, db_connection_failed, State}
761 +% from timer:send_interval/2 (in start handle_info)
762 +handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = State) ->
763 + ?MYDEBUG("Starting scheduled purging of old records for ~p", [VHost]),
764 + spawn(?MODULE, purge_old_records, [VHost, integer_to_list(Days)]),
766 +% from timer:send_interval/2 (in start handle_info)
767 +handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) ->
768 + {ok, DoLog} = DBMod:get_users_settings(VHost),
769 + ?MYDEBUG("DoLog=~p", [DoLog]),
770 + true = ets:delete_all_objects(ets_settings_table(VHost)),
771 + ets:insert(ets_settings_table(VHost), DoLog),
773 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
774 + {stop, db_connection_dropped, State};
775 +handle_info({fetch_result, _, _}, State) ->
776 + ?MYDEBUG("Got timed out mysql fetch result", []),
778 +handle_info(Info, State) ->
779 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
782 +terminate(db_connection_failed, _State) ->
784 +terminate(db_connection_dropped, State) ->
785 + ?MYDEBUG("Got terminate with db_connection_dropped", []),
788 +terminate(Reason, #state{monref=undefined} = State) ->
789 + ?MYDEBUG("Got terminate with undefined monref.~nReason: ~p", [Reason]),
792 +terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) ->
793 + ?INFO_MSG("Reason: ~p", [Reason]),
794 + case erlang:is_process_alive(Pid) of
796 + erlang:demonitor(MonRef, [flush]),
804 +code_change(_OldVsn, State, _Extra) ->
807 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
809 +% ejabberd_hooks callbacks
811 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
812 +% TODO: change to/from to list as sql stores it as list
813 +send_packet({Pkt, #{jid := Owner} = C2SState}) ->
814 + VHost = Owner#jid.lserver,
815 + Peer = xmpp:get_to(Pkt),
816 + %?MYDEBUG("send_packet. Peer=~p, Owner=~p", [Peer, Owner]),
817 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
818 + gen_server:cast(Proc, {addlog, to, Owner, Peer, Pkt}),
821 +receive_packet({Pkt, #{jid := Owner} = C2SState}) ->
822 + VHost = Owner#jid.lserver,
823 + Peer = xmpp:get_from(Pkt),
824 + %?MYDEBUG("receive_packet. Pkt=~p", [Pkt]),
825 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
826 + gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
829 +offline_message({_Action, #message{from = Peer, to = Owner} = Pkt} = Acc) ->
830 + VHost = Owner#jid.lserver,
831 + %?MYDEBUG("offline_message. Pkt=~p", [Pkt]),
832 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
833 + gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
836 +remove_user(User, Server) ->
837 + LUser = jid:nodeprep(User),
838 + LServer = jid:nameprep(Server),
839 + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME),
840 + gen_server:cast(Proc, {remove_user, LUser}).
842 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
846 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
847 +rebuild_stats(VHost) ->
848 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
849 + gen_server:cast(Proc, {rebuild_stats}),
852 +copy_messages_ctl(VHost, Backend, <<"all">>) ->
853 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
854 + gen_server:cast(Proc, {copy_messages, Backend}),
856 +copy_messages_ctl(VHost, Backend, Date) ->
857 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
858 + gen_server:cast(Proc, {copy_messages, Backend, Date}),
861 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
865 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
867 +% handle_cast({addlog, E}, _)
868 +% raw packet -> #msg
869 +packet_parse(_Owner, _Peer, #message{type = error}, _Direction, _State) ->
871 +packet_parse(_Owner, _Peer, #message{meta = #{sm_copy := true}}, _Direction, _State) ->
873 +packet_parse(_Owner, _Peer, #message{meta = #{from_offline := true}}, _Direction, _State) ->
875 +packet_parse(Owner, Peer, #message{body = Body, subject = Subject, type = Type}, Direction, State) ->
876 + %?MYDEBUG("Owner=~p, Peer=~p, Direction=~p", [Owner, Peer, Direction]),
877 + %?MYDEBUG("Body=~p, Subject=~p, Type=~p", [Body, Subject, Type]),
878 + SubjectText = xmpp:get_text(Subject),
879 + BodyText = xmpp:get_text(Body),
880 + if (SubjectText == <<"">>) and (BodyText == <<"">>) ->
886 + groupchat when State#state.groupchat == send, Direction == to ->
888 + groupchat when State#state.groupchat == send, Direction == from ->
890 + groupchat when State#state.groupchat == none ->
896 + #msg{timestamp = get_timestamp(),
897 + owner_name = stringprep:tolower(Owner#jid.user),
898 + peer_name = stringprep:tolower(Peer#jid.user),
899 + peer_server = stringprep:tolower(Peer#jid.server),
900 + peer_resource = Peer#jid.resource,
901 + direction = Direction,
902 + type = misc:atom_to_binary(Type),
903 + subject = SubjectText,
905 +packet_parse(_, _, _, _, _) ->
908 +% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages)
909 +filter(Owner, Peer, State) ->
910 + OwnerBin = << (Owner#jid.luser)/binary, "@", (Owner#jid.lserver)/binary >>,
911 + OwnerServ = << "@", (Owner#jid.lserver)/binary >>,
912 + PeerBin = << (Peer#jid.luser)/binary, "@", (Peer#jid.lserver)/binary >>,
913 + PeerServ = << "@", (Peer#jid.lserver)/binary >>,
915 + LogTo = case ets:match_object(ets_settings_table(State#state.vhost),
916 + #user_settings{owner_name=Owner#jid.luser, _='_'}) of
917 + [#user_settings{dolog_default=Default,
919 + donotlog_list=DNLL}] ->
921 + A = lists:member(PeerBin, DLL),
922 + B = lists:member(PeerBin, DNLL),
926 + Default == true -> true;
927 + Default == false -> false;
928 + true -> State#state.dolog_default
930 + _ -> State#state.dolog_default
932 + lists:all(fun(O) -> O end,
933 + [not lists:member(OwnerBin, State#state.ignore_jids),
934 + not lists:member(PeerBin, State#state.ignore_jids),
935 + not lists:member(OwnerServ, State#state.ignore_jids),
936 + not lists:member(PeerServ, State#state.ignore_jids),
939 +purge_old_records(VHost, Days) ->
940 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
942 + Dates = ?MODULE:get_dates(VHost),
943 + DateNow = calendar:datetime_to_gregorian_seconds({date(), {0,0,1}}),
944 + DateDiff = list_to_integer(Days)*24*60*60,
945 + ?MYDEBUG("Purging tables older than ~s days", [Days]),
946 + lists:foreach(fun(Date) ->
947 + [Year, Month, Day] = ejabberd_regexp:split(iolist_to_binary(Date), <<"[^0-9]+">>),
948 + DateInSec = calendar:datetime_to_gregorian_seconds({{binary_to_integer(Year), binary_to_integer(Month), binary_to_integer(Day)}, {0,0,1}}),
950 + (DateNow - DateInSec) > DateDiff ->
951 + gen_server:call(Proc, {delete_messages_at, Date});
953 + ?MYDEBUG("Skipping messages at ~p", [Date])
957 +% called from get_vhost_stats/2, get_user_stats/3
958 +sort_stats(Stats) ->
959 + % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ]
960 + CFun = fun({TableName, Count}) ->
961 + [Year, Month, Day] = ejabberd_regexp:split(iolist_to_binary(TableName), <<"[^0-9]+">>),
962 + { calendar:datetime_to_gregorian_seconds({{binary_to_integer(Year), binary_to_integer(Month), binary_to_integer(Day)}, {0,0,1}}), Count }
964 + % convert to [{63364377601,1}, {63360662401,1}, ... ]
965 + CStats = lists:map(CFun, Stats),
967 + SortedStats = lists:reverse(lists:keysort(1, CStats)),
968 + % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list
969 + [{mod_logdb:convert_timestamp_brief(TableSec), Count} || {TableSec, Count} <- SortedStats].
971 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
973 +% Date/Time operations
975 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
976 +% return float seconds elapsed from "zero hour" as list
978 + {MegaSec, Sec, MicroSec} = now(),
979 + [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
982 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string
983 +convert_timestamp(Seconds) when is_list(Seconds) ->
984 + case string:to_float(Seconds++".0") of
985 + {F,_} when is_float(F) -> convert_timestamp(F);
986 + _ -> erlang:error(badarg, [Seconds])
988 +convert_timestamp(Seconds) when is_float(Seconds) ->
989 + GregSec = trunc(Seconds + 719528*86400),
990 + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
991 + {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT),
992 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec).
994 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string
995 +convert_timestamp_brief(Seconds) when is_list(Seconds) ->
996 + convert_timestamp_brief(list_to_float(Seconds));
997 +convert_timestamp_brief(Seconds) when is_float(Seconds) ->
998 + GregSec = trunc(Seconds + 719528*86400),
999 + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
1000 + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT),
1001 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day);
1002 +convert_timestamp_brief(Seconds) when is_integer(Seconds) ->
1003 + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds),
1004 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day).
1006 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1008 +% DB operations (get)
1010 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1011 +get_vhost_stats(VHost) ->
1012 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1013 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
1015 +get_vhost_stats_at(VHost, Date) ->
1016 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1017 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
1019 +get_user_stats(User, VHost) ->
1020 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1021 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
1023 +get_user_messages_at(User, VHost, Date) ->
1024 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1025 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
1027 +get_dates(VHost) ->
1028 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1029 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
1031 +get_user_settings(User, VHost) ->
1032 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1033 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
1035 +set_user_settings(User, VHost, Set) ->
1036 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1037 + gen_server:call(Proc, {set_user_settings, User, Set}).
1039 +get_module_settings(VHost) ->
1040 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1041 + gen_server:call(Proc, {get_module_settings}).
1043 +set_module_settings(VHost, Settings) ->
1044 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1045 + gen_server:call(Proc, {set_module_settings, Settings}).
1047 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1049 +% Web admin callbacks (delete)
1051 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1052 +user_messages_at_parse_query(VHost, Date, Msgs, Query) ->
1053 + case lists:keysearch(<<"delete">>, 1, Query) of
1055 + PMsgs = lists:filter(
1057 + ID = misc:encode_base64(term_to_binary(Msg#msg.timestamp)),
1058 + lists:member({<<"selected">>, ID}, Query)
1060 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1061 + gen_server:call(Proc, {delete_messages_by_user_at, PMsgs, Date}, ?CALL_TIMEOUT);
1066 +user_messages_parse_query(User, VHost, Query) ->
1067 + case lists:keysearch(<<"delete">>, 1, Query) of
1069 + Dates = get_dates(VHost),
1070 + PDates = lists:filter(
1072 + ID = misc:encode_base64( << User/binary, (iolist_to_binary(Date))/binary >> ),
1073 + lists:member({<<"selected">>, ID}, Query)
1075 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1076 + Rez = lists:foldl(
1079 + [gen_server:call(Proc,
1080 + {delete_all_messages_by_user_at, User, iolist_to_binary(Date)},
1083 + case lists:member(error, Rez) of
1093 +vhost_messages_parse_query(VHost, Query) ->
1094 + case lists:keysearch(<<"delete">>, 1, Query) of
1096 + Dates = get_dates(VHost),
1097 + PDates = lists:filter(
1099 + ID = misc:encode_base64( << VHost/binary, (iolist_to_binary(Date))/binary >> ),
1100 + lists:member({<<"selected">>, ID}, Query)
1102 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1103 + Rez = lists:foldl(fun(Date, Acc) ->
1104 + lists:append(Acc, [gen_server:call(Proc,
1105 + {delete_messages_at, Date},
1108 + case lists:member(error, Rez) of
1118 +vhost_messages_at_parse_query(VHost, Date, Stats, Query) ->
1119 + case lists:keysearch(<<"delete">>, 1, Query) of
1121 + PStats = lists:filter(
1122 + fun({User, _Count}) ->
1123 + ID = misc:encode_base64( << (iolist_to_binary(User))/binary, VHost/binary >> ),
1124 + lists:member({<<"selected">>, ID}, Query)
1126 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1127 + Rez = lists:foldl(fun({User, _Count}, Acc) ->
1128 + lists:append(Acc, [gen_server:call(Proc,
1129 + {delete_all_messages_by_user_at,
1130 + iolist_to_binary(User), iolist_to_binary(Date)},
1133 + case lists:member(error, Rez) of
1143 +copy_messages([#state{vhost=VHost}=State, From, DatesIn]) ->
1144 + {FromDBName, FromDBOpts} =
1145 + case lists:keysearch(misc:binary_to_atom(From), 1, State#state.dbs) of
1146 + {value, {FN, FO}} ->
1149 + ?ERROR_MSG("Failed to find record for ~p in dbs", [From]),
1153 + FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
1155 + {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
1157 + Dates = case DatesIn of
1158 + [] -> FromDBMod:get_dates(VHost);
1162 + DatesLength = length(Dates),
1164 + catch lists:foldl(fun(Date, Acc) ->
1165 + case catch copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
1167 + ?INFO_MSG("Copied messages at ~p (~p/~p)", [Date, Acc, DatesLength]);
1169 + ?ERROR_MSG("Failed to copy messages at ~p (~p/~p): ~p", [Date, Acc, DatesLength, Value]),
1174 + ?INFO_MSG("copy_messages from ~p finished", [From]),
1175 + FromDBMod:stop(VHost).
1177 +copy_messages_int([FromDBMod, ToDBMod, VHost, Date]) ->
1178 + ets:new(mod_logdb_temp, [named_table, set, public]),
1179 + {Time, Value} = timer:tc(?MODULE, copy_messages_int_tc, [[FromDBMod, ToDBMod, VHost, Date]]),
1180 + ets:delete_all_objects(mod_logdb_temp),
1181 + ets:delete(mod_logdb_temp),
1182 + ?INFO_MSG("copy_messages at ~p elapsed ~p sec", [Date, Time/1000000]),
1185 +copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) ->
1186 + ?INFO_MSG("Going to copy messages from ~p for ~p at ~p", [FromDBMod, VHost, Date]),
1188 + ok = FromDBMod:rebuild_stats_at(VHost, Date),
1189 + catch mod_logdb:rebuild_stats_at(VHost, Date),
1190 + {ok, FromStats} = FromDBMod:get_vhost_stats_at(VHost, Date),
1191 + ToStats = case mod_logdb:get_vhost_stats_at(VHost, iolist_to_binary(Date)) of
1192 + {ok, Stats} -> Stats;
1196 + FromStatsS = lists:keysort(1, FromStats),
1197 + ToStatsS = lists:keysort(1, ToStats),
1199 + StatsLength = length(FromStats),
1202 + % destination table is empty
1204 + fun({User, _Count}, Acc) ->
1205 + {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
1207 + lists:foldl(fun(Msg, MFAcc) ->
1208 + MsgBinary = Msg#msg{owner_name=iolist_to_binary(User),
1209 + peer_name=iolist_to_binary(Msg#msg.peer_name),
1210 + peer_server=iolist_to_binary(Msg#msg.peer_server),
1211 + peer_resource=iolist_to_binary(Msg#msg.peer_resource),
1212 + type=iolist_to_binary(Msg#msg.type),
1213 + subject=iolist_to_binary(Msg#msg.subject),
1214 + body=iolist_to_binary(Msg#msg.body)},
1215 + ok = ToDBMod:log_message(VHost, MsgBinary),
1219 + ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
1220 + %timer:sleep(100),
1223 + % destination table is not empty
1225 + fun({User, _Count}, Acc) ->
1226 + {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date),
1227 + lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
1228 + ets:insert(mod_logdb_temp, {Tst});
1229 + % mysql, pgsql removes final zeros after decimal point
1230 + (#msg{timestamp=Tst}) when length(Tst) < 16 ->
1231 + {F, _} = string:to_float(Tst++".0"),
1232 + [T] = io_lib:format("~.5f", [F]),
1233 + ets:insert(mod_logdb_temp, {T})
1235 + {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
1237 + lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) ->
1238 + case ets:member(mod_logdb_temp, ToTimestamp) of
1240 + MsgBinary = Msg#msg{owner_name=iolist_to_binary(User),
1241 + peer_name=iolist_to_binary(Msg#msg.peer_name),
1242 + peer_server=iolist_to_binary(Msg#msg.peer_server),
1243 + peer_resource=iolist_to_binary(Msg#msg.peer_resource),
1244 + type=iolist_to_binary(Msg#msg.type),
1245 + subject=iolist_to_binary(Msg#msg.subject),
1246 + body=iolist_to_binary(Msg#msg.body)},
1247 + ok = ToDBMod:log_message(VHost, MsgBinary),
1248 + ets:insert(mod_logdb_temp, {ToTimestamp}),
1255 + ets:delete_all_objects(mod_logdb_temp),
1256 + ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
1257 + %timer:sleep(100),
1263 + FromStats == [] ->
1264 + ?INFO_MSG("No messages were found at ~p", [Date]);
1265 + FromStatsS == ToStatsS ->
1266 + ?INFO_MSG("Stats are equal at ~p", [Date]);
1267 + FromStatsS /= ToStatsS ->
1268 + lists:foldl(CopyFun, 0, FromStats),
1269 + ok = ToDBMod:rebuild_stats_at(VHost, Date)
1270 + %timer:sleep(1000)
1275 +list_to_bool(Num) when is_binary(Num) ->
1276 + list_to_bool(binary_to_list(Num));
1277 +list_to_bool(Num) when is_list(Num) ->
1278 + case lists:member(Num, ["t", "true", "y", "yes", "1"]) of
1282 + case lists:member(Num, ["f", "false", "n", "no", "0"]) of
1290 +bool_to_list(true) ->
1292 +bool_to_list(false) ->
1295 +list_to_string([]) ->
1297 +list_to_string(List) when is_list(List) ->
1298 + Str = lists:flatmap(fun(Elm) when is_binary(Elm) ->
1299 + binary_to_list(Elm) ++ "\n";
1300 + (Elm) when is_list(Elm) ->
1303 + lists:sublist(Str, length(Str)-1).
1305 +string_to_list(null) ->
1307 +string_to_list([]) ->
1309 +string_to_list(String) ->
1310 + ejabberd_regexp:split(iolist_to_binary(String), <<"\n">>).
1312 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1314 +% ad-hoc (copy/pasted from mod_configure.erl)
1316 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1317 +-define(ITEMS_RESULT(Allow, LNode, Fallback),
1321 + case get_local_items(LServer, LNode,
1322 + jid:encode(To), Lang) of
1323 + {result, Res} -> {result, Res};
1324 + {error, Error} -> {error, Error}
1328 +get_local_items(Acc, From, #jid{lserver = LServer} = To,
1330 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1333 + Items = case Acc of
1334 + {result, Its} -> Its;
1337 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1338 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1340 + AllowUser == allow; AllowAdmin == allow ->
1341 + case get_local_items(LServer, [],
1342 + jid:encode(To), Lang) of
1344 + {result, Items ++ Res};
1345 + {error, _Error} ->
1352 +get_local_items(Acc, From, #jid{lserver = LServer} = To,
1354 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1357 + LNode = tokenize(Node),
1358 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1359 + Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
1361 + [<<"mod_logdb">>] ->
1362 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1363 + [<<"mod_logdb_users">>] ->
1364 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1365 + [<<"mod_logdb_users">>, <<$@, _/binary>>] ->
1366 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1367 + [<<"mod_logdb_users">>, _User] ->
1368 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1369 + [<<"mod_logdb_settings">>] ->
1370 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1376 +-define(T(Lang, Text), translate:translate(Lang, Text)).
1378 +-define(NODE(Name, Node),
1379 + #disco_item{jid = jid:make(Server),
1381 + name = ?T(Lang, Name)}).
1383 +-define(NS_ADMINX(Sub),
1384 + <<(?NS_ADMIN)/binary, "#", Sub/binary>>).
1386 +tokenize(Node) -> str:tokens(Node, <<"/#">>).
1388 +get_local_items(_Host, [], Server, Lang) ->
1390 + [?NODE(<<"Messages logging engine">>, <<"mod_logdb">>)]
1392 +get_local_items(_Host, [<<"mod_logdb">>], Server, Lang) ->
1394 + [?NODE(<<"Messages logging engine users">>, <<"mod_logdb_users">>),
1395 + ?NODE(<<"Messages logging engine settings">>, <<"mod_logdb_settings">>)]
1397 +get_local_items(Host, [<<"mod_logdb_users">>], Server, _Lang) ->
1398 + {result, get_all_vh_users(Host, Server)};
1399 +get_local_items(Host, [<<"mod_logdb_users">>, <<$@, Diap/binary>>], Server, Lang) ->
1400 + Users = ejabberd_auth:get_vh_registered_users(Host),
1401 + SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1403 + [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>),
1404 + N1 = binary_to_integer(S1),
1405 + N2 = binary_to_integer(S2),
1406 + Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
1407 + {result, lists:map(fun({S, U}) ->
1408 + ?NODE(<< U/binary, "@", S/binary >>,
1409 + << (iolist_to_binary("mod_logdb_users/"))/binary, U/binary, "@", S/binary >>)
1412 + xmpp:err_not_acceptable()
1414 +get_local_items(_Host, [<<"mod_logdb_users">>, _User], _Server, _Lang) ->
1416 +get_local_items(_Host, [<<"mod_logdb_settings">>], _Server, _Lang) ->
1418 +get_local_items(_Host, Item, _Server, _Lang) ->
1419 + ?MYDEBUG("asked for items in ~p", [Item]),
1420 + {error, xmpp:err_item_not_found()}.
1422 +-define(INFO_RESULT(Allow, Feats, Lang),
1424 + deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
1425 + allow -> {result, Feats}
1428 +get_local_features(Acc, From,
1429 + #jid{lserver = LServer} = _To, Node, Lang) ->
1430 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1434 + LNode = tokenize(Node),
1435 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1436 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1438 + [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
1439 + ?INFO_RESULT(allow, [?NS_COMMANDS], Lang);
1440 + [<<"mod_logdb">>] ->
1441 + ?INFO_RESULT(deny, [?NS_COMMANDS], Lang);
1442 + [<<"mod_logdb_users">>] ->
1443 + ?INFO_RESULT(AllowAdmin, [], Lang);
1444 + [<<"mod_logdb_users">>, [$@ | _]] ->
1445 + ?INFO_RESULT(AllowAdmin, [], Lang);
1446 + [<<"mod_logdb_users">>, _User] ->
1447 + ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
1448 + [<<"mod_logdb_settings">>] ->
1449 + ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
1457 +-define(INFO_IDENTITY(Category, Type, Name, Lang),
1458 + [#identity{category = Category, type = Type, name = ?T(Lang, Name)}]).
1460 +-define(INFO_COMMAND(Name, Lang),
1461 + ?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
1464 +get_local_identity(Acc, _From, _To, Node, Lang) ->
1465 + LNode = tokenize(Node),
1467 + [<<"mod_logdb">>] ->
1468 + ?INFO_COMMAND(<<"Messages logging engine">>, Lang);
1469 + [<<"mod_logdb_users">>] ->
1470 + ?INFO_COMMAND(<<"Messages logging engine users">>, Lang);
1471 + [<<"mod_logdb_users">>, User] ->
1472 + ?INFO_COMMAND(User, Lang);
1473 + [<<"mod_logdb_settings">>] ->
1474 + ?INFO_COMMAND(<<"Messages logging engine settings">>, Lang);
1479 +adhoc_local_items(Acc, From,
1480 + #jid{lserver = LServer, server = Server} = To, Lang) ->
1481 + % TODO: case acl:match_rule(LServer, ???, From) of
1482 + Items = case Acc of
1483 + {result, Its} -> Its;
1486 + Nodes = recursively_get_local_items(LServer,
1487 + <<"">>, Server, Lang),
1488 + Nodes1 = lists:filter(
1489 + fun(#disco_item{node = Nd}) ->
1490 + F = get_local_features([], From, To, Nd, Lang),
1492 + {result, [?NS_COMMANDS]} -> true;
1496 + {result, Items ++ Nodes1}.
1498 +recursively_get_local_items(_LServer,
1499 + <<"mod_logdb_users">>, _Server, _Lang) ->
1501 +recursively_get_local_items(LServer,
1502 + Node, Server, Lang) ->
1503 + LNode = tokenize(Node),
1504 + Items = case get_local_items(LServer, LNode,
1506 + {result, Res} -> Res;
1507 + {error, _Error} -> []
1509 + Nodes = lists:flatten(
1511 + fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
1512 + if (S /= Server) or (Nd == <<"">>) ->
1515 + [Item, recursively_get_local_items(
1516 + LServer, Nd, Server, Lang)]
1521 +-define(COMMANDS_RESULT(Allow, From, To, Request),
1524 + {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
1526 + adhoc_local_commands(From, To, Request)
1529 +adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
1530 + #adhoc_command{node = Node, lang = Lang} = Request) ->
1531 + LNode = tokenize(Node),
1532 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1533 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1535 + [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
1536 + ?COMMANDS_RESULT(allow, From, To, Request);
1537 + [<<"mod_logdb_users">>, <<$@, _/binary>>] when AllowAdmin == allow ->
1539 + [<<"mod_logdb_users">>, _User] when AllowAdmin == allow ->
1540 + ?COMMANDS_RESULT(allow, From, To, Request);
1541 + [<<"mod_logdb_settings">>] when AllowAdmin == allow ->
1542 + ?COMMANDS_RESULT(allow, From, To, Request);
1547 +adhoc_local_commands(From, #jid{lserver = LServer} = _To,
1548 + #adhoc_command{lang = Lang,
1552 + xdata = XData} = Request) ->
1553 + LNode = tokenize(Node),
1554 + %% If the "action" attribute is not present, it is
1555 + %% understood as "execute". If there was no <actions/>
1556 + %% element in the first response (which there isn't in our
1557 + %% case), "execute" and "complete" are equivalent.
1558 + ActionIsExecute = Action == execute orelse Action == complete,
1559 + if Action == cancel ->
1560 + %% User cancels request
1561 + #adhoc_command{status = canceled, lang = Lang,
1562 + node = Node, sid = SessionID};
1563 + XData == undefined, ActionIsExecute ->
1564 + %% User requests form
1565 + case get_form(LServer, LNode, Lang) of
1567 + xmpp_util:make_adhoc_response(
1569 + #adhoc_command{status = executing,
1574 + XData /= undefined, ActionIsExecute ->
1575 + %% User returns form.
1576 + case catch set_form(From, LServer, LNode, Lang, XData) of
1578 + xmpp_util:make_adhoc_response(
1580 + #adhoc_command{xdata = Res, status = completed});
1581 + {'EXIT', _} -> {error, xmpp:err_bad_request()};
1582 + {error, Error} -> {error, Error}
1585 + {error, xmpp:err_bad_request(<<"Unexpected action">>, Lang)}
1588 +-define(TVFIELD(Type, Var, Val),
1589 + #xdata_field{type = Type, var = Var, values = [Val]}).
1592 + ?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))).
1594 +get_user_form(LUser, LServer, Lang) ->
1595 + ?MYDEBUG("get_user_form ~p ~p", [LUser, LServer]),
1596 + %From = jid:encode(jid:remove_resource(Jid)),
1597 + #user_settings{dolog_default=DLD,
1599 + donotlog_list=DNLL} = get_user_settings(LUser, LServer),
1602 + type = 'list-single',
1603 + label = ?T(Lang, <<"Default">>),
1604 + var = <<"dolog_default">>,
1605 + values = [misc:atom_to_binary(DLD)],
1606 + options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
1607 + value = <<"true">>},
1608 + #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
1609 + value = <<"false">>}]},
1611 + type = 'text-multi',
1612 + label = ?T(Lang, <<"Log Messages">>),
1613 + var = <<"dolog_list">>,
1616 + type = 'text-multi',
1617 + label = ?T(Lang, <<"Do Not Log Messages">>),
1618 + var = <<"donotlog_list">>,
1622 + title = ?T(Lang, <<"Messages logging engine settings">>),
1624 + instructions = [<< (?T(Lang, <<"Set logging preferences">>))/binary,
1625 + (iolist_to_binary(": "))/binary,
1626 + LUser/binary, "@", LServer/binary >>],
1627 + fields = [?HFIELD()|
1630 +get_settings_form(Host, Lang) ->
1631 + ?MYDEBUG("get_settings_form ~p ~p", [Host, Lang]),
1632 + #state{dbmod=_DBMod,
1634 + dolog_default=DLD,
1635 + ignore_jids=IgnoreJids,
1636 + groupchat=GroupChat,
1637 + purge_older_days=PurgeDaysT,
1638 + drop_messages_on_user_removal=MRemoval,
1639 + poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
1642 + case PurgeDaysT of
1643 + never -> <<"never">>;
1644 + Num when is_integer(Num) -> integer_to_binary(Num);
1645 + _ -> <<"unknown">>
1649 + type = 'list-single',
1650 + label = ?T(Lang, <<"Default">>),
1651 + var = <<"dolog_default">>,
1652 + values = [misc:atom_to_binary(DLD)],
1653 + options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
1654 + value = <<"true">>},
1655 + #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
1656 + value = <<"false">>}]},
1658 + type = 'list-single',
1659 + label = ?T(Lang, <<"Drop messages on user removal">>),
1660 + var = <<"drop_messages_on_user_removal">>,
1661 + values = [misc:atom_to_binary(MRemoval)],
1662 + options = [#xdata_option{label = ?T(Lang, <<"Drop">>),
1663 + value = <<"true">>},
1664 + #xdata_option{label = ?T(Lang, <<"Do not drop">>),
1665 + value = <<"false">>}]},
1667 + type = 'list-single',
1668 + label = ?T(Lang, <<"Groupchat messages logging">>),
1669 + var = <<"groupchat">>,
1670 + values = [misc:atom_to_binary(GroupChat)],
1671 + options = [#xdata_option{label = ?T(Lang, <<"all">>),
1672 + value = <<"all">>},
1673 + #xdata_option{label = ?T(Lang, <<"none">>),
1674 + value = <<"none">>},
1675 + #xdata_option{label = ?T(Lang, <<"send">>),
1676 + value = <<"send">>}]},
1678 + type = 'text-multi',
1679 + label = ?T(Lang, <<"Jids/Domains to ignore">>),
1680 + var = <<"ignore_list">>,
1681 + values = IgnoreJids},
1683 + type = 'text-single',
1684 + label = ?T(Lang, <<"Purge messages older than (days)">>),
1685 + var = <<"purge_older_days">>,
1686 + values = [iolist_to_binary(PurgeDays)]},
1688 + type = 'text-single',
1689 + label = ?T(Lang, <<"Poll users settings (seconds)">>),
1690 + var = <<"poll_users_settings">>,
1691 + values = [integer_to_binary(PollTime)]}
1694 + title = ?T(Lang, <<"Messages logging engine settings (run-time)">>),
1695 + instructions = [?T(Lang, <<"Set run-time settings">>)],
1697 + fields = [?HFIELD()|
1700 +get_form(_Host, [<<"mod_logdb_users">>, User], Lang) ->
1701 + #jid{luser=LUser, lserver=LServer} = jid:decode(User),
1702 + get_user_form(LUser, LServer, Lang);
1703 +get_form(Host, [<<"mod_logdb_settings">>], Lang) ->
1704 + get_settings_form(Host, Lang);
1705 +get_form(_Host, Command, _Lang) ->
1706 + ?MYDEBUG("asked for form ~p", [Command]),
1707 + {error, xmpp:err_service_unavailable()}.
1709 +check_log_list([]) ->
1711 +check_log_list([<<>>]) ->
1713 +check_log_list([Head | Tail]) ->
1714 + case binary:match(Head, <<$@>>) of
1715 + nomatch -> throw(error);
1718 + % this check for Head to be valid jid
1719 + case catch jid:decode(Head) of
1720 + {'EXIT', _Reason} -> throw(error);
1721 + _ -> check_log_list(Tail)
1724 +check_ignore_list([]) ->
1726 +check_ignore_list([<<>>]) ->
1728 +check_ignore_list([<<>> | Tail]) ->
1729 + check_ignore_list(Tail);
1730 +check_ignore_list([Head | Tail]) ->
1731 + case binary:match(Head, <<$@>>) of
1733 + nomatch -> throw(error)
1735 + Jid2Test = case Head of
1736 + << $@, _Rest/binary >> -> << "a", Head/binary >>;
1739 + % this check for Head to be valid jid
1740 + case catch jid:decode(Jid2Test) of
1741 + {'EXIT', _Reason} -> throw(error);
1742 + _ -> check_ignore_list(Tail)
1745 +get_value(Field, XData) -> hd(get_values(Field, XData)).
1747 +get_values(Field, XData) ->
1748 + xmpp_util:get_xdata_values(Field, XData).
1750 +parse_users_settings(XData) ->
1751 + DLD = case get_value(<<"dolog_default">>, XData) of
1752 + ValueDLD when ValueDLD == <<"true">>;
1753 + ValueDLD == <<"false">> ->
1754 + list_to_bool(ValueDLD);
1755 + _ -> throw(bad_request)
1758 + ListDLL = get_values(<<"dolog_list">>, XData),
1759 + DLL = case catch check_log_list(ListDLL) of
1761 + error -> throw(bad_request)
1764 + ListDNLL = get_values(<<"donotlog_list">>, XData),
1765 + DNLL = case catch check_log_list(ListDNLL) of
1767 + error -> throw(bad_request)
1770 + #user_settings{dolog_default=DLD,
1772 + donotlog_list=DNLL}.
1774 +parse_module_settings(XData) ->
1775 + DLD = case get_value(<<"dolog_default">>, XData) of
1776 + ValueDLD when ValueDLD == <<"true">>;
1777 + ValueDLD == <<"false">> ->
1778 + list_to_bool(ValueDLD);
1779 + _ -> throw(bad_request)
1781 + MRemoval = case get_value(<<"drop_messages_on_user_removal">>, XData) of
1782 + ValueMRemoval when ValueMRemoval == <<"true">>;
1783 + ValueMRemoval == <<"false">> ->
1784 + list_to_bool(ValueMRemoval);
1785 + _ -> throw(bad_request)
1787 + GroupChat = case get_value(<<"groupchat">>, XData) of
1788 + ValueGroupChat when ValueGroupChat == <<"none">>;
1789 + ValueGroupChat == <<"all">>;
1790 + ValueGroupChat == <<"send">> ->
1791 + misc:binary_to_atom(ValueGroupChat);
1792 + _ -> throw(bad_request)
1794 + ListIgnore = get_values(<<"ignore_list">>, XData),
1795 + Ignore = case catch check_ignore_list(ListIgnore) of
1797 + error -> throw(bad_request)
1799 + Purge = case get_value(<<"purge_older_days">>, XData) of
1800 + <<"never">> -> never;
1802 + case catch binary_to_integer(ValuePurge) of
1803 + IntValuePurge when is_integer(IntValuePurge) -> IntValuePurge;
1804 + _ -> throw(bad_request)
1807 + Poll = case catch binary_to_integer(get_value(<<"poll_users_settings">>, XData)) of
1808 + IntValuePoll when is_integer(IntValuePoll) -> IntValuePoll;
1809 + _ -> throw(bad_request)
1811 + #state{dolog_default=DLD,
1812 + groupchat=GroupChat,
1813 + ignore_jids=Ignore,
1814 + purge_older_days=Purge,
1815 + drop_messages_on_user_removal=MRemoval,
1816 + poll_users_settings=Poll}.
1818 +set_form(_From, _Host, [<<"mod_logdb_users">>, User], Lang, XData) ->
1819 + #jid{luser=LUser, lserver=LServer} = jid:decode(User),
1820 + Txt = "Parse user settings failed",
1821 + case catch parse_users_settings(XData) of
1823 + ?ERROR_MSG("Failed to set user form: bad_request", []),
1824 + {error, xmpp:err_bad_request(Txt, Lang)};
1825 + {'EXIT', Reason} ->
1826 + ?ERROR_MSG("Failed to set user form ~p", [Reason]),
1827 + {error, xmpp:err_bad_request(Txt, Lang)};
1829 + case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
1831 + {result, undefined};
1833 + {error, xmpp:err_internal_server_error()}
1836 +set_form(_From, Host, [<<"mod_logdb_settings">>], Lang, XData) ->
1837 + Txt = "Parse module settings failed",
1838 + case catch parse_module_settings(XData) of
1840 + ?ERROR_MSG("Failed to set settings form: bad_request", []),
1841 + {error, xmpp:err_bad_request(Txt, Lang)};
1842 + {'EXIT', Reason} ->
1843 + ?ERROR_MSG("Failed to set settings form ~p", [Reason]),
1844 + {error, xmpp:err_bad_request(Txt, Lang)};
1846 + case mod_logdb:set_module_settings(Host, Settings) of
1848 + {result, undefined};
1850 + {error, xmpp:err_internal_server_error()}
1853 +set_form(From, _Host, Node, _Lang, XData) ->
1854 + User = jid:encode(jid:remove_resource(From)),
1855 + ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]),
1856 + {error, xmpp:err_service_unavailable()}.
1858 +get_all_vh_users(Host, Server) ->
1859 + case catch ejabberd_auth:get_vh_registered_users(Host) of
1860 + {'EXIT', _Reason} ->
1863 + SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1864 + case length(SUsers) of
1865 + N when N =< 100 ->
1866 + lists:map(fun({S, U}) ->
1867 + #disco_item{jid = jid:make(Server),
1868 + node = <<"mod_logdb_users/", U/binary, $@, S/binary>>,
1869 + name = << U/binary, "@", S/binary >>}
1872 + NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1,
1873 + M = trunc(N / NParts) + 1,
1874 + lists:map(fun(K) ->
1877 + (integer_to_binary(K))/binary,
1879 + (integer_to_binary(L))/binary
1881 + {FS, FU} = lists:nth(K, SUsers),
1883 + if L < N -> lists:nth(L, SUsers);
1884 + true -> lists:last(SUsers)
1887 + <<FU/binary, "@", FS/binary,
1889 + LU/binary, "@", LS/binary>>,
1890 + #disco_item{jid = jid:make(Host),
1891 + node = <<"mod_logdb_users/", Node/binary>>,
1893 + end, lists:seq(1, N, M))
1897 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1901 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1902 +webadmin_menu(Acc, _Host, Lang) ->
1903 + [{<<"messages">>, ?T(<<"Users Messages">>)} | Acc].
1905 +webadmin_user(Acc, User, Server, Lang) ->
1906 + Sett = get_user_settings(User, Server),
1908 + case Sett#user_settings.dolog_default of
1910 + ?INPUTT(<<"submit">>, <<"dolog">>, <<"Log Messages">>);
1912 + ?INPUTT(<<"submit">>, <<"donotlog">>, <<"Do Not Log Messages">>);
1915 + Acc ++ [?XE(<<"h3">>, [?ACT(<<"messages/">>, <<"Messages">>), ?C(<<" ">>), Log])].
1917 +webadmin_page(_, Host,
1918 + #request{path = [<<"messages">>],
1921 + Res = vhost_messages_stats(Host, Query, Lang),
1923 +webadmin_page(_, Host,
1924 + #request{path = [<<"messages">>, Date],
1927 + Res = vhost_messages_stats_at(Host, Query, Lang, Date),
1929 +webadmin_page(_, Host,
1930 + #request{path = [<<"user">>, U, <<"messages">>],
1933 + Res = user_messages_stats(U, Host, Query, Lang),
1935 +webadmin_page(_, Host,
1936 + #request{path = [<<"user">>, U, <<"messages">>, Date],
1939 + Res = mod_logdb:user_messages_stats_at(U, Host, Query, Lang, Date),
1941 +webadmin_page(Acc, _Host, _R) -> Acc.
1943 +user_parse_query(_, <<"dolog">>, User, Server, _Query) ->
1944 + Sett = get_user_settings(User, Server),
1945 + % TODO: check returned value
1946 + set_user_settings(User, Server, Sett#user_settings{dolog_default=true}),
1948 +user_parse_query(_, <<"donotlog">>, User, Server, _Query) ->
1949 + Sett = get_user_settings(User, Server),
1950 + % TODO: check returned value
1951 + set_user_settings(User, Server, Sett#user_settings{dolog_default=false}),
1953 +user_parse_query(Acc, _Action, _User, _Server, _Query) ->
1956 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1960 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1961 +vhost_messages_stats(Server, Query, Lang) ->
1962 + Res = case catch vhost_messages_parse_query(Server, Query) of
1963 + {'EXIT', Reason} ->
1964 + ?ERROR_MSG("~p", [Reason]),
1966 + VResult -> VResult
1968 + {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]),
1969 + ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]),
1970 + %case get_vhost_stats(Server) of
1972 + {'EXIT', CReason} ->
1973 + ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
1974 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
1975 + {error, GReason} ->
1976 + ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
1977 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
1979 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Server])))];
1981 + Fun = fun({Date, Count}) ->
1982 + DateBin = iolist_to_binary(Date),
1983 + ID = misc:encode_base64( << Server/binary, DateBin/binary >> ),
1985 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
1986 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
1987 + ?XE(<<"td">>, [?AC(DateBin, DateBin)]),
1988 + ?XC(<<"td">>, integer_to_binary(Count))
1992 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s">>), [Server])))] ++
1994 + ok -> [?CT(<<"Submitted">>), ?P];
1995 + error -> [?CT(<<"Bad format">>), ?P];
1998 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2003 + ?XCT(<<"td">>, <<"Date">>),
2004 + ?XCT(<<"td">>, <<"Count">>)
2007 + lists:map(Fun, Dates)
2010 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2014 +vhost_messages_stats_at(Server, Query, Lang, Date) ->
2015 + {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]),
2016 + ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]),
2017 + %case get_vhost_stats_at(Server, Date) of
2019 + {'EXIT', CReason} ->
2020 + ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
2021 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2022 + {error, GReason} ->
2023 + ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
2024 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2026 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Server, Date])))];
2028 + Res = case catch vhost_messages_at_parse_query(Server, Date, Stats, Query) of
2029 + {'EXIT', Reason} ->
2030 + ?ERROR_MSG("~p", [Reason]),
2032 + VResult -> VResult
2034 + Fun = fun({User, Count}) ->
2035 + UserBin = iolist_to_binary(User),
2036 + ID = misc:encode_base64( << UserBin/binary, Server/binary >> ),
2038 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2039 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2040 + ?XE(<<"td">>, [?AC(<< <<"../user/">>/binary, UserBin/binary, <<"/messages/">>/binary, Date/binary >>, UserBin)]),
2041 + ?XC(<<"td">>, integer_to_binary(Count))
2044 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Server, Date])))] ++
2046 + ok -> [?CT(<<"Submitted">>), ?P];
2047 + error -> [?CT(<<"Bad format">>), ?P];
2050 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2055 + ?XCT(<<"td">>, <<"User">>),
2056 + ?XCT(<<"td">>, <<"Count">>)
2059 + lists:map(Fun, Stats)
2062 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2066 +user_messages_stats(User, Server, Query, Lang) ->
2067 + Jid = jid:encode({User, Server, ""}),
2069 + Res = case catch user_messages_parse_query(User, Server, Query) of
2070 + {'EXIT', Reason} ->
2071 + ?ERROR_MSG("~p", [Reason]),
2073 + VResult -> VResult
2076 + {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]),
2077 + ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]),
2080 + {'EXIT', CReason} ->
2081 + ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
2082 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
2083 + {error, GReason} ->
2084 + ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
2085 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
2087 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Jid])))];
2089 + Fun = fun({Date, Count}) ->
2090 + DateBin = iolist_to_binary(Date),
2091 + ID = misc:encode_base64( << User/binary, DateBin/binary >> ),
2093 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2094 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2095 + ?XE(<<"td">>, [?AC(DateBin, DateBin)]),
2096 + ?XC(<<"td">>, iolist_to_binary(integer_to_list(Count)))
2099 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T("Logged messages for ~s"), [Jid])))] ++
2101 + ok -> [?CT(<<"Submitted">>), ?P];
2102 + error -> [?CT(<<"Bad format">>), ?P];
2105 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2110 + ?XCT(<<"td">>, <<"Date">>),
2111 + ?XCT(<<"td">>, <<"Count">>)
2114 + lists:map(Fun, Dates)
2117 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2121 +search_user_nick(User, List) ->
2122 + case lists:keysearch(User, 1, List) of
2123 + {value,{User, []}} ->
2125 + {value,{User, Nick}} ->
2131 +user_messages_stats_at(User, Server, Query, Lang, Date) ->
2132 + Jid = jid:encode({User, Server, ""}),
2134 + {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]),
2135 + ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]),
2137 + {'EXIT', CReason} ->
2138 + ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
2139 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
2140 + {error, GReason} ->
2141 + ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
2142 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
2144 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Jid, Date])))];
2145 + {ok, User_messages} ->
2146 + Res = case catch user_messages_at_parse_query(Server,
2150 + {'EXIT', Reason} ->
2151 + ?ERROR_MSG("~p", [Reason]),
2153 + VResult -> VResult
2156 + UR = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]),
2158 + lists:map(fun(Item) ->
2159 + {jid:encode(Item#roster.jid), Item#roster.name}
2162 + UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) ->
2163 + ToAdd = PName++"@"++PServer,
2164 + case lists:member(ToAdd, List) of
2166 + false -> lists:append([ToAdd], List)
2168 + end, [], User_messages),
2170 + % Users to filter (sublist of UniqUsers)
2171 + CheckedUsers = case lists:keysearch(<<"filter">>, 1, Query) of
2173 + lists:filter(fun(UFUser) ->
2174 + ID = misc:encode_base64(term_to_binary(UFUser)),
2175 + lists:member({<<"selected">>, ID}, Query)
2180 + % UniqUsers in html (noone selected -> everyone selected)
2181 + Users = lists:map(fun(UHUser) ->
2182 + ID = misc:encode_base64(term_to_binary(UHUser)),
2183 + Input = case lists:member(UHUser, CheckedUsers) of
2184 + true -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2185 + false when CheckedUsers == [] -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2186 + false -> [?INPUT(<<"checkbox">>, <<"selected">>, ID)]
2189 + case search_user_nick(UHUser, UserRoster) of
2190 + nothing -> <<"">>;
2191 + N -> iolist_to_binary( " ("++ N ++")" )
2194 + [?XE(<<"td">>, Input),
2195 + ?XC(<<"td">>, iolist_to_binary(UHUser++Nick))])
2196 + end, lists:sort(UniqUsers)),
2197 + % Messages to show (based on Users)
2198 + User_messages_filtered = case CheckedUsers of
2199 + [] -> User_messages;
2200 + _ -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) ->
2201 + lists:member(PName++"@"++PServer, CheckedUsers)
2202 + end, User_messages)
2205 + Msgs_Fun = fun(#msg{timestamp=Timestamp,
2207 + direction=Direction,
2208 + peer_name=PName, peer_server=PServer, peer_resource=PRes,
2211 + Text = case Subject of
2212 + "" -> iolist_to_binary(Body);
2213 + _ -> iolist_to_binary([binary_to_list(?T(<<"Subject">>)) ++ ": " ++ Subject ++ "\n" ++ Body])
2215 + Resource = case PRes of
2221 + case search_user_nick(PName++"@"++PServer, UserRoster) of
2222 + nothing when PServer == Server ->
2224 + nothing when Type == "groupchat", Direction == from ->
2225 + PName++"@"++PServer++Resource;
2227 + PName++"@"++PServer;
2230 + ID = misc:encode_base64(term_to_binary(Timestamp)),
2232 + [?XE(<<"td">>, [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2233 + ?XC(<<"td">>, iolist_to_binary(convert_timestamp(Timestamp))),
2234 + ?XC(<<"td">>, iolist_to_binary(atom_to_list(Direction)++": "++UserNick)),
2235 + ?XE(<<"td">>, [?XC(<<"pre">>, Text)])])
2237 + % Filtered user messages in html
2238 + Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
2240 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Jid, Date])))] ++
2242 + ok -> [?CT(<<"Submitted">>), ?P];
2243 + error -> [?CT(<<"Bad format">>), ?P];
2246 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2250 + ?XCT(<<"td">>, <<"User">>)
2256 + ?INPUTT(<<"submit">>, <<"filter">>, <<"Filter Selected">>)
2262 + ?XCT(<<"td">>, <<"Date, Time">>),
2263 + ?XCT(<<"td">>, <<"Direction: Jid">>),
2264 + ?XCT(<<"td">>, <<"Body">>)
2269 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>),
2274 diff --git a/src/mod_logdb.hrl b/src/mod_logdb.hrl
2275 new file mode 100644
2276 index 00000000..49791f4e
2278 +++ b/src/mod_logdb.hrl
2280 +%%%----------------------------------------------------------------------
2281 +%%% File : mod_logdb.hrl
2282 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2284 +%%% Url : https://paleg.github.io/mod_logdb/
2285 +%%%----------------------------------------------------------------------
2287 +-define(logdb_debug, true).
2289 +-ifdef(logdb_debug).
2290 +-define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n",
2291 + [calendar:local_time(),?MODULE,?LINE]++Args)).
2293 +-define(MYDEBUG(_F,_A),[]).
2296 +-record(msg, {timestamp,
2298 + peer_name, peer_server, peer_resource,
2303 +-record(user_settings, {owner_name,
2306 + donotlog_list=[]}).
2308 +-define(INPUTC(Type, Name, Value),
2309 + ?XA(<<"input">>, [{<<"type">>, Type},
2310 + {<<"name">>, Name},
2311 + {<<"value">>, Value},
2312 + {<<"checked">>, <<"true">>}])).
2313 diff --git a/src/mod_logdb_mnesia.erl b/src/mod_logdb_mnesia.erl
2314 new file mode 100644
2315 index 00000000..ea167d88
2317 +++ b/src/mod_logdb_mnesia.erl
2319 +%%%----------------------------------------------------------------------
2320 +%%% File : mod_logdb_mnesia.erl
2321 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2322 +%%% Purpose : mnesia backend for mod_logdb
2323 +%%% Url : https://paleg.github.io/mod_logdb/
2324 +%%%----------------------------------------------------------------------
2326 +-module(mod_logdb_mnesia).
2327 +-author('o.palij@gmail.com').
2329 +-include("mod_logdb.hrl").
2330 +-include("ejabberd.hrl").
2331 +-include("jlib.hrl").
2332 +-include("logger.hrl").
2334 +-behaviour(gen_logdb).
2335 +-behaviour(gen_server).
2338 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2340 +-export([start/2, stop/1]).
2342 +-export([log_message/2,
2344 + rebuild_stats_at/2,
2345 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2346 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2348 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
2351 +-define(PROCNAME, mod_logdb_mnesia).
2352 +-define(CALL_TIMEOUT, 10000).
2354 +-record(state, {vhost}).
2356 +-record(stats, {user, at, count}).
2362 + "_" ++ binary_to_list(VHost).
2364 +stats_table(VHost) ->
2365 + list_to_atom(prefix() ++ "stats" ++ suffix(VHost)).
2367 +table_name(VHost, Date) ->
2368 + list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)).
2370 +settings_table(VHost) ->
2371 + list_to_atom(prefix() ++ "settings" ++ suffix(VHost)).
2373 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2375 +% gen_mod callbacks
2377 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2378 +start(VHost, Opts) ->
2379 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2380 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2383 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2384 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2386 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2388 +% gen_server callbacks
2390 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2391 +init([VHost, _Opts]) ->
2392 + case mnesia:system_info(is_running) of
2394 + ok = create_stats_table(VHost),
2395 + ok = create_settings_table(VHost),
2396 + {ok, #state{vhost=VHost}};
2398 + ?ERROR_MSG("Mnesia not running", []),
2399 + {stop, db_connection_failed};
2401 + ?ERROR_MSG("Mnesia status: ~p", [Status]),
2402 + {stop, db_connection_failed}
2405 +handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) ->
2406 + {reply, log_message_int(VHost, Msg), State};
2407 +handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) ->
2408 + {atomic, ok} = delete_nonexistent_stats(VHost),
2410 + lists:foreach(fun(Date) ->
2411 + rebuild_stats_at_int(VHost, Date)
2412 + end, get_dates_int(VHost)),
2413 + {reply, Reply, State};
2414 +handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2415 + Reply = rebuild_stats_at_int(VHost, Date),
2416 + {reply, Reply, State};
2417 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
2418 + Table = table_name(VHost, Date),
2422 + mnesia:write_lock_table(stats_table(VHost)),
2423 + mnesia:write_lock_table(Table),
2424 + mnesia:delete_object(Table, Msg, write)
2427 + DRez = case mnesia:transaction(Fun) of
2428 + {aborted, Reason} ->
2429 + ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
2435 + case rebuild_stats_at_int(VHost, Date) of
2441 + {reply, Reply, State};
2442 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) ->
2443 + {reply, delete_all_messages_by_user_at_int(User, VHost, Date), State};
2444 +handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) ->
2446 + case mnesia:delete_table(table_name(VHost, Date)) of
2448 + delete_stats_by_vhost_at_int(VHost, Date);
2449 + {aborted, Reason} ->
2450 + ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]),
2453 + {reply, Reply, State};
2454 +handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) ->
2455 + Fun = fun(#stats{at=Date, count=Count}, Stats) ->
2456 + case lists:keysearch(Date, 1, Stats) of
2458 + lists:append(Stats, [{Date, Count}]);
2459 + {value, {_, TempCount}} ->
2460 + lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count})
2464 + case mnesia:transaction(fun() ->
2465 + mnesia:foldl(Fun, [], stats_table(VHost))
2467 + {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)};
2468 + {aborted, Reason} -> {error, Reason}
2470 + {reply, Reply, State};
2471 +handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2473 + Pat = #stats{user='$1', at=Date, count='$2'},
2474 + mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
2477 + case mnesia:transaction(Fun) of
2478 + {atomic, Result} ->
2479 + {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))};
2480 + {aborted, Reason} ->
2483 + {reply, Reply, State};
2484 +handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) ->
2485 + {reply, get_user_stats_int(User, VHost), State};
2486 +handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) ->
2488 + case mnesia:transaction(fun() ->
2489 + Pat = #msg{owner_name=User, _='_'},
2490 + mnesia:select(table_name(VHost, Date),
2491 + [{Pat, [], ['$_']}])
2493 + {atomic, Result} -> {ok, Result};
2494 + {aborted, Reason} ->
2497 + {reply, Reply, State};
2498 +handle_call({get_dates}, _From, #state{vhost=VHost}=State) ->
2499 + {reply, get_dates_int(VHost), State};
2500 +handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) ->
2501 + Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}),
2502 + {reply, {ok, Reply}, State};
2503 +handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) ->
2505 + case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of
2510 + {reply, Reply, State};
2511 +handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) ->
2512 + ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]),
2513 + Reply = mnesia:dirty_write(settings_table(VHost), Set),
2514 + ?MYDEBUG("~p", [Reply]),
2515 + {reply, Reply, State};
2516 +handle_call({drop_user, User}, _From, #state{vhost=VHost}=State) ->
2517 + {ok, Dates} = get_user_stats_int(User, VHost),
2518 + MDResult = lists:map(fun({Date, _}) ->
2519 + delete_all_messages_by_user_at_int(User, VHost, Date)
2521 + SDResult = delete_user_settings_int(User, VHost),
2523 + case lists:all(fun(Result) when Result == ok ->
2525 + (Result) when Result == error ->
2527 + end, lists:append(MDResult, [SDResult])) of
2533 + {reply, Reply, State};
2534 +handle_call({stop}, _From, State) ->
2535 + {stop, normal, ok, State};
2536 +handle_call(Msg, _From, State) ->
2537 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
2540 +handle_cast(Msg, State) ->
2541 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
2544 +handle_info(Info, State) ->
2545 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
2548 +terminate(_Reason, _State) ->
2551 +code_change(_OldVsn, State, _Extra) ->
2554 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2556 +% gen_logdb callbacks
2558 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2559 +log_message(VHost, Msg) ->
2560 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2561 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
2562 +rebuild_stats(VHost) ->
2563 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2564 + gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
2565 +rebuild_stats_at(VHost, Date) ->
2566 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2567 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
2568 +delete_messages_by_user_at(VHost, Msgs, Date) ->
2569 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2570 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
2571 +delete_all_messages_by_user_at(User, VHost, Date) ->
2572 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2573 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
2574 +delete_messages_at(VHost, Date) ->
2575 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2576 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
2577 +get_vhost_stats(VHost) ->
2578 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2579 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
2580 +get_vhost_stats_at(VHost, Date) ->
2581 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2582 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
2583 +get_user_stats(User, VHost) ->
2584 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2585 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
2586 +get_user_messages_at(User, VHost, Date) ->
2587 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2588 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
2589 +get_dates(VHost) ->
2590 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2591 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
2592 +get_user_settings(User, VHost) ->
2593 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2594 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
2595 +get_users_settings(VHost) ->
2596 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2597 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
2598 +set_user_settings(User, VHost, Set) ->
2599 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2600 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
2601 +drop_user(User, VHost) ->
2602 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2603 + gen_server:call(Proc, {drop_user, User}, ?CALL_TIMEOUT).
2605 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2609 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2610 +log_message_int(VHost, #msg{timestamp=Timestamp}=MsgBin) ->
2611 + Date = mod_logdb:convert_timestamp_brief(Timestamp),
2613 + Msg = #msg{timestamp = MsgBin#msg.timestamp,
2614 + owner_name = binary_to_list(MsgBin#msg.owner_name),
2615 + peer_name = binary_to_list(MsgBin#msg.peer_name),
2616 + peer_server = binary_to_list(MsgBin#msg.peer_server),
2617 + peer_resource = binary_to_list(MsgBin#msg.peer_resource),
2618 + direction = MsgBin#msg.direction,
2619 + type = binary_to_list(MsgBin#msg.type),
2620 + subject = binary_to_list(MsgBin#msg.subject),
2621 + body = binary_to_list(MsgBin#msg.body)},
2623 + ATable = table_name(VHost, Date),
2625 + mnesia:write_lock_table(ATable),
2626 + mnesia:write(ATable, Msg, write)
2628 + % log message, increment stats for both users
2629 + case mnesia:transaction(Fun) of
2630 + % if table does not exists - create it and try to log message again
2631 + {aborted,{no_exists, _Table}} ->
2632 + case create_msg_table(VHost, Date) of
2633 + {aborted, CReason} ->
2634 + ?ERROR_MSG("Failed to log message: ~p", [CReason]),
2637 + ?MYDEBUG("Created msg table for ~s at ~s", [VHost, Date]),
2638 + log_message_int(VHost, MsgBin)
2640 + {aborted, TReason} ->
2641 + ?ERROR_MSG("Failed to log message: ~p", [TReason]),
2644 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
2645 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
2646 + increment_user_stats(Msg#msg.owner_name, VHost, Date)
2649 +increment_user_stats(Owner, VHost, Date) ->
2651 + Pat = #stats{user=Owner, at=Date, count='$1'},
2652 + mnesia:write_lock_table(stats_table(VHost)),
2653 + case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of
2655 + mnesia:write(stats_table(VHost),
2656 + #stats{user=Owner,
2661 + mnesia:delete_object(stats_table(VHost),
2662 + #stats{user=Owner,
2664 + count=Stats#stats.count},
2666 + New = Stats#stats{count = Stats#stats.count+1},
2668 + New#stats.count > 0 -> mnesia:write(stats_table(VHost),
2675 + case mnesia:transaction(Fun) of
2676 + {aborted, Reason} ->
2677 + ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]),
2680 + ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]),
2684 +get_dates_int(VHost) ->
2685 + Tables = mnesia:system_info(tables),
2686 + lists:foldl(fun(ATable, Dates) ->
2687 + Table = term_to_binary(ATable),
2688 + case ejabberd_regexp:run( Table, << VHost/binary, <<"$">>/binary >> ) of
2690 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
2691 + {match, [{S, E}]} ->
2692 + lists:append(Dates, [lists:sublist(binary_to_list(Table), S+1, E)]);
2701 +rebuild_stats_at_int(VHost, Date) ->
2702 + Table = table_name(VHost, Date),
2703 + STable = stats_table(VHost),
2704 + CFun = fun(Msg, Stats) ->
2705 + Owner = Msg#msg.owner_name,
2706 + case lists:keysearch(Owner, 1, Stats) of
2707 + {value, {_, Count}} ->
2708 + lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1});
2710 + lists:append(Stats, [{Owner, 1}])
2713 + DFun = fun(#stats{at=SDate} = Stat, _Acc)
2714 + when SDate == Date ->
2715 + mnesia:delete_object(stats_table(VHost), Stat, write);
2716 + (_Stat, _Acc) -> ok
2718 + % TODO: Maybe unregister hooks ?
2719 + case mnesia:transaction(fun() ->
2720 + mnesia:write_lock_table(Table),
2721 + mnesia:write_lock_table(STable),
2722 + % Delete all stats for VHost at Date
2723 + mnesia:foldl(DFun, [], STable),
2724 + % Calc stats for VHost at Date
2725 + case mnesia:foldl(CFun, [], Table) of
2728 + % Write new calc'ed stats
2729 + lists:foreach(fun({Owner, Count}) ->
2730 + WStat = #stats{user=Owner, at=Date, count=Count},
2731 + mnesia:write(stats_table(VHost), WStat, write)
2736 + {aborted, Reason} ->
2737 + ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]),
2741 + {atomic, empty} ->
2742 + {atomic,ok} = mnesia:delete_table(Table),
2743 + ?MYDEBUG("Dropped table at ~p", [Date]),
2747 +delete_nonexistent_stats(VHost) ->
2748 + Dates = get_dates_int(VHost),
2749 + mnesia:transaction(fun() ->
2750 + mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) ->
2751 + case lists:member(Date, Dates) of
2752 + false -> mnesia:delete_object(Stat);
2755 + end, ok, stats_table(VHost))
2758 +delete_stats_by_vhost_at_int(VHost, Date) ->
2759 + StatsDelete = fun(#stats{at=SDate} = Stat, _Acc)
2760 + when SDate == Date ->
2761 + mnesia:delete_object(stats_table(VHost), Stat, write),
2763 + (_Msg, _Acc) -> ok
2765 + case mnesia:transaction(fun() ->
2766 + mnesia:write_lock_table(stats_table(VHost)),
2767 + mnesia:foldl(StatsDelete, ok, stats_table(VHost))
2769 + {aborted, Reason} ->
2770 + ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]),
2771 + rebuild_stats_at_int(VHost, Date);
2773 + ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]),
2777 +get_user_stats_int(User, VHost) ->
2778 + case mnesia:transaction(fun() ->
2779 + Pat = #stats{user=User, at='$1', count='$2'},
2780 + mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
2782 + {atomic, Result} ->
2783 + {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])};
2784 + {aborted, Reason} ->
2788 +delete_all_messages_by_user_at_int(User, VHost, Date) ->
2789 + Table = table_name(VHost, Date),
2790 + MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc)
2791 + when Owner == User ->
2792 + mnesia:delete_object(Table, Msg, write),
2794 + (_Msg, _Acc) -> ok
2796 + DRez = case mnesia:transaction(fun() ->
2797 + mnesia:foldl(MsgDelete, ok, Table)
2799 + {aborted, Reason} ->
2800 + ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]),
2805 + case rebuild_stats_at_int(VHost, Date) of
2812 +delete_user_settings_int(User, VHost) ->
2813 + STable = settings_table(VHost),
2814 + case mnesia:dirty_match_object(STable, #user_settings{owner_name=User, _='_'}) of
2818 + mnesia:dirty_delete_object(STable, UserSettings)
2821 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2825 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2826 +create_stats_table(VHost) ->
2827 + SName = stats_table(VHost),
2828 + case mnesia:create_table(SName,
2829 + [{disc_only_copies, [node()]},
2831 + {attributes, record_info(fields, stats)},
2832 + {record_name, stats}
2835 + ?MYDEBUG("Created stats table for ~p", [VHost]),
2836 + lists:foreach(fun(Date) ->
2837 + rebuild_stats_at_int(VHost, Date)
2838 + end, get_dates_int(VHost)),
2840 + {aborted, {already_exists, _}} ->
2841 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
2843 + {aborted, Reason} ->
2844 + ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
2848 +create_settings_table(VHost) ->
2849 + SName = settings_table(VHost),
2850 + case mnesia:create_table(SName,
2851 + [{disc_copies, [node()]},
2853 + {attributes, record_info(fields, user_settings)},
2854 + {record_name, user_settings}
2857 + ?MYDEBUG("Created settings table for ~p", [VHost]),
2859 + {aborted, {already_exists, _}} ->
2860 + ?MYDEBUG("Settings table for ~p already exists", [VHost]),
2862 + {aborted, Reason} ->
2863 + ?ERROR_MSG("Failed to create settings table: ~p", [Reason]),
2867 +create_msg_table(VHost, Date) ->
2868 + mnesia:create_table(
2869 + table_name(VHost, Date),
2870 + [{disc_only_copies, [node()]},
2872 + {attributes, record_info(fields, msg)},
2873 + {record_name, msg}]).
2874 diff --git a/src/mod_logdb_mysql.erl b/src/mod_logdb_mysql.erl
2875 new file mode 100644
2876 index 00000000..09036211
2878 +++ b/src/mod_logdb_mysql.erl
2880 +%%%----------------------------------------------------------------------
2881 +%%% File : mod_logdb_mysql.erl
2882 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2883 +%%% Purpose : MySQL backend for mod_logdb
2884 +%%% Url : https://paleg.github.io/mod_logdb/
2885 +%%%----------------------------------------------------------------------
2887 +-module(mod_logdb_mysql).
2888 +-author('o.palij@gmail.com').
2890 +-include("mod_logdb.hrl").
2891 +-include("ejabberd.hrl").
2892 +-include("jlib.hrl").
2893 +-include("logger.hrl").
2895 +-behaviour(gen_logdb).
2896 +-behaviour(gen_server).
2899 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2901 +-export([start/2, stop/1]).
2903 +-export([log_message/2,
2905 + rebuild_stats_at/2,
2906 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2907 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2909 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
2912 +% gen_server call timeout
2913 +-define(CALL_TIMEOUT, 30000).
2914 +-define(MYSQL_TIMEOUT, 60000).
2915 +-define(INDEX_SIZE, integer_to_list(170)).
2916 +-define(PROCNAME, mod_logdb_mysql).
2918 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
2919 + list_to_string/1, string_to_list/1,
2920 + convert_timestamp_brief/1]).
2922 +-record(state, {dbref, vhost, server, port, db, user, password}).
2924 +% replace "." with "_"
2925 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
2927 + end, binary_to_list(VHost)).
2932 + "_" ++ escape_vhost(VHost) ++ "`".
2934 +messages_table(VHost, Date) ->
2935 + prefix() ++ "messages_" ++ Date ++ suffix(VHost).
2937 +stats_table(VHost) ->
2938 + prefix() ++ "stats" ++ suffix(VHost).
2940 +temp_table(VHost) ->
2941 + prefix() ++ "temp" ++ suffix(VHost).
2943 +settings_table(VHost) ->
2944 + prefix() ++ "settings" ++ suffix(VHost).
2946 +users_table(VHost) ->
2947 + prefix() ++ "users" ++ suffix(VHost).
2948 +servers_table(VHost) ->
2949 + prefix() ++ "servers" ++ suffix(VHost).
2950 +resources_table(VHost) ->
2951 + prefix() ++ "resources" ++ suffix(VHost).
2953 +ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ binary_to_list(VHost)).
2954 +ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ binary_to_list(VHost)).
2955 +ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ binary_to_list(VHost)).
2957 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2959 +% gen_mod callbacks
2961 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2962 +start(VHost, Opts) ->
2963 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2964 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2967 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2968 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2970 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2972 +% gen_server callbacks
2974 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2975 +init([VHost, Opts]) ->
2978 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
2979 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
2980 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
2981 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
2982 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
2984 + St = #state{vhost=VHost,
2985 + server=Server, port=Port, db=DB,
2986 + user=User, password=Password},
2988 + case open_mysql_connection(St) of
2990 + State = St#state{dbref=DBRef},
2991 + ok = create_stats_table(State),
2992 + ok = create_settings_table(State),
2993 + ok = create_users_table(State),
2994 + % clear ets cache every ...
2995 + timer:send_interval(timer:hours(12), clear_ets_tables),
2996 + ok = create_servers_table(State),
2997 + ok = create_resources_table(State),
2998 + erlang:monitor(process, DBRef),
3000 + {error, Reason} ->
3001 + ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
3002 + {stop, db_connection_failed}
3005 +open_mysql_connection(#state{server=Server, port=Port, db=DB,
3006 + user=DBUser, password=Password} = _State) ->
3007 + LogFun = fun(debug, _Format, _Argument) ->
3008 + %?MYDEBUG(Format, Argument);
3010 + (error, Format, Argument) ->
3011 + ?ERROR_MSG(Format, Argument);
3012 + (Level, Format, Argument) ->
3013 + ?MYDEBUG("MySQL (~p)~n", [Level]),
3014 + ?MYDEBUG(Format, Argument)
3016 + ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
3017 + p1_mysql_conn:start(binary_to_list(Server), Port,
3018 + binary_to_list(DBUser), binary_to_list(Password),
3019 + binary_to_list(DB), LogFun).
3021 +close_mysql_connection(DBRef) ->
3022 + ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
3023 + catch p1_mysql_conn:stop(DBRef).
3025 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3026 + Date = convert_timestamp_brief(Msg#msg.timestamp),
3028 + Table = messages_table(VHost, Date),
3029 + Owner_id = get_user_id(DBRef, VHost, binary_to_list(Msg#msg.owner_name)),
3030 + Peer_name_id = get_user_id(DBRef, VHost, binary_to_list(Msg#msg.peer_name)),
3031 + Peer_server_id = get_server_id(DBRef, VHost, binary_to_list(Msg#msg.peer_server)),
3032 + Peer_resource_id = get_resource_id(DBRef, VHost, binary_to_list(Msg#msg.peer_resource)),
3034 + Query = ["INSERT INTO ",Table," ",
3037 + "peer_server_id,",
3038 + "peer_resource_id,",
3045 + "('", Owner_id, "',",
3046 + "'", Peer_name_id, "',",
3047 + "'", Peer_server_id, "',",
3048 + "'", Peer_resource_id, "',",
3049 + "'", atom_to_list(Msg#msg.direction), "',",
3050 + "'", binary_to_list(Msg#msg.type), "',",
3051 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
3052 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
3053 + "'", Msg#msg.timestamp, "');"],
3056 + case sql_query_internal_silent(DBRef, Query) of
3058 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
3059 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
3060 + increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date);
3061 + {error, Reason} ->
3062 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S02">>) of
3063 + % Table doesn't exist
3065 + case create_msg_table(DBRef, VHost, Date) of
3069 + {updated, _} = sql_query_internal(DBRef, Query),
3070 + increment_user_stats(DBRef, binary_to_list(Msg#msg.owner_name), Owner_id, VHost, Peer_name_id, Peer_server_id, Date)
3073 + ?ERROR_MSG("Failed to log message: ~p", [Reason]),
3077 + {reply, Reply, State};
3078 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3079 + Reply = rebuild_stats_at_int(DBRef, VHost, Date),
3080 + {reply, Reply, State};
3081 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
3082 + {reply, error, State};
3083 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3084 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
3085 + ["\"",Timestamp,"\"",","]
3088 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3090 + Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
3091 + "WHERE timestamp IN (", Temp1],
3094 + case sql_query_internal(DBRef, Query) of
3096 + ?MYDEBUG("Aff=~p", [Aff]),
3097 + rebuild_stats_at_int(DBRef, VHost, Date);
3101 + {reply, Reply, State};
3102 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3103 + ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
3104 + ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
3105 + {reply, ok, State};
3106 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3108 + case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of
3110 + Query = ["DELETE FROM ",stats_table(VHost)," "
3111 + "WHERE at=\"",Date,"\";"],
3112 + case sql_query_internal(DBRef, Query) of
3121 + {reply, Reply, State};
3122 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3123 + SName = stats_table(VHost),
3124 + Query = ["SELECT at, sum(count) ",
3125 + "FROM ",SName," ",
3127 + "ORDER BY DATE(at) DESC;"
3130 + case sql_query_internal(DBRef, Query) of
3132 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
3133 + {error, Reason} ->
3134 + % TODO: Duplicate error message ?
3137 + {reply, Reply, State};
3138 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3139 + SName = stats_table(VHost),
3140 + Query = ["SELECT username, sum(count) AS allcount ",
3141 + "FROM ",SName," ",
3142 + "JOIN ",users_table(VHost)," ON owner_id=user_id "
3143 + "WHERE at=\"",Date,"\" "
3144 + "GROUP BY username ",
3145 + "ORDER BY allcount DESC;"
3148 + case sql_query_internal(DBRef, Query) of
3150 + {ok, lists:reverse(
3152 + [ {User, list_to_integer(Count)} || [User, Count] <- Result]))};
3153 + {error, Reason} ->
3157 + {reply, Reply, State};
3158 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3159 + {reply, get_user_stats_int(DBRef, User, VHost), State};
3160 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3161 + TName = messages_table(VHost, Date),
3162 + UName = users_table(VHost),
3163 + SName = servers_table(VHost),
3164 + RName = resources_table(VHost),
3165 + Query = ["SELECT users.username,",
3166 + "servers.server,",
3167 + "resources.resource,",
3168 + "messages.direction,"
3170 + "messages.subject,"
3172 + "messages.timestamp "
3173 + "FROM ",TName," AS messages "
3174 + "JOIN ",UName," AS users ON peer_name_id=user_id ",
3175 + "JOIN ",SName," AS servers ON peer_server_id=server_id ",
3176 + "JOIN ",RName," AS resources ON peer_resource_id=resource_id ",
3177 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
3178 + "ORDER BY timestamp ASC;"],
3180 + case sql_query_internal(DBRef, Query) of
3182 + Fun = fun([Peer_name, Peer_server, Peer_resource,
3187 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
3188 + direction=list_to_atom(Direction),
3190 + subject=Subject, body=Body,
3191 + timestamp=Timestamp}
3193 + {ok, lists:map(Fun, Result)};
3194 + {error, Reason} ->
3197 + {reply, Reply, State};
3198 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3199 + SName = stats_table(VHost),
3200 + Query = ["SELECT at ",
3201 + "FROM ",SName," ",
3203 + "ORDER BY DATE(at) DESC;"
3206 + case sql_query_internal(DBRef, Query) of
3208 + [ Date || [Date] <- Result ];
3209 + {error, Reason} ->
3212 + {reply, Reply, State};
3213 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3214 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
3215 + "FROM ",settings_table(VHost)," ",
3216 + "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
3218 + case sql_query_internal(DBRef, Query) of
3220 + {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
3221 + #user_settings{owner_name=Owner,
3222 + dolog_default=list_to_bool(DoLogDef),
3223 + dolog_list=string_to_list(DoLogL),
3224 + donotlog_list=string_to_list(DoNotLogL)
3230 + {reply, Reply, State};
3231 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3232 + Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
3233 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"],
3235 + case sql_query_internal(DBRef, Query) of
3238 + {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
3239 + {ok, #user_settings{owner_name=Owner,
3240 + dolog_default=list_to_bool(DoLogDef),
3241 + dolog_list=string_to_list(DoLogL),
3242 + donotlog_list=string_to_list(DoNotLogL)}};
3246 + {reply, Reply, State};
3247 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
3248 + dolog_list=DoLogL,
3249 + donotlog_list=DoNotLogL}},
3250 + _From, #state{dbref=DBRef, vhost=VHost} = State) ->
3251 + User_id = get_user_id(DBRef, VHost, User),
3253 + Query = ["UPDATE ",settings_table(VHost)," ",
3254 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
3255 + "dolog_list='",list_to_string(DoLogL),"', ",
3256 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
3257 + "WHERE owner_id=\"",User_id,"\";"],
3260 + case sql_query_internal(DBRef, Query) of
3262 + IQuery = ["INSERT INTO ",settings_table(VHost)," ",
3263 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
3265 + "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
3266 + case sql_query_internal_silent(DBRef, IQuery) of
3268 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
3270 + {error, Reason} ->
3271 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
3276 + ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
3281 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
3286 + {reply, Reply, State};
3287 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
3288 + ets:delete(ets_users_table(VHost)),
3289 + ets:delete(ets_servers_table(VHost)),
3290 + ?MYDEBUG("Stoping mysql backend for ~p", [VHost]),
3291 + {stop, normal, ok, State};
3292 +handle_call(Msg, _From, State) ->
3293 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
3296 +handle_cast({rebuild_stats}, State) ->
3297 + rebuild_all_stats_int(State),
3299 +handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
3301 + {ok, DBRef} = open_mysql_connection(State),
3302 + {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
3303 + MDResult = lists:map(fun({Date, _}) ->
3304 + delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
3306 + StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
3307 + SDResult = delete_user_settings_int(DBRef, User, VHost),
3308 + case lists:all(fun(Result) when Result == ok ->
3310 + (Result) when Result == error ->
3312 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
3314 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
3316 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
3318 + close_mysql_connection(DBRef)
3322 +handle_cast(Msg, State) ->
3323 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
3326 +handle_info(clear_ets_tables, State) ->
3327 + ets:delete_all_objects(ets_users_table(State#state.vhost)),
3328 + ets:delete_all_objects(ets_resources_table(State#state.vhost)),
3330 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
3331 + {stop, connection_dropped, State};
3332 +handle_info(Info, State) ->
3333 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
3336 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
3337 + close_mysql_connection(DBRef),
3340 +code_change(_OldVsn, State, _Extra) ->
3343 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3345 +% gen_logdb callbacks
3347 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3348 +log_message(VHost, Msg) ->
3349 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3350 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
3351 +rebuild_stats(VHost) ->
3352 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3353 + gen_server:cast(Proc, {rebuild_stats}).
3354 +rebuild_stats_at(VHost, Date) ->
3355 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3356 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
3357 +delete_messages_by_user_at(VHost, Msgs, Date) ->
3358 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3359 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
3360 +delete_all_messages_by_user_at(User, VHost, Date) ->
3361 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3362 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
3363 +delete_messages_at(VHost, Date) ->
3364 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3365 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
3366 +get_vhost_stats(VHost) ->
3367 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3368 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
3369 +get_vhost_stats_at(VHost, Date) ->
3370 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3371 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
3372 +get_user_stats(User, VHost) ->
3373 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3374 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
3375 +get_user_messages_at(User, VHost, Date) ->
3376 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3377 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
3378 +get_dates(VHost) ->
3379 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3380 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
3381 +get_users_settings(VHost) ->
3382 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3383 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
3384 +get_user_settings(User, VHost) ->
3385 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3386 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
3387 +set_user_settings(User, VHost, Set) ->
3388 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3389 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
3390 +drop_user(User, VHost) ->
3391 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3392 + gen_server:cast(Proc, {drop_user, User}).
3394 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3398 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3399 +increment_user_stats(DBRef, User_name, User_id, VHost, PNameID, PServerID, Date) ->
3400 + SName = stats_table(VHost),
3401 + UQuery = ["UPDATE ",SName," ",
3402 + "SET count=count+1 ",
3403 + "WHERE owner_id=\"",User_id,"\" AND peer_name_id=\"",PNameID,"\" AND peer_server_id=\"",PServerID,"\" AND at=\"",Date,"\";"],
3405 + case sql_query_internal(DBRef, UQuery) of
3407 + IQuery = ["INSERT INTO ",SName," ",
3408 + "(owner_id, peer_name_id, peer_server_id, at, count) ",
3410 + "('",User_id,"', '",PNameID,"', '",PServerID,"', '",Date,"', '1');"],
3411 + case sql_query_internal(DBRef, IQuery) of
3413 + ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]),
3419 + ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]),
3425 +get_dates_int(DBRef, VHost) ->
3426 + case sql_query_internal(DBRef, ["SHOW TABLES"]) of
3428 + Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
3429 + lists:foldl(fun([Table], Dates) ->
3430 + case re:run(Table, Reg) of
3432 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
3433 + {match, [{S, E}]} ->
3434 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
3446 +rebuild_all_stats_int(#state{vhost=VHost}=State) ->
3448 + {ok, DBRef} = open_mysql_connection(State),
3449 + ok = delete_nonexistent_stats(DBRef, VHost),
3450 + case lists:filter(fun(Date) ->
3451 + case catch rebuild_stats_at_int(DBRef, VHost, Date) of
3454 + {'EXIT', _} -> true
3456 + end, get_dates_int(DBRef, VHost)) of
3459 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
3462 + close_mysql_connection(DBRef)
3466 +rebuild_stats_at_int(DBRef, VHost, Date) ->
3467 + TempTable = temp_table(VHost),
3469 + Table = messages_table(VHost, Date),
3470 + STable = stats_table(VHost),
3472 + DQuery = [ "DELETE FROM ",STable," ",
3473 + "WHERE at='",Date,"';"],
3475 + ok = create_temp_table(DBRef, TempTable),
3476 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
3477 + SQuery = ["INSERT INTO ",TempTable," ",
3478 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
3479 + "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
3480 + "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
3481 + case sql_query_internal(DBRef, SQuery) of
3483 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
3485 + {data, [["0"]]} ->
3486 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
3487 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]),
3488 + {updated, _} = sql_query_internal(DBRef, DQuery),
3491 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
3495 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
3496 + {updated, _} = sql_query_internal(DBRef, DQuery),
3497 + SQuery1 = ["INSERT INTO ",STable," ",
3498 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
3499 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
3500 + "FROM ",TempTable,";"],
3501 + case sql_query_internal(DBRef, SQuery1) of
3502 + {updated, _} -> ok;
3503 + {error, _} -> error
3505 + {error, _} -> error
3509 + case catch apply(Fun, []) of
3511 + ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
3515 + {'EXIT', Reason} ->
3516 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
3519 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
3520 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
3524 +delete_nonexistent_stats(DBRef, VHost) ->
3525 + Dates = get_dates_int(DBRef, VHost),
3526 + STable = stats_table(VHost),
3528 + Temp = lists:flatmap(fun(Date) ->
3529 + ["\"",Date,"\"",","]
3536 + % replace last "," with ");"
3537 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3538 + Query = ["DELETE FROM ",STable," ",
3539 + "WHERE at NOT IN (", Temp1],
3540 + case sql_query_internal(DBRef, Query) of
3548 +get_user_stats_int(DBRef, User, VHost) ->
3549 + SName = stats_table(VHost),
3550 + Query = ["SELECT at, sum(count) as allcount ",
3551 + "FROM ",SName," ",
3552 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
3554 + "ORDER BY DATE(at) DESC;"
3556 + case sql_query_internal(DBRef, Query) of
3558 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]};
3559 + {error, Result} ->
3563 +delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
3564 + DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
3565 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3566 + case sql_query_internal(DBRef, DQuery) of
3568 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
3574 +delete_all_stats_by_user_int(DBRef, User, VHost) ->
3575 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
3576 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3577 + case sql_query_internal(DBRef, SQuery) of
3579 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
3581 + {error, _} -> error
3584 +delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
3585 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
3586 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
3587 + "AND at=\"",Date,"\";"],
3588 + case sql_query_internal(DBRef, SQuery) of
3590 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
3592 + {error, _} -> error
3595 +delete_user_settings_int(DBRef, User, VHost) ->
3596 + Query = ["DELETE FROM ",settings_table(VHost)," ",
3597 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3598 + case sql_query_internal(DBRef, Query) of
3600 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
3602 + {error, Reason} ->
3603 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
3607 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3611 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3612 +create_temp_table(DBRef, Name) ->
3613 + Query = ["CREATE TABLE ",Name," (",
3614 + "owner_id MEDIUMINT UNSIGNED, ",
3615 + "peer_name_id MEDIUMINT UNSIGNED, ",
3616 + "peer_server_id MEDIUMINT UNSIGNED, ",
3617 + "at VARCHAR(11), ",
3619 + ") ENGINE=MyISAM CHARACTER SET utf8;"
3621 + case sql_query_internal(DBRef, Query) of
3622 + {updated, _} -> ok;
3623 + {error, _Reason} -> error
3626 +create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
3627 + SName = stats_table(VHost),
3628 + Query = ["CREATE TABLE ",SName," (",
3629 + "owner_id MEDIUMINT UNSIGNED, ",
3630 + "peer_name_id MEDIUMINT UNSIGNED, ",
3631 + "peer_server_id MEDIUMINT UNSIGNED, ",
3632 + "at varchar(20), ",
3633 + "count int(11), ",
3634 + "INDEX(owner_id, peer_name_id, peer_server_id), ",
3636 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3638 + case sql_query_internal_silent(DBRef, Query) of
3640 + ?INFO_MSG("Created stats table for ~p", [VHost]),
3641 + rebuild_all_stats_int(State),
3643 + {error, Reason} ->
3644 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
3646 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
3647 + CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
3648 + case sql_query_internal(DBRef, CheckQuery) of
3649 + {data, Elems} when length(Elems) == 2 ->
3650 + ?MYDEBUG("Stats table structure is ok", []),
3653 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
3654 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
3656 + ?INFO_MSG("Successfully dropped ~p", [SName]);
3658 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
3663 + ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
3668 +create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
3669 + SName = settings_table(VHost),
3670 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3671 + "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
3672 + "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
3673 + "dolog_list TEXT, ",
3674 + "donotlog_list TEXT ",
3675 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3677 + case sql_query_internal(DBRef, Query) of
3679 + ?MYDEBUG("Created settings table for ~p", [VHost]),
3685 +create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
3686 + SName = users_table(VHost),
3687 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3688 + "username TEXT NOT NULL, ",
3689 + "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3690 + "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
3691 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3693 + case sql_query_internal(DBRef, Query) of
3695 + ?MYDEBUG("Created users table for ~p", [VHost]),
3696 + ets:new(ets_users_table(VHost), [named_table, set, public]),
3697 + %update_users_from_db(DBRef, VHost),
3703 +create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
3704 + SName = servers_table(VHost),
3705 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3706 + "server TEXT NOT NULL, ",
3707 + "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3708 + "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
3709 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3711 + case sql_query_internal(DBRef, Query) of
3713 + ?MYDEBUG("Created servers table for ~p", [VHost]),
3714 + ets:new(ets_servers_table(VHost), [named_table, set, public]),
3715 + update_servers_from_db(DBRef, VHost),
3721 +create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
3722 + RName = resources_table(VHost),
3723 + Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
3724 + "resource TEXT NOT NULL, ",
3725 + "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3726 + "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
3727 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3729 + case sql_query_internal(DBRef, Query) of
3731 + ?MYDEBUG("Created resources table for ~p", [VHost]),
3732 + ets:new(ets_resources_table(VHost), [named_table, set, public]),
3738 +create_msg_table(DBRef, VHost, Date) ->
3739 + TName = messages_table(VHost, Date),
3740 + Query = ["CREATE TABLE ",TName," (",
3741 + "owner_id MEDIUMINT UNSIGNED, ",
3742 + "peer_name_id MEDIUMINT UNSIGNED, ",
3743 + "peer_server_id MEDIUMINT UNSIGNED, ",
3744 + "peer_resource_id MEDIUMINT(8) UNSIGNED, ",
3745 + "direction ENUM('to', 'from'), ",
3746 + "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ",
3749 + "timestamp DOUBLE, ",
3750 + "INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ",
3751 + "FULLTEXT (body) "
3752 + ") ENGINE=MyISAM CHARACTER SET utf8;"
3754 + case sql_query_internal(DBRef, Query) of
3755 + {updated, _MySQLRes} ->
3756 + ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
3762 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3764 +% internal ets cache (users, servers, resources)
3766 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3767 +update_servers_from_db(DBRef, VHost) ->
3768 + ?INFO_MSG("Reading servers from db for ~p", [VHost]),
3769 + SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"],
3770 + {data, Result} = sql_query_internal(DBRef, SQuery),
3771 + true = ets:delete_all_objects(ets_servers_table(VHost)),
3772 + true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]).
3774 +%update_users_from_db(DBRef, VHost) ->
3775 +% ?INFO_MSG("Reading users from db for ~p", [VHost]),
3776 +% SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"],
3777 +% {data, Result} = sql_query_internal(DBRef, SQuery),
3778 +% true = ets:delete_all_objects(ets_users_table(VHost)),
3779 +% true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]).
3781 +%get_user_name(DBRef, VHost, User_id) ->
3782 +% case ets:match(ets_users_table(VHost), {'$1', User_id}) of
3783 +% [[User]] -> User;
3784 +% % this can be in clustered environment
3786 +% %update_users_from_db(DBRef, VHost),
3787 +% SQuery = ["SELECT username FROM ",users_table(VHost)," ",
3788 +% "WHERE user_id=\"",User_id,"\";"],
3789 +% {data, [[Name]]} = sql_query_internal(DBRef, SQuery),
3790 +% % cache {user, id} pair
3791 +% ets:insert(ets_users_table(VHost), {Name, User_id}),
3795 +%get_server_name(DBRef, VHost, Server_id) ->
3796 +% case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of
3797 +% [[Server]] -> Server;
3798 + % this can be in clustered environment
3800 +% update_servers_from_db(DBRef, VHost),
3801 +% [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}),
3805 +get_user_id_from_db(DBRef, VHost, User) ->
3806 + SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
3807 + "WHERE username=\"",User,"\";"],
3808 + case sql_query_internal(DBRef, SQuery) of
3809 + % no such user in db
3812 + {data, [[DBId]]} ->
3813 + % cache {user, id} pair
3814 + ets:insert(ets_users_table(VHost), {User, DBId}),
3817 +get_user_id(DBRef, VHost, User) ->
3819 + case ets:match(ets_users_table(VHost), {User, '$1'}) of
3822 + case get_user_id_from_db(DBRef, VHost, User) of
3823 + % no such user in db
3825 + IQuery = ["INSERT INTO ",users_table(VHost)," ",
3826 + "SET username=\"",User,"\";"],
3827 + case sql_query_internal_silent(DBRef, IQuery) of
3829 + {ok, NewId} = get_user_id_from_db(DBRef, VHost, User),
3831 + {error, Reason} ->
3832 + % this can be in clustered environment
3833 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3834 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
3835 + {ok, ClID} = get_user_id_from_db(DBRef, VHost, User),
3841 + [[EtsId]] -> EtsId
3844 +get_server_id(DBRef, VHost, Server) ->
3845 + case ets:match(ets_servers_table(VHost), {Server, '$1'}) of
3847 + IQuery = ["INSERT INTO ",servers_table(VHost)," ",
3848 + "SET server=\"",Server,"\";"],
3849 + case sql_query_internal_silent(DBRef, IQuery) of
3851 + SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ",
3852 + "WHERE server=\"",Server,"\";"],
3853 + {data, [[Id]]} = sql_query_internal(DBRef, SQuery),
3854 + ets:insert(ets_servers_table(VHost), {Server, Id}),
3856 + {error, Reason} ->
3857 + % this can be in clustered environment
3858 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3859 + ?ERROR_MSG("Duplicate key name for ~p", [Server]),
3860 + update_servers_from_db(DBRef, VHost),
3861 + [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}),
3867 +get_resource_id_from_db(DBRef, VHost, Resource) ->
3868 + SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ",
3869 + "WHERE resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
3870 + case sql_query_internal(DBRef, SQuery) of
3871 + % no such resource in db
3874 + {data, [[DBId]]} ->
3875 + % cache {resource, id} pair
3876 + ets:insert(ets_resources_table(VHost), {Resource, DBId}),
3879 +get_resource_id(DBRef, VHost, Resource) ->
3881 + case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of
3884 + case get_resource_id_from_db(DBRef, VHost, Resource) of
3885 + % no such resource in db
3887 + IQuery = ["INSERT INTO ",resources_table(VHost)," ",
3888 + "SET resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
3889 + case sql_query_internal_silent(DBRef, IQuery) of
3891 + {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource),
3893 + {error, Reason} ->
3894 + % this can be in clustered environment
3895 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3896 + ?ERROR_MSG("Duplicate key name for ~s", [Resource]),
3897 + {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource),
3903 + [[EtsId]] -> EtsId
3906 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3910 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3911 +sql_query_internal(DBRef, Query) ->
3912 + case sql_query_internal_silent(DBRef, Query) of
3913 + {error, Reason} ->
3914 + ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
3919 +sql_query_internal_silent(DBRef, Query) ->
3920 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
3921 + get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
3923 +get_result({updated, MySQLRes}) ->
3924 + {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
3925 +get_result({data, MySQLRes}) ->
3926 + {data, p1_mysql:get_result_rows(MySQLRes)};
3927 +get_result({error, "query timed out"}) ->
3928 + {error, "query timed out"};
3929 +get_result({error, MySQLRes}) ->
3930 + Reason = p1_mysql:get_result_reason(MySQLRes),
3932 diff --git a/src/mod_logdb_mysql5.erl b/src/mod_logdb_mysql5.erl
3933 new file mode 100644
3934 index 00000000..b6025a3d
3936 +++ b/src/mod_logdb_mysql5.erl
3938 +%%%----------------------------------------------------------------------
3939 +%%% File : mod_logdb_mysql5.erl
3940 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
3941 +%%% Purpose : MySQL 5 backend for mod_logdb
3942 +%%% Url : https://paleg.github.io/mod_logdb/
3943 +%%%----------------------------------------------------------------------
3945 +-module(mod_logdb_mysql5).
3946 +-author('o.palij@gmail.com').
3948 +-include("mod_logdb.hrl").
3949 +-include("ejabberd.hrl").
3950 +-include("jlib.hrl").
3951 +-include("logger.hrl").
3953 +-behaviour(gen_logdb).
3954 +-behaviour(gen_server).
3957 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
3959 +-export([start/2, stop/1]).
3961 +-export([log_message/2,
3963 + rebuild_stats_at/2,
3964 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
3965 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
3967 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
3970 +% gen_server call timeout
3971 +-define(CALL_TIMEOUT, 30000).
3972 +-define(MYSQL_TIMEOUT, 60000).
3973 +-define(INDEX_SIZE, integer_to_list(170)).
3974 +-define(PROCNAME, mod_logdb_mysql5).
3976 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
3977 + list_to_string/1, string_to_list/1,
3978 + convert_timestamp_brief/1]).
3980 +-record(state, {dbref, vhost, server, port, db, user, password}).
3982 +% replace "." with "_"
3983 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
3985 + end, binary_to_list(VHost)).
3990 + "_" ++ escape_vhost(VHost) ++ "`".
3992 +messages_table(VHost, Date) ->
3993 + prefix() ++ "messages_" ++ Date ++ suffix(VHost).
3995 +% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
3996 +view_table(VHost, Date) ->
3997 + Table = messages_table(VHost, Date),
3998 + TablewoQ = lists:sublist(Table, 2, length(Table) - 2),
3999 + lists:append(["`v_", TablewoQ, "`"]).
4001 +stats_table(VHost) ->
4002 + prefix() ++ "stats" ++ suffix(VHost).
4004 +temp_table(VHost) ->
4005 + prefix() ++ "temp" ++ suffix(VHost).
4007 +settings_table(VHost) ->
4008 + prefix() ++ "settings" ++ suffix(VHost).
4010 +users_table(VHost) ->
4011 + prefix() ++ "users" ++ suffix(VHost).
4012 +servers_table(VHost) ->
4013 + prefix() ++ "servers" ++ suffix(VHost).
4014 +resources_table(VHost) ->
4015 + prefix() ++ "resources" ++ suffix(VHost).
4017 +logmessage_name(VHost) ->
4018 + prefix() ++ "logmessage" ++ suffix(VHost).
4020 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4022 +% gen_mod callbacks
4024 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4025 +start(VHost, Opts) ->
4026 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4027 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
4030 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4031 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
4033 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4035 +% gen_server callbacks
4037 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4038 +init([VHost, Opts]) ->
4041 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
4042 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
4043 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
4044 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
4045 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
4047 + St = #state{vhost=VHost,
4048 + server=Server, port=Port, db=DB,
4049 + user=User, password=Password},
4051 + case open_mysql_connection(St) of
4053 + State = St#state{dbref=DBRef},
4054 + ok = create_internals(State),
4055 + ok = create_stats_table(State),
4056 + ok = create_settings_table(State),
4057 + ok = create_users_table(State),
4058 + ok = create_servers_table(State),
4059 + ok = create_resources_table(State),
4060 + erlang:monitor(process, DBRef),
4062 + {error, Reason} ->
4063 + ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
4064 + {stop, db_connection_failed}
4067 +open_mysql_connection(#state{server=Server, port=Port, db=DB,
4068 + user=DBUser, password=Password} = _State) ->
4069 + LogFun = fun(debug, _Format, _Argument) ->
4070 + %?MYDEBUG(Format, Argument);
4072 + (error, Format, Argument) ->
4073 + ?ERROR_MSG(Format, Argument);
4074 + (Level, Format, Argument) ->
4075 + ?MYDEBUG("MySQL (~p)~n", [Level]),
4076 + ?MYDEBUG(Format, Argument)
4078 + ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
4079 + p1_mysql_conn:start(binary_to_list(Server), Port,
4080 + binary_to_list(DBUser), binary_to_list(Password),
4081 + binary_to_list(DB), LogFun).
4083 +close_mysql_connection(DBRef) ->
4084 + ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
4085 + catch p1_mysql_conn:stop(DBRef).
4087 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4088 + Reply = rebuild_stats_at_int(DBRef, VHost, Date),
4089 + {reply, Reply, State};
4090 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
4091 + {reply, error, State};
4092 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4093 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
4094 + ["\"",Timestamp,"\"",","]
4097 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4099 + Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
4100 + "WHERE timestamp IN (", Temp1],
4103 + case sql_query_internal(DBRef, Query) of
4105 + ?MYDEBUG("Aff=~p", [Aff]),
4106 + rebuild_stats_at_int(DBRef, VHost, Date);
4110 + {reply, Reply, State};
4111 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4112 + ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
4113 + ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
4114 + {reply, ok, State};
4115 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4117 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]),
4118 + TQuery = ["DELETE FROM ",stats_table(VHost)," "
4119 + "WHERE at=\"",Date,"\";"],
4120 + {updated, _} = sql_query_internal(DBRef, TQuery),
4121 + VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"],
4122 + {updated, _} = sql_query_internal(DBRef, VQuery),
4126 + case catch apply(Fun, []) of
4132 + {reply, Reply, State};
4133 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4134 + SName = stats_table(VHost),
4135 + Query = ["SELECT at, sum(count) ",
4136 + "FROM ",SName," ",
4138 + "ORDER BY DATE(at) DESC;"
4141 + case sql_query_internal(DBRef, Query) of
4143 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
4144 + {error, Reason} ->
4145 + % TODO: Duplicate error message ?
4148 + {reply, Reply, State};
4149 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4150 + SName = stats_table(VHost),
4151 + Query = ["SELECT username, sum(count) as allcount ",
4152 + "FROM ",SName," ",
4153 + "JOIN ",users_table(VHost)," ON owner_id=user_id "
4154 + "WHERE at=\"",Date,"\" ",
4155 + "GROUP BY username ",
4156 + "ORDER BY allcount DESC;"
4159 + case sql_query_internal(DBRef, Query) of
4161 + {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
4162 + {error, Reason} ->
4165 + {reply, Reply, State};
4166 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4167 + {reply, get_user_stats_int(DBRef, User, VHost), State};
4168 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4169 + Query = ["SELECT peer_name,",
4177 + "FROM ",view_table(VHost, Date)," "
4178 + "WHERE owner_name=\"",User,"\";"],
4180 + case sql_query_internal(DBRef, Query) of
4182 + Fun = fun([Peer_name, Peer_server, Peer_resource,
4187 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
4188 + direction=list_to_atom(Direction),
4190 + subject=Subject, body=Body,
4191 + timestamp=Timestamp}
4193 + {ok, lists:map(Fun, Result)};
4194 + {error, Reason} ->
4197 + {reply, Reply, State};
4198 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4199 + SName = stats_table(VHost),
4200 + Query = ["SELECT at ",
4201 + "FROM ",SName," ",
4203 + "ORDER BY DATE(at) DESC;"
4206 + case sql_query_internal(DBRef, Query) of
4208 + [ Date || [Date] <- Result ];
4209 + {error, Reason} ->
4212 + {reply, Reply, State};
4213 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4214 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
4215 + "FROM ",settings_table(VHost)," ",
4216 + "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
4218 + case sql_query_internal(DBRef, Query) of
4220 + {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
4221 + #user_settings{owner_name=Owner,
4222 + dolog_default=list_to_bool(DoLogDef),
4223 + dolog_list=string_to_list(DoLogL),
4224 + donotlog_list=string_to_list(DoNotLogL)
4230 + {reply, Reply, State};
4231 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4232 + Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
4233 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4235 + case sql_query_internal(DBRef, Query) of
4238 + {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
4239 + {ok, #user_settings{owner_name=Owner,
4240 + dolog_default=list_to_bool(DoLogDef),
4241 + dolog_list=string_to_list(DoLogL),
4242 + donotlog_list=string_to_list(DoNotLogL)}};
4246 + {reply, Reply, State};
4247 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
4248 + dolog_list=DoLogL,
4249 + donotlog_list=DoNotLogL}},
4250 + _From, #state{dbref=DBRef, vhost=VHost} = State) ->
4251 + User_id = get_user_id(DBRef, VHost, User),
4252 + Query = ["UPDATE ",settings_table(VHost)," ",
4253 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
4254 + "dolog_list='",list_to_string(DoLogL),"', ",
4255 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
4256 + "WHERE owner_id=",User_id,";"],
4259 + case sql_query_internal(DBRef, Query) of
4261 + IQuery = ["INSERT INTO ",settings_table(VHost)," ",
4262 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
4264 + "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
4265 + case sql_query_internal_silent(DBRef, IQuery) of
4267 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
4269 + {error, Reason} ->
4270 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
4275 + ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
4280 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
4285 + {reply, Reply, State};
4286 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
4287 + ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]),
4288 + {stop, normal, ok, State};
4289 +handle_call(Msg, _From, State) ->
4290 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
4293 +handle_cast({log_message, Msg}, #state{dbref=DBRef, vhost=VHost}=State) ->
4295 + Date = convert_timestamp_brief(Msg#msg.timestamp),
4296 + TableName = messages_table(VHost, Date),
4298 + Query = [ "CALL ",logmessage_name(VHost)," "
4299 + "('", TableName, "',",
4301 + "'", binary_to_list(Msg#msg.owner_name), "',",
4302 + "'", binary_to_list(Msg#msg.peer_name), "',",
4303 + "'", binary_to_list(Msg#msg.peer_server), "',",
4304 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
4305 + "'", atom_to_list(Msg#msg.direction), "',",
4306 + "'", binary_to_list(Msg#msg.type), "',",
4307 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
4308 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
4309 + "'", Msg#msg.timestamp, "');"],
4311 + case sql_query_internal(DBRef, Query) of
4313 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
4314 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
4316 + {error, _Reason} ->
4322 +handle_cast({rebuild_stats}, State) ->
4323 + rebuild_all_stats_int(State),
4325 +handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
4327 + {ok, DBRef} = open_mysql_connection(State),
4328 + {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
4329 + MDResult = lists:map(fun({Date, _}) ->
4330 + delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
4332 + StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
4333 + SDResult = delete_user_settings_int(DBRef, User, VHost),
4334 + case lists:all(fun(Result) when Result == ok ->
4336 + (Result) when Result == error ->
4338 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
4340 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
4342 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
4344 + close_mysql_connection(DBRef)
4348 +handle_cast(Msg, State) ->
4349 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
4352 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
4353 + {stop, connection_dropped, State};
4354 +handle_info(Info, State) ->
4355 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
4358 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
4359 + close_mysql_connection(DBRef),
4362 +code_change(_OldVsn, State, _Extra) ->
4365 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4367 +% gen_logdb callbacks
4369 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4370 +log_message(VHost, Msg) ->
4371 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4372 + gen_server:cast(Proc, {log_message, Msg}).
4373 +rebuild_stats(VHost) ->
4374 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4375 + gen_server:cast(Proc, {rebuild_stats}).
4376 +rebuild_stats_at(VHost, Date) ->
4377 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4378 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
4379 +delete_messages_by_user_at(VHost, Msgs, Date) ->
4380 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4381 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
4382 +delete_all_messages_by_user_at(User, VHost, Date) ->
4383 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4384 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
4385 +delete_messages_at(VHost, Date) ->
4386 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4387 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
4388 +get_vhost_stats(VHost) ->
4389 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4390 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
4391 +get_vhost_stats_at(VHost, Date) ->
4392 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4393 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
4394 +get_user_stats(User, VHost) ->
4395 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4396 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
4397 +get_user_messages_at(User, VHost, Date) ->
4398 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4399 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
4400 +get_dates(VHost) ->
4401 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4402 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
4403 +get_users_settings(VHost) ->
4404 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4405 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
4406 +get_user_settings(User, VHost) ->
4407 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4408 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
4409 +set_user_settings(User, VHost, Set) ->
4410 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4411 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
4412 +drop_user(User, VHost) ->
4413 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4414 + gen_server:cast(Proc, {drop_user, User}).
4416 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4420 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4421 +get_dates_int(DBRef, VHost) ->
4422 + case sql_query_internal(DBRef, ["SHOW TABLES"]) of
4424 + Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
4425 + lists:foldl(fun([Table], Dates) ->
4426 + case re:run(Table, Reg) of
4428 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
4429 + {match, [{S, E}]} ->
4430 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
4442 +rebuild_all_stats_int(#state{vhost=VHost}=State) ->
4444 + {ok, DBRef} = open_mysql_connection(State),
4445 + ok = delete_nonexistent_stats(DBRef, VHost),
4446 + case lists:filter(fun(Date) ->
4447 + case catch rebuild_stats_at_int(DBRef, VHost, Date) of
4450 + {'EXIT', _} -> true
4452 + end, get_dates_int(DBRef, VHost)) of
4455 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
4458 + close_mysql_connection(DBRef)
4462 +rebuild_stats_at_int(DBRef, VHost, Date) ->
4463 + TempTable = temp_table(VHost),
4465 + Table = messages_table(VHost, Date),
4466 + STable = stats_table(VHost),
4468 + DQuery = [ "DELETE FROM ",STable," ",
4469 + "WHERE at='",Date,"';"],
4471 + ok = create_temp_table(DBRef, TempTable),
4472 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
4473 + SQuery = ["INSERT INTO ",TempTable," ",
4474 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
4475 + "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
4476 + "FROM ",Table," WHERE ext is NULL GROUP BY owner_id,peer_name_id,peer_server_id;"],
4477 + case sql_query_internal(DBRef, SQuery) of
4479 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
4481 + {data, [["0"]]} ->
4482 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
4483 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
4484 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"]),
4485 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
4486 + {updated, _} = sql_query_internal(DBRef, DQuery),
4489 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
4493 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
4494 + {updated, _} = sql_query_internal(DBRef, DQuery),
4495 + SQuery1 = ["INSERT INTO ",STable," ",
4496 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
4497 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
4498 + "FROM ",TempTable,";"],
4499 + case sql_query_internal(DBRef, SQuery1) of
4500 + {updated, _} -> ok;
4501 + {error, _} -> error
4503 + {error, _} -> error
4507 + case catch apply(Fun, []) of
4509 + ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
4513 + {'EXIT', Reason} ->
4514 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
4517 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
4518 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
4521 +delete_nonexistent_stats(DBRef, VHost) ->
4522 + Dates = get_dates_int(DBRef, VHost),
4523 + STable = stats_table(VHost),
4525 + Temp = lists:flatmap(fun(Date) ->
4526 + ["\"",Date,"\"",","]
4532 + % replace last "," with ");"
4533 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4534 + Query = ["DELETE FROM ",STable," ",
4535 + "WHERE at NOT IN (", Temp1],
4536 + case sql_query_internal(DBRef, Query) of
4544 +get_user_stats_int(DBRef, User, VHost) ->
4545 + SName = stats_table(VHost),
4546 + UName = users_table(VHost),
4547 + Query = ["SELECT stats.at, sum(stats.count) ",
4548 + "FROM ",UName," AS users ",
4549 + "JOIN ",SName," AS stats ON owner_id=user_id "
4550 + "WHERE users.username=\"",User,"\" ",
4551 + "GROUP BY stats.at "
4552 + "ORDER BY DATE(stats.at) DESC;"
4554 + case sql_query_internal(DBRef, Query) of
4556 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
4557 + {error, Result} ->
4561 +delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
4562 + DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
4563 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4564 + case sql_query_internal(DBRef, DQuery) of
4566 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
4572 +delete_all_stats_by_user_int(DBRef, User, VHost) ->
4573 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
4574 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4575 + case sql_query_internal(DBRef, SQuery) of
4577 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
4579 + {error, _} -> error
4582 +delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
4583 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
4584 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
4585 + "AND at=\"",Date,"\";"],
4586 + case sql_query_internal(DBRef, SQuery) of
4588 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
4590 + {error, _} -> error
4593 +delete_user_settings_int(DBRef, User, VHost) ->
4594 + Query = ["DELETE FROM ",settings_table(VHost)," ",
4595 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4596 + case sql_query_internal(DBRef, Query) of
4598 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
4600 + {error, Reason} ->
4601 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
4605 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4609 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4610 +create_temp_table(DBRef, Name) ->
4611 + Query = ["CREATE TABLE ",Name," (",
4612 + "owner_id MEDIUMINT UNSIGNED, ",
4613 + "peer_name_id MEDIUMINT UNSIGNED, ",
4614 + "peer_server_id MEDIUMINT UNSIGNED, ",
4615 + "at VARCHAR(11), ",
4617 + ") ENGINE=MyISAM CHARACTER SET utf8;"
4619 + case sql_query_internal(DBRef, Query) of
4620 + {updated, _} -> ok;
4621 + {error, _Reason} -> error
4624 +create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
4625 + SName = stats_table(VHost),
4626 + Query = ["CREATE TABLE ",SName," (",
4627 + "owner_id MEDIUMINT UNSIGNED, ",
4628 + "peer_name_id MEDIUMINT UNSIGNED, ",
4629 + "peer_server_id MEDIUMINT UNSIGNED, ",
4630 + "at VARCHAR(11), ",
4631 + "count INT(11), ",
4632 + "ext INTEGER DEFAULT NULL, "
4633 + "INDEX ext_i (ext), "
4634 + "INDEX(owner_id,peer_name_id,peer_server_id), ",
4636 + ") ENGINE=MyISAM CHARACTER SET utf8;"
4638 + case sql_query_internal_silent(DBRef, Query) of
4640 + ?MYDEBUG("Created stats table for ~p", [VHost]),
4641 + rebuild_all_stats_int(State),
4643 + {error, Reason} ->
4644 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
4646 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
4647 + CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
4648 + case sql_query_internal(DBRef, CheckQuery) of
4649 + {data, Elems} when length(Elems) == 2 ->
4650 + ?MYDEBUG("Stats table structure is ok", []),
4653 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
4654 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
4656 + ?INFO_MSG("Successfully dropped ~p", [SName]);
4658 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
4663 + ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
4668 +create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
4669 + SName = settings_table(VHost),
4670 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4671 + "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
4672 + "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
4673 + "dolog_list TEXT, ",
4674 + "donotlog_list TEXT ",
4675 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4677 + case sql_query_internal(DBRef, Query) of
4679 + ?MYDEBUG("Created settings table for ~p", [VHost]),
4685 +create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
4686 + SName = users_table(VHost),
4687 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4688 + "username TEXT NOT NULL, ",
4689 + "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4690 + "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
4691 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4693 + case sql_query_internal(DBRef, Query) of
4695 + ?MYDEBUG("Created users table for ~p", [VHost]),
4701 +create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
4702 + SName = servers_table(VHost),
4703 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4704 + "server TEXT NOT NULL, ",
4705 + "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4706 + "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
4707 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4709 + case sql_query_internal(DBRef, Query) of
4711 + ?MYDEBUG("Created servers table for ~p", [VHost]),
4717 +create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
4718 + RName = resources_table(VHost),
4719 + Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
4720 + "resource TEXT NOT NULL, ",
4721 + "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4722 + "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
4723 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4725 + case sql_query_internal(DBRef, Query) of
4727 + ?MYDEBUG("Created resources table for ~p", [VHost]),
4733 +create_internals(#state{dbref=DBRef, vhost=VHost}) ->
4734 + sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS ",logmessage_name(VHost),";"]),
4735 + case sql_query_internal(DBRef, [get_logmessage(VHost)]) of
4737 + ?MYDEBUG("Created logmessage for ~p", [VHost]),
4743 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4747 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4748 +sql_query_internal(DBRef, Query) ->
4749 + case sql_query_internal_silent(DBRef, Query) of
4750 + {error, Reason} ->
4751 + ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
4756 +sql_query_internal_silent(DBRef, Query) ->
4757 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
4758 + get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
4760 +get_result({updated, MySQLRes}) ->
4761 + {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
4762 +get_result({data, MySQLRes}) ->
4763 + {data, p1_mysql:get_result_rows(MySQLRes)};
4764 +get_result({error, "query timed out"}) ->
4765 + {error, "query timed out"};
4766 +get_result({error, MySQLRes}) ->
4767 + Reason = p1_mysql:get_result_reason(MySQLRes),
4770 +get_user_id(DBRef, VHost, User) ->
4771 + SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
4772 + "WHERE username=\"",User,"\";"],
4773 + case sql_query_internal(DBRef, SQuery) of
4775 + IQuery = ["INSERT INTO ",users_table(VHost)," ",
4776 + "SET username=\"",User,"\";"],
4777 + case sql_query_internal_silent(DBRef, IQuery) of
4779 + {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery),
4781 + {error, Reason} ->
4782 + % this can be in clustered environment
4783 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
4784 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
4785 + {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
4788 + {data, [[DBId]]} ->
4792 +get_logmessage(VHost) ->
4793 + UName = users_table(VHost),
4794 + SName = servers_table(VHost),
4795 + RName = resources_table(VHost),
4796 + StName = stats_table(VHost),
4798 +CREATE PROCEDURE ~s(tablename TEXT, atdate TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(10), msubject TEXT, mbody TEXT, mtimestamp DOUBLE)
4800 + DECLARE ownerID MEDIUMINT UNSIGNED;
4801 + DECLARE peer_nameID MEDIUMINT UNSIGNED;
4802 + DECLARE peer_serverID MEDIUMINT UNSIGNED;
4803 + DECLARE peer_resourceID MEDIUMINT UNSIGNED;
4804 + DECLARE Vmtype VARCHAR(10);
4805 + DECLARE Vmtimestamp DOUBLE;
4806 + DECLARE Vmdirection VARCHAR(4);
4807 + DECLARE Vmbody TEXT;
4808 + DECLARE Vmsubject TEXT;
4811 + DECLARE viewname TEXT;
4812 + DECLARE notable INT;
4813 + DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1;
4816 + SET @ownerID = NULL;
4817 + SET @peer_nameID = NULL;
4818 + SET @peer_serverID = NULL;
4819 + SET @peer_resourceID = NULL;
4821 + SET @Vmtype = mtype;
4822 + SET @Vmtimestamp = mtimestamp;
4823 + SET @Vmdirection = mdirection;
4824 + SET @Vmbody = mbody;
4825 + SET @Vmsubject = msubject;
4827 + SELECT user_id INTO @ownerID FROM ~s WHERE username=owner;
4828 + IF @ownerID IS NULL THEN
4829 + INSERT INTO ~s SET username=owner;
4830 + SET @ownerID = LAST_INSERT_ID();
4833 + SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name;
4834 + IF @peer_nameID IS NULL THEN
4835 + INSERT INTO ~s SET username=peer_name;
4836 + SET @peer_nameID = LAST_INSERT_ID();
4839 + SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server;
4840 + IF @peer_serverID IS NULL THEN
4841 + INSERT INTO ~s SET server=peer_server;
4842 + SET @peer_serverID = LAST_INSERT_ID();
4845 + SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource;
4846 + IF @peer_resourceID IS NULL THEN
4847 + INSERT INTO ~s SET resource=peer_resource;
4848 + SET @peer_resourceID = LAST_INSERT_ID();
4851 + SET @iq = CONCAT(\"INSERT INTO \",tablename,\" (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (@ownerID,@peer_nameID,@peer_serverID,@peer_resourceID,@Vmdirection,@Vmtype,@Vmsubject,@Vmbody,@Vmtimestamp);\");
4852 + PREPARE insertmsg FROM @iq;
4854 + IF @notable = 1 THEN
4855 + SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" (
4856 + owner_id MEDIUMINT UNSIGNED NOT NULL,
4857 + peer_name_id MEDIUMINT UNSIGNED NOT NULL,
4858 + peer_server_id MEDIUMINT UNSIGNED NOT NULL,
4859 + peer_resource_id MEDIUMINT(8) UNSIGNED NOT NULL,
4860 + direction ENUM('to', 'from') NOT NULL,
4861 + type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
4864 + timestamp DOUBLE NOT NULL,
4865 + ext INTEGER DEFAULT NULL,
4866 + INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id),
4867 + INDEX ext_i (ext),
4871 + CHARACTER SET utf8;\");
4872 + PREPARE createtable FROM @cq;
4873 + EXECUTE createtable;
4874 + DEALLOCATE PREPARE createtable;
4876 + SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\");
4877 + SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS
4878 + SELECT owner.username AS owner_name,
4879 + peer.username AS peer_name,
4880 + servers.server AS peer_server,
4881 + resources.resource AS peer_resource,
4882 + messages.direction,
4886 + messages.timestamp
4892 + \", tablename,\" messages
4894 + owner.user_id=messages.owner_id and
4895 + peer.user_id=messages.peer_name_id and
4896 + servers.server_id=messages.peer_server_id and
4897 + resources.resource_id=messages.peer_resource_id
4898 + ORDER BY messages.timestamp;\");
4899 + PREPARE createview FROM @cq;
4900 + EXECUTE createview;
4901 + DEALLOCATE PREPARE createview;
4904 + PREPARE insertmsg FROM @iq;
4905 + EXECUTE insertmsg;
4906 + ELSEIF @notable = 0 THEN
4907 + EXECUTE insertmsg;
4910 + DEALLOCATE PREPARE insertmsg;
4912 + IF @notable = 0 THEN
4913 + UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND peer_name_id=@peer_nameID AND peer_server_id=@peer_serverID AND at=atdate;
4914 + IF ROW_COUNT() = 0 THEN
4915 + INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (@ownerID, @peer_nameID, @peer_serverID, atdate, 1);
4918 +END;", [logmessage_name(VHost),UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
4919 diff --git a/src/mod_logdb_pgsql.erl b/src/mod_logdb_pgsql.erl
4920 new file mode 100644
4921 index 00000000..61a71fff
4923 +++ b/src/mod_logdb_pgsql.erl
4925 +% {ok, DBRef} = pgsql:connect([{host, "127.0.0.1"}, {database, "logdb"}, {user, "logdb"}, {password, "logdb"}, {port, 5432}, {as_binary, true}]).
4927 +% pgsql:squery(DBRef, "CREATE TABLE test.\"logdb_stats_test\" (owner_id INTEGER, peer_name_id INTEGER, peer_server_id INTEGER, at VARCHAR(20), count integer);" ).
4928 +%%%----------------------------------------------------------------------
4929 +%%% File : mod_logdb_pgsql.erl
4930 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
4931 +%%% Purpose : Posgresql backend for mod_logdb
4932 +%%% Url : https://paleg.github.io/mod_logdb/
4933 +%%%----------------------------------------------------------------------
4935 +-module(mod_logdb_pgsql).
4936 +-author('o.palij@gmail.com').
4938 +-include("mod_logdb.hrl").
4939 +-include("ejabberd.hrl").
4940 +-include("jlib.hrl").
4941 +-include("logger.hrl").
4943 +-behaviour(gen_logdb).
4944 +-behaviour(gen_server).
4947 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
4949 +-export([start/2, stop/1]).
4951 +-export([log_message/2,
4953 + rebuild_stats_at/2,
4954 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
4955 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
4957 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
4960 +-export([view_table/3]).
4962 +% gen_server call timeout
4963 +-define(CALL_TIMEOUT, 30000).
4964 +-define(PGSQL_TIMEOUT, 60000).
4965 +-define(PROCNAME, mod_logdb_pgsql).
4967 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
4968 + list_to_string/1, string_to_list/1,
4969 + convert_timestamp_brief/1]).
4971 +-record(state, {dbref, vhost, server, port, db, user, password, schema}).
4973 +% replace "." with "_"
4974 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4976 + end, binary_to_list(VHost)).
4979 + Schema ++ ".\"" ++ "logdb_".
4982 + "_" ++ escape_vhost(VHost) ++ "\"".
4984 +messages_table(VHost, Schema, Date) ->
4985 + prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost).
4987 +view_table(VHost, Schema, Date) ->
4988 + Table = messages_table(VHost, Schema, Date),
4989 + TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3),
4990 + lists:append([Schema, ".\"v_", TablewoS, "\""]).
4992 +stats_table(VHost, Schema) ->
4993 + prefix(Schema) ++ "stats" ++ suffix(VHost).
4995 +temp_table(VHost, Schema) ->
4996 + prefix(Schema) ++ "temp" ++ suffix(VHost).
4998 +settings_table(VHost, Schema) ->
4999 + prefix(Schema) ++ "settings" ++ suffix(VHost).
5001 +users_table(VHost, Schema) ->
5002 + prefix(Schema) ++ "users" ++ suffix(VHost).
5003 +servers_table(VHost, Schema) ->
5004 + prefix(Schema) ++ "servers" ++ suffix(VHost).
5005 +resources_table(VHost, Schema) ->
5006 + prefix(Schema) ++ "resources" ++ suffix(VHost).
5008 +logmessage_name(VHost, Schema) ->
5009 + prefix(Schema) ++ "logmessage" ++ suffix(VHost).
5011 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5013 +% gen_mod callbacks
5015 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5016 +start(VHost, Opts) ->
5017 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5018 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
5021 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5022 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
5024 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5026 +% gen_server callbacks
5028 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5029 +init([VHost, Opts]) ->
5030 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
5031 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"ejabberd_logdb">>),
5032 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
5033 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 5432),
5034 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
5035 + Schema = binary_to_list(gen_mod:get_opt(schema, Opts, fun(A) -> A end, <<"public">>)),
5037 + ?MYDEBUG("Starting pgsql backend for ~s", [VHost]),
5039 + St = #state{vhost=VHost,
5040 + server=Server, port=Port, db=DB,
5041 + user=User, password=Password,
5044 + case open_pgsql_connection(St) of
5046 + State = St#state{dbref=DBRef},
5047 + ok = create_internals(State),
5048 + ok = create_stats_table(State),
5049 + ok = create_settings_table(State),
5050 + ok = create_users_table(State),
5051 + ok = create_servers_table(State),
5052 + ok = create_resources_table(State),
5053 + erlang:monitor(process, DBRef),
5055 + % this does not work
5056 + {error, Reason} ->
5057 + ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]),
5058 + {stop, db_connection_failed};
5059 + % and this too, becouse pgsql_conn do exit() which can not be catched
5061 + ?ERROR_MSG("Rez: ~p~n", [Rez]),
5062 + {stop, db_connection_failed}
5065 +open_pgsql_connection(#state{server=Server, port=Port, db=DB, schema=Schema,
5066 + user=User, password=Password} = _State) ->
5067 + ?INFO_MSG("Opening pgsql connection ~s@~s:~p/~s", [User, Server, Port, DB]),
5068 + {ok, DBRef} = pgsql:connect(Server, DB, User, Password, Port),
5069 + {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]),
5072 +close_pgsql_connection(DBRef) ->
5073 + ?MYDEBUG("Closing ~p pgsql connection", [DBRef]),
5074 + pgsql:terminate(DBRef).
5076 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5077 + Date = convert_timestamp_brief(Msg#msg.timestamp),
5078 + TableName = messages_table(VHost, Schema, Date),
5079 + ViewName = view_table(VHost, Schema, Date),
5081 + Query = [ "SELECT ", logmessage_name(VHost, Schema)," "
5082 + "('", TableName, "',",
5083 + "'", ViewName, "',",
5085 + "'", binary_to_list(Msg#msg.owner_name), "',",
5086 + "'", binary_to_list(Msg#msg.peer_name), "',",
5087 + "'", binary_to_list(Msg#msg.peer_server), "',",
5088 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
5089 + "'", atom_to_list(Msg#msg.direction), "',",
5090 + "'", binary_to_list(Msg#msg.type), "',",
5091 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
5092 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
5093 + "'", Msg#msg.timestamp, "');"],
5095 + case sql_query_internal_silent(DBRef, Query) of
5096 + % TODO: change this
5097 + {data, [{"0"}]} ->
5098 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
5099 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
5101 + {error, _Reason} ->
5104 + {reply, ok, State};
5105 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5106 + Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date),
5107 + {reply, Reply, State};
5108 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
5109 + {reply, error, State};
5110 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5111 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
5112 + ["'",Timestamp,"'",","]
5115 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
5117 + Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
5118 + "WHERE timestamp IN (", Temp1],
5121 + case sql_query_internal(DBRef, Query) of
5123 + rebuild_stats_at_int(DBRef, VHost, Schema, Date);
5127 + {reply, Reply, State};
5128 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5129 + ok = delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date),
5130 + ok = delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date),
5131 + {reply, ok, State};
5132 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5133 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5135 + case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date)," CASCADE;"]) of
5137 + Query = ["DELETE FROM ",stats_table(VHost, Schema)," "
5138 + "WHERE at='",Date,"';"],
5139 + case sql_query_internal(DBRef, Query) of
5148 + {reply, Reply, State};
5149 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5150 + SName = stats_table(VHost, Schema),
5151 + Query = ["SELECT at, sum(count) ",
5152 + "FROM ",SName," ",
5154 + "ORDER BY DATE(at) DESC;"
5157 + case sql_query_internal(DBRef, Query) of
5159 + {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]};
5160 + {error, Reason} ->
5161 + % TODO: Duplicate error message ?
5164 + {reply, Reply, State};
5165 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5166 + SName = stats_table(VHost, Schema),
5167 + Query = ["SELECT username, sum(count) AS allcount ",
5168 + "FROM ",SName," ",
5169 + "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id ",
5170 + "WHERE at='",Date,"' ",
5171 + "GROUP BY username ",
5172 + "ORDER BY allcount DESC;"
5175 + case sql_query_internal(DBRef, Query) of
5177 + RFun = fun({User, Count}) ->
5178 + {User, list_to_integer(Count)}
5180 + {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))};
5181 + {error, Reason} ->
5185 + {reply, Reply, State};
5186 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5187 + {reply, get_user_stats_int(DBRef, Schema, User, VHost), State};
5188 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5189 + Query = ["SELECT peer_name,",
5197 + "FROM ",view_table(VHost, Schema, Date)," "
5198 + "WHERE owner_name='",User,"';"],
5200 + case sql_query_internal(DBRef, Query) of
5202 + Fun = fun({Peer_name, Peer_server, Peer_resource,
5207 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
5208 + direction=list_to_atom(Direction),
5210 + subject=Subject, body=Body,
5211 + timestamp=Timestamp}
5213 + {ok, lists:map(Fun, Recs)};
5214 + {error, Reason} ->
5217 + {reply, Reply, State};
5218 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5219 + SName = stats_table(VHost, Schema),
5220 + Query = ["SELECT at ",
5221 + "FROM ",SName," ",
5223 + "ORDER BY at DESC;"
5226 + case sql_query_internal(DBRef, Query) of
5228 + [ Date || {Date} <- Result ];
5229 + {error, Reason} ->
5232 + {reply, Reply, State};
5233 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5234 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
5235 + "FROM ",settings_table(VHost, Schema)," ",
5236 + "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"],
5238 + case sql_query_internal(DBRef, Query) of
5240 + {ok, [#user_settings{owner_name=Owner,
5241 + dolog_default=list_to_bool(DoLogDef),
5242 + dolog_list=string_to_list(DoLogL),
5243 + donotlog_list=string_to_list(DoNotLogL)
5244 + } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]};
5245 + {error, Reason} ->
5248 + {reply, Reply, State};
5249 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5250 + Query = ["SELECT dolog_default,dolog_list,donotlog_list ",
5251 + "FROM ",settings_table(VHost, Schema)," ",
5252 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5254 + case sql_query_internal_silent(DBRef, Query) of
5257 + {data, [{DoLogDef, DoLogL, DoNotLogL}]} ->
5258 + {ok, #user_settings{owner_name=User,
5259 + dolog_default=list_to_bool(DoLogDef),
5260 + dolog_list=string_to_list(DoLogL),
5261 + donotlog_list=string_to_list(DoNotLogL)}};
5262 + {error, Reason} ->
5263 + ?ERROR_MSG("Failed to get_user_settings for ~s@~s: ~p", [User, VHost, Reason]),
5266 + {reply, Reply, State};
5267 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
5268 + dolog_list=DoLogL,
5269 + donotlog_list=DoNotLogL}},
5270 + _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5271 + User_id = get_user_id(DBRef, VHost, Schema, User),
5272 + Query = ["UPDATE ",settings_table(VHost, Schema)," ",
5273 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
5274 + "dolog_list='",list_to_string(DoLogL),"', ",
5275 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
5276 + "WHERE owner_id=",User_id,";"],
5279 + case sql_query_internal(DBRef, Query) of
5281 + IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ",
5282 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
5284 + "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
5285 + case sql_query_internal(DBRef, IQuery) of
5287 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
5293 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
5298 + {reply, Reply, State};
5299 +handle_call({stop}, _From, State) ->
5300 + ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]),
5301 + {stop, normal, ok, State};
5302 +handle_call(Msg, _From, State) ->
5303 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
5307 +handle_cast({rebuild_stats}, State) ->
5308 + rebuild_all_stats_int(State),
5310 +handle_cast({drop_user, User}, #state{vhost=VHost, schema=Schema}=State) ->
5312 + {ok, DBRef} = open_pgsql_connection(State),
5313 + {ok, Dates} = get_user_stats_int(DBRef, Schema, User, VHost),
5314 + MDResult = lists:map(fun({Date, _}) ->
5315 + delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date)
5317 + StDResult = delete_all_stats_by_user_int(DBRef, Schema, User, VHost),
5318 + SDResult = delete_user_settings_int(DBRef, Schema, User, VHost),
5319 + case lists:all(fun(Result) when Result == ok ->
5321 + (Result) when Result == error ->
5323 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
5325 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
5327 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
5329 + close_pgsql_connection(DBRef)
5333 +handle_cast(Msg, State) ->
5334 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
5337 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
5338 + {stop, connection_dropped, State};
5339 +handle_info(Info, State) ->
5340 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
5343 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
5344 + close_pgsql_connection(DBRef),
5347 +code_change(_OldVsn, State, _Extra) ->
5350 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5352 +% gen_logdb callbacks
5354 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5355 +log_message(VHost, Msg) ->
5356 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5357 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
5358 +rebuild_stats(VHost) ->
5359 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5360 + gen_server:cast(Proc, {rebuild_stats}).
5361 +rebuild_stats_at(VHost, Date) ->
5362 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5363 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
5364 +delete_messages_by_user_at(VHost, Msgs, Date) ->
5365 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5366 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
5367 +delete_all_messages_by_user_at(User, VHost, Date) ->
5368 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5369 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
5370 +delete_messages_at(VHost, Date) ->
5371 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5372 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
5373 +get_vhost_stats(VHost) ->
5374 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5375 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
5376 +get_vhost_stats_at(VHost, Date) ->
5377 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5378 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
5379 +get_user_stats(User, VHost) ->
5380 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5381 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
5382 +get_user_messages_at(User, VHost, Date) ->
5383 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5384 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
5385 +get_dates(VHost) ->
5386 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5387 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
5388 +get_users_settings(VHost) ->
5389 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5390 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
5391 +get_user_settings(User, VHost) ->
5392 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5393 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
5394 +set_user_settings(User, VHost, Set) ->
5395 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5396 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
5397 +drop_user(User, VHost) ->
5398 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5399 + gen_server:cast(Proc, {drop_user, User}).
5401 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5405 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5406 +get_dates_int(DBRef, VHost) ->
5407 + Query = ["SELECT n.nspname as \"Schema\",
5408 + c.relname as \"Name\",
5409 + CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as \"Type\",
5410 + r.rolname as \"Owner\"
5411 + FROM pg_catalog.pg_class c
5412 + JOIN pg_catalog.pg_roles r ON r.oid = c.relowner
5413 + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
5414 + WHERE c.relkind IN ('r','')
5415 + AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
5416 + AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$'
5417 + AND pg_catalog.pg_table_is_visible(c.oid)
5419 + case sql_query_internal(DBRef, Query) of
5421 + lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) ->
5422 + case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
5423 + {match, [{S, E}]} ->
5424 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
5433 +rebuild_all_stats_int(#state{vhost=VHost, schema=Schema}=State) ->
5435 + {ok, DBRef} = open_pgsql_connection(State),
5436 + ok = delete_nonexistent_stats(DBRef, Schema, VHost),
5437 + case lists:filter(fun(Date) ->
5438 + case catch rebuild_stats_at_int(DBRef, VHost, Schema, Date) of
5441 + {'EXIT', _} -> true
5443 + end, get_dates_int(DBRef, VHost)) of
5446 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
5449 + close_pgsql_connection(DBRef)
5453 +rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
5454 + TempTable = temp_table(VHost, Schema),
5457 + Table = messages_table(VHost, Schema, Date),
5458 + STable = stats_table(VHost, Schema),
5460 + DQuery = [ "DELETE FROM ",STable," ",
5461 + "WHERE at='",Date,"';"],
5463 + ok = create_temp_table(DBRef, VHost, Schema),
5464 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]),
5465 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
5466 + SQuery = ["INSERT INTO ",TempTable," ",
5467 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
5468 + "SELECT owner_id,peer_name_id,peer_server_id,'",Date,"'",",count(*) ",
5469 + "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
5470 + case sql_query_internal(DBRef, SQuery) of
5472 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
5474 + {data, [{"0"}]} ->
5475 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5476 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table," CASCADE;"]),
5477 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
5478 + {updated, _} = sql_query_internal(DBRef, DQuery),
5481 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
5485 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
5486 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
5487 + {updated, _} = sql_query_internal(DBRef, DQuery),
5488 + SQuery1 = ["INSERT INTO ",STable," ",
5489 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
5490 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
5491 + "FROM ",TempTable,";"],
5492 + case sql_query_internal(DBRef, SQuery1) of
5493 + {updated, _} -> ok;
5494 + {error, _} -> error
5496 + {error, _} -> error
5500 + case sql_transaction_internal(DBRef, Fun) of
5502 + ?INFO_MSG("Rebuilded stats for ~s at ~s", [VHost, Date]),
5504 + {aborted, Reason} ->
5505 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
5508 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
5511 +delete_nonexistent_stats(DBRef, Schema, VHost) ->
5512 + Dates = get_dates_int(DBRef, VHost),
5513 + STable = stats_table(VHost, Schema),
5515 + Temp = lists:flatmap(fun(Date) ->
5516 + ["'",Date,"'",","]
5523 + % replace last "," with ");"
5524 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
5525 + Query = ["DELETE FROM ",STable," ",
5526 + "WHERE at NOT IN (", Temp1],
5527 + case sql_query_internal(DBRef, Query) of
5535 +get_user_stats_int(DBRef, Schema, User, VHost) ->
5536 + SName = stats_table(VHost, Schema),
5537 + UName = users_table(VHost, Schema),
5538 + Query = ["SELECT stats.at, sum(stats.count) ",
5539 + "FROM ",UName," AS users ",
5540 + "JOIN ",SName," AS stats ON owner_id=user_id "
5541 + "WHERE users.username='",User,"' ",
5542 + "GROUP BY stats.at "
5543 + "ORDER BY DATE(at) DESC;"
5545 + case sql_query_internal(DBRef, Query) of
5547 + {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]};
5548 + {error, Result} ->
5552 +delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
5553 + DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
5554 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5555 + case sql_query_internal(DBRef, DQuery) of
5557 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
5563 +delete_all_stats_by_user_int(DBRef, Schema, User, VHost) ->
5564 + SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
5565 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5566 + case sql_query_internal(DBRef, SQuery) of
5568 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
5570 + {error, _} -> error
5573 +delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
5574 + SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
5575 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ",
5576 + "AND at='",Date,"';"],
5577 + case sql_query_internal(DBRef, SQuery) of
5579 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
5581 + {error, _} -> error
5584 +delete_user_settings_int(DBRef, Schema, User, VHost) ->
5585 + Query = ["DELETE FROM ",settings_table(VHost, Schema)," ",
5586 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5587 + case sql_query_internal(DBRef, Query) of
5589 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
5591 + {error, Reason} ->
5592 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
5596 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5600 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5601 +create_temp_table(DBRef, VHost, Schema) ->
5602 + TName = temp_table(VHost, Schema),
5603 + Query = ["CREATE TABLE ",TName," (",
5604 + "owner_id INTEGER, ",
5605 + "peer_name_id INTEGER, ",
5606 + "peer_server_id INTEGER, ",
5607 + "at VARCHAR(20), ",
5611 + case sql_query_internal(DBRef, Query) of
5612 + {updated, _} -> ok;
5613 + {error, _Reason} -> error
5616 +create_stats_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5617 + SName = stats_table(VHost, Schema),
5621 + Query = ["CREATE TABLE ",SName," (",
5622 + "owner_id INTEGER, ",
5623 + "peer_name_id INTEGER, ",
5624 + "peer_server_id INTEGER, ",
5625 + "at VARCHAR(20), ",
5629 + case sql_query_internal_silent(DBRef, Query) of
5631 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_search_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id, peer_name_id, peer_server_id);"]),
5632 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]),
5634 + {error, Reason} ->
5635 + case lists:keysearch(code, 1, Reason) of
5636 + {value, {code, "42P07"}} ->
5639 + ?ERROR_MSG("Failed to create stats table for ~s: ~p", [VHost, Reason]),
5644 + case sql_transaction_internal(DBRef, Fun) of
5645 + {atomic, created} ->
5646 + ?MYDEBUG("Created stats table for ~s", [VHost]),
5647 + rebuild_all_stats_int(State),
5649 + {atomic, exists} ->
5650 + ?MYDEBUG("Stats table for ~s already exists", [VHost]),
5651 + {match, [{F, L}]} = re:run(SName, "\".*\""),
5652 + QTable = lists:sublist(SName, F+2, L-2),
5653 + OIDQuery = ["SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname='",QTable,"' AND pg_catalog.pg_table_is_visible(c.oid);"],
5654 + {data,[{OID}]} = sql_query_internal(DBRef, OIDQuery),
5655 + CheckQuery = ["SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = '",OID,"' AND a.attnum > 0 AND NOT a.attisdropped AND a.attname ~ '^peer_.*_id$';"],
5656 + case sql_query_internal(DBRef, CheckQuery) of
5657 + {data, Elems} when length(Elems) == 2 ->
5658 + ?MYDEBUG("Stats table structure is ok", []),
5661 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
5662 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
5664 + ?INFO_MSG("Successfully dropped ~p", [SName]);
5666 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
5670 + {error, _} -> error
5673 +create_settings_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5674 + SName = settings_table(VHost, Schema),
5675 + Query = ["CREATE TABLE ",SName," (",
5676 + "owner_id INTEGER PRIMARY KEY, ",
5677 + "dolog_default BOOLEAN, ",
5678 + "dolog_list TEXT DEFAULT '', ",
5679 + "donotlog_list TEXT DEFAULT ''",
5682 + case sql_query_internal_silent(DBRef, Query) of
5684 + ?MYDEBUG("Created settings table for ~s", [VHost]),
5686 + {error, Reason} ->
5687 + case lists:keysearch(code, 1, Reason) of
5688 + {value, {code, "42P07"}} ->
5689 + ?MYDEBUG("Settings table for ~s already exists", [VHost]),
5692 + ?ERROR_MSG("Failed to create settings table for ~s: ~p", [VHost, Reason]),
5697 +create_users_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5698 + SName = users_table(VHost, Schema),
5702 + Query = ["CREATE TABLE ",SName," (",
5703 + "username TEXT UNIQUE, ",
5704 + "user_id SERIAL PRIMARY KEY",
5707 + case sql_query_internal_silent(DBRef, Query) of
5709 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]),
5711 + {error, Reason} ->
5712 + case lists:keysearch(code, 1, Reason) of
5713 + {value, {code, "42P07"}} ->
5716 + ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
5721 + case sql_transaction_internal(DBRef, Fun) of
5722 + {atomic, created} ->
5723 + ?MYDEBUG("Created users table for ~s", [VHost]),
5725 + {atomic, exists} ->
5726 + ?MYDEBUG("Users table for ~s already exists", [VHost]),
5728 + {aborted, _} -> error
5731 +create_servers_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5732 + SName = servers_table(VHost, Schema),
5735 + Query = ["CREATE TABLE ",SName," (",
5736 + "server TEXT UNIQUE, ",
5737 + "server_id SERIAL PRIMARY KEY",
5740 + case sql_query_internal_silent(DBRef, Query) of
5742 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]),
5744 + {error, Reason} ->
5745 + case lists:keysearch(code, 1, Reason) of
5746 + {value, {code, "42P07"}} ->
5749 + ?ERROR_MSG("Failed to create servers table for ~s: ~p", [VHost, Reason]),
5754 + case sql_transaction_internal(DBRef, Fun) of
5755 + {atomic, created} ->
5756 + ?MYDEBUG("Created servers table for ~s", [VHost]),
5758 + {atomic, exists} ->
5759 + ?MYDEBUG("Servers table for ~s already exists", [VHost]),
5761 + {aborted, _} -> error
5764 +create_resources_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5765 + RName = resources_table(VHost, Schema),
5767 + Query = ["CREATE TABLE ",RName," (",
5768 + "resource TEXT UNIQUE, ",
5769 + "resource_id SERIAL PRIMARY KEY",
5772 + case sql_query_internal_silent(DBRef, Query) of
5774 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]),
5776 + {error, Reason} ->
5777 + case lists:keysearch(code, 1, Reason) of
5778 + {value, {code, "42P07"}} ->
5781 + ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
5786 + case sql_transaction_internal(DBRef, Fun) of
5787 + {atomic, created} ->
5788 + ?MYDEBUG("Created resources table for ~s", [VHost]),
5790 + {atomic, exists} ->
5791 + ?MYDEBUG("Resources table for ~s already exists", [VHost]),
5793 + {aborted, _} -> error
5796 +create_internals(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5797 + sql_query_internal(DBRef, ["DROP FUNCTION IF EXISTS ",logmessage_name(VHost,Schema)," (tbname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION);"]),
5798 + case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
5800 + ?MYDEBUG("Created logmessage for ~p", [VHost]),
5802 + {error, Reason} ->
5803 + case lists:keysearch(code, 1, Reason) of
5804 + {value, {code, "42704"}} ->
5805 + ?ERROR_MSG("plpgsql language must be installed into database '~s'. Use CREATE LANGUAGE...", [State#state.db]),
5812 +get_user_id(DBRef, VHost, Schema, User) ->
5813 + SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ",
5814 + "WHERE username='",User,"';"],
5815 + case sql_query_internal(DBRef, SQuery) of
5817 + IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ",
5818 + "VALUES ('",User,"');"],
5819 + case sql_query_internal_silent(DBRef, IQuery) of
5821 + {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery),
5823 + {error, Reason} ->
5824 + % this can be in clustered environment
5825 + {value, {code, "23505"}} = lists:keysearch(code, 1, Reason),
5826 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
5827 + {data, [{ClID}]} = sql_query_internal(DBRef, SQuery),
5830 + {data, [{DBId}]} ->
5834 +get_logmessage(VHost,Schema) ->
5835 + UName = users_table(VHost,Schema),
5836 + SName = servers_table(VHost,Schema),
5837 + RName = resources_table(VHost,Schema),
5838 + StName = stats_table(VHost,Schema),
5839 + io_lib:format("CREATE OR REPLACE FUNCTION ~s (tbname TEXT, vname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION) RETURNS INTEGER AS $$
5842 + peer_nameID INTEGER;
5843 + peer_serverID INTEGER;
5844 + peer_resourceID INTEGER;
5845 + tablename ALIAS for $1;
5846 + viewname ALIAS for $2;
5847 + atdate ALIAS for $3;
5849 + SELECT INTO ownerID user_id FROM ~s WHERE username = owner;
5851 + INSERT INTO ~s (username) VALUES (owner);
5852 + ownerID := lastval();
5855 + SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name;
5857 + INSERT INTO ~s (username) VALUES (peer_name);
5858 + peer_nameID := lastval();
5861 + SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server;
5863 + INSERT INTO ~s (server) VALUES (peer_server);
5864 + peer_serverID := lastval();
5867 + SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource;
5869 + INSERT INTO ~s (resource) VALUES (peer_resource);
5870 + peer_resourceID := lastval();
5874 + EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
5875 + EXCEPTION WHEN undefined_table THEN
5876 + EXECUTE 'CREATE TABLE ' || tablename || ' (' ||
5877 + 'owner_id INTEGER, ' ||
5878 + 'peer_name_id INTEGER, ' ||
5879 + 'peer_server_id INTEGER, ' ||
5880 + 'peer_resource_id INTEGER, ' ||
5881 + 'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' ||
5882 + 'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' ||
5883 + 'subject TEXT, ' ||
5885 + 'timestamp DOUBLE PRECISION)';
5886 + EXECUTE 'CREATE INDEX \"search_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id)';
5888 + EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' ||
5889 + 'SELECT owner.username AS owner_name, ' ||
5890 + 'peer.username AS peer_name, ' ||
5891 + 'servers.server AS peer_server, ' ||
5892 + 'resources.resource AS peer_resource, ' ||
5893 + 'messages.direction, ' ||
5894 + 'messages.type, ' ||
5895 + 'messages.subject, ' ||
5896 + 'messages.body, ' ||
5897 + 'messages.timestamp ' ||
5902 + '~s resources, ' ||
5903 + tablename || ' messages ' ||
5905 + 'owner.user_id=messages.owner_id and ' ||
5906 + 'peer.user_id=messages.peer_name_id and ' ||
5907 + 'servers.server_id=messages.peer_server_id and ' ||
5908 + 'resources.resource_id=messages.peer_resource_id ' ||
5909 + 'ORDER BY messages.timestamp';
5911 + EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
5914 + UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID and peer_name_id=peer_nameID and peer_server_id=peer_serverID;
5916 + INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (ownerID, peer_nameID, peer_serverID, atdate, 1);
5920 +$$ LANGUAGE plpgsql;
5921 +", [logmessage_name(VHost,Schema),UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),UName,UName,SName,RName,StName,StName]).
5923 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5927 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5928 +% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
5929 +sql_transaction_internal(DBRef, Fun) ->
5930 + case sql_query_internal(DBRef, ["BEGIN;"]) of
5932 + case catch Fun() of
5934 + rollback_internal(DBRef, Err);
5935 + {error, _} = Err ->
5936 + rollback_internal(DBRef, Err);
5937 + {'EXIT', _} = Err ->
5938 + rollback_internal(DBRef, Err);
5940 + case sql_query_internal(DBRef, ["COMMIT;"]) of
5941 + {error, _} -> rollback_internal(DBRef, {commit_error});
5944 + {atomic, _} -> Res;
5945 + _ -> {atomic, Res}
5950 + {aborted, {begin_error}}
5953 +% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
5954 +rollback_internal(DBRef, Reason) ->
5955 + Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
5956 + {aborted, {Reason, {rollback_result, Res}}}.
5958 +sql_query_internal(DBRef, Query) ->
5959 + case sql_query_internal_silent(DBRef, Query) of
5960 + {error, undefined, Rez} ->
5961 + ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]),
5962 + {error, undefined};
5964 + ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]),
5969 +sql_query_internal_silent(DBRef, Query) ->
5970 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
5971 + % TODO: use pquery?
5972 + get_result(pgsql:squery(DBRef, Query)).
5974 +get_result({ok, ["CREATE TABLE"]}) ->
5976 +get_result({ok, ["DROP TABLE"]}) ->
5978 +get_result({ok, ["ALTER TABLE"]}) ->
5980 +get_result({ok,["DROP VIEW"]}) ->
5982 +get_result({ok,["DROP FUNCTION"]}) ->
5984 +get_result({ok, ["CREATE INDEX"]}) ->
5986 +get_result({ok, ["CREATE FUNCTION"]}) ->
5988 +get_result({ok, [{[$S, $E, $L, $E, $C, $T, $ | _Rest], _Rows, Recs}]}) ->
5991 + lists:map(fun(Elem) when is_binary(Elem) ->
5992 + binary_to_list(Elem);
5993 + (Elem) when is_list(Elem) ->
5995 + (Elem) when is_integer(Elem) ->
5996 + integer_to_list(Elem);
5997 + (Elem) when is_float(Elem) ->
5998 + float_to_list(Elem);
5999 + (Elem) when is_boolean(Elem) ->
6000 + atom_to_list(Elem);
6002 + ?ERROR_MSG("Unknown element type ~p", [Elem]),
6006 + Res = lists:map(Fun, Recs),
6007 + %{data, [list_to_tuple(Rec) || Rec <- Recs]};
6009 +get_result({ok, ["INSERT " ++ OIDN]}) ->
6010 + [_OID, N] = string:tokens(OIDN, " "),
6011 + {updated, list_to_integer(N)};
6012 +get_result({ok, ["DELETE " ++ N]}) ->
6013 + {updated, list_to_integer(N)};
6014 +get_result({ok, ["UPDATE " ++ N]}) ->
6015 + {updated, list_to_integer(N)};
6016 +get_result({ok, ["BEGIN"]}) ->
6018 +get_result({ok, ["LOCK TABLE"]}) ->
6020 +get_result({ok, ["ROLLBACK"]}) ->
6022 +get_result({ok, ["COMMIT"]}) ->
6024 +get_result({ok, ["SET"]}) ->
6026 +get_result({ok, [{error, Error}]}) ->
6029 + {error, undefined, Rez}.
6031 diff --git a/src/mod_roster.erl b/src/mod_roster.erl
6032 index cf281528..c3a5c92a 100644
6033 --- a/src/mod_roster.erl
6034 +++ b/src/mod_roster.erl
6037 -define(SETS, gb_sets).
6039 +-include("mod_logdb.hrl").
6041 -export_type([subscription/0]).
6043 -callback init(binary(), gen_mod:opts()) -> any().
6044 @@ -943,6 +945,14 @@ user_roster(User, Server, Query, Lang) ->
6046 Items = get_roster(LUser, LServer),
6047 SItems = lists:sort(Items),
6049 + Settings = case gen_mod:is_loaded(Server, mod_logdb) of
6051 + mod_logdb:get_user_settings(User, Server);
6056 FItems = case SItems of
6057 [] -> [?CT(<<"None">>)];
6059 @@ -1000,7 +1010,33 @@ user_roster(User, Server, Query, Lang) ->
6060 [?INPUTT(<<"submit">>,
6062 (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6065 + case gen_mod:is_loaded(Server, mod_logdb) of
6067 + Peer = jid:encode(R#roster.jid),
6068 + A = lists:member(Peer, Settings#user_settings.dolog_list),
6069 + B = lists:member(Peer, Settings#user_settings.donotlog_list),
6073 + {<<"donotlog">>, <<"Do Not Log Messages">>};
6075 + {<<"dolog">>, <<"Log Messages">>};
6076 + Settings#user_settings.dolog_default == true ->
6077 + {<<"donotlog">>, <<"Do Not Log Messages">>};
6078 + Settings#user_settings.dolog_default == false ->
6079 + {<<"dolog">>, <<"Log Messages">>}
6082 + ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
6083 + [?INPUTT(<<"submit">>,
6085 + (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6094 @@ -1107,9 +1143,42 @@ user_roster_item_parse_query(User, Server, Items,
6095 sub_els = [#roster_query{
6096 items = [RosterItem]}]}),
6102 + case lists:keysearch(
6103 + <<"donotlog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
6105 + Peer = jid:encode(JID),
6106 + Settings = mod_logdb:get_user_settings(User, Server),
6107 + DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of
6108 + false -> lists:append(Settings#user_settings.donotlog_list, [Peer]);
6109 + true -> Settings#user_settings.donotlog_list
6111 + DLL = lists:delete(jid:encode(JID), Settings#user_settings.dolog_list),
6112 + Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
6113 + % TODO: check returned value
6114 + ok = mod_logdb:set_user_settings(User, Server, Sett),
6117 + case lists:keysearch(
6118 + <<"dolog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
6120 + Peer = jid:encode(JID),
6121 + Settings = mod_logdb:get_user_settings(User, Server),
6122 + DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of
6123 + false -> lists:append(Settings#user_settings.dolog_list, [Peer]);
6124 + true -> Settings#user_settings.dolog_list
6126 + DNLL = lists:delete(jid:encode(JID), Settings#user_settings.donotlog_list),
6127 + Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
6128 + % TODO: check returned value
6129 + ok = mod_logdb:set_user_settings(User, Server, Sett),