mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-22 16:20:52 +01:00
a5adaf5798
This reverts commit b81250d667
.
4502 lines
142 KiB
Erlang
4502 lines
142 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : mod_muc_room.erl
|
|
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
%%% Purpose : MUC room stuff
|
|
%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2015 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_room).
|
|
|
|
-author('alexey@process-one.net').
|
|
|
|
-behaviour(gen_fsm).
|
|
|
|
%% External exports
|
|
-export([start_link/9,
|
|
start_link/7,
|
|
start/9,
|
|
start/7,
|
|
route/4]).
|
|
|
|
%% gen_fsm callbacks
|
|
-export([init/1,
|
|
normal_state/2,
|
|
handle_event/3,
|
|
handle_sync_event/4,
|
|
handle_info/3,
|
|
terminate/3,
|
|
code_change/4]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("logger.hrl").
|
|
|
|
-include("jlib.hrl").
|
|
|
|
-include("mod_muc_room.hrl").
|
|
|
|
-define(MAX_USERS_DEFAULT_LIST,
|
|
[5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
|
|
|
|
%-define(DBGFSM, true).
|
|
|
|
-ifdef(DBGFSM).
|
|
|
|
-define(FSMOPTS, [{debug, [trace]}]).
|
|
|
|
-else.
|
|
|
|
-define(FSMOPTS, []).
|
|
|
|
-endif.
|
|
|
|
%% Module start with or without supervisor:
|
|
-ifdef(NO_TRANSIENT_SUPERVISORS).
|
|
-define(SUPERVISOR_START,
|
|
gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
|
|
RoomShaper, Creator, Nick, DefRoomOpts],
|
|
?FSMOPTS)).
|
|
-else.
|
|
-define(SUPERVISOR_START,
|
|
Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
|
|
supervisor:start_child(
|
|
Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper,
|
|
Creator, Nick, DefRoomOpts])).
|
|
-endif.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% API
|
|
%%%----------------------------------------------------------------------
|
|
start(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
|
|
Creator, Nick, DefRoomOpts) ->
|
|
?SUPERVISOR_START.
|
|
|
|
start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
|
|
Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup),
|
|
supervisor:start_child(
|
|
Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper,
|
|
Opts]).
|
|
|
|
start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper,
|
|
Creator, Nick, DefRoomOpts) ->
|
|
gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
|
|
RoomShaper, Creator, Nick, DefRoomOpts],
|
|
?FSMOPTS).
|
|
|
|
start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) ->
|
|
gen_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize,
|
|
RoomShaper, Opts],
|
|
?FSMOPTS).
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Callback functions from gen_fsm
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Func: init/1
|
|
%% Returns: {ok, StateName, StateData} |
|
|
%% {ok, StateName, StateData, Timeout} |
|
|
%% ignore |
|
|
%% {stop, StopReason}
|
|
%%----------------------------------------------------------------------
|
|
init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) ->
|
|
process_flag(trap_exit, true),
|
|
Shaper = shaper:new(RoomShaper),
|
|
State = set_affiliation(Creator, owner,
|
|
#state{host = Host, server_host = ServerHost,
|
|
access = Access, room = Room,
|
|
history = lqueue_new(HistorySize),
|
|
jid = jlib:make_jid(Room, Host, <<"">>),
|
|
just_created = true,
|
|
room_shaper = Shaper}),
|
|
State1 = set_opts(DefRoomOpts, State),
|
|
if (State1#state.config)#config.persistent ->
|
|
mod_muc:store_room(State1#state.server_host,
|
|
State1#state.host,
|
|
State1#state.room,
|
|
make_opts(State1));
|
|
true -> ok
|
|
end,
|
|
?INFO_MSG("Created MUC room ~s@~s by ~s",
|
|
[Room, Host, jlib:jid_to_string(Creator)]),
|
|
add_to_log(room_existence, created, State1),
|
|
add_to_log(room_existence, started, State1),
|
|
{ok, normal_state, State1};
|
|
init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) ->
|
|
process_flag(trap_exit, true),
|
|
Shaper = shaper:new(RoomShaper),
|
|
State = set_opts(Opts, #state{host = Host,
|
|
server_host = ServerHost,
|
|
access = Access,
|
|
room = Room,
|
|
history = lqueue_new(HistorySize),
|
|
jid = jlib:make_jid(Room, Host, <<"">>),
|
|
room_shaper = Shaper}),
|
|
add_to_log(room_existence, started, State),
|
|
{ok, normal_state, State}.
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Func: StateName/2
|
|
%% Returns: {next_state, NextStateName, NextStateData} |
|
|
%% {next_state, NextStateName, NextStateData, Timeout} |
|
|
%% {stop, Reason, NewStateData}
|
|
%%----------------------------------------------------------------------
|
|
normal_state({route, From, <<"">>,
|
|
#xmlel{name = <<"message">>, attrs = Attrs,
|
|
children = Els} =
|
|
Packet},
|
|
StateData) ->
|
|
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
case is_user_online(From, StateData) orelse
|
|
is_user_allowed_message_nonparticipant(From, StateData)
|
|
of
|
|
true ->
|
|
case xml:get_attr_s(<<"type">>, Attrs) of
|
|
<<"groupchat">> ->
|
|
Activity = get_user_activity(From, StateData),
|
|
Now = now_to_usec(now()),
|
|
MinMessageInterval =
|
|
trunc(gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, min_message_interval, fun(MMI) when is_number(MMI) -> MMI end, 0)
|
|
* 1000000),
|
|
Size = element_size(Packet),
|
|
{MessageShaper, MessageShaperInterval} =
|
|
shaper:update(Activity#activity.message_shaper, Size),
|
|
if Activity#activity.message /= undefined ->
|
|
ErrText = <<"Traffic rate limit is exceeded">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_RESOURCE_CONSTRAINT(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData};
|
|
Now >=
|
|
Activity#activity.message_time + MinMessageInterval,
|
|
MessageShaperInterval == 0 ->
|
|
{RoomShaper, RoomShaperInterval} =
|
|
shaper:update(StateData#state.room_shaper, Size),
|
|
RoomQueueEmpty =
|
|
queue:is_empty(StateData#state.room_queue),
|
|
if RoomShaperInterval == 0, RoomQueueEmpty ->
|
|
NewActivity = Activity#activity{message_time =
|
|
Now,
|
|
message_shaper =
|
|
MessageShaper},
|
|
StateData1 = store_user_activity(From,
|
|
NewActivity,
|
|
StateData),
|
|
StateData2 = StateData1#state{room_shaper =
|
|
RoomShaper},
|
|
process_groupchat_message(From, Packet,
|
|
StateData2);
|
|
true ->
|
|
StateData1 = if RoomQueueEmpty ->
|
|
erlang:send_after(RoomShaperInterval,
|
|
self(),
|
|
process_room_queue),
|
|
StateData#state{room_shaper =
|
|
RoomShaper};
|
|
true -> StateData
|
|
end,
|
|
NewActivity = Activity#activity{message_time =
|
|
Now,
|
|
message_shaper =
|
|
MessageShaper,
|
|
message = Packet},
|
|
RoomQueue = queue:in({message, From},
|
|
StateData#state.room_queue),
|
|
StateData2 = store_user_activity(From,
|
|
NewActivity,
|
|
StateData1),
|
|
StateData3 = StateData2#state{room_queue =
|
|
RoomQueue},
|
|
{next_state, normal_state, StateData3}
|
|
end;
|
|
true ->
|
|
MessageInterval = (Activity#activity.message_time +
|
|
MinMessageInterval
|
|
- Now)
|
|
div 1000,
|
|
Interval = lists:max([MessageInterval,
|
|
MessageShaperInterval]),
|
|
erlang:send_after(Interval, self(),
|
|
{process_user_message, From}),
|
|
NewActivity = Activity#activity{message = Packet,
|
|
message_shaper =
|
|
MessageShaper},
|
|
StateData1 = store_user_activity(From, NewActivity,
|
|
StateData),
|
|
{next_state, normal_state, StateData1}
|
|
end;
|
|
<<"error">> ->
|
|
case is_user_online(From, StateData) of
|
|
true ->
|
|
ErrorText = <<"This participant is kicked from the "
|
|
"room because he sent an error message">>,
|
|
NewState = expulse_participant(Packet, From, StateData,
|
|
translate:translate(Lang,
|
|
ErrorText)),
|
|
close_room_if_temporary_and_empty(NewState);
|
|
_ -> {next_state, normal_state, StateData}
|
|
end;
|
|
<<"chat">> ->
|
|
ErrText =
|
|
<<"It is not allowed to send private messages "
|
|
"to the conference">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData};
|
|
Type when (Type == <<"">>) or (Type == <<"normal">>) ->
|
|
IsInvitation = is_invitation(Els),
|
|
IsVoiceRequest = is_voice_request(Els) and
|
|
is_visitor(From, StateData),
|
|
IsVoiceApprovement = is_voice_approvement(Els) and
|
|
not is_visitor(From, StateData),
|
|
if IsInvitation ->
|
|
case catch check_invitation(From, Els, Lang, StateData)
|
|
of
|
|
{error, Error} ->
|
|
Err = jlib:make_error_reply(Packet, Error),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData};
|
|
IJID ->
|
|
Config = StateData#state.config,
|
|
case Config#config.members_only of
|
|
true ->
|
|
case get_affiliation(IJID, StateData) of
|
|
none ->
|
|
NSD = set_affiliation(IJID, member,
|
|
StateData),
|
|
case
|
|
(NSD#state.config)#config.persistent
|
|
of
|
|
true ->
|
|
mod_muc:store_room(NSD#state.server_host,
|
|
NSD#state.host,
|
|
NSD#state.room,
|
|
make_opts(NSD));
|
|
_ -> ok
|
|
end,
|
|
{next_state, normal_state, NSD};
|
|
_ -> {next_state, normal_state, StateData}
|
|
end;
|
|
false -> {next_state, normal_state, StateData}
|
|
end
|
|
end;
|
|
IsVoiceRequest ->
|
|
NewStateData = case
|
|
(StateData#state.config)#config.allow_voice_requests
|
|
of
|
|
true ->
|
|
MinInterval =
|
|
(StateData#state.config)#config.voice_request_min_interval,
|
|
BareFrom =
|
|
jlib:jid_remove_resource(jlib:jid_tolower(From)),
|
|
NowPriority = -now_to_usec(now()),
|
|
CleanPriority = NowPriority +
|
|
MinInterval *
|
|
1000000,
|
|
Times =
|
|
clean_treap(StateData#state.last_voice_request_time,
|
|
CleanPriority),
|
|
case treap:lookup(BareFrom, Times)
|
|
of
|
|
error ->
|
|
Times1 =
|
|
treap:insert(BareFrom,
|
|
NowPriority,
|
|
true, Times),
|
|
NSD =
|
|
StateData#state{last_voice_request_time
|
|
=
|
|
Times1},
|
|
send_voice_request(From, NSD),
|
|
NSD;
|
|
{ok, _, _} ->
|
|
ErrText =
|
|
<<"Please, wait for a while before sending "
|
|
"new voice request">>,
|
|
Err =
|
|
jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid,
|
|
From, Err),
|
|
StateData#state{last_voice_request_time
|
|
= Times}
|
|
end;
|
|
false ->
|
|
ErrText =
|
|
<<"Voice requests are disabled in this "
|
|
"conference">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_FORBIDDEN(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid,
|
|
From, Err),
|
|
StateData
|
|
end,
|
|
{next_state, normal_state, NewStateData};
|
|
IsVoiceApprovement ->
|
|
NewStateData = case is_moderator(From, StateData) of
|
|
true ->
|
|
case
|
|
extract_jid_from_voice_approvement(Els)
|
|
of
|
|
error ->
|
|
ErrText =
|
|
<<"Failed to extract JID from your voice "
|
|
"request approval">>,
|
|
Err =
|
|
jlib:make_error_reply(Packet,
|
|
?ERRT_BAD_REQUEST(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid,
|
|
From, Err),
|
|
StateData;
|
|
{ok, TargetJid} ->
|
|
case is_visitor(TargetJid,
|
|
StateData)
|
|
of
|
|
true ->
|
|
Reason = <<>>,
|
|
NSD =
|
|
set_role(TargetJid,
|
|
participant,
|
|
StateData),
|
|
catch
|
|
send_new_presence(TargetJid,
|
|
Reason,
|
|
NSD),
|
|
NSD;
|
|
_ -> StateData
|
|
end
|
|
end;
|
|
_ ->
|
|
ErrText =
|
|
<<"Only moderators can approve voice requests">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ALLOWED(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid,
|
|
From, Err),
|
|
StateData
|
|
end,
|
|
{next_state, normal_state, NewStateData};
|
|
true -> {next_state, normal_state, StateData}
|
|
end;
|
|
_ ->
|
|
ErrText = <<"Improper message type">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData}
|
|
end;
|
|
_ ->
|
|
case xml:get_attr_s(<<"type">>, Attrs) of
|
|
<<"error">> -> ok;
|
|
_ ->
|
|
handle_roommessage_from_nonparticipant(Packet, Lang,
|
|
StateData, From)
|
|
end,
|
|
{next_state, normal_state, StateData}
|
|
end;
|
|
normal_state({route, From, <<"">>,
|
|
#xmlel{name = <<"iq">>} = Packet},
|
|
StateData) ->
|
|
case jlib:iq_query_info(Packet) of
|
|
#iq{type = Type, xmlns = XMLNS, lang = Lang,
|
|
sub_el = #xmlel{name = SubElName} = SubEl} =
|
|
IQ
|
|
when (XMLNS == (?NS_MUC_ADMIN)) or
|
|
(XMLNS == (?NS_MUC_OWNER))
|
|
or (XMLNS == (?NS_DISCO_INFO))
|
|
or (XMLNS == (?NS_DISCO_ITEMS))
|
|
or (XMLNS == (?NS_VCARD))
|
|
or (XMLNS == (?NS_CAPTCHA)) ->
|
|
Res1 = case XMLNS of
|
|
?NS_MUC_ADMIN ->
|
|
process_iq_admin(From, Type, Lang, SubEl, StateData);
|
|
?NS_MUC_OWNER ->
|
|
process_iq_owner(From, Type, Lang, SubEl, StateData);
|
|
?NS_DISCO_INFO ->
|
|
process_iq_disco_info(From, Type, Lang, StateData);
|
|
?NS_DISCO_ITEMS ->
|
|
process_iq_disco_items(From, Type, Lang, StateData);
|
|
?NS_VCARD ->
|
|
process_iq_vcard(From, Type, Lang, SubEl, StateData);
|
|
?NS_CAPTCHA ->
|
|
process_iq_captcha(From, Type, Lang, SubEl, StateData)
|
|
end,
|
|
{IQRes, NewStateData} = case Res1 of
|
|
{result, Res, SD} ->
|
|
{IQ#iq{type = result,
|
|
sub_el =
|
|
[#xmlel{name = SubElName,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
XMLNS}],
|
|
children = Res}]},
|
|
SD};
|
|
{error, Error} ->
|
|
{IQ#iq{type = error,
|
|
sub_el = [SubEl, Error]},
|
|
StateData}
|
|
end,
|
|
ejabberd_router:route(StateData#state.jid, From,
|
|
jlib:iq_to_xml(IQRes)),
|
|
case NewStateData of
|
|
stop -> {stop, normal, StateData};
|
|
_ -> {next_state, normal_state, NewStateData}
|
|
end;
|
|
reply -> {next_state, normal_state, StateData};
|
|
_ ->
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERR_FEATURE_NOT_IMPLEMENTED),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData}
|
|
end;
|
|
normal_state({route, From, Nick,
|
|
#xmlel{name = <<"presence">>} = Packet},
|
|
StateData) ->
|
|
Activity = get_user_activity(From, StateData),
|
|
Now = now_to_usec(now()),
|
|
MinPresenceInterval =
|
|
trunc(gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, min_presence_interval,
|
|
fun(I) when is_number(I), I>=0 ->
|
|
I
|
|
end, 0)
|
|
* 1000000),
|
|
if (Now >=
|
|
Activity#activity.presence_time + MinPresenceInterval)
|
|
and (Activity#activity.presence == undefined) ->
|
|
NewActivity = Activity#activity{presence_time = Now},
|
|
StateData1 = store_user_activity(From, NewActivity,
|
|
StateData),
|
|
process_presence(From, Nick, Packet, StateData1);
|
|
true ->
|
|
if Activity#activity.presence == undefined ->
|
|
Interval = (Activity#activity.presence_time +
|
|
MinPresenceInterval
|
|
- Now)
|
|
div 1000,
|
|
erlang:send_after(Interval, self(),
|
|
{process_user_presence, From});
|
|
true -> ok
|
|
end,
|
|
NewActivity = Activity#activity{presence =
|
|
{Nick, Packet}},
|
|
StateData1 = store_user_activity(From, NewActivity,
|
|
StateData),
|
|
{next_state, normal_state, StateData1}
|
|
end;
|
|
normal_state({route, From, ToNick,
|
|
#xmlel{name = <<"message">>, attrs = Attrs} = Packet},
|
|
StateData) ->
|
|
Type = xml:get_attr_s(<<"type">>, Attrs),
|
|
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
case decide_fate_message(Type, Packet, From, StateData)
|
|
of
|
|
{expulse_sender, Reason} ->
|
|
?DEBUG(Reason, []),
|
|
ErrorText = <<"This participant is kicked from the "
|
|
"room because he sent an error message "
|
|
"to another participant">>,
|
|
NewState = expulse_participant(Packet, From, StateData,
|
|
translate:translate(Lang, ErrorText)),
|
|
{next_state, normal_state, NewState};
|
|
forget_message -> {next_state, normal_state, StateData};
|
|
continue_delivery ->
|
|
case
|
|
{(StateData#state.config)#config.allow_private_messages,
|
|
is_user_online(From, StateData)}
|
|
of
|
|
{true, true} ->
|
|
case Type of
|
|
<<"groupchat">> ->
|
|
ErrText =
|
|
<<"It is not allowed to send private messages "
|
|
"of type \"groupchat\"">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_BAD_REQUEST(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err);
|
|
_ ->
|
|
case find_jids_by_nick(ToNick, StateData) of
|
|
false ->
|
|
ErrText =
|
|
<<"Recipient is not in the conference room">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_ITEM_NOT_FOUND(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err);
|
|
ToJIDs ->
|
|
SrcIsVisitor = is_visitor(From, StateData),
|
|
DstIsModerator = is_moderator(hd(ToJIDs),
|
|
StateData),
|
|
PmFromVisitors =
|
|
(StateData#state.config)#config.allow_private_messages_from_visitors,
|
|
if SrcIsVisitor == false;
|
|
PmFromVisitors == anyone;
|
|
(PmFromVisitors == moderators) and
|
|
DstIsModerator ->
|
|
{ok, #user{nick = FromNick}} =
|
|
(?DICT):find(jlib:jid_tolower(From),
|
|
StateData#state.users),
|
|
FromNickJID =
|
|
jlib:jid_replace_resource(StateData#state.jid,
|
|
FromNick),
|
|
[ejabberd_router:route(FromNickJID, ToJID, Packet)
|
|
|| ToJID <- ToJIDs];
|
|
true ->
|
|
ErrText =
|
|
<<"It is not allowed to send private messages">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_FORBIDDEN(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err)
|
|
end
|
|
end
|
|
end;
|
|
{true, false} ->
|
|
ErrText =
|
|
<<"Only occupants are allowed to send messages "
|
|
"to the conference">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err);
|
|
{false, _} ->
|
|
ErrText =
|
|
<<"It is not allowed to send private messages">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_FORBIDDEN(Lang, ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err)
|
|
end,
|
|
{next_state, normal_state, StateData}
|
|
end;
|
|
normal_state({route, From, ToNick,
|
|
#xmlel{name = <<"iq">>, attrs = Attrs} = Packet},
|
|
StateData) ->
|
|
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
StanzaId = xml:get_attr_s(<<"id">>, Attrs),
|
|
case {(StateData#state.config)#config.allow_query_users,
|
|
is_user_online_iq(StanzaId, From, StateData)}
|
|
of
|
|
{true, {true, NewId, FromFull}} ->
|
|
case find_jid_by_nick(ToNick, StateData) of
|
|
false ->
|
|
case jlib:iq_query_info(Packet) of
|
|
reply -> ok;
|
|
_ ->
|
|
ErrText = <<"Recipient is not in the conference room">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_ITEM_NOT_FOUND(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err)
|
|
end;
|
|
ToJID ->
|
|
{ok, #user{nick = FromNick}} =
|
|
(?DICT):find(jlib:jid_tolower(FromFull),
|
|
StateData#state.users),
|
|
{ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID,
|
|
StanzaId, NewId, Packet),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
FromNick),
|
|
ToJID2, Packet2)
|
|
end;
|
|
{_, {false, _, _}} ->
|
|
case jlib:iq_query_info(Packet) of
|
|
reply -> ok;
|
|
_ ->
|
|
ErrText =
|
|
<<"Only occupants are allowed to send queries "
|
|
"to the conference">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err)
|
|
end;
|
|
_ ->
|
|
case jlib:iq_query_info(Packet) of
|
|
reply -> ok;
|
|
_ ->
|
|
ErrText = <<"Queries to the conference members are "
|
|
"not allowed in this room">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ALLOWED(Lang, ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
ToNick),
|
|
From, Err)
|
|
end
|
|
end,
|
|
{next_state, normal_state, StateData};
|
|
normal_state(_Event, StateData) ->
|
|
{next_state, normal_state, StateData}.
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Func: handle_event/3
|
|
%% Returns: {next_state, NextStateName, NextStateData} |
|
|
%% {next_state, NextStateName, NextStateData, Timeout} |
|
|
%% {stop, Reason, NewStateData}
|
|
%%----------------------------------------------------------------------
|
|
handle_event({service_message, Msg}, _StateName,
|
|
StateData) ->
|
|
MessagePkt = #xmlel{name = <<"message">>,
|
|
attrs = [{<<"type">>, <<"groupchat">>}],
|
|
children =
|
|
[#xmlel{name = <<"body">>, attrs = [],
|
|
children = [{xmlcdata, Msg}]}]},
|
|
send_multiple(
|
|
StateData#state.jid,
|
|
StateData#state.server_host,
|
|
StateData#state.users,
|
|
MessagePkt),
|
|
NSD = add_message_to_history(<<"">>,
|
|
StateData#state.jid, MessagePkt, StateData),
|
|
{next_state, normal_state, NSD};
|
|
handle_event({destroy, Reason}, _StateName,
|
|
StateData) ->
|
|
{result, [], stop} = destroy_room(#xmlel{name =
|
|
<<"destroy">>,
|
|
attrs =
|
|
[{<<"xmlns">>, ?NS_MUC_OWNER}],
|
|
children =
|
|
case Reason of
|
|
none -> [];
|
|
_Else ->
|
|
[#xmlel{name =
|
|
<<"reason">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
Reason}]}]
|
|
end},
|
|
StateData),
|
|
?INFO_MSG("Destroyed MUC room ~s with reason: ~p",
|
|
[jlib:jid_to_string(StateData#state.jid), Reason]),
|
|
add_to_log(room_existence, destroyed, StateData),
|
|
{stop, shutdown, StateData};
|
|
handle_event(destroy, StateName, StateData) ->
|
|
?INFO_MSG("Destroyed MUC room ~s",
|
|
[jlib:jid_to_string(StateData#state.jid)]),
|
|
handle_event({destroy, none}, StateName, StateData);
|
|
handle_event({set_affiliations, Affiliations},
|
|
StateName, StateData) ->
|
|
{next_state, StateName,
|
|
StateData#state{affiliations = Affiliations}};
|
|
handle_event(_Event, StateName, StateData) ->
|
|
{next_state, StateName, StateData}.
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Func: handle_sync_event/4
|
|
%% Returns: {next_state, NextStateName, NextStateData} |
|
|
%% {next_state, NextStateName, NextStateData, Timeout} |
|
|
%% {reply, Reply, NextStateName, NextStateData} |
|
|
%% {reply, Reply, NextStateName, NextStateData, Timeout} |
|
|
%% {stop, Reason, NewStateData} |
|
|
%% {stop, Reason, Reply, NewStateData}
|
|
%%----------------------------------------------------------------------
|
|
handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) ->
|
|
Reply = get_roomdesc_reply(JID, StateData,
|
|
get_roomdesc_tail(StateData, Lang)),
|
|
{reply, Reply, StateName, StateData};
|
|
handle_sync_event(get_config, _From, StateName,
|
|
StateData) ->
|
|
{reply, {ok, StateData#state.config}, StateName,
|
|
StateData};
|
|
handle_sync_event(get_state, _From, StateName,
|
|
StateData) ->
|
|
{reply, {ok, StateData}, StateName, StateData};
|
|
handle_sync_event({change_config, Config}, _From,
|
|
StateName, StateData) ->
|
|
{result, [], NSD} = change_config(Config, StateData),
|
|
{reply, {ok, NSD#state.config}, StateName, NSD};
|
|
handle_sync_event({change_state, NewStateData}, _From,
|
|
StateName, _StateData) ->
|
|
{reply, {ok, NewStateData}, StateName, NewStateData};
|
|
handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
|
|
NSD = process_item_change(Item, StateData, UJID),
|
|
{reply, {ok, NSD}, StateName, NSD};
|
|
handle_sync_event(_Event, _From, StateName,
|
|
StateData) ->
|
|
Reply = ok, {reply, Reply, StateName, StateData}.
|
|
|
|
code_change(_OldVsn, StateName, StateData, _Extra) ->
|
|
{ok, StateName, StateData}.
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Func: handle_info/3
|
|
%% Returns: {next_state, NextStateName, NextStateData} |
|
|
%% {next_state, NextStateName, NextStateData, Timeout} |
|
|
%% {stop, Reason, NewStateData}
|
|
%%----------------------------------------------------------------------
|
|
handle_info({process_user_presence, From}, normal_state = _StateName, StateData) ->
|
|
RoomQueueEmpty = queue:is_empty(StateData#state.room_queue),
|
|
RoomQueue = queue:in({presence, From}, StateData#state.room_queue),
|
|
StateData1 = StateData#state{room_queue = RoomQueue},
|
|
if RoomQueueEmpty ->
|
|
StateData2 = prepare_room_queue(StateData1),
|
|
{next_state, normal_state, StateData2};
|
|
true -> {next_state, normal_state, StateData1}
|
|
end;
|
|
handle_info({process_user_message, From},
|
|
normal_state = _StateName, StateData) ->
|
|
RoomQueueEmpty =
|
|
queue:is_empty(StateData#state.room_queue),
|
|
RoomQueue = queue:in({message, From},
|
|
StateData#state.room_queue),
|
|
StateData1 = StateData#state{room_queue = RoomQueue},
|
|
if RoomQueueEmpty ->
|
|
StateData2 = prepare_room_queue(StateData1),
|
|
{next_state, normal_state, StateData2};
|
|
true -> {next_state, normal_state, StateData1}
|
|
end;
|
|
handle_info(process_room_queue,
|
|
normal_state = StateName, StateData) ->
|
|
case queue:out(StateData#state.room_queue) of
|
|
{{value, {message, From}}, RoomQueue} ->
|
|
Activity = get_user_activity(From, StateData),
|
|
Packet = Activity#activity.message,
|
|
NewActivity = Activity#activity{message = undefined},
|
|
StateData1 = store_user_activity(From, NewActivity,
|
|
StateData),
|
|
StateData2 = StateData1#state{room_queue = RoomQueue},
|
|
StateData3 = prepare_room_queue(StateData2),
|
|
process_groupchat_message(From, Packet, StateData3);
|
|
{{value, {presence, From}}, RoomQueue} ->
|
|
Activity = get_user_activity(From, StateData),
|
|
{Nick, Packet} = Activity#activity.presence,
|
|
NewActivity = Activity#activity{presence = undefined},
|
|
StateData1 = store_user_activity(From, NewActivity,
|
|
StateData),
|
|
StateData2 = StateData1#state{room_queue = RoomQueue},
|
|
StateData3 = prepare_room_queue(StateData2),
|
|
process_presence(From, Nick, Packet, StateData3);
|
|
{empty, _} -> {next_state, StateName, StateData}
|
|
end;
|
|
handle_info({captcha_succeed, From}, normal_state,
|
|
StateData) ->
|
|
NewState = case (?DICT):find(From,
|
|
StateData#state.robots)
|
|
of
|
|
{ok, {Nick, Packet}} ->
|
|
Robots = (?DICT):store(From, passed,
|
|
StateData#state.robots),
|
|
add_new_user(From, Nick, Packet,
|
|
StateData#state{robots = Robots});
|
|
_ -> StateData
|
|
end,
|
|
{next_state, normal_state, NewState};
|
|
handle_info({captcha_failed, From}, normal_state,
|
|
StateData) ->
|
|
NewState = case (?DICT):find(From,
|
|
StateData#state.robots)
|
|
of
|
|
{ok, {Nick, Packet}} ->
|
|
Robots = (?DICT):erase(From, StateData#state.robots),
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERR_NOT_AUTHORIZED),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData#state{robots = Robots};
|
|
_ -> StateData
|
|
end,
|
|
{next_state, normal_state, NewState};
|
|
handle_info(shutdown, _StateName, StateData) ->
|
|
{stop, shutdown, StateData};
|
|
handle_info(_Info, StateName, StateData) ->
|
|
{next_state, StateName, StateData}.
|
|
|
|
%%----------------------------------------------------------------------
|
|
%% Func: terminate/3
|
|
%% Purpose: Shutdown the fsm
|
|
%% Returns: any
|
|
%%----------------------------------------------------------------------
|
|
terminate(Reason, _StateName, StateData) ->
|
|
?INFO_MSG("Stopping MUC room ~s@~s",
|
|
[StateData#state.room, StateData#state.host]),
|
|
ReasonT = case Reason of
|
|
shutdown ->
|
|
<<"You are being removed from the room "
|
|
"because of a system shutdown">>;
|
|
_ -> <<"Room terminates">>
|
|
end,
|
|
ItemAttrs = [{<<"affiliation">>, <<"none">>},
|
|
{<<"role">>, <<"none">>}],
|
|
ReasonEl = #xmlel{name = <<"reason">>, attrs = [],
|
|
children = [{xmlcdata, ReasonT}]},
|
|
Packet = #xmlel{name = <<"presence">>,
|
|
attrs = [{<<"type">>, <<"unavailable">>}],
|
|
children =
|
|
[#xmlel{name = <<"x">>,
|
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name = <<"item">>,
|
|
attrs = ItemAttrs,
|
|
children = [ReasonEl]},
|
|
#xmlel{name = <<"status">>,
|
|
attrs = [{<<"code">>, <<"332">>}],
|
|
children = []}]}]},
|
|
(?DICT):fold(fun (LJID, Info, _) ->
|
|
Nick = Info#user.nick,
|
|
case Reason of
|
|
shutdown ->
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
Info#user.jid, Packet);
|
|
_ -> ok
|
|
end,
|
|
tab_remove_online_user(LJID, StateData)
|
|
end,
|
|
[], StateData#state.users),
|
|
add_to_log(room_existence, stopped, StateData),
|
|
mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(),
|
|
StateData#state.server_host),
|
|
ok.
|
|
|
|
%%%----------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%%----------------------------------------------------------------------
|
|
|
|
route(Pid, From, ToNick, Packet) ->
|
|
gen_fsm:send_event(Pid, {route, From, ToNick, Packet}).
|
|
|
|
process_groupchat_message(From,
|
|
#xmlel{name = <<"message">>, attrs = Attrs} = Packet,
|
|
StateData) ->
|
|
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
case is_user_online(From, StateData) orelse
|
|
is_user_allowed_message_nonparticipant(From, StateData)
|
|
of
|
|
true ->
|
|
{FromNick, Role} = get_participant_data(From,
|
|
StateData),
|
|
if (Role == moderator) or (Role == participant) or
|
|
((StateData#state.config)#config.moderated == false) ->
|
|
{NewStateData1, IsAllowed} = case check_subject(Packet)
|
|
of
|
|
false -> {StateData, true};
|
|
Subject ->
|
|
case
|
|
can_change_subject(Role,
|
|
StateData)
|
|
of
|
|
true ->
|
|
NSD =
|
|
StateData#state{subject
|
|
=
|
|
Subject,
|
|
subject_author
|
|
=
|
|
FromNick},
|
|
case
|
|
(NSD#state.config)#config.persistent
|
|
of
|
|
true ->
|
|
mod_muc:store_room(NSD#state.server_host,
|
|
NSD#state.host,
|
|
NSD#state.room,
|
|
make_opts(NSD));
|
|
_ -> ok
|
|
end,
|
|
{NSD, true};
|
|
_ -> {StateData, false}
|
|
end
|
|
end,
|
|
case IsAllowed of
|
|
true ->
|
|
send_multiple(
|
|
jlib:jid_replace_resource(StateData#state.jid, FromNick),
|
|
StateData#state.server_host,
|
|
StateData#state.users,
|
|
Packet),
|
|
NewStateData2 = case has_body_or_subject(Packet) of
|
|
true ->
|
|
add_message_to_history(FromNick, From,
|
|
Packet,
|
|
NewStateData1);
|
|
false ->
|
|
NewStateData1
|
|
end,
|
|
{next_state, normal_state, NewStateData2};
|
|
_ ->
|
|
Err = case
|
|
(StateData#state.config)#config.allow_change_subj
|
|
of
|
|
true ->
|
|
?ERRT_FORBIDDEN(Lang,
|
|
<<"Only moderators and participants are "
|
|
"allowed to change the subject in this "
|
|
"room">>);
|
|
_ ->
|
|
?ERRT_FORBIDDEN(Lang,
|
|
<<"Only moderators are allowed to change "
|
|
"the subject in this room">>)
|
|
end,
|
|
ejabberd_router:route(StateData#state.jid, From,
|
|
jlib:make_error_reply(Packet, Err)),
|
|
{next_state, normal_state, StateData}
|
|
end;
|
|
true ->
|
|
ErrText = <<"Visitors are not allowed to send messages "
|
|
"to all occupants">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_FORBIDDEN(Lang, ErrText)),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData}
|
|
end;
|
|
false ->
|
|
ErrText =
|
|
<<"Only occupants are allowed to send messages "
|
|
"to the conference">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
|
|
ejabberd_router:route(StateData#state.jid, From, Err),
|
|
{next_state, normal_state, StateData}
|
|
end.
|
|
|
|
%% @doc Check if this non participant can send message to room.
|
|
%%
|
|
%% XEP-0045 v1.23:
|
|
%% 7.9 Sending a Message to All Occupants
|
|
%% an implementation MAY allow users with certain privileges
|
|
%% (e.g., a room owner, room admin, or service-level admin)
|
|
%% to send messages to the room even if those users are not occupants.
|
|
is_user_allowed_message_nonparticipant(JID,
|
|
StateData) ->
|
|
case get_service_affiliation(JID, StateData) of
|
|
owner -> true;
|
|
_ -> false
|
|
end.
|
|
|
|
%% @doc Get information of this participant, or default values.
|
|
%% If the JID is not a participant, return values for a service message.
|
|
get_participant_data(From, StateData) ->
|
|
case (?DICT):find(jlib:jid_tolower(From),
|
|
StateData#state.users)
|
|
of
|
|
{ok, #user{nick = FromNick, role = Role}} ->
|
|
{FromNick, Role};
|
|
error -> {<<"">>, moderator}
|
|
end.
|
|
|
|
process_presence(From, Nick,
|
|
#xmlel{name = <<"presence">>, attrs = Attrs} = Packet,
|
|
StateData) ->
|
|
Type = xml:get_attr_s(<<"type">>, Attrs),
|
|
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
StateData1 = case Type of
|
|
<<"unavailable">> ->
|
|
case is_user_online(From, StateData) of
|
|
true ->
|
|
NewPacket = case
|
|
{(StateData#state.config)#config.allow_visitor_status,
|
|
is_visitor(From, StateData)}
|
|
of
|
|
{false, true} ->
|
|
strip_status(Packet);
|
|
_ -> Packet
|
|
end,
|
|
NewState = add_user_presence_un(From, NewPacket,
|
|
StateData),
|
|
case (?DICT):find(Nick, StateData#state.nicks) of
|
|
{ok, [_, _ | _]} -> ok;
|
|
_ -> send_new_presence(From, NewState)
|
|
end,
|
|
Reason = case xml:get_subtag(NewPacket,
|
|
<<"status">>)
|
|
of
|
|
false -> <<"">>;
|
|
Status_el ->
|
|
xml:get_tag_cdata(Status_el)
|
|
end,
|
|
remove_online_user(From, NewState, Reason);
|
|
_ -> StateData
|
|
end;
|
|
<<"error">> ->
|
|
case is_user_online(From, StateData) of
|
|
true ->
|
|
ErrorText =
|
|
<<"This participant is kicked from the "
|
|
"room because he sent an error presence">>,
|
|
expulse_participant(Packet, From, StateData,
|
|
translate:translate(Lang,
|
|
ErrorText));
|
|
_ -> StateData
|
|
end;
|
|
<<"">> ->
|
|
case is_user_online(From, StateData) of
|
|
true ->
|
|
case is_nick_change(From, Nick, StateData) of
|
|
true ->
|
|
case {nick_collision(From, Nick, StateData),
|
|
mod_muc:can_use_nick(StateData#state.server_host,
|
|
StateData#state.host,
|
|
From, Nick),
|
|
{(StateData#state.config)#config.allow_visitor_nickchange,
|
|
is_visitor(From, StateData)}}
|
|
of
|
|
{_, _, {false, true}} ->
|
|
ErrText =
|
|
<<"Visitors are not allowed to change their "
|
|
"nicknames in this room">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ALLOWED(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData;
|
|
{true, _, _} ->
|
|
Lang = xml:get_attr_s(<<"xml:lang">>,
|
|
Attrs),
|
|
ErrText =
|
|
<<"That nickname is already in use by another "
|
|
"occupant">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_CONFLICT(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick), % TODO: s/Nick/""/
|
|
From, Err),
|
|
StateData;
|
|
{_, false, _} ->
|
|
ErrText =
|
|
<<"That nickname is registered by another "
|
|
"person">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_CONFLICT(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData;
|
|
_ -> change_nick(From, Nick, StateData)
|
|
end;
|
|
_NotNickChange ->
|
|
Stanza = case
|
|
{(StateData#state.config)#config.allow_visitor_status,
|
|
is_visitor(From, StateData)}
|
|
of
|
|
{false, true} ->
|
|
strip_status(Packet);
|
|
_Allowed -> Packet
|
|
end,
|
|
NewState = add_user_presence(From, Stanza,
|
|
StateData),
|
|
send_new_presence(From, NewState),
|
|
NewState
|
|
end;
|
|
_ -> add_new_user(From, Nick, Packet, StateData)
|
|
end;
|
|
_ -> StateData
|
|
end,
|
|
close_room_if_temporary_and_empty(StateData1).
|
|
|
|
close_room_if_temporary_and_empty(StateData1) ->
|
|
case not (StateData1#state.config)#config.persistent
|
|
andalso (?DICT):to_list(StateData1#state.users) == []
|
|
of
|
|
true ->
|
|
?INFO_MSG("Destroyed MUC room ~s because it's temporary "
|
|
"and empty",
|
|
[jlib:jid_to_string(StateData1#state.jid)]),
|
|
add_to_log(room_existence, destroyed, StateData1),
|
|
{stop, normal, StateData1};
|
|
_ -> {next_state, normal_state, StateData1}
|
|
end.
|
|
|
|
is_user_online(JID, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
(?DICT):is_key(LJID, StateData#state.users).
|
|
|
|
%% Check if the user is occupant of the room, or at least is an admin or owner.
|
|
is_occupant_or_admin(JID, StateData) ->
|
|
FAffiliation = get_affiliation(JID, StateData),
|
|
FRole = get_role(JID, StateData),
|
|
case FRole /= none orelse
|
|
FAffiliation == member orelse
|
|
FAffiliation == admin orelse FAffiliation == owner
|
|
of
|
|
true -> true;
|
|
_ -> false
|
|
end.
|
|
|
|
%%%
|
|
%%% Handle IQ queries of vCard
|
|
%%%
|
|
is_user_online_iq(StanzaId, JID, StateData)
|
|
when JID#jid.lresource /= <<"">> ->
|
|
{is_user_online(JID, StateData), StanzaId, JID};
|
|
is_user_online_iq(StanzaId, JID, StateData)
|
|
when JID#jid.lresource == <<"">> ->
|
|
try stanzaid_unpack(StanzaId) of
|
|
{OriginalId, Resource} ->
|
|
JIDWithResource = jlib:jid_replace_resource(JID,
|
|
Resource),
|
|
{is_user_online(JIDWithResource, StateData), OriginalId,
|
|
JIDWithResource}
|
|
catch
|
|
_:_ -> {is_user_online(JID, StateData), StanzaId, JID}
|
|
end.
|
|
|
|
handle_iq_vcard(FromFull, ToJID, StanzaId, NewId,
|
|
Packet) ->
|
|
ToBareJID = jlib:jid_remove_resource(ToJID),
|
|
IQ = jlib:iq_query_info(Packet),
|
|
handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId,
|
|
NewId, IQ, Packet).
|
|
|
|
handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId,
|
|
_NewId, #iq{type = get, xmlns = ?NS_VCARD}, Packet)
|
|
when ToBareJID /= ToJID ->
|
|
{ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)};
|
|
handle_iq_vcard2(_FromFull, ToJID, _ToBareJID,
|
|
_StanzaId, NewId, _IQ, Packet) ->
|
|
{ToJID, change_stanzaid(NewId, Packet)}.
|
|
|
|
stanzaid_pack(OriginalId, Resource) ->
|
|
<<"berd",
|
|
(jlib:encode_base64(<<"ejab\000",
|
|
OriginalId/binary, "\000",
|
|
Resource/binary>>))/binary>>.
|
|
|
|
stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) ->
|
|
StanzaId = jlib:decode_base64(StanzaIdBase64),
|
|
[<<"ejab">>, OriginalId, Resource] =
|
|
str:tokens(StanzaId, <<"\000">>),
|
|
{OriginalId, Resource}.
|
|
|
|
change_stanzaid(NewId, Packet) ->
|
|
#xmlel{name = Name, attrs = Attrs, children = Els} =
|
|
jlib:remove_attr(<<"id">>, Packet),
|
|
#xmlel{name = Name, attrs = [{<<"id">>, NewId} | Attrs],
|
|
children = Els}.
|
|
|
|
change_stanzaid(PreviousId, ToJID, Packet) ->
|
|
NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource),
|
|
change_stanzaid(NewId, Packet).
|
|
|
|
%%%
|
|
%%%
|
|
|
|
role_to_list(Role) ->
|
|
case Role of
|
|
moderator -> <<"moderator">>;
|
|
participant -> <<"participant">>;
|
|
visitor -> <<"visitor">>;
|
|
none -> <<"none">>
|
|
end.
|
|
|
|
affiliation_to_list(Affiliation) ->
|
|
case Affiliation of
|
|
owner -> <<"owner">>;
|
|
admin -> <<"admin">>;
|
|
member -> <<"member">>;
|
|
outcast -> <<"outcast">>;
|
|
none -> <<"none">>
|
|
end.
|
|
|
|
list_to_role(Role) ->
|
|
case Role of
|
|
<<"moderator">> -> moderator;
|
|
<<"participant">> -> participant;
|
|
<<"visitor">> -> visitor;
|
|
<<"none">> -> none
|
|
end.
|
|
|
|
list_to_affiliation(Affiliation) ->
|
|
case Affiliation of
|
|
<<"owner">> -> owner;
|
|
<<"admin">> -> admin;
|
|
<<"member">> -> member;
|
|
<<"outcast">> -> outcast;
|
|
<<"none">> -> none
|
|
end.
|
|
|
|
%% Decide the fate of the message and its sender
|
|
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
|
|
decide_fate_message(<<"error">>, Packet, From,
|
|
StateData) ->
|
|
PD = case check_error_kick(Packet) of
|
|
%% If this is an error stanza and its condition matches a criteria
|
|
true ->
|
|
Reason =
|
|
io_lib:format("This participant is considered a ghost "
|
|
"and is expulsed: ~s",
|
|
[jlib:jid_to_string(From)]),
|
|
{expulse_sender, Reason};
|
|
false -> continue_delivery
|
|
end,
|
|
case PD of
|
|
{expulse_sender, R} ->
|
|
case is_user_online(From, StateData) of
|
|
true -> {expulse_sender, R};
|
|
false -> forget_message
|
|
end;
|
|
Other -> Other
|
|
end;
|
|
decide_fate_message(_, _, _, _) -> continue_delivery.
|
|
|
|
%% Check if the elements of this error stanza indicate
|
|
%% that the sender is a dead participant.
|
|
%% If so, return true to kick the participant.
|
|
check_error_kick(Packet) ->
|
|
case get_error_condition(Packet) of
|
|
<<"gone">> -> true;
|
|
<<"internal-server-error">> -> true;
|
|
<<"item-not-found">> -> true;
|
|
<<"jid-malformed">> -> true;
|
|
<<"recipient-unavailable">> -> true;
|
|
<<"redirect">> -> true;
|
|
<<"remote-server-not-found">> -> true;
|
|
<<"remote-server-timeout">> -> true;
|
|
<<"service-unavailable">> -> true;
|
|
_ -> false
|
|
end.
|
|
|
|
get_error_condition(Packet) ->
|
|
case catch get_error_condition2(Packet) of
|
|
{condition, ErrorCondition} -> ErrorCondition;
|
|
{'EXIT', _} -> <<"badformed error stanza">>
|
|
end.
|
|
|
|
get_error_condition2(Packet) ->
|
|
#xmlel{children = EEls} = xml:get_subtag(Packet,
|
|
<<"error">>),
|
|
[Condition] = [Name
|
|
|| #xmlel{name = Name,
|
|
attrs = [{<<"xmlns">>, ?NS_STANZAS}],
|
|
children = []}
|
|
<- EEls],
|
|
{condition, Condition}.
|
|
|
|
expulse_participant(Packet, From, StateData, Reason1) ->
|
|
ErrorCondition = get_error_condition(Packet),
|
|
Reason2 = iolist_to_binary(
|
|
io_lib:format(binary_to_list(Reason1) ++ ": " ++ "~s",
|
|
[ErrorCondition])),
|
|
NewState = add_user_presence_un(From,
|
|
#xmlel{name = <<"presence">>,
|
|
attrs =
|
|
[{<<"type">>,
|
|
<<"unavailable">>}],
|
|
children =
|
|
[#xmlel{name = <<"status">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
Reason2}]}]},
|
|
StateData),
|
|
send_new_presence(From, NewState),
|
|
remove_online_user(From, NewState).
|
|
|
|
set_affiliation(JID, Affiliation, StateData) ->
|
|
set_affiliation(JID, Affiliation, StateData, <<"">>).
|
|
|
|
set_affiliation(JID, Affiliation, StateData, Reason) ->
|
|
LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)),
|
|
Affiliations = case Affiliation of
|
|
none ->
|
|
(?DICT):erase(LJID, StateData#state.affiliations);
|
|
_ ->
|
|
(?DICT):store(LJID, {Affiliation, Reason},
|
|
StateData#state.affiliations)
|
|
end,
|
|
StateData#state{affiliations = Affiliations}.
|
|
|
|
get_affiliation(JID, StateData) ->
|
|
{_AccessRoute, _AccessCreate, AccessAdmin,
|
|
_AccessPersistent} =
|
|
StateData#state.access,
|
|
Res = case acl:match_rule(StateData#state.server_host,
|
|
AccessAdmin, JID)
|
|
of
|
|
allow -> owner;
|
|
_ ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
case (?DICT):find(LJID, StateData#state.affiliations) of
|
|
{ok, Affiliation} -> Affiliation;
|
|
_ ->
|
|
LJID1 = jlib:jid_remove_resource(LJID),
|
|
case (?DICT):find(LJID1, StateData#state.affiliations)
|
|
of
|
|
{ok, Affiliation} -> Affiliation;
|
|
_ ->
|
|
LJID2 = setelement(1, LJID, <<"">>),
|
|
case (?DICT):find(LJID2,
|
|
StateData#state.affiliations)
|
|
of
|
|
{ok, Affiliation} -> Affiliation;
|
|
_ ->
|
|
LJID3 = jlib:jid_remove_resource(LJID2),
|
|
case (?DICT):find(LJID3,
|
|
StateData#state.affiliations)
|
|
of
|
|
{ok, Affiliation} -> Affiliation;
|
|
_ -> none
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
case Res of
|
|
{A, _Reason} -> A;
|
|
_ -> Res
|
|
end.
|
|
|
|
get_service_affiliation(JID, StateData) ->
|
|
{_AccessRoute, _AccessCreate, AccessAdmin,
|
|
_AccessPersistent} =
|
|
StateData#state.access,
|
|
case acl:match_rule(StateData#state.server_host,
|
|
AccessAdmin, JID)
|
|
of
|
|
allow -> owner;
|
|
_ -> none
|
|
end.
|
|
|
|
set_role(JID, Role, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
LJIDs = case LJID of
|
|
{U, S, <<"">>} ->
|
|
(?DICT):fold(fun (J, _, Js) ->
|
|
case J of
|
|
{U, S, _} -> [J | Js];
|
|
_ -> Js
|
|
end
|
|
end,
|
|
[], StateData#state.users);
|
|
_ ->
|
|
case (?DICT):is_key(LJID, StateData#state.users) of
|
|
true -> [LJID];
|
|
_ -> []
|
|
end
|
|
end,
|
|
{Users, Nicks} = case Role of
|
|
none ->
|
|
lists:foldl(fun (J, {Us, Ns}) ->
|
|
NewNs = case (?DICT):find(J, Us)
|
|
of
|
|
{ok,
|
|
#user{nick = Nick}} ->
|
|
(?DICT):erase(Nick,
|
|
Ns);
|
|
_ -> Ns
|
|
end,
|
|
{(?DICT):erase(J, Us), NewNs}
|
|
end,
|
|
{StateData#state.users,
|
|
StateData#state.nicks},
|
|
LJIDs);
|
|
_ ->
|
|
{lists:foldl(fun (J, Us) ->
|
|
{ok, User} = (?DICT):find(J,
|
|
Us),
|
|
(?DICT):store(J,
|
|
User#user{role =
|
|
Role},
|
|
Us)
|
|
end,
|
|
StateData#state.users, LJIDs),
|
|
StateData#state.nicks}
|
|
end,
|
|
StateData#state{users = Users, nicks = Nicks}.
|
|
|
|
get_role(JID, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
case (?DICT):find(LJID, StateData#state.users) of
|
|
{ok, #user{role = Role}} -> Role;
|
|
_ -> none
|
|
end.
|
|
|
|
get_default_role(Affiliation, StateData) ->
|
|
case Affiliation of
|
|
owner -> moderator;
|
|
admin -> moderator;
|
|
member -> participant;
|
|
outcast -> none;
|
|
none ->
|
|
case (StateData#state.config)#config.members_only of
|
|
true -> none;
|
|
_ ->
|
|
case (StateData#state.config)#config.members_by_default
|
|
of
|
|
true -> participant;
|
|
_ -> visitor
|
|
end
|
|
end
|
|
end.
|
|
|
|
is_visitor(Jid, StateData) ->
|
|
get_role(Jid, StateData) =:= visitor.
|
|
|
|
is_moderator(Jid, StateData) ->
|
|
get_role(Jid, StateData) =:= moderator.
|
|
|
|
get_max_users(StateData) ->
|
|
MaxUsers = (StateData#state.config)#config.max_users,
|
|
ServiceMaxUsers = get_service_max_users(StateData),
|
|
if MaxUsers =< ServiceMaxUsers -> MaxUsers;
|
|
true -> ServiceMaxUsers
|
|
end.
|
|
|
|
get_service_max_users(StateData) ->
|
|
gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, max_users,
|
|
fun(I) when is_integer(I), I>0 -> I end,
|
|
?MAX_USERS_DEFAULT).
|
|
|
|
get_max_users_admin_threshold(StateData) ->
|
|
gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, max_users_admin_threshold,
|
|
fun(I) when is_integer(I), I>0 -> I end,
|
|
5).
|
|
|
|
get_user_activity(JID, StateData) ->
|
|
case treap:lookup(jlib:jid_tolower(JID),
|
|
StateData#state.activity)
|
|
of
|
|
{ok, _P, A} -> A;
|
|
error ->
|
|
MessageShaper =
|
|
shaper:new(gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, user_message_shaper,
|
|
fun(A) when is_atom(A) -> A end,
|
|
none)),
|
|
PresenceShaper =
|
|
shaper:new(gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, user_presence_shaper,
|
|
fun(A) when is_atom(A) -> A end,
|
|
none)),
|
|
#activity{message_shaper = MessageShaper,
|
|
presence_shaper = PresenceShaper}
|
|
end.
|
|
|
|
store_user_activity(JID, UserActivity, StateData) ->
|
|
MinMessageInterval =
|
|
trunc(gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, min_message_interval,
|
|
fun(I) when is_number(I), I>=0 -> I end,
|
|
0)
|
|
* 1000),
|
|
MinPresenceInterval =
|
|
trunc(gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, min_presence_interval,
|
|
fun(I) when is_number(I), I>=0 -> I end,
|
|
0)
|
|
* 1000),
|
|
Key = jlib:jid_tolower(JID),
|
|
Now = now_to_usec(now()),
|
|
Activity1 = clean_treap(StateData#state.activity,
|
|
{1, -Now}),
|
|
Activity = case treap:lookup(Key, Activity1) of
|
|
{ok, _P, _A} -> treap:delete(Key, Activity1);
|
|
error -> Activity1
|
|
end,
|
|
StateData1 = case MinMessageInterval == 0 andalso
|
|
MinPresenceInterval == 0 andalso
|
|
UserActivity#activity.message_shaper == none andalso
|
|
UserActivity#activity.presence_shaper == none
|
|
andalso
|
|
UserActivity#activity.message == undefined andalso
|
|
UserActivity#activity.presence == undefined
|
|
of
|
|
true -> StateData#state{activity = Activity};
|
|
false ->
|
|
case UserActivity#activity.message == undefined andalso
|
|
UserActivity#activity.presence == undefined
|
|
of
|
|
true ->
|
|
{_, MessageShaperInterval} =
|
|
shaper:update(UserActivity#activity.message_shaper,
|
|
100000),
|
|
{_, PresenceShaperInterval} =
|
|
shaper:update(UserActivity#activity.presence_shaper,
|
|
100000),
|
|
Delay = lists:max([MessageShaperInterval,
|
|
PresenceShaperInterval,
|
|
MinMessageInterval,
|
|
MinPresenceInterval])
|
|
* 1000,
|
|
Priority = {1, -(Now + Delay)},
|
|
StateData#state{activity =
|
|
treap:insert(Key, Priority,
|
|
UserActivity,
|
|
Activity)};
|
|
false ->
|
|
Priority = {0, 0},
|
|
StateData#state{activity =
|
|
treap:insert(Key, Priority,
|
|
UserActivity,
|
|
Activity)}
|
|
end
|
|
end,
|
|
StateData1.
|
|
|
|
clean_treap(Treap, CleanPriority) ->
|
|
case treap:is_empty(Treap) of
|
|
true -> Treap;
|
|
false ->
|
|
{_Key, Priority, _Value} = treap:get_root(Treap),
|
|
if Priority > CleanPriority ->
|
|
clean_treap(treap:delete_root(Treap), CleanPriority);
|
|
true -> Treap
|
|
end
|
|
end.
|
|
|
|
prepare_room_queue(StateData) ->
|
|
case queue:out(StateData#state.room_queue) of
|
|
{{value, {message, From}}, _RoomQueue} ->
|
|
Activity = get_user_activity(From, StateData),
|
|
Packet = Activity#activity.message,
|
|
Size = element_size(Packet),
|
|
{RoomShaper, RoomShaperInterval} =
|
|
shaper:update(StateData#state.room_shaper, Size),
|
|
erlang:send_after(RoomShaperInterval, self(),
|
|
process_room_queue),
|
|
StateData#state{room_shaper = RoomShaper};
|
|
{{value, {presence, From}}, _RoomQueue} ->
|
|
Activity = get_user_activity(From, StateData),
|
|
{_Nick, Packet} = Activity#activity.presence,
|
|
Size = element_size(Packet),
|
|
{RoomShaper, RoomShaperInterval} =
|
|
shaper:update(StateData#state.room_shaper, Size),
|
|
erlang:send_after(RoomShaperInterval, self(),
|
|
process_room_queue),
|
|
StateData#state{room_shaper = RoomShaper};
|
|
{empty, _} -> StateData
|
|
end.
|
|
|
|
add_online_user(JID, Nick, Role, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
Users = (?DICT):store(LJID,
|
|
#user{jid = JID, nick = Nick, role = Role},
|
|
StateData#state.users),
|
|
add_to_log(join, Nick, StateData),
|
|
Nicks = (?DICT):update(Nick,
|
|
fun (Entry) ->
|
|
case lists:member(LJID, Entry) of
|
|
true -> Entry;
|
|
false -> [LJID | Entry]
|
|
end
|
|
end,
|
|
[LJID], StateData#state.nicks),
|
|
tab_add_online_user(JID, StateData),
|
|
StateData#state{users = Users, nicks = Nicks}.
|
|
|
|
remove_online_user(JID, StateData) ->
|
|
remove_online_user(JID, StateData, <<"">>).
|
|
|
|
remove_online_user(JID, StateData, Reason) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
|
|
StateData#state.users),
|
|
add_to_log(leave, {Nick, Reason}, StateData),
|
|
tab_remove_online_user(JID, StateData),
|
|
Users = (?DICT):erase(LJID, StateData#state.users),
|
|
Nicks = case (?DICT):find(Nick, StateData#state.nicks)
|
|
of
|
|
{ok, [LJID]} ->
|
|
(?DICT):erase(Nick, StateData#state.nicks);
|
|
{ok, U} ->
|
|
(?DICT):store(Nick, U -- [LJID], StateData#state.nicks);
|
|
error -> StateData#state.nicks
|
|
end,
|
|
StateData#state{users = Users, nicks = Nicks}.
|
|
|
|
filter_presence(#xmlel{name = <<"presence">>,
|
|
attrs = Attrs, children = Els}) ->
|
|
FEls = lists:filter(fun (El) ->
|
|
case El of
|
|
{xmlcdata, _} -> false;
|
|
#xmlel{attrs = Attrs1} ->
|
|
XMLNS = xml:get_attr_s(<<"xmlns">>,
|
|
Attrs1),
|
|
NS_MUC = ?NS_MUC,
|
|
Size = byte_size(NS_MUC),
|
|
case XMLNS of
|
|
<<NS_MUC:Size/binary, _/binary>> ->
|
|
false;
|
|
_ ->
|
|
true
|
|
end
|
|
end
|
|
end,
|
|
Els),
|
|
#xmlel{name = <<"presence">>, attrs = Attrs,
|
|
children = FEls}.
|
|
|
|
strip_status(#xmlel{name = <<"presence">>,
|
|
attrs = Attrs, children = Els}) ->
|
|
FEls = lists:filter(fun (#xmlel{name = <<"status">>}) ->
|
|
false;
|
|
(_) -> true
|
|
end,
|
|
Els),
|
|
#xmlel{name = <<"presence">>, attrs = Attrs,
|
|
children = FEls}.
|
|
|
|
add_user_presence(JID, Presence, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
FPresence = filter_presence(Presence),
|
|
Users = (?DICT):update(LJID,
|
|
fun (#user{} = User) ->
|
|
User#user{last_presence = FPresence}
|
|
end,
|
|
StateData#state.users),
|
|
StateData#state{users = Users}.
|
|
|
|
add_user_presence_un(JID, Presence, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
FPresence = filter_presence(Presence),
|
|
Users = (?DICT):update(LJID,
|
|
fun (#user{} = User) ->
|
|
User#user{last_presence = FPresence,
|
|
role = none}
|
|
end,
|
|
StateData#state.users),
|
|
StateData#state{users = Users}.
|
|
|
|
%% Find and return a list of the full JIDs of the users of Nick.
|
|
%% Return jid record.
|
|
find_jids_by_nick(Nick, StateData) ->
|
|
case (?DICT):find(Nick, StateData#state.nicks) of
|
|
{ok, [User]} -> [jlib:make_jid(User)];
|
|
{ok, Users} -> [jlib:make_jid(LJID) || LJID <- Users];
|
|
error -> false
|
|
end.
|
|
|
|
%% Find and return the full JID of the user of Nick with
|
|
%% highest-priority presence. Return jid record.
|
|
find_jid_by_nick(Nick, StateData) ->
|
|
case (?DICT):find(Nick, StateData#state.nicks) of
|
|
{ok, [User]} -> jlib:make_jid(User);
|
|
{ok, [FirstUser | Users]} ->
|
|
#user{last_presence = FirstPresence} =
|
|
(?DICT):fetch(FirstUser, StateData#state.users),
|
|
{LJID, _} = lists:foldl(fun (Compare,
|
|
{HighestUser, HighestPresence}) ->
|
|
#user{last_presence = P1} =
|
|
(?DICT):fetch(Compare,
|
|
StateData#state.users),
|
|
case higher_presence(P1,
|
|
HighestPresence)
|
|
of
|
|
true -> {Compare, P1};
|
|
false ->
|
|
{HighestUser, HighestPresence}
|
|
end
|
|
end,
|
|
{FirstUser, FirstPresence}, Users),
|
|
jlib:make_jid(LJID);
|
|
error -> false
|
|
end.
|
|
|
|
higher_presence(Pres1, Pres2) ->
|
|
Pri1 = get_priority_from_presence(Pres1),
|
|
Pri2 = get_priority_from_presence(Pres2),
|
|
Pri1 > Pri2.
|
|
|
|
get_priority_from_presence(PresencePacket) ->
|
|
case xml:get_subtag(PresencePacket, <<"priority">>) of
|
|
false -> 0;
|
|
SubEl ->
|
|
case catch
|
|
jlib:binary_to_integer(xml:get_tag_cdata(SubEl))
|
|
of
|
|
P when is_integer(P) -> P;
|
|
_ -> 0
|
|
end
|
|
end.
|
|
|
|
find_nick_by_jid(Jid, StateData) ->
|
|
[{_, #user{nick = Nick}}] = lists:filter(fun ({_,
|
|
#user{jid = FJid}}) ->
|
|
FJid == Jid
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)),
|
|
Nick.
|
|
|
|
is_nick_change(JID, Nick, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
case Nick of
|
|
<<"">> -> false;
|
|
_ ->
|
|
{ok, #user{nick = OldNick}} = (?DICT):find(LJID,
|
|
StateData#state.users),
|
|
Nick /= OldNick
|
|
end.
|
|
|
|
nick_collision(User, Nick, StateData) ->
|
|
UserOfNick = find_jid_by_nick(Nick, StateData),
|
|
UserOfNick /= false andalso
|
|
jlib:jid_remove_resource(jlib:jid_tolower(UserOfNick))
|
|
/= jlib:jid_remove_resource(jlib:jid_tolower(User)).
|
|
|
|
add_new_user(From, Nick,
|
|
#xmlel{attrs = Attrs, children = Els} = Packet,
|
|
StateData) ->
|
|
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs),
|
|
MaxUsers = get_max_users(StateData),
|
|
MaxAdminUsers = MaxUsers +
|
|
get_max_users_admin_threshold(StateData),
|
|
NUsers = dict:fold(fun (_, _, Acc) -> Acc + 1 end, 0,
|
|
StateData#state.users),
|
|
Affiliation = get_affiliation(From, StateData),
|
|
ServiceAffiliation = get_service_affiliation(From,
|
|
StateData),
|
|
NConferences = tab_count_user(From),
|
|
MaxConferences =
|
|
gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, max_user_conferences,
|
|
fun(I) when is_integer(I), I>0 -> I end,
|
|
10),
|
|
Collision = nick_collision(From, Nick, StateData),
|
|
case {(ServiceAffiliation == owner orelse
|
|
(Affiliation == admin orelse Affiliation == owner)
|
|
andalso NUsers < MaxAdminUsers
|
|
orelse NUsers < MaxUsers)
|
|
andalso NConferences < MaxConferences,
|
|
Collision,
|
|
mod_muc:can_use_nick(StateData#state.server_host,
|
|
StateData#state.host, From, Nick),
|
|
get_default_role(Affiliation, StateData)}
|
|
of
|
|
{false, _, _, _} ->
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERR_SERVICE_UNAVAILABLE),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid, Nick),
|
|
From, Err),
|
|
StateData;
|
|
{_, _, _, none} ->
|
|
Err = jlib:make_error_reply(Packet,
|
|
case Affiliation of
|
|
outcast ->
|
|
ErrText =
|
|
<<"You have been banned from this room">>,
|
|
?ERRT_FORBIDDEN(Lang, ErrText);
|
|
_ ->
|
|
ErrText =
|
|
<<"Membership is required to enter this room">>,
|
|
?ERRT_REGISTRATION_REQUIRED(Lang,
|
|
ErrText)
|
|
end),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid, Nick),
|
|
From, Err),
|
|
StateData;
|
|
{_, true, _, _} ->
|
|
ErrText = <<"That nickname is already in use by another occupant">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_CONFLICT(Lang, ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData;
|
|
{_, _, false, _} ->
|
|
ErrText = <<"That nickname is registered by another person">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_CONFLICT(Lang, ErrText)),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData;
|
|
{_, _, _, Role} ->
|
|
case check_password(ServiceAffiliation, Affiliation,
|
|
Els, From, StateData)
|
|
of
|
|
true ->
|
|
NewState = add_user_presence(From, Packet,
|
|
add_online_user(From, Nick, Role,
|
|
StateData)),
|
|
if not (NewState#state.config)#config.anonymous ->
|
|
WPacket = #xmlel{name = <<"message">>,
|
|
attrs = [{<<"type">>, <<"groupchat">>}],
|
|
children =
|
|
[#xmlel{name = <<"body">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
translate:translate(Lang,
|
|
<<"This room is not anonymous">>)}]},
|
|
#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name =
|
|
<<"status">>,
|
|
attrs =
|
|
[{<<"code">>,
|
|
<<"100">>}],
|
|
children =
|
|
[]}]}]},
|
|
ejabberd_router:route(StateData#state.jid, From, WPacket);
|
|
true -> ok
|
|
end,
|
|
send_existing_presences(From, NewState),
|
|
send_new_presence(From, NewState),
|
|
Shift = count_stanza_shift(Nick, Els, NewState),
|
|
case send_history(From, Shift, NewState) of
|
|
true -> ok;
|
|
_ -> send_subject(From, Lang, StateData)
|
|
end,
|
|
case NewState#state.just_created of
|
|
true -> NewState#state{just_created = false};
|
|
false ->
|
|
Robots = (?DICT):erase(From, StateData#state.robots),
|
|
NewState#state{robots = Robots}
|
|
end;
|
|
nopass ->
|
|
ErrText = <<"A password is required to enter this room">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_AUTHORIZED(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData;
|
|
captcha_required ->
|
|
SID = xml:get_attr_s(<<"id">>, Attrs),
|
|
RoomJID = StateData#state.jid,
|
|
To = jlib:jid_replace_resource(RoomJID, Nick),
|
|
Limiter = {From#jid.luser, From#jid.lserver},
|
|
case ejabberd_captcha:create_captcha(SID, RoomJID, To,
|
|
Lang, Limiter, From)
|
|
of
|
|
{ok, ID, CaptchaEls} ->
|
|
MsgPkt = #xmlel{name = <<"message">>,
|
|
attrs = [{<<"id">>, ID}],
|
|
children = CaptchaEls},
|
|
Robots = (?DICT):store(From, {Nick, Packet},
|
|
StateData#state.robots),
|
|
ejabberd_router:route(RoomJID, From, MsgPkt),
|
|
StateData#state{robots = Robots};
|
|
{error, limit} ->
|
|
ErrText = <<"Too many CAPTCHA requests">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_RESOURCE_CONSTRAINT(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData;
|
|
_ ->
|
|
ErrText = <<"Unable to generate a CAPTCHA">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_INTERNAL_SERVER_ERROR(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData
|
|
end;
|
|
_ ->
|
|
ErrText = <<"Incorrect password">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_AUTHORIZED(Lang,
|
|
ErrText)),
|
|
ejabberd_router:route % TODO: s/Nick/""/
|
|
(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
From, Err),
|
|
StateData
|
|
end
|
|
end.
|
|
|
|
check_password(owner, _Affiliation, _Els, _From,
|
|
_StateData) ->
|
|
%% Don't check pass if user is owner in MUC service (access_admin option)
|
|
true;
|
|
check_password(_ServiceAffiliation, Affiliation, Els,
|
|
From, StateData) ->
|
|
case (StateData#state.config)#config.password_protected
|
|
of
|
|
false -> check_captcha(Affiliation, From, StateData);
|
|
true ->
|
|
Pass = extract_password(Els),
|
|
case Pass of
|
|
false -> nopass;
|
|
_ ->
|
|
case (StateData#state.config)#config.password of
|
|
Pass -> true;
|
|
_ -> false
|
|
end
|
|
end
|
|
end.
|
|
|
|
check_captcha(Affiliation, From, StateData) ->
|
|
case (StateData#state.config)#config.captcha_protected
|
|
andalso ejabberd_captcha:is_feature_available()
|
|
of
|
|
true when Affiliation == none ->
|
|
case (?DICT):find(From, StateData#state.robots) of
|
|
{ok, passed} -> true;
|
|
_ ->
|
|
WList =
|
|
(StateData#state.config)#config.captcha_whitelist,
|
|
#jid{luser = U, lserver = S, lresource = R} = From,
|
|
case (?SETS):is_element({U, S, R}, WList) of
|
|
true -> true;
|
|
false ->
|
|
case (?SETS):is_element({U, S, <<"">>}, WList) of
|
|
true -> true;
|
|
false ->
|
|
case (?SETS):is_element({<<"">>, S, <<"">>}, WList)
|
|
of
|
|
true -> true;
|
|
false -> captcha_required
|
|
end
|
|
end
|
|
end
|
|
end;
|
|
_ -> true
|
|
end.
|
|
|
|
extract_password([]) -> false;
|
|
extract_password([#xmlel{attrs = Attrs} = El | Els]) ->
|
|
case xml:get_attr_s(<<"xmlns">>, Attrs) of
|
|
?NS_MUC ->
|
|
case xml:get_subtag(El, <<"password">>) of
|
|
false -> false;
|
|
SubEl -> xml:get_tag_cdata(SubEl)
|
|
end;
|
|
_ -> extract_password(Els)
|
|
end;
|
|
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 = byte_size(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([#xmlel{attrs = Attrs} = 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">> ->
|
|
case jlib:datetime_string_to_timestamp(AttrVal) of
|
|
undefined -> false;
|
|
TS -> calendar:now_to_universal_time(TS)
|
|
end;
|
|
_ ->
|
|
case catch jlib:binary_to_integer(AttrVal) of
|
|
IntVal when is_integer(IntVal) and (IntVal >= 0) ->
|
|
IntVal;
|
|
_ -> false
|
|
end
|
|
end;
|
|
_ -> extract_history(Els, Type)
|
|
end;
|
|
extract_history([_ | Els], Type) ->
|
|
extract_history(Els, Type).
|
|
|
|
send_update_presence(JID, StateData) ->
|
|
send_update_presence(JID, <<"">>, StateData).
|
|
|
|
send_update_presence(JID, Reason, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
LJIDs = case LJID of
|
|
{U, S, <<"">>} ->
|
|
(?DICT):fold(fun (J, _, Js) ->
|
|
case J of
|
|
{U, S, _} -> [J | Js];
|
|
_ -> Js
|
|
end
|
|
end,
|
|
[], StateData#state.users);
|
|
_ ->
|
|
case (?DICT):is_key(LJID, StateData#state.users) of
|
|
true -> [LJID];
|
|
_ -> []
|
|
end
|
|
end,
|
|
lists:foreach(fun (J) ->
|
|
send_new_presence(J, Reason, StateData)
|
|
end,
|
|
LJIDs).
|
|
|
|
send_new_presence(NJID, StateData) ->
|
|
send_new_presence(NJID, <<"">>, StateData).
|
|
|
|
send_new_presence(NJID, Reason, StateData) ->
|
|
#user{nick = Nick} =
|
|
(?DICT):fetch(jlib:jid_tolower(NJID),
|
|
StateData#state.users),
|
|
LJID = find_jid_by_nick(Nick, StateData),
|
|
{ok,
|
|
#user{jid = RealJID, role = Role,
|
|
last_presence = Presence}} =
|
|
(?DICT):find(jlib:jid_tolower(LJID),
|
|
StateData#state.users),
|
|
Affiliation = get_affiliation(LJID, StateData),
|
|
SAffiliation = affiliation_to_list(Affiliation),
|
|
SRole = role_to_list(Role),
|
|
lists:foreach(fun ({_LJID, Info}) ->
|
|
ItemAttrs = case Info#user.role == moderator orelse
|
|
(StateData#state.config)#config.anonymous
|
|
== false
|
|
of
|
|
true ->
|
|
[{<<"jid">>,
|
|
jlib:jid_to_string(RealJID)},
|
|
{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, SRole}];
|
|
_ ->
|
|
[{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, SRole}]
|
|
end,
|
|
ItemEls = case Reason of
|
|
<<"">> -> [];
|
|
_ ->
|
|
[#xmlel{name = <<"reason">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata, Reason}]}]
|
|
end,
|
|
Status = case StateData#state.just_created of
|
|
true ->
|
|
[#xmlel{name = <<"status">>,
|
|
attrs =
|
|
[{<<"code">>, <<"201">>}],
|
|
children = []}];
|
|
false -> []
|
|
end,
|
|
Status2 = case
|
|
(StateData#state.config)#config.anonymous
|
|
== false
|
|
andalso NJID == Info#user.jid
|
|
of
|
|
true ->
|
|
[#xmlel{name = <<"status">>,
|
|
attrs =
|
|
[{<<"code">>, <<"100">>}],
|
|
children = []}
|
|
| Status];
|
|
false -> Status
|
|
end,
|
|
Status3 = case NJID == Info#user.jid of
|
|
true ->
|
|
[#xmlel{name = <<"status">>,
|
|
attrs =
|
|
[{<<"code">>, <<"110">>}],
|
|
children = []}
|
|
| Status2];
|
|
false -> Status2
|
|
end,
|
|
Packet = xml:append_subtags(Presence,
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name =
|
|
<<"item">>,
|
|
attrs
|
|
=
|
|
ItemAttrs,
|
|
children
|
|
=
|
|
ItemEls}
|
|
| Status3]}]),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
Info#user.jid, Packet)
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)).
|
|
|
|
send_existing_presences(ToJID, StateData) ->
|
|
LToJID = jlib:jid_tolower(ToJID),
|
|
{ok, #user{jid = RealToJID, role = Role}} =
|
|
(?DICT):find(LToJID, StateData#state.users),
|
|
lists:foreach(fun ({FromNick, _Users}) ->
|
|
LJID = find_jid_by_nick(FromNick, StateData),
|
|
#user{jid = FromJID, role = FromRole,
|
|
last_presence = Presence} =
|
|
(?DICT):fetch(jlib:jid_tolower(LJID),
|
|
StateData#state.users),
|
|
case RealToJID of
|
|
FromJID -> ok;
|
|
_ ->
|
|
FromAffiliation = get_affiliation(LJID,
|
|
StateData),
|
|
ItemAttrs = case Role == moderator orelse
|
|
(StateData#state.config)#config.anonymous
|
|
== false
|
|
of
|
|
true ->
|
|
[{<<"jid">>,
|
|
jlib:jid_to_string(FromJID)},
|
|
{<<"affiliation">>,
|
|
affiliation_to_list(FromAffiliation)},
|
|
{<<"role">>,
|
|
role_to_list(FromRole)}];
|
|
_ ->
|
|
[{<<"affiliation">>,
|
|
affiliation_to_list(FromAffiliation)},
|
|
{<<"role">>,
|
|
role_to_list(FromRole)}]
|
|
end,
|
|
Packet = xml:append_subtags(Presence,
|
|
[#xmlel{name =
|
|
<<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name
|
|
=
|
|
<<"item">>,
|
|
attrs
|
|
=
|
|
ItemAttrs,
|
|
children
|
|
=
|
|
[]}]}]),
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
FromNick),
|
|
RealToJID, Packet)
|
|
end
|
|
end,
|
|
(?DICT):to_list(StateData#state.nicks)).
|
|
|
|
now_to_usec({MSec, Sec, USec}) ->
|
|
(MSec * 1000000 + Sec) * 1000000 + USec.
|
|
|
|
change_nick(JID, Nick, StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
{ok, #user{nick = OldNick}} = (?DICT):find(LJID,
|
|
StateData#state.users),
|
|
Users = (?DICT):update(LJID,
|
|
fun (#user{} = User) -> User#user{nick = Nick} end,
|
|
StateData#state.users),
|
|
OldNickUsers = (?DICT):fetch(OldNick,
|
|
StateData#state.nicks),
|
|
NewNickUsers = case (?DICT):find(Nick,
|
|
StateData#state.nicks)
|
|
of
|
|
{ok, U} -> U;
|
|
error -> []
|
|
end,
|
|
SendOldUnavailable = length(OldNickUsers) == 1,
|
|
SendNewAvailable = SendOldUnavailable orelse
|
|
NewNickUsers == [],
|
|
Nicks = case OldNickUsers of
|
|
[LJID] ->
|
|
(?DICT):store(Nick, [LJID | NewNickUsers],
|
|
(?DICT):erase(OldNick, StateData#state.nicks));
|
|
[_ | _] ->
|
|
(?DICT):store(Nick, [LJID | NewNickUsers],
|
|
(?DICT):store(OldNick, OldNickUsers -- [LJID],
|
|
StateData#state.nicks))
|
|
end,
|
|
NewStateData = StateData#state{users = Users,
|
|
nicks = Nicks},
|
|
send_nick_changing(JID, OldNick, NewStateData,
|
|
SendOldUnavailable, SendNewAvailable),
|
|
add_to_log(nickchange, {OldNick, Nick}, StateData),
|
|
NewStateData.
|
|
|
|
send_nick_changing(JID, OldNick, StateData,
|
|
SendOldUnavailable, SendNewAvailable) ->
|
|
{ok,
|
|
#user{jid = RealJID, nick = Nick, role = Role,
|
|
last_presence = Presence}} =
|
|
(?DICT):find(jlib:jid_tolower(JID),
|
|
StateData#state.users),
|
|
Affiliation = get_affiliation(JID, StateData),
|
|
SAffiliation = affiliation_to_list(Affiliation),
|
|
SRole = role_to_list(Role),
|
|
lists:foreach(fun ({_LJID, Info}) ->
|
|
ItemAttrs1 = case Info#user.role == moderator orelse
|
|
(StateData#state.config)#config.anonymous
|
|
== false
|
|
of
|
|
true ->
|
|
[{<<"jid">>,
|
|
jlib:jid_to_string(RealJID)},
|
|
{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, SRole},
|
|
{<<"nick">>, Nick}];
|
|
_ ->
|
|
[{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, SRole},
|
|
{<<"nick">>, Nick}]
|
|
end,
|
|
ItemAttrs2 = case Info#user.role == moderator orelse
|
|
(StateData#state.config)#config.anonymous
|
|
== false
|
|
of
|
|
true ->
|
|
[{<<"jid">>,
|
|
jlib:jid_to_string(RealJID)},
|
|
{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, SRole}];
|
|
_ ->
|
|
[{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, SRole}]
|
|
end,
|
|
Status110 = case JID == Info#user.jid of
|
|
true ->
|
|
[#xmlel{name = <<"status">>,
|
|
attrs = [{<<"code">>, <<"110">>}]
|
|
}];
|
|
false ->
|
|
[]
|
|
end,
|
|
Packet1 = #xmlel{name = <<"presence">>,
|
|
attrs =
|
|
[{<<"type">>,
|
|
<<"unavailable">>}],
|
|
children =
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name =
|
|
<<"item">>,
|
|
attrs =
|
|
ItemAttrs1,
|
|
children =
|
|
[]},
|
|
#xmlel{name =
|
|
<<"status">>,
|
|
attrs =
|
|
[{<<"code">>,
|
|
<<"303">>}],
|
|
children =
|
|
[]}|Status110]}]},
|
|
Packet2 = xml:append_subtags(Presence,
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name
|
|
=
|
|
<<"item">>,
|
|
attrs
|
|
=
|
|
ItemAttrs2,
|
|
children
|
|
=
|
|
[]}|Status110]}]),
|
|
if SendOldUnavailable ->
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
OldNick),
|
|
Info#user.jid, Packet1);
|
|
true -> ok
|
|
end,
|
|
if SendNewAvailable ->
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
Info#user.jid, Packet2);
|
|
true -> ok
|
|
end
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)).
|
|
|
|
lqueue_new(Max) ->
|
|
#lqueue{queue = queue:new(), len = 0, max = Max}.
|
|
|
|
%% If the message queue limit is set to 0, do not store messages.
|
|
lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ;
|
|
%% Otherwise, rotate messages in the queue store.
|
|
lqueue_in(Item,
|
|
#lqueue{queue = Q1, len = Len, max = Max}) ->
|
|
Q2 = queue:in(Item, Q1),
|
|
if Len >= Max ->
|
|
Q3 = lqueue_cut(Q2, Len - Max + 1),
|
|
#lqueue{queue = Q3, len = Max, max = Max};
|
|
true -> #lqueue{queue = Q2, len = Len + 1, max = Max}
|
|
end.
|
|
|
|
lqueue_cut(Q, 0) -> Q;
|
|
lqueue_cut(Q, N) ->
|
|
{_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1).
|
|
|
|
lqueue_to_list(#lqueue{queue = Q1}) ->
|
|
queue:to_list(Q1).
|
|
|
|
|
|
add_message_to_history(FromNick, FromJID, Packet, StateData) ->
|
|
HaveSubject = case xml:get_subtag(Packet, <<"subject">>)
|
|
of
|
|
false -> false;
|
|
_ -> true
|
|
end,
|
|
TimeStamp = now(),
|
|
AddrPacket = case (StateData#state.config)#config.anonymous of
|
|
true -> Packet;
|
|
false ->
|
|
Address = #xmlel{name = <<"address">>,
|
|
attrs = [{<<"type">>, <<"ofrom">>},
|
|
{<<"jid">>,
|
|
jlib:jid_to_string(FromJID)}],
|
|
children = []},
|
|
Addresses = #xmlel{name = <<"addresses">>,
|
|
attrs = [{<<"xmlns">>, ?NS_ADDRESS}],
|
|
children = [Address]},
|
|
xml:append_subtags(Packet, [Addresses])
|
|
end,
|
|
TSPacket = jlib:add_delay_info(AddrPacket, StateData#state.jid, TimeStamp),
|
|
SPacket =
|
|
jlib:replace_from_to(jlib:jid_replace_resource(StateData#state.jid,
|
|
FromNick),
|
|
StateData#state.jid, TSPacket),
|
|
Size = element_size(SPacket),
|
|
Q1 = lqueue_in({FromNick, TSPacket, HaveSubject,
|
|
calendar:now_to_universal_time(TimeStamp), Size},
|
|
StateData#state.history),
|
|
add_to_log(text, {FromNick, Packet}, StateData),
|
|
StateData#state{history = Q1}.
|
|
|
|
send_history(JID, Shift, StateData) ->
|
|
lists:foldl(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,
|
|
lists:nthtail(Shift,
|
|
lqueue_to_list(StateData#state.history))).
|
|
|
|
send_subject(JID, Lang, StateData) ->
|
|
case StateData#state.subject_author of
|
|
<<"">> -> ok;
|
|
Nick ->
|
|
Subject = StateData#state.subject,
|
|
Packet = #xmlel{name = <<"message">>,
|
|
attrs = [{<<"type">>, <<"groupchat">>}],
|
|
children =
|
|
[#xmlel{name = <<"subject">>, attrs = [],
|
|
children = [{xmlcdata, Subject}]},
|
|
#xmlel{name = <<"body">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<Nick/binary,
|
|
(translate:translate(Lang,
|
|
<<" has set the subject to: ">>))/binary,
|
|
Subject/binary>>}]}]},
|
|
ejabberd_router:route(StateData#state.jid, JID, Packet)
|
|
end.
|
|
|
|
check_subject(Packet) ->
|
|
case xml:get_subtag(Packet, <<"subject">>) of
|
|
false -> false;
|
|
SubjEl -> xml:get_tag_cdata(SubjEl)
|
|
end.
|
|
|
|
can_change_subject(Role, StateData) ->
|
|
case (StateData#state.config)#config.allow_change_subj
|
|
of
|
|
true -> Role == moderator orelse Role == participant;
|
|
_ -> Role == moderator
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Admin stuff
|
|
|
|
process_iq_admin(From, set, Lang, SubEl, StateData) ->
|
|
#xmlel{children = Items} = SubEl,
|
|
process_admin_items_set(From, Items, Lang, StateData);
|
|
process_iq_admin(From, get, Lang, SubEl, StateData) ->
|
|
case xml:get_subtag(SubEl, <<"item">>) of
|
|
false -> {error, ?ERR_BAD_REQUEST};
|
|
Item ->
|
|
FAffiliation = get_affiliation(From, StateData),
|
|
FRole = get_role(From, StateData),
|
|
case xml:get_tag_attr(<<"role">>, Item) of
|
|
false ->
|
|
case xml:get_tag_attr(<<"affiliation">>, Item) of
|
|
false -> {error, ?ERR_BAD_REQUEST};
|
|
{value, StrAffiliation} ->
|
|
case catch list_to_affiliation(StrAffiliation) of
|
|
{'EXIT', _} -> {error, ?ERR_BAD_REQUEST};
|
|
SAffiliation ->
|
|
if (FAffiliation == owner) or
|
|
(FAffiliation == admin) or
|
|
((FAffiliation == member) and (SAffiliation == member)) ->
|
|
Items = items_with_affiliation(SAffiliation,
|
|
StateData),
|
|
{result, Items, StateData};
|
|
true ->
|
|
ErrText =
|
|
<<"Administrator privileges required">>,
|
|
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
|
|
end
|
|
end
|
|
end;
|
|
{value, StrRole} ->
|
|
case catch list_to_role(StrRole) of
|
|
{'EXIT', _} -> {error, ?ERR_BAD_REQUEST};
|
|
SRole ->
|
|
if FRole == moderator ->
|
|
Items = items_with_role(SRole, StateData),
|
|
{result, Items, StateData};
|
|
true ->
|
|
ErrText = <<"Moderator privileges required">>,
|
|
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
|
|
end
|
|
end
|
|
end
|
|
end.
|
|
|
|
items_with_role(SRole, StateData) ->
|
|
lists:map(fun ({_, U}) -> user_to_item(U, StateData)
|
|
end,
|
|
search_role(SRole, StateData)).
|
|
|
|
items_with_affiliation(SAffiliation, StateData) ->
|
|
lists:map(fun ({JID, {Affiliation, Reason}}) ->
|
|
#xmlel{name = <<"item">>,
|
|
attrs =
|
|
[{<<"affiliation">>,
|
|
affiliation_to_list(Affiliation)},
|
|
{<<"jid">>, jlib:jid_to_string(JID)}],
|
|
children =
|
|
[#xmlel{name = <<"reason">>, attrs = [],
|
|
children = [{xmlcdata, Reason}]}]};
|
|
({JID, Affiliation}) ->
|
|
#xmlel{name = <<"item">>,
|
|
attrs =
|
|
[{<<"affiliation">>,
|
|
affiliation_to_list(Affiliation)},
|
|
{<<"jid">>, jlib:jid_to_string(JID)}],
|
|
children = []}
|
|
end,
|
|
search_affiliation(SAffiliation, StateData)).
|
|
|
|
user_to_item(#user{role = Role, nick = Nick, jid = JID},
|
|
StateData) ->
|
|
Affiliation = get_affiliation(JID, StateData),
|
|
#xmlel{name = <<"item">>,
|
|
attrs =
|
|
[{<<"role">>, role_to_list(Role)},
|
|
{<<"affiliation">>, affiliation_to_list(Affiliation)},
|
|
{<<"nick">>, Nick},
|
|
{<<"jid">>, jlib:jid_to_string(JID)}],
|
|
children = []}.
|
|
|
|
search_role(Role, StateData) ->
|
|
lists:filter(fun ({_, #user{role = R}}) -> Role == R
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)).
|
|
|
|
search_affiliation(Affiliation, StateData) ->
|
|
lists:filter(fun ({_, A}) ->
|
|
case A of
|
|
{A1, _Reason} -> Affiliation == A1;
|
|
_ -> Affiliation == A
|
|
end
|
|
end,
|
|
(?DICT):to_list(StateData#state.affiliations)).
|
|
|
|
process_admin_items_set(UJID, Items, Lang, StateData) ->
|
|
UAffiliation = get_affiliation(UJID, StateData),
|
|
URole = get_role(UJID, StateData),
|
|
case find_changed_items(UJID, UAffiliation, URole,
|
|
Items, Lang, StateData, [])
|
|
of
|
|
{result, Res} ->
|
|
?INFO_MSG("Processing MUC admin query from ~s in "
|
|
"room ~s:~n ~p",
|
|
[jlib:jid_to_string(UJID),
|
|
jlib:jid_to_string(StateData#state.jid), Res]),
|
|
NSD = lists:foldl(process_item_change(UJID),
|
|
StateData, lists:flatten(Res)),
|
|
case (NSD#state.config)#config.persistent of
|
|
true ->
|
|
mod_muc:store_room(NSD#state.server_host,
|
|
NSD#state.host, NSD#state.room,
|
|
make_opts(NSD));
|
|
_ -> ok
|
|
end,
|
|
{result, [], NSD};
|
|
Err -> Err
|
|
end.
|
|
|
|
process_item_change(UJID) ->
|
|
fun(E, SD) ->
|
|
process_item_change(E, SD, UJID)
|
|
end.
|
|
|
|
process_item_change(E, SD, UJID) ->
|
|
case catch case E of
|
|
{JID, affiliation, owner, _} when JID#jid.luser == <<"">> ->
|
|
%% If the provided JID does not have username,
|
|
%% forget the affiliation completely
|
|
SD;
|
|
{JID, role, none, Reason} ->
|
|
catch
|
|
send_kickban_presence(UJID, JID,
|
|
Reason,
|
|
<<"307">>,
|
|
SD),
|
|
set_role(JID, none, SD);
|
|
{JID, affiliation, none, Reason} ->
|
|
case (SD#state.config)#config.members_only of
|
|
true ->
|
|
catch
|
|
send_kickban_presence(UJID, JID,
|
|
Reason,
|
|
<<"321">>,
|
|
none,
|
|
SD),
|
|
SD1 = set_affiliation(JID, none, SD),
|
|
set_role(JID, none, SD1);
|
|
_ ->
|
|
SD1 = set_affiliation(JID, none, SD),
|
|
send_update_presence(JID, SD1),
|
|
SD1
|
|
end;
|
|
{JID, affiliation, outcast, Reason} ->
|
|
catch
|
|
send_kickban_presence(UJID, JID,
|
|
Reason,
|
|
<<"301">>,
|
|
outcast,
|
|
SD),
|
|
set_affiliation(JID,
|
|
outcast,
|
|
set_role(JID, none, SD),
|
|
Reason);
|
|
{JID, affiliation, A, Reason}
|
|
when (A == admin) or (A == owner) ->
|
|
SD1 = set_affiliation(JID, A, SD, Reason),
|
|
SD2 = set_role(JID, moderator, SD1),
|
|
send_update_presence(JID, Reason, SD2),
|
|
SD2;
|
|
{JID, affiliation, member, Reason} ->
|
|
SD1 = set_affiliation(JID, member, SD, Reason),
|
|
SD2 = set_role(JID, participant, SD1),
|
|
send_update_presence(JID, Reason, SD2),
|
|
SD2;
|
|
{JID, role, Role, Reason} ->
|
|
SD1 = set_role(JID, Role, SD),
|
|
catch
|
|
send_new_presence(JID, Reason, SD1),
|
|
SD1;
|
|
{JID, affiliation, A, _Reason} ->
|
|
SD1 = set_affiliation(JID, A, SD),
|
|
send_update_presence(JID, SD1),
|
|
SD1
|
|
end
|
|
of
|
|
{'EXIT', ErrReason} ->
|
|
?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", [ErrReason]),
|
|
SD;
|
|
NSD -> NSD
|
|
end.
|
|
|
|
find_changed_items(_UJID, _UAffiliation, _URole, [],
|
|
_Lang, _StateData, Res) ->
|
|
{result, Res};
|
|
find_changed_items(UJID, UAffiliation, URole,
|
|
[{xmlcdata, _} | Items], Lang, StateData, Res) ->
|
|
find_changed_items(UJID, UAffiliation, URole, Items,
|
|
Lang, StateData, Res);
|
|
find_changed_items(UJID, UAffiliation, URole,
|
|
[#xmlel{name = <<"item">>, attrs = Attrs} = Item
|
|
| Items],
|
|
Lang, StateData, Res) ->
|
|
TJID = case xml:get_attr(<<"jid">>, Attrs) of
|
|
{value, S} ->
|
|
case jlib:string_to_jid(S) of
|
|
error ->
|
|
ErrText = iolist_to_binary(
|
|
io_lib:format(translate:translate(
|
|
Lang,
|
|
<<"Jabber ID ~s is invalid">>),
|
|
[S])),
|
|
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
|
|
J -> {value, [J]}
|
|
end;
|
|
_ ->
|
|
case xml:get_attr(<<"nick">>, Attrs) of
|
|
{value, N} ->
|
|
case find_jids_by_nick(N, StateData) of
|
|
false ->
|
|
ErrText = iolist_to_binary(
|
|
io_lib:format(
|
|
translate:translate(
|
|
Lang,
|
|
<<"Nickname ~s does not exist in the room">>),
|
|
[N])),
|
|
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
|
|
J -> {value, J}
|
|
end;
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end
|
|
end,
|
|
case TJID of
|
|
{value, [JID | _] = JIDs} ->
|
|
TAffiliation = get_affiliation(JID, StateData),
|
|
TRole = get_role(JID, StateData),
|
|
case xml:get_attr(<<"role">>, Attrs) of
|
|
false ->
|
|
case xml:get_attr(<<"affiliation">>, Attrs) of
|
|
false -> {error, ?ERR_BAD_REQUEST};
|
|
{value, StrAffiliation} ->
|
|
case catch list_to_affiliation(StrAffiliation) of
|
|
{'EXIT', _} ->
|
|
ErrText1 = iolist_to_binary(
|
|
io_lib:format(
|
|
translate:translate(
|
|
Lang,
|
|
<<"Invalid affiliation: ~s">>),
|
|
[StrAffiliation])),
|
|
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)};
|
|
SAffiliation ->
|
|
ServiceAf = get_service_affiliation(JID, StateData),
|
|
CanChangeRA = case can_change_ra(UAffiliation,
|
|
URole,
|
|
TAffiliation,
|
|
TRole, affiliation,
|
|
SAffiliation,
|
|
ServiceAf)
|
|
of
|
|
nothing -> nothing;
|
|
true -> true;
|
|
check_owner ->
|
|
case search_affiliation(owner,
|
|
StateData)
|
|
of
|
|
[{OJID, _}] ->
|
|
jlib:jid_remove_resource(OJID)
|
|
/=
|
|
jlib:jid_tolower(jlib:jid_remove_resource(UJID));
|
|
_ -> true
|
|
end;
|
|
_ -> false
|
|
end,
|
|
case CanChangeRA of
|
|
nothing ->
|
|
find_changed_items(UJID, UAffiliation, URole,
|
|
Items, Lang, StateData,
|
|
Res);
|
|
true ->
|
|
Reason = xml:get_path_s(Item,
|
|
[{elem, <<"reason">>},
|
|
cdata]),
|
|
MoreRes = [{jlib:jid_remove_resource(Jidx),
|
|
affiliation, SAffiliation, Reason}
|
|
|| Jidx <- JIDs],
|
|
find_changed_items(UJID, UAffiliation, URole,
|
|
Items, Lang, StateData,
|
|
[MoreRes | Res]);
|
|
false -> {error, ?ERR_NOT_ALLOWED}
|
|
end
|
|
end
|
|
end;
|
|
{value, StrRole} ->
|
|
case catch list_to_role(StrRole) of
|
|
{'EXIT', _} ->
|
|
ErrText1 = iolist_to_binary(
|
|
io_lib:format(translate:translate(
|
|
Lang,
|
|
<<"Invalid role: ~s">>),
|
|
[StrRole])),
|
|
{error, ?ERRT_BAD_REQUEST(Lang, ErrText1)};
|
|
SRole ->
|
|
ServiceAf = get_service_affiliation(JID, StateData),
|
|
CanChangeRA = case can_change_ra(UAffiliation, URole,
|
|
TAffiliation, TRole,
|
|
role, SRole, ServiceAf)
|
|
of
|
|
nothing -> nothing;
|
|
true -> true;
|
|
check_owner ->
|
|
case search_affiliation(owner,
|
|
StateData)
|
|
of
|
|
[{OJID, _}] ->
|
|
jlib:jid_remove_resource(OJID)
|
|
/=
|
|
jlib:jid_tolower(jlib:jid_remove_resource(UJID));
|
|
_ -> true
|
|
end;
|
|
_ -> false
|
|
end,
|
|
case CanChangeRA of
|
|
nothing ->
|
|
find_changed_items(UJID, UAffiliation, URole, Items,
|
|
Lang, StateData, Res);
|
|
true ->
|
|
Reason = xml:get_path_s(Item,
|
|
[{elem, <<"reason">>},
|
|
cdata]),
|
|
MoreRes = [{Jidx, role, SRole, Reason}
|
|
|| Jidx <- JIDs],
|
|
find_changed_items(UJID, UAffiliation, URole, Items,
|
|
Lang, StateData,
|
|
[MoreRes | Res]);
|
|
_ -> {error, ?ERR_NOT_ALLOWED}
|
|
end
|
|
end
|
|
end;
|
|
Err -> Err
|
|
end;
|
|
find_changed_items(_UJID, _UAffiliation, _URole, _Items,
|
|
_Lang, _StateData, _Res) ->
|
|
{error, ?ERR_BAD_REQUEST}.
|
|
|
|
can_change_ra(_FAffiliation, _FRole, owner, _TRole,
|
|
affiliation, owner, owner) ->
|
|
%% A room owner tries to add as persistent owner a
|
|
%% participant that is already owner because he is MUC admin
|
|
true;
|
|
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
|
|
_TRole, _RoleorAffiliation, _Value, owner) ->
|
|
%% Nobody can decrease MUC admin's role/affiliation
|
|
false;
|
|
can_change_ra(_FAffiliation, _FRole, TAffiliation,
|
|
_TRole, affiliation, Value, _ServiceAf)
|
|
when TAffiliation == Value ->
|
|
nothing;
|
|
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
|
|
TRole, role, Value, _ServiceAf)
|
|
when TRole == Value ->
|
|
nothing;
|
|
can_change_ra(FAffiliation, _FRole, outcast, _TRole,
|
|
affiliation, none, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, outcast, _TRole,
|
|
affiliation, member, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, outcast, _TRole,
|
|
affiliation, admin, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, outcast, _TRole,
|
|
affiliation, owner, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, none, _TRole,
|
|
affiliation, outcast, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, none, _TRole,
|
|
affiliation, member, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, none, _TRole, affiliation,
|
|
admin, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, none, _TRole, affiliation,
|
|
owner, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, member, _TRole,
|
|
affiliation, outcast, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, member, _TRole,
|
|
affiliation, none, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, member, _TRole,
|
|
affiliation, admin, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, member, _TRole,
|
|
affiliation, owner, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, admin, _TRole, affiliation,
|
|
_Affiliation, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(owner, _FRole, owner, _TRole, affiliation,
|
|
_Affiliation, _ServiceAf) ->
|
|
check_owner;
|
|
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
|
|
_TRole, affiliation, _Value, _ServiceAf) ->
|
|
false;
|
|
can_change_ra(_FAffiliation, moderator, _TAffiliation,
|
|
visitor, role, none, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, moderator, _TAffiliation,
|
|
visitor, role, participant, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, _TAffiliation,
|
|
visitor, role, moderator, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, moderator, _TAffiliation,
|
|
participant, role, none, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, moderator, _TAffiliation,
|
|
participant, role, visitor, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(FAffiliation, _FRole, _TAffiliation,
|
|
participant, role, moderator, _ServiceAf)
|
|
when (FAffiliation == owner) or
|
|
(FAffiliation == admin) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, _FRole, owner, moderator,
|
|
role, visitor, _ServiceAf) ->
|
|
false;
|
|
can_change_ra(owner, _FRole, _TAffiliation, moderator,
|
|
role, visitor, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, _FRole, admin, moderator,
|
|
role, visitor, _ServiceAf) ->
|
|
false;
|
|
can_change_ra(admin, _FRole, _TAffiliation, moderator,
|
|
role, visitor, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, _FRole, owner, moderator,
|
|
role, participant, _ServiceAf) ->
|
|
false;
|
|
can_change_ra(owner, _FRole, _TAffiliation, moderator,
|
|
role, participant, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, _FRole, admin, moderator,
|
|
role, participant, _ServiceAf) ->
|
|
false;
|
|
can_change_ra(admin, _FRole, _TAffiliation, moderator,
|
|
role, participant, _ServiceAf) ->
|
|
true;
|
|
can_change_ra(_FAffiliation, _FRole, _TAffiliation,
|
|
_TRole, role, _Value, _ServiceAf) ->
|
|
false.
|
|
|
|
send_kickban_presence(UJID, JID, Reason, Code, StateData) ->
|
|
NewAffiliation = get_affiliation(JID, StateData),
|
|
send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation,
|
|
StateData).
|
|
|
|
send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation,
|
|
StateData) ->
|
|
LJID = jlib:jid_tolower(JID),
|
|
LJIDs = case LJID of
|
|
{U, S, <<"">>} ->
|
|
(?DICT):fold(fun (J, _, Js) ->
|
|
case J of
|
|
{U, S, _} -> [J | Js];
|
|
_ -> Js
|
|
end
|
|
end,
|
|
[], StateData#state.users);
|
|
_ ->
|
|
case (?DICT):is_key(LJID, StateData#state.users) of
|
|
true -> [LJID];
|
|
_ -> []
|
|
end
|
|
end,
|
|
lists:foreach(fun (J) ->
|
|
{ok, #user{nick = Nick}} = (?DICT):find(J,
|
|
StateData#state.users),
|
|
add_to_log(kickban, {Nick, Reason, Code}, StateData),
|
|
tab_remove_online_user(J, StateData),
|
|
send_kickban_presence1(UJID, J, Reason, Code,
|
|
NewAffiliation, StateData)
|
|
end,
|
|
LJIDs).
|
|
|
|
send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
|
|
StateData) ->
|
|
{ok, #user{jid = RealJID, nick = Nick}} =
|
|
(?DICT):find(jlib:jid_tolower(UJID),
|
|
StateData#state.users),
|
|
SAffiliation = affiliation_to_list(Affiliation),
|
|
BannedJIDString = jlib:jid_to_string(RealJID),
|
|
case MJID /= <<"">> of
|
|
true ->
|
|
{ok, #user{nick = ActorNick}} =
|
|
(?DICT):find(jlib:jid_tolower(MJID),
|
|
StateData#state.users);
|
|
false ->
|
|
ActorNick = <<"">>
|
|
end,
|
|
lists:foreach(fun ({_LJID, Info}) ->
|
|
JidAttrList = case Info#user.role == moderator orelse
|
|
(StateData#state.config)#config.anonymous
|
|
== false
|
|
of
|
|
true ->
|
|
[{<<"jid">>, BannedJIDString}];
|
|
false -> []
|
|
end,
|
|
ItemAttrs = [{<<"affiliation">>, SAffiliation},
|
|
{<<"role">>, <<"none">>}]
|
|
++ JidAttrList,
|
|
ItemEls = case Reason of
|
|
<<"">> -> [];
|
|
_ ->
|
|
[#xmlel{name = <<"reason">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata, Reason}]}]
|
|
end,
|
|
ItemElsActor = case MJID of
|
|
<<"">> -> [];
|
|
_ -> [#xmlel{name = <<"actor">>,
|
|
attrs =
|
|
[{<<"nick">>, ActorNick}]}]
|
|
end,
|
|
Packet = #xmlel{name = <<"presence">>,
|
|
attrs =
|
|
[{<<"type">>, <<"unavailable">>}],
|
|
children =
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name =
|
|
<<"item">>,
|
|
attrs =
|
|
ItemAttrs,
|
|
children =
|
|
ItemElsActor ++ ItemEls},
|
|
#xmlel{name =
|
|
<<"status">>,
|
|
attrs =
|
|
[{<<"code">>,
|
|
Code}],
|
|
children =
|
|
[]}]}]},
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
Info#user.jid, Packet)
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Owner stuff
|
|
|
|
process_iq_owner(From, set, Lang, SubEl, StateData) ->
|
|
FAffiliation = get_affiliation(From, StateData),
|
|
case FAffiliation of
|
|
owner ->
|
|
#xmlel{children = Els} = SubEl,
|
|
case xml:remove_cdata(Els) of
|
|
[#xmlel{name = <<"x">>} = XEl] ->
|
|
case {xml:get_tag_attr_s(<<"xmlns">>, XEl),
|
|
xml:get_tag_attr_s(<<"type">>, XEl)}
|
|
of
|
|
{?NS_XDATA, <<"cancel">>} -> {result, [], StateData};
|
|
{?NS_XDATA, <<"submit">>} ->
|
|
case is_allowed_log_change(XEl, StateData, From) andalso
|
|
is_allowed_persistent_change(XEl, StateData, From)
|
|
andalso
|
|
is_allowed_room_name_desc_limits(XEl, StateData)
|
|
andalso
|
|
is_password_settings_correct(XEl, StateData)
|
|
of
|
|
true -> set_config(XEl, StateData);
|
|
false -> {error, ?ERR_NOT_ACCEPTABLE}
|
|
end;
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
[#xmlel{name = <<"destroy">>} = SubEl1] ->
|
|
?INFO_MSG("Destroyed MUC room ~s by the owner ~s",
|
|
[jlib:jid_to_string(StateData#state.jid),
|
|
jlib:jid_to_string(From)]),
|
|
add_to_log(room_existence, destroyed, StateData),
|
|
destroy_room(SubEl1, StateData);
|
|
Items ->
|
|
process_admin_items_set(From, Items, Lang, StateData)
|
|
end;
|
|
_ ->
|
|
ErrText = <<"Owner privileges required">>,
|
|
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
|
|
end;
|
|
process_iq_owner(From, get, Lang, SubEl, StateData) ->
|
|
FAffiliation = get_affiliation(From, StateData),
|
|
case FAffiliation of
|
|
owner ->
|
|
#xmlel{children = Els} = SubEl,
|
|
case xml:remove_cdata(Els) of
|
|
[] -> get_config(Lang, StateData, From);
|
|
[Item] ->
|
|
case xml:get_tag_attr(<<"affiliation">>, Item) of
|
|
false -> {error, ?ERR_BAD_REQUEST};
|
|
{value, StrAffiliation} ->
|
|
case catch list_to_affiliation(StrAffiliation) of
|
|
{'EXIT', _} ->
|
|
ErrText = iolist_to_binary(
|
|
io_lib:format(
|
|
translate:translate(
|
|
Lang,
|
|
<<"Invalid affiliation: ~s">>),
|
|
[StrAffiliation])),
|
|
{error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)};
|
|
SAffiliation ->
|
|
Items = items_with_affiliation(SAffiliation,
|
|
StateData),
|
|
{result, Items, StateData}
|
|
end
|
|
end;
|
|
_ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED}
|
|
end;
|
|
_ ->
|
|
ErrText = <<"Owner privileges required">>,
|
|
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
|
|
end.
|
|
|
|
is_allowed_log_change(XEl, StateData, From) ->
|
|
case lists:keymember(<<"muc#roomconfig_enablelogging">>,
|
|
1, jlib:parse_xdata_submit(XEl))
|
|
of
|
|
false -> true;
|
|
true ->
|
|
allow ==
|
|
mod_muc_log:check_access_log(StateData#state.server_host,
|
|
From)
|
|
end.
|
|
|
|
is_allowed_persistent_change(XEl, StateData, From) ->
|
|
case
|
|
lists:keymember(<<"muc#roomconfig_persistentroom">>, 1,
|
|
jlib:parse_xdata_submit(XEl))
|
|
of
|
|
false -> true;
|
|
true ->
|
|
{_AccessRoute, _AccessCreate, _AccessAdmin,
|
|
AccessPersistent} =
|
|
StateData#state.access,
|
|
allow ==
|
|
acl:match_rule(StateData#state.server_host,
|
|
AccessPersistent, From)
|
|
end.
|
|
|
|
%% Check if the Room Name and Room Description defined in the Data Form
|
|
%% are conformant to the configured limits
|
|
is_allowed_room_name_desc_limits(XEl, StateData) ->
|
|
IsNameAccepted = case
|
|
lists:keysearch(<<"muc#roomconfig_roomname">>, 1,
|
|
jlib:parse_xdata_submit(XEl))
|
|
of
|
|
{value, {_, [N]}} ->
|
|
byte_size(N) =<
|
|
gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, max_room_name,
|
|
fun(infinity) -> infinity;
|
|
(I) when is_integer(I),
|
|
I>0 -> I
|
|
end, infinity);
|
|
_ -> true
|
|
end,
|
|
IsDescAccepted = case
|
|
lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1,
|
|
jlib:parse_xdata_submit(XEl))
|
|
of
|
|
{value, {_, [D]}} ->
|
|
byte_size(D) =<
|
|
gen_mod:get_module_opt(StateData#state.server_host,
|
|
mod_muc, max_room_desc,
|
|
fun(infinity) -> infinity;
|
|
(I) when is_integer(I),
|
|
I>0 ->
|
|
I
|
|
end, infinity);
|
|
_ -> true
|
|
end,
|
|
IsNameAccepted and IsDescAccepted.
|
|
|
|
%% Return false if:
|
|
%% "the password for a password-protected room is blank"
|
|
is_password_settings_correct(XEl, StateData) ->
|
|
Config = StateData#state.config,
|
|
OldProtected = Config#config.password_protected,
|
|
OldPassword = Config#config.password,
|
|
NewProtected = case
|
|
lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>,
|
|
1, jlib:parse_xdata_submit(XEl))
|
|
of
|
|
{value, {_, [<<"1">>]}} -> true;
|
|
{value, {_, [<<"0">>]}} -> false;
|
|
_ -> undefined
|
|
end,
|
|
NewPassword = case
|
|
lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1,
|
|
jlib:parse_xdata_submit(XEl))
|
|
of
|
|
{value, {_, [P]}} -> P;
|
|
_ -> undefined
|
|
end,
|
|
case {OldProtected, NewProtected, OldPassword,
|
|
NewPassword}
|
|
of
|
|
{true, undefined, <<"">>, undefined} -> false;
|
|
{true, undefined, _, <<"">>} -> false;
|
|
{_, true, <<"">>, undefined} -> false;
|
|
{_, true, _, <<"">>} -> false;
|
|
_ -> true
|
|
end.
|
|
|
|
-define(XFIELD(Type, Label, Var, Val),
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"type">>, Type},
|
|
{<<"label">>, translate:translate(Lang, Label)},
|
|
{<<"var">>, Var}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children = [{xmlcdata, Val}]}]}).
|
|
|
|
-define(BOOLXFIELD(Label, Var, Val),
|
|
?XFIELD(<<"boolean">>, Label, Var,
|
|
case Val of
|
|
true -> <<"1">>;
|
|
_ -> <<"0">>
|
|
end)).
|
|
|
|
-define(STRINGXFIELD(Label, Var, Val),
|
|
?XFIELD(<<"text-single">>, Label, Var, Val)).
|
|
|
|
-define(PRIVATEXFIELD(Label, Var, Val),
|
|
?XFIELD(<<"text-private">>, Label, Var, Val)).
|
|
|
|
-define(JIDMULTIXFIELD(Label, Var, JIDList),
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"type">>, <<"jid-multi">>},
|
|
{<<"label">>, translate:translate(Lang, Label)},
|
|
{<<"var">>, Var}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children = [{xmlcdata, jlib:jid_to_string(JID)}]}
|
|
|| JID <- JIDList]}).
|
|
|
|
get_default_room_maxusers(RoomState) ->
|
|
DefRoomOpts =
|
|
gen_mod:get_module_opt(RoomState#state.server_host,
|
|
mod_muc, default_room_options,
|
|
fun(L) when is_list(L) -> L end,
|
|
[]),
|
|
RoomState2 = set_opts(DefRoomOpts, RoomState),
|
|
(RoomState2#state.config)#config.max_users.
|
|
|
|
get_config(Lang, StateData, From) ->
|
|
{_AccessRoute, _AccessCreate, _AccessAdmin,
|
|
AccessPersistent} =
|
|
StateData#state.access,
|
|
ServiceMaxUsers = get_service_max_users(StateData),
|
|
DefaultRoomMaxUsers =
|
|
get_default_room_maxusers(StateData),
|
|
Config = StateData#state.config,
|
|
{MaxUsersRoomInteger, MaxUsersRoomString} = case
|
|
get_max_users(StateData)
|
|
of
|
|
N when is_integer(N) ->
|
|
{N,
|
|
jlib:integer_to_binary(N)};
|
|
_ -> {0, <<"none">>}
|
|
end,
|
|
Res = [#xmlel{name = <<"title">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
iolist_to_binary(
|
|
io_lib:format(
|
|
translate:translate(
|
|
Lang,
|
|
<<"Configuration of room ~s">>),
|
|
[jlib:jid_to_string(StateData#state.jid)]))}]},
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"type">>, <<"hidden">>},
|
|
{<<"var">>, <<"FORM_TYPE">>}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"http://jabber.org/protocol/muc#roomconfig">>}]}]},
|
|
?STRINGXFIELD(<<"Room title">>,
|
|
<<"muc#roomconfig_roomname">>, (Config#config.title)),
|
|
?STRINGXFIELD(<<"Room description">>,
|
|
<<"muc#roomconfig_roomdesc">>,
|
|
(Config#config.description))]
|
|
++
|
|
case acl:match_rule(StateData#state.server_host,
|
|
AccessPersistent, From)
|
|
of
|
|
allow ->
|
|
[?BOOLXFIELD(<<"Make room persistent">>,
|
|
<<"muc#roomconfig_persistentroom">>,
|
|
(Config#config.persistent))];
|
|
_ -> []
|
|
end
|
|
++
|
|
[?BOOLXFIELD(<<"Make room public searchable">>,
|
|
<<"muc#roomconfig_publicroom">>,
|
|
(Config#config.public)),
|
|
?BOOLXFIELD(<<"Make participants list public">>,
|
|
<<"public_list">>, (Config#config.public_list)),
|
|
?BOOLXFIELD(<<"Make room password protected">>,
|
|
<<"muc#roomconfig_passwordprotectedroom">>,
|
|
(Config#config.password_protected)),
|
|
?PRIVATEXFIELD(<<"Password">>,
|
|
<<"muc#roomconfig_roomsecret">>,
|
|
case Config#config.password_protected of
|
|
true -> Config#config.password;
|
|
false -> <<"">>
|
|
end),
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"type">>, <<"list-single">>},
|
|
{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"Maximum Number of Occupants">>)},
|
|
{<<"var">>, <<"muc#roomconfig_maxusers">>}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children = [{xmlcdata, MaxUsersRoomString}]}]
|
|
++
|
|
if is_integer(ServiceMaxUsers) -> [];
|
|
true ->
|
|
[#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"No limit">>)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"none">>}]}]}]
|
|
end
|
|
++
|
|
[#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
jlib:integer_to_binary(N)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>,
|
|
attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
jlib:integer_to_binary(N)}]}]}
|
|
|| N
|
|
<- lists:usort([ServiceMaxUsers,
|
|
DefaultRoomMaxUsers,
|
|
MaxUsersRoomInteger
|
|
| ?MAX_USERS_DEFAULT_LIST]),
|
|
N =< ServiceMaxUsers]},
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"type">>, <<"list-single">>},
|
|
{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"Present real Jabber IDs to">>)},
|
|
{<<"var">>, <<"muc#roomconfig_whois">>}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
if Config#config.anonymous ->
|
|
<<"moderators">>;
|
|
true -> <<"anyone">>
|
|
end}]},
|
|
#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"moderators only">>)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"moderators">>}]}]},
|
|
#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"anyone">>)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"anyone">>}]}]}]},
|
|
?BOOLXFIELD(<<"Make room members-only">>,
|
|
<<"muc#roomconfig_membersonly">>,
|
|
(Config#config.members_only)),
|
|
?BOOLXFIELD(<<"Make room moderated">>,
|
|
<<"muc#roomconfig_moderatedroom">>,
|
|
(Config#config.moderated)),
|
|
?BOOLXFIELD(<<"Default users as participants">>,
|
|
<<"members_by_default">>,
|
|
(Config#config.members_by_default)),
|
|
?BOOLXFIELD(<<"Allow users to change the subject">>,
|
|
<<"muc#roomconfig_changesubject">>,
|
|
(Config#config.allow_change_subj)),
|
|
?BOOLXFIELD(<<"Allow users to send private messages">>,
|
|
<<"allow_private_messages">>,
|
|
(Config#config.allow_private_messages)),
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"type">>, <<"list-single">>},
|
|
{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"Allow visitors to send private messages to">>)},
|
|
{<<"var">>,
|
|
<<"allow_private_messages_from_visitors">>}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
case
|
|
Config#config.allow_private_messages_from_visitors
|
|
of
|
|
anyone -> <<"anyone">>;
|
|
moderators -> <<"moderators">>;
|
|
nobody -> <<"nobody">>
|
|
end}]},
|
|
#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"nobody">>)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata, <<"nobody">>}]}]},
|
|
#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"moderators only">>)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"moderators">>}]}]},
|
|
#xmlel{name = <<"option">>,
|
|
attrs =
|
|
[{<<"label">>,
|
|
translate:translate(Lang,
|
|
<<"anyone">>)}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"anyone">>}]}]}]},
|
|
?BOOLXFIELD(<<"Allow users to query other users">>,
|
|
<<"allow_query_users">>,
|
|
(Config#config.allow_query_users)),
|
|
?BOOLXFIELD(<<"Allow users to send invites">>,
|
|
<<"muc#roomconfig_allowinvites">>,
|
|
(Config#config.allow_user_invites)),
|
|
?BOOLXFIELD(<<"Allow visitors to send status text in "
|
|
"presence updates">>,
|
|
<<"muc#roomconfig_allowvisitorstatus">>,
|
|
(Config#config.allow_visitor_status)),
|
|
?BOOLXFIELD(<<"Allow visitors to change nickname">>,
|
|
<<"muc#roomconfig_allowvisitornickchange">>,
|
|
(Config#config.allow_visitor_nickchange)),
|
|
?BOOLXFIELD(<<"Allow visitors to send voice requests">>,
|
|
<<"muc#roomconfig_allowvoicerequests">>,
|
|
(Config#config.allow_voice_requests)),
|
|
?STRINGXFIELD(<<"Minimum interval between voice requests "
|
|
"(in seconds)">>,
|
|
<<"muc#roomconfig_voicerequestmininterval">>,
|
|
(jlib:integer_to_binary(Config#config.voice_request_min_interval)))]
|
|
++
|
|
case ejabberd_captcha:is_feature_available() of
|
|
true ->
|
|
[?BOOLXFIELD(<<"Make room CAPTCHA protected">>,
|
|
<<"captcha_protected">>,
|
|
(Config#config.captcha_protected))];
|
|
false -> []
|
|
end
|
|
++
|
|
[?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>,
|
|
<<"muc#roomconfig_captcha_whitelist">>,
|
|
((?SETS):to_list(Config#config.captcha_whitelist)))]
|
|
++
|
|
case
|
|
mod_muc_log:check_access_log(StateData#state.server_host,
|
|
From)
|
|
of
|
|
allow ->
|
|
[?BOOLXFIELD(<<"Enable logging">>,
|
|
<<"muc#roomconfig_enablelogging">>,
|
|
(Config#config.logging))];
|
|
_ -> []
|
|
end,
|
|
{result,
|
|
[#xmlel{name = <<"instructions">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
translate:translate(Lang,
|
|
<<"You need an x:data capable client to "
|
|
"configure room">>)}]},
|
|
#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}],
|
|
children = Res}],
|
|
StateData}.
|
|
|
|
set_config(XEl, StateData) ->
|
|
XData = jlib:parse_xdata_submit(XEl),
|
|
case XData of
|
|
invalid -> {error, ?ERR_BAD_REQUEST};
|
|
_ ->
|
|
case set_xoption(XData, StateData#state.config) of
|
|
#config{} = Config ->
|
|
Res = change_config(Config, StateData),
|
|
{result, _, NSD} = Res,
|
|
Type = case {(StateData#state.config)#config.logging,
|
|
Config#config.logging}
|
|
of
|
|
{true, false} -> roomconfig_change_disabledlogging;
|
|
{false, true} -> roomconfig_change_enabledlogging;
|
|
{_, _} -> roomconfig_change
|
|
end,
|
|
Users = [{U#user.jid, U#user.nick, U#user.role}
|
|
|| {_, U} <- (?DICT):to_list(StateData#state.users)],
|
|
add_to_log(Type, Users, NSD),
|
|
Res;
|
|
Err -> Err
|
|
end
|
|
end.
|
|
|
|
-define(SET_BOOL_XOPT(Opt, Val),
|
|
case Val of
|
|
<<"0">> ->
|
|
set_xoption(Opts, Config#config{Opt = false});
|
|
<<"false">> ->
|
|
set_xoption(Opts, Config#config{Opt = false});
|
|
<<"1">> -> set_xoption(Opts, Config#config{Opt = true});
|
|
<<"true">> ->
|
|
set_xoption(Opts, Config#config{Opt = true});
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end).
|
|
|
|
-define(SET_NAT_XOPT(Opt, Val),
|
|
case catch jlib:binary_to_integer(Val) of
|
|
I when is_integer(I), I > 0 ->
|
|
set_xoption(Opts, Config#config{Opt = I});
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end).
|
|
|
|
-define(SET_STRING_XOPT(Opt, Val),
|
|
set_xoption(Opts, Config#config{Opt = Val})).
|
|
|
|
-define(SET_JIDMULTI_XOPT(Opt, Vals),
|
|
begin
|
|
Set = lists:foldl(fun ({U, S, R}, Set1) ->
|
|
(?SETS):add_element({U, S, R}, Set1);
|
|
(#jid{luser = U, lserver = S, lresource = R},
|
|
Set1) ->
|
|
(?SETS):add_element({U, S, R}, Set1);
|
|
(_, Set1) -> Set1
|
|
end,
|
|
(?SETS):empty(), Vals),
|
|
set_xoption(Opts, Config#config{Opt = Set})
|
|
end).
|
|
|
|
set_xoption([], Config) -> Config;
|
|
set_xoption([{<<"muc#roomconfig_roomname">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_STRING_XOPT(title, Val);
|
|
set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_STRING_XOPT(description, Val);
|
|
set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_change_subj, Val);
|
|
set_xoption([{<<"allow_query_users">>, [Val]} | Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_query_users, Val);
|
|
set_xoption([{<<"allow_private_messages">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_private_messages, Val);
|
|
set_xoption([{<<"allow_private_messages_from_visitors">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
case Val of
|
|
<<"anyone">> ->
|
|
?SET_STRING_XOPT(allow_private_messages_from_visitors,
|
|
anyone);
|
|
<<"moderators">> ->
|
|
?SET_STRING_XOPT(allow_private_messages_from_visitors,
|
|
moderators);
|
|
<<"nobody">> ->
|
|
?SET_STRING_XOPT(allow_private_messages_from_visitors,
|
|
nobody);
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_visitor_status, Val);
|
|
set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_visitor_nickchange, Val);
|
|
set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(public, Val);
|
|
set_xoption([{<<"public_list">>, [Val]} | Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(public_list, Val);
|
|
set_xoption([{<<"muc#roomconfig_persistentroom">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(persistent, Val);
|
|
set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(moderated, Val);
|
|
set_xoption([{<<"members_by_default">>, [Val]} | Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(members_by_default, Val);
|
|
set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(members_only, Val);
|
|
set_xoption([{<<"captcha_protected">>, [Val]} | Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(captcha_protected, Val);
|
|
set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_user_invites, Val);
|
|
set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(password_protected, Val);
|
|
set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_STRING_XOPT(password, Val);
|
|
set_xoption([{<<"anonymous">>, [Val]} | Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(anonymous, Val);
|
|
set_xoption([{<<"muc#roomconfig_allowvoicerequests">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(allow_voice_requests, Val);
|
|
set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>,
|
|
[Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_NAT_XOPT(voice_request_min_interval, Val);
|
|
set_xoption([{<<"muc#roomconfig_whois">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
case Val of
|
|
<<"moderators">> ->
|
|
?SET_BOOL_XOPT(anonymous,
|
|
(iolist_to_binary(integer_to_list(1))));
|
|
<<"anyone">> ->
|
|
?SET_BOOL_XOPT(anonymous,
|
|
(iolist_to_binary(integer_to_list(0))));
|
|
_ -> {error, ?ERR_BAD_REQUEST}
|
|
end;
|
|
set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
case Val of
|
|
<<"none">> -> ?SET_STRING_XOPT(max_users, none);
|
|
_ -> ?SET_NAT_XOPT(max_users, Val)
|
|
end;
|
|
set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]}
|
|
| Opts],
|
|
Config) ->
|
|
?SET_BOOL_XOPT(logging, Val);
|
|
set_xoption([{<<"muc#roomconfig_captcha_whitelist">>,
|
|
Vals}
|
|
| Opts],
|
|
Config) ->
|
|
JIDs = [jlib:string_to_jid(Val) || Val <- Vals],
|
|
?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs);
|
|
set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) ->
|
|
set_xoption(Opts, Config);
|
|
set_xoption([_ | _Opts], _Config) ->
|
|
{error, ?ERR_BAD_REQUEST}.
|
|
|
|
change_config(Config, StateData) ->
|
|
NSD = StateData#state{config = Config},
|
|
case {(StateData#state.config)#config.persistent,
|
|
Config#config.persistent}
|
|
of
|
|
{_, true} ->
|
|
mod_muc:store_room(NSD#state.server_host,
|
|
NSD#state.host, NSD#state.room, make_opts(NSD));
|
|
{true, false} ->
|
|
mod_muc:forget_room(NSD#state.server_host,
|
|
NSD#state.host, NSD#state.room);
|
|
{false, false} -> ok
|
|
end,
|
|
case {(StateData#state.config)#config.members_only,
|
|
Config#config.members_only}
|
|
of
|
|
{false, true} ->
|
|
NSD1 = remove_nonmembers(NSD), {result, [], NSD1};
|
|
_ -> {result, [], NSD}
|
|
end.
|
|
|
|
remove_nonmembers(StateData) ->
|
|
lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) ->
|
|
Affiliation = get_affiliation(JID, SD),
|
|
case Affiliation of
|
|
none ->
|
|
catch send_kickban_presence(<<"">>, JID, <<"">>,
|
|
<<"322">>, SD),
|
|
set_role(JID, none, SD);
|
|
_ -> SD
|
|
end
|
|
end,
|
|
StateData, (?DICT):to_list(StateData#state.users)).
|
|
|
|
set_opts([], StateData) -> StateData;
|
|
set_opts([{Opt, Val} | Opts], StateData) ->
|
|
NSD = case Opt of
|
|
title ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{title =
|
|
Val}};
|
|
description ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{description
|
|
= Val}};
|
|
allow_change_subj ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_change_subj
|
|
= Val}};
|
|
allow_query_users ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_query_users
|
|
= Val}};
|
|
allow_private_messages ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_private_messages
|
|
= Val}};
|
|
allow_private_messages_from_visitors ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_private_messages_from_visitors
|
|
= Val}};
|
|
allow_visitor_nickchange ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_visitor_nickchange
|
|
= Val}};
|
|
allow_visitor_status ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_visitor_status
|
|
= Val}};
|
|
public ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{public =
|
|
Val}};
|
|
public_list ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{public_list
|
|
= Val}};
|
|
persistent ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{persistent =
|
|
Val}};
|
|
moderated ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{moderated =
|
|
Val}};
|
|
members_by_default ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{members_by_default
|
|
= Val}};
|
|
members_only ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{members_only
|
|
= Val}};
|
|
allow_user_invites ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_user_invites
|
|
= Val}};
|
|
password_protected ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{password_protected
|
|
= Val}};
|
|
captcha_protected ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{captcha_protected
|
|
= Val}};
|
|
password ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{password =
|
|
Val}};
|
|
anonymous ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{anonymous =
|
|
Val}};
|
|
logging ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{logging =
|
|
Val}};
|
|
captcha_whitelist ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{captcha_whitelist
|
|
=
|
|
(?SETS):from_list(Val)}};
|
|
allow_voice_requests ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{allow_voice_requests
|
|
= Val}};
|
|
voice_request_min_interval ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{voice_request_min_interval
|
|
= Val}};
|
|
max_users ->
|
|
ServiceMaxUsers = get_service_max_users(StateData),
|
|
MaxUsers = if Val =< ServiceMaxUsers -> Val;
|
|
true -> ServiceMaxUsers
|
|
end,
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{max_users =
|
|
MaxUsers}};
|
|
vcard ->
|
|
StateData#state{config =
|
|
(StateData#state.config)#config{vcard =
|
|
Val}};
|
|
affiliations ->
|
|
StateData#state{affiliations = (?DICT):from_list(Val)};
|
|
subject -> StateData#state{subject = Val};
|
|
subject_author -> StateData#state{subject_author = Val};
|
|
_ -> StateData
|
|
end,
|
|
set_opts(Opts, NSD).
|
|
|
|
-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}).
|
|
|
|
|
|
make_opts(StateData) ->
|
|
Config = StateData#state.config,
|
|
[?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description),
|
|
?MAKE_CONFIG_OPT(allow_change_subj),
|
|
?MAKE_CONFIG_OPT(allow_query_users),
|
|
?MAKE_CONFIG_OPT(allow_private_messages),
|
|
?MAKE_CONFIG_OPT(allow_private_messages_from_visitors),
|
|
?MAKE_CONFIG_OPT(allow_visitor_status),
|
|
?MAKE_CONFIG_OPT(allow_visitor_nickchange),
|
|
?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(captcha_protected),
|
|
?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous),
|
|
?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users),
|
|
?MAKE_CONFIG_OPT(allow_voice_requests),
|
|
?MAKE_CONFIG_OPT(voice_request_min_interval),
|
|
?MAKE_CONFIG_OPT(vcard),
|
|
{captcha_whitelist,
|
|
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
|
|
{affiliations,
|
|
(?DICT):to_list(StateData#state.affiliations)},
|
|
{subject, StateData#state.subject},
|
|
{subject_author, StateData#state.subject_author}].
|
|
|
|
destroy_room(DEl, StateData) ->
|
|
lists:foreach(fun ({_LJID, Info}) ->
|
|
Nick = Info#user.nick,
|
|
ItemAttrs = [{<<"affiliation">>, <<"none">>},
|
|
{<<"role">>, <<"none">>}],
|
|
Packet = #xmlel{name = <<"presence">>,
|
|
attrs =
|
|
[{<<"type">>, <<"unavailable">>}],
|
|
children =
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>,
|
|
?NS_MUC_USER}],
|
|
children =
|
|
[#xmlel{name =
|
|
<<"item">>,
|
|
attrs =
|
|
ItemAttrs,
|
|
children =
|
|
[]},
|
|
DEl]}]},
|
|
ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid,
|
|
Nick),
|
|
Info#user.jid, Packet)
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)),
|
|
case (StateData#state.config)#config.persistent of
|
|
true ->
|
|
mod_muc:forget_room(StateData#state.server_host,
|
|
StateData#state.host, StateData#state.room);
|
|
false -> ok
|
|
end,
|
|
{result, [], stop}.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Disco
|
|
|
|
-define(FEATURE(Var),
|
|
#xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}],
|
|
children = []}).
|
|
|
|
-define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse),
|
|
case Opt of
|
|
true -> ?FEATURE(Fiftrue);
|
|
false -> ?FEATURE(Fiffalse)
|
|
end).
|
|
|
|
process_iq_disco_info(_From, set, _Lang, _StateData) ->
|
|
{error, ?ERR_NOT_ALLOWED};
|
|
process_iq_disco_info(_From, get, Lang, StateData) ->
|
|
Config = StateData#state.config,
|
|
{result,
|
|
[#xmlel{name = <<"identity">>,
|
|
attrs =
|
|
[{<<"category">>, <<"conference">>},
|
|
{<<"type">>, <<"text">>},
|
|
{<<"name">>, get_title(StateData)}],
|
|
children = []},
|
|
#xmlel{name = <<"feature">>,
|
|
attrs = [{<<"var">>, ?NS_VCARD}], children = []},
|
|
#xmlel{name = <<"feature">>,
|
|
attrs = [{<<"var">>, ?NS_MUC}], children = []},
|
|
?CONFIG_OPT_TO_FEATURE((Config#config.public),
|
|
<<"muc_public">>, <<"muc_hidden">>),
|
|
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
|
|
<<"muc_persistent">>, <<"muc_temporary">>),
|
|
?CONFIG_OPT_TO_FEATURE((Config#config.members_only),
|
|
<<"muc_membersonly">>, <<"muc_open">>),
|
|
?CONFIG_OPT_TO_FEATURE((Config#config.anonymous),
|
|
<<"muc_semianonymous">>, <<"muc_nonanonymous">>),
|
|
?CONFIG_OPT_TO_FEATURE((Config#config.moderated),
|
|
<<"muc_moderated">>, <<"muc_unmoderated">>),
|
|
?CONFIG_OPT_TO_FEATURE((Config#config.password_protected),
|
|
<<"muc_passwordprotected">>, <<"muc_unsecured">>)]
|
|
++ iq_disco_info_extras(Lang, StateData),
|
|
StateData}.
|
|
|
|
-define(RFIELDT(Type, Var, Val),
|
|
#xmlel{name = <<"field">>,
|
|
attrs = [{<<"type">>, Type}, {<<"var">>, Var}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children = [{xmlcdata, Val}]}]}).
|
|
|
|
-define(RFIELD(Label, Var, Val),
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"label">>, translate:translate(Lang, Label)},
|
|
{<<"var">>, Var}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children = [{xmlcdata, Val}]}]}).
|
|
|
|
iq_disco_info_extras(Lang, StateData) ->
|
|
Len = (?DICT):size(StateData#state.users),
|
|
RoomDescription =
|
|
(StateData#state.config)#config.description,
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}],
|
|
children =
|
|
[?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>,
|
|
<<"http://jabber.org/protocol/muc#roominfo">>),
|
|
?RFIELD(<<"Room description">>,
|
|
<<"muc#roominfo_description">>, RoomDescription),
|
|
?RFIELD(<<"Number of occupants">>,
|
|
<<"muc#roominfo_occupants">>,
|
|
(iolist_to_binary(integer_to_list(Len))))]}].
|
|
|
|
process_iq_disco_items(_From, set, _Lang, _StateData) ->
|
|
{error, ?ERR_NOT_ALLOWED};
|
|
process_iq_disco_items(From, get, _Lang, StateData) ->
|
|
case (StateData#state.config)#config.public_list of
|
|
true ->
|
|
{result, get_mucroom_disco_items(StateData), StateData};
|
|
_ ->
|
|
case is_occupant_or_admin(From, StateData) of
|
|
true ->
|
|
{result, get_mucroom_disco_items(StateData), StateData};
|
|
_ -> {error, ?ERR_FORBIDDEN}
|
|
end
|
|
end.
|
|
|
|
process_iq_captcha(_From, get, _Lang, _SubEl,
|
|
_StateData) ->
|
|
{error, ?ERR_NOT_ALLOWED};
|
|
process_iq_captcha(_From, set, _Lang, SubEl,
|
|
StateData) ->
|
|
case ejabberd_captcha:process_reply(SubEl) of
|
|
ok -> {result, [], StateData};
|
|
_ -> {error, ?ERR_NOT_ACCEPTABLE}
|
|
end.
|
|
|
|
process_iq_vcard(_From, get, _Lang, _SubEl, StateData) ->
|
|
#state{config = #config{vcard = VCardRaw}} = StateData,
|
|
case xml_stream:parse_element(VCardRaw) of
|
|
#xmlel{children = VCardEls} ->
|
|
{result, VCardEls, StateData};
|
|
{error, _} ->
|
|
{result, [], StateData}
|
|
end;
|
|
process_iq_vcard(From, set, Lang, SubEl, StateData) ->
|
|
case get_affiliation(From, StateData) of
|
|
owner ->
|
|
VCardRaw = xml:element_to_binary(SubEl),
|
|
Config = StateData#state.config,
|
|
NewConfig = Config#config{vcard = VCardRaw},
|
|
change_config(NewConfig, StateData);
|
|
_ ->
|
|
ErrText = <<"Owner privileges required">>,
|
|
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
|
|
end.
|
|
|
|
get_title(StateData) ->
|
|
case (StateData#state.config)#config.title of
|
|
<<"">> -> StateData#state.room;
|
|
Name -> Name
|
|
end.
|
|
|
|
get_roomdesc_reply(JID, StateData, Tail) ->
|
|
IsOccupantOrAdmin = is_occupant_or_admin(JID,
|
|
StateData),
|
|
if (StateData#state.config)#config.public or
|
|
IsOccupantOrAdmin ->
|
|
if (StateData#state.config)#config.public_list or
|
|
IsOccupantOrAdmin ->
|
|
{item, <<(get_title(StateData))/binary,Tail/binary>>};
|
|
true -> {item, get_title(StateData)}
|
|
end;
|
|
true -> false
|
|
end.
|
|
|
|
get_roomdesc_tail(StateData, Lang) ->
|
|
Desc = case (StateData#state.config)#config.public of
|
|
true -> <<"">>;
|
|
_ -> translate:translate(Lang, <<"private, ">>)
|
|
end,
|
|
Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0,
|
|
StateData#state.users),
|
|
<<" (", Desc/binary,
|
|
(iolist_to_binary(integer_to_list(Len)))/binary, ")">>.
|
|
|
|
get_mucroom_disco_items(StateData) ->
|
|
lists:map(fun ({_LJID, Info}) ->
|
|
Nick = Info#user.nick,
|
|
#xmlel{name = <<"item">>,
|
|
attrs =
|
|
[{<<"jid">>,
|
|
jlib:jid_to_string({StateData#state.room,
|
|
StateData#state.host,
|
|
Nick})},
|
|
{<<"name">>, Nick}],
|
|
children = []}
|
|
end,
|
|
(?DICT):to_list(StateData#state.users)).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Voice request support
|
|
|
|
is_voice_request(Els) ->
|
|
lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} =
|
|
El,
|
|
false) ->
|
|
case xml:get_attr_s(<<"xmlns">>, Attrs) of
|
|
?NS_XDATA ->
|
|
case jlib:parse_xdata_submit(El) of
|
|
[_ | _] = Fields ->
|
|
case {lists:keysearch(<<"FORM_TYPE">>, 1,
|
|
Fields),
|
|
lists:keysearch(<<"muc#role">>, 1,
|
|
Fields)}
|
|
of
|
|
{{value,
|
|
{_,
|
|
[<<"http://jabber.org/protocol/muc#request">>]}},
|
|
{value, {_, [<<"participant">>]}}} ->
|
|
true;
|
|
_ -> false
|
|
end;
|
|
_ -> false
|
|
end;
|
|
_ -> false
|
|
end;
|
|
(_, Acc) -> Acc
|
|
end,
|
|
false, Els).
|
|
|
|
prepare_request_form(Requester, Nick, Lang) ->
|
|
#xmlel{name = <<"message">>,
|
|
attrs = [{<<"type">>, <<"normal">>}],
|
|
children =
|
|
[#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}],
|
|
children =
|
|
[#xmlel{name = <<"title">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
translate:translate(Lang,
|
|
<<"Voice request">>)}]},
|
|
#xmlel{name = <<"instructions">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
translate:translate(Lang,
|
|
<<"Either approve or decline the voice "
|
|
"request.">>)}]},
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"var">>, <<"FORM_TYPE">>},
|
|
{<<"type">>, <<"hidden">>}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"http://jabber.org/protocol/muc#request">>}]}]},
|
|
#xmlel{name = <<"field">>,
|
|
attrs =
|
|
[{<<"var">>, <<"muc#role">>},
|
|
{<<"type">>, <<"hidden">>}],
|
|
children =
|
|
[#xmlel{name = <<"value">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
<<"participant">>}]}]},
|
|
?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>,
|
|
(jlib:jid_to_string(Requester))),
|
|
?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>,
|
|
Nick),
|
|
?BOOLXFIELD(<<"Grant voice to this person?">>,
|
|
<<"muc#request_allow">>,
|
|
(jlib:binary_to_atom(<<"false">>)))]}]}.
|
|
|
|
send_voice_request(From, StateData) ->
|
|
Moderators = search_role(moderator, StateData),
|
|
FromNick = find_nick_by_jid(From, StateData),
|
|
lists:foreach(fun ({_, User}) ->
|
|
ejabberd_router:route(StateData#state.jid, User#user.jid,
|
|
prepare_request_form(From, FromNick,
|
|
<<"">>))
|
|
end,
|
|
Moderators).
|
|
|
|
is_voice_approvement(Els) ->
|
|
lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} =
|
|
El,
|
|
false) ->
|
|
case xml:get_attr_s(<<"xmlns">>, Attrs) of
|
|
?NS_XDATA ->
|
|
case jlib:parse_xdata_submit(El) of
|
|
[_ | _] = Fs ->
|
|
case {lists:keysearch(<<"FORM_TYPE">>, 1,
|
|
Fs),
|
|
lists:keysearch(<<"muc#role">>, 1,
|
|
Fs),
|
|
lists:keysearch(<<"muc#request_allow">>,
|
|
1, Fs)}
|
|
of
|
|
{{value,
|
|
{_,
|
|
[<<"http://jabber.org/protocol/muc#request">>]}},
|
|
{value, {_, [<<"participant">>]}},
|
|
{value, {_, [Flag]}}}
|
|
when Flag == <<"true">>;
|
|
Flag == <<"1">> ->
|
|
true;
|
|
_ -> false
|
|
end;
|
|
_ -> false
|
|
end;
|
|
_ -> false
|
|
end;
|
|
(_, Acc) -> Acc
|
|
end,
|
|
false, Els).
|
|
|
|
extract_jid_from_voice_approvement(Els) ->
|
|
lists:foldl(fun (#xmlel{name = <<"x">>} = El, error) ->
|
|
Fields = case jlib:parse_xdata_submit(El) of
|
|
invalid -> [];
|
|
Res -> Res
|
|
end,
|
|
lists:foldl(fun ({<<"muc#jid">>, [JIDStr]}, error) ->
|
|
case jlib:string_to_jid(JIDStr) of
|
|
error -> error;
|
|
J -> {ok, J}
|
|
end;
|
|
(_, Acc) -> Acc
|
|
end,
|
|
error, Fields);
|
|
(_, Acc) -> Acc
|
|
end,
|
|
error, Els).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Invitation support
|
|
|
|
is_invitation(Els) ->
|
|
lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} =
|
|
El,
|
|
false) ->
|
|
case xml:get_attr_s(<<"xmlns">>, Attrs) of
|
|
?NS_MUC_USER ->
|
|
case xml:get_subtag(El, <<"invite">>) of
|
|
false -> false;
|
|
_ -> true
|
|
end;
|
|
_ -> false
|
|
end;
|
|
(_, Acc) -> Acc
|
|
end,
|
|
false, Els).
|
|
|
|
check_invitation(From, Els, Lang, StateData) ->
|
|
FAffiliation = get_affiliation(From, StateData),
|
|
CanInvite =
|
|
(StateData#state.config)#config.allow_user_invites
|
|
orelse
|
|
FAffiliation == admin orelse FAffiliation == owner,
|
|
InviteEl = case xml:remove_cdata(Els) of
|
|
[#xmlel{name = <<"x">>, children = Els1} = XEl] ->
|
|
case xml:get_tag_attr_s(<<"xmlns">>, XEl) of
|
|
?NS_MUC_USER -> ok;
|
|
_ -> throw({error, ?ERR_BAD_REQUEST})
|
|
end,
|
|
case xml:remove_cdata(Els1) of
|
|
[#xmlel{name = <<"invite">>} = InviteEl1] -> InviteEl1;
|
|
_ -> throw({error, ?ERR_BAD_REQUEST})
|
|
end;
|
|
_ -> throw({error, ?ERR_BAD_REQUEST})
|
|
end,
|
|
JID = case
|
|
jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>,
|
|
InviteEl))
|
|
of
|
|
error -> throw({error, ?ERR_JID_MALFORMED});
|
|
JID1 -> JID1
|
|
end,
|
|
case CanInvite of
|
|
false -> throw({error, ?ERR_NOT_ALLOWED});
|
|
true ->
|
|
Reason = xml:get_path_s(InviteEl,
|
|
[{elem, <<"reason">>}, cdata]),
|
|
ContinueEl = case xml:get_path_s(InviteEl,
|
|
[{elem, <<"continue">>}])
|
|
of
|
|
<<>> -> [];
|
|
Continue1 -> [Continue1]
|
|
end,
|
|
IEl = [#xmlel{name = <<"invite">>,
|
|
attrs = [{<<"from">>, jlib:jid_to_string(From)}],
|
|
children =
|
|
[#xmlel{name = <<"reason">>, attrs = [],
|
|
children = [{xmlcdata, Reason}]}]
|
|
++ ContinueEl}],
|
|
PasswdEl = case
|
|
(StateData#state.config)#config.password_protected
|
|
of
|
|
true ->
|
|
[#xmlel{name = <<"password">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
(StateData#state.config)#config.password}]}];
|
|
_ -> []
|
|
end,
|
|
Body = #xmlel{name = <<"body">>, attrs = [],
|
|
children =
|
|
[{xmlcdata,
|
|
iolist_to_binary(
|
|
[io_lib:format(
|
|
translate:translate(
|
|
Lang,
|
|
<<"~s invites you to the room ~s">>),
|
|
[jlib:jid_to_string(From),
|
|
jlib:jid_to_string({StateData#state.room,
|
|
StateData#state.host,
|
|
<<"">>})]),
|
|
|
|
case
|
|
(StateData#state.config)#config.password_protected
|
|
of
|
|
true ->
|
|
<<", ",
|
|
(translate:translate(Lang,
|
|
<<"the password is">>))/binary,
|
|
" '",
|
|
((StateData#state.config)#config.password)/binary,
|
|
"'">>;
|
|
_ -> <<"">>
|
|
end
|
|
,
|
|
case Reason of
|
|
<<"">> -> <<"">>;
|
|
_ -> <<" (", Reason/binary, ") ">>
|
|
end])}]},
|
|
Msg = #xmlel{name = <<"message">>,
|
|
attrs = [{<<"type">>, <<"normal">>}],
|
|
children =
|
|
[#xmlel{name = <<"x">>,
|
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
|
|
children = IEl ++ PasswdEl},
|
|
#xmlel{name = <<"x">>,
|
|
attrs =
|
|
[{<<"xmlns">>, ?NS_XCONFERENCE},
|
|
{<<"jid">>,
|
|
jlib:jid_to_string({StateData#state.room,
|
|
StateData#state.host,
|
|
<<"">>})}],
|
|
children = [{xmlcdata, Reason}]},
|
|
Body]},
|
|
ejabberd_router:route(StateData#state.jid, JID, Msg),
|
|
JID
|
|
end.
|
|
|
|
%% Handle a message sent to the room by a non-participant.
|
|
%% If it is a decline, send to the inviter.
|
|
%% Otherwise, an error message is sent to the sender.
|
|
handle_roommessage_from_nonparticipant(Packet, Lang,
|
|
StateData, From) ->
|
|
case catch check_decline_invitation(Packet) of
|
|
{true, Decline_data} ->
|
|
send_decline_invitation(Decline_data,
|
|
StateData#state.jid, From);
|
|
_ ->
|
|
send_error_only_occupants(Packet, Lang,
|
|
StateData#state.jid, From)
|
|
end.
|
|
|
|
%% Check in the packet is a decline.
|
|
%% If so, also returns the splitted packet.
|
|
%% This function must be catched,
|
|
%% because it crashes when the packet is not a decline message.
|
|
check_decline_invitation(Packet) ->
|
|
#xmlel{name = <<"message">>} = Packet,
|
|
XEl = xml:get_subtag(Packet, <<"x">>),
|
|
(?NS_MUC_USER) = xml:get_tag_attr_s(<<"xmlns">>, XEl),
|
|
DEl = xml:get_subtag(XEl, <<"decline">>),
|
|
ToString = xml:get_tag_attr_s(<<"to">>, DEl),
|
|
ToJID = jlib:string_to_jid(ToString),
|
|
{true, {Packet, XEl, DEl, ToJID}}.
|
|
|
|
%% Send the decline to the inviter user.
|
|
%% The original stanza must be slightly modified.
|
|
send_decline_invitation({Packet, XEl, DEl, ToJID},
|
|
RoomJID, FromJID) ->
|
|
FromString =
|
|
jlib:jid_to_string(jlib:jid_remove_resource(FromJID)),
|
|
#xmlel{name = <<"decline">>, attrs = DAttrs,
|
|
children = DEls} =
|
|
DEl,
|
|
DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs),
|
|
DAttrs3 = [{<<"from">>, FromString} | DAttrs2],
|
|
DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3,
|
|
children = DEls},
|
|
XEl2 = replace_subelement(XEl, DEl2),
|
|
Packet2 = replace_subelement(Packet, XEl2),
|
|
ejabberd_router:route(RoomJID, ToJID, Packet2).
|
|
|
|
%% Given an element and a new subelement,
|
|
%% replace the instance of the subelement in element with the new subelement.
|
|
replace_subelement(#xmlel{name = Name, attrs = Attrs,
|
|
children = SubEls},
|
|
NewSubEl) ->
|
|
{_, NameNewSubEl, _, _} = NewSubEl,
|
|
SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl),
|
|
#xmlel{name = Name, attrs = Attrs, children = SubEls2}.
|
|
|
|
send_error_only_occupants(Packet, Lang, RoomJID, From) ->
|
|
ErrText =
|
|
<<"Only occupants are allowed to send messages "
|
|
"to the conference">>,
|
|
Err = jlib:make_error_reply(Packet,
|
|
?ERRT_NOT_ACCEPTABLE(Lang, ErrText)),
|
|
ejabberd_router:route(RoomJID, From, Err).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
% Logging
|
|
|
|
add_to_log(Type, Data, StateData)
|
|
when Type == roomconfig_change_disabledlogging ->
|
|
mod_muc_log:add_to_log(StateData#state.server_host,
|
|
roomconfig_change, Data, StateData#state.jid,
|
|
make_opts(StateData));
|
|
add_to_log(Type, Data, StateData) ->
|
|
case (StateData#state.config)#config.logging of
|
|
true ->
|
|
mod_muc_log:add_to_log(StateData#state.server_host,
|
|
Type, Data, StateData#state.jid,
|
|
make_opts(StateData));
|
|
false -> ok
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% Users number checking
|
|
|
|
tab_add_online_user(JID, StateData) ->
|
|
{LUser, LServer, LResource} = jlib:jid_tolower(JID),
|
|
US = {LUser, LServer},
|
|
Room = StateData#state.room,
|
|
Host = StateData#state.host,
|
|
catch ets:insert(muc_online_users,
|
|
#muc_online_users{us = US, resource = LResource,
|
|
room = Room, host = Host}).
|
|
|
|
tab_remove_online_user(JID, StateData) ->
|
|
{LUser, LServer, LResource} = jlib:jid_tolower(JID),
|
|
US = {LUser, LServer},
|
|
Room = StateData#state.room,
|
|
Host = StateData#state.host,
|
|
catch ets:delete_object(muc_online_users,
|
|
#muc_online_users{us = US, resource = LResource,
|
|
room = Room, host = Host}).
|
|
|
|
tab_count_user(JID) ->
|
|
{LUser, LServer, _} = jlib:jid_tolower(JID),
|
|
US = {LUser, LServer},
|
|
case catch ets:select(muc_online_users,
|
|
[{#muc_online_users{us = US, _ = '_'}, [], [[]]}])
|
|
of
|
|
Res when is_list(Res) -> length(Res);
|
|
_ -> 0
|
|
end.
|
|
|
|
element_size(El) ->
|
|
byte_size(xml:element_to_binary(El)).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% Detect messange stanzas that don't have meaninful content
|
|
|
|
has_body_or_subject(Packet) ->
|
|
[] /= lists:dropwhile(fun
|
|
(#xmlel{name = <<"body">>}) -> false;
|
|
(#xmlel{name = <<"subject">>}) -> false;
|
|
(_) -> true
|
|
end, Packet#xmlel.children).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% Multicast
|
|
|
|
send_multiple(From, Server, Users, Packet) ->
|
|
JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
|
|
ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
|