%%%---------------------------------------------------------------------- %%% File : mod_muc_admin.erl %%% Author : Badlop %%% Purpose : Tools for additional MUC administration %%% Created : 8 Sep 2007 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(mod_muc_admin). -author('badlop@ono.com'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, depends/2, mod_doc/0, muc_online_rooms/1, muc_online_rooms_by_regex/2, muc_register_nick/3, muc_unregister_nick/2, create_room_with_opts/4, create_room/3, destroy_room/2, create_rooms_file/1, destroy_rooms_file/1, rooms_unused_list/2, rooms_unused_destroy/2, rooms_empty_list/1, rooms_empty_destroy/1, get_user_rooms/2, get_user_subscriptions/2, get_room_occupants/2, get_room_occupants_number/2, send_direct_invitation/5, change_room_option/4, get_room_options/2, set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3, web_menu_main/2, web_page_main/2, web_menu_host/3, subscribe_room/4, unsubscribe_room/2, get_subscribers/2, web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("ejabberd_commands.hrl"). -include("translate.hrl"). %%---------------------------- %% gen_mod %%---------------------------- start(Host, _Opts) -> ejabberd_commands:register_commands(get_commands_spec()), 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) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end, 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). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_muc, hard}]. %%% %%% Register commands %%% get_commands_spec() -> [ #ejabberd_commands{name = muc_online_rooms, tags = [muc], desc = "List existing rooms ('global' to get all vhosts)", policy = admin, module = ?MODULE, function = muc_online_rooms, args_desc = ["MUC service, or 'global' for all"], args_example = ["muc.example.com"], result_desc = "List of rooms", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = muc_online_rooms_by_regex, tags = [muc], desc = "List existing rooms ('global' to get all vhosts) by regex", policy = admin, module = ?MODULE, function = muc_online_rooms_by_regex, args_desc = ["MUC service, or 'global' for all", "Regex pattern for room name"], args_example = ["muc.example.com", "^prefix"], result_desc = "List of rooms with summary", result_example = [{"room1@muc.example.com", "true", 10}, {"room2@muc.example.com", "false", 10}], args = [{service, binary}, {regex, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, {tuple, [{jid, string}, {public, string}, {participants, integer} ]}}}}}, #ejabberd_commands{name = muc_register_nick, tags = [muc], desc = "Register a nick to a User JID in a MUC service", module = ?MODULE, function = muc_register_nick, args_desc = ["Nick", "User JID", "Service"], args_example = [<<"Tim">>, <<"tim@example.org">>, <<"muc.example.org">>], args = [{nick, binary}, {jid, binary}, {service, binary}], args_rename = [{host, service}], result = {res, rescode}}, #ejabberd_commands{name = muc_unregister_nick, tags = [muc], desc = "Unregister the nick registered by that account in the MUC service", module = ?MODULE, function = muc_unregister_nick, args_desc = ["User JID", "MUC service"], args_example = [<<"tim@example.org">>, <<"muc.example.org">>], args = [{jid, binary}, {service, binary}], args_rename = [{host, service}], 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_desc = ["Room name", "MUC service", "Server host"], args_example = ["room1", "muc.example.com", "example.com"], 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_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], args = [{name, binary}, {service, binary}], result = {res, rescode}}, #ejabberd_commands{name = create_rooms_file, tags = [muc], desc = "Create the rooms indicated in file", longdesc = "Provide one room JID per line. Rooms will be created after restart.", module = ?MODULE, function = create_rooms_file, args_desc = ["Path to the text file with one room JID per line"], args_example = ["/home/ejabberd/rooms.txt"], args = [{file, string}], result = {res, rescode}}, #ejabberd_commands{name = create_room_with_opts, tags = [muc_room], desc = "Create a MUC room name@service in host with given options", module = ?MODULE, function = create_room_with_opts, args_desc = ["Room name", "MUC service", "Server host", "List of options"], args_example = ["room1", "muc.example.com", "localhost", [{"members_only","true"}]], args = [{name, binary}, {service, binary}, {host, binary}, {options, {list, {option, {tuple, [{name, binary}, {value, binary} ]}} }}], result = {res, rescode}}, #ejabberd_commands{name = destroy_rooms_file, tags = [muc], desc = "Destroy the rooms indicated in file", longdesc = "Provide one room JID per line.", module = ?MODULE, function = destroy_rooms_file, args_desc = ["Path to the text file with one room JID per line"], args_example = ["/home/ejabberd/rooms.txt"], 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 the service", longdesc = "The room recent history is used, so it's recommended " " to wait a few days after service start before running this." " The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_unused_list, args_desc = ["MUC service, or 'global' for all", "Number of days"], args_example = ["muc.example.com", 31], result_desc = "List of unused rooms", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}, {days, integer}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = rooms_unused_destroy, tags = [muc], desc = "Destroy the rooms that are unused for many days in the service", longdesc = "The room recent history is used, so it's recommended " " to wait a few days after service start before running this." " The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_unused_destroy, args_desc = ["MUC service, or 'global' for all", "Number of days"], args_example = ["muc.example.com", 31], result_desc = "List of unused rooms that has been destroyed", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}, {days, integer}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = rooms_empty_list, tags = [muc], desc = "List the rooms that have no messages in archive", longdesc = "The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_empty_list, args_desc = ["MUC service, or 'global' for all"], args_example = ["muc.example.com"], result_desc = "List of empty rooms", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = rooms_empty_destroy, tags = [muc], desc = "Destroy the rooms that have no messages in archive", longdesc = "The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_empty_destroy, args_desc = ["MUC service, or 'global' for all"], args_example = ["muc.example.com"], result_desc = "List of empty rooms that have been destroyed", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = get_user_rooms, tags = [muc], desc = "Get the list of rooms where this user is occupant", module = ?MODULE, function = get_user_rooms, args_desc = ["Username", "Server host"], args_example = ["tom", "example.com"], result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{user, binary}, {host, binary}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = get_user_subscriptions, tags = [muc], desc = "Get the list of rooms where this user is subscribed", note = "added in 21.04", module = ?MODULE, function = get_user_subscriptions, args_desc = ["Username", "Server host"], args_example = ["tom", "example.com"], result_example = [{"room1@muc.example.com", "Tommy", ["mucsub:config"]}], args = [{user, binary}, {host, binary}], result = {rooms, {list, {room, {tuple, [{roomjid, string}, {usernick, string}, {nodes, {list, {node, 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_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of occupants with JID, nick and affiliation", result_example = [{"user1@example.com/psi", "User 1", "owner"}], 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_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "Number of room occupants", result_example = 7, 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 = "Since ejabberd 20.10, this command is " "asynchronous: the API call may return before the " "server has send all the invitations.\n\n" "Password and Message can also be: none. " "Users JIDs are separated with : ", module = ?MODULE, function = send_direct_invitation, args_desc = ["Room name", "MUC service", "Password, or none", "Reason text, or none", "Users JIDs separated with : characters"], args_example = [<<"room1">>, <<"muc.example.com">>, <<>>, <<"Check this out!">>, "user2@localhost:user3@example.com"], args = [{name, binary}, {service, 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_desc = ["Room name", "MUC service", "Option name", "Value to assign"], args_example = ["room1", "muc.example.com", "members_only", "true"], args = [{name, binary}, {service, binary}, {option, binary}, {value, binary}], result = {res, rescode}}, #ejabberd_commands{name = get_room_options, tags = [muc_room], desc = "Get options from a MUC room", module = ?MODULE, function = get_room_options, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "List of room options tuples with name and value", result_example = [{"members_only", "true"}], args = [{name, binary}, {service, binary}], result = {options, {list, {option, {tuple, [{name, string}, {value, string} ]}} }}}, #ejabberd_commands{name = subscribe_room, tags = [muc_room], desc = "Subscribe to a MUC conference", module = ?MODULE, function = subscribe_room, args_desc = ["User JID", "a user's nick", "the room to subscribe", "nodes separated by commas: ,"], args_example = ["tom@localhost", "Tom", "room1@conference.localhost", "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], result_desc = "The list of nodes that has subscribed", result_example = ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"], args = [{user, binary}, {nick, binary}, {room, binary}, {nodes, binary}], result = {nodes, {list, {node, string}}}}, #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], desc = "Unsubscribe from a MUC conference", module = ?MODULE, function = unsubscribe_room, args_desc = ["User JID", "the room to subscribe"], args_example = ["tom@localhost", "room1@conference.localhost"], args = [{user, binary}, {room, binary}], result = {res, rescode}}, #ejabberd_commands{name = get_subscribers, tags = [muc_room], desc = "List subscribers of a MUC conference", module = ?MODULE, function = get_subscribers, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of users that are subscribed to that room", result_example = ["user2@example.com", "user3@example.com"], args = [{name, binary}, {service, binary}], result = {subscribers, {list, {jid, string}}}}, #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], desc = "Change an affiliation in a MUC room", module = ?MODULE, function = set_room_affiliation, args_desc = ["Room name", "MUC service", "User JID", "Affiliation to set"], args_example = ["room1", "muc.example.com", "user2@example.com", "member"], 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_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of affiliations with username, domain, affiliation and reason", result_example = [{"user1", "example.com", member, "member"}], args = [{name, binary}, {service, binary}], result = {affiliations, {list, {affiliation, {tuple, [{username, string}, {domain, string}, {affiliation, atom}, {reason, string} ]}} }}}, #ejabberd_commands{name = get_room_affiliation, tags = [muc_room], desc = "Get affiliation of a user in MUC room", module = ?MODULE, function = get_room_affiliation, args_desc = ["Room name", "MUC service", "User JID"], args_example = ["room1", "muc.example.com", "user1@example.com"], result_desc = "Affiliation of the user", result_example = member, args = [{name, binary}, {service, binary}, {jid, binary}], result = {affiliation, atom}} ]. %%% %%% ejabberd commands %%% muc_online_rooms(ServiceArg) -> Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> [<> || {Name, _, _} <- mod_muc:get_online_rooms(Host)] end, Hosts). muc_online_rooms_by_regex(ServiceArg, Regex) -> {_, P} = re:compile(Regex), Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> [build_summary_room(Name, RoomHost, Pid) || {Name, RoomHost, Pid} <- mod_muc:get_online_rooms(Host), is_name_match(Name, P)] end, Hosts). is_name_match(Name, P) -> case re:run(Name, P) of {match, _} -> true; nomatch -> false end. build_summary_room(Name, Host, Pid) -> C = get_room_config(Pid), Public = C#config.public, S = get_room_state(Pid), Participants = maps:size(S#state.users), {<>, misc:atom_to_binary(Public), Participants }. muc_register_nick(Nick, FromBinary, Service) -> try {get_room_serverhost(Service), jid:decode(FromBinary)} of {ServerHost, From} -> Lang = <<"en">>, case mod_muc:iq_set_register_info(ServerHost, Service, From, Nick, Lang) of {result, undefined} -> ok; {error, #stanza_error{reason = 'conflict'}} -> throw({error, "Nick already registered"}); {error, _} -> throw({error, "Database error"}) end catch error:{invalid_domain, _} -> throw({error, "Invalid 'service'"}); error:{unregistered_route, _} -> throw({error, "Invalid 'service'"}); error:{bad_jid, _} -> throw({error, "Invalid 'jid'"}); _ -> throw({error, "Internal error"}) end. muc_unregister_nick(FromBinary, Service) -> muc_register_nick(<<"">>, FromBinary, Service). get_user_rooms(User, Server) -> lists:flatmap( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> Rooms = mod_muc:get_online_rooms_by_user( ServerHost, jid:nodeprep(User), jid:nodeprep(Server)), [<> || {Name, Host} <- Rooms]; false -> [] end end, ejabberd_option:hosts()). get_user_subscriptions(User, Server) -> Services = find_services(global), UserJid = jid:make(jid:nodeprep(User), jid:nodeprep(Server)), lists:flatmap( fun(ServerHost) -> {ok, Rooms} = mod_muc:get_subscribed_rooms(ServerHost, UserJid), [{jid:encode(RoomJid), UserNick, Nodes} || {RoomJid, UserNick, Nodes} <- Rooms] end, Services). %%---------------------------- %% Ad-hoc commands %%---------------------------- %%---------------------------- %% Web Admin %%---------------------------- %%--------------- %% Web Admin Menu web_menu_main(Acc, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. web_menu_host(Acc, _Host, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. %%--------------- %% Web Admin Page -define(TDTD(L, N), ?XE(<<"tr">>, [?XCT(<<"td">>, L), ?XC(<<"td">>, integer_to_binary(N)) ])). web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) -> OnlineRoomsNumber = lists:foldl( fun(Host, Acc) -> Acc + mod_muc:count_online_rooms(Host) end, 0, find_hosts(global)), PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), Res = ?H1GL(PageTitle, <<"modules/#mod-muc">>, <<"mod_muc">>) ++ [?XCT(<<"h3">>, ?T("Statistics")), ?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?TDTD(?T("Total rooms"), OnlineRoomsNumber) ]) ]), ?XE(<<"ul">>, [?LI([?ACT(<<"rooms/">>, ?T("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(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, {_, Binary}} = lists:keysearch(<<"sort">>, 1, Q), Integer = list_to_integer(string:strip(binary_to_list(Binary), right, $/)), case Integer >= 0 of true -> {ok, {normal, Integer}}; false -> {ok, {reverse, abs(Integer)}} end. make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) -> Service = find_service(Host), Rooms_names = get_online_rooms(Service), 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 = [?T("Jabber ID"), ?T("# participants"), ?T("Last message"), ?T("Public"), ?T("Persistent"), ?T("Logging"), ?T("Just created"), ?T("Room title"), ?T("Node")], {Titles_TR, _} = lists:mapfoldl( fun(Title, Num_column) -> NCS = integer_to_binary(Num_column), TD = ?XE(<<"td">>, [?CT(Title), ?C(<<" ">>), ?AC(<<"?sort=", NCS/binary>>, <<"<">>), ?C(<<" ">>), ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]), {TD, Num_column+1} end, 1, Titles), PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), ?H1GL(PageTitle, <<"modules/#mod-muc">>, <<"mod_muc">>) ++ [?XCT(<<"h2">>, ?T("Chatrooms")), ?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, _ServerHost, 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 = maps:size(S#state.users), Node = node(Pid), History = (S#state.history)#lqueue.queue, Ts_last_message = case p1_queue:is_empty(History) of true -> <<"A long time ago">>; false -> Last_message1 = get_queue_last(History), {_, _, _, Ts_last, _} = Last_message1, xmpp_util:encode_timestamp(Ts_last) end, {<>, Num_participants, Ts_last_message, Public, Persistent, Logging, Just_created, Title, Node}. get_queue_last(Queue) -> List = p1_queue:to_list(Queue), lists:last(List). 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, Node} = Room_info, [NameHost, integer_to_binary(Num_participants), Ts_last_message, misc:atom_to_binary(Public), misc:atom_to_binary(Persistent), misc:atom_to_binary(Logging), justcreated_to_binary(Just_created), Title, misc:atom_to_binary(Node)]. justcreated_to_binary(J) when is_integer(J) -> JNow = misc:usec_to_now(J), {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(JNow), str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second]); justcreated_to_binary(J) when is_atom(J) -> misc:atom_to_binary(J). %%---------------------------- %% Create/Delete Room %%---------------------------- %% @spec (Name::binary(), Host::binary(), ServerHost::binary()) -> %% ok | error %% @doc Create a room immediately with the default options. create_room(Name1, Host1, ServerHost) -> create_room_with_opts(Name1, Host1, ServerHost, []). create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) -> case {jid:nodeprep(Name1), jid:nodeprep(Host1), jid:nodeprep(ServerHost1)} of {error, _, _} -> throw({error, "Invalid 'name'"}); {_, error, _} -> throw({error, "Invalid 'host'"}); {_, _, error} -> throw({error, "Invalid 'serverhost'"}); {Name, Host, ServerHost} -> case get_room_pid(Name, Host) of room_not_found -> %% Get the default room options from the muc configuration DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), %% Change default room options as required FormattedRoomOpts = [format_room_option(Opt, Val) || {Opt, Val}<-CustomRoomOpts], RoomOpts = lists:ukeymerge(1, lists:keysort(1, FormattedRoomOpts), lists:keysort(1, DefRoomOpts)), case mod_muc:create_room(Host, Name, RoomOpts) of ok -> maybe_store_room(ServerHost, Host, Name, RoomOpts); {error, _} -> throw({error, "Unable to start room"}) end; _ -> throw({error, "Room already exists"}) end end. maybe_store_room(ServerHost, Host, Name, RoomOpts) -> case proplists:get_bool(persistent, RoomOpts) of true -> {atomic, ok} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts), ok; false -> ok end. %% 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 ~ts@~ts~n", [Name, Host]), mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). %% @spec (Name::binary(), Host::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(Name1, Service1) -> case {jid:nodeprep(Name1), jid:nodeprep(Service1)} of {error, _} -> throw({error, "Invalid 'name'"}); {_, error} -> throw({error, "Invalid 'service'"}); {Name, Service} -> case get_room_pid(Name, Service) of room_not_found -> throw({error, "Room doesn't exists"}); invalid_service -> throw({error, "Invalid 'service'"}); Pid -> mod_muc_room:destroy(Pid), ok end end. destroy_room({N, H, SH}) -> io:format("Destroying room: ~ts@~ts - vhost: ~ts~n", [N, H, SH]), destroy_room(N, H). %%---------------------------- %% 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, no_room, L) -> RJID2 = read_room(F), read_rooms(F, RJID2, 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("~ts", String) of {ok, [RoomJID], _} -> split_roomjid(list_to_binary(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) -> split_roomjid2(binary:split(RoomJID, <<"@">>)). split_roomjid2([Name, Host]) -> [_MUC_service_name, ServerHost] = binary:split(Host, <<".">>), {Name, Host, ServerHost}; split_roomjid2(_) -> no_room. %%---------------------------- %% 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 = mod_muc_opt:default_room_options(ejabberd_config:get_myname()), [muc_create_room(ejabberd_config:get_myname(), A, DefRoomOpts) || A <- Rooms], ok. %%--------------------------------- %% List/Delete Unused/Empty Rooms %%--------------------------------- %%--------------- %% Control rooms_unused_list(Service, Days) -> rooms_report(unused, list, Service, Days). rooms_unused_destroy(Service, Days) -> rooms_report(unused, destroy, Service, Days). rooms_empty_list(Service) -> rooms_report(empty, list, Service, 0). rooms_empty_destroy(Service) -> rooms_report(empty, destroy, Service, 0). rooms_report(Method, Action, Service, Days) -> {NA, NP, RP} = muc_unused(Method, Action, Service, Days), io:format("rooms ~ts: ~p out of ~p~n", [Method, NP, NA]), [<> || {R, H, _SH, _P} <- RP]. muc_unused(Method, Action, Service, Last_allowed) -> %% Get all required info about all existing rooms Rooms_all = get_all_rooms(Service), %% Decide which ones pass the requirements Rooms_pass = decide_rooms(Method, 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(Method, Action, Rooms_pass), {Num_rooms_all, Num_rooms_pass, Rooms_pass}. %%--------------- %% Get info get_online_rooms(ServiceArg) -> Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> ServerHost = get_room_serverhost(Host), [{RoomName, RoomHost, ServerHost, Pid} || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)] end, Hosts). get_all_rooms(Host) -> ServerHost = ejabberd_router:host_of_route(Host), OnlineRooms = get_online_rooms(Host), OnlineMap = lists:foldl( fun({Room, _, _, _}, Map) -> Map#{Room => 1} end, #{}, OnlineRooms), Mod = gen_mod:db_mod(ServerHost, mod_muc), DbRooms = case erlang:function_exported(Mod, get_rooms_without_subscribers, 2) of true -> Mod:get_rooms_without_subscribers(ServerHost, Host); _ -> Mod:get_rooms(ServerHost, Host) end, StoredRooms = lists:filtermap( fun(#muc_room{name_host = {Room, _}, opts = Opts}) -> case maps:is_key(Room, OnlineMap) of true -> false; _ -> {true, {Room, Host, ServerHost, Opts}} end end, DbRooms), OnlineRooms ++ StoredRooms. get_room_config(Room_pid) -> {ok, R} = mod_muc_room:get_config(Room_pid), R. get_room_state(Room_pid) -> {ok, R} = mod_muc_room:get_state(Room_pid), R. %%--------------- %% Decide decide_rooms(Method, Rooms, Last_allowed) -> Decide = fun(R) -> decide_room(Method, R, Last_allowed) end, lists:filter(Decide, Rooms). decide_room(unused, {_Room_name, _Host, ServerHost, Room_pid}, Last_allowed) -> NodeStartTime = erlang:system_time(microsecond) - 1000000*(erlang:monotonic_time(second)-ejabberd_config:get_node_start()), OnlyHibernated = case mod_muc_opt:hibernation_timeout(ServerHost) of Value when Value < Last_allowed*24*60*60*1000 -> true; _ -> false end, {Just_created, Num_users} = case Room_pid of Pid when is_pid(Pid) andalso OnlyHibernated -> {erlang:system_time(microsecond), 0}; Pid when is_pid(Pid) -> case mod_muc_room:get_state(Room_pid) of {ok, #state{just_created = JC, users = U}} -> {JC, maps:size(U)}; _ -> {erlang:system_time(microsecond), 0} end; Opts -> case lists:keyfind(hibernation_time, 1, Opts) of false -> {NodeStartTime, 0}; {_, T} -> {T, 0} end end, Last = case Just_created of true -> 0; _ -> (erlang:system_time(microsecond) - Just_created) div 1000000 end, case {Num_users, seconds_to_days(Last)} of {0, Last_days} when (Last_days >= Last_allowed) -> true; _ -> false end; decide_room(empty, {Room_name, Host, ServerHost, Room_pid}, _Last_allowed) -> case gen_mod:is_loaded(ServerHost, mod_mam) of true -> Room_options = case Room_pid of _ when is_pid(Room_pid) -> get_room_options(Room_pid); Opts -> Opts end, case lists:keyfind(<<"mam">>, 1, Room_options) of {<<"mam">>, <<"true">>} -> mod_mam:is_empty_for_room(ServerHost, Room_name, Host); _ -> false end; _ -> false end. seconds_to_days(S) -> S div (60*60*24). %%--------------- %% Act act_on_rooms(Method, Action, Rooms) -> Delete = fun(Room) -> act_on_room(Method, Action, Room) end, lists:foreach(Delete, Rooms). act_on_room(Method, destroy, {N, H, _SH, Pid}) -> Message = iolist_to_binary(io_lib:format( <<"Room destroyed by rooms_~s_destroy.">>, [Method])), case Pid of V when is_pid(V) -> mod_muc_room:destroy(Pid, Message); _ -> case get_room_pid(N, H) of Pid2 when is_pid(Pid2) -> mod_muc_room:destroy(Pid2, Message); _ -> ok end end; act_on_room(_Method, list, _) -> ok. %%---------------------------- %% Change Room Option %%---------------------------- get_room_occupants(Room, Host) -> case get_room_pid(Room, Host) of Pid when is_pid(Pid) -> get_room_occupants(Pid); _ -> throw({error, room_not_found}) end. get_room_occupants(Pid) -> S = get_room_state(Pid), lists:map( fun({_LJID, Info}) -> {jid:encode(Info#user.jid), Info#user.nick, atom_to_list(Info#user.role)} end, maps:to_list(S#state.users)). get_room_occupants_number(Room, Host) -> case get_room_pid(Room, Host) of Pid when is_pid(Pid )-> S = get_room_state(Pid), maps:size(S#state.users); _ -> throw({error, room_not_found}) end. %%---------------------------- %% Send Direct Invitation %%---------------------------- %% http://xmpp.org/extensions/xep-0249.html send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) -> case jid:make(RoomName, RoomService) of error -> throw({error, "Invalid 'roomname' or 'service'"}); RoomJid -> XmlEl = build_invitation(Password, Reason, RoomJid), Users = get_users_to_invite(RoomJid, UsersString), [send_direct_invitation(RoomJid, UserJid, XmlEl) || UserJid <- Users], ok end. get_users_to_invite(RoomJid, UsersString) -> UsersStrings = binary:split(UsersString, <<":">>, [global]), OccupantsTuples = get_room_occupants(RoomJid#jid.luser, RoomJid#jid.lserver), OccupantsJids = [jid:decode(JidString) || {JidString, _Nick, _} <- OccupantsTuples], lists:filtermap( fun(UserString) -> UserJid = jid:decode(UserString), Val = lists:all(fun(OccupantJid) -> UserJid#jid.luser /= OccupantJid#jid.luser orelse UserJid#jid.lserver /= OccupantJid#jid.lserver end, OccupantsJids), case {UserJid#jid.luser, Val} of {<<>>, _} -> false; {_, true} -> {true, UserJid}; _ -> false end end, UsersStrings). build_invitation(Password, Reason, RoomJid) -> Invite = #x_conference{jid = RoomJid, password = case Password of <<"none">> -> <<>>; _ -> Password end, reason = case Reason of <<"none">> -> <<>>; _ -> Reason end}, #message{sub_els = [Invite]}. send_direct_invitation(FromJid, UserJid, Msg) -> ejabberd_router:route(xmpp:set_from_to(Msg, FromJid, UserJid)). %%---------------------------- %% 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, OptionString, ValueString) -> case get_room_pid(Name, Service) of room_not_found -> throw({error, "Room not found"}); invalid_service -> throw({error, "Invalid 'service'"}); Pid -> {Option, Value} = format_room_option(OptionString, ValueString), change_room_option(Pid, Option, Value) end. change_room_option(Pid, Option, Value) -> case {Option, gen_mod:is_loaded((get_room_state(Pid))#state.server_host, mod_muc_log)} of {logging, false} -> mod_muc_log_not_enabled; _ -> Config = get_room_config(Pid), Config2 = change_option(Option, Value, Config), {ok, _} = mod_muc_room:set_config(Pid, Config2), ok end. format_room_option(OptionString, ValueString) -> Option = misc:binary_to_atom(OptionString), Value = case Option of title -> ValueString; description -> ValueString; password -> ValueString; subject ->ValueString; subject_author ->ValueString; presence_broadcast ->misc:expr_to_term(ValueString); max_users -> binary_to_integer(ValueString); voice_request_min_interval -> binary_to_integer(ValueString); vcard -> ValueString; vcard_xupdate when ValueString /= <<"undefined">>, ValueString /= <<"external">> -> ValueString; lang -> ValueString; pubsub -> ValueString; _ -> misc:binary_to_atom(ValueString) end, {Option, Value}. %% @doc Get the Pid of an existing MUC room, or 'room_not_found'. -spec get_room_pid(binary(), binary()) -> pid() | room_not_found | invalid_service. get_room_pid(Name, Service) -> try get_room_serverhost(Service) of ServerHost -> case mod_muc:unhibernate_room(ServerHost, Service, Name) of error -> room_not_found; {ok, Pid} -> Pid end catch error:{invalid_domain, _} -> invalid_service; error:{unregistered_route, _} -> invalid_service end. %% It is required to put explicitly 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_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value}; allow_query_users -> Config#config{allow_query_users = Value}; allow_subscription -> Config#config{allow_subscription = Value}; allow_user_invites -> Config#config{allow_user_invites = Value}; allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value}; allow_visitor_status -> Config#config{allow_visitor_status = Value}; allow_voice_requests -> Config#config{allow_voice_requests = Value}; anonymous -> Config#config{anonymous = Value}; captcha_protected -> Config#config{captcha_protected = Value}; description -> Config#config{description = Value}; logging -> Config#config{logging = Value}; mam -> Config#config{mam = 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}; presence_broadcast -> Config#config{presence_broadcast = Value}; public -> Config#config{public = Value}; public_list -> Config#config{public_list = Value}; title -> Config#config{title = Value}; vcard -> Config#config{vcard = Value}; voice_request_min_interval -> Config#config{voice_request_min_interval = Value} end. %%---------------------------- %% Get Room Options %%---------------------------- get_room_options(Name, Service) -> case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> get_room_options(Pid); _ -> [] end. get_room_options(Pid) -> Config = get_room_config(Pid), get_options(Config). get_options(Config) -> Fields = [misc:atom_to_binary(Field) || Field <- record_info(fields, config)], [config | ValuesRaw] = tuple_to_list(Config), Values = lists:map(fun(V) when is_atom(V) -> misc:atom_to_binary(V); (V) when is_integer(V) -> integer_to_binary(V); (V) when is_tuple(V); is_list(V) -> list_to_binary(hd(io_lib:format("~w", [V]))); (V) -> V end, ValuesRaw), lists:zip(Fields, Values). %%---------------------------- %% 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 get_room_pid(Name, Service) of Pid when is_pid(Pid) -> %% Get the PID of the online room, then request its state {ok, StateData} = mod_muc_room:get_state(Pid), Affiliations = maps: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. %%---------------------------- %% Get Room Affiliation %%---------------------------- %% @spec(Name::binary(), Service::binary(), JID::binary()) -> %% {Affiliation::string()} %% @doc Get affiliation of a user in the room Name@Service. get_room_affiliation(Name, Service, JID) -> case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> %% Get the PID of the online room, then request its state {ok, StateData} = mod_muc_room:get_state(Pid), UserJID = jid:decode(JID), mod_muc_room:get_affiliation(UserJID, StateData); _ -> 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 = case AffiliationString of <<"outcast">> -> outcast; <<"none">> -> none; <<"member">> -> member; <<"admin">> -> admin; <<"owner">> -> owner; _ -> throw({error, "Invalid affiliation"}) end, case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> %% Get the PID for the online room so we can get the state of the room case mod_muc_room:change_item(Pid, jid:decode(JID), affiliation, Affiliation, <<"">>) of {ok, _} -> ok; {error, notfound} -> throw({error, "Room doesn't exists"}); {error, _} -> throw({error, "Unable to perform change"}) end; room_not_found -> throw({error, "Room doesn't exists"}); invalid_service -> throw({error, "Invalid 'service'"}) end. %%% %%% MUC Subscription %%% subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> -> throw({error, "Nickname must be set"}); subscribe_room(User, Nick, Room, Nodes) -> NodeList = re:split(Nodes, "\\h*,\\h*"), try jid:decode(Room) of #jid{luser = Name, lserver = Host} when Name /= <<"">> -> try jid:decode(User) of UserJID1 -> UserJID = jid:replace_resource(UserJID1, <<"modmucadmin">>), case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> case mod_muc_room:subscribe( Pid, UserJID, Nick, NodeList) of {ok, SubscribedNodes} -> SubscribedNodes; {error, Reason} -> throw({error, binary_to_list(Reason)}) end; _ -> throw({error, "The room does not exist"}) end catch _:{bad_jid, _} -> throw({error, "Malformed user JID"}) end; _ -> throw({error, "Malformed room JID"}) catch _:{bad_jid, _} -> throw({error, "Malformed room JID"}) end. unsubscribe_room(User, Room) -> try jid:decode(Room) of #jid{luser = Name, lserver = Host} when Name /= <<"">> -> try jid:decode(User) of UserJID -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> case mod_muc_room:unsubscribe(Pid, UserJID) of ok -> ok; {error, Reason} -> throw({error, binary_to_list(Reason)}) end; _ -> throw({error, "The room does not exist"}) end catch _:{bad_jid, _} -> throw({error, "Malformed user JID"}) end; _ -> throw({error, "Malformed room JID"}) catch _:{bad_jid, _} -> throw({error, "Malformed room JID"}) end. get_subscribers(Name, Host) -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> {ok, JIDList} = mod_muc_room:get_subscribers(Pid), [jid:encode(jid:remove_resource(J)) || J <- JIDList]; _ -> throw({error, "The room does not exist"}) end. %%---------------------------- %% Utils %%---------------------------- find_service(global) -> global; find_service(ServerHost) -> hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). find_services(Global) when Global == global; Global == <<"global">> -> lists:flatmap( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> [find_service(ServerHost)]; false -> [] end end, ejabberd_option:hosts()); find_services(Service) when is_binary(Service) -> [Service]. get_room_serverhost(Service) when is_binary(Service) -> ejabberd_router:host_of_route(Service). find_host(ServerHost) -> hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). find_hosts(Global) when Global == global; Global == <<"global">> -> lists:flatmap( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> [find_host(ServerHost)]; false -> [] end end, ejabberd_option:hosts()); find_hosts(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> [find_host(ServerHost)]; false -> [] end. mod_options(_) -> []. mod_doc() -> #{desc => [?T("This module provides commands to administer local MUC " "services and their MUC rooms. It also provides simple " "WebAdmin pages to view the existing rooms."), "", ?T("This module depends on 'mod_muc'.")]}.