25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-28 16:34:13 +01:00

* src/mod_muc/mod_muc_room.erl: Support for history management

(thanks to Sergei Golovan)

* src/mod_stats.erl: Updated error codes (thanks to Sergei
Golovan)
* src/mod_irc/mod_irc.erl: Likewise

* src/mod_configure.erl: "jabber:iq:data" replaced with
"ejabber:config" namespace (thanks to Sergei Golovan)
* src/mod_disco.erl: Likewise

SVN Revision: 204
This commit is contained in:
Alexey Shchepin 2004-02-15 20:10:40 +00:00
parent 35bfd03669
commit fdf25720e0
6 changed files with 639 additions and 325 deletions

View File

@ -1,3 +1,16 @@
2004-02-15 Alexey Shchepin <alexey@sevcom.net>
* src/mod_muc/mod_muc_room.erl: Support for history management
(thanks to Sergei Golovan)
* src/mod_stats.erl: Updated error codes (thanks to Sergei
Golovan)
* src/mod_irc/mod_irc.erl: Likewise
* src/mod_configure.erl: "jabber:iq:data" replaced with
"ejabber:config" namespace (thanks to Sergei Golovan)
* src/mod_disco.erl: Likewise
2004-02-12 Alexey Shchepin <alexey@sevcom.net>
* src/ejabberd_c2s.erl: Added <session/> to stream features

View File

@ -23,15 +23,15 @@
start(Opts) ->
IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
gen_iq_handler:add_iq_handler(ejabberd_local, ?NS_IQDATA,
gen_iq_handler:add_iq_handler(ejabberd_local, ?NS_EJABBERD_CONFIG,
?MODULE, process_local_iq, IQDisc),
gen_iq_handler:add_iq_handler(ejabberd_sm, ?NS_IQDATA,
gen_iq_handler:add_iq_handler(ejabberd_sm, ?NS_EJABBERD_CONFIG,
?MODULE, process_sm_iq, IQDisc),
ok.
stop() ->
gen_iq_handler:remove_iq_handler(ejabberd_local, ?NS_IQDATA),
gen_iq_handler:remove_iq_handler(ejabberd_sm, ?NS_IQDATA).
gen_iq_handler:remove_iq_handler(ejabberd_local, ?NS_EJABBERD_CONFIG),
gen_iq_handler:remove_iq_handler(ejabberd_sm, ?NS_EJABBERD_CONFIG).
process_local_iq(From, _To, #iq{id = ID, type = Type,
@ -43,13 +43,18 @@ process_local_iq(From, _To, #iq{id = ID, type = Type,
Lang = xml:get_tag_attr_s("xml:lang", SubEl),
case Type of
set ->
case xml:get_tag_attr_s("type", SubEl) of
XDataEl = find_xdata_el(SubEl),
case XDataEl of
false ->
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ACCEPTABLE]};
{xmlelement, _Name, Attrs, SubEls} ->
case xml:get_attr_s("type", Attrs) of
"cancel" ->
IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}], []}]};
"submit" ->
XData = jlib:parse_xdata_submit(SubEl),
XData = jlib:parse_xdata_submit(XDataEl),
case XData of
invalid ->
IQ#iq{type = error,
@ -74,7 +79,8 @@ process_local_iq(From, _To, #iq{id = ID, type = Type,
end;
_ ->
IQ#iq{type = error,
sub_el = [SubEl, ?ERR_NOT_ALLOWED]}
sub_el = [SubEl, ?ERR_BAD_REQUEST]}
end
end;
get ->
Node =
@ -137,7 +143,8 @@ get_form(["running nodes", ENode, "DB"], Lang) ->
{error, ?ERR_INTERNAL_SERVER_ERROR};
Tables ->
STables = lists:sort(Tables),
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "DB Tables Configuration")}]},
@ -157,7 +164,7 @@ get_form(["running nodes", ENode, "DB"], Lang) ->
?TABLEFIELD(Table, Type)
end
end, STables)
]}
]}]}
end
end;
@ -171,7 +178,8 @@ get_form(["running nodes", ENode, "modules", "stop"], Lang) ->
{error, ?ERR_INTERNAL_SERVER_ERROR};
Modules ->
SModules = lists:sort(Modules),
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Stop Modules")}]},
@ -183,12 +191,13 @@ get_form(["running nodes", ENode, "modules", "stop"], Lang) ->
S = atom_to_list(M),
?XFIELD("boolean", S, S, "0")
end, SModules)
]}
]}]}
end
end;
get_form(["running nodes", ENode, "modules", "start"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Start Modules")}]},
@ -203,10 +212,11 @@ get_form(["running nodes", ENode, "modules", "start"], Lang) ->
{"var", "modules"}],
[{xmlelement, "value", [], [{xmlcdata, "[]."}]}]
}
]};
]}]};
get_form(["running nodes", ENode, "backup", "backup"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Backup to File")}]},
@ -221,10 +231,11 @@ get_form(["running nodes", ENode, "backup", "backup"], Lang) ->
{"var", "path"}],
[{xmlelement, "value", [], [{xmlcdata, ""}]}]
}
]};
]}]};
get_form(["running nodes", ENode, "backup", "restore"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Restore Backup from File")}]},
@ -239,10 +250,11 @@ get_form(["running nodes", ENode, "backup", "restore"], Lang) ->
{"var", "path"}],
[{xmlelement, "value", [], [{xmlcdata, ""}]}]
}
]};
]}]};
get_form(["running nodes", ENode, "backup", "textfile"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Dump Backup to Text File")}]},
@ -257,10 +269,11 @@ get_form(["running nodes", ENode, "backup", "textfile"], Lang) ->
{"var", "path"}],
[{xmlelement, "value", [], [{xmlcdata, ""}]}]
}
]};
]}]};
get_form(["running nodes", ENode, "import", "file"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Import User from File")}]},
@ -275,10 +288,11 @@ get_form(["running nodes", ENode, "import", "file"], Lang) ->
{"var", "path"}],
[{xmlelement, "value", [], [{xmlcdata, ""}]}]
}
]};
]}]};
get_form(["running nodes", ENode, "import", "dir"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Import User from Dir")}]},
@ -293,10 +307,11 @@ get_form(["running nodes", ENode, "import", "dir"], Lang) ->
{"var", "path"}],
[{xmlelement, "value", [], [{xmlcdata, ""}]}]
}
]};
]}]};
get_form(["config", "hostname"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Hostname Configuration")}]},
@ -309,10 +324,11 @@ get_form(["config", "hostname"], Lang) ->
translate:translate(Lang, "Host name")},
{"var", "hostname"}],
[{xmlelement, "value", [], [{xmlcdata, ?MYNAME}]}]}
]};
]}]};
get_form(["config", "acls"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "ACLs Configuration")}]},
@ -332,10 +348,11 @@ get_form(["config", "acls"], Lang) ->
[ets:tab2list(acl)])),
"\n"))
}
]};
]}]};
get_form(["config", "access"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Access Configuration")}]},
@ -362,10 +379,11 @@ get_form(["config", "access"], Lang) ->
])),
"\n"))
}
]};
]}]};
get_form(["config", "remusers"], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Remove Users")}]},
@ -381,7 +399,7 @@ get_form(["config", "remusers"], Lang) ->
?XFIELD("boolean", U, U, "0")
end, lists:sort(Users))
end
};
}]};
get_form(_, Lang) ->
{error, ?ERR_SERVICE_UNAVAILABLE}.
@ -712,13 +730,18 @@ process_sm_iq(From, To,
Lang = xml:get_tag_attr_s("xml:lang", SubEl),
case Type of
set ->
case xml:get_tag_attr_s("type", SubEl) of
XDataEl = find_xdata_el(SubEl),
case XDataEl of
false ->
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ACCEPTABLE]};
{xmlelement, _Name, Attrs, SubEls} ->
case xml:get_attr_s("type", Attrs) of
"cancel" ->
IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}], []}]};
"submit" ->
XData = jlib:parse_xdata_submit(SubEl),
XData = jlib:parse_xdata_submit(XDataEl),
case XData of
invalid ->
IQ#iq{type = error,
@ -744,7 +767,8 @@ process_sm_iq(From, To,
end;
_ ->
IQ#iq{type = error,
sub_el = [SubEl, ?ERR_NOT_ALLOWED]}
sub_el = [SubEl, ?ERR_BAD_REQUEST]}
end
end;
get ->
Node =
@ -764,7 +788,8 @@ process_sm_iq(From, To,
get_sm_form(User, [], Lang) ->
{result, [{xmlelement, "title", [],
{result, [{xmlelement, "x", [{"xmlns", ?NS_XDATA}],
[{xmlelement, "title", [],
[{xmlcdata,
translate:translate(
Lang, "Administration of " ++ User)}]},
@ -791,7 +816,7 @@ get_sm_form(User, [], Lang) ->
% translate:translate(Lang, "Host name")},
% {"var", "hostname"}],
% [{xmlelement, "value", [], [{xmlcdata, ?MYNAME}]}]}
]};
]}]};
get_sm_form(_, _, Lang) ->
{error, ?ERR_SERVICE_UNAVAILABLE}.
@ -820,3 +845,20 @@ set_sm_form(User, [], Lang, XData) ->
end;
set_sm_form(_, _, Lang, XData) ->
{error, ?ERR_SERVICE_UNAVAILABLE}.
find_xdata_el({xmlelement, _Name, _Attrs, SubEls}) ->
find_xdata_el1(SubEls).
find_xdata_el1([]) ->
false;
find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) ->
case xml:get_attr_s("xmlns", Attrs) of
?NS_XDATA ->
{xmlelement, Name, Attrs, SubEls};
_ ->
find_xdata_el1(Els)
end;
find_xdata_el1([_ | Els]) ->
find_xdata_el1(Els).

View File

@ -157,7 +157,7 @@ process_local_iq_info(From, _To, #iq{type = Type, xmlns = XMLNS,
"query",
[{"xmlns", XMLNS},
{"node", SNode}],
[feature_to_xml({?NS_IQDATA})]}]};
[feature_to_xml({?NS_EJABBERD_CONFIG})]}]};
{allow, ["running nodes", ENode, "modules"]} ->
?EMPTY_INFO_RESULT;
{allow, ["running nodes", ENode, "modules", _]} ->
@ -165,7 +165,7 @@ process_local_iq_info(From, _To, #iq{type = Type, xmlns = XMLNS,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS},
{"node", SNode}],
[feature_to_xml({?NS_IQDATA})]}]};
[feature_to_xml({?NS_EJABBERD_CONFIG})]}]};
{allow, ["running nodes", ENode, "backup"]} ->
?EMPTY_INFO_RESULT;
{allow, ["running nodes", ENode, "backup", _]} ->
@ -173,7 +173,7 @@ process_local_iq_info(From, _To, #iq{type = Type, xmlns = XMLNS,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS},
{"node", SNode}],
[feature_to_xml({?NS_IQDATA})]}]};
[feature_to_xml({?NS_EJABBERD_CONFIG})]}]};
{allow, ["running nodes", ENode, "import"]} ->
?EMPTY_INFO_RESULT;
{allow, ["running nodes", ENode, "import", _]} ->
@ -181,13 +181,13 @@ process_local_iq_info(From, _To, #iq{type = Type, xmlns = XMLNS,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS},
{"node", SNode}],
[feature_to_xml({?NS_IQDATA})]}]};
[feature_to_xml({?NS_EJABBERD_CONFIG})]}]};
{allow, ["config", _]} ->
IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS},
{"node", SNode}],
[feature_to_xml({?NS_IQDATA})]}]};
[feature_to_xml({?NS_EJABBERD_CONFIG})]}]};
_ ->
IQ#iq{type = error, sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}
end
@ -489,7 +489,7 @@ process_sm_iq_info(From, To, #iq{type = Type, xmlns = XMLNS,
"" ->
IQ#iq{type = result,
sub_el = [{xmlelement, "query", [{"xmlns", XMLNS}],
[feature_to_xml({?NS_IQDATA})]}]};
[feature_to_xml({?NS_EJABBERD_CONFIG})]}]};
_ ->
IQ#iq{type = error, sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}
end

View File

@ -358,16 +358,16 @@ set_form(From, [], Lang, XData) ->
{atomic, _} ->
{result, []};
_ ->
{error, "406", "Not Acceptable"}
{error, ?ERR_NOT_ACCEPTABLE}
end;
_ ->
{error, "406", "Not Acceptable"}
{error, ?ERR_NOT_ACCEPTABLE}
end;
_ ->
{error, "406", "Not Acceptable"}
{error, ?ERR_NOT_ACCEPTABLE}
end;
_ ->
{error, "406", "Not Acceptable"}
{error, ?ERR_NOT_ACCEPTABLE}
end;

View File

@ -63,7 +63,7 @@
config = #config{},
users = ?DICT:new(),
affiliations = ?DICT:new(),
history = lqueue_new(10),
history = lqueue_new(20),
subject = "",
subject_author = ""}).
@ -831,7 +831,8 @@ add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) ->
add_online_user(From, Nick, Role, StateData)),
send_new_presence(From, NewState),
send_existing_presences(From, NewState),
case send_history(From, NewState) of
Shift = count_stanza_shift(Nick, Els, NewState),
case send_history(From, Shift, NewState) of
true ->
ok;
_ ->
@ -879,7 +880,259 @@ extract_password([{xmlelement, Name, Attrs, SubEls} = El | Els]) ->
extract_password([_ | Els]) ->
extract_password(Els).
count_stanza_shift(Nick, Els, StateData) ->
HL = lqueue_to_list(StateData#state.history),
Since = extract_history(Els, "since"),
Shift0 = case Since of
false ->
0;
_ ->
Sin = calendar:datetime_to_gregorian_seconds(Since),
count_seconds_shift(Sin, HL)
end,
Seconds = extract_history(Els, "seconds"),
Shift1 = case Seconds of
false ->
0;
_ ->
Sec = calendar:datetime_to_gregorian_seconds(
calendar:now_to_universal_time(now())) - Seconds,
count_seconds_shift(Sec, HL)
end,
MaxStanzas = extract_history(Els, "maxstanzas"),
Shift2 = case MaxStanzas of
false ->
0;
_ ->
count_maxstanzas_shift(MaxStanzas, HL)
end,
MaxChars = extract_history(Els, "maxchars"),
Shift3 = case MaxChars of
false ->
0;
_ ->
count_maxchars_shift(Nick, MaxChars, HL)
end,
lists:max([Shift0, Shift1, Shift2, Shift3]).
count_seconds_shift(Seconds, HistoryList) ->
lists:sum(
lists:map(
fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) ->
T = calendar:datetime_to_gregorian_seconds(TimeStamp),
if
T < Seconds ->
1;
true ->
0
end
end, HistoryList)).
count_maxstanzas_shift(MaxStanzas, HistoryList) ->
S = length(HistoryList) - MaxStanzas,
if
S =< 0 ->
0;
true ->
S
end.
count_maxchars_shift(Nick, MaxSize, HistoryList) ->
NLen = string:len(Nick) + 1,
Sizes = lists:map(
fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) ->
Size + NLen
end, HistoryList),
calc_shift(MaxSize, Sizes).
calc_shift(MaxSize, Sizes) ->
Total = lists:sum(Sizes),
calc_shift(MaxSize, Total, 0, Sizes).
calc_shift(_MaxSize, _Size, Shift, []) ->
Shift;
calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
if
MaxSize >= Size ->
Shift;
true ->
calc_shift(MaxSize, Size - S, Shift + 1, TSizes)
end.
extract_history([], Type) ->
false;
extract_history([{xmlelement, Name, Attrs, SubEls} = El | Els], Type) ->
case xml:get_attr_s("xmlns", Attrs) of
?NS_MUC ->
AttrVal = xml:get_path_s(El,
[{elem, "history"}, {attr, Type}]),
case Type of
"since" ->
parse_datetime(AttrVal);
_ ->
case catch list_to_integer(AttrVal) of
{'EXIT', _} ->
false;
IntVal ->
if
IntVal >= 0 ->
IntVal;
true ->
false
end
end
end;
_ ->
extract_history(Els, Type)
end;
extract_history([_ | Els], Type) ->
extract_history(Els, Type).
% JEP-0082
% yyyy-mm-ddThh:mm:ss[.sss]{Z|{+|-}hh:mm} -> {{yyyy, mm, dd}, {hh, mm, ss}} (UTC)
parse_datetime(TimeStr) ->
DateTime = string:tokens(TimeStr, "T"),
case DateTime of
[Date, Time] ->
case parse_date(Date) of
false ->
false;
D ->
case parse_time(Time) of
false ->
false;
{T, TZH, TZM} ->
S = calendar:datetime_to_gregorian_seconds(
{D, T}),
calendar:gregorian_seconds_to_datetime(
S - TZH * 60 * 60 - TZM * 60 * 30)
end
end;
_ ->
false
end.
% yyyy-mm-dd
parse_date(Date) ->
YearMonthDay = string:tokens(Date, "-"),
case length(YearMonthDay) of
3 ->
[Y, M, D] = lists:map(
fun(L)->
case catch list_to_integer(L) of
{'EXIT', _} ->
false;
Int ->
Int
end
end, YearMonthDay),
case catch calendar:valid_date(Y, M, D) of
true ->
{Y, M, D};
_ ->
false
end;
_ ->
false
end.
% hh:mm:ss[.sss]TZD
parse_time(Time) ->
case string:str(Time, "Z") of
0 ->
parse_time_with_timezone(Time);
_ ->
[T | _] = string:tokens(Time, "Z"),
case parse_time1(T) of
false ->
false;
TT ->
{TT, 0, 0}
end
end.
parse_time_with_timezone(Time) ->
case string:str(Time, "+") of
0 ->
case string:str(Time, "-") of
0 ->
false;
_ ->
parse_time_with_timezone(Time, "-")
end;
_ ->
parse_time_with_timezone(Time, "+")
end.
parse_time_with_timezone(Time, Delim) ->
TTZ = string:tokens(Time, Delim),
case TTZ of
[T, TZ] ->
case parse_timezone(TZ) of
false ->
false;
{TZH, TZM} ->
case parse_time1(T) of
false ->
false;
TT ->
case Delim of
"-" ->
{TT, -TZH, -TZM};
"+" ->
{TT, TZH, TZM};
_ ->
false
end
end
end;
_ ->
false
end.
parse_timezone(TZ) ->
case string:tokens(TZ, ":") of
[H, M] ->
case check_list([{H, 12}, {M, 60}]) of
{[H, M], true} ->
{H, M};
_ ->
false
end;
_ ->
false
end.
parse_time1(Time) ->
case string:tokens(Time, ".") of
[HMS | _] ->
case string:tokens(HMS, ":") of
[H, M, S] ->
case check_list([{H, 24}, {M, 60}, {S, 60}]) of
{[H1, M1, S1], true} ->
{H1, M1, S1};
_ ->
false
end;
_ ->
false
end;
_ ->
false
end.
check_list(List) ->
lists:mapfoldl(
fun({L, N}, B)->
case catch list_to_integer(L) of
{'EXIT', _} ->
{false, false};
Int when (Int >= 0) and (Int =< N) ->
{Int, B};
_ ->
{false, false}
end
end, true, List).
send_update_presence(JID, StateData) ->
LJID = jlib:jid_tolower(JID),
@ -1087,22 +1340,28 @@ add_message_to_history(FromNick, Packet, StateData) ->
_ ->
true
end,
TimeStamp = calendar:now_to_universal_time(now()),
TSPacket = append_subtags(Packet,
[jlib:timestamp_to_xml(
calendar:now_to_universal_time(
now()))]),
Q1 = lqueue_in({FromNick, TSPacket, HaveSubject}, StateData#state.history),
[jlib:timestamp_to_xml(TimeStamp)]),
{xmlelement, Name, Attrs, Els} = TSPacket,
SPacket = jlib:replace_from_to(
jlib:jid_replace_resource(StateData#state.jid, FromNick),
StateData#state.jid,
TSPacket),
Size = string:len(xml:element_to_string(SPacket)),
Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size},
StateData#state.history),
StateData#state{history = Q1}.
send_history(JID, StateData) ->
send_history(JID, Shift, StateData) ->
lists:foldl(
fun({Nick, Packet, HaveSubject}, B) ->
fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) ->
ejabberd_router:route(
jlib:jid_replace_resource(StateData#state.jid, Nick),
JID,
Packet),
B or HaveSubject
end, false, lqueue_to_list(StateData#state.history)).
end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))).
send_subject(JID, StateData) ->

View File

@ -89,7 +89,7 @@ get_local_stats(["running nodes", _], []) ->
get_local_stats(["running nodes", ENode], Names) ->
case search_running_node(ENode) of
false ->
{error, "404", "Not Found"};
{error, ?ERR_ITEM_NOT_FOUND};
Node ->
{result,
lists:map(fun(Name) -> get_node_stat(Node, Name) end, Names)}