diff --git a/ChangeLog b/ChangeLog index fe4695784..490436747 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,30 @@ +2004-09-10 Alexey Shchepin + + * tools/ejabberdctl: Added call to "exec" (thanks to Sergei + Golovan) + + * src/msgs/ru.msg: Updated (thanks to Sergei Golovan) + + * src/mod_vcard.erl: Support for searching of prefix substring and + limiting of result items (thanks to Sergei Golovan) + + * src/mod_offline.erl: Support for message expiration (JEP-0023) + (thanks to Sergei Golovan) + * src/jlib.hrl: Added NS_EXPIRE macros (thanks to Sergei Golovan) + + * src/ejabberd_logger_h.erl: Added reopen_log/0 (thanks to Sergei + Golovan) + + * src/ejabberd_ctl.erl: Added return codes, updated "reopen-log" + command, added "delete-expired-messages" and "status" commands + (thanks to Sergei Golovan) + + * doc/guide.tex: Updated (thanks to Sergei Golovan) + +2004-09-04 Alexey Shchepin + + * src/mod_roster.erl: Removed useless transactions + 2004-08-28 Alexey Shchepin * doc/guide.tex: Fix (thanks to Sander Devrieze) diff --git a/doc/guide.tex b/doc/guide.tex index d8c758572..f656d9323 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -1131,6 +1131,8 @@ Options: \titem{search} Specifies wheather search is enabled (value is \term{true}, default) or disabled (value is \term{false}) by the service. If \term{search} is set to \term{false}, option \term{host} is ignored and service does not appear in Jabber Discovery items. +\titem{matches} Limits the number of reported search results. If value is set to +\term{infinity} then all search results are reported. Default value is \term{30}. \end{description} Example: @@ -1138,7 +1140,7 @@ Example: {modules, [ ... - {mod_vcard, [{search, false}]} + {mod_vcard, [{search, false}, {matches, 20}]} ... ]}. \end{verbatim} diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 36be47a04..1c1d33877 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -11,124 +11,166 @@ -export([start/0]). +-define(STATUS_SUCCESS, 0). +-define(STATUS_ERROR, 1). +-define(STATUS_USAGE, 2). +-define(STATUS_BADRPC, 3). + start() -> case init:get_plain_arguments() of [SNode | Args] -> Node = list_to_atom(SNode), - process(Node, Args); + Status = process(Node, Args), + halt(Status); _ -> - print_usage() - end, - halt(). + print_usage(), + halt(?STATUS_USAGE) + end. +process(Node, ["status"]) -> + case rpc:call(Node, init, get_status, []) of + {badrpc, Reason} -> + io:format("Can't get node ~p status: ~p~n", + [Node, Reason]), + ?STATUS_BADRPC; + {InternalStatus, ProvidedStatus} -> + io:format("Node ~p is ~p. Status: ~p~n", + [Node, InternalStatus, ProvidedStatus]), + ?STATUS_SUCCESS + end; + process(Node, ["stop"]) -> case rpc:call(Node, init, stop, []) of {badrpc, Reason} -> io:format("Can't stop node ~p: ~p~n", - [Node, Reason]); + [Node, Reason]), + ?STATUS_BADRPC; _ -> - ok + ?STATUS_SUCCESS end; process(Node, ["restart"]) -> case rpc:call(Node, init, restart, []) of {badrpc, Reason} -> io:format("Can't restart node ~p: ~p~n", - [Node, Reason]); + [Node, Reason]), + ?STATUS_BADRPC; _ -> - ok + ?STATUS_SUCCESS end; process(Node, ["reopen-log"]) -> - {error_logger, Node} ! {emulator, noproc, reopen}; + case rpc:call(Node, ejabberd_logger_h, reopen_log, []) of + {badrpc, Reason} -> + io:format("Can't reopen node ~p log: ~p~n", + [Node, Reason]), + ?STATUS_BADRPC; + _ -> + ?STATUS_SUCCESS + end; process(Node, ["register", User, Password]) -> case rpc:call(Node, ejabberd_auth, try_register, [User, Password]) of {atomic, ok} -> - ok; + ?STATUS_SUCCESS; {atomic, exists} -> io:format("User ~p already registered on node ~p~n", - [User, Node]); + [User, Node]), + ?STATUS_ERROR; {error, Reason} -> io:format("Can't register user ~p on node ~p: ~p~n", - [User, Node, Reason]); + [User, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't register user ~p on node ~p: ~p~n", - [User, Node, Reason]) + [User, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["unregister", User]) -> case rpc:call(Node, ejabberd_auth, remove_user, [User]) of {atomic, ok} -> - ok; + ?STATUS_SUCCESS; {error, Reason} -> io:format("Can't unregister user ~p on node ~p: ~p~n", - [User, Node, Reason]); + [User, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't unregister user ~p on node ~p: ~p~n", - [User, Node, Reason]) + [User, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["backup", Path]) -> case rpc:call(Node, mnesia, backup, [Path]) of ok -> - ok; + ?STATUS_SUCCESS; {error, Reason} -> io:format("Can't store backup in ~p on node ~p: ~p~n", - [Path, Node, Reason]); + [Path, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't store backup in ~p on node ~p: ~p~n", - [Path, Node, Reason]) + [Path, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["dump", Path]) -> case rpc:call(Node, mnesia, dump_to_textfile, [Path]) of ok -> - ok; + ?STATUS_SUCCESS; {error, Reason} -> io:format("Can't store dump in ~p on node ~p: ~p~n", - [Path, Node, Reason]); + [Path, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't store dump in ~p on node ~p: ~p~n", - [Path, Node, Reason]) + [Path, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["load", Path]) -> case rpc:call(Node, mnesia, load_textfile, [Path]) of ok -> - ok; + ?STATUS_SUCCESS; {error, Reason} -> io:format("Can't load dump in ~p on node ~p: ~p~n", - [Path, Node, Reason]); + [Path, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't load dump in ~p on node ~p: ~p~n", - [Path, Node, Reason]) + [Path, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["restore", Path]) -> case rpc:call(Node, mnesia, restore, [Path, [{default_op, keep_tables}]]) of {atomic, ok} -> - ok; + ?STATUS_SUCCESS; {error, Reason} -> io:format("Can't restore backup from ~p on node ~p: ~p~n", - [Path, Node, Reason]); + [Path, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't restore backup from ~p on node ~p: ~p~n", - [Path, Node, Reason]) + [Path, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["install-fallback", Path]) -> case rpc:call(Node, mnesia, install_fallback, [Path]) of {atomic, ok} -> - ok; + ?STATUS_SUCCESS; {error, Reason} -> io:format("Can't install fallback from ~p on node ~p: ~p~n", - [Path, Node, Reason]); + [Path, Node, Reason]), + ?STATUS_ERROR; {badrpc, Reason} -> io:format("Can't install fallback from ~p on node ~p: ~p~n", - [Path, Node, Reason]) + [Path, Node, Reason]), + ?STATUS_BADRPC end; process(Node, ["registered-users"]) -> @@ -138,14 +180,30 @@ process(Node, ["registered-users"]) -> SUsers = lists:sort(Users), FUsers = lists:map(fun(U) -> [U, NewLine] end, SUsers), io:format("~s", [FUsers]), - ok; - {ErrorTag, Reason} when (ErrorTag == error) or (ErrorTag == badrpc) -> + ?STATUS_SUCCESS; + {error, Reason} -> io:format("Can't get list of registered users on node ~p: ~p~n", - [Node, Reason]) + [Node, Reason]), + ?STATUS_ERROR; + {badrpc, Reason} -> + io:format("Can't get list of registered users on node ~p: ~p~n", + [Node, Reason]), + ?STATUS_BADRPC + end; + +process(Node, ["delete-expired-messages"]) -> + case rpc:call(Node, mod_offline, remove_expired_messages, []) of + {badrpc, Reason} -> + io:format("Can't delete expired messages at node ~p: ~p~n", + [Node, Reason]), + ?STATUS_BADRPC; + _ -> + ?STATUS_SUCCESS end; process(_Node, _Args) -> - print_usage(). + print_usage(), + ?STATUS_USAGE. @@ -154,6 +212,7 @@ print_usage() -> "Usage: ejabberdctl node command~n" "~n" "Available commands:~n" + " status\t\t\tget ejabberd status~n" " stop\t\t\t\tstop ejabberd~n" " restart\t\t\trestart ejabberd~n" " reopen-log\t\t\treopen log file~n" @@ -165,6 +224,7 @@ print_usage() -> " dump file\t\t\tdump a database in a text file~n" " load file\t\t\trestore a database from a text file~n" " registered-users\t\tlist all registered users~n" + " delete-expired-messages\tdelete expired offline messages from database~n" "~n" "Example:~n" " ejabberdctl ejabberd@host restart~n" diff --git a/src/ejabberd_logger_h.erl b/src/ejabberd_logger_h.erl index 8c18f7220..dff4553a2 100644 --- a/src/ejabberd_logger_h.erl +++ b/src/ejabberd_logger_h.erl @@ -15,7 +15,7 @@ %% gen_event callbacks -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, - code_change/3]). + code_change/3, reopen_log/0]). -record(state, {fd, file}). @@ -89,6 +89,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +reopen_log() -> + error_logger ! {emulator, noproc, reopen}. + %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- diff --git a/src/jlib.hrl b/src/jlib.hrl index c684a7f3b..1e0b31992 100644 --- a/src/jlib.hrl +++ b/src/jlib.hrl @@ -21,6 +21,7 @@ -define(NS_XDATA, "jabber:x:data"). -define(NS_IQDATA, "jabber:iq:data"). -define(NS_DELAY, "jabber:x:delay"). +-define(NS_EXPIRE, "jabber:x:expire"). -define(NS_EVENT, "jabber:x:event"). -define(NS_XCONFERENCE, "jabber:x:conference"). -define(NS_STATS, "http://jabber.org/protocol/stats"). diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 89ee0765c..6959dbcef 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -17,12 +17,14 @@ store_packet/3, resend_offline_messages/1, pop_offline_messages/2, + remove_expired_messages/0, remove_old_messages/1, remove_user/1]). +-include("ejabberd.hrl"). -include("jlib.hrl"). --record(offline_msg, {user, timestamp, from, to, packet}). +-record(offline_msg, {user, timestamp, expire, from, to, packet}). -define(PROCNAME, ejabberd_offline). -define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). @@ -32,6 +34,7 @@ start(_) -> [{disc_only_copies, [node()]}, {type, bag}, {attributes, record_info(fields, offline_msg)}]), + update_table(), ejabberd_hooks:add(offline_message_hook, ?MODULE, store_packet, 50), ejabberd_hooks:add(offline_subscription_hook, @@ -92,8 +95,11 @@ store_packet(From, To, Packet) -> true -> #jid{luser = LUser} = To, TimeStamp = now(), + {xmlelement, _Name, _Attrs, Els} = Packet, + Expire = find_x_expire(TimeStamp, Els), ?PROCNAME ! #offline_msg{user = LUser, timestamp = TimeStamp, + expire = Expire, from = From, to = To, packet = Packet}, @@ -150,6 +156,34 @@ find_x_event([El | Els]) -> find_x_event(Els) end. +find_x_expire(_, []) -> + never; +find_x_expire(TimeStamp, [{xmlcdata, _} | Els]) -> + find_x_expire(TimeStamp, Els); +find_x_expire(TimeStamp, [El | Els]) -> + case xml:get_tag_attr_s("xmlns", El) of + ?NS_EXPIRE -> + case xml:get_tag_attr_s("seconds", El) of + Val -> + case catch list_to_integer(Val) of + {'EXIT', _} -> + never; + Int when Int > 0 -> + {MegaSecs, Secs, MicroSecs} = TimeStamp, + S = MegaSecs * 1000000 + Secs + Int, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + {MegaSecs1, Secs1, MicroSecs}; + _ -> + never + end; + _ -> + never + end; + _ -> + find_x_expire(TimeStamp, Els) + end. + resend_offline_messages(User) -> LUser = jlib:nodeprep(User), @@ -187,23 +221,54 @@ pop_offline_messages(Ls, User) -> end, case mnesia:transaction(F) of {atomic, Rs} -> - lists:map( - fun(R) -> - {xmlelement, Name, Attrs, Els} = R#offline_msg.packet, - {route, - R#offline_msg.from, - R#offline_msg.to, - {xmlelement, Name, Attrs, - Els ++ - [jlib:timestamp_to_xml( - calendar:now_to_universal_time( - R#offline_msg.timestamp))]}} - end, - Ls ++ lists:keysort(#offline_msg.timestamp, Rs)); + TS = now(), + Ls ++ lists:map( + fun(R) -> + {xmlelement, Name, Attrs, Els} = R#offline_msg.packet, + {route, + R#offline_msg.from, + R#offline_msg.to, + {xmlelement, Name, Attrs, + Els ++ + [jlib:timestamp_to_xml( + calendar:now_to_universal_time( + R#offline_msg.timestamp))]}} + end, + lists:filter( + fun(R) -> + case R#offline_msg.expire of + never -> + true; + TimeStamp -> + TS < TimeStamp + end + end, + lists:keysort(#offline_msg.timestamp, Rs))); _ -> Ls end. +remove_expired_messages() -> + TimeStamp = now(), + F = fun() -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl( + fun(Rec, _Acc) -> + case Rec#offline_msg.expire of + never -> + ok; + TS -> + if + TS < TimeStamp -> + mnesia:delete_object(Rec); + true -> + ok + end + end + end, ok, offline_msg) + end, + mnesia:transaction(F). + remove_old_messages(Days) -> {MegaSecs, Secs, _MicroSecs} = now(), S = MegaSecs * 1000000 + Secs - 60 * 60 * 24 * Days, @@ -227,3 +292,29 @@ remove_user(User) -> mnesia:delete({offline_msg, LUser}) end, mnesia:transaction(F). + +update_table() -> + Fields = record_info(fields, offline_msg), + case mnesia:table_info(offline_msg, attributes) of + Fields -> + ok; + [user, timestamp, from, to, packet] -> + ?INFO_MSG("Converting offline_msg table from " + "{user, timestamp, from, to, packet} format", []), + mnesia:transform_table( + offline_msg, + fun({_, U, TS, F, T, P}) -> + {xmlelement, _Name, _Attrs, Els} = P, + Expire = find_x_expire(TS, Els), + #offline_msg{user = U, + timestamp = TS, + expire = Expire, + from = F, + to = T, + packet = P} + end, Fields); + _ -> + ?INFO_MSG("Recreating offline_msg table", []), + mnesia:transform_table(last_activity, ignore, Fields) + end. + diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 384bf13f3..b356b412e 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -1,7 +1,7 @@ %%%---------------------------------------------------------------------- %%% File : mod_roster.erl %%% Author : Alexey Shchepin -%%% Purpose : +%%% Purpose : Roster management %%% Created : 11 Dec 2002 by Alexey Shchepin %%% Id : $Id$ %%%---------------------------------------------------------------------- @@ -90,11 +90,8 @@ process_local_iq(From, To, #iq{type = Type} = IQ) -> process_iq_get(From, _To, #iq{sub_el = SubEl} = IQ) -> #jid{luser = LUser} = From, - F = fun() -> - mnesia:index_read(roster, LUser, #roster.user) - end, - case mnesia:transaction(F) of - {atomic, Items} -> + case catch mnesia:dirty_index_read(roster, LUser, #roster.user) of + Items when is_list(Items) -> XItems = lists:map(fun item_to_xml/1, Items), IQ#iq{type = result, sub_el = [{xmlelement, "query", @@ -316,11 +313,8 @@ push_item(User, Resource, From, Item) -> get_subscription_lists(User) -> LUser = jlib:nodeprep(User), - F = fun() -> - mnesia:index_read(roster, LUser, #roster.user) - end, - case mnesia:transaction(F) of - {atomic, Items} -> + case mnesia:dirty_index_read(roster, LUser, #roster.user) of + Items when is_list(Items) -> fill_subscription_lists(Items, [], []); _ -> {[], []} diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index e2257772a..854df2c0a 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -22,7 +22,8 @@ -include("jlib.hrl"). -%-define(JUD_ALLOW_RETURN_ALL, true) +%-define(JUD_ALLOW_RETURN_ALL, true). +-define(JUD_MATCHES, 30). -record(vcard_search, {user, luser, fn, lfn, @@ -234,8 +235,10 @@ set_vcard(User, VCARD) -> [{xmlcdata, translate:translate(Lang, "Search users in ") ++ jlib:jid_to_string(JID)}]}, {xmlelement, "instructions", [], - [{xmlcdata, translate:translate(Lang, "Fill in fields to search " - "for any matching Jabber User")}]}, + [{xmlcdata, translate:translate(Lang, "Fill in the form to search " + "for any matching Jabber User " + "(Add * to the end of field to " + "match substring)")}]}, ?TLFIELD("text-single", "User", "user"), ?TLFIELD("text-single", "Full Name", "fn"), ?TLFIELD("text-single", "Name", "given"), @@ -452,7 +455,17 @@ search(Data) -> ?ERROR_MSG("~p", [Reason]), []; Rs -> - Rs + case gen_mod:get_module_opt(?MODULE, matches, 30) of + infinity -> + Rs; + Val when is_integer(Val) and Val > 0 -> + lists:sublist(Rs, Val); + Val -> + ?ERROR_MSG("Illegal option value ~p. " + "Default value ~p substituted.", + [{matches, Val}, ?JUD_MATCHES]), + lists:sublist(Rs, ?JUD_MATCHES) + end end. -else. @@ -469,7 +482,17 @@ search(Data) -> ?ERROR_MSG("~p", [Reason]), []; Rs -> - Rs + case gen_mod:get_module_opt(?MODULE, matches, ?JUD_MATCHES) of + infinity -> + Rs; + Val when is_integer(Val) and (Val > 0) -> + lists:sublist(Rs, Val); + Val -> + ?ERROR_MSG("Illegal option value ~p. " + "Default value ~p substituted.", + [{matches, Val}, ?JUD_MATCHES]), + lists:sublist(Rs, ?JUD_MATCHES) + end end end. @@ -499,24 +522,31 @@ filter_fields([{SVar, [Val]} | Ds], Match) when is_list(Val) and (Val /= "") -> LVal = stringprep:tolower(Val), NewMatch = case SVar of - "user" -> Match#vcard_search{luser = LVal}; - "fn" -> Match#vcard_search{lfn = LVal}; - "family" -> Match#vcard_search{lfamily = LVal}; - "given" -> Match#vcard_search{lgiven = LVal}; - "middle" -> Match#vcard_search{lmiddle = LVal}; - "nickname" -> Match#vcard_search{lnickname = LVal}; - "bday" -> Match#vcard_search{lbday = LVal}; - "ctry" -> Match#vcard_search{lctry = LVal}; - "locality" -> Match#vcard_search{llocality = LVal}; - "email" -> Match#vcard_search{lemail = LVal}; - "orgname" -> Match#vcard_search{lorgname = LVal}; - "orgunit" -> Match#vcard_search{lorgunit = LVal}; + "user" -> Match#vcard_search{luser = make_val(LVal)}; + "fn" -> Match#vcard_search{lfn = make_val(LVal)}; + "family" -> Match#vcard_search{lfamily = make_val(LVal)}; + "given" -> Match#vcard_search{lgiven = make_val(LVal)}; + "middle" -> Match#vcard_search{lmiddle = make_val(LVal)}; + "nickname" -> Match#vcard_search{lnickname = make_val(LVal)}; + "bday" -> Match#vcard_search{lbday = make_val(LVal)}; + "ctry" -> Match#vcard_search{lctry = make_val(LVal)}; + "locality" -> Match#vcard_search{llocality = make_val(LVal)}; + "email" -> Match#vcard_search{lemail = make_val(LVal)}; + "orgname" -> Match#vcard_search{lorgname = make_val(LVal)}; + "orgunit" -> Match#vcard_search{lorgunit = make_val(LVal)}; _ -> Match end, filter_fields(Ds, NewMatch); filter_fields([_ | Ds], Match) -> filter_fields(Ds, Match). +make_val(Val) -> + case lists:suffix("*", Val) of + true -> + lists:sublist(Val, length(Val) - 1) ++ '_'; + _ -> + Val + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/msgs/ru.msg b/src/msgs/ru.msg index ede822d2c..551048060 100644 --- a/src/msgs/ru.msg +++ b/src/msgs/ru.msg @@ -74,8 +74,8 @@ {"You need an x:data capable client to search", "Чтобы воспользоваться поиском, требуется x:data-совместимый клиент"}. {"Search users in ", "Поиск пользователей в "}. -{"Fill in fields to search for any matching Jabber User", - "Заполните поля для поиска пользователя Jabber"}. +{"Fill in the form to search for any matching Jabber User (Add * to the end of field to match substring)", +"Заполните форму для поиска пользователя Jabber (Если добавить * в конец поля, то происходит поиск подстроки)"}. {"Results of search in ", "Результаты поиска в "}. {"User", "Пользователь"}. diff --git a/tools/ejabberdctl b/tools/ejabberdctl index 4fa0edf8f..48294cae9 100755 --- a/tools/ejabberdctl +++ b/tools/ejabberdctl @@ -1,4 +1,4 @@ #!/bin/sh -erl -noinput -sname ejabberdctl -s ejabberd_ctl -extra $@ +exec erl -noinput -sname ejabberdctl -s ejabberd_ctl -extra $@