Move occupant_id storage and processing to mod_muc_room

This commit is contained in:
Pawel Chmielowski
2026-02-02 15:34:33 +01:00
parent 6f59b7e63d
commit 7732984133
5 changed files with 73 additions and 211 deletions
+4 -2
View File
@@ -81,7 +81,8 @@
role :: role(),
%%is_subscriber = false :: boolean(),
%%subscriptions = [] :: [binary()],
last_presence :: presence() | undefined
last_presence :: presence() | undefined,
occupant_id :: binary()
}).
-record(subscriber, {jid :: jid(),
@@ -132,7 +133,8 @@
activity = treap:empty() :: treap:treap(),
room_shaper = none :: ejabberd_shaper:shaper(),
room_queue :: p1_queue:queue({message | presence, jid()}) | undefined,
hibernate_timer = none :: reference() | none | hibernating
hibernate_timer = none :: reference() | none | hibernating,
salt = <<>> :: binary()
}).
-type users() :: #{ljid() => #user{}}.
+9 -7
View File
@@ -73,7 +73,7 @@ transform(Host, Y, Acc) ->
transform(Host, modules, ModOpts, Acc) ->
{ModOpts1, Acc2} =
lists:mapfoldr(
filtermapfoldr(
fun({Mod, Opts}, Acc1) ->
Opts1 = transform_module_options(Opts),
transform_module(Host, Mod, Opts1, Acc1)
@@ -446,7 +446,7 @@ transform_module(_Host, mod_blocking, Opts, Acc) ->
(_) ->
true
end, Opts),
{{mod_blocking, Opts1}, Acc};
{{true, {mod_blocking, Opts1}}, Acc};
transform_module(_Host, mod_carboncopy, Opts, Acc) ->
Opts1 = lists:filter(
fun({Opt, _}) when Opt == ram_db_type;
@@ -459,7 +459,7 @@ transform_module(_Host, mod_carboncopy, Opts, Acc) ->
(_) ->
true
end, Opts),
{{mod_carboncopy, Opts1}, Acc};
{{true, {mod_carboncopy, Opts1}}, Acc};
transform_module(_Host, mod_http_api, Opts, Acc) ->
Opts1 = lists:filter(
fun({admin_ip_access, _}) ->
@@ -468,7 +468,7 @@ transform_module(_Host, mod_http_api, Opts, Acc) ->
(_) ->
true
end, Opts),
{{mod_http_api, Opts1}, Acc};
{{true, {mod_http_api, Opts1}}, Acc};
transform_module(_Host, mod_http_upload, Opts, Acc) ->
Opts1 = lists:filter(
fun({service_url, _}) ->
@@ -477,7 +477,7 @@ transform_module(_Host, mod_http_upload, Opts, Acc) ->
(_) ->
true
end, Opts),
{{mod_http_upload, Opts1}, Acc};
{{true, {mod_http_upload, Opts1}}, Acc};
transform_module(_Host, mod_pubsub, Opts, Acc) ->
Opts1 = lists:map(
fun({plugins, Plugins}) ->
@@ -505,9 +505,11 @@ transform_module(_Host, mod_pubsub, Opts, Acc) ->
(Opt) ->
Opt
end, Opts),
{{mod_pubsub, Opts1}, Acc};
{{true, {mod_pubsub, Opts1}}, Acc};
transform_module(_Host, mod_muc_occupantid, _Opts, Acc) ->
{false, Acc};
transform_module(_Host, Mod, Opts, Acc) ->
{{Mod, Opts}, Acc}.
{{true, {Mod, Opts}}, Acc}.
strip_odbc_suffix(M) ->
[_|T] = lists:reverse(string:tokens(atom_to_list(M), "_")),
+3 -6
View File
@@ -27,6 +27,7 @@
-protocol({xep, 45, '1.35.3', '0.5.0', "complete", ""}).
-protocol({xep, 249, '1.2', '0.5.0', "complete", ""}).
-protocol({xep, 486, '0.1.0', '24.07', "complete", ""}).
-protocol({xep, 421, '1.0.1', '23.10', "complete", ""}).
-ifndef(GEN_SERVER).
-define(GEN_SERVER, gen_server).
-endif.
@@ -729,10 +730,6 @@ process_disco_info(#iq{type = get, from = From, to = To, lang = Lang,
true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2];
false -> []
end,
OccupantIdFeatures = case gen_mod:is_loaded(ServerHost, mod_muc_occupantid) of
true -> [?NS_OCCUPANT_ID];
false -> []
end,
RSMFeatures = case RMod:rsm_supported() of
true -> [?NS_RSM];
false -> []
@@ -743,8 +740,8 @@ process_disco_info(#iq{type = get, from = From, to = To, lang = Lang,
end,
Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE,
?NS_MUC_STABLE_ID
| RegisterFeatures ++ RSMFeatures ++ MAMFeatures ++ OccupantIdFeatures],
?NS_MUC_STABLE_ID, ?NS_OCCUPANT_ID
| RegisterFeatures ++ RSMFeatures ++ MAMFeatures],
Name = mod_muc_opt:name(ServerHost),
Identity = #identity{category = <<"conference">>,
type = <<"text">>,
-162
View File
@@ -1,162 +0,0 @@
%%%----------------------------------------------------------------------
%%% File : mod_muc_occupantid.erl
%%% Author : Badlop <badlop@process-one.net>
%%% Purpose : Add Occupant Ids to stanzas in anonymous MUC rooms (XEP-0421)
%%% Created :
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2026 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_occupantid).
-author('badlop@process-one.net').
-protocol({xep, 421, '1.0.1', '23.10', "complete", ""}).
-behaviour(gen_mod).
-include_lib("xmpp/include/xmpp.hrl").
-include("logger.hrl").
-include("translate.hrl").
-include("mod_muc_room.hrl").
-export([start/2, stop/1,
mod_options/1, mod_doc/0, depends/2]).
-export([filter_packet/3, clean_obsolete_salts/0]).
%%%
%%% gen_mod
%%%
start(_Host, _Opts) ->
prepare_table(),
{ok, [{hook, muc_filter_presence, filter_packet, 10},
{hook, muc_filter_message, filter_packet, 10},
{hook, config_reloaded, clean_obsolete_salts, 90, global}]}.
stop(_Host) ->
ok.
%%%
%%% Hooks
%%%
filter_packet(Packet, State, _Nick) ->
add_occupantid_packet(Packet, State#state.jid).
%%%
%%% XEP-0421 Occupant-id
%%%
add_occupantid_packet(Packet, RoomJid) ->
From = xmpp:get_from(Packet),
OccupantId = calculate_occupantid(From, RoomJid),
OccupantElement = #occupant_id{id = OccupantId},
xmpp:append_subtags(xmpp:remove_subtag(Packet, OccupantElement), [OccupantElement]).
calculate_occupantid(From, RoomJid) ->
Term = {get_salt(RoomJid#jid.lserver), RoomJid, jid:remove_resource(From)},
misc:term_to_base64(crypto:hash(sha256, io_lib:format("~p", [Term]))).
%%%
%%% Table storing rooms' salt
%%%
-record(muc_occupant_id, {service_jid, salt}).
prepare_table() ->
try
mnesia:table_info(muc_occupant_id, attributes),
%% Wait some time to ensure mod_muc is started in all hosts
timer:apply_after(60000, ?MODULE, clean_obsolete_salts, [])
catch
exit:{aborted, {no_exists, _, _}} ->
create_table()
end.
create_table() ->
ejabberd_mnesia:create(?MODULE, muc_occupant_id,
[{disc_copies, [node()]},
{attributes, record_info(fields, muc_occupant_id)},
{type, set}]).
get_salt(ServiceJid) ->
case mnesia:dirty_read(muc_occupant_id, ServiceJid) of
[] ->
Salt = p1_rand:get_string(),
ok = write_salt(ServiceJid, Salt),
Salt;
[#muc_occupant_id{salt = Salt}] ->
Salt
end.
write_salt(ServiceJid, Salt) ->
mnesia:dirty_write(#muc_occupant_id{service_jid = ServiceJid, salt = Salt}).
%% @format-begin
clean_obsolete_salts() ->
Hosts = ejabberd_option:hosts(),
MucHosts =
lists:foldl(fun(Host, Acc) -> gen_mod:get_module_opt_hosts(Host, mod_muc) ++ Acc end,
[],
Hosts),
F = fun() ->
mnesia:write_lock_table(muc_occupant_id),
Ft = fun(#muc_occupant_id{service_jid = Service, salt = _Salt} = Rec, Acc) ->
case lists:member(Service, MucHosts) of
true ->
Acc;
false ->
mnesia:delete_object(Rec),
[Service | Acc]
end
end,
mnesia:foldl(Ft, [], muc_occupant_id)
end,
case mnesia:transaction(F) of
{atomic, Res} ->
?DEBUG("Deleted Salts for occupant-id of MUC services: ~p", [Res]),
ok;
_ ->
ok
end.
%% @format-end
%%%
%%% Doc
%%%
mod_options(_Host) ->
[].
mod_doc() ->
#{desc =>
[?T("This module implements "
"https://xmpp.org/extensions/xep-0421.html"
"[XEP-0421: Anonymous unique occupant identifiers for MUCs]."), "",
?T("When the module is enabled, the feature is enabled "
"in all semi-anonymous rooms.")],
note => "added in 23.10"
}.
depends(_, _) ->
[{mod_muc, hard}].
+57 -34
View File
@@ -310,6 +310,7 @@ init([Host, ServerHost, Access, Room, HistorySize,
history = lqueue_new(HistorySize, QueueType),
jid = jid:make(Room, Host),
just_created = true,
salt = p1_rand:get_string(),
room_queue = RoomQueue,
room_shaper = Shaper}),
State1 = set_affiliation(Creator, owner, State),
@@ -334,6 +335,7 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType])
room = Room,
history = lqueue_new(HistorySize, QueueType),
jid = Jid,
salt = p1_rand:get_string(),
room_queue = RoomQueue,
room_shaper = Shaper}),
add_to_log(room_existence, started, State),
@@ -638,11 +640,9 @@ normal_state({route, ToNick,
FromNick),
X = #muc_user{},
Packet2 = xmpp:set_subtag(Packet, X),
case ejabberd_hooks:run_fold(muc_filter_message,
StateData#state.server_host,
xmpp:put_meta(Packet2, mam_ignore, true),
[StateData, FromNick]) of
drop ->
case filter_message_hook(StateData, FromNick,
xmpp:put_meta(Packet2, mam_ignore, true)) of
drop ->
ok;
Packet3 ->
PrivMsg = xmpp:set_from(xmpp:del_meta(Packet3, mam_ignore), FromNickJID),
@@ -1092,12 +1092,8 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData
end,
case IsAllowed of
true ->
case
ejabberd_hooks:run_fold(muc_filter_message,
StateData#state.server_host,
Packet,
[StateData, FromNick])
of
case filter_message_hook(StateData, FromNick,
Packet) of
drop ->
{next_state, normal_state, StateData};
NewPacket1 ->
@@ -1401,10 +1397,7 @@ process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData
IsOnline = is_user_online(From, StateData),
if Type0 == available;
IsOnline and ((Type0 == unavailable) or (Type0 == error)) ->
case ejabberd_hooks:run_fold(muc_filter_presence,
StateData#state.server_host,
Packet0,
[StateData, Nick]) of
case filter_presence_hook(StateData, Nick, Packet0) of
drop ->
{next_state, normal_state, StateData};
#presence{} = Packet ->
@@ -1561,7 +1554,8 @@ get_users_and_subscribers_aux(Subscribers, StateData) ->
#user{jid = jid:make(LBareJID),
nick = Nick,
role = none,
last_presence = undefined},
last_presence = undefined,
occupant_id = <<>>},
Acc);
true ->
Acc
@@ -2130,10 +2124,44 @@ set_subscriber(JID, Nick, Nodes,
end,
NewStateData.
-spec calculate_occupant_id(jid(), state()) -> binary().
calculate_occupant_id(Jid, #state{salt = Salt, jid = RoomJid}) ->
JidS = jid:encode(jid:remove_resource(Jid)),
RoomJidS = jid:encode(RoomJid),
Term = <<Salt/binary, ":", RoomJidS/binary, ":", JidS/binary>>,
misc:term_to_base64(crypto:hash(sha256, Term)).
-spec filter_message_hook(state(), binary(), #message{}) -> drop | #message{}.
filter_message_hook(#state{users = Users} = StateData, Nick, #message{from = From} = Message) ->
OccupantId = case maps:find(jid:tolower(From), Users) of
{ok, #user{occupant_id = Id}} -> Id;
_ -> calculate_occupant_id(From, StateData)
end,
Message2 = xmpp:append_subtags(xmpp:remove_subtag(Message, #occupant_id{}),
[#occupant_id{id = OccupantId}]),
ejabberd_hooks:run_fold(muc_filter_message,
StateData#state.server_host,
Message2,
[StateData, Nick]).
-spec filter_presence_hook(state(), binary(), #presence{}) -> drop | #presence{}.
filter_presence_hook(#state{users = Users} = StateData, Nick, #presence{from = From} = Pres) ->
OccupantId = case maps:find(jid:tolower(From), Users) of
{ok, #user{occupant_id = Id}} -> Id;
_ -> calculate_occupant_id(From, StateData)
end,
Pres2 = xmpp:append_subtags(xmpp:remove_subtag(Pres, #occupant_id{}),
[#occupant_id{id = OccupantId}]),
ejabberd_hooks:run_fold(muc_filter_message,
StateData#state.server_host,
Pres2,
[StateData, Nick]).
-spec add_online_user(jid(), binary(), role(), state()) -> state().
add_online_user(JID, Nick, Role, StateData) ->
tab_add_online_user(JID, StateData),
User = #user{jid = JID, nick = Nick, role = Role},
User = #user{jid = JID, nick = Nick, role = Role, occupant_id = calculate_occupant_id(JID, StateData)},
reset_hibernate_timer(update_online_user(JID, User, StateData)).
-spec remove_online_user(jid(), state()) -> state().
@@ -3043,10 +3071,8 @@ send_subject(JID, #state{subject_author = {Nick, AuthorJID}} = StateData) ->
end,
Packet = #message{from = AuthorJID,
to = JID, type = groupchat, subject = Subject},
case ejabberd_hooks:run_fold(muc_filter_message,
StateData#state.server_host,
xmpp:put_meta(Packet, mam_ignore, true),
[StateData, Nick]) of
case filter_message_hook(StateData, Nick,
xmpp:put_meta(Packet, mam_ignore, true)) of
drop ->
ok;
NewPacket1 ->
@@ -4271,6 +4297,8 @@ set_opts2([{Opt, Val} | Opts], StateData) ->
hats_users ->
StateData#state{hats_users = maps:from_list(Val)};
hibernation_time -> StateData;
salt ->
StateData#state{salt = Val};
Other ->
?INFO_MSG("Unknown MUC room option, will be discarded: ~p", [Other]),
StateData
@@ -4353,6 +4381,7 @@ make_opts(StateData, Hibernation) ->
{hats_defs, maps:to_list(StateData#state.hats_defs)},
{hats_users, maps:to_list(StateData#state.hats_users)},
{hibernation_time, if Hibernation -> erlang:system_time(microsecond); true -> undefined end},
{salt, StateData#state.salt},
{subscribers, Subscribers}].
expand_opts(CompactOpts) ->
@@ -4377,14 +4406,16 @@ expand_opts(CompactOpts) ->
Subject = proplists:get_value(subject, CompactOpts, <<"">>),
Subscribers = proplists:get_value(subscribers, CompactOpts, []),
HibernationTime = proplists:get_value(hibernation_time, CompactOpts, 0),
Salt = proplists:get_value(hibernation_time, CompactOpts, <<>>),
[{subject, Subject},
{subject_author, SubjectAuthor},
{subscribers, Subscribers},
{hibernation_time, HibernationTime}
{hibernation_time, HibernationTime},
{salt, Salt}
| lists:reverse(Opts1)].
config_fields() ->
[subject, subject_author, subscribers, hibernate_time | record_info(fields, config)].
[subject, subject_author, subscribers, hibernate_time, salt | record_info(fields, config)].
-spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}.
destroy_room(DEl, StateData) ->
@@ -4447,7 +4478,7 @@ make_disco_info(From, StateData) ->
?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
?NS_COMMANDS,
?NS_MESSAGE_MODERATE_0, ?NS_MESSAGE_MODERATE_1,
?NS_MESSAGE_RETRACT,
?NS_MESSAGE_RETRACT, ?NS_OCCUPANT_ID,
?CONFIG_OPT_TO_FEATURE((Config#config.public),
<<"muc_public">>, <<"muc_hidden">>),
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
@@ -4472,12 +4503,6 @@ make_disco_info(From, StateData) ->
true -> [?NS_HATS];
false -> []
end
++ case gen_mod:is_loaded(StateData#state.server_host, mod_muc_occupantid) of
true ->
[?NS_OCCUPANT_ID];
_ ->
[]
end
++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam),
Config#config.mam} of
{true, true} ->
@@ -5423,10 +5448,8 @@ process_iq_moderate(From, #iq{type = set, lang = Lang}, Id, Reason,
from = From,
sub_els = SubEl},
{FromNick, _Role} = get_participant_data(From, StateData),
Packet = ejabberd_hooks:run_fold(muc_filter_message,
StateData#state.server_host,
xmpp:put_meta(Packet0, mam_ignore, true),
[StateData, FromNick]),
Packet = filter_message_hook(StateData, FromNick,
xmpp:put_meta(Packet0, mam_ignore, true)),
send_wrapped_multiple(JID,
get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData),
Packet, ?NS_MUCSUB_NODES_MESSAGES, StateData),