aboutsummaryrefslogtreecommitdiff
path: root/src/mod_muc_admin.erl
diff options
context:
space:
mode:
authorJerome Sautret <jerome.sautret@process-one.net>2015-04-07 16:30:34 +0200
committerChristophe Romain <christophe.romain@process-one.net>2015-04-07 17:05:43 +0200
commitc3280e9dadec1f4792a7114d214e3065a5866405 (patch)
tree0ae6313bfc0c0f800bb412c98c35c910497d2163 /src/mod_muc_admin.erl
parentejabberd_listener: Remove Erlang/OTP version check (diff)
Add mod_muc_admin contrib.
Diffstat (limited to 'src/mod_muc_admin.erl')
-rw-r--r--src/mod_muc_admin.erl888
1 files changed, 888 insertions, 0 deletions
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
new file mode 100644
index 000000000..3e659d2dc
--- /dev/null
+++ b/src/mod_muc_admin.erl
@@ -0,0 +1,888 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_muc_admin.erl
+%%% Author : Badlop <badlop@ono.com>
+%%% Purpose : Tools for additional MUC administration
+%%% Created : 8 Sep 2007 by Badlop <badlop@ono.com>
+%%% Id : $Id: mod_muc_admin.erl 1133 2012-10-17 22:13:06Z badlop $
+%%%----------------------------------------------------------------------
+
+-module(mod_muc_admin).
+-author('badlop@ono.com').
+
+-behaviour(gen_mod).
+
+-export([
+ start/2, stop/1, % gen_mod API
+ muc_online_rooms/1,
+ muc_unregister_nick/1,
+ create_room/3, destroy_room/3,
+ create_rooms_file/1, destroy_rooms_file/1,
+ rooms_unused_list/2, rooms_unused_destroy/2,
+ get_room_occupants/2,
+ get_room_occupants_number/2,
+ send_direct_invitation/4,
+ change_room_option/4,
+ set_room_affiliation/4,
+ get_room_affiliations/2,
+ web_menu_main/2, web_page_main/2, % Web Admin API
+ web_menu_host/3, web_page_host/3
+ ]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+-include("jlib.hrl").
+-include("mod_muc_room.hrl").
+-include("ejabberd_http.hrl").
+-include("ejabberd_web_admin.hrl").
+-include("ejabberd_commands.hrl").
+
+%% Copied from mod_muc/mod_muc.erl
+-record(muc_online_room, {name_host, pid}).
+
+%%----------------------------
+%% gen_mod
+%%----------------------------
+
+start(Host, _Opts) ->
+ ejabberd_commands:register_commands(commands()),
+ ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50),
+ ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50),
+ ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50),
+ ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50).
+
+stop(Host) ->
+ ejabberd_commands:unregister_commands(commands()),
+ ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50),
+ ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50),
+ ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50),
+ ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50).
+
+%%%
+%%% Register commands
+%%%
+
+commands() ->
+ [
+ #ejabberd_commands{name = muc_online_rooms, tags = [muc],
+ desc = "List existing rooms ('global' to get all vhosts)",
+ module = ?MODULE, function = muc_online_rooms,
+ args = [{host, binary}],
+ result = {rooms, {list, {room, string}}}},
+ #ejabberd_commands{name = muc_unregister_nick, tags = [muc],
+ desc = "Unregister the nick in the MUC service",
+ module = ?MODULE, function = muc_unregister_nick,
+ args = [{nick, binary}],
+ result = {res, rescode}},
+
+ #ejabberd_commands{name = create_room, tags = [muc_room],
+ desc = "Create a MUC room name@service in host",
+ module = ?MODULE, function = create_room,
+ args = [{name, binary}, {service, binary},
+ {host, binary}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = destroy_room, tags = [muc_room],
+ desc = "Destroy a MUC room",
+ module = ?MODULE, function = destroy_room,
+ args = [{name, binary}, {service, binary},
+ {host, binary}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = create_rooms_file, tags = [muc],
+ desc = "Create the rooms indicated in file",
+ module = ?MODULE, function = create_rooms_file,
+ args = [{file, string}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = destroy_rooms_file, tags = [muc],
+ desc = "Destroy the rooms indicated in file",
+ module = ?MODULE, function = destroy_rooms_file,
+ args = [{file, string}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = rooms_unused_list, tags = [muc],
+ desc = "List the rooms that are unused for many days in host",
+ module = ?MODULE, function = rooms_unused_list,
+ args = [{host, binary}, {days, integer}],
+ result = {rooms, {list, {room, string}}}},
+ #ejabberd_commands{name = rooms_unused_destroy, tags = [muc],
+ desc = "Destroy the rooms that are unused for many days in host",
+ module = ?MODULE, function = rooms_unused_destroy,
+ args = [{host, binary}, {days, integer}],
+ result = {rooms, {list, {room, string}}}},
+
+ #ejabberd_commands{name = get_room_occupants, tags = [muc_room],
+ desc = "Get the list of occupants of a MUC room",
+ module = ?MODULE, function = get_room_occupants,
+ args = [{name, binary}, {service, binary}],
+ result = {occupants, {list,
+ {occupant, {tuple,
+ [{jid, string},
+ {nick, string},
+ {role, string}
+ ]}}
+ }}},
+
+ #ejabberd_commands{name = get_room_occupants_number, tags = [muc_room],
+ desc = "Get the number of occupants of a MUC room",
+ module = ?MODULE, function = get_room_occupants_number,
+ args = [{name, binary}, {service, binary}],
+ result = {occupants, integer}},
+
+ #ejabberd_commands{name = send_direct_invitation, tags = [muc_room],
+ desc = "Send a direct invitation to several destinations",
+ longdesc = "Password and Message can also be: none. Users JIDs are separated with : ",
+ module = ?MODULE, function = send_direct_invitation,
+ args = [{room, binary}, {password, binary}, {reason, binary}, {users, binary}],
+ result = {res, rescode}},
+
+ #ejabberd_commands{name = change_room_option, tags = [muc_room],
+ desc = "Change an option in a MUC room",
+ module = ?MODULE, function = change_room_option,
+ args = [{name, binary}, {service, binary},
+ {option, binary}, {value, binary}],
+ result = {res, rescode}},
+
+ #ejabberd_commands{name = set_room_affiliation, tags = [muc_room],
+ desc = "Change an affiliation in a MUC room",
+ module = ?MODULE, function = set_room_affiliation,
+ args = [{name, binary}, {service, binary},
+ {jid, binary}, {affiliation, binary}],
+ result = {res, rescode}},
+ #ejabberd_commands{name = get_room_affiliations, tags = [muc_room],
+ desc = "Get the list of affiliations of a MUC room",
+ module = ?MODULE, function = get_room_affiliations,
+ args = [{name, binary}, {service, binary}],
+ result = {affiliations, {list,
+ {affiliation, {tuple,
+ [{username, string},
+ {domain, string},
+ {affiliation, atom},
+ {reason, string}
+ ]}}
+ }}}
+ ].
+
+
+%%%
+%%% ejabberd commands
+%%%
+
+muc_online_rooms(ServerHost) ->
+ MUCHost = find_host(ServerHost),
+ Rooms = ets:tab2list(muc_online_room),
+ lists:foldl(
+ fun({_, {Roomname, Host}, _}, Results) ->
+ case MUCHost of
+ global ->
+ [<<Roomname/binary, "@", Host/binary>> | Results];
+ Host ->
+ [<<Roomname/binary, "@", Host/binary>> | Results];
+ _ ->
+ Results
+ end
+ end,
+ [],
+ Rooms).
+
+muc_unregister_nick(Nick) ->
+ F2 = fun(N) ->
+ [{_,Key,_}] = mnesia:index_read(muc_registered, N, 3),
+ mnesia:delete({muc_registered, Key})
+ end,
+ case mnesia:transaction(F2, [Nick], 1) of
+ {atomic, ok} ->
+ ok;
+ {aborted, _Error} ->
+ error
+ end.
+
+
+%%----------------------------
+%% Ad-hoc commands
+%%----------------------------
+
+
+%%----------------------------
+%% Web Admin
+%%----------------------------
+
+%%---------------
+%% Web Admin Menu
+
+web_menu_main(Acc, Lang) ->
+ Acc ++ [{<<"muc">>, ?T(<<"Multi-User Chat">>)}].
+
+web_menu_host(Acc, _Host, Lang) ->
+ Acc ++ [{<<"muc">>, ?T(<<"Multi-User Chat">>)}].
+
+
+%%---------------
+%% Web Admin Page
+
+-define(TDTD(L, N),
+ ?XE(<<"tr">>, [?XCT(<<"td">>, L),
+ ?XC(<<"td">>, jlib:integer_to_binary(N))
+ ])).
+
+web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) ->
+ Res = [?XC(<<"h1">>, <<"Multi-User Chat">>),
+ ?XC(<<"h3">>, <<"Statistics">>),
+ ?XAE(<<"table">>, [],
+ [?XE(<<"tbody">>, [?TDTD(<<"Total rooms">>, ets:info(muc_online_room, size)),
+ ?TDTD(<<"Permanent rooms">>, mnesia:table_info(muc_room, size)),
+ ?TDTD(<<"Registered nicknames">>, mnesia:table_info(muc_registered, size))
+ ])
+ ]),
+ ?XE(<<"ul">>, [?LI([?ACT(<<"rooms">>, <<"List of rooms">>)])])
+ ],
+ {stop, Res};
+
+web_page_main(_, #request{path=[<<"muc">>, <<"rooms">>], q = Q, lang = Lang} = _Request) ->
+ Sort_query = get_sort_query(Q),
+ Res = make_rooms_page(global, Lang, Sort_query),
+ {stop, Res};
+
+web_page_main(Acc, _) -> Acc.
+
+web_page_host(_, Host,
+ #request{path = [<<"muc">>],
+ q = Q,
+ lang = Lang} = _Request) ->
+ Sort_query = get_sort_query(Q),
+ Res = make_rooms_page(find_host(Host), Lang, Sort_query),
+ {stop, Res};
+web_page_host(Acc, _, _) -> Acc.
+
+
+%% Returns: {normal | reverse, Integer}
+get_sort_query(Q) ->
+ case catch get_sort_query2(Q) of
+ {ok, Res} -> Res;
+ _ -> {normal, 1}
+ end.
+
+get_sort_query2(Q) ->
+ {value, {_, String}} = lists:keysearch(<<"sort">>, 1, Q),
+ Integer = list_to_integer(String),
+ case Integer >= 0 of
+ true -> {ok, {normal, Integer}};
+ false -> {ok, {reverse, abs(Integer)}}
+ end.
+
+make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) ->
+ Rooms_names = get_rooms(Host),
+ Rooms_infos = build_info_rooms(Rooms_names),
+ Rooms_sorted = sort_rooms(Sort_direction, Sort_column, Rooms_infos),
+ Rooms_prepared = prepare_rooms_infos(Rooms_sorted),
+ TList = lists:map(
+ fun(Room) ->
+ ?XE(<<"tr">>, [?XC(<<"td">>, E) || E <- Room])
+ end, Rooms_prepared),
+ Titles = [<<"Jabber ID">>,
+ <<"# participants">>,
+ <<"Last message">>,
+ <<"Public">>,
+ <<"Persistent">>,
+ <<"Logging">>,
+ <<"Just created">>,
+ <<"Title">>],
+ {Titles_TR, _} =
+ lists:mapfoldl(
+ fun(Title, Num_column) ->
+ NCS = jlib:integer_to_binary(Num_column),
+ TD = ?XE(<<"td">>, [?CT(Title),
+ ?C(<<" ">>),
+ ?ACT(<<"?sort=", NCS/binary>>, <<"<">>),
+ ?C(<<" ">>),
+ ?ACT(<<"?sort=-", NCS/binary>>, <<">">>)]),
+ {TD, Num_column+1}
+ end,
+ 1,
+ Titles),
+ [?XC(<<"h1">>, <<"Multi-User Chat">>),
+ ?XC(<<"h2">>, <<"Rooms">>),
+ ?XE(<<"table">>,
+ [?XE(<<"thead">>,
+ [?XE(<<"tr">>, Titles_TR)]
+ ),
+ ?XE(<<"tbody">>, TList)
+ ]
+ )
+ ].
+
+sort_rooms(Direction, Column, Rooms) ->
+ Rooms2 = lists:keysort(Column, Rooms),
+ case Direction of
+ normal -> Rooms2;
+ reverse -> lists:reverse(Rooms2)
+ end.
+
+build_info_rooms(Rooms) ->
+ [build_info_room(Room) || Room <- Rooms].
+
+build_info_room({Name, Host, Pid}) ->
+ C = get_room_config(Pid),
+ Title = C#config.title,
+ Public = C#config.public,
+ Persistent = C#config.persistent,
+ Logging = C#config.logging,
+
+ S = get_room_state(Pid),
+ Just_created = S#state.just_created,
+ Num_participants = length(dict:fetch_keys(S#state.users)),
+
+ History = (S#state.history)#lqueue.queue,
+ Ts_last_message =
+ case queue:is_empty(History) of
+ true ->
+ <<"A long time ago">>;
+ false ->
+ Last_message1 = queue:last(History),
+ {_, _, _, Ts_last, _} = Last_message1,
+ jlib:timestamp_to_iso(Ts_last)
+ end,
+
+ {<<Name/binary, "@", Host/binary>>,
+ Num_participants,
+ Ts_last_message,
+ Public,
+ Persistent,
+ Logging,
+ Just_created,
+ Title}.
+
+prepare_rooms_infos(Rooms) ->
+ [prepare_room_info(Room) || Room <- Rooms].
+prepare_room_info(Room_info) ->
+ {NameHost,
+ Num_participants,
+ Ts_last_message,
+ Public,
+ Persistent,
+ Logging,
+ Just_created,
+ Title} = Room_info,
+ [NameHost,
+ jlib:integer_to_binary(Num_participants),
+ Ts_last_message,
+ jlib:atom_to_binary(Public),
+ jlib:atom_to_binary(Persistent),
+ jlib:atom_to_binary(Logging),
+ jlib:atom_to_binary(Just_created),
+ Title].
+
+
+%%----------------------------
+%% Create/Delete Room
+%%----------------------------
+
+%% @spec (Name::binary(), Host::binary(), ServerHost::binary()) ->
+%% ok | error
+%% @doc Create a room immediately with the default options.
+create_room(Name, Host, ServerHost) ->
+
+ %% Get the default room options from the muc configuration
+ DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc,
+ default_room_options, fun(X) -> X end, []),
+
+ %% Store the room on the server, it is not started yet though at this point
+ mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts),
+
+ %% Get all remaining mod_muc parameters that might be utilized
+ Access = gen_mod:get_module_opt(ServerHost, mod_muc, access, fun(X) -> X end, all),
+ AcCreate = gen_mod:get_module_opt(ServerHost, mod_muc, access_create, fun(X) -> X end, all),
+ AcAdmin = gen_mod:get_module_opt(ServerHost, mod_muc, access_admin, fun(X) -> X end, none),
+ AcPer = gen_mod:get_module_opt(ServerHost, mod_muc, access_persistent, fun(X) -> X end, all),
+ _PersistHistory = gen_mod:get_module_opt(ServerHost, mod_muc, persist_history, fun(X) -> X end, false),
+ HistorySize = gen_mod:get_module_opt(ServerHost, mod_muc, history_size, fun(X) -> X end, 20),
+ RoomShaper = gen_mod:get_module_opt(ServerHost, mod_muc, room_shaper, fun(X) -> X end, none),
+
+ %% If the room does not exist yet in the muc_online_room
+ case mnesia:dirty_read(muc_online_room, {Name, Host}) of
+ [] ->
+ %% Start the room
+ {ok, Pid} = mod_muc_room:start(
+ Host,
+ ServerHost,
+ {Access, AcCreate, AcAdmin, AcPer},
+ Name,
+ HistorySize,
+ RoomShaper,
+ DefRoomOpts),
+ {atomic, ok} = register_room(Host, Name, Pid),
+ ok;
+ _ ->
+ error
+ end.
+
+register_room(Host, Name, Pid) ->
+ F = fun() ->
+ mnesia:write(#muc_online_room{name_host = {Name, Host},
+ pid = Pid})
+ end,
+ mnesia:transaction(F).
+
+%% Create the room only in the database.
+%% It is required to restart the MUC service for the room to appear.
+muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) ->
+ io:format("Creating room ~s@~s~n", [Name, Host]),
+ mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts).
+
+%% @spec (Name::binary(), Host::binary(), ServerHost::binary()) ->
+%% ok | {error, room_not_exists}
+%% @doc Destroy the room immediately.
+%% If the room has participants, they are not notified that the room was destroyed;
+%% they will notice when they try to chat and receive an error that the room doesn't exist.
+destroy_room(Name, Service, _Server) ->
+ case mnesia:dirty_read(muc_online_room, {Name, Service}) of
+ [R] ->
+ Pid = R#muc_online_room.pid,
+ gen_fsm:send_all_state_event(Pid, destroy),
+ ok;
+ [] ->
+ error
+ end.
+
+destroy_room({N, H, SH}) ->
+ io:format("Destroying room: ~s@~s - vhost: ~s~n", [N, H, SH]),
+ destroy_room(N, H, SH).
+
+
+%%----------------------------
+%% Destroy Rooms in File
+%%----------------------------
+
+%% The format of the file is: one chatroom JID per line
+%% The file encoding must be UTF-8
+
+destroy_rooms_file(Filename) ->
+ {ok, F} = file:open(Filename, [read]),
+ RJID = read_room(F),
+ Rooms = read_rooms(F, RJID, []),
+ file:close(F),
+ [destroy_room(A) || A <- Rooms],
+ ok.
+
+read_rooms(_F, eof, L) ->
+ L;
+
+read_rooms(F, RJID, L) ->
+ RJID2 = read_room(F),
+ read_rooms(F, RJID2, [RJID | L]).
+
+read_room(F) ->
+ case io:get_line(F, "") of
+ eof -> eof;
+ String ->
+ case io_lib:fread("~s", String) of
+ {ok, [RoomJID], _} -> split_roomjid(RoomJID);
+ {error, What} ->
+ io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String])
+ end
+ end.
+
+%% This function is quite rudimentary
+%% and may not be accurate
+split_roomjid(RoomJID) ->
+ [Name, Host] = string:tokens(RoomJID, "@"),
+ [_MUC_service_name | ServerHostList] = string:tokens(Host, "."),
+ ServerHost = join(ServerHostList, "."),
+ {Name, Host, ServerHost}.
+
+%% This function is copied from string:join/2 in Erlang/OTP R12B-1
+%% Note that string:join/2 is not implemented in Erlang/OTP R11B
+join([H|T], Sep) ->
+ H ++ lists:concat([Sep ++ X || X <- T]).
+
+
+%%----------------------------
+%% Create Rooms in File
+%%----------------------------
+
+create_rooms_file(Filename) ->
+ {ok, F} = file:open(Filename, [read]),
+ RJID = read_room(F),
+ Rooms = read_rooms(F, RJID, []),
+ file:close(F),
+ %% Read the default room options defined for the first virtual host
+ DefRoomOpts = gen_mod:get_module_opt(?MYNAME, mod_muc,
+ default_room_options,
+ fun(L) when is_list(L) -> L end, []),
+ [muc_create_room(?MYNAME, A, DefRoomOpts) || A <- Rooms],
+ ok.
+
+
+%%----------------------------
+%% List/Delete Unused Rooms
+%%----------------------------
+
+%%---------------
+%% Control
+
+rooms_unused_list(Host, Days) ->
+ rooms_unused_report(list, Host, Days).
+rooms_unused_destroy(Host, Days) ->
+ rooms_unused_report(destroy, Host, Days).
+
+rooms_unused_report(Action, Host, Days) ->
+ {NA, NP, RP} = muc_unused(Action, Host, Days),
+ io:format("Unused rooms: ~p out of ~p~n", [NP, NA]),
+ [[R, <<"@">>, H] || {R, H, _P} <- RP].
+
+muc_unused(Action, ServerHost, Days) ->
+ Host = find_host(ServerHost),
+ muc_unused2(Action, ServerHost, Host, Days).
+
+muc_unused2(Action, ServerHost, Host, Last_allowed) ->
+ %% Get all required info about all existing rooms
+ Rooms_all = get_rooms(Host),
+
+ %% Decide which ones pass the requirements
+ Rooms_pass = decide_rooms(Rooms_all, Last_allowed),
+
+ Num_rooms_all = length(Rooms_all),
+ Num_rooms_pass = length(Rooms_pass),
+
+ %% Perform the desired action for matching rooms
+ act_on_rooms(Action, Rooms_pass, ServerHost),
+
+ {Num_rooms_all, Num_rooms_pass, Rooms_pass}.
+
+%%---------------
+%% Get info
+
+get_rooms(Host) ->
+ Get_room_names = fun(Room_reg, Names) ->
+ Pid = Room_reg#muc_online_room.pid,
+ case {Host, Room_reg#muc_online_room.name_host} of
+ {Host, {Name1, Host}} ->
+ [{Name1, Host, Pid} | Names];
+ {global, {Name1, Host1}} ->
+ [{Name1, Host1, Pid} | Names];
+ _ ->
+ Names
+ end
+ end,
+ ets:foldr(Get_room_names, [], muc_online_room).
+
+get_room_config(Room_pid) ->
+ {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_config),
+ R.
+
+get_room_state(Room_pid) ->
+ {ok, R} = gen_fsm:sync_send_all_state_event(Room_pid, get_state),
+ R.
+
+%%---------------
+%% Decide
+
+decide_rooms(Rooms, Last_allowed) ->
+ Decide = fun(R) -> decide_room(R, Last_allowed) end,
+ lists:filter(Decide, Rooms).
+
+decide_room({_Room_name, _Host, Room_pid}, Last_allowed) ->
+ C = get_room_config(Room_pid),
+ Persistent = C#config.persistent,
+
+ S = get_room_state(Room_pid),
+ Just_created = S#state.just_created,
+
+ Room_users = S#state.users,
+ Num_users = length(?DICT:to_list(Room_users)),
+
+ History = (S#state.history)#lqueue.queue,
+ Ts_now = calendar:now_to_universal_time(now()),
+ Ts_uptime = uptime_seconds(),
+ {Has_hist, Last} = case queue:is_empty(History) of
+ true ->
+ {false, Ts_uptime};
+ false ->
+ Last_message = queue:last(History),
+ {_, _, _, Ts_last, _} = Last_message,
+ Ts_diff =
+ calendar:datetime_to_gregorian_seconds(Ts_now)
+ - calendar:datetime_to_gregorian_seconds(Ts_last),
+ {true, Ts_diff}
+ end,
+
+ case {Persistent, Just_created, Num_users, Has_hist, seconds_to_days(Last)} of
+ {_true, false, 0, _, Last_days}
+ when Last_days >= Last_allowed ->
+ true;
+ _ ->
+ false
+ end.
+
+seconds_to_days(S) ->
+ S div (60*60*24).
+
+%%---------------
+%% Act
+
+act_on_rooms(Action, Rooms, ServerHost) ->
+ ServerHosts = [ {A, find_host(A)} || A <- ?MYHOSTS ],
+ Delete = fun({_N, H, _Pid} = Room) ->
+ SH = case ServerHost of
+ global -> find_serverhost(H, ServerHosts);
+ O -> O
+ end,
+
+ act_on_room(Action, Room, SH)
+ end,
+ lists:foreach(Delete, Rooms).
+
+find_serverhost(Host, ServerHosts) ->
+ {value, {ServerHost, Host}} = lists:keysearch(Host, 2, ServerHosts),
+ ServerHost.
+
+act_on_room(destroy, {N, H, Pid}, SH) ->
+ gen_fsm:send_all_state_event(
+ Pid, {destroy, <<"Room destroyed by rooms_unused_destroy.">>}),
+ mod_muc:room_destroyed(H, N, Pid, SH),
+ mod_muc:forget_room(SH, H, N);
+
+act_on_room(list, _, _) ->
+ ok.
+
+
+%%----------------------------
+%% Change Room Option
+%%----------------------------
+
+get_room_occupants(Room, Host) ->
+ case get_room_pid(Room, Host) of
+ room_not_found -> throw({error, room_not_found});
+ Pid -> get_room_occupants(Pid)
+ end.
+
+get_room_occupants(Pid) ->
+ S = get_room_state(Pid),
+ lists:map(
+ fun({_LJID, Info}) ->
+ {jlib:jid_to_string(Info#user.jid),
+ Info#user.nick,
+ atom_to_list(Info#user.role)}
+ end,
+ dict:to_list(S#state.users)).
+
+get_room_occupants_number(Room, Host) ->
+ length(get_room_occupants(Room, Host)).
+
+%%----------------------------
+%% Send Direct Invitation
+%%----------------------------
+%% http://xmpp.org/extensions/xep-0249.html
+
+send_direct_invitation(RoomString, Password, Reason, UsersString) ->
+ RoomJid = jlib:string_to_jid(RoomString),
+ XmlEl = build_invitation(Password, Reason, RoomString),
+ UsersStrings = get_users_to_invite(RoomJid, binary_to_list(UsersString)),
+ [send_direct_invitation(RoomJid, jlib:string_to_jid(list_to_binary(UserStrings)), XmlEl)
+ || UserStrings <- UsersStrings],
+ timer:sleep(1000),
+ ok.
+
+get_users_to_invite(RoomJid, UsersString) ->
+ UsersStrings = string:tokens(UsersString, ":"),
+ OccupantsTuples = get_room_occupants(RoomJid#jid.luser,
+ RoomJid#jid.lserver),
+ OccupantsJids = [jlib:string_to_jid(JidString)
+ || {JidString, _Nick, _} <- OccupantsTuples],
+ lists:filter(
+ fun(UserString) ->
+ UserJid = jlib:string_to_jid(list_to_binary(UserString)),
+ %% [{"badlop@localhost/work","badlop","moderator"}]
+ lists:all(fun(OccupantJid) ->
+ UserJid#jid.luser /= OccupantJid#jid.luser
+ orelse UserJid#jid.lserver /= OccupantJid#jid.lserver
+ end,
+ OccupantsJids)
+ end,
+ UsersStrings).
+
+build_invitation(Password, Reason, RoomString) ->
+ PasswordAttrList = case Password of
+ <<"none">> -> [];
+ _ -> [{<<"password">>, Password}]
+ end,
+ ReasonAttrList = case Reason of
+ <<"none">> -> [];
+ _ -> [{<<"reason">>, Reason}]
+ end,
+ XAttrs = [{<<"xmlns">>, ?NS_XCONFERENCE},
+ {<<"jid">>, RoomString}]
+ ++ PasswordAttrList
+ ++ ReasonAttrList,
+ XEl = {xmlel, <<"x">>, XAttrs, []},
+ {xmlel, <<"message">>, [], [XEl]}.
+
+send_direct_invitation(FromJid, UserJid, XmlEl) ->
+ ejabberd_router:route(FromJid, UserJid, XmlEl).
+
+%%----------------------------
+%% Change Room Option
+%%----------------------------
+
+%% @spec(Name::string(), Service::string(), Option::string(), Value) -> ok
+%% Value = atom() | integer() | string()
+%% @doc Change an option in an existing room.
+%% Requires the name of the room, the MUC service where it exists,
+%% the option to change (for example title or max_users),
+%% and the value to assign to the new option.
+%% For example:
+%% change_room_option("testroom", "conference.localhost", "title", "Test Room")
+change_room_option(Name, Service, Option, Value) when is_atom(Option) ->
+ Pid = get_room_pid(Name, Service),
+ {ok, _} = change_room_option(Pid, Option, Value),
+ ok;
+change_room_option(Name, Service, OptionString, ValueString) ->
+ Option = jlib:binary_to_atom(OptionString),
+ Value = case Option of
+ title -> ValueString;
+ description -> ValueString;
+ password -> ValueString;
+ subject ->ValueString;
+ subject_author ->ValueString;
+ max_users -> jlib:binary_to_integer(ValueString);
+ _ -> jlib:binary_to_atom(ValueString)
+ end,
+ change_room_option(Name, Service, Option, Value).
+
+change_room_option(Pid, Option, Value) ->
+ Config = get_room_config(Pid),
+ Config2 = change_option(Option, Value, Config),
+ gen_fsm:sync_send_all_state_event(Pid, {change_config, Config2}).
+
+%% @doc Get the Pid of an existing MUC room, or 'room_not_found'.
+get_room_pid(Name, Service) ->
+ case mnesia:dirty_read(muc_online_room, {Name, Service}) of
+ [] ->
+ room_not_found;
+ [Room] ->
+ Room#muc_online_room.pid
+ end.
+
+%% It is required to put explicitely all the options because
+%% the record elements are replaced at compile time.
+%% So, this can't be parametrized.
+change_option(Option, Value, Config) ->
+ case Option of
+ allow_change_subj -> Config#config{allow_change_subj = Value};
+ allow_private_messages -> Config#config{allow_private_messages = Value};
+ allow_query_users -> Config#config{allow_query_users = Value};
+ allow_user_invites -> Config#config{allow_user_invites = Value};
+ anonymous -> Config#config{anonymous = Value};
+ logging -> Config#config{logging = Value};
+ max_users -> Config#config{max_users = Value};
+ members_by_default -> Config#config{members_by_default = Value};
+ members_only -> Config#config{members_only = Value};
+ moderated -> Config#config{moderated = Value};
+ password -> Config#config{password = Value};
+ password_protected -> Config#config{password_protected = Value};
+ persistent -> Config#config{persistent = Value};
+ public -> Config#config{public = Value};
+ public_list -> Config#config{public_list = Value};
+ title -> Config#config{title = Value}
+ end.
+
+
+%%----------------------------
+%% Get Room Affiliations
+%%----------------------------
+
+%% @spec(Name::binary(), Service::binary()) ->
+%% [{JID::string(), Domain::string(), Role::string(), Reason::string()}]
+%% @doc Get the affiliations of the room Name@Service.
+get_room_affiliations(Name, Service) ->
+ case mnesia:dirty_read(muc_online_room, {Name, Service}) of
+ [R] ->
+ %% Get the PID of the online room, then request its state
+ Pid = R#muc_online_room.pid,
+ {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state),
+ Affiliations = ?DICT:to_list(StateData#state.affiliations),
+ lists:map(
+ fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)->
+ {Uname, Domain, Aff, Reason};
+ ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)->
+ {Uname, Domain, Aff, <<>>}
+ end, Affiliations);
+ [] ->
+ throw({error, "The room does not exist."})
+ end.
+
+%%----------------------------
+%% Change Room Affiliation
+%%----------------------------
+
+%% @spec(Name, Service, JID, AffiliationString) -> ok | {error, Error}
+%% Name = binary()
+%% Service = binary()
+%% JID = binary()
+%% AffiliationString = "outcast" | "none" | "member" | "admin" | "owner"
+%% @doc Set the affiliation of JID in the room Name@Service.
+%% If the affiliation is 'none', the action is to remove,
+%% In any other case the action will be to create the affiliation.
+set_room_affiliation(Name, Service, JID, AffiliationString) ->
+ Affiliation = jlib:binary_to_atom(AffiliationString),
+ case mnesia:dirty_read(muc_online_room, {Name, Service}) of
+ [R] ->
+ %% Get the PID for the online room so we can get the state of the room
+ Pid = R#muc_online_room.pid,
+ {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, get_state),
+ SJID = jlib:string_to_jid(JID),
+ LJID = jlib:jid_remove_resource(jlib:jid_tolower(SJID)),
+ Affiliations = change_affiliation(Affiliation, LJID, StateData#state.affiliations),
+ Res = StateData#state{affiliations = Affiliations},
+ {ok, _State} = gen_fsm:sync_send_all_state_event(Pid, {change_state, Res}),
+ mod_muc:store_room(Res#state.server_host, Res#state.host, Res#state.room, make_opts(Res)),
+ ok;
+ [] ->
+ error
+ end.
+
+change_affiliation(none, LJID, Affiliations) ->
+ ?DICT:erase(LJID, Affiliations);
+change_affiliation(Affiliation, LJID, Affiliations) ->
+ ?DICT:store(LJID, Affiliation, Affiliations).
+
+-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}).
+
+make_opts(StateData) ->
+ Config = StateData#state.config,
+ [
+ ?MAKE_CONFIG_OPT(title),
+ ?MAKE_CONFIG_OPT(allow_change_subj),
+ ?MAKE_CONFIG_OPT(allow_query_users),
+ ?MAKE_CONFIG_OPT(allow_private_messages),
+ ?MAKE_CONFIG_OPT(public),
+ ?MAKE_CONFIG_OPT(public_list),
+ ?MAKE_CONFIG_OPT(persistent),
+ ?MAKE_CONFIG_OPT(moderated),
+ ?MAKE_CONFIG_OPT(members_by_default),
+ ?MAKE_CONFIG_OPT(members_only),
+ ?MAKE_CONFIG_OPT(allow_user_invites),
+ ?MAKE_CONFIG_OPT(password_protected),
+ ?MAKE_CONFIG_OPT(password),
+ ?MAKE_CONFIG_OPT(anonymous),
+ ?MAKE_CONFIG_OPT(logging),
+ ?MAKE_CONFIG_OPT(max_users),
+ {affiliations, ?DICT:to_list(StateData#state.affiliations)},
+ {subject, StateData#state.subject},
+ {subject_author, StateData#state.subject_author}
+ ].
+
+
+%%----------------------------
+%% Utils
+%%----------------------------
+
+uptime_seconds() ->
+ trunc(element(1, erlang:statistics(wall_clock))/1000).
+
+find_host(global) ->
+ global;
+find_host("global") ->
+ global;
+find_host(<<"global">>) ->
+ global;
+find_host(ServerHost) when is_list(ServerHost) ->
+ find_host(list_to_binary(ServerHost));
+find_host(ServerHost) ->
+ gen_mod:get_module_opt_host(ServerHost, mod_muc, <<"conference.@HOST@">>).