1 From 9a2ed8d2b20ef052b71b065e686cc049d18999ac Mon Sep 17 00:00:00 2001
2 From: Oleh Palii <o.palij@gmail.com>
3 Date: Sat, 31 Aug 2019 11:04:57 +0300
4 Subject: [PATCH 1/3] apply mod_logdb to 19.08
7 priv/msgs/nl.msg | 14 +
8 priv/msgs/pl.msg | 26 +
9 priv/msgs/ru.msg | 30 +
10 priv/msgs/uk.msg | 30 +
12 src/gen_logdb.erl | 162 ++++
13 src/mod_logdb.erl | 1951 ++++++++++++++++++++++++++++++++++++++
14 src/mod_logdb.hrl | 33 +
15 src/mod_logdb_mnesia.erl | 553 +++++++++++
16 src/mod_logdb_mysql.erl | 1050 ++++++++++++++++++++
17 src/mod_logdb_mysql5.erl | 979 +++++++++++++++++++
18 src/mod_logdb_pgsql.erl | 1104 +++++++++++++++++++++
19 src/mod_roster.erl | 77 +-
20 13 files changed, 6007 insertions(+), 6 deletions(-)
21 create mode 100644 src/gen_logdb.erl
22 create mode 100644 src/mod_logdb.erl
23 create mode 100644 src/mod_logdb.hrl
24 create mode 100644 src/mod_logdb_mnesia.erl
25 create mode 100644 src/mod_logdb_mysql.erl
26 create mode 100644 src/mod_logdb_mysql5.erl
27 create mode 100644 src/mod_logdb_pgsql.erl
29 diff --git a/priv/msgs/nl.msg b/priv/msgs/nl.msg
30 index 8009d529ff..fd3d3b0ebb 100644
31 --- a/priv/msgs/nl.msg
32 +++ b/priv/msgs/nl.msg
34 {"Your contact offline message queue is full. The message has been discarded.","Te veel offline berichten voor dit contactpersoon. Het bericht is niet opgeslagen."}.
35 {"Your Jabber account was successfully created.","Uw Jabber-account is succesvol gecreeerd."}.
36 {"Your Jabber account was successfully deleted.","Uw Jabber-account is succesvol verwijderd."}.
38 +{"Users Messages", "Gebruikersberichten"}.
41 +{"Logged messages for ~s", "Gelogde berichten van ~s"}.
42 +{"Logged messages for ~s at ~s", "Gelogde berichten van ~s op ~s"}.
44 +{"No logged messages for ~s", "Geen gelogde berichten van ~s"}.
45 +{"No logged messages for ~s at ~s", "Geen gelogde berichten van ~s op ~s"}.
46 +{"Date, Time", "Datum en tijd"}.
47 +{"Direction: Jid", "Richting: Jabber ID"}.
48 +{"Subject", "Onderwerp"}.
49 +{"Body", "Berichtveld"}.
50 +{"Messages", "Berichten"}.
51 diff --git a/priv/msgs/pl.msg b/priv/msgs/pl.msg
52 index 2ca75b259c..fffae5742e 100644
53 --- a/priv/msgs/pl.msg
54 +++ b/priv/msgs/pl.msg
56 {"Your Jabber account was successfully created.","Twoje konto zostało stworzone."}.
57 {"Your Jabber account was successfully deleted.","Twoje konto zostało usunięte."}.
58 {"You're not allowed to create nodes","Nie masz uprawnień do tworzenia węzłów"}.
60 +{"Users Messages", "Wiadomości użytkownika"}.
63 +{"Logged messages for ~s", "Zapisane wiadomości dla ~s"}.
64 +{"Logged messages for ~s at", "Zapisane wiadomości dla ~s o ~s"}.
66 +{"No logged messages for ~s", "Brak zapisanych wiadomości dla ~s"}.
67 +{"No logged messages for ~s at ~s", "Brak zapisanych wiadomości dla ~s o ~s"}.
68 +{"Date, Time", "Data, Godzina"}.
69 +{"Direction: Jid", "Kierunek: Jid"}.
70 +{"Subject", "Temat"}.
72 +{"Messages","Wiadomości"}.
73 +{"Filter Selected", "Odfiltruj zaznaczone"}.
74 +{"Do Not Log Messages", "Nie zapisuj wiadomości"}.
75 +{"Log Messages", "Zapisuj wiadomości"}.
76 +{"Messages logging engine", "System zapisywania historii rozmów"}.
77 +{"Default", "Domyślne"}.
78 +{"Set logging preferences", "Ustaw preferencje zapisywania"}.
79 +{"Messages logging engine settings", "Ustawienia systemu logowania"}.
80 +{"Set run-time settings", "Zapisz ustawienia systemu logowania"}.
81 +{"Groupchat messages logging", "Zapisywanie rozmów z konferencji"}.
82 +{"Jids/Domains to ignore", "JID/Domena która ma być ignorowana"}.
83 +{"Purge messages older than (days)", "Usuń wiadomości starsze niż (w dniach)"}.
84 +{"Poll users settings (seconds)", "Czas aktualizacji preferencji użytkowników (sekundy)"}.
85 diff --git a/priv/msgs/ru.msg b/priv/msgs/ru.msg
86 index f7dff97ea1..42be5d4f15 100644
87 --- a/priv/msgs/ru.msg
88 +++ b/priv/msgs/ru.msg
90 {"Your Jabber account was successfully created.","Ваш Jabber-аккаунт был успешно создан."}.
91 {"Your Jabber account was successfully deleted.","Ваш Jabber-аккаунт был успешно удален."}.
92 {"You're not allowed to create nodes","Вам не разрешается создавать узлы"}.
94 +{"Users Messages", "Сообщения пользователей"}.
96 +{"Count", "Количество"}.
97 +{"Logged messages for ~s", "Сохранённые cообщения для ~s"}.
98 +{"Logged messages for ~s at ~s", "Сохранённые cообщения для ~s за ~s"}.
100 +{"No logged messages for ~s", "Отсутствуют сообщения для ~s"}.
101 +{"No logged messages for ~s at ~s", "Отсутствуют сообщения для ~s за ~s"}.
102 +{"Date, Time", "Дата, Время"}.
103 +{"Direction: Jid", "Направление: Jid"}.
104 +{"Subject", "Тема"}.
106 +{"Messages", "Сообщения"}.
107 +{"Filter Selected", "Отфильтровать выделенные"}.
108 +{"Do Not Log Messages", "Не сохранять сообщения"}.
109 +{"Log Messages", "Сохранять сообщения"}.
110 +{"Messages logging engine", "Система логирования сообщений"}.
111 +{"Default", "По умолчанию"}.
112 +{"Set logging preferences", "Задайте настройки логирования"}.
113 +{"Messages logging engine users", "Пользователи системы логирования сообщений"}.
114 +{"Messages logging engine settings", "Настройки системы логирования сообщений"}.
115 +{"Set run-time settings", "Задайте текущие настройки"}.
116 +{"Groupchat messages logging", "Логирование сообщений типа groupchat"}.
117 +{"Jids/Domains to ignore", "Игнорировать следующие jids/домены"}.
118 +{"Purge messages older than (days)", "Удалять сообщения старее чем (дни)"}.
119 +{"Poll users settings (seconds)", "Обновлять настройки пользователей через (секунд)"}.
120 +{"Drop", "Удалять"}.
121 +{"Do not drop", "Не удалять"}.
122 +{"Drop messages on user removal", "Удалять сообщения при удалении пользователя"}.
123 diff --git a/priv/msgs/uk.msg b/priv/msgs/uk.msg
124 index 0fbc336d51..c0b90047fa 100644
125 --- a/priv/msgs/uk.msg
126 +++ b/priv/msgs/uk.msg
128 {"Your contact offline message queue is full. The message has been discarded.","Черга повідомлень, що не були доставлені, переповнена. Повідомлення не було збережено."}.
129 {"Your Jabber account was successfully created.","Ваш Jabber-акаунт було успішно створено."}.
130 {"Your Jabber account was successfully deleted.","Ваш Jabber-акаунт було успішно видалено."}.
132 +{"Users Messages", "Повідомлення користувачів"}.
134 +{"Count", "Кількість"}.
135 +{"Logged messages for ~s", "Збережені повідомлення для ~s"}.
136 +{"Logged messages for ~s at ~s", "Збережені повідомлення для ~s за ~s"}.
138 +{"No logged messages for ~s", "Відсутні повідомлення для ~s"}.
139 +{"No logged messages for ~s at ~s", "Відсутні повідомлення для ~s за ~s"}.
140 +{"Date, Time", "Дата, Час"}.
141 +{"Direction: Jid", "Напрямок: Jid"}.
142 +{"Subject", "Тема"}.
144 +{"Messages", "Повідомлення"}.
145 +{"Filter Selected", "Відфільтрувати виділені"}.
146 +{"Do Not Log Messages", "Не зберігати повідомлення"}.
147 +{"Log Messages", "Зберігати повідомлення"}.
148 +{"Messages logging engine", "Система збереження повідомлень"}.
149 +{"Default", "За замовчуванням"}.
150 +{"Set logging preferences", "Вкажіть налагоджування збереження повідомлень"}.
151 +{"Messages logging engine users", "Користувачі системи збереження повідомлень"}.
152 +{"Messages logging engine settings", "Налагоджування системи збереження повідомлень"}.
153 +{"Set run-time settings", "Вкажіть поточні налагоджування"}.
154 +{"Groupchat messages logging", "Збереження повідомлень типу groupchat"}.
155 +{"Jids/Domains to ignore", "Ігнорувати наступні jids/домени"}.
156 +{"Purge messages older than (days)", "Видаляти повідомлення старіші ніж (дні)"}.
157 +{"Poll users settings (seconds)", "Оновлювати налагоджування користувачів кожні (секунд)"}.
158 +{"Drop", "Видаляти"}.
159 +{"Do not drop", "Не видаляти"}.
160 +{"Drop messages on user removal", "Видаляти повідомлення під час видалення користувача"}.
161 diff --git a/rebar.config b/rebar.config
162 index e05fe84e6e..c3b87afd28 100644
166 {base64url, ".*", {git, "https://github.com/dvv/base64url.git", {tag, "v1.0"}}},
167 {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.31"}}}},
168 {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.32"}}}},
169 - {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql",
170 - {tag, "1.0.13"}}}},
171 + {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/paleg/p1_mysql",
172 + {tag, "1.0.11_multi"}}}},
173 {if_var_true, pgsql, {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql",
175 {if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3",
176 diff --git a/src/gen_logdb.erl b/src/gen_logdb.erl
178 index 0000000000..8bad112969
180 +++ b/src/gen_logdb.erl
182 +%%%----------------------------------------------------------------------
183 +%%% File : gen_logdb.erl
184 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
185 +%%% Purpose : Describes generic behaviour for mod_logdb backends.
186 +%%% Url : https://paleg.github.io/mod_logdb/
187 +%%%----------------------------------------------------------------------
190 +-author('o.palij@gmail.com').
192 +-export([behaviour_info/1]).
194 +behaviour_info(callbacks) ->
196 + % called from handle_info(start, _)
197 + % it should logon database and return reference to started instance
198 + % start(VHost, Opts) -> {ok, SPid} | error
199 + % Options - list of options to connect to db
200 + % Types: Options = list() -> [] |
201 + % [{user, "logdb"},
203 + % {db, "logdb"}] | ...
204 + % VHost = list() -> "jabber.example.org"
207 + % called from cleanup/1
208 + % it should logoff database and do cleanup
210 + % Types: VHost = list() -> "jabber.example.org"
213 + % called from handle_call({addlog, _}, _, _)
214 + % it should log messages to database
215 + % log_message(VHost, Msg) -> ok | error
217 + % VHost = list() -> "jabber.example.org"
218 + % Msg = record() -> #msg
221 + % called from ejabberdctl rebuild_stats
222 + % it should rebuild stats table (if used) for vhost
223 + % rebuild_stats(VHost)
225 + % VHost = list() -> "jabber.example.org"
226 + {rebuild_stats, 1},
228 + % it should rebuild stats table (if used) for vhost at Date
229 + % rebuild_stats_at(VHost, Date)
231 + % VHost = list() -> "jabber.example.org"
232 + % Date = list() -> "2007-02-12"
233 + {rebuild_stats_at, 2},
235 + % called from user_messages_at_parse_query/5
236 + % it should delete selected user messages at date
237 + % delete_messages_by_user_at(VHost, Msgs, Date) -> ok | error
239 + % VHost = list() -> "jabber.example.org"
240 + % Msgs = list() -> [ #msg1, msg2, ... ]
241 + % Date = list() -> "2007-02-12"
242 + {delete_messages_by_user_at, 3},
244 + % called from user_messages_parse_query/4 | vhost_messages_at_parse_query/4
245 + % it should delete all user messages at date
246 + % delete_all_messages_by_user_at(User, VHost, Date) -> ok | error
248 + % User = list() -> "admin"
249 + % VHost = list() -> "jabber.example.org"
250 + % Date = list() -> "2007-02-12"
251 + {delete_all_messages_by_user_at, 3},
253 + % called from vhost_messages_parse_query/3
254 + % it should delete messages for vhost at date and update stats
255 + % delete_messages_at(VHost, Date) -> ok | error
257 + % VHost = list() -> "jabber.example.org"
258 + % Date = list() -> "2007-02-12"
259 + {delete_messages_at, 2},
261 + % called from ejabberd_web_admin:vhost_messages_stats/3
262 + % it should return sorted list of count of messages by dates for vhost
263 + % get_vhost_stats(VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ... ]} |
266 + % VHost = list() -> "jabber.example.org"
267 + % DateN = list() -> "2007-02-12"
268 + % Msgs_countN = number() -> 241
269 + {get_vhost_stats, 1},
271 + % called from ejabberd_web_admin:vhost_messages_stats_at/4
272 + % it should return sorted list of count of messages by users at date for vhost
273 + % get_vhost_stats_at(VHost, Date) -> {ok, [{User1, Msgs_count1}, {User2, Msgs_count2}, ....]} |
276 + % VHost = list() -> "jabber.example.org"
277 + % Date = list() -> "2007-02-12"
278 + % UserN = list() -> "admin"
279 + % Msgs_countN = number() -> 241
280 + {get_vhost_stats_at, 2},
282 + % called from ejabberd_web_admin:user_messages_stats/4
283 + % it should return sorted list of count of messages by date for user at vhost
284 + % get_user_stats(User, VHost) -> {ok, [{Date1, Msgs_count1}, {Date2, Msgs_count2}, ...]} |
287 + % User = list() -> "admin"
288 + % VHost = list() -> "jabber.example.org"
289 + % DateN = list() -> "2007-02-12"
290 + % Msgs_countN = number() -> 241
291 + {get_user_stats, 2},
293 + % called from ejabberd_web_admin:user_messages_stats_at/5
294 + % it should return all user messages at date
295 + % get_user_messages_at(User, VHost, Date) -> {ok, Msgs} | {error, Reason}
297 + % User = list() -> "admin"
298 + % VHost = list() -> "jabber.example.org"
299 + % Date = list() -> "2007-02-12"
300 + % Msgs = list() -> [ #msg1, msg2, ... ]
301 + {get_user_messages_at, 3},
303 + % called from many places
304 + % it should return list of dates for vhost
305 + % get_dates(VHost) -> [Date1, Date2, ... ]
307 + % VHost = list() -> "jabber.example.org"
308 + % DateN = list() -> "2007-02-12"
311 + % called from start
312 + % it should return list with users settings for VHost in db
313 + % get_users_settings(VHost) -> [#user_settings1, #user_settings2, ... ] | error
315 + % VHost = list() -> "jabber.example.org"
316 + {get_users_settings, 1},
318 + % called from many places
319 + % it should return User settings at VHost from db
320 + % get_user_settings(User, VHost) -> error | {ok, #user_settings}
322 + % User = list() -> "admin"
323 + % VHost = list() -> "jabber.example.org"
324 + {get_user_settings, 2},
326 + % called from web admin
327 + % it should set User settings at VHost
328 + % set_user_settings(User, VHost, #user_settings) -> ok | error
330 + % User = list() -> "admin"
331 + % VHost = list() -> "jabber.example.org"
332 + {set_user_settings, 3},
334 + % called from remove_user (ejabberd hook)
335 + % it should remove user messages and settings at VHost
336 + % drop_user(User, VHost) -> ok | error
338 + % User = list() -> "admin"
339 + % VHost = list() -> "jabber.example.org"
342 +behaviour_info(_) ->
344 diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
346 index 0000000000..bf0240d139
348 +++ b/src/mod_logdb.erl
350 +%%%----------------------------------------------------------------------
351 +%%% File : mod_logdb.erl
352 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
353 +%%% Purpose : Frontend for log user messages to db
354 +%%% Url : https://paleg.github.io/mod_logdb/
355 +%%%----------------------------------------------------------------------
358 +-author('o.palij@gmail.com').
360 +-behaviour(gen_server).
361 +-behaviour(gen_mod).
364 +-export([start_link/2]).
366 +-export([start/2, stop/1,
368 + depends/2, reload/3]).
370 +-export([code_change/3,
371 + handle_call/3, handle_cast/2, handle_info/2,
372 + init/1, terminate/2]).
374 +-export([send_packet/1, receive_packet/1, offline_message/1, remove_user/2]).
375 +-export([get_local_identity/5,
376 + get_local_features/5,
378 + adhoc_local_items/4,
379 + adhoc_local_commands/4
382 +-export([rebuild_stats/1,
383 + copy_messages/1, copy_messages_ctl/3, copy_messages_int_tc/1]).
385 +-export([get_vhost_stats/1, get_vhost_stats_at/2,
386 + get_user_stats/2, get_user_messages_at/3,
389 + convert_timestamp/1, convert_timestamp_brief/1,
390 + get_user_settings/2, set_user_settings/3,
391 + user_messages_at_parse_query/4, user_messages_parse_query/3,
392 + vhost_messages_parse_query/2, vhost_messages_at_parse_query/4,
393 + list_to_bool/1, bool_to_list/1,
394 + list_to_string/1, string_to_list/1,
395 + get_module_settings/1, set_module_settings/2,
396 + purge_old_records/2]).
398 +-export([webadmin_menu/3,
401 + user_parse_query/5]).
403 +-export([vhost_messages_stats/3,
404 + vhost_messages_stats_at/4,
405 + user_messages_stats/4,
406 + user_messages_stats_at/5]).
408 +-include("mod_logdb.hrl").
409 +-include("xmpp.hrl").
410 +-include("mod_roster.hrl").
411 +-include("ejabberd_commands.hrl").
412 +-include("adhoc.hrl").
413 +-include("ejabberd_web_admin.hrl").
414 +-include("ejabberd_http.hrl").
415 +-include("logger.hrl").
417 +-define(PROCNAME, ejabberd_mod_logdb).
418 +% gen_server call timeout
419 +-define(CALL_TIMEOUT, 10000).
421 +-record(state, {vhost, dbmod, backendPid, monref, purgeRef, pollRef, dbopts, dbs, dolog_default, ignore_jids, groupchat, purge_older_days, poll_users_settings, drop_messages_on_user_removal}).
423 +ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ binary_to_list(VHost)).
425 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
427 +% gen_mod/gen_server callbacks
429 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
430 +% ejabberd starts module
431 +start(VHost, Opts) ->
433 + {gen_mod:get_module_proc(VHost, ?PROCNAME),
434 + {?MODULE, start_link, [VHost, Opts]},
439 + % add child to ejabberd_sup
440 + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec).
442 +depends(_Host, _Opts) ->
445 +reload(_Host, _NewOpts, _OldOpts) ->
449 +% supervisor starts gen_server
450 +start_link(VHost, Opts) ->
451 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
452 + {ok, Pid} = gen_server:start_link({local, Proc}, ?MODULE, [VHost, Opts], []),
456 +init([VHost, Opts]) ->
457 + process_flag(trap_exit, true),
458 + DBsRaw = gen_mod:get_opt(dbs, Opts, fun(A) -> A end, [{mnesia, []}]),
459 + DBs = case lists:keysearch(mnesia, 1, DBsRaw) of
460 + false -> lists:append(DBsRaw, [{mnesia,[]}]);
461 + {value, _} -> DBsRaw
463 + VHostDB = gen_mod:get_opt(vhosts, Opts, fun(A) -> A end, [{VHost, mnesia}]),
464 + % 10 is default because of using in clustered environment
465 + PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, fun(A) -> A end, 10),
468 + case lists:keysearch(VHost, 1, VHostDB) of
470 + ?WARNING_MSG("There is no logging backend defined for '~s', switching to mnesia", [VHost]),
472 + {value,{_, DBNameResult}} ->
473 + case lists:keysearch(DBNameResult, 1, DBs) of
475 + ?WARNING_MSG("There is no such logging backend '~s' defined for '~s', switching to mnesia", [DBNameResult, VHost]),
477 + {value, {_, DBOptsResult}} ->
478 + {DBNameResult, DBOptsResult}
482 + ?MYDEBUG("Starting mod_logdb for '~s' with '~s' backend", [VHost, DBName]),
484 + DBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(DBName)),
486 + {ok, #state{vhost=VHost,
489 + % dbs used for convert messages from one backend to other
491 + dolog_default=gen_mod:get_opt(dolog_default, Opts, fun(A) -> A end, true),
492 + drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts, fun(A) -> A end, true),
493 + ignore_jids=gen_mod:get_opt(ignore_jids, Opts, fun(A) -> A end, []),
494 + groupchat=gen_mod:get_opt(groupchat, Opts, fun(A) -> A end, none),
495 + purge_older_days=gen_mod:get_opt(purge_older_days, Opts, fun(A) -> A end, never),
496 + poll_users_settings=PollUsersSettings}}.
498 +cleanup(#state{vhost=VHost} = _State) ->
499 + ?MYDEBUG("Stopping ~s for ~p", [?MODULE, VHost]),
501 + %ets:delete(ets_settings_table(VHost)),
503 + ejabberd_hooks:delete(remove_user, VHost, ?MODULE, remove_user, 90),
504 + ejabberd_hooks:delete(user_send_packet, VHost, ?MODULE, send_packet, 90),
505 + ejabberd_hooks:delete(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
506 + ejabberd_hooks:delete(offline_message_hook, VHost, ?MODULE, offline_message, 40),
508 + ejabberd_hooks:delete(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
509 + ejabberd_hooks:delete(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
510 + ejabberd_hooks:delete(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
511 + ejabberd_hooks:delete(disco_local_features, VHost, ?MODULE, get_local_features, 50),
512 + ejabberd_hooks:delete(disco_local_items, VHost, ?MODULE, get_local_items, 50),
514 + ejabberd_hooks:delete(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
515 + ejabberd_hooks:delete(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
516 + ejabberd_hooks:delete(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
517 + ejabberd_hooks:delete(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
519 + ?MYDEBUG("Removed hooks for ~p", [VHost]),
521 + ejabberd_commands:unregister_commands(get_commands_spec()),
522 + ?MYDEBUG("Unregistered commands for ~p", [VHost]).
525 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
526 + %gen_server:call(Proc, {cleanup}),
527 + %?MYDEBUG("Cleanup in stop finished!!!!", []),
528 + %timer:sleep(10000),
529 + ok = supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),
530 + ok = supervisor:delete_child(ejabberd_gen_mod_sup, Proc).
532 +get_commands_spec() ->
533 + [#ejabberd_commands{name = rebuild_stats, tags = [logdb],
534 + desc = "Rebuild mod_logdb stats for given host",
535 + module = ?MODULE, function = rebuild_stats,
536 + args = [{host, binary}],
537 + result = {res, rescode}},
538 + #ejabberd_commands{name = copy_messages, tags = [logdb],
539 + desc = "Copy logdb messages from given backend to current backend for given host",
540 + module = ?MODULE, function = copy_messages_ctl,
541 + args = [{host, binary}, {backend, binary}, {date, binary}],
542 + result = {res, rescode}}].
544 +mod_opt_type(dbs) ->
545 + fun (A) when is_list(A) -> A end;
546 +mod_opt_type(vhosts) ->
547 + fun (A) when is_list(A) -> A end;
548 +mod_opt_type(poll_users_settings) ->
549 + fun (I) when is_integer(I) -> I end;
550 +mod_opt_type(groupchat) ->
555 +mod_opt_type(dolog_default) ->
556 + fun (B) when is_boolean(B) -> B end;
557 +mod_opt_type(ignore_jids) ->
558 + fun (A) when is_list(A) -> A end;
559 +mod_opt_type(purge_older_days) ->
560 + fun (I) when is_integer(I) -> I end;
562 + [dbs, vhosts, poll_users_settings, groupchat, dolog_default, ignore_jids, purge_older_days].
564 +handle_call({cleanup}, _From, State) ->
566 + ?MYDEBUG("Cleanup finished!!!!!", []),
567 + {reply, ok, State};
568 +handle_call({get_dates}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
569 + Reply = DBMod:get_dates(VHost),
570 + {reply, Reply, State};
571 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
572 +% ejabberd_web_admin callbacks
573 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
574 +handle_call({delete_messages_by_user_at, PMsgs, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
575 + Reply = DBMod:delete_messages_by_user_at(VHost, PMsgs, binary_to_list(Date)),
576 + {reply, Reply, State};
577 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
578 + Reply = DBMod:delete_all_messages_by_user_at(binary_to_list(User), VHost, binary_to_list(Date)),
579 + {reply, Reply, State};
580 +handle_call({delete_messages_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
581 + Reply = DBMod:delete_messages_at(VHost, Date),
582 + {reply, Reply, State};
583 +handle_call({get_vhost_stats}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
584 + Reply = DBMod:get_vhost_stats(VHost),
585 + {reply, Reply, State};
586 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
587 + Reply = DBMod:get_vhost_stats_at(VHost, binary_to_list(Date)),
588 + {reply, Reply, State};
589 +handle_call({get_user_stats, User}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
590 + Reply = DBMod:get_user_stats(binary_to_list(User), VHost),
591 + {reply, Reply, State};
592 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
593 + Reply = DBMod:get_user_messages_at(binary_to_list(User), VHost, binary_to_list(Date)),
594 + {reply, Reply, State};
595 +handle_call({get_user_settings, User}, _From, #state{dbmod=_DBMod, vhost=VHost}=State) ->
596 + Reply = case ets:match_object(ets_settings_table(VHost),
597 + #user_settings{owner_name=User, _='_'}) of
599 + _ -> #user_settings{owner_name=User,
600 + dolog_default=State#state.dolog_default,
604 + {reply, Reply, State};
605 +% TODO: remove User ??
606 +handle_call({set_user_settings, User, GSet}, _From, #state{dbmod=DBMod, vhost=VHost}=State) ->
607 + Set = GSet#user_settings{owner_name=User},
609 + case ets:match_object(ets_settings_table(VHost),
610 + #user_settings{owner_name=User, _='_'}) of
614 + case DBMod:set_user_settings(binary_to_list(User), VHost, Set) of
618 + true = ets:insert(ets_settings_table(VHost), Set),
622 + {reply, Reply, State};
623 +handle_call({get_module_settings}, _From, State) ->
624 + {reply, State, State};
625 +handle_call({set_module_settings, #state{purge_older_days=PurgeDays,
626 + poll_users_settings=PollSec} = Settings},
628 + #state{purgeRef=PurgeRefOld,
629 + pollRef=PollRefOld,
630 + purge_older_days=PurgeDaysOld,
631 + poll_users_settings=PollSecOld} = State) ->
633 + PurgeDays == never, PurgeDaysOld /= never ->
634 + {ok, cancel} = timer:cancel(PurgeRefOld),
636 + is_integer(PurgeDays), PurgeDaysOld == never ->
637 + set_purge_timer(PurgeDays);
643 + PollSec == PollSecOld ->
645 + PollSec == 0, PollSecOld /= 0 ->
646 + {ok, cancel} = timer:cancel(PollRefOld),
648 + is_integer(PollSec), PollSecOld == 0 ->
649 + set_poll_timer(PollSec);
650 + is_integer(PollSec), PollSecOld /= 0 ->
651 + {ok, cancel} = timer:cancel(PollRefOld),
652 + set_poll_timer(PollSec)
655 + NewState = State#state{dolog_default=Settings#state.dolog_default,
656 + ignore_jids=Settings#state.ignore_jids,
657 + groupchat=Settings#state.groupchat,
658 + drop_messages_on_user_removal=Settings#state.drop_messages_on_user_removal,
659 + purge_older_days=PurgeDays,
660 + poll_users_settings=PollSec,
663 + {reply, ok, NewState};
664 +handle_call(Msg, _From, State) ->
665 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
667 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
668 +% end ejabberd_web_admin callbacks
669 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
671 +% ejabberd_hooks call
672 +handle_cast({addlog, Direction, Owner, Peer, Packet}, #state{dbmod=DBMod, vhost=VHost}=State) ->
673 + case filter(Owner, Peer, State) of
675 + case catch packet_parse(Owner, Peer, Packet, Direction, State) of
678 + {'EXIT', Reason} ->
679 + ?ERROR_MSG("Failed to parse: ~p", [Reason]);
681 + DBMod:log_message(VHost, Msg)
687 +handle_cast({remove_user, User}, #state{dbmod=DBMod, vhost=VHost}=State) ->
688 + case State#state.drop_messages_on_user_removal of
690 + DBMod:drop_user(binary_to_list(User), VHost),
691 + ?INFO_MSG("Launched ~s@~s removal", [User, VHost]);
693 + ?INFO_MSG("Message removing is disabled. Keeping messages for ~s@~s", [User, VHost])
696 +% ejabberdctl rebuild_stats/3
697 +handle_cast({rebuild_stats}, #state{dbmod=DBMod, vhost=VHost}=State) ->
698 + DBMod:rebuild_stats(VHost),
700 +handle_cast({copy_messages, Backend}, State) ->
701 + spawn(?MODULE, copy_messages, [[State, Backend, []]]),
703 +handle_cast({copy_messages, Backend, Date}, State) ->
704 + spawn(?MODULE, copy_messages, [[State, Backend, [binary_to_list(Date)]]]),
706 +handle_cast(Msg, State) ->
707 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
710 +% return: disabled | timer reference
711 +set_purge_timer(PurgeDays) ->
714 + Days when is_integer(Days) ->
715 + {ok, Ref1} = timer:send_interval(timer:hours(24), scheduled_purging),
719 +% return: disabled | timer reference
720 +set_poll_timer(PollSec) ->
723 + {ok, Ref2} = timer:send_interval(timer:seconds(PollSec), poll_users_settings),
725 + % db polling disabled
729 + {ok, Ref3} = timer:send_interval(timer:seconds(10), poll_users_settings),
733 +% actual starting of logging
734 +% from timer:send_after (in init)
735 +handle_info(start, #state{dbmod=DBMod, vhost=VHost}=State) ->
736 + case DBMod:start(VHost, State#state.dbopts) of
737 + {error,{already_started,_}} ->
738 + ?MYDEBUG("backend module already started - trying to stop it", []),
740 + {stop, already_started, State};
742 + timer:sleep(30000),
743 + ?ERROR_MSG("Failed to start: ~p", [Reason]),
744 + {stop, db_connection_failed, State};
746 + ?INFO_MSG("~p connection established", [DBMod]),
748 + MonRef = erlang:monitor(process, SPid),
750 + ets:new(ets_settings_table(VHost), [named_table,public,set,{keypos, #user_settings.owner_name}]),
751 + DoLog = case DBMod:get_users_settings(VHost) of
752 + {ok, Settings} -> [Sett#user_settings{owner_name = iolist_to_binary(Sett#user_settings.owner_name)} || Sett <- Settings];
753 + {error, _Reason} -> []
755 + ets:insert(ets_settings_table(VHost), DoLog),
757 + TrefPurge = set_purge_timer(State#state.purge_older_days),
758 + TrefPoll = set_poll_timer(State#state.poll_users_settings),
760 + ejabberd_hooks:add(remove_user, VHost, ?MODULE, remove_user, 90),
761 + ejabberd_hooks:add(user_send_packet, VHost, ?MODULE, send_packet, 90),
762 + ejabberd_hooks:add(user_receive_packet, VHost, ?MODULE, receive_packet, 90),
763 + ejabberd_hooks:add(offline_message_hook, VHost, ?MODULE, offline_message, 40),
765 + ejabberd_hooks:add(adhoc_local_commands, VHost, ?MODULE, adhoc_local_commands, 50),
766 + ejabberd_hooks:add(disco_local_items, VHost, ?MODULE, get_local_items, 50),
767 + ejabberd_hooks:add(disco_local_identity, VHost, ?MODULE, get_local_identity, 50),
768 + ejabberd_hooks:add(disco_local_features, VHost, ?MODULE, get_local_features, 50),
769 + ejabberd_hooks:add(adhoc_local_items, VHost, ?MODULE, adhoc_local_items, 50),
771 + ejabberd_hooks:add(webadmin_menu_host, VHost, ?MODULE, webadmin_menu, 70),
772 + ejabberd_hooks:add(webadmin_user, VHost, ?MODULE, webadmin_user, 50),
773 + ejabberd_hooks:add(webadmin_page_host, VHost, ?MODULE, webadmin_page, 50),
774 + ejabberd_hooks:add(webadmin_user_parse_query, VHost, ?MODULE, user_parse_query, 50),
776 + ?MYDEBUG("Added hooks for ~p", [VHost]),
778 + ejabberd_commands:register_commands(get_commands_spec()),
779 + ?MYDEBUG("Registered commands for ~p", [VHost]),
781 + NewState=State#state{monref = MonRef, backendPid=SPid, purgeRef=TrefPurge, pollRef=TrefPoll},
782 + {noreply, NewState};
784 + ?ERROR_MSG("Rez=~p", [Rez]),
785 + timer:sleep(30000),
786 + {stop, db_connection_failed, State}
788 +% from timer:send_interval/2 (in start handle_info)
789 +handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = State) ->
790 + ?MYDEBUG("Starting scheduled purging of old records for ~p", [VHost]),
791 + spawn(?MODULE, purge_old_records, [VHost, integer_to_list(Days)]),
793 +% from timer:send_interval/2 (in start handle_info)
794 +handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) ->
795 + {ok, DoLog} = DBMod:get_users_settings(VHost),
796 + ?MYDEBUG("DoLog=~p", [DoLog]),
797 + true = ets:delete_all_objects(ets_settings_table(VHost)),
798 + ets:insert(ets_settings_table(VHost), DoLog),
800 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
801 + {stop, db_connection_dropped, State};
802 +handle_info({fetch_result, _, _}, State) ->
803 + ?MYDEBUG("Got timed out mysql fetch result", []),
805 +handle_info(Info, State) ->
806 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
809 +terminate(db_connection_failed, _State) ->
811 +terminate(db_connection_dropped, State) ->
812 + ?MYDEBUG("Got terminate with db_connection_dropped", []),
815 +terminate(Reason, #state{monref=undefined} = State) ->
816 + ?MYDEBUG("Got terminate with undefined monref.~nReason: ~p", [Reason]),
819 +terminate(Reason, #state{dbmod=DBMod, vhost=VHost, monref=MonRef, backendPid=Pid} = State) ->
820 + ?INFO_MSG("Reason: ~p", [Reason]),
821 + case erlang:is_process_alive(Pid) of
823 + erlang:demonitor(MonRef, [flush]),
831 +code_change(_OldVsn, State, _Extra) ->
834 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
836 +% ejabberd_hooks callbacks
838 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
839 +% TODO: change to/from to list as sql stores it as list
840 +send_packet({Pkt, #{jid := Owner} = C2SState}) ->
841 + VHost = Owner#jid.lserver,
842 + Peer = xmpp:get_to(Pkt),
843 + %?MYDEBUG("send_packet. Peer=~p, Owner=~p", [Peer, Owner]),
844 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
845 + gen_server:cast(Proc, {addlog, to, Owner, Peer, Pkt}),
848 +receive_packet({Pkt, #{jid := Owner} = C2SState}) ->
849 + VHost = Owner#jid.lserver,
850 + Peer = xmpp:get_from(Pkt),
851 + %?MYDEBUG("receive_packet. Pkt=~p", [Pkt]),
852 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
853 + gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
856 +offline_message({_Action, #message{from = Peer, to = Owner} = Pkt} = Acc) ->
857 + VHost = Owner#jid.lserver,
858 + %?MYDEBUG("offline_message. Pkt=~p", [Pkt]),
859 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
860 + gen_server:cast(Proc, {addlog, from, Owner, Peer, Pkt}),
863 +remove_user(User, Server) ->
864 + LUser = jid:nodeprep(User),
865 + LServer = jid:nameprep(Server),
866 + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME),
867 + gen_server:cast(Proc, {remove_user, LUser}).
869 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
873 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
874 +rebuild_stats(VHost) ->
875 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
876 + gen_server:cast(Proc, {rebuild_stats}),
879 +copy_messages_ctl(VHost, Backend, <<"all">>) ->
880 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
881 + gen_server:cast(Proc, {copy_messages, Backend}),
883 +copy_messages_ctl(VHost, Backend, Date) ->
884 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
885 + gen_server:cast(Proc, {copy_messages, Backend, Date}),
888 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
892 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
894 +% handle_cast({addlog, E}, _)
895 +% raw packet -> #msg
896 +packet_parse(_Owner, _Peer, #message{type = error}, _Direction, _State) ->
898 +packet_parse(_Owner, _Peer, #message{meta = #{sm_copy := true}}, _Direction, _State) ->
900 +packet_parse(_Owner, _Peer, #message{meta = #{from_offline := true}}, _Direction, _State) ->
902 +packet_parse(Owner, Peer, #message{body = Body, subject = Subject, type = Type}, Direction, State) ->
903 + %?MYDEBUG("Owner=~p, Peer=~p, Direction=~p", [Owner, Peer, Direction]),
904 + %?MYDEBUG("Body=~p, Subject=~p, Type=~p", [Body, Subject, Type]),
905 + SubjectText = xmpp:get_text(Subject),
906 + BodyText = xmpp:get_text(Body),
907 + if (SubjectText == <<"">>) and (BodyText == <<"">>) ->
913 + groupchat when State#state.groupchat == send, Direction == to ->
915 + groupchat when State#state.groupchat == send, Direction == from ->
917 + groupchat when State#state.groupchat == none ->
923 + #msg{timestamp = get_timestamp(),
924 + owner_name = stringprep:tolower(Owner#jid.user),
925 + peer_name = stringprep:tolower(Peer#jid.user),
926 + peer_server = stringprep:tolower(Peer#jid.server),
927 + peer_resource = Peer#jid.resource,
928 + direction = Direction,
929 + type = misc:atom_to_binary(Type),
930 + subject = SubjectText,
932 +packet_parse(_, _, _, _, _) ->
935 +% called from handle_cast({addlog, _}, _) -> true (log messages) | false (do not log messages)
936 +filter(Owner, Peer, State) ->
937 + OwnerBin = << (Owner#jid.luser)/binary, "@", (Owner#jid.lserver)/binary >>,
938 + OwnerServ = << "@", (Owner#jid.lserver)/binary >>,
939 + PeerBin = << (Peer#jid.luser)/binary, "@", (Peer#jid.lserver)/binary >>,
940 + PeerServ = << "@", (Peer#jid.lserver)/binary >>,
942 + LogTo = case ets:match_object(ets_settings_table(State#state.vhost),
943 + #user_settings{owner_name=Owner#jid.luser, _='_'}) of
944 + [#user_settings{dolog_default=Default,
946 + donotlog_list=DNLL}] ->
948 + A = lists:member(PeerBin, DLL),
949 + B = lists:member(PeerBin, DNLL),
953 + Default == true -> true;
954 + Default == false -> false;
955 + true -> State#state.dolog_default
957 + _ -> State#state.dolog_default
959 + lists:all(fun(O) -> O end,
960 + [not lists:member(OwnerBin, State#state.ignore_jids),
961 + not lists:member(PeerBin, State#state.ignore_jids),
962 + not lists:member(OwnerServ, State#state.ignore_jids),
963 + not lists:member(PeerServ, State#state.ignore_jids),
966 +purge_old_records(VHost, Days) ->
967 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
969 + Dates = ?MODULE:get_dates(VHost),
970 + DateNow = calendar:datetime_to_gregorian_seconds({date(), {0,0,1}}),
971 + DateDiff = list_to_integer(Days)*24*60*60,
972 + ?MYDEBUG("Purging tables older than ~s days", [Days]),
973 + lists:foreach(fun(Date) ->
974 + [Year, Month, Day] = ejabberd_regexp:split(iolist_to_binary(Date), <<"[^0-9]+">>),
975 + DateInSec = calendar:datetime_to_gregorian_seconds({{binary_to_integer(Year), binary_to_integer(Month), binary_to_integer(Day)}, {0,0,1}}),
977 + (DateNow - DateInSec) > DateDiff ->
978 + gen_server:call(Proc, {delete_messages_at, Date});
980 + ?MYDEBUG("Skipping messages at ~p", [Date])
984 +% called from get_vhost_stats/2, get_user_stats/3
985 +sort_stats(Stats) ->
986 + % Stats = [{"2003-4-15",1}, {"2006-8-18",1}, ... ]
987 + CFun = fun({TableName, Count}) ->
988 + [Year, Month, Day] = ejabberd_regexp:split(iolist_to_binary(TableName), <<"[^0-9]+">>),
989 + { calendar:datetime_to_gregorian_seconds({{binary_to_integer(Year), binary_to_integer(Month), binary_to_integer(Day)}, {0,0,1}}), Count }
991 + % convert to [{63364377601,1}, {63360662401,1}, ... ]
992 + CStats = lists:map(CFun, Stats),
994 + SortedStats = lists:reverse(lists:keysort(1, CStats)),
995 + % convert to [{"2007-12-9",1}, {"2007-10-27",1}, ... ] sorted list
996 + [{mod_logdb:convert_timestamp_brief(TableSec), Count} || {TableSec, Count} <- SortedStats].
998 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1000 +% Date/Time operations
1002 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1003 +% return float seconds elapsed from "zero hour" as list
1005 + {MegaSec, Sec, MicroSec} = now(),
1006 + [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
1009 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string
1010 +convert_timestamp(Seconds) when is_list(Seconds) ->
1011 + case string:to_float(Seconds++".0") of
1012 + {F,_} when is_float(F) -> convert_timestamp(F);
1013 + _ -> erlang:error(badarg, [Seconds])
1015 +convert_timestamp(Seconds) when is_float(Seconds) ->
1016 + GregSec = trunc(Seconds + 719528*86400),
1017 + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
1018 + {{Year, Month, Day},{Hour, Minute, Sec}} = calendar:universal_time_to_local_time(UnivDT),
1019 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day) ++ " " ++ integer_to_list(Hour) ++ ":" ++ integer_to_list(Minute) ++ ":" ++ integer_to_list(Sec).
1021 +% convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d" string
1022 +convert_timestamp_brief(Seconds) when is_list(Seconds) ->
1023 + convert_timestamp_brief(list_to_float(Seconds));
1024 +convert_timestamp_brief(Seconds) when is_float(Seconds) ->
1025 + GregSec = trunc(Seconds + 719528*86400),
1026 + UnivDT = calendar:gregorian_seconds_to_datetime(GregSec),
1027 + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:universal_time_to_local_time(UnivDT),
1028 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day);
1029 +convert_timestamp_brief(Seconds) when is_integer(Seconds) ->
1030 + {{Year, Month, Day},{_Hour, _Minute, _Sec}} = calendar:gregorian_seconds_to_datetime(Seconds),
1031 + integer_to_list(Year) ++ "-" ++ integer_to_list(Month) ++ "-" ++ integer_to_list(Day).
1033 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1035 +% DB operations (get)
1037 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1038 +get_vhost_stats(VHost) ->
1039 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1040 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
1042 +get_vhost_stats_at(VHost, Date) ->
1043 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1044 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
1046 +get_user_stats(User, VHost) ->
1047 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1048 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
1050 +get_user_messages_at(User, VHost, Date) ->
1051 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1052 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
1054 +get_dates(VHost) ->
1055 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1056 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
1058 +get_user_settings(User, VHost) ->
1059 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1060 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
1062 +set_user_settings(User, VHost, Set) ->
1063 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1064 + gen_server:call(Proc, {set_user_settings, User, Set}).
1066 +get_module_settings(VHost) ->
1067 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1068 + gen_server:call(Proc, {get_module_settings}).
1070 +set_module_settings(VHost, Settings) ->
1071 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1072 + gen_server:call(Proc, {set_module_settings, Settings}).
1074 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1076 +% Web admin callbacks (delete)
1078 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1079 +user_messages_at_parse_query(VHost, Date, Msgs, Query) ->
1080 + case lists:keysearch(<<"delete">>, 1, Query) of
1082 + PMsgs = lists:filter(
1084 + ID = misc:encode_base64(term_to_binary(Msg#msg.timestamp)),
1085 + lists:member({<<"selected">>, ID}, Query)
1087 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1088 + gen_server:call(Proc, {delete_messages_by_user_at, PMsgs, Date}, ?CALL_TIMEOUT);
1093 +user_messages_parse_query(User, VHost, Query) ->
1094 + case lists:keysearch(<<"delete">>, 1, Query) of
1096 + Dates = get_dates(VHost),
1097 + PDates = lists:filter(
1099 + ID = misc:encode_base64( << User/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(
1106 + [gen_server:call(Proc,
1107 + {delete_all_messages_by_user_at, User, iolist_to_binary(Date)},
1110 + case lists:member(error, Rez) of
1120 +vhost_messages_parse_query(VHost, Query) ->
1121 + case lists:keysearch(<<"delete">>, 1, Query) of
1123 + Dates = get_dates(VHost),
1124 + PDates = lists:filter(
1126 + ID = misc:encode_base64( << VHost/binary, (iolist_to_binary(Date))/binary >> ),
1127 + lists:member({<<"selected">>, ID}, Query)
1129 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1130 + Rez = lists:foldl(fun(Date, Acc) ->
1131 + lists:append(Acc, [gen_server:call(Proc,
1132 + {delete_messages_at, Date},
1135 + case lists:member(error, Rez) of
1145 +vhost_messages_at_parse_query(VHost, Date, Stats, Query) ->
1146 + case lists:keysearch(<<"delete">>, 1, Query) of
1148 + PStats = lists:filter(
1149 + fun({User, _Count}) ->
1150 + ID = misc:encode_base64( << (iolist_to_binary(User))/binary, VHost/binary >> ),
1151 + lists:member({<<"selected">>, ID}, Query)
1153 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
1154 + Rez = lists:foldl(fun({User, _Count}, Acc) ->
1155 + lists:append(Acc, [gen_server:call(Proc,
1156 + {delete_all_messages_by_user_at,
1157 + iolist_to_binary(User), iolist_to_binary(Date)},
1160 + case lists:member(error, Rez) of
1170 +copy_messages([#state{vhost=VHost}=State, From, DatesIn]) ->
1171 + {FromDBName, FromDBOpts} =
1172 + case lists:keysearch(misc:binary_to_atom(From), 1, State#state.dbs) of
1173 + {value, {FN, FO}} ->
1176 + ?ERROR_MSG("Failed to find record for ~p in dbs", [From]),
1180 + FromDBMod = list_to_atom(atom_to_list(?MODULE) ++ "_" ++ atom_to_list(FromDBName)),
1182 + {ok, _FromPid} = FromDBMod:start(VHost, FromDBOpts),
1184 + Dates = case DatesIn of
1185 + [] -> FromDBMod:get_dates(VHost);
1189 + DatesLength = length(Dates),
1191 + catch lists:foldl(fun(Date, Acc) ->
1192 + case catch copy_messages_int([FromDBMod, State#state.dbmod, VHost, Date]) of
1194 + ?INFO_MSG("Copied messages at ~p (~p/~p)", [Date, Acc, DatesLength]);
1196 + ?ERROR_MSG("Failed to copy messages at ~p (~p/~p): ~p", [Date, Acc, DatesLength, Value]),
1201 + ?INFO_MSG("copy_messages from ~p finished", [From]),
1202 + FromDBMod:stop(VHost).
1204 +copy_messages_int([FromDBMod, ToDBMod, VHost, Date]) ->
1205 + ets:new(mod_logdb_temp, [named_table, set, public]),
1206 + {Time, Value} = timer:tc(?MODULE, copy_messages_int_tc, [[FromDBMod, ToDBMod, VHost, Date]]),
1207 + ets:delete_all_objects(mod_logdb_temp),
1208 + ets:delete(mod_logdb_temp),
1209 + ?INFO_MSG("copy_messages at ~p elapsed ~p sec", [Date, Time/1000000]),
1212 +copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) ->
1213 + ?INFO_MSG("Going to copy messages from ~p for ~p at ~p", [FromDBMod, VHost, Date]),
1215 + ok = FromDBMod:rebuild_stats_at(VHost, Date),
1216 + catch mod_logdb:rebuild_stats_at(VHost, Date),
1217 + {ok, FromStats} = FromDBMod:get_vhost_stats_at(VHost, Date),
1218 + ToStats = case mod_logdb:get_vhost_stats_at(VHost, iolist_to_binary(Date)) of
1219 + {ok, Stats} -> Stats;
1223 + FromStatsS = lists:keysort(1, FromStats),
1224 + ToStatsS = lists:keysort(1, ToStats),
1226 + StatsLength = length(FromStats),
1229 + % destination table is empty
1231 + fun({User, _Count}, Acc) ->
1232 + {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
1234 + lists:foldl(fun(Msg, MFAcc) ->
1235 + MsgBinary = Msg#msg{owner_name=iolist_to_binary(User),
1236 + peer_name=iolist_to_binary(Msg#msg.peer_name),
1237 + peer_server=iolist_to_binary(Msg#msg.peer_server),
1238 + peer_resource=iolist_to_binary(Msg#msg.peer_resource),
1239 + type=iolist_to_binary(Msg#msg.type),
1240 + subject=iolist_to_binary(Msg#msg.subject),
1241 + body=iolist_to_binary(Msg#msg.body)},
1242 + ok = ToDBMod:log_message(VHost, MsgBinary),
1246 + ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
1247 + %timer:sleep(100),
1250 + % destination table is not empty
1252 + fun({User, _Count}, Acc) ->
1253 + {ok, ToMsgs} = ToDBMod:get_user_messages_at(User, VHost, Date),
1254 + lists:foreach(fun(#msg{timestamp=Tst}) when length(Tst) == 16 ->
1255 + ets:insert(mod_logdb_temp, {Tst});
1256 + % mysql, pgsql removes final zeros after decimal point
1257 + (#msg{timestamp=Tst}) when length(Tst) < 16 ->
1258 + {F, _} = string:to_float(Tst++".0"),
1259 + [T] = io_lib:format("~.5f", [F]),
1260 + ets:insert(mod_logdb_temp, {T})
1262 + {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
1264 + lists:foldl(fun(#msg{timestamp=ToTimestamp} = Msg, MFAcc) ->
1265 + case ets:member(mod_logdb_temp, ToTimestamp) of
1267 + MsgBinary = Msg#msg{owner_name=iolist_to_binary(User),
1268 + peer_name=iolist_to_binary(Msg#msg.peer_name),
1269 + peer_server=iolist_to_binary(Msg#msg.peer_server),
1270 + peer_resource=iolist_to_binary(Msg#msg.peer_resource),
1271 + type=iolist_to_binary(Msg#msg.type),
1272 + subject=iolist_to_binary(Msg#msg.subject),
1273 + body=iolist_to_binary(Msg#msg.body)},
1274 + ok = ToDBMod:log_message(VHost, MsgBinary),
1275 + ets:insert(mod_logdb_temp, {ToTimestamp}),
1282 + ets:delete_all_objects(mod_logdb_temp),
1283 + ?INFO_MSG("Copied ~p messages for ~p (~p/~p) at ~p", [MAcc, User, NewAcc, StatsLength, Date]),
1284 + %timer:sleep(100),
1290 + FromStats == [] ->
1291 + ?INFO_MSG("No messages were found at ~p", [Date]);
1292 + FromStatsS == ToStatsS ->
1293 + ?INFO_MSG("Stats are equal at ~p", [Date]);
1294 + FromStatsS /= ToStatsS ->
1295 + lists:foldl(CopyFun, 0, FromStats),
1296 + ok = ToDBMod:rebuild_stats_at(VHost, Date)
1297 + %timer:sleep(1000)
1302 +list_to_bool(Num) when is_binary(Num) ->
1303 + list_to_bool(binary_to_list(Num));
1304 +list_to_bool(Num) when is_list(Num) ->
1305 + case lists:member(Num, ["t", "true", "y", "yes", "1"]) of
1309 + case lists:member(Num, ["f", "false", "n", "no", "0"]) of
1317 +bool_to_list(true) ->
1319 +bool_to_list(false) ->
1322 +list_to_string([]) ->
1324 +list_to_string(List) when is_list(List) ->
1325 + Str = lists:flatmap(fun(Elm) when is_binary(Elm) ->
1326 + binary_to_list(Elm) ++ "\n";
1327 + (Elm) when is_list(Elm) ->
1330 + lists:sublist(Str, length(Str)-1).
1332 +string_to_list(null) ->
1334 +string_to_list([]) ->
1336 +string_to_list(String) ->
1337 + ejabberd_regexp:split(iolist_to_binary(String), <<"\n">>).
1339 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1341 +% ad-hoc (copy/pasted from mod_configure.erl)
1343 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1344 +-define(ITEMS_RESULT(Allow, LNode, Fallback),
1348 + case get_local_items(LServer, LNode,
1349 + jid:encode(To), Lang) of
1350 + {result, Res} -> {result, Res};
1351 + {error, Error} -> {error, Error}
1355 +get_local_items(Acc, From, #jid{lserver = LServer} = To,
1357 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1360 + Items = case Acc of
1361 + {result, Its} -> Its;
1364 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1365 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1367 + AllowUser == allow; AllowAdmin == allow ->
1368 + case get_local_items(LServer, [],
1369 + jid:encode(To), Lang) of
1371 + {result, Items ++ Res};
1372 + {error, _Error} ->
1379 +get_local_items(Acc, From, #jid{lserver = LServer} = To,
1381 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1384 + LNode = tokenize(Node),
1385 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1386 + Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang),
1388 + [<<"mod_logdb">>] ->
1389 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1390 + [<<"mod_logdb_users">>] ->
1391 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1392 + [<<"mod_logdb_users">>, <<$@, _/binary>>] ->
1393 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1394 + [<<"mod_logdb_users">>, _User] ->
1395 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1396 + [<<"mod_logdb_settings">>] ->
1397 + ?ITEMS_RESULT(AllowAdmin, LNode, {error, Err});
1403 +-define(T(Lang, Text), translate:translate(Lang, Text)).
1405 +-define(NODE(Name, Node),
1406 + #disco_item{jid = jid:make(Server),
1408 + name = ?T(Lang, Name)}).
1410 +-define(NS_ADMINX(Sub),
1411 + <<(?NS_ADMIN)/binary, "#", Sub/binary>>).
1413 +tokenize(Node) -> str:tokens(Node, <<"/#">>).
1415 +get_local_items(_Host, [], Server, Lang) ->
1417 + [?NODE(<<"Messages logging engine">>, <<"mod_logdb">>)]
1419 +get_local_items(_Host, [<<"mod_logdb">>], Server, Lang) ->
1421 + [?NODE(<<"Messages logging engine users">>, <<"mod_logdb_users">>),
1422 + ?NODE(<<"Messages logging engine settings">>, <<"mod_logdb_settings">>)]
1424 +get_local_items(Host, [<<"mod_logdb_users">>], Server, _Lang) ->
1425 + {result, get_all_vh_users(Host, Server)};
1426 +get_local_items(Host, [<<"mod_logdb_users">>, <<$@, Diap/binary>>], Server, Lang) ->
1427 + Users = ejabberd_auth:get_vh_registered_users(Host),
1428 + SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1430 + [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>),
1431 + N1 = binary_to_integer(S1),
1432 + N2 = binary_to_integer(S2),
1433 + Sub = lists:sublist(SUsers, N1, N2 - N1 + 1),
1434 + {result, lists:map(fun({S, U}) ->
1435 + ?NODE(<< U/binary, "@", S/binary >>,
1436 + << (iolist_to_binary("mod_logdb_users/"))/binary, U/binary, "@", S/binary >>)
1439 + xmpp:err_not_acceptable()
1441 +get_local_items(_Host, [<<"mod_logdb_users">>, _User], _Server, _Lang) ->
1443 +get_local_items(_Host, [<<"mod_logdb_settings">>], _Server, _Lang) ->
1445 +get_local_items(_Host, Item, _Server, _Lang) ->
1446 + ?MYDEBUG("asked for items in ~p", [Item]),
1447 + {error, xmpp:err_item_not_found()}.
1449 +-define(INFO_RESULT(Allow, Feats, Lang),
1451 + deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
1452 + allow -> {result, Feats}
1455 +get_local_features(Acc, From,
1456 + #jid{lserver = LServer} = _To, Node, Lang) ->
1457 + case gen_mod:is_loaded(LServer, mod_adhoc) of
1461 + LNode = tokenize(Node),
1462 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1463 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1465 + [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
1466 + ?INFO_RESULT(allow, [?NS_COMMANDS], Lang);
1467 + [<<"mod_logdb">>] ->
1468 + ?INFO_RESULT(deny, [?NS_COMMANDS], Lang);
1469 + [<<"mod_logdb_users">>] ->
1470 + ?INFO_RESULT(AllowAdmin, [], Lang);
1471 + [<<"mod_logdb_users">>, [$@ | _]] ->
1472 + ?INFO_RESULT(AllowAdmin, [], Lang);
1473 + [<<"mod_logdb_users">>, _User] ->
1474 + ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
1475 + [<<"mod_logdb_settings">>] ->
1476 + ?INFO_RESULT(AllowAdmin, [?NS_COMMANDS], Lang);
1484 +-define(INFO_IDENTITY(Category, Type, Name, Lang),
1485 + [#identity{category = Category, type = Type, name = ?T(Lang, Name)}]).
1487 +-define(INFO_COMMAND(Name, Lang),
1488 + ?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
1491 +get_local_identity(Acc, _From, _To, Node, Lang) ->
1492 + LNode = tokenize(Node),
1494 + [<<"mod_logdb">>] ->
1495 + ?INFO_COMMAND(<<"Messages logging engine">>, Lang);
1496 + [<<"mod_logdb_users">>] ->
1497 + ?INFO_COMMAND(<<"Messages logging engine users">>, Lang);
1498 + [<<"mod_logdb_users">>, User] ->
1499 + ?INFO_COMMAND(User, Lang);
1500 + [<<"mod_logdb_settings">>] ->
1501 + ?INFO_COMMAND(<<"Messages logging engine settings">>, Lang);
1506 +adhoc_local_items(Acc, From,
1507 + #jid{lserver = LServer, server = Server} = To, Lang) ->
1508 + % TODO: case acl:match_rule(LServer, ???, From) of
1509 + Items = case Acc of
1510 + {result, Its} -> Its;
1513 + Nodes = recursively_get_local_items(LServer,
1514 + <<"">>, Server, Lang),
1515 + Nodes1 = lists:filter(
1516 + fun(#disco_item{node = Nd}) ->
1517 + F = get_local_features([], From, To, Nd, Lang),
1519 + {result, [?NS_COMMANDS]} -> true;
1523 + {result, Items ++ Nodes1}.
1525 +recursively_get_local_items(_LServer,
1526 + <<"mod_logdb_users">>, _Server, _Lang) ->
1528 +recursively_get_local_items(LServer,
1529 + Node, Server, Lang) ->
1530 + LNode = tokenize(Node),
1531 + Items = case get_local_items(LServer, LNode,
1533 + {result, Res} -> Res;
1534 + {error, _Error} -> []
1536 + Nodes = lists:flatten(
1538 + fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
1539 + if (S /= Server) or (Nd == <<"">>) ->
1542 + [Item, recursively_get_local_items(
1543 + LServer, Nd, Server, Lang)]
1548 +-define(COMMANDS_RESULT(Allow, From, To, Request),
1551 + {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
1553 + adhoc_local_commands(From, To, Request)
1556 +adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
1557 + #adhoc_command{node = Node, lang = Lang} = Request) ->
1558 + LNode = tokenize(Node),
1559 + AllowUser = acl:match_rule(LServer, mod_logdb, From),
1560 + AllowAdmin = acl:match_rule(LServer, mod_logdb_admin, From),
1562 + [<<"mod_logdb">>] when AllowUser == allow; AllowAdmin == allow ->
1563 + ?COMMANDS_RESULT(allow, From, To, Request);
1564 + [<<"mod_logdb_users">>, <<$@, _/binary>>] when AllowAdmin == allow ->
1566 + [<<"mod_logdb_users">>, _User] when AllowAdmin == allow ->
1567 + ?COMMANDS_RESULT(allow, From, To, Request);
1568 + [<<"mod_logdb_settings">>] when AllowAdmin == allow ->
1569 + ?COMMANDS_RESULT(allow, From, To, Request);
1574 +adhoc_local_commands(From, #jid{lserver = LServer} = _To,
1575 + #adhoc_command{lang = Lang,
1579 + xdata = XData} = Request) ->
1580 + LNode = tokenize(Node),
1581 + %% If the "action" attribute is not present, it is
1582 + %% understood as "execute". If there was no <actions/>
1583 + %% element in the first response (which there isn't in our
1584 + %% case), "execute" and "complete" are equivalent.
1585 + ActionIsExecute = Action == execute orelse Action == complete,
1586 + if Action == cancel ->
1587 + %% User cancels request
1588 + #adhoc_command{status = canceled, lang = Lang,
1589 + node = Node, sid = SessionID};
1590 + XData == undefined, ActionIsExecute ->
1591 + %% User requests form
1592 + case get_form(LServer, LNode, Lang) of
1594 + xmpp_util:make_adhoc_response(
1596 + #adhoc_command{status = executing,
1601 + XData /= undefined, ActionIsExecute ->
1602 + %% User returns form.
1603 + case catch set_form(From, LServer, LNode, Lang, XData) of
1605 + xmpp_util:make_adhoc_response(
1607 + #adhoc_command{xdata = Res, status = completed});
1608 + {'EXIT', _} -> {error, xmpp:err_bad_request()};
1609 + {error, Error} -> {error, Error}
1612 + {error, xmpp:err_bad_request(<<"Unexpected action">>, Lang)}
1615 +-define(TVFIELD(Type, Var, Val),
1616 + #xdata_field{type = Type, var = Var, values = [Val]}).
1619 + ?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))).
1621 +get_user_form(LUser, LServer, Lang) ->
1622 + ?MYDEBUG("get_user_form ~p ~p", [LUser, LServer]),
1623 + %From = jid:encode(jid:remove_resource(Jid)),
1624 + #user_settings{dolog_default=DLD,
1626 + donotlog_list=DNLL} = get_user_settings(LUser, LServer),
1629 + type = 'list-single',
1630 + label = ?T(Lang, <<"Default">>),
1631 + var = <<"dolog_default">>,
1632 + values = [misc:atom_to_binary(DLD)],
1633 + options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
1634 + value = <<"true">>},
1635 + #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
1636 + value = <<"false">>}]},
1638 + type = 'text-multi',
1639 + label = ?T(Lang, <<"Log Messages">>),
1640 + var = <<"dolog_list">>,
1643 + type = 'text-multi',
1644 + label = ?T(Lang, <<"Do Not Log Messages">>),
1645 + var = <<"donotlog_list">>,
1649 + title = ?T(Lang, <<"Messages logging engine settings">>),
1651 + instructions = [<< (?T(Lang, <<"Set logging preferences">>))/binary,
1652 + (iolist_to_binary(": "))/binary,
1653 + LUser/binary, "@", LServer/binary >>],
1654 + fields = [?HFIELD()|
1657 +get_settings_form(Host, Lang) ->
1658 + ?MYDEBUG("get_settings_form ~p ~p", [Host, Lang]),
1659 + #state{dbmod=_DBMod,
1661 + dolog_default=DLD,
1662 + ignore_jids=IgnoreJids,
1663 + groupchat=GroupChat,
1664 + purge_older_days=PurgeDaysT,
1665 + drop_messages_on_user_removal=MRemoval,
1666 + poll_users_settings=PollTime} = mod_logdb:get_module_settings(Host),
1669 + case PurgeDaysT of
1670 + never -> <<"never">>;
1671 + Num when is_integer(Num) -> integer_to_binary(Num);
1672 + _ -> <<"unknown">>
1676 + type = 'list-single',
1677 + label = ?T(Lang, <<"Default">>),
1678 + var = <<"dolog_default">>,
1679 + values = [misc:atom_to_binary(DLD)],
1680 + options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
1681 + value = <<"true">>},
1682 + #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
1683 + value = <<"false">>}]},
1685 + type = 'list-single',
1686 + label = ?T(Lang, <<"Drop messages on user removal">>),
1687 + var = <<"drop_messages_on_user_removal">>,
1688 + values = [misc:atom_to_binary(MRemoval)],
1689 + options = [#xdata_option{label = ?T(Lang, <<"Drop">>),
1690 + value = <<"true">>},
1691 + #xdata_option{label = ?T(Lang, <<"Do not drop">>),
1692 + value = <<"false">>}]},
1694 + type = 'list-single',
1695 + label = ?T(Lang, <<"Groupchat messages logging">>),
1696 + var = <<"groupchat">>,
1697 + values = [misc:atom_to_binary(GroupChat)],
1698 + options = [#xdata_option{label = ?T(Lang, <<"all">>),
1699 + value = <<"all">>},
1700 + #xdata_option{label = ?T(Lang, <<"none">>),
1701 + value = <<"none">>},
1702 + #xdata_option{label = ?T(Lang, <<"send">>),
1703 + value = <<"send">>}]},
1705 + type = 'text-multi',
1706 + label = ?T(Lang, <<"Jids/Domains to ignore">>),
1707 + var = <<"ignore_list">>,
1708 + values = IgnoreJids},
1710 + type = 'text-single',
1711 + label = ?T(Lang, <<"Purge messages older than (days)">>),
1712 + var = <<"purge_older_days">>,
1713 + values = [iolist_to_binary(PurgeDays)]},
1715 + type = 'text-single',
1716 + label = ?T(Lang, <<"Poll users settings (seconds)">>),
1717 + var = <<"poll_users_settings">>,
1718 + values = [integer_to_binary(PollTime)]}
1721 + title = ?T(Lang, <<"Messages logging engine settings (run-time)">>),
1722 + instructions = [?T(Lang, <<"Set run-time settings">>)],
1724 + fields = [?HFIELD()|
1727 +get_form(_Host, [<<"mod_logdb_users">>, User], Lang) ->
1728 + #jid{luser=LUser, lserver=LServer} = jid:decode(User),
1729 + get_user_form(LUser, LServer, Lang);
1730 +get_form(Host, [<<"mod_logdb_settings">>], Lang) ->
1731 + get_settings_form(Host, Lang);
1732 +get_form(_Host, Command, _Lang) ->
1733 + ?MYDEBUG("asked for form ~p", [Command]),
1734 + {error, xmpp:err_service_unavailable()}.
1736 +check_log_list([]) ->
1738 +check_log_list([<<>>]) ->
1740 +check_log_list([Head | Tail]) ->
1741 + case binary:match(Head, <<$@>>) of
1742 + nomatch -> throw(error);
1745 + % this check for Head to be valid jid
1746 + case catch jid:decode(Head) of
1747 + {'EXIT', _Reason} -> throw(error);
1748 + _ -> check_log_list(Tail)
1751 +check_ignore_list([]) ->
1753 +check_ignore_list([<<>>]) ->
1755 +check_ignore_list([<<>> | Tail]) ->
1756 + check_ignore_list(Tail);
1757 +check_ignore_list([Head | Tail]) ->
1758 + case binary:match(Head, <<$@>>) of
1760 + nomatch -> throw(error)
1762 + Jid2Test = case Head of
1763 + << $@, _Rest/binary >> -> << "a", Head/binary >>;
1766 + % this check for Head to be valid jid
1767 + case catch jid:decode(Jid2Test) of
1768 + {'EXIT', _Reason} -> throw(error);
1769 + _ -> check_ignore_list(Tail)
1772 +get_value(Field, XData) -> hd(get_values(Field, XData)).
1774 +get_values(Field, XData) ->
1775 + xmpp_util:get_xdata_values(Field, XData).
1777 +parse_users_settings(XData) ->
1778 + DLD = case get_value(<<"dolog_default">>, XData) of
1779 + ValueDLD when ValueDLD == <<"true">>;
1780 + ValueDLD == <<"false">> ->
1781 + list_to_bool(ValueDLD);
1782 + _ -> throw(bad_request)
1785 + ListDLL = get_values(<<"dolog_list">>, XData),
1786 + DLL = case catch check_log_list(ListDLL) of
1788 + error -> throw(bad_request)
1791 + ListDNLL = get_values(<<"donotlog_list">>, XData),
1792 + DNLL = case catch check_log_list(ListDNLL) of
1794 + error -> throw(bad_request)
1797 + #user_settings{dolog_default=DLD,
1799 + donotlog_list=DNLL}.
1801 +parse_module_settings(XData) ->
1802 + DLD = case get_value(<<"dolog_default">>, XData) of
1803 + ValueDLD when ValueDLD == <<"true">>;
1804 + ValueDLD == <<"false">> ->
1805 + list_to_bool(ValueDLD);
1806 + _ -> throw(bad_request)
1808 + MRemoval = case get_value(<<"drop_messages_on_user_removal">>, XData) of
1809 + ValueMRemoval when ValueMRemoval == <<"true">>;
1810 + ValueMRemoval == <<"false">> ->
1811 + list_to_bool(ValueMRemoval);
1812 + _ -> throw(bad_request)
1814 + GroupChat = case get_value(<<"groupchat">>, XData) of
1815 + ValueGroupChat when ValueGroupChat == <<"none">>;
1816 + ValueGroupChat == <<"all">>;
1817 + ValueGroupChat == <<"send">> ->
1818 + misc:binary_to_atom(ValueGroupChat);
1819 + _ -> throw(bad_request)
1821 + ListIgnore = get_values(<<"ignore_list">>, XData),
1822 + Ignore = case catch check_ignore_list(ListIgnore) of
1824 + error -> throw(bad_request)
1826 + Purge = case get_value(<<"purge_older_days">>, XData) of
1827 + <<"never">> -> never;
1829 + case catch binary_to_integer(ValuePurge) of
1830 + IntValuePurge when is_integer(IntValuePurge) -> IntValuePurge;
1831 + _ -> throw(bad_request)
1834 + Poll = case catch binary_to_integer(get_value(<<"poll_users_settings">>, XData)) of
1835 + IntValuePoll when is_integer(IntValuePoll) -> IntValuePoll;
1836 + _ -> throw(bad_request)
1838 + #state{dolog_default=DLD,
1839 + groupchat=GroupChat,
1840 + ignore_jids=Ignore,
1841 + purge_older_days=Purge,
1842 + drop_messages_on_user_removal=MRemoval,
1843 + poll_users_settings=Poll}.
1845 +set_form(_From, _Host, [<<"mod_logdb_users">>, User], Lang, XData) ->
1846 + #jid{luser=LUser, lserver=LServer} = jid:decode(User),
1847 + Txt = "Parse user settings failed",
1848 + case catch parse_users_settings(XData) of
1850 + ?ERROR_MSG("Failed to set user form: bad_request", []),
1851 + {error, xmpp:err_bad_request(Txt, Lang)};
1852 + {'EXIT', Reason} ->
1853 + ?ERROR_MSG("Failed to set user form ~p", [Reason]),
1854 + {error, xmpp:err_bad_request(Txt, Lang)};
1856 + case mod_logdb:set_user_settings(LUser, LServer, UserSettings) of
1858 + {result, undefined};
1860 + {error, xmpp:err_internal_server_error()}
1863 +set_form(_From, Host, [<<"mod_logdb_settings">>], Lang, XData) ->
1864 + Txt = "Parse module settings failed",
1865 + case catch parse_module_settings(XData) of
1867 + ?ERROR_MSG("Failed to set settings form: bad_request", []),
1868 + {error, xmpp:err_bad_request(Txt, Lang)};
1869 + {'EXIT', Reason} ->
1870 + ?ERROR_MSG("Failed to set settings form ~p", [Reason]),
1871 + {error, xmpp:err_bad_request(Txt, Lang)};
1873 + case mod_logdb:set_module_settings(Host, Settings) of
1875 + {result, undefined};
1877 + {error, xmpp:err_internal_server_error()}
1880 +set_form(From, _Host, Node, _Lang, XData) ->
1881 + User = jid:encode(jid:remove_resource(From)),
1882 + ?MYDEBUG("set form for ~p at ~p XData=~p", [User, Node, XData]),
1883 + {error, xmpp:err_service_unavailable()}.
1885 +get_all_vh_users(Host, Server) ->
1886 + case catch ejabberd_auth:get_vh_registered_users(Host) of
1887 + {'EXIT', _Reason} ->
1890 + SUsers = lists:sort([{S, U} || {U, S} <- Users]),
1891 + case length(SUsers) of
1892 + N when N =< 100 ->
1893 + lists:map(fun({S, U}) ->
1894 + #disco_item{jid = jid:make(Server),
1895 + node = <<"mod_logdb_users/", U/binary, $@, S/binary>>,
1896 + name = << U/binary, "@", S/binary >>}
1899 + NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1,
1900 + M = trunc(N / NParts) + 1,
1901 + lists:map(fun(K) ->
1904 + (integer_to_binary(K))/binary,
1906 + (integer_to_binary(L))/binary
1908 + {FS, FU} = lists:nth(K, SUsers),
1910 + if L < N -> lists:nth(L, SUsers);
1911 + true -> lists:last(SUsers)
1914 + <<FU/binary, "@", FS/binary,
1916 + LU/binary, "@", LS/binary>>,
1917 + #disco_item{jid = jid:make(Host),
1918 + node = <<"mod_logdb_users/", Node/binary>>,
1920 + end, lists:seq(1, N, M))
1924 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1928 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1929 +webadmin_menu(Acc, _Host, Lang) ->
1930 + [{<<"messages">>, ?T(<<"Users Messages">>)} | Acc].
1932 +webadmin_user(Acc, User, Server, Lang) ->
1933 + Sett = get_user_settings(User, Server),
1935 + case Sett#user_settings.dolog_default of
1937 + ?INPUTT(<<"submit">>, <<"dolog">>, <<"Log Messages">>);
1939 + ?INPUTT(<<"submit">>, <<"donotlog">>, <<"Do Not Log Messages">>);
1942 + Acc ++ [?XE(<<"h3">>, [?ACT(<<"messages/">>, <<"Messages">>), ?C(<<" ">>), Log])].
1944 +webadmin_page(_, Host,
1945 + #request{path = [<<"messages">>],
1948 + Res = vhost_messages_stats(Host, Query, Lang),
1950 +webadmin_page(_, Host,
1951 + #request{path = [<<"messages">>, Date],
1954 + Res = vhost_messages_stats_at(Host, Query, Lang, Date),
1956 +webadmin_page(_, Host,
1957 + #request{path = [<<"user">>, U, <<"messages">>],
1960 + Res = user_messages_stats(U, Host, Query, Lang),
1962 +webadmin_page(_, Host,
1963 + #request{path = [<<"user">>, U, <<"messages">>, Date],
1966 + Res = mod_logdb:user_messages_stats_at(U, Host, Query, Lang, Date),
1968 +webadmin_page(Acc, _Host, _R) -> Acc.
1970 +user_parse_query(_, <<"dolog">>, User, Server, _Query) ->
1971 + Sett = get_user_settings(User, Server),
1972 + % TODO: check returned value
1973 + set_user_settings(User, Server, Sett#user_settings{dolog_default=true}),
1975 +user_parse_query(_, <<"donotlog">>, User, Server, _Query) ->
1976 + Sett = get_user_settings(User, Server),
1977 + % TODO: check returned value
1978 + set_user_settings(User, Server, Sett#user_settings{dolog_default=false}),
1980 +user_parse_query(Acc, _Action, _User, _Server, _Query) ->
1983 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1987 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1988 +vhost_messages_stats(Server, Query, Lang) ->
1989 + Res = case catch vhost_messages_parse_query(Server, Query) of
1990 + {'EXIT', Reason} ->
1991 + ?ERROR_MSG("~p", [Reason]),
1993 + VResult -> VResult
1995 + {Time, Value} = timer:tc(mod_logdb, get_vhost_stats, [Server]),
1996 + ?INFO_MSG("get_vhost_stats(~p) elapsed ~p sec", [Server, Time/1000000]),
1997 + %case get_vhost_stats(Server) of
1999 + {'EXIT', CReason} ->
2000 + ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
2001 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2002 + {error, GReason} ->
2003 + ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
2004 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2006 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Server])))];
2008 + Fun = fun({Date, Count}) ->
2009 + DateBin = iolist_to_binary(Date),
2010 + ID = misc:encode_base64( << Server/binary, DateBin/binary >> ),
2012 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2013 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2014 + ?XE(<<"td">>, [?AC(DateBin, DateBin)]),
2015 + ?XC(<<"td">>, integer_to_binary(Count))
2019 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s">>), [Server])))] ++
2021 + ok -> [?CT(<<"Submitted">>), ?P];
2022 + error -> [?CT(<<"Bad format">>), ?P];
2025 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2030 + ?XCT(<<"td">>, <<"Date">>),
2031 + ?XCT(<<"td">>, <<"Count">>)
2034 + lists:map(Fun, Dates)
2037 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2041 +vhost_messages_stats_at(Server, Query, Lang, Date) ->
2042 + {Time, Value} = timer:tc(mod_logdb, get_vhost_stats_at, [Server, Date]),
2043 + ?INFO_MSG("get_vhost_stats_at(~p,~p) elapsed ~p sec", [Server, Date, Time/1000000]),
2044 + %case get_vhost_stats_at(Server, Date) of
2046 + {'EXIT', CReason} ->
2047 + ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
2048 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2049 + {error, GReason} ->
2050 + ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
2051 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
2053 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Server, Date])))];
2055 + Res = case catch vhost_messages_at_parse_query(Server, Date, Stats, Query) of
2056 + {'EXIT', Reason} ->
2057 + ?ERROR_MSG("~p", [Reason]),
2059 + VResult -> VResult
2061 + Fun = fun({User, Count}) ->
2062 + UserBin = iolist_to_binary(User),
2063 + ID = misc:encode_base64( << UserBin/binary, Server/binary >> ),
2065 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2066 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2067 + ?XE(<<"td">>, [?AC(<< <<"../user/">>/binary, UserBin/binary, <<"/messages/">>/binary, Date/binary >>, UserBin)]),
2068 + ?XC(<<"td">>, integer_to_binary(Count))
2071 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Server, Date])))] ++
2073 + ok -> [?CT(<<"Submitted">>), ?P];
2074 + error -> [?CT(<<"Bad format">>), ?P];
2077 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2082 + ?XCT(<<"td">>, <<"User">>),
2083 + ?XCT(<<"td">>, <<"Count">>)
2086 + lists:map(Fun, Stats)
2089 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2093 +user_messages_stats(User, Server, Query, Lang) ->
2094 + Jid = jid:encode({User, Server, ""}),
2096 + Res = case catch user_messages_parse_query(User, Server, Query) of
2097 + {'EXIT', Reason} ->
2098 + ?ERROR_MSG("~p", [Reason]),
2100 + VResult -> VResult
2103 + {Time, Value} = timer:tc(mod_logdb, get_user_stats, [User, Server]),
2104 + ?INFO_MSG("get_user_stats(~p,~p) elapsed ~p sec", [User, Server, Time/1000000]),
2107 + {'EXIT', CReason} ->
2108 + ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
2109 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
2110 + {error, GReason} ->
2111 + ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
2112 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
2114 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Jid])))];
2116 + Fun = fun({Date, Count}) ->
2117 + DateBin = iolist_to_binary(Date),
2118 + ID = misc:encode_base64( << User/binary, DateBin/binary >> ),
2120 + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
2121 + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2122 + ?XE(<<"td">>, [?AC(DateBin, DateBin)]),
2123 + ?XC(<<"td">>, iolist_to_binary(integer_to_list(Count)))
2126 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T("Logged messages for ~s"), [Jid])))] ++
2128 + ok -> [?CT(<<"Submitted">>), ?P];
2129 + error -> [?CT(<<"Bad format">>), ?P];
2132 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2137 + ?XCT(<<"td">>, <<"Date">>),
2138 + ?XCT(<<"td">>, <<"Count">>)
2141 + lists:map(Fun, Dates)
2144 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)
2148 +search_user_nick(User, List) ->
2149 + case lists:keysearch(User, 1, List) of
2150 + {value,{User, []}} ->
2152 + {value,{User, Nick}} ->
2158 +user_messages_stats_at(User, Server, Query, Lang, Date) ->
2159 + Jid = jid:encode({User, Server, ""}),
2161 + {Time, Value} = timer:tc(mod_logdb, get_user_messages_at, [User, Server, Date]),
2162 + ?INFO_MSG("get_user_messages_at(~p,~p,~p) elapsed ~p sec", [User, Server, Date, Time/1000000]),
2164 + {'EXIT', CReason} ->
2165 + ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
2166 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
2167 + {error, GReason} ->
2168 + ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
2169 + [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
2171 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Jid, Date])))];
2172 + {ok, User_messages} ->
2173 + Res = case catch user_messages_at_parse_query(Server,
2177 + {'EXIT', Reason} ->
2178 + ?ERROR_MSG("~p", [Reason]),
2180 + VResult -> VResult
2183 + UR = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]),
2185 + lists:map(fun(Item) ->
2186 + {jid:encode(Item#roster.jid), Item#roster.name}
2189 + UniqUsers = lists:foldl(fun(#msg{peer_name=PName, peer_server=PServer}, List) ->
2190 + ToAdd = PName++"@"++PServer,
2191 + case lists:member(ToAdd, List) of
2193 + false -> lists:append([ToAdd], List)
2195 + end, [], User_messages),
2197 + % Users to filter (sublist of UniqUsers)
2198 + CheckedUsers = case lists:keysearch(<<"filter">>, 1, Query) of
2200 + lists:filter(fun(UFUser) ->
2201 + ID = misc:encode_base64(term_to_binary(UFUser)),
2202 + lists:member({<<"selected">>, ID}, Query)
2207 + % UniqUsers in html (noone selected -> everyone selected)
2208 + Users = lists:map(fun(UHUser) ->
2209 + ID = misc:encode_base64(term_to_binary(UHUser)),
2210 + Input = case lists:member(UHUser, CheckedUsers) of
2211 + true -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2212 + false when CheckedUsers == [] -> [?INPUTC(<<"checkbox">>, <<"selected">>, ID)];
2213 + false -> [?INPUT(<<"checkbox">>, <<"selected">>, ID)]
2216 + case search_user_nick(UHUser, UserRoster) of
2217 + nothing -> <<"">>;
2218 + N -> iolist_to_binary( " ("++ N ++")" )
2221 + [?XE(<<"td">>, Input),
2222 + ?XC(<<"td">>, iolist_to_binary(UHUser++Nick))])
2223 + end, lists:sort(UniqUsers)),
2224 + % Messages to show (based on Users)
2225 + User_messages_filtered = case CheckedUsers of
2226 + [] -> User_messages;
2227 + _ -> lists:filter(fun(#msg{peer_name=PName, peer_server=PServer}) ->
2228 + lists:member(PName++"@"++PServer, CheckedUsers)
2229 + end, User_messages)
2232 + Msgs_Fun = fun(#msg{timestamp=Timestamp,
2234 + direction=Direction,
2235 + peer_name=PName, peer_server=PServer, peer_resource=PRes,
2238 + Text = case Subject of
2239 + "" -> iolist_to_binary(Body);
2240 + _ -> iolist_to_binary([binary_to_list(?T(<<"Subject">>)) ++ ": " ++ Subject ++ "\n" ++ Body])
2242 + Resource = case PRes of
2248 + case search_user_nick(PName++"@"++PServer, UserRoster) of
2249 + nothing when PServer == Server ->
2251 + nothing when Type == "groupchat", Direction == from ->
2252 + PName++"@"++PServer++Resource;
2254 + PName++"@"++PServer;
2257 + ID = misc:encode_base64(term_to_binary(Timestamp)),
2259 + [?XE(<<"td">>, [?INPUT(<<"checkbox">>, <<"selected">>, ID)]),
2260 + ?XC(<<"td">>, iolist_to_binary(convert_timestamp(Timestamp))),
2261 + ?XC(<<"td">>, iolist_to_binary(atom_to_list(Direction)++": "++UserNick)),
2262 + ?XE(<<"td">>, [?XC(<<"pre">>, Text)])])
2264 + % Filtered user messages in html
2265 + Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
2267 + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Jid, Date])))] ++
2269 + ok -> [?CT(<<"Submitted">>), ?P];
2270 + error -> [?CT(<<"Bad format">>), ?P];
2273 + [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}],
2277 + ?XCT(<<"td">>, <<"User">>)
2283 + ?INPUTT(<<"submit">>, <<"filter">>, <<"Filter Selected">>)
2289 + ?XCT(<<"td">>, <<"Date, Time">>),
2290 + ?XCT(<<"td">>, <<"Direction: Jid">>),
2291 + ?XCT(<<"td">>, <<"Body">>)
2296 + ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>),
2301 diff --git a/src/mod_logdb.hrl b/src/mod_logdb.hrl
2302 new file mode 100644
2303 index 0000000000..49791f4e69
2305 +++ b/src/mod_logdb.hrl
2307 +%%%----------------------------------------------------------------------
2308 +%%% File : mod_logdb.hrl
2309 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2311 +%%% Url : https://paleg.github.io/mod_logdb/
2312 +%%%----------------------------------------------------------------------
2314 +-define(logdb_debug, true).
2316 +-ifdef(logdb_debug).
2317 +-define(MYDEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n",
2318 + [calendar:local_time(),?MODULE,?LINE]++Args)).
2320 +-define(MYDEBUG(_F,_A),[]).
2323 +-record(msg, {timestamp,
2325 + peer_name, peer_server, peer_resource,
2330 +-record(user_settings, {owner_name,
2333 + donotlog_list=[]}).
2335 +-define(INPUTC(Type, Name, Value),
2336 + ?XA(<<"input">>, [{<<"type">>, Type},
2337 + {<<"name">>, Name},
2338 + {<<"value">>, Value},
2339 + {<<"checked">>, <<"true">>}])).
2340 diff --git a/src/mod_logdb_mnesia.erl b/src/mod_logdb_mnesia.erl
2341 new file mode 100644
2342 index 0000000000..a08d5262c2
2344 +++ b/src/mod_logdb_mnesia.erl
2346 +%%%----------------------------------------------------------------------
2347 +%%% File : mod_logdb_mnesia.erl
2348 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2349 +%%% Purpose : mnesia backend for mod_logdb
2350 +%%% Url : https://paleg.github.io/mod_logdb/
2351 +%%%----------------------------------------------------------------------
2353 +-module(mod_logdb_mnesia).
2354 +-author('o.palij@gmail.com').
2356 +-include("mod_logdb.hrl").
2357 +-include("logger.hrl").
2359 +-behaviour(gen_logdb).
2360 +-behaviour(gen_server).
2363 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2365 +-export([start/2, stop/1]).
2367 +-export([log_message/2,
2369 + rebuild_stats_at/2,
2370 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2371 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2373 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
2376 +-define(PROCNAME, mod_logdb_mnesia).
2377 +-define(CALL_TIMEOUT, 10000).
2379 +-record(state, {vhost}).
2381 +-record(stats, {user, at, count}).
2387 + "_" ++ binary_to_list(VHost).
2389 +stats_table(VHost) ->
2390 + list_to_atom(prefix() ++ "stats" ++ suffix(VHost)).
2392 +table_name(VHost, Date) ->
2393 + list_to_atom(prefix() ++ "messages_" ++ Date ++ suffix(VHost)).
2395 +settings_table(VHost) ->
2396 + list_to_atom(prefix() ++ "settings" ++ suffix(VHost)).
2398 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2400 +% gen_mod callbacks
2402 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2403 +start(VHost, Opts) ->
2404 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2405 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2408 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2409 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2411 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2413 +% gen_server callbacks
2415 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2416 +init([VHost, _Opts]) ->
2417 + case mnesia:system_info(is_running) of
2419 + ok = create_stats_table(VHost),
2420 + ok = create_settings_table(VHost),
2421 + {ok, #state{vhost=VHost}};
2423 + ?ERROR_MSG("Mnesia not running", []),
2424 + {stop, db_connection_failed};
2426 + ?ERROR_MSG("Mnesia status: ~p", [Status]),
2427 + {stop, db_connection_failed}
2430 +handle_call({log_message, Msg}, _From, #state{vhost=VHost}=State) ->
2431 + {reply, log_message_int(VHost, Msg), State};
2432 +handle_call({rebuild_stats}, _From, #state{vhost=VHost}=State) ->
2433 + {atomic, ok} = delete_nonexistent_stats(VHost),
2435 + lists:foreach(fun(Date) ->
2436 + rebuild_stats_at_int(VHost, Date)
2437 + end, get_dates_int(VHost)),
2438 + {reply, Reply, State};
2439 +handle_call({rebuild_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2440 + Reply = rebuild_stats_at_int(VHost, Date),
2441 + {reply, Reply, State};
2442 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{vhost=VHost}=State) ->
2443 + Table = table_name(VHost, Date),
2447 + mnesia:write_lock_table(stats_table(VHost)),
2448 + mnesia:write_lock_table(Table),
2449 + mnesia:delete_object(Table, Msg, write)
2452 + DRez = case mnesia:transaction(Fun) of
2453 + {aborted, Reason} ->
2454 + ?ERROR_MSG("Failed to delete_messages_by_user_at at ~p for ~p: ~p", [Date, VHost, Reason]),
2460 + case rebuild_stats_at_int(VHost, Date) of
2466 + {reply, Reply, State};
2467 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{vhost=VHost}=State) ->
2468 + {reply, delete_all_messages_by_user_at_int(User, VHost, Date), State};
2469 +handle_call({delete_messages_at, Date}, _From, #state{vhost=VHost}=State) ->
2471 + case mnesia:delete_table(table_name(VHost, Date)) of
2473 + delete_stats_by_vhost_at_int(VHost, Date);
2474 + {aborted, Reason} ->
2475 + ?ERROR_MSG("Failed to delete_messages_at for ~p at ~p", [VHost, Date, Reason]),
2478 + {reply, Reply, State};
2479 +handle_call({get_vhost_stats}, _From, #state{vhost=VHost}=State) ->
2480 + Fun = fun(#stats{at=Date, count=Count}, Stats) ->
2481 + case lists:keysearch(Date, 1, Stats) of
2483 + lists:append(Stats, [{Date, Count}]);
2484 + {value, {_, TempCount}} ->
2485 + lists:keyreplace(Date, 1, Stats, {Date, TempCount+Count})
2489 + case mnesia:transaction(fun() ->
2490 + mnesia:foldl(Fun, [], stats_table(VHost))
2492 + {atomic, Result} -> {ok, mod_logdb:sort_stats(Result)};
2493 + {aborted, Reason} -> {error, Reason}
2495 + {reply, Reply, State};
2496 +handle_call({get_vhost_stats_at, Date}, _From, #state{vhost=VHost}=State) ->
2498 + Pat = #stats{user='$1', at=Date, count='$2'},
2499 + mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
2502 + case mnesia:transaction(Fun) of
2503 + {atomic, Result} ->
2504 + {ok, lists:reverse(lists:keysort(2, [{User, Count} || [User, Count] <- Result]))};
2505 + {aborted, Reason} ->
2508 + {reply, Reply, State};
2509 +handle_call({get_user_stats, User}, _From, #state{vhost=VHost}=State) ->
2510 + {reply, get_user_stats_int(User, VHost), State};
2511 +handle_call({get_user_messages_at, User, Date}, _From, #state{vhost=VHost}=State) ->
2513 + case mnesia:transaction(fun() ->
2514 + Pat = #msg{owner_name=User, _='_'},
2515 + mnesia:select(table_name(VHost, Date),
2516 + [{Pat, [], ['$_']}])
2518 + {atomic, Result} -> {ok, Result};
2519 + {aborted, Reason} ->
2522 + {reply, Reply, State};
2523 +handle_call({get_dates}, _From, #state{vhost=VHost}=State) ->
2524 + {reply, get_dates_int(VHost), State};
2525 +handle_call({get_users_settings}, _From, #state{vhost=VHost}=State) ->
2526 + Reply = mnesia:dirty_match_object(settings_table(VHost), #user_settings{_='_'}),
2527 + {reply, {ok, Reply}, State};
2528 +handle_call({get_user_settings, User}, _From, #state{vhost=VHost}=State) ->
2530 + case mnesia:dirty_match_object(settings_table(VHost), #user_settings{owner_name=User, _='_'}) of
2535 + {reply, Reply, State};
2536 +handle_call({set_user_settings, _User, Set}, _From, #state{vhost=VHost}=State) ->
2537 + ?MYDEBUG("~p~n~p", [settings_table(VHost), Set]),
2538 + Reply = mnesia:dirty_write(settings_table(VHost), Set),
2539 + ?MYDEBUG("~p", [Reply]),
2540 + {reply, Reply, State};
2541 +handle_call({drop_user, User}, _From, #state{vhost=VHost}=State) ->
2542 + {ok, Dates} = get_user_stats_int(User, VHost),
2543 + MDResult = lists:map(fun({Date, _}) ->
2544 + delete_all_messages_by_user_at_int(User, VHost, Date)
2546 + SDResult = delete_user_settings_int(User, VHost),
2548 + case lists:all(fun(Result) when Result == ok ->
2550 + (Result) when Result == error ->
2552 + end, lists:append(MDResult, [SDResult])) of
2558 + {reply, Reply, State};
2559 +handle_call({stop}, _From, State) ->
2560 + {stop, normal, ok, State};
2561 +handle_call(Msg, _From, State) ->
2562 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
2565 +handle_cast(Msg, State) ->
2566 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
2569 +handle_info(Info, State) ->
2570 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
2573 +terminate(_Reason, _State) ->
2576 +code_change(_OldVsn, State, _Extra) ->
2579 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2581 +% gen_logdb callbacks
2583 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2584 +log_message(VHost, Msg) ->
2585 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2586 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
2587 +rebuild_stats(VHost) ->
2588 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2589 + gen_server:call(Proc, {rebuild_stats}, ?CALL_TIMEOUT).
2590 +rebuild_stats_at(VHost, Date) ->
2591 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2592 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
2593 +delete_messages_by_user_at(VHost, Msgs, Date) ->
2594 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2595 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
2596 +delete_all_messages_by_user_at(User, VHost, Date) ->
2597 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2598 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
2599 +delete_messages_at(VHost, Date) ->
2600 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2601 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
2602 +get_vhost_stats(VHost) ->
2603 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2604 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
2605 +get_vhost_stats_at(VHost, Date) ->
2606 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2607 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
2608 +get_user_stats(User, VHost) ->
2609 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2610 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
2611 +get_user_messages_at(User, VHost, Date) ->
2612 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2613 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
2614 +get_dates(VHost) ->
2615 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2616 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
2617 +get_user_settings(User, VHost) ->
2618 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2619 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
2620 +get_users_settings(VHost) ->
2621 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2622 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
2623 +set_user_settings(User, VHost, Set) ->
2624 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2625 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
2626 +drop_user(User, VHost) ->
2627 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2628 + gen_server:call(Proc, {drop_user, User}, ?CALL_TIMEOUT).
2630 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2634 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2635 +log_message_int(VHost, #msg{timestamp=Timestamp}=MsgBin) ->
2636 + Date = mod_logdb:convert_timestamp_brief(Timestamp),
2638 + Msg = #msg{timestamp = MsgBin#msg.timestamp,
2639 + owner_name = binary_to_list(MsgBin#msg.owner_name),
2640 + peer_name = binary_to_list(MsgBin#msg.peer_name),
2641 + peer_server = binary_to_list(MsgBin#msg.peer_server),
2642 + peer_resource = binary_to_list(MsgBin#msg.peer_resource),
2643 + direction = MsgBin#msg.direction,
2644 + type = binary_to_list(MsgBin#msg.type),
2645 + subject = binary_to_list(MsgBin#msg.subject),
2646 + body = binary_to_list(MsgBin#msg.body)},
2648 + ATable = table_name(VHost, Date),
2650 + mnesia:write_lock_table(ATable),
2651 + mnesia:write(ATable, Msg, write)
2653 + % log message, increment stats for both users
2654 + case mnesia:transaction(Fun) of
2655 + % if table does not exists - create it and try to log message again
2656 + {aborted,{no_exists, _Table}} ->
2657 + case create_msg_table(VHost, Date) of
2658 + {aborted, CReason} ->
2659 + ?ERROR_MSG("Failed to log message: ~p", [CReason]),
2662 + ?MYDEBUG("Created msg table for ~s at ~s", [VHost, Date]),
2663 + log_message_int(VHost, MsgBin)
2665 + {aborted, TReason} ->
2666 + ?ERROR_MSG("Failed to log message: ~p", [TReason]),
2669 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
2670 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
2671 + increment_user_stats(Msg#msg.owner_name, VHost, Date)
2674 +increment_user_stats(Owner, VHost, Date) ->
2676 + Pat = #stats{user=Owner, at=Date, count='$1'},
2677 + mnesia:write_lock_table(stats_table(VHost)),
2678 + case mnesia:select(stats_table(VHost), [{Pat, [], ['$_']}]) of
2680 + mnesia:write(stats_table(VHost),
2681 + #stats{user=Owner,
2686 + mnesia:delete_object(stats_table(VHost),
2687 + #stats{user=Owner,
2689 + count=Stats#stats.count},
2691 + New = Stats#stats{count = Stats#stats.count+1},
2693 + New#stats.count > 0 -> mnesia:write(stats_table(VHost),
2700 + case mnesia:transaction(Fun) of
2701 + {aborted, Reason} ->
2702 + ?ERROR_MSG("Failed to update stats for ~s@~s: ~p", [Owner, VHost, Reason]),
2705 + ?MYDEBUG("Updated stats for ~s@~s", [Owner, VHost]),
2709 +get_dates_int(VHost) ->
2710 + Tables = mnesia:system_info(tables),
2711 + lists:foldl(fun(ATable, Dates) ->
2712 + Table = term_to_binary(ATable),
2713 + case ejabberd_regexp:run( Table, << VHost/binary, <<"$">>/binary >> ) of
2715 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
2716 + {match, [{S, E}]} ->
2717 + lists:append(Dates, [lists:sublist(binary_to_list(Table), S+1, E)]);
2726 +rebuild_stats_at_int(VHost, Date) ->
2727 + Table = table_name(VHost, Date),
2728 + STable = stats_table(VHost),
2729 + CFun = fun(Msg, Stats) ->
2730 + Owner = Msg#msg.owner_name,
2731 + case lists:keysearch(Owner, 1, Stats) of
2732 + {value, {_, Count}} ->
2733 + lists:keyreplace(Owner, 1, Stats, {Owner, Count + 1});
2735 + lists:append(Stats, [{Owner, 1}])
2738 + DFun = fun(#stats{at=SDate} = Stat, _Acc)
2739 + when SDate == Date ->
2740 + mnesia:delete_object(stats_table(VHost), Stat, write);
2741 + (_Stat, _Acc) -> ok
2743 + % TODO: Maybe unregister hooks ?
2744 + case mnesia:transaction(fun() ->
2745 + mnesia:write_lock_table(Table),
2746 + mnesia:write_lock_table(STable),
2747 + % Delete all stats for VHost at Date
2748 + mnesia:foldl(DFun, [], STable),
2749 + % Calc stats for VHost at Date
2750 + case mnesia:foldl(CFun, [], Table) of
2753 + % Write new calc'ed stats
2754 + lists:foreach(fun({Owner, Count}) ->
2755 + WStat = #stats{user=Owner, at=Date, count=Count},
2756 + mnesia:write(stats_table(VHost), WStat, write)
2761 + {aborted, Reason} ->
2762 + ?ERROR_MSG("Failed to rebuild_stats_at for ~p at ~p: ~p", [VHost, Date, Reason]),
2766 + {atomic, empty} ->
2767 + {atomic,ok} = mnesia:delete_table(Table),
2768 + ?MYDEBUG("Dropped table at ~p", [Date]),
2772 +delete_nonexistent_stats(VHost) ->
2773 + Dates = get_dates_int(VHost),
2774 + mnesia:transaction(fun() ->
2775 + mnesia:foldl(fun(#stats{at=Date} = Stat, _Acc) ->
2776 + case lists:member(Date, Dates) of
2777 + false -> mnesia:delete_object(Stat);
2780 + end, ok, stats_table(VHost))
2783 +delete_stats_by_vhost_at_int(VHost, Date) ->
2784 + StatsDelete = fun(#stats{at=SDate} = Stat, _Acc)
2785 + when SDate == Date ->
2786 + mnesia:delete_object(stats_table(VHost), Stat, write),
2788 + (_Msg, _Acc) -> ok
2790 + case mnesia:transaction(fun() ->
2791 + mnesia:write_lock_table(stats_table(VHost)),
2792 + mnesia:foldl(StatsDelete, ok, stats_table(VHost))
2794 + {aborted, Reason} ->
2795 + ?ERROR_MSG("Failed to update stats at ~p for ~p: ~p", [Date, VHost, Reason]),
2796 + rebuild_stats_at_int(VHost, Date);
2798 + ?INFO_MSG("Updated stats at ~p for ~p", [Date, VHost]),
2802 +get_user_stats_int(User, VHost) ->
2803 + case mnesia:transaction(fun() ->
2804 + Pat = #stats{user=User, at='$1', count='$2'},
2805 + mnesia:select(stats_table(VHost), [{Pat, [], [['$1', '$2']]}])
2807 + {atomic, Result} ->
2808 + {ok, mod_logdb:sort_stats([{Date, Count} || [Date, Count] <- Result])};
2809 + {aborted, Reason} ->
2813 +delete_all_messages_by_user_at_int(User, VHost, Date) ->
2814 + Table = table_name(VHost, Date),
2815 + MsgDelete = fun(#msg{owner_name=Owner} = Msg, _Acc)
2816 + when Owner == User ->
2817 + mnesia:delete_object(Table, Msg, write),
2819 + (_Msg, _Acc) -> ok
2821 + DRez = case mnesia:transaction(fun() ->
2822 + mnesia:foldl(MsgDelete, ok, Table)
2824 + {aborted, Reason} ->
2825 + ?ERROR_MSG("Failed to delete_all_messages_by_user_at for ~p@~p at ~p: ~p", [User, VHost, Date, Reason]),
2830 + case rebuild_stats_at_int(VHost, Date) of
2837 +delete_user_settings_int(User, VHost) ->
2838 + STable = settings_table(VHost),
2839 + case mnesia:dirty_match_object(STable, #user_settings{owner_name=User, _='_'}) of
2843 + mnesia:dirty_delete_object(STable, UserSettings)
2846 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2850 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2851 +create_stats_table(VHost) ->
2852 + SName = stats_table(VHost),
2853 + case mnesia:create_table(SName,
2854 + [{disc_only_copies, [node()]},
2856 + {attributes, record_info(fields, stats)},
2857 + {record_name, stats}
2860 + ?MYDEBUG("Created stats table for ~p", [VHost]),
2861 + lists:foreach(fun(Date) ->
2862 + rebuild_stats_at_int(VHost, Date)
2863 + end, get_dates_int(VHost)),
2865 + {aborted, {already_exists, _}} ->
2866 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
2868 + {aborted, Reason} ->
2869 + ?ERROR_MSG("Failed to create stats table: ~p", [Reason]),
2873 +create_settings_table(VHost) ->
2874 + SName = settings_table(VHost),
2875 + case mnesia:create_table(SName,
2876 + [{disc_copies, [node()]},
2878 + {attributes, record_info(fields, user_settings)},
2879 + {record_name, user_settings}
2882 + ?MYDEBUG("Created settings table for ~p", [VHost]),
2884 + {aborted, {already_exists, _}} ->
2885 + ?MYDEBUG("Settings table for ~p already exists", [VHost]),
2887 + {aborted, Reason} ->
2888 + ?ERROR_MSG("Failed to create settings table: ~p", [Reason]),
2892 +create_msg_table(VHost, Date) ->
2893 + mnesia:create_table(
2894 + table_name(VHost, Date),
2895 + [{disc_only_copies, [node()]},
2897 + {attributes, record_info(fields, msg)},
2898 + {record_name, msg}]).
2899 diff --git a/src/mod_logdb_mysql.erl b/src/mod_logdb_mysql.erl
2900 new file mode 100644
2901 index 0000000000..21d65e6578
2903 +++ b/src/mod_logdb_mysql.erl
2905 +%%%----------------------------------------------------------------------
2906 +%%% File : mod_logdb_mysql.erl
2907 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
2908 +%%% Purpose : MySQL backend for mod_logdb
2909 +%%% Url : https://paleg.github.io/mod_logdb/
2910 +%%%----------------------------------------------------------------------
2912 +-module(mod_logdb_mysql).
2913 +-author('o.palij@gmail.com').
2915 +-include("mod_logdb.hrl").
2916 +-include("logger.hrl").
2918 +-behaviour(gen_logdb).
2919 +-behaviour(gen_server).
2922 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
2924 +-export([start/2, stop/1]).
2926 +-export([log_message/2,
2928 + rebuild_stats_at/2,
2929 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
2930 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
2932 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
2935 +% gen_server call timeout
2936 +-define(CALL_TIMEOUT, 30000).
2937 +-define(MYSQL_TIMEOUT, 60000).
2938 +-define(INDEX_SIZE, integer_to_list(170)).
2939 +-define(PROCNAME, mod_logdb_mysql).
2941 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
2942 + list_to_string/1, string_to_list/1,
2943 + convert_timestamp_brief/1]).
2945 +-record(state, {dbref, vhost, server, port, db, user, password}).
2947 +% replace "." with "_"
2948 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
2950 + end, binary_to_list(VHost)).
2955 + "_" ++ escape_vhost(VHost) ++ "`".
2957 +messages_table(VHost, Date) ->
2958 + prefix() ++ "messages_" ++ Date ++ suffix(VHost).
2960 +stats_table(VHost) ->
2961 + prefix() ++ "stats" ++ suffix(VHost).
2963 +temp_table(VHost) ->
2964 + prefix() ++ "temp" ++ suffix(VHost).
2966 +settings_table(VHost) ->
2967 + prefix() ++ "settings" ++ suffix(VHost).
2969 +users_table(VHost) ->
2970 + prefix() ++ "users" ++ suffix(VHost).
2971 +servers_table(VHost) ->
2972 + prefix() ++ "servers" ++ suffix(VHost).
2973 +resources_table(VHost) ->
2974 + prefix() ++ "resources" ++ suffix(VHost).
2976 +ets_users_table(VHost) -> list_to_atom("logdb_users_" ++ binary_to_list(VHost)).
2977 +ets_servers_table(VHost) -> list_to_atom("logdb_servers_" ++ binary_to_list(VHost)).
2978 +ets_resources_table(VHost) -> list_to_atom("logdb_resources_" ++ binary_to_list(VHost)).
2980 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2982 +% gen_mod callbacks
2984 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2985 +start(VHost, Opts) ->
2986 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2987 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
2990 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
2991 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
2993 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2995 +% gen_server callbacks
2997 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2998 +init([VHost, Opts]) ->
3001 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
3002 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
3003 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
3004 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
3005 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
3007 + St = #state{vhost=VHost,
3008 + server=Server, port=Port, db=DB,
3009 + user=User, password=Password},
3011 + case open_mysql_connection(St) of
3013 + State = St#state{dbref=DBRef},
3014 + ok = create_stats_table(State),
3015 + ok = create_settings_table(State),
3016 + ok = create_users_table(State),
3017 + % clear ets cache every ...
3018 + timer:send_interval(timer:hours(12), clear_ets_tables),
3019 + ok = create_servers_table(State),
3020 + ok = create_resources_table(State),
3021 + erlang:monitor(process, DBRef),
3023 + {error, Reason} ->
3024 + ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
3025 + {stop, db_connection_failed}
3028 +open_mysql_connection(#state{server=Server, port=Port, db=DB,
3029 + user=DBUser, password=Password} = _State) ->
3030 + LogFun = fun(debug, _Format, _Argument) ->
3031 + %?MYDEBUG(Format, Argument);
3033 + (error, Format, Argument) ->
3034 + ?ERROR_MSG(Format, Argument);
3035 + (Level, Format, Argument) ->
3036 + ?MYDEBUG("MySQL (~p)~n", [Level]),
3037 + ?MYDEBUG(Format, Argument)
3039 + ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
3040 + p1_mysql_conn:start(binary_to_list(Server), Port,
3041 + binary_to_list(DBUser), binary_to_list(Password),
3042 + binary_to_list(DB), LogFun).
3044 +close_mysql_connection(DBRef) ->
3045 + ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
3046 + catch p1_mysql_conn:stop(DBRef).
3048 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3049 + Date = convert_timestamp_brief(Msg#msg.timestamp),
3051 + Table = messages_table(VHost, Date),
3052 + Owner_id = get_user_id(DBRef, VHost, binary_to_list(Msg#msg.owner_name)),
3053 + Peer_name_id = get_user_id(DBRef, VHost, binary_to_list(Msg#msg.peer_name)),
3054 + Peer_server_id = get_server_id(DBRef, VHost, binary_to_list(Msg#msg.peer_server)),
3055 + Peer_resource_id = get_resource_id(DBRef, VHost, binary_to_list(Msg#msg.peer_resource)),
3057 + Query = ["INSERT INTO ",Table," ",
3060 + "peer_server_id,",
3061 + "peer_resource_id,",
3068 + "('", Owner_id, "',",
3069 + "'", Peer_name_id, "',",
3070 + "'", Peer_server_id, "',",
3071 + "'", Peer_resource_id, "',",
3072 + "'", atom_to_list(Msg#msg.direction), "',",
3073 + "'", binary_to_list(Msg#msg.type), "',",
3074 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
3075 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
3076 + "'", Msg#msg.timestamp, "');"],
3079 + case sql_query_internal_silent(DBRef, Query) of
3081 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
3082 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
3083 + increment_user_stats(DBRef, Msg#msg.owner_name, Owner_id, VHost, Peer_name_id, Peer_server_id, Date);
3084 + {error, Reason} ->
3085 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S02">>) of
3086 + % Table doesn't exist
3088 + case create_msg_table(DBRef, VHost, Date) of
3092 + {updated, _} = sql_query_internal(DBRef, Query),
3093 + increment_user_stats(DBRef, binary_to_list(Msg#msg.owner_name), Owner_id, VHost, Peer_name_id, Peer_server_id, Date)
3096 + ?ERROR_MSG("Failed to log message: ~p", [Reason]),
3100 + {reply, Reply, State};
3101 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3102 + Reply = rebuild_stats_at_int(DBRef, VHost, Date),
3103 + {reply, Reply, State};
3104 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
3105 + {reply, error, State};
3106 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3107 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
3108 + ["\"",Timestamp,"\"",","]
3111 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3113 + Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
3114 + "WHERE timestamp IN (", Temp1],
3117 + case sql_query_internal(DBRef, Query) of
3119 + ?MYDEBUG("Aff=~p", [Aff]),
3120 + rebuild_stats_at_int(DBRef, VHost, Date);
3124 + {reply, Reply, State};
3125 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3126 + ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
3127 + ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
3128 + {reply, ok, State};
3129 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3131 + case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]) of
3133 + Query = ["DELETE FROM ",stats_table(VHost)," "
3134 + "WHERE at=\"",Date,"\";"],
3135 + case sql_query_internal(DBRef, Query) of
3144 + {reply, Reply, State};
3145 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3146 + SName = stats_table(VHost),
3147 + Query = ["SELECT at, sum(count) ",
3148 + "FROM ",SName," ",
3150 + "ORDER BY DATE(at) DESC;"
3153 + case sql_query_internal(DBRef, Query) of
3155 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
3156 + {error, Reason} ->
3157 + % TODO: Duplicate error message ?
3160 + {reply, Reply, State};
3161 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3162 + SName = stats_table(VHost),
3163 + Query = ["SELECT username, sum(count) AS allcount ",
3164 + "FROM ",SName," ",
3165 + "JOIN ",users_table(VHost)," ON owner_id=user_id "
3166 + "WHERE at=\"",Date,"\" "
3167 + "GROUP BY username ",
3168 + "ORDER BY allcount DESC;"
3171 + case sql_query_internal(DBRef, Query) of
3173 + {ok, lists:reverse(
3175 + [ {User, list_to_integer(Count)} || [User, Count] <- Result]))};
3176 + {error, Reason} ->
3180 + {reply, Reply, State};
3181 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3182 + {reply, get_user_stats_int(DBRef, User, VHost), State};
3183 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3184 + TName = messages_table(VHost, Date),
3185 + UName = users_table(VHost),
3186 + SName = servers_table(VHost),
3187 + RName = resources_table(VHost),
3188 + Query = ["SELECT users.username,",
3189 + "servers.server,",
3190 + "resources.resource,",
3191 + "messages.direction,"
3193 + "messages.subject,"
3195 + "messages.timestamp "
3196 + "FROM ",TName," AS messages "
3197 + "JOIN ",UName," AS users ON peer_name_id=user_id ",
3198 + "JOIN ",SName," AS servers ON peer_server_id=server_id ",
3199 + "JOIN ",RName," AS resources ON peer_resource_id=resource_id ",
3200 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
3201 + "ORDER BY timestamp ASC;"],
3203 + case sql_query_internal(DBRef, Query) of
3205 + Fun = fun([Peer_name, Peer_server, Peer_resource,
3210 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
3211 + direction=list_to_atom(Direction),
3213 + subject=Subject, body=Body,
3214 + timestamp=Timestamp}
3216 + {ok, lists:map(Fun, Result)};
3217 + {error, Reason} ->
3220 + {reply, Reply, State};
3221 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3222 + SName = stats_table(VHost),
3223 + Query = ["SELECT at ",
3224 + "FROM ",SName," ",
3226 + "ORDER BY DATE(at) DESC;"
3229 + case sql_query_internal(DBRef, Query) of
3231 + [ Date || [Date] <- Result ];
3232 + {error, Reason} ->
3235 + {reply, Reply, State};
3236 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3237 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
3238 + "FROM ",settings_table(VHost)," ",
3239 + "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
3241 + case sql_query_internal(DBRef, Query) of
3243 + {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
3244 + #user_settings{owner_name=Owner,
3245 + dolog_default=list_to_bool(DoLogDef),
3246 + dolog_list=string_to_list(DoLogL),
3247 + donotlog_list=string_to_list(DoNotLogL)
3253 + {reply, Reply, State};
3254 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
3255 + Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
3256 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\";"],
3258 + case sql_query_internal(DBRef, Query) of
3261 + {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
3262 + {ok, #user_settings{owner_name=Owner,
3263 + dolog_default=list_to_bool(DoLogDef),
3264 + dolog_list=string_to_list(DoLogL),
3265 + donotlog_list=string_to_list(DoNotLogL)}};
3269 + {reply, Reply, State};
3270 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
3271 + dolog_list=DoLogL,
3272 + donotlog_list=DoNotLogL}},
3273 + _From, #state{dbref=DBRef, vhost=VHost} = State) ->
3274 + User_id = get_user_id(DBRef, VHost, User),
3276 + Query = ["UPDATE ",settings_table(VHost)," ",
3277 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
3278 + "dolog_list='",list_to_string(DoLogL),"', ",
3279 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
3280 + "WHERE owner_id=\"",User_id,"\";"],
3283 + case sql_query_internal(DBRef, Query) of
3285 + IQuery = ["INSERT INTO ",settings_table(VHost)," ",
3286 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
3288 + "('",User_id,"', ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
3289 + case sql_query_internal_silent(DBRef, IQuery) of
3291 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
3293 + {error, Reason} ->
3294 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
3299 + ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
3304 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
3309 + {reply, Reply, State};
3310 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
3311 + ets:delete(ets_users_table(VHost)),
3312 + ets:delete(ets_servers_table(VHost)),
3313 + ?MYDEBUG("Stoping mysql backend for ~p", [VHost]),
3314 + {stop, normal, ok, State};
3315 +handle_call(Msg, _From, State) ->
3316 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
3319 +handle_cast({rebuild_stats}, State) ->
3320 + rebuild_all_stats_int(State),
3322 +handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
3324 + {ok, DBRef} = open_mysql_connection(State),
3325 + {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
3326 + MDResult = lists:map(fun({Date, _}) ->
3327 + delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
3329 + StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
3330 + SDResult = delete_user_settings_int(DBRef, User, VHost),
3331 + case lists:all(fun(Result) when Result == ok ->
3333 + (Result) when Result == error ->
3335 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
3337 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
3339 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
3341 + close_mysql_connection(DBRef)
3345 +handle_cast(Msg, State) ->
3346 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
3349 +handle_info(clear_ets_tables, State) ->
3350 + ets:delete_all_objects(ets_users_table(State#state.vhost)),
3351 + ets:delete_all_objects(ets_resources_table(State#state.vhost)),
3353 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
3354 + {stop, connection_dropped, State};
3355 +handle_info(Info, State) ->
3356 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
3359 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
3360 + close_mysql_connection(DBRef),
3363 +code_change(_OldVsn, State, _Extra) ->
3366 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3368 +% gen_logdb callbacks
3370 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3371 +log_message(VHost, Msg) ->
3372 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3373 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
3374 +rebuild_stats(VHost) ->
3375 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3376 + gen_server:cast(Proc, {rebuild_stats}).
3377 +rebuild_stats_at(VHost, Date) ->
3378 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3379 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
3380 +delete_messages_by_user_at(VHost, Msgs, Date) ->
3381 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3382 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
3383 +delete_all_messages_by_user_at(User, VHost, Date) ->
3384 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3385 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
3386 +delete_messages_at(VHost, Date) ->
3387 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3388 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
3389 +get_vhost_stats(VHost) ->
3390 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3391 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
3392 +get_vhost_stats_at(VHost, Date) ->
3393 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3394 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
3395 +get_user_stats(User, VHost) ->
3396 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3397 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
3398 +get_user_messages_at(User, VHost, Date) ->
3399 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3400 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
3401 +get_dates(VHost) ->
3402 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3403 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
3404 +get_users_settings(VHost) ->
3405 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3406 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
3407 +get_user_settings(User, VHost) ->
3408 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3409 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
3410 +set_user_settings(User, VHost, Set) ->
3411 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3412 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
3413 +drop_user(User, VHost) ->
3414 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
3415 + gen_server:cast(Proc, {drop_user, User}).
3417 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3421 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3422 +increment_user_stats(DBRef, User_name, User_id, VHost, PNameID, PServerID, Date) ->
3423 + SName = stats_table(VHost),
3424 + UQuery = ["UPDATE ",SName," ",
3425 + "SET count=count+1 ",
3426 + "WHERE owner_id=\"",User_id,"\" AND peer_name_id=\"",PNameID,"\" AND peer_server_id=\"",PServerID,"\" AND at=\"",Date,"\";"],
3428 + case sql_query_internal(DBRef, UQuery) of
3430 + IQuery = ["INSERT INTO ",SName," ",
3431 + "(owner_id, peer_name_id, peer_server_id, at, count) ",
3433 + "('",User_id,"', '",PNameID,"', '",PServerID,"', '",Date,"', '1');"],
3434 + case sql_query_internal(DBRef, IQuery) of
3436 + ?MYDEBUG("New stats for ~s@~s at ~s", [User_name, VHost, Date]),
3442 + ?MYDEBUG("Updated stats for ~s@~s at ~s", [User_name, VHost, Date]),
3448 +get_dates_int(DBRef, VHost) ->
3449 + case sql_query_internal(DBRef, ["SHOW TABLES"]) of
3451 + Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
3452 + lists:foldl(fun([Table], Dates) ->
3453 + case re:run(Table, Reg) of
3455 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
3456 + {match, [{S, E}]} ->
3457 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
3469 +rebuild_all_stats_int(#state{vhost=VHost}=State) ->
3471 + {ok, DBRef} = open_mysql_connection(State),
3472 + ok = delete_nonexistent_stats(DBRef, VHost),
3473 + case lists:filter(fun(Date) ->
3474 + case catch rebuild_stats_at_int(DBRef, VHost, Date) of
3477 + {'EXIT', _} -> true
3479 + end, get_dates_int(DBRef, VHost)) of
3482 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
3485 + close_mysql_connection(DBRef)
3489 +rebuild_stats_at_int(DBRef, VHost, Date) ->
3490 + TempTable = temp_table(VHost),
3492 + Table = messages_table(VHost, Date),
3493 + STable = stats_table(VHost),
3495 + DQuery = [ "DELETE FROM ",STable," ",
3496 + "WHERE at='",Date,"';"],
3498 + ok = create_temp_table(DBRef, TempTable),
3499 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
3500 + SQuery = ["INSERT INTO ",TempTable," ",
3501 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
3502 + "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
3503 + "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
3504 + case sql_query_internal(DBRef, SQuery) of
3506 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
3508 + {data, [["0"]]} ->
3509 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
3510 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE;"]),
3511 + {updated, _} = sql_query_internal(DBRef, DQuery),
3514 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
3518 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
3519 + {updated, _} = sql_query_internal(DBRef, DQuery),
3520 + SQuery1 = ["INSERT INTO ",STable," ",
3521 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
3522 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
3523 + "FROM ",TempTable,";"],
3524 + case sql_query_internal(DBRef, SQuery1) of
3525 + {updated, _} -> ok;
3526 + {error, _} -> error
3528 + {error, _} -> error
3532 + case catch apply(Fun, []) of
3534 + ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
3538 + {'EXIT', Reason} ->
3539 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
3542 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
3543 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
3547 +delete_nonexistent_stats(DBRef, VHost) ->
3548 + Dates = get_dates_int(DBRef, VHost),
3549 + STable = stats_table(VHost),
3551 + Temp = lists:flatmap(fun(Date) ->
3552 + ["\"",Date,"\"",","]
3559 + % replace last "," with ");"
3560 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
3561 + Query = ["DELETE FROM ",STable," ",
3562 + "WHERE at NOT IN (", Temp1],
3563 + case sql_query_internal(DBRef, Query) of
3571 +get_user_stats_int(DBRef, User, VHost) ->
3572 + SName = stats_table(VHost),
3573 + Query = ["SELECT at, sum(count) as allcount ",
3574 + "FROM ",SName," ",
3575 + "WHERE owner_id=\"",get_user_id(DBRef, VHost, User),"\" ",
3577 + "ORDER BY DATE(at) DESC;"
3579 + case sql_query_internal(DBRef, Query) of
3581 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result]};
3582 + {error, Result} ->
3586 +delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
3587 + DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
3588 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3589 + case sql_query_internal(DBRef, DQuery) of
3591 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
3597 +delete_all_stats_by_user_int(DBRef, User, VHost) ->
3598 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
3599 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3600 + case sql_query_internal(DBRef, SQuery) of
3602 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
3604 + {error, _} -> error
3607 +delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
3608 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
3609 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
3610 + "AND at=\"",Date,"\";"],
3611 + case sql_query_internal(DBRef, SQuery) of
3613 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
3615 + {error, _} -> error
3618 +delete_user_settings_int(DBRef, User, VHost) ->
3619 + Query = ["DELETE FROM ",settings_table(VHost)," ",
3620 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
3621 + case sql_query_internal(DBRef, Query) of
3623 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
3625 + {error, Reason} ->
3626 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
3630 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3634 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3635 +create_temp_table(DBRef, Name) ->
3636 + Query = ["CREATE TABLE ",Name," (",
3637 + "owner_id MEDIUMINT UNSIGNED, ",
3638 + "peer_name_id MEDIUMINT UNSIGNED, ",
3639 + "peer_server_id MEDIUMINT UNSIGNED, ",
3640 + "at VARCHAR(11), ",
3642 + ") ENGINE=MyISAM CHARACTER SET utf8;"
3644 + case sql_query_internal(DBRef, Query) of
3645 + {updated, _} -> ok;
3646 + {error, _Reason} -> error
3649 +create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
3650 + SName = stats_table(VHost),
3651 + Query = ["CREATE TABLE ",SName," (",
3652 + "owner_id MEDIUMINT UNSIGNED, ",
3653 + "peer_name_id MEDIUMINT UNSIGNED, ",
3654 + "peer_server_id MEDIUMINT UNSIGNED, ",
3655 + "at varchar(20), ",
3656 + "count int(11), ",
3657 + "INDEX(owner_id, peer_name_id, peer_server_id), ",
3659 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3661 + case sql_query_internal_silent(DBRef, Query) of
3663 + ?INFO_MSG("Created stats table for ~p", [VHost]),
3664 + rebuild_all_stats_int(State),
3666 + {error, Reason} ->
3667 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
3669 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
3670 + CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
3671 + case sql_query_internal(DBRef, CheckQuery) of
3672 + {data, Elems} when length(Elems) == 2 ->
3673 + ?MYDEBUG("Stats table structure is ok", []),
3676 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
3677 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
3679 + ?INFO_MSG("Successfully dropped ~p", [SName]);
3681 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
3686 + ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
3691 +create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
3692 + SName = settings_table(VHost),
3693 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3694 + "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
3695 + "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
3696 + "dolog_list TEXT, ",
3697 + "donotlog_list TEXT ",
3698 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3700 + case sql_query_internal(DBRef, Query) of
3702 + ?MYDEBUG("Created settings table for ~p", [VHost]),
3708 +create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
3709 + SName = users_table(VHost),
3710 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3711 + "username TEXT NOT NULL, ",
3712 + "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3713 + "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
3714 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3716 + case sql_query_internal(DBRef, Query) of
3718 + ?MYDEBUG("Created users table for ~p", [VHost]),
3719 + ets:new(ets_users_table(VHost), [named_table, set, public]),
3720 + %update_users_from_db(DBRef, VHost),
3726 +create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
3727 + SName = servers_table(VHost),
3728 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
3729 + "server TEXT NOT NULL, ",
3730 + "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3731 + "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
3732 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3734 + case sql_query_internal(DBRef, Query) of
3736 + ?MYDEBUG("Created servers table for ~p", [VHost]),
3737 + ets:new(ets_servers_table(VHost), [named_table, set, public]),
3738 + update_servers_from_db(DBRef, VHost),
3744 +create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
3745 + RName = resources_table(VHost),
3746 + Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
3747 + "resource TEXT NOT NULL, ",
3748 + "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
3749 + "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
3750 + ") ENGINE=InnoDB CHARACTER SET utf8;"
3752 + case sql_query_internal(DBRef, Query) of
3754 + ?MYDEBUG("Created resources table for ~p", [VHost]),
3755 + ets:new(ets_resources_table(VHost), [named_table, set, public]),
3761 +create_msg_table(DBRef, VHost, Date) ->
3762 + TName = messages_table(VHost, Date),
3763 + Query = ["CREATE TABLE ",TName," (",
3764 + "owner_id MEDIUMINT UNSIGNED, ",
3765 + "peer_name_id MEDIUMINT UNSIGNED, ",
3766 + "peer_server_id MEDIUMINT UNSIGNED, ",
3767 + "peer_resource_id MEDIUMINT(8) UNSIGNED, ",
3768 + "direction ENUM('to', 'from'), ",
3769 + "type ENUM('chat','error','groupchat','headline','normal') NOT NULL, ",
3772 + "timestamp DOUBLE, ",
3773 + "INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id), ",
3774 + "FULLTEXT (body) "
3775 + ") ENGINE=MyISAM CHARACTER SET utf8;"
3777 + case sql_query_internal(DBRef, Query) of
3778 + {updated, _MySQLRes} ->
3779 + ?MYDEBUG("Created msg table for ~p at ~p", [VHost, Date]),
3785 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3787 +% internal ets cache (users, servers, resources)
3789 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3790 +update_servers_from_db(DBRef, VHost) ->
3791 + ?INFO_MSG("Reading servers from db for ~p", [VHost]),
3792 + SQuery = ["SELECT server, server_id FROM ",servers_table(VHost),";"],
3793 + {data, Result} = sql_query_internal(DBRef, SQuery),
3794 + true = ets:delete_all_objects(ets_servers_table(VHost)),
3795 + true = ets:insert(ets_servers_table(VHost), [ {Server, Server_id} || [Server, Server_id] <- Result]).
3797 +%update_users_from_db(DBRef, VHost) ->
3798 +% ?INFO_MSG("Reading users from db for ~p", [VHost]),
3799 +% SQuery = ["SELECT username, user_id FROM ",users_table(VHost),";"],
3800 +% {data, Result} = sql_query_internal(DBRef, SQuery),
3801 +% true = ets:delete_all_objects(ets_users_table(VHost)),
3802 +% true = ets:insert(ets_users_table(VHost), [ {Username, User_id} || [Username, User_id] <- Result]).
3804 +%get_user_name(DBRef, VHost, User_id) ->
3805 +% case ets:match(ets_users_table(VHost), {'$1', User_id}) of
3806 +% [[User]] -> User;
3807 +% % this can be in clustered environment
3809 +% %update_users_from_db(DBRef, VHost),
3810 +% SQuery = ["SELECT username FROM ",users_table(VHost)," ",
3811 +% "WHERE user_id=\"",User_id,"\";"],
3812 +% {data, [[Name]]} = sql_query_internal(DBRef, SQuery),
3813 +% % cache {user, id} pair
3814 +% ets:insert(ets_users_table(VHost), {Name, User_id}),
3818 +%get_server_name(DBRef, VHost, Server_id) ->
3819 +% case ets:match(ets_servers_table(VHost), {'$1', Server_id}) of
3820 +% [[Server]] -> Server;
3821 + % this can be in clustered environment
3823 +% update_servers_from_db(DBRef, VHost),
3824 +% [[Server1]] = ets:match(ets_servers_table(VHost), {'$1', Server_id}),
3828 +get_user_id_from_db(DBRef, VHost, User) ->
3829 + SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
3830 + "WHERE username=\"",User,"\";"],
3831 + case sql_query_internal(DBRef, SQuery) of
3832 + % no such user in db
3835 + {data, [[DBId]]} ->
3836 + % cache {user, id} pair
3837 + ets:insert(ets_users_table(VHost), {User, DBId}),
3840 +get_user_id(DBRef, VHost, User) ->
3842 + case ets:match(ets_users_table(VHost), {User, '$1'}) of
3845 + case get_user_id_from_db(DBRef, VHost, User) of
3846 + % no such user in db
3848 + IQuery = ["INSERT INTO ",users_table(VHost)," ",
3849 + "SET username=\"",User,"\";"],
3850 + case sql_query_internal_silent(DBRef, IQuery) of
3852 + {ok, NewId} = get_user_id_from_db(DBRef, VHost, User),
3854 + {error, Reason} ->
3855 + % this can be in clustered environment
3856 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3857 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
3858 + {ok, ClID} = get_user_id_from_db(DBRef, VHost, User),
3864 + [[EtsId]] -> EtsId
3867 +get_server_id(DBRef, VHost, Server) ->
3868 + case ets:match(ets_servers_table(VHost), {Server, '$1'}) of
3870 + IQuery = ["INSERT INTO ",servers_table(VHost)," ",
3871 + "SET server=\"",Server,"\";"],
3872 + case sql_query_internal_silent(DBRef, IQuery) of
3874 + SQuery = ["SELECT server_id FROM ",servers_table(VHost)," ",
3875 + "WHERE server=\"",Server,"\";"],
3876 + {data, [[Id]]} = sql_query_internal(DBRef, SQuery),
3877 + ets:insert(ets_servers_table(VHost), {Server, Id}),
3879 + {error, Reason} ->
3880 + % this can be in clustered environment
3881 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3882 + ?ERROR_MSG("Duplicate key name for ~p", [Server]),
3883 + update_servers_from_db(DBRef, VHost),
3884 + [[Id1]] = ets:match(ets_servers_table(VHost), {Server, '$1'}),
3890 +get_resource_id_from_db(DBRef, VHost, Resource) ->
3891 + SQuery = ["SELECT resource_id FROM ",resources_table(VHost)," ",
3892 + "WHERE resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
3893 + case sql_query_internal(DBRef, SQuery) of
3894 + % no such resource in db
3897 + {data, [[DBId]]} ->
3898 + % cache {resource, id} pair
3899 + ets:insert(ets_resources_table(VHost), {Resource, DBId}),
3902 +get_resource_id(DBRef, VHost, Resource) ->
3904 + case ets:match(ets_resources_table(VHost), {Resource, '$1'}) of
3907 + case get_resource_id_from_db(DBRef, VHost, Resource) of
3908 + % no such resource in db
3910 + IQuery = ["INSERT INTO ",resources_table(VHost)," ",
3911 + "SET resource=\"",binary_to_list(ejabberd_sql:escape(iolist_to_binary(Resource))),"\";"],
3912 + case sql_query_internal_silent(DBRef, IQuery) of
3914 + {ok, NewId} = get_resource_id_from_db(DBRef, VHost, Resource),
3916 + {error, Reason} ->
3917 + % this can be in clustered environment
3918 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
3919 + ?ERROR_MSG("Duplicate key name for ~s", [Resource]),
3920 + {ok, ClID} = get_resource_id_from_db(DBRef, VHost, Resource),
3926 + [[EtsId]] -> EtsId
3929 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3933 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3934 +sql_query_internal(DBRef, Query) ->
3935 + case sql_query_internal_silent(DBRef, Query) of
3936 + {error, Reason} ->
3937 + ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
3942 +sql_query_internal_silent(DBRef, Query) ->
3943 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
3944 + get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
3946 +get_result({updated, MySQLRes}) ->
3947 + {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
3948 +get_result({data, MySQLRes}) ->
3949 + {data, p1_mysql:get_result_rows(MySQLRes)};
3950 +get_result({error, "query timed out"}) ->
3951 + {error, "query timed out"};
3952 +get_result({error, MySQLRes}) ->
3953 + Reason = p1_mysql:get_result_reason(MySQLRes),
3955 diff --git a/src/mod_logdb_mysql5.erl b/src/mod_logdb_mysql5.erl
3956 new file mode 100644
3957 index 0000000000..c05ab958e2
3959 +++ b/src/mod_logdb_mysql5.erl
3961 +%%%----------------------------------------------------------------------
3962 +%%% File : mod_logdb_mysql5.erl
3963 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
3964 +%%% Purpose : MySQL 5 backend for mod_logdb
3965 +%%% Url : https://paleg.github.io/mod_logdb/
3966 +%%%----------------------------------------------------------------------
3968 +-module(mod_logdb_mysql5).
3969 +-author('o.palij@gmail.com').
3971 +-include("mod_logdb.hrl").
3972 +-include("logger.hrl").
3974 +-behaviour(gen_logdb).
3975 +-behaviour(gen_server).
3978 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
3980 +-export([start/2, stop/1]).
3982 +-export([log_message/2,
3984 + rebuild_stats_at/2,
3985 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
3986 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
3988 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
3991 +% gen_server call timeout
3992 +-define(CALL_TIMEOUT, 30000).
3993 +-define(MYSQL_TIMEOUT, 60000).
3994 +-define(INDEX_SIZE, integer_to_list(170)).
3995 +-define(PROCNAME, mod_logdb_mysql5).
3997 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
3998 + list_to_string/1, string_to_list/1,
3999 + convert_timestamp_brief/1]).
4001 +-record(state, {dbref, vhost, server, port, db, user, password}).
4003 +% replace "." with "_"
4004 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4006 + end, binary_to_list(VHost)).
4011 + "_" ++ escape_vhost(VHost) ++ "`".
4013 +messages_table(VHost, Date) ->
4014 + prefix() ++ "messages_" ++ Date ++ suffix(VHost).
4016 +% TODO: this needs to be redone to unify view name in stored procedure and in delete_messages_at/2
4017 +view_table(VHost, Date) ->
4018 + Table = messages_table(VHost, Date),
4019 + TablewoQ = lists:sublist(Table, 2, length(Table) - 2),
4020 + lists:append(["`v_", TablewoQ, "`"]).
4022 +stats_table(VHost) ->
4023 + prefix() ++ "stats" ++ suffix(VHost).
4025 +temp_table(VHost) ->
4026 + prefix() ++ "temp" ++ suffix(VHost).
4028 +settings_table(VHost) ->
4029 + prefix() ++ "settings" ++ suffix(VHost).
4031 +users_table(VHost) ->
4032 + prefix() ++ "users" ++ suffix(VHost).
4033 +servers_table(VHost) ->
4034 + prefix() ++ "servers" ++ suffix(VHost).
4035 +resources_table(VHost) ->
4036 + prefix() ++ "resources" ++ suffix(VHost).
4038 +logmessage_name(VHost) ->
4039 + prefix() ++ "logmessage" ++ suffix(VHost).
4041 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4043 +% gen_mod callbacks
4045 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4046 +start(VHost, Opts) ->
4047 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4048 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
4051 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4052 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
4054 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4056 +% gen_server callbacks
4058 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4059 +init([VHost, Opts]) ->
4062 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
4063 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
4064 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
4065 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
4066 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
4068 + St = #state{vhost=VHost,
4069 + server=Server, port=Port, db=DB,
4070 + user=User, password=Password},
4072 + case open_mysql_connection(St) of
4074 + State = St#state{dbref=DBRef},
4075 + ok = create_internals(State),
4076 + ok = create_stats_table(State),
4077 + ok = create_settings_table(State),
4078 + ok = create_users_table(State),
4079 + ok = create_servers_table(State),
4080 + ok = create_resources_table(State),
4081 + erlang:monitor(process, DBRef),
4083 + {error, Reason} ->
4084 + ?ERROR_MSG("MySQL connection failed: ~p~n", [Reason]),
4085 + {stop, db_connection_failed}
4088 +open_mysql_connection(#state{server=Server, port=Port, db=DB,
4089 + user=DBUser, password=Password} = _State) ->
4090 + LogFun = fun(debug, _Format, _Argument) ->
4091 + %?MYDEBUG(Format, Argument);
4093 + (error, Format, Argument) ->
4094 + ?ERROR_MSG(Format, Argument);
4095 + (Level, Format, Argument) ->
4096 + ?MYDEBUG("MySQL (~p)~n", [Level]),
4097 + ?MYDEBUG(Format, Argument)
4099 + ?INFO_MSG("Opening mysql connection ~s@~s:~p/~s", [DBUser, Server, Port, DB]),
4100 + p1_mysql_conn:start(binary_to_list(Server), Port,
4101 + binary_to_list(DBUser), binary_to_list(Password),
4102 + binary_to_list(DB), LogFun).
4104 +close_mysql_connection(DBRef) ->
4105 + ?MYDEBUG("Closing ~p mysql connection", [DBRef]),
4106 + catch p1_mysql_conn:stop(DBRef).
4108 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4109 + Reply = rebuild_stats_at_int(DBRef, VHost, Date),
4110 + {reply, Reply, State};
4111 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
4112 + {reply, error, State};
4113 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4114 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
4115 + ["\"",Timestamp,"\"",","]
4118 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4120 + Query = ["DELETE FROM ",messages_table(VHost, Date)," ",
4121 + "WHERE timestamp IN (", Temp1],
4124 + case sql_query_internal(DBRef, Query) of
4126 + ?MYDEBUG("Aff=~p", [Aff]),
4127 + rebuild_stats_at_int(DBRef, VHost, Date);
4131 + {reply, Reply, State};
4132 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4133 + ok = delete_all_messages_by_user_at_int(DBRef, User, VHost, Date),
4134 + ok = delete_stats_by_user_at_int(DBRef, User, VHost, Date),
4135 + {reply, ok, State};
4136 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4138 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Date),";"]),
4139 + TQuery = ["DELETE FROM ",stats_table(VHost)," "
4140 + "WHERE at=\"",Date,"\";"],
4141 + {updated, _} = sql_query_internal(DBRef, TQuery),
4142 + VQuery = ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"],
4143 + {updated, _} = sql_query_internal(DBRef, VQuery),
4147 + case catch apply(Fun, []) of
4153 + {reply, Reply, State};
4154 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4155 + SName = stats_table(VHost),
4156 + Query = ["SELECT at, sum(count) ",
4157 + "FROM ",SName," ",
4159 + "ORDER BY DATE(at) DESC;"
4162 + case sql_query_internal(DBRef, Query) of
4164 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
4165 + {error, Reason} ->
4166 + % TODO: Duplicate error message ?
4169 + {reply, Reply, State};
4170 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4171 + SName = stats_table(VHost),
4172 + Query = ["SELECT username, sum(count) as allcount ",
4173 + "FROM ",SName," ",
4174 + "JOIN ",users_table(VHost)," ON owner_id=user_id "
4175 + "WHERE at=\"",Date,"\" ",
4176 + "GROUP BY username ",
4177 + "ORDER BY allcount DESC;"
4180 + case sql_query_internal(DBRef, Query) of
4182 + {ok, [ {User, list_to_integer(Count)} || [User, Count] <- Result ]};
4183 + {error, Reason} ->
4186 + {reply, Reply, State};
4187 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4188 + {reply, get_user_stats_int(DBRef, User, VHost), State};
4189 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4190 + Query = ["SELECT peer_name,",
4198 + "FROM ",view_table(VHost, Date)," "
4199 + "WHERE owner_name=\"",User,"\";"],
4201 + case sql_query_internal(DBRef, Query) of
4203 + Fun = fun([Peer_name, Peer_server, Peer_resource,
4208 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
4209 + direction=list_to_atom(Direction),
4211 + subject=Subject, body=Body,
4212 + timestamp=Timestamp}
4214 + {ok, lists:map(Fun, Result)};
4215 + {error, Reason} ->
4218 + {reply, Reply, State};
4219 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4220 + SName = stats_table(VHost),
4221 + Query = ["SELECT at ",
4222 + "FROM ",SName," ",
4224 + "ORDER BY DATE(at) DESC;"
4227 + case sql_query_internal(DBRef, Query) of
4229 + [ Date || [Date] <- Result ];
4230 + {error, Reason} ->
4233 + {reply, Reply, State};
4234 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4235 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
4236 + "FROM ",settings_table(VHost)," ",
4237 + "JOIN ",users_table(VHost)," ON user_id=owner_id;"],
4239 + case sql_query_internal(DBRef, Query) of
4241 + {ok, lists:map(fun([Owner, DoLogDef, DoLogL, DoNotLogL]) ->
4242 + #user_settings{owner_name=Owner,
4243 + dolog_default=list_to_bool(DoLogDef),
4244 + dolog_list=string_to_list(DoLogL),
4245 + donotlog_list=string_to_list(DoNotLogL)
4251 + {reply, Reply, State};
4252 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost}=State) ->
4253 + Query = ["SELECT dolog_default,dolog_list,donotlog_list FROM ",settings_table(VHost)," ",
4254 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4256 + case sql_query_internal(DBRef, Query) of
4259 + {data, [[Owner, DoLogDef, DoLogL, DoNotLogL]]} ->
4260 + {ok, #user_settings{owner_name=Owner,
4261 + dolog_default=list_to_bool(DoLogDef),
4262 + dolog_list=string_to_list(DoLogL),
4263 + donotlog_list=string_to_list(DoNotLogL)}};
4267 + {reply, Reply, State};
4268 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
4269 + dolog_list=DoLogL,
4270 + donotlog_list=DoNotLogL}},
4271 + _From, #state{dbref=DBRef, vhost=VHost} = State) ->
4272 + User_id = get_user_id(DBRef, VHost, User),
4273 + Query = ["UPDATE ",settings_table(VHost)," ",
4274 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
4275 + "dolog_list='",list_to_string(DoLogL),"', ",
4276 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
4277 + "WHERE owner_id=",User_id,";"],
4280 + case sql_query_internal(DBRef, Query) of
4282 + IQuery = ["INSERT INTO ",settings_table(VHost)," ",
4283 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
4285 + "(",User_id,",",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
4286 + case sql_query_internal_silent(DBRef, IQuery) of
4288 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
4290 + {error, Reason} ->
4291 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>) of
4296 + ?ERROR_MSG("Failed setup user ~p@~p: ~p", [User, VHost, Reason]),
4301 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
4306 + {reply, Reply, State};
4307 +handle_call({stop}, _From, #state{vhost=VHost}=State) ->
4308 + ?MYDEBUG("Stoping mysql5 backend for ~p", [VHost]),
4309 + {stop, normal, ok, State};
4310 +handle_call(Msg, _From, State) ->
4311 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
4314 +handle_cast({log_message, Msg}, #state{dbref=DBRef, vhost=VHost}=State) ->
4316 + Date = convert_timestamp_brief(Msg#msg.timestamp),
4317 + TableName = messages_table(VHost, Date),
4319 + Query = [ "CALL ",logmessage_name(VHost)," "
4320 + "('", TableName, "',",
4322 + "'", binary_to_list(Msg#msg.owner_name), "',",
4323 + "'", binary_to_list(Msg#msg.peer_name), "',",
4324 + "'", binary_to_list(Msg#msg.peer_server), "',",
4325 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
4326 + "'", atom_to_list(Msg#msg.direction), "',",
4327 + "'", binary_to_list(Msg#msg.type), "',",
4328 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
4329 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
4330 + "'", Msg#msg.timestamp, "');"],
4332 + case sql_query_internal(DBRef, Query) of
4334 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
4335 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
4337 + {error, _Reason} ->
4343 +handle_cast({rebuild_stats}, State) ->
4344 + rebuild_all_stats_int(State),
4346 +handle_cast({drop_user, User}, #state{vhost=VHost} = State) ->
4348 + {ok, DBRef} = open_mysql_connection(State),
4349 + {ok, Dates} = get_user_stats_int(DBRef, User, VHost),
4350 + MDResult = lists:map(fun({Date, _}) ->
4351 + delete_all_messages_by_user_at_int(DBRef, User, VHost, Date)
4353 + StDResult = delete_all_stats_by_user_int(DBRef, User, VHost),
4354 + SDResult = delete_user_settings_int(DBRef, User, VHost),
4355 + case lists:all(fun(Result) when Result == ok ->
4357 + (Result) when Result == error ->
4359 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
4361 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
4363 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
4365 + close_mysql_connection(DBRef)
4369 +handle_cast(Msg, State) ->
4370 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
4373 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
4374 + {stop, connection_dropped, State};
4375 +handle_info(Info, State) ->
4376 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
4379 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
4380 + close_mysql_connection(DBRef),
4383 +code_change(_OldVsn, State, _Extra) ->
4386 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4388 +% gen_logdb callbacks
4390 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4391 +log_message(VHost, Msg) ->
4392 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4393 + gen_server:cast(Proc, {log_message, Msg}).
4394 +rebuild_stats(VHost) ->
4395 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4396 + gen_server:cast(Proc, {rebuild_stats}).
4397 +rebuild_stats_at(VHost, Date) ->
4398 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4399 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
4400 +delete_messages_by_user_at(VHost, Msgs, Date) ->
4401 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4402 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
4403 +delete_all_messages_by_user_at(User, VHost, Date) ->
4404 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4405 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
4406 +delete_messages_at(VHost, Date) ->
4407 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4408 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
4409 +get_vhost_stats(VHost) ->
4410 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4411 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
4412 +get_vhost_stats_at(VHost, Date) ->
4413 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4414 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
4415 +get_user_stats(User, VHost) ->
4416 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4417 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
4418 +get_user_messages_at(User, VHost, Date) ->
4419 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4420 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
4421 +get_dates(VHost) ->
4422 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4423 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
4424 +get_users_settings(VHost) ->
4425 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4426 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
4427 +get_user_settings(User, VHost) ->
4428 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4429 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
4430 +set_user_settings(User, VHost, Set) ->
4431 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4432 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
4433 +drop_user(User, VHost) ->
4434 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
4435 + gen_server:cast(Proc, {drop_user, User}).
4437 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4441 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4442 +get_dates_int(DBRef, VHost) ->
4443 + case sql_query_internal(DBRef, ["SHOW TABLES"]) of
4445 + Reg = "^" ++ lists:sublist(prefix(),2,length(prefix())) ++ ".*" ++ escape_vhost(VHost),
4446 + lists:foldl(fun([Table], Dates) ->
4447 + case re:run(Table, Reg) of
4449 + case re:run(Table, "[0-9]+-[0-9]+-[0-9]+") of
4450 + {match, [{S, E}]} ->
4451 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
4463 +rebuild_all_stats_int(#state{vhost=VHost}=State) ->
4465 + {ok, DBRef} = open_mysql_connection(State),
4466 + ok = delete_nonexistent_stats(DBRef, VHost),
4467 + case lists:filter(fun(Date) ->
4468 + case catch rebuild_stats_at_int(DBRef, VHost, Date) of
4471 + {'EXIT', _} -> true
4473 + end, get_dates_int(DBRef, VHost)) of
4476 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
4479 + close_mysql_connection(DBRef)
4483 +rebuild_stats_at_int(DBRef, VHost, Date) ->
4484 + TempTable = temp_table(VHost),
4486 + Table = messages_table(VHost, Date),
4487 + STable = stats_table(VHost),
4489 + DQuery = [ "DELETE FROM ",STable," ",
4490 + "WHERE at='",Date,"';"],
4492 + ok = create_temp_table(DBRef, TempTable),
4493 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," WRITE, ",TempTable," WRITE;"]),
4494 + SQuery = ["INSERT INTO ",TempTable," ",
4495 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
4496 + "SELECT owner_id,peer_name_id,peer_server_id,\"",Date,"\",count(*) ",
4497 + "FROM ",Table," WHERE ext is NULL GROUP BY owner_id,peer_name_id,peer_server_id;"],
4498 + case sql_query_internal(DBRef, SQuery) of
4500 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
4502 + {data, [["0"]]} ->
4503 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table,";"]),
4504 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
4505 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW IF EXISTS ",view_table(VHost,Date),";"]),
4506 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
4507 + {updated, _} = sql_query_internal(DBRef, DQuery),
4510 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
4514 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," WRITE, ",TempTable," WRITE;"]),
4515 + {updated, _} = sql_query_internal(DBRef, DQuery),
4516 + SQuery1 = ["INSERT INTO ",STable," ",
4517 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
4518 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
4519 + "FROM ",TempTable,";"],
4520 + case sql_query_internal(DBRef, SQuery1) of
4521 + {updated, _} -> ok;
4522 + {error, _} -> error
4524 + {error, _} -> error
4528 + case catch apply(Fun, []) of
4530 + ?INFO_MSG("Rebuilded stats for ~p at ~p", [VHost, Date]),
4534 + {'EXIT', Reason} ->
4535 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
4538 + sql_query_internal(DBRef, ["UNLOCK TABLES;"]),
4539 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
4542 +delete_nonexistent_stats(DBRef, VHost) ->
4543 + Dates = get_dates_int(DBRef, VHost),
4544 + STable = stats_table(VHost),
4546 + Temp = lists:flatmap(fun(Date) ->
4547 + ["\"",Date,"\"",","]
4553 + % replace last "," with ");"
4554 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
4555 + Query = ["DELETE FROM ",STable," ",
4556 + "WHERE at NOT IN (", Temp1],
4557 + case sql_query_internal(DBRef, Query) of
4565 +get_user_stats_int(DBRef, User, VHost) ->
4566 + SName = stats_table(VHost),
4567 + UName = users_table(VHost),
4568 + Query = ["SELECT stats.at, sum(stats.count) ",
4569 + "FROM ",UName," AS users ",
4570 + "JOIN ",SName," AS stats ON owner_id=user_id "
4571 + "WHERE users.username=\"",User,"\" ",
4572 + "GROUP BY stats.at "
4573 + "ORDER BY DATE(stats.at) DESC;"
4575 + case sql_query_internal(DBRef, Query) of
4577 + {ok, [ {Date, list_to_integer(Count)} || [Date, Count] <- Result ]};
4578 + {error, Result} ->
4582 +delete_all_messages_by_user_at_int(DBRef, User, VHost, Date) ->
4583 + DQuery = ["DELETE FROM ",messages_table(VHost, Date)," ",
4584 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4585 + case sql_query_internal(DBRef, DQuery) of
4587 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
4593 +delete_all_stats_by_user_int(DBRef, User, VHost) ->
4594 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
4595 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4596 + case sql_query_internal(DBRef, SQuery) of
4598 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
4600 + {error, _} -> error
4603 +delete_stats_by_user_at_int(DBRef, User, VHost, Date) ->
4604 + SQuery = ["DELETE FROM ",stats_table(VHost)," ",
4605 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\") ",
4606 + "AND at=\"",Date,"\";"],
4607 + case sql_query_internal(DBRef, SQuery) of
4609 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
4611 + {error, _} -> error
4614 +delete_user_settings_int(DBRef, User, VHost) ->
4615 + Query = ["DELETE FROM ",settings_table(VHost)," ",
4616 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost)," WHERE username=\"",User,"\");"],
4617 + case sql_query_internal(DBRef, Query) of
4619 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
4621 + {error, Reason} ->
4622 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
4626 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4630 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4631 +create_temp_table(DBRef, Name) ->
4632 + Query = ["CREATE TABLE ",Name," (",
4633 + "owner_id MEDIUMINT UNSIGNED, ",
4634 + "peer_name_id MEDIUMINT UNSIGNED, ",
4635 + "peer_server_id MEDIUMINT UNSIGNED, ",
4636 + "at VARCHAR(11), ",
4638 + ") ENGINE=MyISAM CHARACTER SET utf8;"
4640 + case sql_query_internal(DBRef, Query) of
4641 + {updated, _} -> ok;
4642 + {error, _Reason} -> error
4645 +create_stats_table(#state{dbref=DBRef, vhost=VHost}=State) ->
4646 + SName = stats_table(VHost),
4647 + Query = ["CREATE TABLE ",SName," (",
4648 + "owner_id MEDIUMINT UNSIGNED, ",
4649 + "peer_name_id MEDIUMINT UNSIGNED, ",
4650 + "peer_server_id MEDIUMINT UNSIGNED, ",
4651 + "at VARCHAR(11), ",
4652 + "count INT(11), ",
4653 + "ext INTEGER DEFAULT NULL, "
4654 + "INDEX ext_i (ext), "
4655 + "INDEX(owner_id,peer_name_id,peer_server_id), ",
4657 + ") ENGINE=MyISAM CHARACTER SET utf8;"
4659 + case sql_query_internal_silent(DBRef, Query) of
4661 + ?MYDEBUG("Created stats table for ~p", [VHost]),
4662 + rebuild_all_stats_int(State),
4664 + {error, Reason} ->
4665 + case ejabberd_regexp:run(iolist_to_binary(Reason), <<"#42S01">>) of
4667 + ?MYDEBUG("Stats table for ~p already exists", [VHost]),
4668 + CheckQuery = ["SHOW COLUMNS FROM ",SName," LIKE 'peer_%_id';"],
4669 + case sql_query_internal(DBRef, CheckQuery) of
4670 + {data, Elems} when length(Elems) == 2 ->
4671 + ?MYDEBUG("Stats table structure is ok", []),
4674 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
4675 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
4677 + ?INFO_MSG("Successfully dropped ~p", [SName]);
4679 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
4684 + ?ERROR_MSG("Failed to create stats table for ~p: ~p", [VHost, Reason]),
4689 +create_settings_table(#state{dbref=DBRef, vhost=VHost}) ->
4690 + SName = settings_table(VHost),
4691 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4692 + "owner_id MEDIUMINT UNSIGNED PRIMARY KEY, ",
4693 + "dolog_default TINYINT(1) NOT NULL DEFAULT 1, ",
4694 + "dolog_list TEXT, ",
4695 + "donotlog_list TEXT ",
4696 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4698 + case sql_query_internal(DBRef, Query) of
4700 + ?MYDEBUG("Created settings table for ~p", [VHost]),
4706 +create_users_table(#state{dbref=DBRef, vhost=VHost}) ->
4707 + SName = users_table(VHost),
4708 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4709 + "username TEXT NOT NULL, ",
4710 + "user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4711 + "UNIQUE INDEX(username(",?INDEX_SIZE,")) ",
4712 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4714 + case sql_query_internal(DBRef, Query) of
4716 + ?MYDEBUG("Created users table for ~p", [VHost]),
4722 +create_servers_table(#state{dbref=DBRef, vhost=VHost}) ->
4723 + SName = servers_table(VHost),
4724 + Query = ["CREATE TABLE IF NOT EXISTS ",SName," (",
4725 + "server TEXT NOT NULL, ",
4726 + "server_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4727 + "UNIQUE INDEX(server(",?INDEX_SIZE,")) ",
4728 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4730 + case sql_query_internal(DBRef, Query) of
4732 + ?MYDEBUG("Created servers table for ~p", [VHost]),
4738 +create_resources_table(#state{dbref=DBRef, vhost=VHost}) ->
4739 + RName = resources_table(VHost),
4740 + Query = ["CREATE TABLE IF NOT EXISTS ",RName," (",
4741 + "resource TEXT NOT NULL, ",
4742 + "resource_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, ",
4743 + "UNIQUE INDEX(resource(",?INDEX_SIZE,")) ",
4744 + ") ENGINE=InnoDB CHARACTER SET utf8;"
4746 + case sql_query_internal(DBRef, Query) of
4748 + ?MYDEBUG("Created resources table for ~p", [VHost]),
4754 +create_internals(#state{dbref=DBRef, vhost=VHost}) ->
4755 + sql_query_internal(DBRef, ["DROP PROCEDURE IF EXISTS ",logmessage_name(VHost),";"]),
4756 + case sql_query_internal(DBRef, [get_logmessage(VHost)]) of
4758 + ?MYDEBUG("Created logmessage for ~p", [VHost]),
4764 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4768 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4769 +sql_query_internal(DBRef, Query) ->
4770 + case sql_query_internal_silent(DBRef, Query) of
4771 + {error, Reason} ->
4772 + ?ERROR_MSG("~p while ~p", [Reason, lists:append(Query)]),
4777 +sql_query_internal_silent(DBRef, Query) ->
4778 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
4779 + get_result(p1_mysql_conn:fetch(DBRef, Query, self(), ?MYSQL_TIMEOUT)).
4781 +get_result({updated, MySQLRes}) ->
4782 + {updated, p1_mysql:get_result_affected_rows(MySQLRes)};
4783 +get_result({data, MySQLRes}) ->
4784 + {data, p1_mysql:get_result_rows(MySQLRes)};
4785 +get_result({error, "query timed out"}) ->
4786 + {error, "query timed out"};
4787 +get_result({error, MySQLRes}) ->
4788 + Reason = p1_mysql:get_result_reason(MySQLRes),
4791 +get_user_id(DBRef, VHost, User) ->
4792 + SQuery = ["SELECT user_id FROM ",users_table(VHost)," ",
4793 + "WHERE username=\"",User,"\";"],
4794 + case sql_query_internal(DBRef, SQuery) of
4796 + IQuery = ["INSERT INTO ",users_table(VHost)," ",
4797 + "SET username=\"",User,"\";"],
4798 + case sql_query_internal_silent(DBRef, IQuery) of
4800 + {data, [[DBIdNew]]} = sql_query_internal(DBRef, SQuery),
4802 + {error, Reason} ->
4803 + % this can be in clustered environment
4804 + match = ejabberd_regexp:run(iolist_to_binary(Reason), <<"#23000">>),
4805 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
4806 + {data, [[ClID]]} = sql_query_internal(DBRef, SQuery),
4809 + {data, [[DBId]]} ->
4813 +get_logmessage(VHost) ->
4814 + UName = users_table(VHost),
4815 + SName = servers_table(VHost),
4816 + RName = resources_table(VHost),
4817 + StName = stats_table(VHost),
4819 +CREATE PROCEDURE ~s(tablename TEXT, atdate TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(10), msubject TEXT, mbody TEXT, mtimestamp DOUBLE)
4821 + DECLARE ownerID MEDIUMINT UNSIGNED;
4822 + DECLARE peer_nameID MEDIUMINT UNSIGNED;
4823 + DECLARE peer_serverID MEDIUMINT UNSIGNED;
4824 + DECLARE peer_resourceID MEDIUMINT UNSIGNED;
4825 + DECLARE Vmtype VARCHAR(10);
4826 + DECLARE Vmtimestamp DOUBLE;
4827 + DECLARE Vmdirection VARCHAR(4);
4828 + DECLARE Vmbody TEXT;
4829 + DECLARE Vmsubject TEXT;
4832 + DECLARE viewname TEXT;
4833 + DECLARE notable INT;
4834 + DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @notable = 1;
4837 + SET @ownerID = NULL;
4838 + SET @peer_nameID = NULL;
4839 + SET @peer_serverID = NULL;
4840 + SET @peer_resourceID = NULL;
4842 + SET @Vmtype = mtype;
4843 + SET @Vmtimestamp = mtimestamp;
4844 + SET @Vmdirection = mdirection;
4845 + SET @Vmbody = mbody;
4846 + SET @Vmsubject = msubject;
4848 + SELECT user_id INTO @ownerID FROM ~s WHERE username=owner;
4849 + IF @ownerID IS NULL THEN
4850 + INSERT INTO ~s SET username=owner;
4851 + SET @ownerID = LAST_INSERT_ID();
4854 + SELECT user_id INTO @peer_nameID FROM ~s WHERE username=peer_name;
4855 + IF @peer_nameID IS NULL THEN
4856 + INSERT INTO ~s SET username=peer_name;
4857 + SET @peer_nameID = LAST_INSERT_ID();
4860 + SELECT server_id INTO @peer_serverID FROM ~s WHERE server=peer_server;
4861 + IF @peer_serverID IS NULL THEN
4862 + INSERT INTO ~s SET server=peer_server;
4863 + SET @peer_serverID = LAST_INSERT_ID();
4866 + SELECT resource_id INTO @peer_resourceID FROM ~s WHERE resource=peer_resource;
4867 + IF @peer_resourceID IS NULL THEN
4868 + INSERT INTO ~s SET resource=peer_resource;
4869 + SET @peer_resourceID = LAST_INSERT_ID();
4872 + SET @iq = CONCAT(\"INSERT INTO \",tablename,\" (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (@ownerID,@peer_nameID,@peer_serverID,@peer_resourceID,@Vmdirection,@Vmtype,@Vmsubject,@Vmbody,@Vmtimestamp);\");
4873 + PREPARE insertmsg FROM @iq;
4875 + IF @notable = 1 THEN
4876 + SET @cq = CONCAT(\"CREATE TABLE \",tablename,\" (
4877 + owner_id MEDIUMINT UNSIGNED NOT NULL,
4878 + peer_name_id MEDIUMINT UNSIGNED NOT NULL,
4879 + peer_server_id MEDIUMINT UNSIGNED NOT NULL,
4880 + peer_resource_id MEDIUMINT(8) UNSIGNED NOT NULL,
4881 + direction ENUM('to', 'from') NOT NULL,
4882 + type ENUM('chat','error','groupchat','headline','normal') NOT NULL,
4885 + timestamp DOUBLE NOT NULL,
4886 + ext INTEGER DEFAULT NULL,
4887 + INDEX search_i (owner_id, peer_name_id, peer_server_id, peer_resource_id),
4888 + INDEX ext_i (ext),
4892 + CHARACTER SET utf8;\");
4893 + PREPARE createtable FROM @cq;
4894 + EXECUTE createtable;
4895 + DEALLOCATE PREPARE createtable;
4897 + SET @viewname = CONCAT(\"`v_\", TRIM(BOTH '`' FROM tablename), \"`\");
4898 + SET @cq = CONCAT(\"CREATE OR REPLACE VIEW \",@viewname,\" AS
4899 + SELECT owner.username AS owner_name,
4900 + peer.username AS peer_name,
4901 + servers.server AS peer_server,
4902 + resources.resource AS peer_resource,
4903 + messages.direction,
4907 + messages.timestamp
4913 + \", tablename,\" messages
4915 + owner.user_id=messages.owner_id and
4916 + peer.user_id=messages.peer_name_id and
4917 + servers.server_id=messages.peer_server_id and
4918 + resources.resource_id=messages.peer_resource_id
4919 + ORDER BY messages.timestamp;\");
4920 + PREPARE createview FROM @cq;
4921 + EXECUTE createview;
4922 + DEALLOCATE PREPARE createview;
4925 + PREPARE insertmsg FROM @iq;
4926 + EXECUTE insertmsg;
4927 + ELSEIF @notable = 0 THEN
4928 + EXECUTE insertmsg;
4931 + DEALLOCATE PREPARE insertmsg;
4933 + IF @notable = 0 THEN
4934 + UPDATE ~s SET count=count+1 WHERE owner_id=@ownerID AND peer_name_id=@peer_nameID AND peer_server_id=@peer_serverID AND at=atdate;
4935 + IF ROW_COUNT() = 0 THEN
4936 + INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (@ownerID, @peer_nameID, @peer_serverID, atdate, 1);
4939 +END;", [logmessage_name(VHost),UName,UName,UName,UName,SName,SName,RName,RName,UName,UName,SName,RName,StName,StName]).
4940 diff --git a/src/mod_logdb_pgsql.erl b/src/mod_logdb_pgsql.erl
4941 new file mode 100644
4942 index 0000000000..202c6ed4a8
4944 +++ b/src/mod_logdb_pgsql.erl
4946 +% {ok, DBRef} = pgsql:connect([{host, "127.0.0.1"}, {database, "logdb"}, {user, "logdb"}, {password, "logdb"}, {port, 5432}, {as_binary, true}]).
4948 +% pgsql:squery(DBRef, "CREATE TABLE test.\"logdb_stats_test\" (owner_id INTEGER, peer_name_id INTEGER, peer_server_id INTEGER, at VARCHAR(20), count integer);" ).
4949 +%%%----------------------------------------------------------------------
4950 +%%% File : mod_logdb_pgsql.erl
4951 +%%% Author : Oleg Palij (mailto:o.palij@gmail.com)
4952 +%%% Purpose : Posgresql backend for mod_logdb
4953 +%%% Url : https://paleg.github.io/mod_logdb/
4954 +%%%----------------------------------------------------------------------
4956 +-module(mod_logdb_pgsql).
4957 +-author('o.palij@gmail.com').
4959 +-include("mod_logdb.hrl").
4960 +-include("logger.hrl").
4962 +-behaviour(gen_logdb).
4963 +-behaviour(gen_server).
4966 +-export([code_change/3,handle_call/3,handle_cast/2,handle_info/2,init/1,terminate/2]).
4968 +-export([start/2, stop/1]).
4970 +-export([log_message/2,
4972 + rebuild_stats_at/2,
4973 + delete_messages_by_user_at/3, delete_all_messages_by_user_at/3, delete_messages_at/2,
4974 + get_vhost_stats/1, get_vhost_stats_at/2, get_user_stats/2, get_user_messages_at/3,
4976 + get_users_settings/1, get_user_settings/2, set_user_settings/3,
4979 +-export([view_table/3]).
4981 +% gen_server call timeout
4982 +-define(CALL_TIMEOUT, 30000).
4983 +-define(PGSQL_TIMEOUT, 60000).
4984 +-define(PROCNAME, mod_logdb_pgsql).
4986 +-import(mod_logdb, [list_to_bool/1, bool_to_list/1,
4987 + list_to_string/1, string_to_list/1,
4988 + convert_timestamp_brief/1]).
4990 +-record(state, {dbref, vhost, server, port, db, user, password, schema}).
4992 +% replace "." with "_"
4993 +escape_vhost(VHost) -> lists:map(fun(46) -> 95;
4995 + end, binary_to_list(VHost)).
4998 + Schema ++ ".\"" ++ "logdb_".
5001 + "_" ++ escape_vhost(VHost) ++ "\"".
5003 +messages_table(VHost, Schema, Date) ->
5004 + prefix(Schema) ++ "messages_" ++ Date ++ suffix(VHost).
5006 +view_table(VHost, Schema, Date) ->
5007 + Table = messages_table(VHost, Schema, Date),
5008 + TablewoS = lists:sublist(Table, length(Schema) + 3, length(Table) - length(Schema) - 3),
5009 + lists:append([Schema, ".\"v_", TablewoS, "\""]).
5011 +stats_table(VHost, Schema) ->
5012 + prefix(Schema) ++ "stats" ++ suffix(VHost).
5014 +temp_table(VHost, Schema) ->
5015 + prefix(Schema) ++ "temp" ++ suffix(VHost).
5017 +settings_table(VHost, Schema) ->
5018 + prefix(Schema) ++ "settings" ++ suffix(VHost).
5020 +users_table(VHost, Schema) ->
5021 + prefix(Schema) ++ "users" ++ suffix(VHost).
5022 +servers_table(VHost, Schema) ->
5023 + prefix(Schema) ++ "servers" ++ suffix(VHost).
5024 +resources_table(VHost, Schema) ->
5025 + prefix(Schema) ++ "resources" ++ suffix(VHost).
5027 +logmessage_name(VHost, Schema) ->
5028 + prefix(Schema) ++ "logmessage" ++ suffix(VHost).
5030 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5032 +% gen_mod callbacks
5034 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5035 +start(VHost, Opts) ->
5036 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5037 + gen_server:start({local, Proc}, ?MODULE, [VHost, Opts], []).
5040 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5041 + gen_server:call(Proc, {stop}, ?CALL_TIMEOUT).
5043 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5045 +% gen_server callbacks
5047 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5048 +init([VHost, Opts]) ->
5049 + Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
5050 + DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"ejabberd_logdb">>),
5051 + User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
5052 + Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 5432),
5053 + Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
5054 + Schema = binary_to_list(gen_mod:get_opt(schema, Opts, fun(A) -> A end, <<"public">>)),
5056 + ?MYDEBUG("Starting pgsql backend for ~s", [VHost]),
5058 + St = #state{vhost=VHost,
5059 + server=Server, port=Port, db=DB,
5060 + user=User, password=Password,
5063 + case open_pgsql_connection(St) of
5065 + State = St#state{dbref=DBRef},
5066 + ok = create_internals(State),
5067 + ok = create_stats_table(State),
5068 + ok = create_settings_table(State),
5069 + ok = create_users_table(State),
5070 + ok = create_servers_table(State),
5071 + ok = create_resources_table(State),
5072 + erlang:monitor(process, DBRef),
5074 + % this does not work
5075 + {error, Reason} ->
5076 + ?ERROR_MSG("PgSQL connection failed: ~p~n", [Reason]),
5077 + {stop, db_connection_failed};
5078 + % and this too, becouse pgsql_conn do exit() which can not be catched
5080 + ?ERROR_MSG("Rez: ~p~n", [Rez]),
5081 + {stop, db_connection_failed}
5084 +open_pgsql_connection(#state{server=Server, port=Port, db=DB, schema=Schema,
5085 + user=User, password=Password} = _State) ->
5086 + ?INFO_MSG("Opening pgsql connection ~s@~s:~p/~s", [User, Server, Port, DB]),
5087 + {ok, DBRef} = pgsql:connect(Server, DB, User, Password, Port),
5088 + {updated, _} = sql_query_internal(DBRef, ["SET SEARCH_PATH TO ",Schema,";"]),
5091 +close_pgsql_connection(DBRef) ->
5092 + ?MYDEBUG("Closing ~p pgsql connection", [DBRef]),
5093 + pgsql:terminate(DBRef).
5095 +handle_call({log_message, Msg}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5096 + Date = convert_timestamp_brief(Msg#msg.timestamp),
5097 + TableName = messages_table(VHost, Schema, Date),
5098 + ViewName = view_table(VHost, Schema, Date),
5100 + Query = [ "SELECT ", logmessage_name(VHost, Schema)," "
5101 + "('", TableName, "',",
5102 + "'", ViewName, "',",
5104 + "'", binary_to_list(Msg#msg.owner_name), "',",
5105 + "'", binary_to_list(Msg#msg.peer_name), "',",
5106 + "'", binary_to_list(Msg#msg.peer_server), "',",
5107 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.peer_resource) ), "',",
5108 + "'", atom_to_list(Msg#msg.direction), "',",
5109 + "'", binary_to_list(Msg#msg.type), "',",
5110 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.subject) ), "',",
5111 + "'", binary_to_list( ejabberd_sql:escape(Msg#msg.body) ), "',",
5112 + "'", Msg#msg.timestamp, "');"],
5114 + case sql_query_internal_silent(DBRef, Query) of
5115 + % TODO: change this
5116 + {data, [{"0"}]} ->
5117 + ?MYDEBUG("Logged ok for ~s, peer: ~s", [ [Msg#msg.owner_name, <<"@">>, VHost],
5118 + [Msg#msg.peer_name, <<"@">>, Msg#msg.peer_server] ]),
5120 + {error, _Reason} ->
5123 + {reply, ok, State};
5124 +handle_call({rebuild_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5125 + Reply = rebuild_stats_at_int(DBRef, VHost, Schema, Date),
5126 + {reply, Reply, State};
5127 +handle_call({delete_messages_by_user_at, [], _Date}, _From, State) ->
5128 + {reply, error, State};
5129 +handle_call({delete_messages_by_user_at, Msgs, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5130 + Temp = lists:flatmap(fun(#msg{timestamp=Timestamp} = _Msg) ->
5131 + ["'",Timestamp,"'",","]
5134 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
5136 + Query = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
5137 + "WHERE timestamp IN (", Temp1],
5140 + case sql_query_internal(DBRef, Query) of
5142 + rebuild_stats_at_int(DBRef, VHost, Schema, Date);
5146 + {reply, Reply, State};
5147 +handle_call({delete_all_messages_by_user_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5148 + ok = delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date),
5149 + ok = delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date),
5150 + {reply, ok, State};
5151 +handle_call({delete_messages_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5152 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5154 + case sql_query_internal(DBRef, ["DROP TABLE ",messages_table(VHost, Schema, Date)," CASCADE;"]) of
5156 + Query = ["DELETE FROM ",stats_table(VHost, Schema)," "
5157 + "WHERE at='",Date,"';"],
5158 + case sql_query_internal(DBRef, Query) of
5167 + {reply, Reply, State};
5168 +handle_call({get_vhost_stats}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5169 + SName = stats_table(VHost, Schema),
5170 + Query = ["SELECT at, sum(count) ",
5171 + "FROM ",SName," ",
5173 + "ORDER BY DATE(at) DESC;"
5176 + case sql_query_internal(DBRef, Query) of
5178 + {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs]};
5179 + {error, Reason} ->
5180 + % TODO: Duplicate error message ?
5183 + {reply, Reply, State};
5184 +handle_call({get_vhost_stats_at, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5185 + SName = stats_table(VHost, Schema),
5186 + Query = ["SELECT username, sum(count) AS allcount ",
5187 + "FROM ",SName," ",
5188 + "JOIN ",users_table(VHost, Schema)," ON owner_id=user_id ",
5189 + "WHERE at='",Date,"' ",
5190 + "GROUP BY username ",
5191 + "ORDER BY allcount DESC;"
5194 + case sql_query_internal(DBRef, Query) of
5196 + RFun = fun({User, Count}) ->
5197 + {User, list_to_integer(Count)}
5199 + {ok, lists:reverse(lists:keysort(2, lists:map(RFun, Recs)))};
5200 + {error, Reason} ->
5204 + {reply, Reply, State};
5205 +handle_call({get_user_stats, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5206 + {reply, get_user_stats_int(DBRef, Schema, User, VHost), State};
5207 +handle_call({get_user_messages_at, User, Date}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5208 + Query = ["SELECT peer_name,",
5216 + "FROM ",view_table(VHost, Schema, Date)," "
5217 + "WHERE owner_name='",User,"';"],
5219 + case sql_query_internal(DBRef, Query) of
5221 + Fun = fun({Peer_name, Peer_server, Peer_resource,
5226 + #msg{peer_name=Peer_name, peer_server=Peer_server, peer_resource=Peer_resource,
5227 + direction=list_to_atom(Direction),
5229 + subject=Subject, body=Body,
5230 + timestamp=Timestamp}
5232 + {ok, lists:map(Fun, Recs)};
5233 + {error, Reason} ->
5236 + {reply, Reply, State};
5237 +handle_call({get_dates}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5238 + SName = stats_table(VHost, Schema),
5239 + Query = ["SELECT at ",
5240 + "FROM ",SName," ",
5242 + "ORDER BY at DESC;"
5245 + case sql_query_internal(DBRef, Query) of
5247 + [ Date || {Date} <- Result ];
5248 + {error, Reason} ->
5251 + {reply, Reply, State};
5252 +handle_call({get_users_settings}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5253 + Query = ["SELECT username,dolog_default,dolog_list,donotlog_list ",
5254 + "FROM ",settings_table(VHost, Schema)," ",
5255 + "JOIN ",users_table(VHost, Schema)," ON user_id=owner_id;"],
5257 + case sql_query_internal(DBRef, Query) of
5259 + {ok, [#user_settings{owner_name=Owner,
5260 + dolog_default=list_to_bool(DoLogDef),
5261 + dolog_list=string_to_list(DoLogL),
5262 + donotlog_list=string_to_list(DoNotLogL)
5263 + } || {Owner, DoLogDef, DoLogL, DoNotLogL} <- Recs]};
5264 + {error, Reason} ->
5267 + {reply, Reply, State};
5268 +handle_call({get_user_settings, User}, _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5269 + Query = ["SELECT dolog_default,dolog_list,donotlog_list ",
5270 + "FROM ",settings_table(VHost, Schema)," ",
5271 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5273 + case sql_query_internal_silent(DBRef, Query) of
5276 + {data, [{DoLogDef, DoLogL, DoNotLogL}]} ->
5277 + {ok, #user_settings{owner_name=User,
5278 + dolog_default=list_to_bool(DoLogDef),
5279 + dolog_list=string_to_list(DoLogL),
5280 + donotlog_list=string_to_list(DoNotLogL)}};
5281 + {error, Reason} ->
5282 + ?ERROR_MSG("Failed to get_user_settings for ~s@~s: ~p", [User, VHost, Reason]),
5285 + {reply, Reply, State};
5286 +handle_call({set_user_settings, User, #user_settings{dolog_default=DoLogDef,
5287 + dolog_list=DoLogL,
5288 + donotlog_list=DoNotLogL}},
5289 + _From, #state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5290 + User_id = get_user_id(DBRef, VHost, Schema, User),
5291 + Query = ["UPDATE ",settings_table(VHost, Schema)," ",
5292 + "SET dolog_default=",bool_to_list(DoLogDef),", ",
5293 + "dolog_list='",list_to_string(DoLogL),"', ",
5294 + "donotlog_list='",list_to_string(DoNotLogL),"' ",
5295 + "WHERE owner_id=",User_id,";"],
5298 + case sql_query_internal(DBRef, Query) of
5300 + IQuery = ["INSERT INTO ",settings_table(VHost, Schema)," ",
5301 + "(owner_id, dolog_default, dolog_list, donotlog_list) ",
5303 + "(",User_id,", ",bool_to_list(DoLogDef),",'",list_to_string(DoLogL),"','",list_to_string(DoNotLogL),"');"],
5304 + case sql_query_internal(DBRef, IQuery) of
5306 + ?MYDEBUG("New settings for ~s@~s", [User, VHost]),
5312 + ?MYDEBUG("Updated settings for ~s@~s", [User, VHost]),
5317 + {reply, Reply, State};
5318 +handle_call({stop}, _From, State) ->
5319 + ?MYDEBUG("Stoping pgsql backend for ~p", [State#state.vhost]),
5320 + {stop, normal, ok, State};
5321 +handle_call(Msg, _From, State) ->
5322 + ?INFO_MSG("Got call Msg: ~p, State: ~p", [Msg, State]),
5326 +handle_cast({rebuild_stats}, State) ->
5327 + rebuild_all_stats_int(State),
5329 +handle_cast({drop_user, User}, #state{vhost=VHost, schema=Schema}=State) ->
5331 + {ok, DBRef} = open_pgsql_connection(State),
5332 + {ok, Dates} = get_user_stats_int(DBRef, Schema, User, VHost),
5333 + MDResult = lists:map(fun({Date, _}) ->
5334 + delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date)
5336 + StDResult = delete_all_stats_by_user_int(DBRef, Schema, User, VHost),
5337 + SDResult = delete_user_settings_int(DBRef, Schema, User, VHost),
5338 + case lists:all(fun(Result) when Result == ok ->
5340 + (Result) when Result == error ->
5342 + end, lists:append([MDResult, [StDResult], [SDResult]])) of
5344 + ?INFO_MSG("Removed ~s@~s", [User, VHost]);
5346 + ?ERROR_MSG("Failed to remove ~s@~s", [User, VHost])
5348 + close_pgsql_connection(DBRef)
5352 +handle_cast(Msg, State) ->
5353 + ?INFO_MSG("Got cast Msg:~p, State:~p", [Msg, State]),
5356 +handle_info({'DOWN', _MonitorRef, process, _Pid, _Info}, State) ->
5357 + {stop, connection_dropped, State};
5358 +handle_info(Info, State) ->
5359 + ?INFO_MSG("Got Info:~p, State:~p", [Info, State]),
5362 +terminate(_Reason, #state{dbref=DBRef}=_State) ->
5363 + close_pgsql_connection(DBRef),
5366 +code_change(_OldVsn, State, _Extra) ->
5369 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5371 +% gen_logdb callbacks
5373 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5374 +log_message(VHost, Msg) ->
5375 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5376 + gen_server:call(Proc, {log_message, Msg}, ?CALL_TIMEOUT).
5377 +rebuild_stats(VHost) ->
5378 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5379 + gen_server:cast(Proc, {rebuild_stats}).
5380 +rebuild_stats_at(VHost, Date) ->
5381 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5382 + gen_server:call(Proc, {rebuild_stats_at, Date}, ?CALL_TIMEOUT).
5383 +delete_messages_by_user_at(VHost, Msgs, Date) ->
5384 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5385 + gen_server:call(Proc, {delete_messages_by_user_at, Msgs, Date}, ?CALL_TIMEOUT).
5386 +delete_all_messages_by_user_at(User, VHost, Date) ->
5387 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5388 + gen_server:call(Proc, {delete_all_messages_by_user_at, User, Date}, ?CALL_TIMEOUT).
5389 +delete_messages_at(VHost, Date) ->
5390 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5391 + gen_server:call(Proc, {delete_messages_at, Date}, ?CALL_TIMEOUT).
5392 +get_vhost_stats(VHost) ->
5393 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5394 + gen_server:call(Proc, {get_vhost_stats}, ?CALL_TIMEOUT).
5395 +get_vhost_stats_at(VHost, Date) ->
5396 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5397 + gen_server:call(Proc, {get_vhost_stats_at, Date}, ?CALL_TIMEOUT).
5398 +get_user_stats(User, VHost) ->
5399 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5400 + gen_server:call(Proc, {get_user_stats, User}, ?CALL_TIMEOUT).
5401 +get_user_messages_at(User, VHost, Date) ->
5402 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5403 + gen_server:call(Proc, {get_user_messages_at, User, Date}, ?CALL_TIMEOUT).
5404 +get_dates(VHost) ->
5405 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5406 + gen_server:call(Proc, {get_dates}, ?CALL_TIMEOUT).
5407 +get_users_settings(VHost) ->
5408 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5409 + gen_server:call(Proc, {get_users_settings}, ?CALL_TIMEOUT).
5410 +get_user_settings(User, VHost) ->
5411 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5412 + gen_server:call(Proc, {get_user_settings, User}, ?CALL_TIMEOUT).
5413 +set_user_settings(User, VHost, Set) ->
5414 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5415 + gen_server:call(Proc, {set_user_settings, User, Set}, ?CALL_TIMEOUT).
5416 +drop_user(User, VHost) ->
5417 + Proc = gen_mod:get_module_proc(VHost, ?PROCNAME),
5418 + gen_server:cast(Proc, {drop_user, User}).
5420 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5424 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5425 +get_dates_int(DBRef, VHost) ->
5426 + Query = ["SELECT n.nspname as \"Schema\",
5427 + c.relname as \"Name\",
5428 + CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' END as \"Type\",
5429 + r.rolname as \"Owner\"
5430 + FROM pg_catalog.pg_class c
5431 + JOIN pg_catalog.pg_roles r ON r.oid = c.relowner
5432 + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
5433 + WHERE c.relkind IN ('r','')
5434 + AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
5435 + AND c.relname ~ '^(.*",escape_vhost(VHost),".*)$'
5436 + AND pg_catalog.pg_table_is_visible(c.oid)
5438 + case sql_query_internal(DBRef, Query) of
5440 + lists:foldl(fun({_Schema, Table, _Type, _Owner}, Dates) ->
5441 + case re:run(Table,"[0-9]+-[0-9]+-[0-9]+") of
5442 + {match, [{S, E}]} ->
5443 + lists:append(Dates, [lists:sublist(Table, S+1, E)]);
5452 +rebuild_all_stats_int(#state{vhost=VHost, schema=Schema}=State) ->
5454 + {ok, DBRef} = open_pgsql_connection(State),
5455 + ok = delete_nonexistent_stats(DBRef, Schema, VHost),
5456 + case lists:filter(fun(Date) ->
5457 + case catch rebuild_stats_at_int(DBRef, VHost, Schema, Date) of
5460 + {'EXIT', _} -> true
5462 + end, get_dates_int(DBRef, VHost)) of
5465 + ?ERROR_MSG("Failed to rebuild stats for ~p dates", [FTables]),
5468 + close_pgsql_connection(DBRef)
5472 +rebuild_stats_at_int(DBRef, VHost, Schema, Date) ->
5473 + TempTable = temp_table(VHost, Schema),
5476 + Table = messages_table(VHost, Schema, Date),
5477 + STable = stats_table(VHost, Schema),
5479 + DQuery = [ "DELETE FROM ",STable," ",
5480 + "WHERE at='",Date,"';"],
5482 + ok = create_temp_table(DBRef, VHost, Schema),
5483 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",Table," IN ACCESS EXCLUSIVE MODE;"]),
5484 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
5485 + SQuery = ["INSERT INTO ",TempTable," ",
5486 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
5487 + "SELECT owner_id,peer_name_id,peer_server_id,'",Date,"'",",count(*) ",
5488 + "FROM ",Table," GROUP BY owner_id,peer_name_id,peer_server_id;"],
5489 + case sql_query_internal(DBRef, SQuery) of
5491 + Count = sql_query_internal(DBRef, ["SELECT count(*) FROM ",Table,";"]),
5493 + {data, [{"0"}]} ->
5494 + {updated, _} = sql_query_internal(DBRef, ["DROP VIEW ",view_table(VHost, Schema, Date),";"]),
5495 + {updated, _} = sql_query_internal(DBRef, ["DROP TABLE ",Table," CASCADE;"]),
5496 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
5497 + {updated, _} = sql_query_internal(DBRef, DQuery),
5500 + ?ERROR_MSG("Failed to calculate stats for ~s table! Count was ~p.", [Date, Count]),
5504 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",STable," IN ACCESS EXCLUSIVE MODE;"]),
5505 + {updated, _} = sql_query_internal(DBRef, ["LOCK TABLE ",TempTable," IN ACCESS EXCLUSIVE MODE;"]),
5506 + {updated, _} = sql_query_internal(DBRef, DQuery),
5507 + SQuery1 = ["INSERT INTO ",STable," ",
5508 + "(owner_id,peer_name_id,peer_server_id,at,count) ",
5509 + "SELECT owner_id,peer_name_id,peer_server_id,at,count ",
5510 + "FROM ",TempTable,";"],
5511 + case sql_query_internal(DBRef, SQuery1) of
5512 + {updated, _} -> ok;
5513 + {error, _} -> error
5515 + {error, _} -> error
5519 + case sql_transaction_internal(DBRef, Fun) of
5521 + ?INFO_MSG("Rebuilded stats for ~s at ~s", [VHost, Date]),
5523 + {aborted, Reason} ->
5524 + ?ERROR_MSG("Failed to rebuild stats for ~s table: ~p.", [Date, Reason]),
5527 + sql_query_internal(DBRef, ["DROP TABLE ",TempTable,";"]),
5530 +delete_nonexistent_stats(DBRef, Schema, VHost) ->
5531 + Dates = get_dates_int(DBRef, VHost),
5532 + STable = stats_table(VHost, Schema),
5534 + Temp = lists:flatmap(fun(Date) ->
5535 + ["'",Date,"'",","]
5542 + % replace last "," with ");"
5543 + Temp1 = lists:append([lists:sublist(Temp, length(Temp)-1), ");"]),
5544 + Query = ["DELETE FROM ",STable," ",
5545 + "WHERE at NOT IN (", Temp1],
5546 + case sql_query_internal(DBRef, Query) of
5554 +get_user_stats_int(DBRef, Schema, User, VHost) ->
5555 + SName = stats_table(VHost, Schema),
5556 + UName = users_table(VHost, Schema),
5557 + Query = ["SELECT stats.at, sum(stats.count) ",
5558 + "FROM ",UName," AS users ",
5559 + "JOIN ",SName," AS stats ON owner_id=user_id "
5560 + "WHERE users.username='",User,"' ",
5561 + "GROUP BY stats.at "
5562 + "ORDER BY DATE(at) DESC;"
5564 + case sql_query_internal(DBRef, Query) of
5566 + {ok, [ {Date, list_to_integer(Count)} || {Date, Count} <- Recs ]};
5567 + {error, Result} ->
5571 +delete_all_messages_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
5572 + DQuery = ["DELETE FROM ",messages_table(VHost, Schema, Date)," ",
5573 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5574 + case sql_query_internal(DBRef, DQuery) of
5576 + ?INFO_MSG("Dropped messages for ~s@~s at ~s", [User, VHost, Date]),
5582 +delete_all_stats_by_user_int(DBRef, Schema, User, VHost) ->
5583 + SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
5584 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5585 + case sql_query_internal(DBRef, SQuery) of
5587 + ?INFO_MSG("Dropped all stats for ~s@~s", [User, VHost]),
5589 + {error, _} -> error
5592 +delete_stats_by_user_at_int(DBRef, Schema, User, VHost, Date) ->
5593 + SQuery = ["DELETE FROM ",stats_table(VHost, Schema)," ",
5594 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"') ",
5595 + "AND at='",Date,"';"],
5596 + case sql_query_internal(DBRef, SQuery) of
5598 + ?INFO_MSG("Dropped stats for ~s@~s at ~s", [User, VHost, Date]),
5600 + {error, _} -> error
5603 +delete_user_settings_int(DBRef, Schema, User, VHost) ->
5604 + Query = ["DELETE FROM ",settings_table(VHost, Schema)," ",
5605 + "WHERE owner_id=(SELECT user_id FROM ",users_table(VHost, Schema)," WHERE username='",User,"');"],
5606 + case sql_query_internal(DBRef, Query) of
5608 + ?INFO_MSG("Dropped ~s@~s settings", [User, VHost]),
5610 + {error, Reason} ->
5611 + ?ERROR_MSG("Failed to drop ~s@~s settings: ~p", [User, VHost, Reason]),
5615 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5619 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5620 +create_temp_table(DBRef, VHost, Schema) ->
5621 + TName = temp_table(VHost, Schema),
5622 + Query = ["CREATE TABLE ",TName," (",
5623 + "owner_id INTEGER, ",
5624 + "peer_name_id INTEGER, ",
5625 + "peer_server_id INTEGER, ",
5626 + "at VARCHAR(20), ",
5630 + case sql_query_internal(DBRef, Query) of
5631 + {updated, _} -> ok;
5632 + {error, _Reason} -> error
5635 +create_stats_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5636 + SName = stats_table(VHost, Schema),
5640 + Query = ["CREATE TABLE ",SName," (",
5641 + "owner_id INTEGER, ",
5642 + "peer_name_id INTEGER, ",
5643 + "peer_server_id INTEGER, ",
5644 + "at VARCHAR(20), ",
5648 + case sql_query_internal_silent(DBRef, Query) of
5650 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_search_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (owner_id, peer_name_id, peer_server_id);"]),
5651 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"s_at_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (at);"]),
5653 + {error, Reason} ->
5654 + case lists:keysearch(code, 1, Reason) of
5655 + {value, {code, "42P07"}} ->
5658 + ?ERROR_MSG("Failed to create stats table for ~s: ~p", [VHost, Reason]),
5663 + case sql_transaction_internal(DBRef, Fun) of
5664 + {atomic, created} ->
5665 + ?MYDEBUG("Created stats table for ~s", [VHost]),
5666 + rebuild_all_stats_int(State),
5668 + {atomic, exists} ->
5669 + ?MYDEBUG("Stats table for ~s already exists", [VHost]),
5670 + {match, [{F, L}]} = re:run(SName, "\".*\""),
5671 + QTable = lists:sublist(SName, F+2, L-2),
5672 + OIDQuery = ["SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname='",QTable,"' AND pg_catalog.pg_table_is_visible(c.oid);"],
5673 + {data,[{OID}]} = sql_query_internal(DBRef, OIDQuery),
5674 + CheckQuery = ["SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = '",OID,"' AND a.attnum > 0 AND NOT a.attisdropped AND a.attname ~ '^peer_.*_id$';"],
5675 + case sql_query_internal(DBRef, CheckQuery) of
5676 + {data, Elems} when length(Elems) == 2 ->
5677 + ?MYDEBUG("Stats table structure is ok", []),
5680 + ?INFO_MSG("It seems like stats table structure is invalid. I will drop it and recreate", []),
5681 + case sql_query_internal(DBRef, ["DROP TABLE ",SName,";"]) of
5683 + ?INFO_MSG("Successfully dropped ~p", [SName]);
5685 + ?ERROR_MSG("Failed to drop ~p. You should drop it and restart module", [SName])
5689 + {error, _} -> error
5692 +create_settings_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5693 + SName = settings_table(VHost, Schema),
5694 + Query = ["CREATE TABLE ",SName," (",
5695 + "owner_id INTEGER PRIMARY KEY, ",
5696 + "dolog_default BOOLEAN, ",
5697 + "dolog_list TEXT DEFAULT '', ",
5698 + "donotlog_list TEXT DEFAULT ''",
5701 + case sql_query_internal_silent(DBRef, Query) of
5703 + ?MYDEBUG("Created settings table for ~s", [VHost]),
5705 + {error, Reason} ->
5706 + case lists:keysearch(code, 1, Reason) of
5707 + {value, {code, "42P07"}} ->
5708 + ?MYDEBUG("Settings table for ~s already exists", [VHost]),
5711 + ?ERROR_MSG("Failed to create settings table for ~s: ~p", [VHost, Reason]),
5716 +create_users_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5717 + SName = users_table(VHost, Schema),
5721 + Query = ["CREATE TABLE ",SName," (",
5722 + "username TEXT UNIQUE, ",
5723 + "user_id SERIAL PRIMARY KEY",
5726 + case sql_query_internal_silent(DBRef, Query) of
5728 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"username_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (username);"]),
5730 + {error, Reason} ->
5731 + case lists:keysearch(code, 1, Reason) of
5732 + {value, {code, "42P07"}} ->
5735 + ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
5740 + case sql_transaction_internal(DBRef, Fun) of
5741 + {atomic, created} ->
5742 + ?MYDEBUG("Created users table for ~s", [VHost]),
5744 + {atomic, exists} ->
5745 + ?MYDEBUG("Users table for ~s already exists", [VHost]),
5747 + {aborted, _} -> error
5750 +create_servers_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5751 + SName = servers_table(VHost, Schema),
5754 + Query = ["CREATE TABLE ",SName," (",
5755 + "server TEXT UNIQUE, ",
5756 + "server_id SERIAL PRIMARY KEY",
5759 + case sql_query_internal_silent(DBRef, Query) of
5761 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"server_i_",Schema,"_",escape_vhost(VHost),"\" ON ",SName," (server);"]),
5763 + {error, Reason} ->
5764 + case lists:keysearch(code, 1, Reason) of
5765 + {value, {code, "42P07"}} ->
5768 + ?ERROR_MSG("Failed to create servers table for ~s: ~p", [VHost, Reason]),
5773 + case sql_transaction_internal(DBRef, Fun) of
5774 + {atomic, created} ->
5775 + ?MYDEBUG("Created servers table for ~s", [VHost]),
5777 + {atomic, exists} ->
5778 + ?MYDEBUG("Servers table for ~s already exists", [VHost]),
5780 + {aborted, _} -> error
5783 +create_resources_table(#state{dbref=DBRef, vhost=VHost, schema=Schema}) ->
5784 + RName = resources_table(VHost, Schema),
5786 + Query = ["CREATE TABLE ",RName," (",
5787 + "resource TEXT UNIQUE, ",
5788 + "resource_id SERIAL PRIMARY KEY",
5791 + case sql_query_internal_silent(DBRef, Query) of
5793 + {updated, _} = sql_query_internal(DBRef, ["CREATE INDEX \"resource_i_",Schema,"_",escape_vhost(VHost),"\" ON ",RName," (resource);"]),
5795 + {error, Reason} ->
5796 + case lists:keysearch(code, 1, Reason) of
5797 + {value, {code, "42P07"}} ->
5800 + ?ERROR_MSG("Failed to create users table for ~s: ~p", [VHost, Reason]),
5805 + case sql_transaction_internal(DBRef, Fun) of
5806 + {atomic, created} ->
5807 + ?MYDEBUG("Created resources table for ~s", [VHost]),
5809 + {atomic, exists} ->
5810 + ?MYDEBUG("Resources table for ~s already exists", [VHost]),
5812 + {aborted, _} -> error
5815 +create_internals(#state{dbref=DBRef, vhost=VHost, schema=Schema}=State) ->
5816 + sql_query_internal(DBRef, ["DROP FUNCTION IF EXISTS ",logmessage_name(VHost,Schema)," (tbname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION);"]),
5817 + case sql_query_internal(DBRef, [get_logmessage(VHost, Schema)]) of
5819 + ?MYDEBUG("Created logmessage for ~p", [VHost]),
5821 + {error, Reason} ->
5822 + case lists:keysearch(code, 1, Reason) of
5823 + {value, {code, "42704"}} ->
5824 + ?ERROR_MSG("plpgsql language must be installed into database '~s'. Use CREATE LANGUAGE...", [State#state.db]),
5831 +get_user_id(DBRef, VHost, Schema, User) ->
5832 + SQuery = ["SELECT user_id FROM ",users_table(VHost, Schema)," ",
5833 + "WHERE username='",User,"';"],
5834 + case sql_query_internal(DBRef, SQuery) of
5836 + IQuery = ["INSERT INTO ",users_table(VHost, Schema)," ",
5837 + "VALUES ('",User,"');"],
5838 + case sql_query_internal_silent(DBRef, IQuery) of
5840 + {data, [{DBIdNew}]} = sql_query_internal(DBRef, SQuery),
5842 + {error, Reason} ->
5843 + % this can be in clustered environment
5844 + {value, {code, "23505"}} = lists:keysearch(code, 1, Reason),
5845 + ?ERROR_MSG("Duplicate key name for ~p", [User]),
5846 + {data, [{ClID}]} = sql_query_internal(DBRef, SQuery),
5849 + {data, [{DBId}]} ->
5853 +get_logmessage(VHost,Schema) ->
5854 + UName = users_table(VHost,Schema),
5855 + SName = servers_table(VHost,Schema),
5856 + RName = resources_table(VHost,Schema),
5857 + StName = stats_table(VHost,Schema),
5858 + io_lib:format("CREATE OR REPLACE FUNCTION ~s (tbname TEXT, vname TEXT, atdt TEXT, owner TEXT, peer_name TEXT, peer_server TEXT, peer_resource TEXT, mdirection VARCHAR(4), mtype VARCHAR(9), msubj TEXT, mbody TEXT, mtimestamp DOUBLE PRECISION) RETURNS INTEGER AS $$
5861 + peer_nameID INTEGER;
5862 + peer_serverID INTEGER;
5863 + peer_resourceID INTEGER;
5864 + tablename ALIAS for $1;
5865 + viewname ALIAS for $2;
5866 + atdate ALIAS for $3;
5868 + SELECT INTO ownerID user_id FROM ~s WHERE username = owner;
5870 + INSERT INTO ~s (username) VALUES (owner);
5871 + ownerID := lastval();
5874 + SELECT INTO peer_nameID user_id FROM ~s WHERE username = peer_name;
5876 + INSERT INTO ~s (username) VALUES (peer_name);
5877 + peer_nameID := lastval();
5880 + SELECT INTO peer_serverID server_id FROM ~s WHERE server = peer_server;
5882 + INSERT INTO ~s (server) VALUES (peer_server);
5883 + peer_serverID := lastval();
5886 + SELECT INTO peer_resourceID resource_id FROM ~s WHERE resource = peer_resource;
5888 + INSERT INTO ~s (resource) VALUES (peer_resource);
5889 + peer_resourceID := lastval();
5893 + EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
5894 + EXCEPTION WHEN undefined_table THEN
5895 + EXECUTE 'CREATE TABLE ' || tablename || ' (' ||
5896 + 'owner_id INTEGER, ' ||
5897 + 'peer_name_id INTEGER, ' ||
5898 + 'peer_server_id INTEGER, ' ||
5899 + 'peer_resource_id INTEGER, ' ||
5900 + 'direction VARCHAR(4) CHECK (direction IN (''to'',''from'')), ' ||
5901 + 'type VARCHAR(9) CHECK (type IN (''chat'',''error'',''groupchat'',''headline'',''normal'')), ' ||
5902 + 'subject TEXT, ' ||
5904 + 'timestamp DOUBLE PRECISION)';
5905 + EXECUTE 'CREATE INDEX \"search_i_' || '~s' || '_' || atdate || '_' || '~s' || '\"' || ' ON ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id)';
5907 + EXECUTE 'CREATE OR REPLACE VIEW ' || viewname || ' AS ' ||
5908 + 'SELECT owner.username AS owner_name, ' ||
5909 + 'peer.username AS peer_name, ' ||
5910 + 'servers.server AS peer_server, ' ||
5911 + 'resources.resource AS peer_resource, ' ||
5912 + 'messages.direction, ' ||
5913 + 'messages.type, ' ||
5914 + 'messages.subject, ' ||
5915 + 'messages.body, ' ||
5916 + 'messages.timestamp ' ||
5921 + '~s resources, ' ||
5922 + tablename || ' messages ' ||
5924 + 'owner.user_id=messages.owner_id and ' ||
5925 + 'peer.user_id=messages.peer_name_id and ' ||
5926 + 'servers.server_id=messages.peer_server_id and ' ||
5927 + 'resources.resource_id=messages.peer_resource_id ' ||
5928 + 'ORDER BY messages.timestamp';
5930 + EXECUTE 'INSERT INTO ' || tablename || ' (owner_id, peer_name_id, peer_server_id, peer_resource_id, direction, type, subject, body, timestamp) VALUES (' || ownerID || ',' || peer_nameID || ',' || peer_serverID || ',' || peer_resourceID || ',''' || mdirection || ''',''' || mtype || ''',' || quote_literal(msubj) || ',' || quote_literal(mbody) || ',' || mtimestamp || ')';
5933 + UPDATE ~s SET count=count+1 where at=atdate and owner_id=ownerID and peer_name_id=peer_nameID and peer_server_id=peer_serverID;
5935 + INSERT INTO ~s (owner_id, peer_name_id, peer_server_id, at, count) VALUES (ownerID, peer_nameID, peer_serverID, atdate, 1);
5939 +$$ LANGUAGE plpgsql;
5940 +", [logmessage_name(VHost,Schema),UName,UName,UName,UName,SName,SName,RName,RName,Schema,escape_vhost(VHost),UName,UName,SName,RName,StName,StName]).
5942 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5946 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5947 +% like do_transaction/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
5948 +sql_transaction_internal(DBRef, Fun) ->
5949 + case sql_query_internal(DBRef, ["BEGIN;"]) of
5951 + case catch Fun() of
5953 + rollback_internal(DBRef, Err);
5954 + {error, _} = Err ->
5955 + rollback_internal(DBRef, Err);
5956 + {'EXIT', _} = Err ->
5957 + rollback_internal(DBRef, Err);
5959 + case sql_query_internal(DBRef, ["COMMIT;"]) of
5960 + {error, _} -> rollback_internal(DBRef, {commit_error});
5963 + {atomic, _} -> Res;
5964 + _ -> {atomic, Res}
5969 + {aborted, {begin_error}}
5972 +% like rollback/2 in mysql_conn.erl (changeset by Yariv Sadan <yarivvv@gmail.com>)
5973 +rollback_internal(DBRef, Reason) ->
5974 + Res = sql_query_internal(DBRef, ["ROLLBACK;"]),
5975 + {aborted, {Reason, {rollback_result, Res}}}.
5977 +sql_query_internal(DBRef, Query) ->
5978 + case sql_query_internal_silent(DBRef, Query) of
5979 + {error, undefined, Rez} ->
5980 + ?ERROR_MSG("Got undefined result: ~p while ~p", [Rez, lists:append(Query)]),
5981 + {error, undefined};
5983 + ?ERROR_MSG("Failed: ~p while ~p", [Error, lists:append(Query)]),
5988 +sql_query_internal_silent(DBRef, Query) ->
5989 + ?MYDEBUG("DOING: \"~s\"", [lists:append(Query)]),
5990 + % TODO: use pquery?
5991 + get_result(pgsql:squery(DBRef, Query)).
5993 +get_result({ok, ["CREATE TABLE"]}) ->
5995 +get_result({ok, ["DROP TABLE"]}) ->
5997 +get_result({ok, ["ALTER TABLE"]}) ->
5999 +get_result({ok,["DROP VIEW"]}) ->
6001 +get_result({ok,["DROP FUNCTION"]}) ->
6003 +get_result({ok, ["CREATE INDEX"]}) ->
6005 +get_result({ok, ["CREATE FUNCTION"]}) ->
6007 +get_result({ok, [{[$S, $E, $L, $E, $C, $T, $ | _Rest], _Rows, Recs}]}) ->
6010 + lists:map(fun(Elem) when is_binary(Elem) ->
6011 + binary_to_list(Elem);
6012 + (Elem) when is_list(Elem) ->
6014 + (Elem) when is_integer(Elem) ->
6015 + integer_to_list(Elem);
6016 + (Elem) when is_float(Elem) ->
6017 + float_to_list(Elem);
6018 + (Elem) when is_boolean(Elem) ->
6019 + atom_to_list(Elem);
6021 + ?ERROR_MSG("Unknown element type ~p", [Elem]),
6025 + Res = lists:map(Fun, Recs),
6026 + %{data, [list_to_tuple(Rec) || Rec <- Recs]};
6028 +get_result({ok, ["INSERT " ++ OIDN]}) ->
6029 + [_OID, N] = string:tokens(OIDN, " "),
6030 + {updated, list_to_integer(N)};
6031 +get_result({ok, ["DELETE " ++ N]}) ->
6032 + {updated, list_to_integer(N)};
6033 +get_result({ok, ["UPDATE " ++ N]}) ->
6034 + {updated, list_to_integer(N)};
6035 +get_result({ok, ["BEGIN"]}) ->
6037 +get_result({ok, ["LOCK TABLE"]}) ->
6039 +get_result({ok, ["ROLLBACK"]}) ->
6041 +get_result({ok, ["COMMIT"]}) ->
6043 +get_result({ok, ["SET"]}) ->
6045 +get_result({ok, [{error, Error}]}) ->
6048 + {error, undefined, Rez}.
6050 diff --git a/src/mod_roster.erl b/src/mod_roster.erl
6051 index 426589319c..6b51d3c381 100644
6052 --- a/src/mod_roster.erl
6053 +++ b/src/mod_roster.erl
6055 -define(ROSTER_ITEM_CACHE, roster_item_cache).
6056 -define(ROSTER_VERSION_CACHE, roster_version_cache).
6058 +-include("mod_logdb.hrl").
6060 -type c2s_state() :: ejabberd_c2s:state().
6061 -export_type([subscription/0]).
6063 @@ -943,6 +945,14 @@ user_roster(User, Server, Query, Lang) ->
6065 Items = get_roster(LUser, LServer),
6066 SItems = lists:sort(Items),
6068 + Settings = case gen_mod:is_loaded(Server, mod_logdb) of
6070 + mod_logdb:get_user_settings(User, Server);
6075 FItems = case SItems of
6076 [] -> [?CT(?T("None"))];
6078 @@ -1000,7 +1010,33 @@ user_roster(User, Server, Query, Lang) ->
6079 [?INPUTT(<<"submit">>,
6081 (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6084 + case gen_mod:is_loaded(Server, mod_logdb) of
6086 + Peer = jid:encode(R#roster.jid),
6087 + A = lists:member(Peer, Settings#user_settings.dolog_list),
6088 + B = lists:member(Peer, Settings#user_settings.donotlog_list),
6092 + {<<"donotlog">>, <<"Do Not Log Messages">>};
6094 + {<<"dolog">>, <<"Log Messages">>};
6095 + Settings#user_settings.dolog_default == true ->
6096 + {<<"donotlog">>, <<"Do Not Log Messages">>};
6097 + Settings#user_settings.dolog_default == false ->
6098 + {<<"dolog">>, <<"Log Messages">>}
6101 + ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}],
6102 + [?INPUTT(<<"submit">>,
6104 + (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>,
6113 @@ -1107,9 +1143,42 @@ user_roster_item_parse_query(User, Server, Items,
6114 sub_els = [#roster_query{
6115 items = [RosterItem]}]}),
6121 + case lists:keysearch(
6122 + <<"donotlog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
6124 + Peer = jid:encode(JID),
6125 + Settings = mod_logdb:get_user_settings(User, Server),
6126 + DNLL = case lists:member(Peer, Settings#user_settings.donotlog_list) of
6127 + false -> lists:append(Settings#user_settings.donotlog_list, [Peer]);
6128 + true -> Settings#user_settings.donotlog_list
6130 + DLL = lists:delete(jid:encode(JID), Settings#user_settings.dolog_list),
6131 + Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
6132 + % TODO: check returned value
6133 + ok = mod_logdb:set_user_settings(User, Server, Sett),
6136 + case lists:keysearch(
6137 + <<"dolog", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of
6139 + Peer = jid:encode(JID),
6140 + Settings = mod_logdb:get_user_settings(User, Server),
6141 + DLL = case lists:member(Peer, Settings#user_settings.dolog_list) of
6142 + false -> lists:append(Settings#user_settings.dolog_list, [Peer]);
6143 + true -> Settings#user_settings.dolog_list
6145 + DNLL = lists:delete(jid:encode(JID), Settings#user_settings.donotlog_list),
6146 + Sett = Settings#user_settings{donotlog_list=DNLL, dolog_list=DLL},
6147 + % TODO: check returned value
6148 + ok = mod_logdb:set_user_settings(User, Server, Sett),
6160 From 5043114bc1a74caa522e8a1569b485ccc1808a79 Mon Sep 17 00:00:00 2001
6161 From: Oleh Palii <o.palij@gmail.com>
6162 Date: Sat, 31 Aug 2019 15:23:19 +0300
6163 Subject: [PATCH 2/3] mod_logdb 19.08 adaptation
6166 src/mod_logdb.erl | 187 +++++++++++++++++++++++----------------
6167 src/mod_logdb_mysql.erl | 10 +--
6168 src/mod_logdb_mysql5.erl | 10 +--
6169 src/mod_logdb_pgsql.erl | 12 +--
6170 4 files changed, 125 insertions(+), 94 deletions(-)
6172 diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
6173 index bf0240d139..0b5c2ec687 100644
6174 --- a/src/mod_logdb.erl
6175 +++ b/src/mod_logdb.erl
6177 -export([get_local_identity/5,
6178 get_local_features/5,
6181 adhoc_local_items/4,
6182 adhoc_local_commands/4
6185 user_messages_stats/4,
6186 user_messages_stats_at/5]).
6188 +-export([get_opt/3]).
6190 -include("mod_logdb.hrl").
6191 -include("xmpp.hrl").
6192 -include("mod_roster.hrl").
6194 -include("ejabberd_web_admin.hrl").
6195 -include("ejabberd_http.hrl").
6196 -include("logger.hrl").
6197 +-include("translate.hrl").
6199 -define(PROCNAME, ejabberd_mod_logdb).
6200 % gen_server call timeout
6203 ets_settings_table(VHost) -> list_to_atom("ets_logdb_settings_" ++ binary_to_list(VHost)).
6205 +-spec tr(binary(), binary()) -> binary().
6207 + translate:translate(Lang, Text).
6209 +mod_options(VHost) ->
6211 + {dbs, [{mnesia, []}]},
6212 + {vhosts, [{VHost, mnesia}]},
6213 + {ignore_jids, []},
6214 + {groupchat, none},
6215 + {drop_messages_on_user_removal, true},
6216 + {purge_older_days, never},
6217 + {dolog_default, true},
6218 + {poll_users_settings, 5}
6221 +get_opt(Opt, Opts, Default) ->
6222 + case lists:keyfind(Opt, 1, Opts) of
6224 + {_, Result} -> Result
6227 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
6229 % gen_mod/gen_server callbacks
6230 @@ -88,7 +114,8 @@ start(VHost, Opts) ->
6233 % add child to ejabberd_sup
6234 - supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec).
6235 + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec),
6238 depends(_Host, _Opts) ->
6240 @@ -106,14 +133,14 @@ start_link(VHost, Opts) ->
6242 init([VHost, Opts]) ->
6243 process_flag(trap_exit, true),
6244 - DBsRaw = gen_mod:get_opt(dbs, Opts, fun(A) -> A end, [{mnesia, []}]),
6245 + DBsRaw = gen_mod:get_opt(dbs, Opts),
6246 DBs = case lists:keysearch(mnesia, 1, DBsRaw) of
6247 false -> lists:append(DBsRaw, [{mnesia,[]}]);
6248 {value, _} -> DBsRaw
6250 - VHostDB = gen_mod:get_opt(vhosts, Opts, fun(A) -> A end, [{VHost, mnesia}]),
6251 + VHostDB = gen_mod:get_opt(vhosts, Opts),
6252 % 10 is default because of using in clustered environment
6253 - PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts, fun(A) -> A end, 10),
6254 + PollUsersSettings = gen_mod:get_opt(poll_users_settings, Opts),
6257 case lists:keysearch(VHost, 1, VHostDB) of
6258 @@ -139,11 +166,11 @@ init([VHost, Opts]) ->
6260 % dbs used for convert messages from one backend to other
6262 - dolog_default=gen_mod:get_opt(dolog_default, Opts, fun(A) -> A end, true),
6263 - drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts, fun(A) -> A end, true),
6264 - ignore_jids=gen_mod:get_opt(ignore_jids, Opts, fun(A) -> A end, []),
6265 - groupchat=gen_mod:get_opt(groupchat, Opts, fun(A) -> A end, none),
6266 - purge_older_days=gen_mod:get_opt(purge_older_days, Opts, fun(A) -> A end, never),
6267 + dolog_default=gen_mod:get_opt(dolog_default, Opts),
6268 + drop_messages_on_user_removal=gen_mod:get_opt(drop_messages_on_user_removal, Opts),
6269 + ignore_jids=gen_mod:get_opt(ignore_jids, Opts),
6270 + groupchat=gen_mod:get_opt(groupchat, Opts),
6271 + purge_older_days=gen_mod:get_opt(purge_older_days, Opts),
6272 poll_users_settings=PollUsersSettings}}.
6274 cleanup(#state{vhost=VHost} = _State) ->
6275 @@ -444,7 +471,7 @@ handle_info(scheduled_purging, #state{vhost=VHost, purge_older_days=Days} = Stat
6276 % from timer:send_interval/2 (in start handle_info)
6277 handle_info(poll_users_settings, #state{dbmod=DBMod, vhost=VHost}=State) ->
6278 {ok, DoLog} = DBMod:get_users_settings(VHost),
6279 - ?MYDEBUG("DoLog=~p", [DoLog]),
6280 +% ?MYDEBUG("DoLog=~p", [DoLog]),
6281 true = ets:delete_all_objects(ets_settings_table(VHost)),
6282 ets:insert(ets_settings_table(VHost), DoLog),
6284 @@ -654,8 +681,7 @@ sort_stats(Stats) ->
6285 % return float seconds elapsed from "zero hour" as list
6287 {MegaSec, Sec, MicroSec} = now(),
6288 - [List] = io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]),
6290 + io_lib:format("~.5f", [MegaSec*1000000 + Sec + MicroSec/1000000]).
6292 % convert float seconds elapsed from "zero hour" to local time "%Y-%m-%d %H:%M:%S" string
6293 convert_timestamp(Seconds) when is_list(Seconds) ->
6294 @@ -907,7 +933,7 @@ copy_messages_int_tc([FromDBMod, ToDBMod, VHost, Date]) ->
6295 % mysql, pgsql removes final zeros after decimal point
6296 (#msg{timestamp=Tst}) when length(Tst) < 16 ->
6297 {F, _} = string:to_float(Tst++".0"),
6298 - [T] = io_lib:format("~.5f", [F]),
6299 + T = io_lib:format("~.5f", [F]),
6300 ets:insert(mod_logdb_temp, {T})
6302 {ok, Msgs} = FromDBMod:get_user_messages_at(User, VHost, Date),
6303 @@ -992,16 +1018,25 @@ string_to_list(String) ->
6304 % ad-hoc (copy/pasted from mod_configure.erl)
6306 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
6307 +-spec get_permission_level(jid()) -> global | vhost.
6308 +get_permission_level(JID) ->
6309 + case acl:match_rule(global, configure, JID) of
6314 -define(ITEMS_RESULT(Allow, LNode, Fallback),
6318 - case get_local_items(LServer, LNode,
6319 - jid:encode(To), Lang) of
6320 - {result, Res} -> {result, Res};
6321 - {error, Error} -> {error, Error}
6327 + PermLev = get_permission_level(From),
6328 + case get_local_items({PermLev, LServer}, LNode,
6329 + jid:encode(To), Lang)
6331 + {result, Res} -> {result, Res};
6332 + {error, Error} -> {error, Error}
6336 get_local_items(Acc, From, #jid{lserver = LServer} = To,
6338 @@ -1051,15 +1086,13 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To,
6342 --define(T(Lang, Text), translate:translate(Lang, Text)).
6344 -define(NODE(Name, Node),
6345 - #disco_item{jid = jid:make(Server),
6347 - name = ?T(Lang, Name)}).
6348 + #disco_item{jid = jid:make(Server),
6350 + name = tr(Lang, Name)}).
6352 -define(NS_ADMINX(Sub),
6353 - <<(?NS_ADMIN)/binary, "#", Sub/binary>>).
6354 + <<(?NS_ADMIN)/binary, "#", Sub/binary>>).
6356 tokenize(Node) -> str:tokens(Node, <<"/#">>).
6358 @@ -1098,10 +1131,10 @@ get_local_items(_Host, Item, _Server, _Lang) ->
6359 {error, xmpp:err_item_not_found()}.
6361 -define(INFO_RESULT(Allow, Feats, Lang),
6363 - deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
6364 - allow -> {result, Feats}
6367 + deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
6368 + allow -> {result, Feats}
6371 get_local_features(Acc, From,
6372 #jid{lserver = LServer} = _To, Node, Lang) ->
6373 @@ -1133,11 +1166,11 @@ get_local_features(Acc, From,
6376 -define(INFO_IDENTITY(Category, Type, Name, Lang),
6377 - [#identity{category = Category, type = Type, name = ?T(Lang, Name)}]).
6378 + [#identity{category = Category, type = Type, name = tr(Lang, Name)}]).
6380 -define(INFO_COMMAND(Name, Lang),
6381 - ?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
6383 + ?INFO_IDENTITY(<<"automation">>, <<"command-node">>,
6386 get_local_identity(Acc, _From, _To, Node, Lang) ->
6387 LNode = tokenize(Node),
6388 @@ -1198,10 +1231,8 @@ recursively_get_local_items(LServer,
6390 -define(COMMANDS_RESULT(Allow, From, To, Request),
6393 - {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)};
6395 - adhoc_local_commands(From, To, Request)
6396 + deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
6397 + allow -> adhoc_local_commands(From, To, Request)
6400 adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To,
6401 @@ -1278,28 +1309,28 @@ get_user_form(LUser, LServer, Lang) ->
6404 type = 'list-single',
6405 - label = ?T(Lang, <<"Default">>),
6406 + label = tr(Lang, ?T("Default")),
6407 var = <<"dolog_default">>,
6408 values = [misc:atom_to_binary(DLD)],
6409 - options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
6410 + options = [#xdata_option{label = tr(Lang, ?T("Log Messages")),
6411 value = <<"true">>},
6412 - #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
6413 + #xdata_option{label = tr(Lang, ?T("Do Not Log Messages")),
6414 value = <<"false">>}]},
6416 type = 'text-multi',
6417 - label = ?T(Lang, <<"Log Messages">>),
6418 + label = tr(Lang, ?T("Log Messages")),
6419 var = <<"dolog_list">>,
6422 type = 'text-multi',
6423 - label = ?T(Lang, <<"Do Not Log Messages">>),
6424 + label = tr(Lang, ?T("Do Not Log Messages")),
6425 var = <<"donotlog_list">>,
6429 - title = ?T(Lang, <<"Messages logging engine settings">>),
6430 + title = tr(Lang, ?T("Messages logging engine settings")),
6432 - instructions = [<< (?T(Lang, <<"Set logging preferences">>))/binary,
6433 + instructions = [<< (tr(Lang, ?T("Set logging preferences")))/binary,
6434 (iolist_to_binary(": "))/binary,
6435 LUser/binary, "@", LServer/binary >>],
6436 fields = [?HFIELD()|
6437 @@ -1325,52 +1356,52 @@ get_settings_form(Host, Lang) ->
6440 type = 'list-single',
6441 - label = ?T(Lang, <<"Default">>),
6442 + label = tr(Lang, ?T("Default")),
6443 var = <<"dolog_default">>,
6444 values = [misc:atom_to_binary(DLD)],
6445 - options = [#xdata_option{label = ?T(Lang, <<"Log Messages">>),
6446 + options = [#xdata_option{label = tr(Lang, ?T("Log Messages")),
6447 value = <<"true">>},
6448 - #xdata_option{label = ?T(Lang, <<"Do Not Log Messages">>),
6449 + #xdata_option{label = tr(Lang, ?T("Do Not Log Messages")),
6450 value = <<"false">>}]},
6452 type = 'list-single',
6453 - label = ?T(Lang, <<"Drop messages on user removal">>),
6454 + label = tr(Lang, ?T("Drop messages on user removal")),
6455 var = <<"drop_messages_on_user_removal">>,
6456 values = [misc:atom_to_binary(MRemoval)],
6457 - options = [#xdata_option{label = ?T(Lang, <<"Drop">>),
6458 + options = [#xdata_option{label = tr(Lang, ?T("Drop")),
6459 value = <<"true">>},
6460 - #xdata_option{label = ?T(Lang, <<"Do not drop">>),
6461 + #xdata_option{label = tr(Lang, ?T("Do not drop")),
6462 value = <<"false">>}]},
6464 type = 'list-single',
6465 - label = ?T(Lang, <<"Groupchat messages logging">>),
6466 + label = tr(Lang, ?T("Groupchat messages logging")),
6467 var = <<"groupchat">>,
6468 values = [misc:atom_to_binary(GroupChat)],
6469 - options = [#xdata_option{label = ?T(Lang, <<"all">>),
6470 + options = [#xdata_option{label = tr(Lang, ?T("all")),
6472 - #xdata_option{label = ?T(Lang, <<"none">>),
6473 + #xdata_option{label = tr(Lang, ?T("none")),
6474 value = <<"none">>},
6475 - #xdata_option{label = ?T(Lang, <<"send">>),
6476 + #xdata_option{label = tr(Lang, ?T("send")),
6477 value = <<"send">>}]},
6479 type = 'text-multi',
6480 - label = ?T(Lang, <<"Jids/Domains to ignore">>),
6481 + label = tr(Lang, ?T("Jids/Domains to ignore")),
6482 var = <<"ignore_list">>,
6483 values = IgnoreJids},
6485 type = 'text-single',
6486 - label = ?T(Lang, <<"Purge messages older than (days)">>),
6487 + label = tr(Lang, ?T("Purge messages older than (days)")),
6488 var = <<"purge_older_days">>,
6489 values = [iolist_to_binary(PurgeDays)]},
6491 type = 'text-single',
6492 - label = ?T(Lang, <<"Poll users settings (seconds)">>),
6493 + label = tr(Lang, ?T("Poll users settings (seconds)")),
6494 var = <<"poll_users_settings">>,
6495 values = [integer_to_binary(PollTime)]}
6498 - title = ?T(Lang, <<"Messages logging engine settings (run-time)">>),
6499 - instructions = [?T(Lang, <<"Set run-time settings">>)],
6500 + title = tr(Lang, ?T("Messages logging engine settings (run-time)")),
6501 + instructions = [tr(Lang, ?T("Set run-time settings"))],
6503 fields = [?HFIELD()|
6505 @@ -1578,7 +1609,7 @@ get_all_vh_users(Host, Server) ->
6507 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
6508 webadmin_menu(Acc, _Host, Lang) ->
6509 - [{<<"messages">>, ?T(<<"Users Messages">>)} | Acc].
6510 + [{<<"messages">>, tr(Lang, ?T("Users Messages"))} | Acc].
6512 webadmin_user(Acc, User, Server, Lang) ->
6513 Sett = get_user_settings(User, Server),
6514 @@ -1649,12 +1680,12 @@ vhost_messages_stats(Server, Query, Lang) ->
6516 {'EXIT', CReason} ->
6517 ?ERROR_MSG("Failed to get_vhost_stats: ~p", [CReason]),
6518 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
6519 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching list")))];
6521 ?ERROR_MSG("Failed to get_vhost_stats: ~p", [GReason]),
6522 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
6523 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching list")))];
6525 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Server])))];
6526 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("No logged messages for ~s")), [Server])))];
6528 Fun = fun({Date, Count}) ->
6529 DateBin = iolist_to_binary(Date),
6530 @@ -1667,7 +1698,7 @@ vhost_messages_stats(Server, Query, Lang) ->
6534 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s">>), [Server])))] ++
6535 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("Logged messages for ~s")), [Server])))] ++
6537 ok -> [?CT(<<"Submitted">>), ?P];
6538 error -> [?CT(<<"Bad format">>), ?P];
6539 @@ -1696,12 +1727,12 @@ vhost_messages_stats_at(Server, Query, Lang, Date) ->
6541 {'EXIT', CReason} ->
6542 ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [CReason]),
6543 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
6544 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching list")))];
6546 ?ERROR_MSG("Failed to get_vhost_stats_at: ~p", [GReason]),
6547 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching list">>))];
6548 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching list")))];
6550 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Server, Date])))];
6551 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("No logged messages for ~s at ~s")), [Server, Date])))];
6553 Res = case catch vhost_messages_at_parse_query(Server, Date, Stats, Query) of
6555 @@ -1719,7 +1750,7 @@ vhost_messages_stats_at(Server, Query, Lang, Date) ->
6556 ?XC(<<"td">>, integer_to_binary(Count))
6559 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Server, Date])))] ++
6560 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("Logged messages for ~s at ~s")), [Server, Date])))] ++
6562 ok -> [?CT(<<"Submitted">>), ?P];
6563 error -> [?CT(<<"Bad format">>), ?P];
6564 @@ -1757,12 +1788,12 @@ user_messages_stats(User, Server, Query, Lang) ->
6566 {'EXIT', CReason} ->
6567 ?ERROR_MSG("Failed to get_user_stats: ~p", [CReason]),
6568 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
6569 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching days")))];
6571 ?ERROR_MSG("Failed to get_user_stats: ~p", [GReason]),
6572 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching days">>))];
6573 + [?XC(<<"h1">>, tr(Lang,?T("Error occupied while fetching days")))];
6575 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s">>), [Jid])))];
6576 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("No logged messages for ~s")), [Jid])))];
6578 Fun = fun({Date, Count}) ->
6579 DateBin = iolist_to_binary(Date),
6580 @@ -1814,12 +1845,12 @@ user_messages_stats_at(User, Server, Query, Lang, Date) ->
6582 {'EXIT', CReason} ->
6583 ?ERROR_MSG("Failed to get_user_messages_at: ~p", [CReason]),
6584 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
6585 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching messages")))];
6587 ?ERROR_MSG("Failed to get_user_messages_at: ~p", [GReason]),
6588 - [?XC(<<"h1">>, ?T(<<"Error occupied while fetching messages">>))];
6589 + [?XC(<<"h1">>, tr(Lang, ?T("Error occupied while fetching messages")))];
6591 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"No logged messages for ~s at ~s">>), [Jid, Date])))];
6592 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("No logged messages for ~s at ~s")), [Jid, Date])))];
6593 {ok, User_messages} ->
6594 Res = case catch user_messages_at_parse_query(Server,
6596 @@ -1888,7 +1919,7 @@ user_messages_stats_at(User, Server, Query, Lang, Date) ->
6598 Text = case Subject of
6599 "" -> iolist_to_binary(Body);
6600 - _ -> iolist_to_binary([binary_to_list(?T(<<"Subject">>)) ++ ": " ++ Subject ++ "\n" ++ Body])
6601 + _ -> iolist_to_binary([binary_to_list(tr(Lang, ?T("Subject"))) ++ ": " ++ Subject ++ "\n" ++ Body])
6603 Resource = case PRes of
6605 @@ -1915,7 +1946,7 @@ user_messages_stats_at(User, Server, Query, Lang, Date) ->
6606 % Filtered user messages in html
6607 Msgs = lists:map(Msgs_Fun, lists:sort(User_messages_filtered)),
6609 - [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Logged messages for ~s at ~s">>), [Jid, Date])))] ++
6610 + [?XC(<<"h1">>, list_to_binary(io_lib:format(tr(Lang, ?T("Logged messages for ~s at ~s")), [Jid, Date])))] ++
6612 ok -> [?CT(<<"Submitted">>), ?P];
6613 error -> [?CT(<<"Bad format">>), ?P];
6614 diff --git a/src/mod_logdb_mysql.erl b/src/mod_logdb_mysql.erl
6615 index 21d65e6578..66b50acc86 100644
6616 --- a/src/mod_logdb_mysql.erl
6617 +++ b/src/mod_logdb_mysql.erl
6618 @@ -94,11 +94,11 @@ stop(VHost) ->
6619 init([VHost, Opts]) ->
6622 - Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
6623 - Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
6624 - DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
6625 - User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
6626 - Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
6627 + Server = mod_logdb:get_opt(server, Opts, <<"localhost">>),
6628 + Port = mod_logdb:get_opt(port, Opts, 3306),
6629 + DB = mod_logdb:get_opt(db, Opts, <<"logdb">>),
6630 + User = mod_logdb:get_opt(user, Opts, <<"root">>),
6631 + Password = mod_logdb:get_opt(password, Opts, <<"">>),
6633 St = #state{vhost=VHost,
6634 server=Server, port=Port, db=DB,
6635 diff --git a/src/mod_logdb_mysql5.erl b/src/mod_logdb_mysql5.erl
6636 index c05ab958e2..72fa72e72e 100644
6637 --- a/src/mod_logdb_mysql5.erl
6638 +++ b/src/mod_logdb_mysql5.erl
6639 @@ -99,11 +99,11 @@ stop(VHost) ->
6640 init([VHost, Opts]) ->
6643 - Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
6644 - Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 3306),
6645 - DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"logdb">>),
6646 - User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
6647 - Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
6648 + Server = mod_logdb:get_opt(server, Opts, <<"localhost">>),
6649 + Port = mod_logdb:get_opt(port, Opts, 3306),
6650 + DB = mod_logdb:get_opt(db, Opts, <<"logdb">>),
6651 + User = mod_logdb:get_opt(user, Opts, <<"root">>),
6652 + Password = mod_logdb:get_opt(password, Opts, <<"">>),
6654 St = #state{vhost=VHost,
6655 server=Server, port=Port, db=DB,
6656 diff --git a/src/mod_logdb_pgsql.erl b/src/mod_logdb_pgsql.erl
6657 index 202c6ed4a8..7f74887b9d 100644
6658 --- a/src/mod_logdb_pgsql.erl
6659 +++ b/src/mod_logdb_pgsql.erl
6660 @@ -101,12 +101,12 @@ stop(VHost) ->
6662 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
6663 init([VHost, Opts]) ->
6664 - Server = gen_mod:get_opt(server, Opts, fun(A) -> A end, <<"localhost">>),
6665 - DB = gen_mod:get_opt(db, Opts, fun(A) -> A end, <<"ejabberd_logdb">>),
6666 - User = gen_mod:get_opt(user, Opts, fun(A) -> A end, <<"root">>),
6667 - Port = gen_mod:get_opt(port, Opts, fun(A) -> A end, 5432),
6668 - Password = gen_mod:get_opt(password, Opts, fun(A) -> A end, <<"">>),
6669 - Schema = binary_to_list(gen_mod:get_opt(schema, Opts, fun(A) -> A end, <<"public">>)),
6670 + Server = mod_logdb:get_opt(server, Opts, <<"localhost">>),
6671 + Port = mod_logdb:get_opt(port, Opts, 5432),
6672 + DB = mod_logdb:get_opt(db, Opts, <<"logdb">>),
6673 + User = mod_logdb:get_opt(user, Opts, <<"root">>),
6674 + Password = mod_logdb:get_opt(password, Opts, <<"">>),
6675 + Schema = mod_logdb:get_opt(schema, Opts, <<"public">>),
6677 ?MYDEBUG("Starting pgsql backend for ~s", [VHost]),
6680 From 55274ef5a3deb5979e0d97cdb48768eb472c36ec Mon Sep 17 00:00:00 2001
6681 From: Oleh Palii <o.palij@gmail.com>
6682 Date: Sat, 31 Aug 2019 22:43:11 +0300
6683 Subject: [PATCH 3/3] mod_logdb mod_opt_type fixes
6686 src/mod_logdb.erl | 33 ++++++++++++++++++++++-----------
6687 1 file changed, 22 insertions(+), 11 deletions(-)
6689 diff --git a/src/mod_logdb.erl b/src/mod_logdb.erl
6690 index 0b5c2ec687..0766241fec 100644
6691 --- a/src/mod_logdb.erl
6692 +++ b/src/mod_logdb.erl
6693 @@ -220,24 +220,35 @@ get_commands_spec() ->
6694 result = {res, rescode}}].
6696 mod_opt_type(dbs) ->
6697 - fun (A) when is_list(A) -> A end;
6699 + econf:enum([mnesia, mysql, mysql5, pgsql]),
6701 + econf:enum([user, password, server, port, db, schema]),
6705 mod_opt_type(vhosts) ->
6706 - fun (A) when is_list(A) -> A end;
6709 + econf:enum([mnesia, mysql, mysql5, pgsql])
6711 mod_opt_type(poll_users_settings) ->
6712 - fun (I) when is_integer(I) -> I end;
6713 + econf:non_neg_int();
6714 mod_opt_type(groupchat) ->
6719 + econf:enum([all, send, none]);
6720 mod_opt_type(dolog_default) ->
6721 - fun (B) when is_boolean(B) -> B end;
6723 +mod_opt_type(drop_messages_on_user_removal) ->
6725 mod_opt_type(ignore_jids) ->
6726 - fun (A) when is_list(A) -> A end;
6727 + econf:list(econf:string());
6728 mod_opt_type(purge_older_days) ->
6729 - fun (I) when is_integer(I) -> I end;
6732 + econf:non_neg_int()
6735 - [dbs, vhosts, poll_users_settings, groupchat, dolog_default, ignore_jids, purge_older_days].
6736 + [dbs, vhosts, poll_users_settings, groupchat, dolog_default, drop_messages_on_user_removal, ignore_jids, purge_older_days].
6738 handle_call({cleanup}, _From, State) ->