From 32de9a56a5d6113e0db94cf0a38c17d26522bf7e Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Sun, 26 Jun 2016 09:08:37 +0300 Subject: [PATCH] Experimental MUC/Sub support --- include/mod_muc_room.hrl | 3 + include/ns.hrl | 8 + src/mod_muc_room.erl | 1044 +++++++++++++++++++++++++------------- 3 files changed, 708 insertions(+), 347 deletions(-) diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index 4d82856ca..d985f3f3b 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -53,6 +53,7 @@ members_by_default = true :: boolean(), members_only = false :: boolean(), allow_user_invites = false :: boolean(), + allow_subscription = false :: boolean(), password_protected = false :: boolean(), password = <<"">> :: binary(), anonymous = true :: boolean(), @@ -76,6 +77,8 @@ jid :: jid(), nick :: binary(), role :: role(), + is_subscriber = false :: boolean(), + subscriptions = [] :: [binary()], last_presence :: xmlel() }). diff --git a/include/ns.hrl b/include/ns.hrl index c7f556372..4fcf03ddf 100644 --- a/include/ns.hrl +++ b/include/ns.hrl @@ -164,3 +164,11 @@ -define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>). -define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>). -define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>). +-define(NS_MUCSUB, <<"urn:xmpp:mucsub:0">>). +-define(NS_MUCSUB_NODES_PRESENCES, <<"urn:xmpp:mucsub:nodes:presences">>). +-define(NS_MUCSUB_NODES_MESSAGES, <<"urn:xmpp:mucsub:nodes:messages">>). +-define(NS_MUCSUB_NODES_PARTICIPANTS, <<"urn:xmpp:mucsub:nodes:participants">>). +-define(NS_MUCSUB_NODES_AFFILIATIONS, <<"urn:xmpp:mucsub:nodes:affiliations">>). +-define(NS_MUCSUB_NODES_SUBJECT, <<"urn:xmpp:mucsub:nodes:subject">>). +-define(NS_MUCSUB_NODES_CONFIG, <<"urn:xmpp:mucsub:nodes:config">>). +-define(NS_MUCSUB_NODES_SYSTEM, <<"urn:xmpp:mucsub:nodes:system">>). diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index eabff103d..373ae3a60 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -113,13 +113,7 @@ init([Host, ServerHost, Access, Room, HistorySize, 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, + store_room(State1), ?INFO_MSG("Created MUC room ~s@~s by ~s", [Room, Host, jid:to_string(Creator)]), add_to_log(room_existence, created, State1), @@ -268,16 +262,7 @@ normal_state({route, From, <<"">>, StateData), send_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, + store_room(NSD), {next_state, normal_state, NSD}; _ -> {next_state, normal_state, StateData} end; @@ -427,6 +412,7 @@ normal_state({route, From, <<"">>, or (XMLNS == (?NS_DISCO_INFO)) or (XMLNS == (?NS_DISCO_ITEMS)) or (XMLNS == (?NS_VCARD)) + or (XMLNS == (?NS_MUCSUB)) or (XMLNS == (?NS_CAPTCHA)) -> Res1 = case XMLNS of ?NS_MUC_ADMIN -> @@ -444,6 +430,8 @@ normal_state({route, From, <<"">>, process_iq_disco_items(From, Type, Lang, StateData); ?NS_VCARD -> process_iq_vcard(From, Type, Lang, SubEl, StateData); + ?NS_MUCSUB -> + process_iq_mucsub(From, Packet, IQ, StateData); ?NS_CAPTCHA -> process_iq_captcha(From, Type, Lang, SubEl, StateData) end, @@ -458,12 +446,22 @@ normal_state({route, From, <<"">>, XMLNS}], children = Res}]}, SD}; + {ignore, SD} -> {ignore, SD}; + {error, Error, ResStateData} -> + {IQ#iq{type = error, + sub_el = [SubEl, Error]}, + ResStateData}; {error, Error} -> {IQ#iq{type = error, sub_el = [SubEl, Error]}, StateData} end, - ejabberd_router:route(StateData#state.jid, From, jlib:iq_to_xml(IQRes)), + if IQRes /= ignore -> + ejabberd_router:route( + StateData#state.jid, From, jlib:iq_to_xml(IQRes)); + true -> + ok + end, case NewStateData of stop -> {stop, normal, StateData}; _ -> {next_state, normal_state, NewStateData} @@ -678,11 +676,12 @@ handle_event({service_message, Msg}, _StateName, children = [#xmlel{name = <<"body">>, attrs = [], children = [{xmlcdata, Msg}]}]}, - send_multiple( + send_wrapped_multiple( StateData#state.jid, - StateData#state.server_host, StateData#state.users, - MessagePkt), + MessagePkt, + ?NS_MUCSUB_NODES_MESSAGES, + StateData), NSD = add_message_to_history(<<"">>, StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; @@ -866,9 +865,11 @@ terminate(Reason, _StateName, StateData) -> Nick = Info#user.nick, case Reason of shutdown -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet); + send_wrapped(jid:replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, + StateData); _ -> ok end, tab_remove_online_user(LJID, StateData) @@ -894,14 +895,13 @@ process_groupchat_message(From, is_user_allowed_message_nonparticipant(From, StateData) of true -> - {FromNick, Role} = get_participant_data(From, - StateData), - if (Role == moderator) or (Role == participant) or + {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData), + if (Role == moderator) or (Role == participant) or IsSubscriber or ((StateData#state.config)#config.moderated == false) -> - {NewStateData1, IsAllowed} = case check_subject(Packet) - of + Subject = check_subject(Packet), + {NewStateData1, IsAllowed} = case Subject of false -> {StateData, true}; - Subject -> + _ -> case can_change_subject(Role, StateData) @@ -914,16 +914,7 @@ process_groupchat_message(From, 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, + store_room(NSD), {NSD, true}; _ -> {StateData, false} end @@ -942,11 +933,13 @@ process_groupchat_message(From, {next_state, normal_state, StateData}; NewPacket1 -> NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), - send_multiple(jid:replace_resource(StateData#state.jid, - FromNick), - StateData#state.server_host, - StateData#state.users, - NewPacket), + Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES; + true -> ?NS_MUCSUB_NODES_SUBJECT + end, + send_wrapped_multiple( + jid:replace_resource(StateData#state.jid, FromNick), + StateData#state.users, + NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> add_message_to_history(FromNick, From, @@ -1013,9 +1006,9 @@ get_participant_data(From, StateData) -> case (?DICT):find(jid:tolower(From), StateData#state.users) of - {ok, #user{nick = FromNick, role = Role}} -> - {FromNick, Role}; - error -> {<<"">>, moderator} + {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} -> + {FromNick, Role, IsSubscriber}; + error -> {<<"">>, moderator, false} end. process_presence(From, Nick, @@ -1023,6 +1016,7 @@ process_presence(From, Nick, StateData) -> Type0 = fxml:get_attr_s(<<"type">>, Attrs0), IsOnline = is_user_online(From, StateData), + IsSubscriber = is_subscriber(From, StateData), if Type0 == <<"">>; IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) -> case ejabberd_hooks:run_fold(muc_filter_presence, @@ -1059,7 +1053,7 @@ process_presence(From, Nick, Status_el -> fxml:get_tag_cdata(Status_el) end, - remove_online_user(From, NewState, Reason); + remove_online_user(From, NewState, IsSubscriber, Reason); <<"error">> -> ErrorText = <<"It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " @@ -1115,22 +1109,28 @@ process_presence(From, Nick, Nick), From, Err), StateData; - _ -> change_nick(From, Nick, StateData) + _ -> + case is_initial_presence(From, StateData) of + true -> + subscriber_becomes_available( + From, Nick, Packet, StateData); + false -> + change_nick(From, Nick, StateData) + end 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, StateData), - NewState + case is_initial_presence(From, StateData) of + true -> + subscriber_becomes_available( + From, Nick, Packet, StateData); + false -> + Stanza = maybe_strip_status_from_presence( + From, Packet, StateData), + NewState = add_user_presence(From, Stanza, + StateData), + send_new_presence(From, NewState, StateData), + NewState + end end end end, @@ -1140,6 +1140,25 @@ process_presence(From, Nick, {next_state, normal_state, StateData} end. +maybe_strip_status_from_presence(From, Packet, StateData) -> + case {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} of + {false, true} -> + strip_status(Packet); + _Allowed -> Packet + end. + +subscriber_becomes_available(From, Nick, Packet, StateData) -> + Stanza = maybe_strip_status_from_presence(From, Packet, StateData), + State1 = add_user_presence(From, Stanza, StateData), + Aff = get_affiliation(From, State1), + Role = get_default_role(Aff, State1), + State2 = set_role(From, Role, State1), + State3 = set_nick(From, Nick, State2), + send_existing_presences(From, State3), + send_initial_presence(From, State3, StateData), + State3. + close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent andalso (?DICT):to_list(StateData1#state.users) == [] @@ -1157,6 +1176,15 @@ is_user_online(JID, StateData) -> LJID = jid:tolower(JID), (?DICT):is_key(LJID, StateData#state.users). +is_subscriber(JID, StateData) -> + LJID = jid:tolower(JID), + case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{is_subscriber = IsSubscriber}} -> + IsSubscriber; + _ -> + false + end. + %% 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), @@ -1324,6 +1352,7 @@ make_reason(Packet, From, StateData, Reason1) -> iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])). expulse_participant(Packet, From, StateData, Reason1) -> + IsSubscriber = is_subscriber(From, StateData), Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, #xmlel{name = <<"presence">>, @@ -1338,7 +1367,7 @@ expulse_participant(Packet, From, StateData, Reason1) -> Reason2}]}]}, StateData), send_new_presence(From, NewState, StateData), - remove_online_user(From, NewState). + remove_online_user(From, NewState, IsSubscriber). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). @@ -1439,15 +1468,16 @@ set_role(JID, Role, StateData) -> 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), + {lists:foldl( + fun (J, Us) -> + {ok, User} = (?DICT):find(J, Us), + if User#user.last_presence == undefined -> + Us; + true -> + (?DICT):store(J, User#user{role = Role}, Us) + end + end, + StateData#state.users, LJIDs), StateData#state.nicks} end, StateData#state{users = Users, nicks = Nicks}. @@ -1617,27 +1647,66 @@ prepare_room_queue(StateData) -> {empty, _} -> StateData end. -add_online_user(JID, Nick, Role, StateData) -> +update_online_user(JID, #user{nick = Nick, subscriptions = Nodes, + is_subscriber = IsSubscriber} = User, StateData) -> LJID = jid:tolower(JID), - Users = (?DICT):store(LJID, - #user{jid = JID, nick = Nick, role = Role}, - StateData#state.users), - add_to_log(join, Nick, StateData), + Nicks1 = case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{nick = OldNick}} -> + case lists:delete( + LJID, ?DICT:fetch(OldNick, StateData#state.nicks)) of + [] -> + ?DICT:erase(OldNick, StateData#state.nicks); + LJIDs -> + ?DICT:store(OldNick, LJIDs, StateData#state.nicks) + end; + error -> + StateData#state.nicks + end, Nicks = (?DICT):update(Nick, - fun (Entry) -> - case lists:member(LJID, Entry) of - true -> Entry; - false -> [LJID | Entry] - end - end, - [LJID], StateData#state.nicks), + fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, + [LJID], Nicks1), + Users = (?DICT):update(LJID, + fun(U) -> + U#user{nick = Nick, + subscriptions = Nodes, + is_subscriber = IsSubscriber} + end, User, StateData#state.users), + NewStateData = StateData#state{users = Users, nicks = Nicks}, + case {?DICT:find(LJID, StateData#state.users), + ?DICT:find(LJID, NewStateData#state.users)} of + {{ok, #user{nick = Old}}, {ok, #user{nick = New}}} when Old /= New -> + send_nick_changing(JID, Old, NewStateData, true, true); + _ -> + ok + end, + NewStateData. + +add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) -> tab_add_online_user(JID, StateData), - StateData#state{users = Users, nicks = Nicks}. + User = #user{jid = JID, nick = Nick, role = Role, + is_subscriber = IsSubscriber, subscriptions = Nodes}, + StateData1 = update_online_user(JID, User, StateData), + if IsSubscriber -> + store_room(StateData1); + true -> + ok + end, + StateData1. -remove_online_user(JID, StateData) -> - remove_online_user(JID, StateData, <<"">>). +remove_online_user(JID, StateData, IsSubscriber) -> + remove_online_user(JID, StateData, IsSubscriber, <<"">>). -remove_online_user(JID, StateData, Reason) -> +remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) -> + LJID = jid:tolower(JID), + Users = case (?DICT):find(LJID, StateData#state.users) of + {ok, U} -> + (?DICT):store(LJID, U#user{last_presence = undefined}, + StateData#state.users); + error -> + StateData#state.users + end, + StateData#state{users = Users}; +remove_online_user(JID, StateData, _IsSubscriber, Reason) -> LJID = jid:tolower(JID), {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), @@ -1742,10 +1811,12 @@ find_jid_by_nick(Nick, StateData) -> error -> false end. -higher_presence(Pres1, Pres2) -> +higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), - Pri1 > Pri2. + Pri1 > Pri2; +higher_presence(Pres1, Pres2) -> + Pres1 > Pres2. get_priority_from_presence(PresencePacket) -> case fxml:get_subtag(PresencePacket, <<"priority">>) of @@ -1784,9 +1855,10 @@ nick_collision(User, Nick, StateData) -> /= jid:remove_resource(jid:tolower(User))). add_new_user(From, Nick, - #xmlel{attrs = Attrs, children = Els} = Packet, + #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, StateData) -> Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + UserRoomJID = jid:replace_resource(StateData#state.jid, Nick), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), @@ -1802,6 +1874,7 @@ add_new_user(From, Nick, fun(I) when is_integer(I), I>0 -> I end, 10), Collision = nick_collision(From, Nick, StateData), + IsSubscribeRequest = Name /= <<"presence">>, case {(ServiceAffiliation == owner orelse ((Affiliation == admin orelse Affiliation == owner) andalso NUsers < MaxAdminUsers) @@ -1814,91 +1887,116 @@ add_new_user(From, Nick, of {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> Txt = <<"Too many users in this conference">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {false, _, _, _} when NConferences >= MaxConferences -> Txt = <<"You have joined too many conferences">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {false, _, _, _} -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = ?ERR_SERVICE_UNAVAILABLE, + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, _, _, 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/""/ - (jid:replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; + Err = 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, + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, true, _, _} -> ErrText = <<"That nickname is already in use by another occupant">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_CONFLICT(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, _, false, _} -> ErrText = <<"That nickname is registered by another person">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_CONFLICT(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; {_, _, _, Role} -> case check_password(ServiceAffiliation, Affiliation, Els, From, StateData) of true -> - NewState = add_user_presence(From, Packet, - add_online_user(From, Nick, Role, - StateData)), - send_existing_presences(From, NewState), - send_initial_presence(From, NewState, StateData), - Shift = count_stanza_shift(Nick, Els, NewState), - case send_history(From, Shift, NewState) of - true -> ok; - _ -> send_subject(From, 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; + Nodes = get_subscription_nodes(Packet), + NewStateData = + if not IsSubscribeRequest -> + NewState = add_user_presence( + From, Packet, + add_online_user(From, Nick, Role, + IsSubscribeRequest, + Nodes, StateData)), + send_existing_presences(From, NewState), + send_initial_presence(From, NewState, StateData), + Shift = count_stanza_shift(Nick, Els, NewState), + case send_history(From, Shift, NewState) of + true -> ok; + _ -> send_subject(From, StateData) + end, + NewState; + true -> + add_online_user(From, Nick, none, + IsSubscribeRequest, + Nodes, StateData) + end, + ResultState = + case NewStateData#state.just_created of + true -> + NewStateData#state{just_created = false}; + false -> + Robots = (?DICT):erase(From, StateData#state.robots), + NewStateData#state{robots = Robots} + end, + if not IsSubscribeRequest -> ResultState; + true -> {result, subscription_nodes_to_events(Nodes), ResultState} + 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/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; captcha_required -> SID = fxml:get_attr_s(<<"id">>, Attrs), RoomJID = StateData#state.jid, @@ -1906,7 +2004,7 @@ add_new_user(From, Nick, Limiter = {From#jid.luser, From#jid.lserver}, case ejabberd_captcha:create_captcha(SID, RoomJID, To, Lang, Limiter, From) - of + of {ok, ID, CaptchaEls} -> MsgPkt = #xmlel{name = <<"message">>, attrs = [{<<"id">>, ID}], @@ -1914,38 +2012,43 @@ add_new_user(From, Nick, Robots = (?DICT):store(From, {Nick, Packet}, StateData#state.robots), ejabberd_router:route(RoomJID, From, MsgPkt), - StateData#state{robots = Robots}; + NewState = StateData#state{robots = Robots}, + if not IsSubscribeRequest -> + NewState; + true -> + {ignore, NewState} + end; {error, limit} -> ErrText = <<"Too many CAPTCHA requests">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_RESOURCE_CONSTRAINT(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData; + Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end; _ -> ErrText = <<"Unable to generate a CAPTCHA">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_INTERNAL_SERVER_ERROR(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData + Err = ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end end; _ -> ErrText = <<"Incorrect password">>, - Err = jlib:make_error_reply(Packet, - ?ERRT_NOT_AUTHORIZED(Lang, - ErrText)), - ejabberd_router:route % TODO: s/Nick/""/ - (jid:replace_resource(StateData#state.jid, - Nick), - From, Err), - StateData + Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText), + ErrPacket = jlib:make_error_reply(Packet, Err), + if not IsSubscribeRequest -> + ejabberd_router:route(UserRoomJID, From, ErrPacket), + StateData; + true -> + {error, Err, StateData} + end end end. @@ -2111,6 +2214,15 @@ presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). +is_initial_presence(From, StateData) -> + LJID = jid:tolower(From), + case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{last_presence = Pres}} when Pres /= undefined -> + false; + _ -> + true + end. + send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). @@ -2159,6 +2271,24 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> OldStateData) end. +is_ra_changed(_, _IsInitialPresence = true, _, _) -> + false; +is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) -> + JID = case LJID of + #jid{} -> LJID; + _ -> jid:make(LJID) + end, + NewRole = get_role(LJID, NewStateData), + NewAff = get_affiliation(JID, NewStateData), + OldRole = get_role(LJID, OldStateData), + OldAff = get_affiliation(JID, OldStateData), + if (NewRole == none) and (NewAff == OldAff) -> + %% A user is leaving the room; + false; + true -> + (NewRole /= OldRole) or (NewAff /= OldAff) + end. + send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), #user{nick = Nick} = (?DICT):fetch(LNJID, StateData#state.users), @@ -2188,56 +2318,72 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> false -> (?DICT):to_list(StateData#state.users) end, - lists:foreach(fun ({LUJID, Info}) -> - {Role, Presence} = - if - LNJID == LUJID -> {Role0, Presence0}; - true -> {Role1, Presence1} - end, - SRole = role_to_list(Role), - ItemAttrs = case Info#user.role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - 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, - StatusEls = status_els(IsInitialPresence, NJID, Info, - StateData), - Packet = fxml:append_subtags(Presence, - [#xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name = - <<"item">>, - attrs - = - ItemAttrs, - children - = - ItemEls} - | StatusEls]}]), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) - end, - UserList). + lists:foreach( + fun({LUJID, Info}) -> + {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0}; + true -> {Role1, Presence1} + end, + SRole = role_to_list(Role), + ItemAttrs = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + 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, + StatusEls = status_els(IsInitialPresence, NJID, Info, + StateData), + Pres = if Presence == undefined -> #xmlel{name = <<"presence">>}; + true -> Presence + end, + Packet = fxml:append_subtags(Pres, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs + = + ItemAttrs, + children + = + ItemEls} + | StatusEls]}]), + Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of + true -> ?NS_MUCSUB_NODES_AFFILIATIONS; + false -> ?NS_MUCSUB_NODES_PRESENCES + end, + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet, Node1, StateData), + Type = fxml:get_tag_attr_s(<<"type">>, Packet), + IsSubscriber = Info#user.is_subscriber, + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) and + (IsInitialPresence or (Type == <<"unavailable">>)) -> + Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + Info#user.jid, Packet, Node2, StateData); + true -> + ok + end + end, + UserList). send_existing_presences(ToJID, StateData) -> case is_room_overcrowded(StateData) of @@ -2249,90 +2395,94 @@ send_existing_presences1(ToJID, StateData) -> LToJID = 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(jid:tolower(LJID), - StateData#state.users), - PresenceBroadcast = - lists:member( - FromRole, (StateData#state.config)#config.presence_broadcast), - case {RealToJID, PresenceBroadcast} of - {FromJID, _} -> ok; - {_, false} -> ok; - _ -> - FromAffiliation = get_affiliation(LJID, - StateData), - ItemAttrs = case Role == moderator orelse - (StateData#state.config)#config.anonymous - == false - of - true -> - [{<<"jid">>, - 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 = fxml:append_subtags(Presence, - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_MUC_USER}], - children = - [#xmlel{name - = - <<"item">>, - attrs - = - ItemAttrs, - children - = - []}]}]), - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - FromNick), - RealToJID, Packet) - end - end, - (?DICT):to_list(StateData#state.nicks)). + lists:foreach( + fun({FromNick, _Users}) -> + LJID = find_jid_by_nick(FromNick, StateData), + #user{jid = FromJID, role = FromRole, + last_presence = Presence} = + (?DICT):fetch(jid:tolower(LJID), + StateData#state.users), + PresenceBroadcast = + lists:member( + FromRole, (StateData#state.config)#config.presence_broadcast), + case {RealToJID, PresenceBroadcast} of + {FromJID, _} -> ok; + {_, false} -> ok; + _ -> + FromAffiliation = get_affiliation(LJID, StateData), + ItemAttrs = case Role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + 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 = fxml:append_subtags( + Presence, + [#xmlel{name = + <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs, + children + = + []}]}]), + send_wrapped(jid:replace_resource(StateData#state.jid, FromNick), + RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCES, StateData) + end + end, + (?DICT):to_list(StateData#state.nicks)). + +set_nick(JID, Nick, State) -> + LJID = jid:tolower(JID), + {ok, #user{nick = OldNick}} = (?DICT):find(LJID, State#state.users), + Users = (?DICT):update(LJID, + fun (#user{} = User) -> User#user{nick = Nick} end, + State#state.users), + OldNickUsers = (?DICT):fetch(OldNick, State#state.nicks), + NewNickUsers = case (?DICT):find(Nick, State#state.nicks) of + {ok, U} -> U; + error -> [] + end, + Nicks = case OldNickUsers of + [LJID] -> + (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], + (?DICT):erase(OldNick, State#state.nicks)); + [_ | _] -> + (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]], + (?DICT):store(OldNick, OldNickUsers -- [LJID], + State#state.nicks)) + end, + State#state{users = Users, nicks = Nicks}. change_nick(JID, Nick, StateData) -> LJID = 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 -> [] + {ok, #user{nick = OldNick}} = (?DICT):find(LJID, 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}, + SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [], + NewStateData = set_nick(JID, Nick, StateData), case presence_broadcast_allowed(JID, NewStateData) of true -> send_nick_changing(JID, OldNick, NewStateData, @@ -2352,7 +2502,7 @@ send_nick_changing(JID, OldNick, StateData, Affiliation = get_affiliation(JID, StateData), SAffiliation = affiliation_to_list(Affiliation), SRole = role_to_list(Role), - lists:foreach(fun ({_LJID, Info}) -> + lists:foreach(fun ({_LJID, Info}) when Presence /= undefined -> ItemAttrs1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false @@ -2428,17 +2578,23 @@ send_nick_changing(JID, OldNick, StateData, = []}|Status110]}]), if SendOldUnavailable -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - OldNick), - Info#user.jid, Packet1); + send_wrapped(jid:replace_resource(StateData#state.jid, + OldNick), + Info#user.jid, Packet1, + ?NS_MUCSUB_NODES_PRESENCES, + StateData); true -> ok end, if SendNewAvailable -> - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet2); + send_wrapped(jid:replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet2, + ?NS_MUCSUB_NODES_PRESENCES, + StateData); true -> ok - end + end; + (_) -> + ok end, (?DICT):to_list(StateData#state.users)). @@ -2731,13 +2887,7 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> 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, + store_room(NSD), {result, [], NSD}; Err -> Err end. @@ -3194,9 +3344,18 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, Code}], children = []}]}]}, - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) + RoomJIDNick = jid:replace_resource( + StateData#state.jid, Nick), + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), + IsSubscriber = Info#user.is_subscriber, + IsOccupant = Info#user.last_presence /= undefined, + if (IsSubscriber and not IsOccupant) -> + send_wrapped(RoomJIDNick, Info#user.jid, Packet, + ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); + true -> + ok + end end, (?DICT):to_list(StateData#state.users)). @@ -3686,6 +3845,9 @@ get_config(Lang, StateData, From) -> ?BOOLXFIELD(<<"Allow visitors to send voice requests">>, <<"muc#roomconfig_allowvoicerequests">>, (Config#config.allow_voice_requests)), + ?BOOLXFIELD(<<"Allow subscription">>, + <<"muc#roomconfig_allow_subscription">>, + (Config#config.allow_subscription)), ?STRINGXFIELD(<<"Minimum interval between voice requests " "(in seconds)">>, <<"muc#roomconfig_voicerequestmininterval">>, @@ -3877,6 +4039,10 @@ set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(allow_user_invites, Val); +set_xoption([{<<"muc#roomconfig_allow_subscription">>, [Val]} + | Opts], + Config) -> + ?SET_BOOL_XOPT(allow_subscription, Val); set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, [Val]} | Opts], @@ -3967,7 +4133,7 @@ set_xoption([{Opt, _Vals} | _Opts], _Config) -> change_config(Config, StateData) -> send_config_change_info(Config, StateData), - NSD = StateData#state{config = Config}, + NSD = remove_subscriptions(StateData#state{config = Config}), case {(StateData#state.config)#config.persistent, Config#config.persistent} of @@ -4015,10 +4181,11 @@ send_config_change_info(New, #state{config = Old} = StateData) -> children = [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_MUC_USER}], children = StatusEls}]}, - send_multiple(StateData#state.jid, - StateData#state.server_host, - StateData#state.users, - Message). + send_wrapped_multiple(StateData#state.jid, + StateData#state.users, + Message, + ?NS_MUCSUB_NODES_CONFIG, + StateData). remove_nonmembers(StateData) -> lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) -> @@ -4148,6 +4315,18 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; + allow_subscription -> + StateData#state{config = + (StateData#state.config)#config{allow_subscription = Val}}; + subscribers -> + lists:foldl( + fun({JID, Nick, Nodes}, State) -> + User = #user{jid = JID, nick = Nick, + subscriptions = Nodes, + is_subscriber = true, + role = none}, + update_online_user(JID, User, State) + end, StateData, Val); affiliations -> StateData#state{affiliations = (?DICT):from_list(Val)}; subject -> StateData#state{subject = Val}; @@ -4161,6 +4340,13 @@ set_opts([{Opt, Val} | Opts], StateData) -> make_opts(StateData) -> Config = StateData#state.config, + Subscribers = (?DICT):fold( + fun(_LJID, #user{is_subscriber = true} = User, Acc) -> + [{User#user.jid, User#user.nick, + User#user.subscriptions}|Acc]; + (_, _, Acc) -> + Acc + end, [], StateData#state.users), [?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description), ?MAKE_CONFIG_OPT(allow_change_subj), ?MAKE_CONFIG_OPT(allow_query_users), @@ -4187,7 +4373,8 @@ make_opts(StateData) -> {affiliations, (?DICT):to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, - {subject_author, StateData#state.subject_author}]. + {subject_author, StateData#state.subject_author}, + {subscribers, Subscribers}]. destroy_room(DEl, StateData) -> lists:foreach(fun ({_LJID, Info}) -> @@ -4210,9 +4397,10 @@ destroy_room(DEl, StateData) -> children = []}, DEl]}]}, - ejabberd_router:route(jid:replace_resource(StateData#state.jid, - Nick), - Info#user.jid, Packet) + send_wrapped(jid:replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet, + ?NS_MUCSUB_NODES_CONFIG, StateData) end, (?DICT):to_list(StateData#state.users)), case (StateData#state.config)#config.persistent of @@ -4264,6 +4452,10 @@ process_iq_disco_info(_From, get, Lang, StateData) -> <<"muc_moderated">>, <<"muc_unmoderated">>), ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), <<"muc_passwordprotected">>, <<"muc_unsecured">>)] + ++ case Config#config.allow_subscription of + true -> [?FEATURE(?NS_MUCSUB)]; + false -> [] + end ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), Config#config.mam} of {true, true} -> @@ -4361,6 +4553,118 @@ process_iq_vcard(From, set, Lang, SubEl, StateData) -> {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end. +process_iq_mucsub(From, Packet, + #iq{type = set, lang = Lang, + sub_el = #xmlel{name = <<"subscribe">>} = SubEl}, + #state{config = Config} = StateData) -> + case fxml:get_tag_attr_s(<<"nick">>, SubEl) of + <<"">> -> + Err = ?ERRT_BAD_REQUEST(Lang, <<"Missing 'nick' attribute">>), + {error, Err}; + Nick when Config#config.allow_subscription -> + LJID = jid:tolower(From), + case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick -> + Nodes = get_subscription_nodes(Packet), + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, + From, Nick)} of + {true, _} -> + ErrText = <<"That nickname is already in use by another occupant">>, + {error, ?ERRT_CONFLICT(Lang, ErrText)}; + {_, false} -> + ErrText = <<"That nickname is registered by another person">>, + {error, ?ERRT_CONFLICT(Lang, ErrText)}; + _ -> + NewStateData = add_online_user( + From, Nick, Role, true, Nodes, StateData), + {result, subscription_nodes_to_events(Nodes), NewStateData} + end; + {ok, #user{role = Role}} -> + Nodes = get_subscription_nodes(Packet), + NewStateData = add_online_user( + From, Nick, Role, true, Nodes, StateData), + {result, subscription_nodes_to_events(Nodes), NewStateData}; + error -> + add_new_user(From, Nick, Packet, StateData) + end; + _ -> + Err = ?ERRT_NOT_ALLOWED(Lang, <<"Subscriptions are not allowed">>), + {error, Err} + end; +process_iq_mucsub(From, _Packet, + #iq{type = set, + sub_el = #xmlel{name = <<"unsubscribe">>}}, + StateData) -> + LJID = jid:tolower(From), + case ?DICT:find(LJID, StateData#state.users) of + {ok, #user{is_subscriber = true} = User} -> + NewStateData = remove_subscription(From, User, StateData), + store_room(NewStateData), + {result, [], NewStateData}; + _ -> + {result, [], StateData} + end; +process_iq_mucsub(_From, _Packet, #iq{type = set, lang = Lang}, _StateData) -> + Txt = <<"Unrecognized subscription command">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; +process_iq_mucsub(_From, _Packet, #iq{type = get, lang = Lang}, _StateData) -> + Txt = <<"Value 'get' of 'type' attribute is not allowed">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}. + +remove_subscription(JID, #user{is_subscriber = true} = User, StateData) -> + case User#user.last_presence of + undefined -> + remove_online_user(JID, StateData, false); + _ -> + LJID = jid:tolower(JID), + Users = ?DICT:store(LJID, User#user{is_subscriber = false}, + StateData#state.users), + StateData#state{users = Users} + end; +remove_subscription(_JID, #user{}, StateData) -> + StateData. + +remove_subscriptions(StateData) -> + if not (StateData#state.config)#config.allow_subscription -> + dict:fold( + fun(_LJID, User, State) -> + remove_subscription(User#user.jid, User, State) + end, StateData, StateData#state.users); + true -> + StateData + end. + +get_subscription_nodes(#xmlel{name = <<"iq">>} = Packet) -> + case fxml:get_subtag_with_xmlns(Packet, <<"subscribe">>, ?NS_MUCSUB) of + #xmlel{children = Els} -> + lists:flatmap( + fun(#xmlel{name = <<"event">>, attrs = Attrs}) -> + Node = fxml:get_attr_s(<<"node">>, Attrs), + case lists:member(Node, [?NS_MUCSUB_NODES_PRESENCES, + ?NS_MUCSUB_NODES_MESSAGES, + ?NS_MUCSUB_NODES_AFFILIATIONS, + ?NS_MUCSUB_NODES_SUBJECT, + ?NS_MUCSUB_NODES_CONFIG, + ?NS_MUCSUB_NODES_PARTICIPANTS]) of + true -> + [Node]; + false -> + [] + end; + (_) -> + [] + end, Els); + false -> + [] + end; +get_subscription_nodes(_) -> + []. + +subscription_nodes_to_events(Nodes) -> + [#xmlel{name = <<"event">>, attrs = [{<<"node">>, Node}]} || Node <- Nodes]. + get_title(StateData) -> case (StateData#state.config)#config.title of <<"">> -> StateData#state.room; @@ -4484,9 +4788,9 @@ 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, - <<"">>)) + ejabberd_router:route( + StateData#state.jid, User#user.jid, + prepare_request_form(From, FromNick, <<"">>)) end, Moderators). @@ -4777,6 +5081,46 @@ tab_count_user(JID) -> element_size(El) -> byte_size(fxml:element_to_binary(El)). +store_room(StateData) -> + if (StateData#state.config)#config.persistent -> + mod_muc:store_room(StateData#state.server_host, + StateData#state.host, StateData#state.room, + make_opts(StateData)); + true -> + ok + end. + +send_wrapped(From, To, Packet, Node, State) -> + LTo = jid:tolower(To), + case ?DICT:find(LTo, State#state.users) of + {ok, #user{is_subscriber = true, + subscriptions = Nodes, + last_presence = undefined}} -> + case lists:member(Node, Nodes) of + true -> + NewPacket = wrap(From, To, Packet, Node), + ejabberd_router:route(State#state.jid, To, NewPacket); + false -> + ok + end; + _ -> + ejabberd_router:route(From, To, Packet) + end. + +wrap(From, To, Packet, Node) -> + Pkt1 = jlib:replace_from_to(From, To, Packet), + Pkt2 = #xmlel{attrs = Attrs} = jlib:remove_attr(<<"xmlns">>, Pkt1), + Pkt3 = Pkt2#xmlel{attrs = [{<<"xmlns">>, <<"jabber:client">>}|Attrs]}, + Item = #xmlel{name = <<"item">>, + attrs = [{<<"id">>, randoms:get_string()}], + children = [Pkt3]}, + Items = #xmlel{name = <<"items">>, attrs = [{<<"node">>, Node}], + children = [Item]}, + Event = #xmlel{name = <<"event">>, + attrs = [{<<"xmlns">>, ?NS_PUBSUB_EVENT}], + children = [Items]}, + #xmlel{name = <<"message">>, children = [Event]}. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast @@ -4784,6 +5128,12 @@ send_multiple(From, Server, Users, Packet) -> JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). +send_wrapped_multiple(From, Users, Packet, Node, State) -> + lists:foreach( + fun({_, #user{jid = To}}) -> + send_wrapped(From, To, Packet, Node, State) + end, ?DICT:to_list(Users)). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaninful content