From 8ddbf1c1c558d67d26eaf03dc28276104286f886 Mon Sep 17 00:00:00 2001 From: Badlop Date: Tue, 23 Aug 2011 21:52:08 +0200 Subject: [PATCH] Allow multiple entry with same nick to MUC rooms (thanks to Magnus Henoch)(EJAB-305) --- src/mod_muc/mod_muc_room.erl | 242 +++++++++++++++++++++++++---------- src/mod_muc/mod_muc_room.hrl | 2 + 2 files changed, 177 insertions(+), 67 deletions(-) diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index 6ac949dee..dc97cf040 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -451,8 +451,8 @@ normal_state({route, From, ToNick, ToNick), From, Err); _ -> - ToJID = find_jid_by_nick(ToNick, StateData), - case ToJID of + ToJIDs = find_jids_by_nick(ToNick, StateData), + case ToJIDs of false -> ErrText = "Recipient is not in the conference room", Err = exmpp_stanza:reply_with_error(Packet, @@ -465,7 +465,7 @@ normal_state({route, From, ToNick, From, Err); _ -> SrcIsVisitor = is_visitor(From, StateData), - DstIsModerator = is_moderator(ToJID, StateData), + DstIsModerator = is_moderator(hd(ToJIDs), StateData), PmFromVisitors = (StateData#state.config)#config.allow_private_messages_from_visitors, if SrcIsVisitor == false; PmFromVisitors == anyone; @@ -473,11 +473,8 @@ normal_state({route, From, ToNick, {ok, #user{nick = FromNick}} = ?DICT:find(jlib:jid_tolower(From), StateData#state.users), - ejabberd_router:route( - jid_replace_resource( - StateData#state.jid, - FromNick), - ToJID, Packet); + FromNickJID = 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 = exmpp_stanza:reply_with_error(Packet, @@ -953,7 +950,10 @@ process_presence(From, Nick, #xmlel{name = 'presence'} = Packet, true -> NewState = add_user_presence_un(From, Packet, StateData), - send_new_presence(From, NewState), + case ?DICT:find(Nick, StateData#state.nicks) of + {ok, [_, _ | _]} -> ok; + _ -> send_new_presence(From, NewState) + end, Reason = case exmpp_xml:get_element(Packet, 'status') of undefined -> <<>>; Status_el -> exmpp_xml:get_cdata(Status_el) @@ -977,7 +977,7 @@ process_presence(From, Nick, #xmlel{name = 'presence'} = Packet, true -> case is_nick_change(From, Nick, StateData) of true -> - case {is_nick_exists(Nick, StateData), + case {nick_collision(From, Nick, StateData), mod_muc:can_use_nick( StateData#state.host, From, Nick), {(StateData#state.config)#config.allow_visitor_nickchange, @@ -1296,21 +1296,31 @@ set_role(JID, Role, StateData) -> [] end end, - Users = case Role of - none -> - lists:foldl(fun(J, Us) -> - ?DICT:erase(J, - Us) - end, StateData#state.users, 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) - end, - StateData#state{users = Users}. + {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:short_prepd_jid(JID), @@ -1493,8 +1503,19 @@ add_online_user(JID, Nick, Role, StateData) -> 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}. + StateData#state{users = Users, nicks = Nicks}. remove_online_user(JID, StateData) -> remove_online_user(JID, StateData, <<>>). @@ -1506,7 +1527,15 @@ remove_online_user(JID, StateData, Reason) -> add_to_log(leave, {Nick, Reason}, StateData), tab_remove_online_user(JID, StateData), Users = ?DICT:erase(LJID, StateData#state.users), - StateData#state{users = 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); + false -> + StateData#state.nicks + end, + StateData#state{users = Users, nicks = Nicks}. filter_presence(#xmlel{name = 'presence'} = Packet) -> @@ -1556,19 +1585,48 @@ add_user_presence_un(JID, Presence, StateData) -> StateData#state{users = Users}. -is_nick_exists(Nick, StateData) -> - ?DICT:fold(fun(_, #user{nick = N}, B) -> - B orelse (N == Nick) - end, false, StateData#state.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]} -> + [exmpp_jid:make(User)]; + {ok, Users} -> + [exmpp_jid:make(LJID) || LJID <- Users]; + error -> + false + end. %% @spec(Nick::binary(), StateData) -> JID::jid() | false +%% Find and return the full JID of the user of Nick with +%% highest-priority presence. Return jid record. find_jid_by_nick(Nick, StateData) -> - ?DICT:fold(fun(_, #user{jid = JID, nick = N}, R) -> - case Nick of - N -> JID; - _ -> R - end - end, false, StateData#state.users). + case ?DICT:find(Nick, StateData#state.nicks) of + {ok, [User]} -> + exmpp_jid:make(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), + exmpp_jid:make(LJID); + error -> + false + end. + +higher_presence(Pres1, Pres2) -> + Pri1 = exmpp_presence:get_priority(Pres1), + Pri2 = exmpp_presence:get_priority(Pres2), + Pri1 > Pri2. is_nick_change(JID, Nick, StateData) -> LJID = jlib:short_prepd_jid(JID), @@ -1581,6 +1639,13 @@ is_nick_change(JID, Nick, StateData) -> Nick /= OldNick end. +nick_collision(User, Nick, StateData) -> + UserOfNick = find_jid_by_nick(Nick, StateData), + %% if nick is not used, or is used by another resource of the same + %% user, it's ok. + UserOfNick /= false andalso + not exmpp_jid:bare_compare(UserOfNick, User). + add_new_user(From, Nick, Packet, StateData) -> Lang = exmpp_stanza:get_lang(Packet), MaxUsers = get_max_users(StateData), @@ -1593,13 +1658,14 @@ add_new_user(From, Nick, Packet, StateData) -> MaxConferences = gen_mod:get_module_opt( StateData#state.server_host, mod_muc, max_user_conferences, 10), + Collision = nick_collision(From, Nick, StateData), case {(ServiceAffiliation == owner orelse MaxUsers < 0 orelse ((Affiliation == admin orelse Affiliation == owner) andalso NUsers < MaxAdminUsers) orelse NUsers < MaxUsers) andalso NConferences < MaxConferences, - is_nick_exists(Nick, StateData), + Collision, mod_muc:can_use_nick(StateData#state.host, From, Nick), get_default_role(Affiliation, StateData)} of {false, _, _, _} -> @@ -1937,13 +2003,19 @@ send_new_presence(NJID, StateData) -> %% @spec(NJID::jid(), Reason::binary(), StateData) -> send_new_presence({U, S, R}, Reason, StateData) -> send_new_presence(exmpp_jid:make(U, S, R), Reason, StateData); -send_new_presence(NJID, Reason, StateData) -> +send_new_presence(NJID1, Reason, StateData) -> + NJID = {exmpp_jid:node(NJID1), exmpp_jid:domain(NJID1), exmpp_jid:resource(NJID1)}, + %% First, find the nick associated with this JID. + #user{nick = Nick} = ?DICT:fetch(NJID, StateData#state.users), + %% Then find the JID using this nick with highest priority. + LJID1 = find_jid_by_nick(Nick, StateData), + LJID = {exmpp_jid:node(LJID1), exmpp_jid:domain(LJID1), exmpp_jid:resource(LJID1)}, + %% Then we get the presence data we're supposed to send. {ok, #user{jid = RealJID, - nick = Nick, role = Role, last_presence = Presence}} = - ?DICT:find(jlib:short_prepd_jid(NJID), StateData#state.users), - Affiliation = get_affiliation(NJID, StateData), + ?DICT:find(LJID, StateData#state.users), + Affiliation = get_affiliation(LJID1, StateData), SAffiliation = affiliation_to_binary(Affiliation), SRole = role_to_binary(Role), lists:foreach( @@ -2003,21 +2075,23 @@ send_new_presence(NJID, Reason, StateData) -> send_existing_presences(ToJID, StateData) -> - LToJID = jlib:short_prepd_jid(ToJID), + LToJID = {exmpp_jid:node(ToJID), exmpp_jid:domain(ToJID), exmpp_jid:resource(ToJID)}, {ok, #user{jid = RealToJID, role = Role}} = ?DICT:find(LToJID, StateData#state.users), lists:foreach( - fun({LJID, #user{jid = FromJID, - nick = FromNick, - role = FromRole, - last_presence = Presence - }}) -> + fun({FromNick, _Users}) -> + LJID1 = find_jid_by_nick(FromNick, StateData), + LJID = {exmpp_jid:node(LJID1), exmpp_jid:domain(LJID1), exmpp_jid:resource(LJID1)}, + #user{jid = FromJID, + role = FromRole, + last_presence = Presence + } = ?DICT:fetch(LJID, StateData#state.users), case RealToJID of FromJID -> ok; _ -> - {N,D,R} = LJID, + {N,D,R} = LJID, FromAffiliation = get_affiliation(exmpp_jid:make(N,D,R), StateData), ItemAttrs = @@ -2044,7 +2118,7 @@ send_existing_presences(ToJID, StateData) -> RealToJID, Packet) end - end, ?DICT:to_list(StateData#state.users)). + end, ?DICT:to_list(StateData#state.nicks)). @@ -2062,12 +2136,37 @@ change_nick(JID, Nick, StateData) -> fun(#user{} = User) -> User#user{nick = Nick} end, StateData#state.users), - NewStateData = StateData#state{users = Users}, - send_nick_changing(JID, OldNick, NewStateData), + OldNickUsers = ?DICT:fetch(OldNick, StateData#state.nicks), + NewNickUsers = case ?DICT:find(Nick, StateData#state.nicks) of + {ok, U} -> U; + error -> [] + end, + %% Send unavailable presence from the old nick if it's no longer + %% used. + SendOldUnavailable = length(OldNickUsers) == 1, + %% If we send unavailable presence from the old nick, we should + %% probably send presence from the new nick, in order not to + %% confuse clients. Otherwise, do it only if the new nick was + %% unused. + 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) -> +send_nick_changing(JID, OldNick, StateData, + SendOldUnavailable, SendNewAvailable) -> {ok, #user{jid = RealJID, nick = Nick, role = Role, @@ -2120,15 +2219,22 @@ send_nick_changing(JID, OldNick, StateData) -> children =[#xmlel{ns = ?NS_MUC_USER, name = 'item', attrs = ItemAttrs2}]}), - - ejabberd_router:route( - jid_replace_resource(StateData#state.jid, OldNick), - Info#user.jid, - Packet1), - ejabberd_router:route( - jid_replace_resource(StateData#state.jid, Nick), - Info#user.jid, - Packet2) + if SendOldUnavailable -> + ejabberd_router:route( + jid_replace_resource(StateData#state.jid, OldNick), + Info#user.jid, + Packet1); + true -> + ok + end, + if SendNewAvailable -> + ejabberd_router:route( + jid_replace_resource(StateData#state.jid, Nick), + Info#user.jid, + Packet2); + true -> + ok + end end, ?DICT:to_list(StateData#state.users)). @@ -3530,7 +3636,7 @@ process_iq_disco_info(_From, get, Lang, StateData) -> #xmlcdata{cdata = list_to_binary(Val)}]}]}). iq_disco_info_extras(Lang, StateData) -> - Len = length(?DICT:to_list(StateData#state.users)), + Len = ?DICT:size(StateData#state.users), RoomDescription = (StateData#state.config)#config.description, [#xmlel{ns = ?NS_DATA_FORMS, name = 'x', attrs = [?XMLATTR(<<"type">>, <<"result">>)], @@ -3795,27 +3901,29 @@ add_to_log(Type, Data, StateData) -> tab_add_online_user(JID, StateData) -> LUser = exmpp_jid:prep_node(JID), LServer = exmpp_jid:prep_domain(JID), + LResource = exmpp_jid:prep_resource(JID), US = {LUser, LServer}, Room = StateData#state.room, Host = StateData#state.host, catch ets:insert( muc_online_users, - #muc_online_users{us = US, room = Room, host = Host}). + #muc_online_users{us = US, resource = LResource, room = Room, host = Host}). tab_remove_online_user(JID, StateData) when ?IS_JID(JID) -> LUser = exmpp_jid:prep_node(JID), LServer = exmpp_jid:prep_domain(JID), - tab_remove_online_user({LUser, LServer, none},StateData); + LResource = exmpp_jid:prep_resource(JID), + tab_remove_online_user({LUser, LServer, LResource},StateData); -tab_remove_online_user({LUser, LServer,_}, StateData) -> +tab_remove_online_user({LUser, LServer, LResource}, StateData) -> US = {LUser, LServer}, Room = StateData#state.room, Host = StateData#state.host, catch ets:delete_object( muc_online_users, - #muc_online_users{us = US, room = Room, host = Host}). + #muc_online_users{us = US, resource = LResource, room = Room, host = Host}). tab_count_user(JID) -> LUser = exmpp_jid:prep_node(JID), diff --git a/src/mod_muc/mod_muc_room.hrl b/src/mod_muc/mod_muc_room.hrl index 210795935..302ed37eb 100644 --- a/src/mod_muc/mod_muc_room.hrl +++ b/src/mod_muc/mod_muc_room.hrl @@ -69,6 +69,7 @@ config = #config{}, users = ?DICT:new(), robots = ?DICT:new(), + nicks = ?DICT:new(), affiliations = ?DICT:new(), history, subject = "", @@ -79,6 +80,7 @@ room_queue = queue:new()}). -record(muc_online_users, {us, + resource, room, host}).