aboutsummaryrefslogtreecommitdiff
path: root/src/mod_carboncopy.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_carboncopy.erl')
-rw-r--r--src/mod_carboncopy.erl357
1 files changed, 187 insertions, 170 deletions
diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl
index de8d8e1a7..bda77f816 100644
--- a/src/mod_carboncopy.erl
+++ b/src/mod_carboncopy.erl
@@ -7,7 +7,7 @@
%%% {mod_carboncopy, []}
%%%
%%%
-%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%% ejabberd, Copyright (C) 2002-2019 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
@@ -29,110 +29,143 @@
-author ('ecestari@process-one.net').
-protocol({xep, 280, '0.8'}).
--behavior(gen_mod).
+-behaviour(gen_mod).
%% API:
--export([start/2,
- stop/1]).
+-export([start/2, stop/1, reload/3]).
--export([user_send_packet/4, user_receive_packet/5,
- iq_handler2/3, iq_handler1/3, remove_connection/4,
- is_carbon_copy/1, mod_opt_type/1, depends/2]).
+-export([user_send_packet/1, user_receive_packet/1,
+ iq_handler/1, disco_features/5,
+ is_carbon_copy/1, depends/2,
+ mod_options/1]).
+-export([c2s_copy_session/2, c2s_session_opened/1, c2s_session_resumed/1]).
+%% For debugging purposes
+-export([list/2]).
--include("ejabberd.hrl").
-include("logger.hrl").
--include("jlib.hrl").
--define(PROCNAME, ?MODULE).
+-include("xmpp.hrl").
+-include("translate.hrl").
--callback init(binary(), gen_mod:opts()) -> any().
--callback enable(binary(), binary(), binary(), binary()) -> ok | {error, any()}.
--callback disable(binary(), binary(), binary()) -> ok | {error, any()}.
--callback list(binary(), binary()) -> [{binary(), binary()}].
+-type direction() :: sent | received.
+-type c2s_state() :: ejabberd_c2s:state().
-is_carbon_copy(Packet) ->
- is_carbon_copy(Packet, <<"sent">>) orelse
- is_carbon_copy(Packet, <<"received">>).
+-spec is_carbon_copy(stanza()) -> boolean().
+is_carbon_copy(#message{meta = #{carbon_copy := true}}) ->
+ true;
+is_carbon_copy(_) ->
+ false.
-is_carbon_copy(Packet, Direction) ->
- case fxml:get_subtag(Packet, Direction) of
- #xmlel{name = Direction, attrs = Attrs} ->
- case fxml:get_attr_s(<<"xmlns">>, Attrs) of
- ?NS_CARBONS_2 -> true;
- ?NS_CARBONS_1 -> true;
- _ -> false
- end;
- _ -> false
- end.
-
-start(Host, Opts) ->
- IQDisc = gen_mod:get_opt(iqdisc, Opts,fun gen_iq_handler:check_type/1, one_queue),
- mod_disco:register_feature(Host, ?NS_CARBONS_1),
- mod_disco:register_feature(Host, ?NS_CARBONS_2),
- Mod = gen_mod:db_mod(Host, ?MODULE),
- Mod:init(Host, Opts),
- ejabberd_hooks:add(unset_presence_hook,Host, ?MODULE, remove_connection, 10),
+start(Host, _Opts) ->
+ ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50),
%% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90)
ejabberd_hooks:add(user_send_packet,Host, ?MODULE, user_send_packet, 89),
ejabberd_hooks:add(user_receive_packet,Host, ?MODULE, user_receive_packet, 89),
- gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2, ?MODULE, iq_handler2, IQDisc),
- gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_1, ?MODULE, iq_handler1, IQDisc).
+ ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50),
+ ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50),
+ ejabberd_hooks:add(c2s_session_opened, Host, ?MODULE, c2s_session_opened, 50),
+ gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2, ?MODULE, iq_handler).
stop(Host) ->
- gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_1),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2),
- mod_disco:unregister_feature(Host, ?NS_CARBONS_2),
- mod_disco:unregister_feature(Host, ?NS_CARBONS_1),
+ ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50),
%% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90)
ejabberd_hooks:delete(user_send_packet,Host, ?MODULE, user_send_packet, 89),
ejabberd_hooks:delete(user_receive_packet,Host, ?MODULE, user_receive_packet, 89),
- ejabberd_hooks:delete(unset_presence_hook,Host, ?MODULE, remove_connection, 10).
+ ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50),
+ ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50),
+ ejabberd_hooks:delete(c2s_session_opened, Host, ?MODULE, c2s_session_opened, 50).
-iq_handler2(From, To, IQ) ->
- iq_handler(From, To, IQ, ?NS_CARBONS_2).
-iq_handler1(From, To, IQ) ->
- iq_handler(From, To, IQ, ?NS_CARBONS_1).
+reload(_Host, _NewOpts, _OldOpts) ->
+ ok.
-iq_handler(From, _To,
- #iq{type=set, lang = Lang,
- sub_el = #xmlel{name = Operation} = SubEl} = IQ, CC)->
- ?DEBUG("carbons IQ received: ~p", [IQ]),
+-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty,
+ jid(), jid(), binary(), binary()) ->
+ {error, stanza_error()} | {result, [binary()]}.
+disco_features({error, Err}, _From, _To, _Node, _Lang) ->
+ {error, Err};
+disco_features(empty, _From, _To, <<"">>, _Lang) ->
+ {result, [?NS_CARBONS_2]};
+disco_features({result, Feats}, _From, _To, <<"">>, _Lang) ->
+ {result, [?NS_CARBONS_2|Feats]};
+disco_features(Acc, _From, _To, _Node, _Lang) ->
+ Acc.
+
+-spec iq_handler(iq()) -> iq().
+iq_handler(#iq{type = set, lang = Lang, from = From,
+ sub_els = [El]} = IQ) when is_record(El, carbons_enable);
+ is_record(El, carbons_disable) ->
{U, S, R} = jid:tolower(From),
- Result = case Operation of
- <<"enable">>->
- ?INFO_MSG("carbons enabled for user ~s@~s/~s", [U,S,R]),
- enable(S,U,R,CC);
- <<"disable">>->
- ?INFO_MSG("carbons disabled for user ~s@~s/~s", [U,S,R]),
- disable(S, U, R)
- end,
+ Result = case El of
+ #carbons_enable{} -> enable(S, U, R, ?NS_CARBONS_2);
+ #carbons_disable{} -> disable(S, U, R)
+ end,
case Result of
- ok ->
- ?DEBUG("carbons IQ result: ok", []),
- IQ#iq{type=result, sub_el=[]};
- {error,_Error} ->
- ?ERROR_MSG("Error enabling / disabling carbons: ~p", [Result]),
- Txt = <<"Database failure">>,
- IQ#iq{type=error,sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]}
+ ok ->
+ xmpp:make_iq_result(IQ);
+ {error, _} ->
+ Txt = ?T("Database failure"),
+ xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
end;
+iq_handler(#iq{type = set, lang = Lang} = IQ) ->
+ Txt = ?T("Only <enable/> or <disable/> tags are allowed"),
+ xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
+iq_handler(#iq{type = get, lang = Lang} = IQ)->
+ Txt = ?T("Value 'get' of 'type' attribute is not allowed"),
+ xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
+
+-spec user_send_packet({stanza(), ejabberd_c2s:state()})
+ -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}.
+user_send_packet({Packet, C2SState}) ->
+ From = xmpp:get_from(Packet),
+ To = xmpp:get_to(Packet),
+ case check_and_forward(From, To, Packet, sent) of
+ {stop, Pkt} -> {stop, {Pkt, C2SState}};
+ Pkt -> {Pkt, C2SState}
+ end.
-iq_handler(_From, _To, #iq{lang = Lang, sub_el = SubEl} = IQ, _CC)->
- Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
- IQ#iq{type=error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}.
+-spec user_receive_packet({stanza(), ejabberd_c2s:state()})
+ -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}.
+user_receive_packet({Packet, #{jid := JID} = C2SState}) ->
+ To = xmpp:get_to(Packet),
+ case check_and_forward(JID, To, Packet, received) of
+ {stop, Pkt} -> {stop, {Pkt, C2SState}};
+ Pkt -> {Pkt, C2SState}
+ end.
+
+-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state().
+c2s_copy_session(State, #{user := U, server := S, resource := R}) ->
+ case ejabberd_sm:get_user_info(U, S, R) of
+ offline -> State;
+ Info ->
+ case lists:keyfind(carboncopy, 1, Info) of
+ {_, CC} -> State#{carboncopy => CC};
+ false -> State
+ end
+ end.
-user_send_packet(Packet, _C2SState, From, To) ->
- check_and_forward(From, To, Packet, sent).
+-spec c2s_session_resumed(c2s_state()) -> c2s_state().
+c2s_session_resumed(#{user := U, server := S, resource := R,
+ carboncopy := CC} = State) ->
+ ejabberd_sm:set_user_info(U, S, R, carboncopy, CC),
+ maps:remove(carboncopy, State);
+c2s_session_resumed(State) ->
+ State.
-user_receive_packet(Packet, _C2SState, JID, _From, To) ->
- check_and_forward(JID, To, Packet, received).
+-spec c2s_session_opened(c2s_state()) -> c2s_state().
+c2s_session_opened(State) ->
+ maps:remove(carboncopy, State).
% Modified from original version:
% - registered to the user_send_packet hook, to be called only once even for multicast
% - do not support "private" message mode, and do not modify the original packet in any way
% - we also replicate "read" notifications
+-spec check_and_forward(jid(), jid(), stanza(), direction()) ->
+ stanza() | {stop, stanza()}.
check_and_forward(JID, To, Packet, Direction)->
case is_chat_message(Packet) andalso
- fxml:get_subtag(Packet, <<"private">>) == false andalso
- fxml:get_subtag(Packet, <<"no-copy">>) == false of
+ not is_received_muc_pm(To, Packet, Direction) andalso
+ not xmpp:has_subtag(Packet, #carbons_private{}) andalso
+ not xmpp:has_subtag(Packet, #hint{type = 'no-copy'}) of
true ->
case is_carbon_copy(Packet) of
false ->
@@ -147,18 +180,14 @@ check_and_forward(JID, To, Packet, Direction)->
Packet
end.
-remove_connection(User, Server, Resource, _Status)->
- disable(Server, User, Resource),
- ok.
-
-
%%% Internal
%% Direction = received | sent <received xmlns='urn:xmpp:carbons:1'/>
+-spec send_copies(jid(), jid(), message(), direction()) -> ok.
send_copies(JID, To, Packet, Direction)->
{U, S, R} = jid:tolower(JID),
PrioRes = ejabberd_sm:get_user_present_resources(U, S),
{_, AvailRs} = lists:unzip(PrioRes),
- {MaxPrio, MaxRes} = case catch lists:max(PrioRes) of
+ {MaxPrio, _MaxRes} = case catch lists:max(PrioRes) of
{Prio, Res} -> {Prio, Res};
_ -> {0, undefined}
end,
@@ -171,19 +200,19 @@ send_copies(JID, To, Packet, Direction)->
end,
%% list of JIDs that should receive a carbon copy of this message (excluding the
%% receiver(s) of the original message
- TargetJIDs = case {IsBareTo, R} of
- {true, MaxRes} ->
- OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end,
- [ {jid:make({U, S, CCRes}), CC_Version}
- || {CCRes, CC_Version} <- list(U, S),
- lists:member(CCRes, AvailRs), not OrigTo(CCRes) ];
- {true, _} ->
+ TargetJIDs = case {IsBareTo, Packet} of
+ {true, #message{meta = #{sm_copy := true}}} ->
%% The message was sent to our bare JID, and we currently have
%% multiple resources with the same highest priority, so the session
%% manager routes the message to each of them. We create carbon
- %% copies only from one of those resources (the one where R equals
- %% MaxRes) in order to avoid duplicates.
+ %% copies only from one of those resources in order to avoid
+ %% duplicates.
[];
+ {true, _} ->
+ OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end,
+ [ {jid:make({U, S, CCRes}), CC_Version}
+ || {CCRes, CC_Version} <- list(U, S),
+ lists:member(CCRes, AvailRs), not OrigTo(CCRes) ];
{false, _} ->
[ {jid:make({U, S, CCRes}), CC_Version}
|| {CCRes, CC_Version} <- list(U, S),
@@ -191,96 +220,84 @@ send_copies(JID, To, Packet, Direction)->
%TargetJIDs = lists:delete(JID, [ jid:make({U, S, CCRes}) || CCRes <- list(U, S) ]),
end,
- lists:map(fun({Dest,Version}) ->
- {_, _, Resource} = jid:tolower(Dest),
- ?DEBUG("Sending: ~p =/= ~p", [R, Resource]),
- Sender = jid:make({U, S, <<>>}),
- %{xmlelement, N, A, C} = Packet,
- New = build_forward_packet(JID, Packet, Sender, Dest, Direction, Version),
- ejabberd_router:route(Sender, Dest, New)
- end, TargetJIDs),
- ok.
-
-build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_2) ->
- #xmlel{name = <<"message">>,
- attrs = [{<<"xmlns">>, <<"jabber:client">>},
- {<<"type">>, message_type(Packet)},
- {<<"from">>, jid:to_string(Sender)},
- {<<"to">>, jid:to_string(Dest)}],
- children = [
- #xmlel{name = list_to_binary(atom_to_list(Direction)),
- attrs = [{<<"xmlns">>, ?NS_CARBONS_2}],
- children = [
- #xmlel{name = <<"forwarded">>,
- attrs = [{<<"xmlns">>, ?NS_FORWARD}],
- children = [
- complete_packet(JID, Packet, Direction)]}
- ]}
- ]};
-build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_1) ->
- #xmlel{name = <<"message">>,
- attrs = [{<<"xmlns">>, <<"jabber:client">>},
- {<<"type">>, message_type(Packet)},
- {<<"from">>, jid:to_string(Sender)},
- {<<"to">>, jid:to_string(Dest)}],
- children = [
- #xmlel{name = list_to_binary(atom_to_list(Direction)),
- attrs = [{<<"xmlns">>, ?NS_CARBONS_1}]},
- #xmlel{name = <<"forwarded">>,
- attrs = [{<<"xmlns">>, ?NS_FORWARD}],
- children = [complete_packet(JID, Packet, Direction)]}
- ]}.
-
-
+ lists:foreach(
+ fun({Dest, _Version}) ->
+ {_, _, Resource} = jid:tolower(Dest),
+ ?DEBUG("Sending: ~p =/= ~p", [R, Resource]),
+ Sender = jid:make({U, S, <<>>}),
+ New = build_forward_packet(JID, Packet, Sender, Dest, Direction),
+ ejabberd_router:route(xmpp:set_from_to(New, Sender, Dest))
+ end, TargetJIDs).
+
+-spec build_forward_packet(jid(), message(), jid(), jid(), direction()) -> message().
+build_forward_packet(JID, #message{type = T} = Msg, Sender, Dest, Direction) ->
+ Forwarded = #forwarded{sub_els = [complete_packet(JID, Msg, Direction)]},
+ Carbon = case Direction of
+ sent -> #carbons_sent{forwarded = Forwarded};
+ received -> #carbons_received{forwarded = Forwarded}
+ end,
+ #message{from = Sender, to = Dest, type = T, sub_els = [Carbon],
+ meta = #{carbon_copy => true}}.
+
+-spec enable(binary(), binary(), binary(), binary()) -> ok | {error, any()}.
enable(Host, U, R, CC)->
- ?DEBUG("enabling for ~p", [U]),
- Mod = gen_mod:db_mod(Host, ?MODULE),
- Mod:enable(U, Host, R, CC).
+ ?DEBUG("Enabling carbons for ~ts@~ts/~ts", [U, Host, R]),
+ case ejabberd_sm:set_user_info(U, Host, R, carboncopy, CC) of
+ ok -> ok;
+ {error, Reason} = Err ->
+ ?ERROR_MSG("Failed to enable carbons for ~ts@~ts/~ts: ~p",
+ [U, Host, R, Reason]),
+ Err
+ end.
+-spec disable(binary(), binary(), binary()) -> ok | {error, any()}.
disable(Host, U, R)->
- ?DEBUG("disabling for ~p", [U]),
- Mod = gen_mod:db_mod(Host, ?MODULE),
- Mod:disable(U, Host, R).
+ ?DEBUG("Disabling carbons for ~ts@~ts/~ts", [U, Host, R]),
+ case ejabberd_sm:del_user_info(U, Host, R, carboncopy) of
+ ok -> ok;
+ {error, notfound} -> ok;
+ {error, Reason} = Err ->
+ ?ERROR_MSG("Failed to disable carbons for ~ts@~ts/~ts: ~p",
+ [U, Host, R, Reason]),
+ Err
+ end.
-complete_packet(From, #xmlel{name = <<"message">>, attrs = OrigAttrs} = Packet, sent) ->
+-spec complete_packet(jid(), message(), direction()) -> message().
+complete_packet(From, #message{from = undefined} = Msg, sent) ->
%% if this is a packet sent by user on this host, then Packet doesn't
%% include the 'from' attribute. We must add it.
- Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}),
- case proplists:get_value(<<"from">>, Attrs) of
- undefined ->
- Packet#xmlel{attrs = [{<<"from">>, jid:to_string(From)}|Attrs]};
- _ ->
- Packet#xmlel{attrs = Attrs}
- end;
-complete_packet(_From, #xmlel{name = <<"message">>, attrs=OrigAttrs} = Packet, received) ->
- Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}),
- Packet#xmlel{attrs = Attrs}.
-
-message_type(#xmlel{attrs = Attrs}) ->
- case fxml:get_attr(<<"type">>, Attrs) of
- {value, Type} -> Type;
- false -> <<"normal">>
- end.
-
-is_chat_message(#xmlel{name = <<"message">>} = Packet) ->
- case message_type(Packet) of
- <<"chat">> -> true;
- <<"normal">> -> has_non_empty_body(Packet);
- _ -> false
- end;
-is_chat_message(_Packet) -> false.
-
-has_non_empty_body(Packet) ->
- fxml:get_subtag_cdata(Packet, <<"body">>) =/= <<"">>.
-
-%% list {resource, cc_version} with carbons enabled for given user and host
+ Msg#message{from = From};
+complete_packet(_From, Msg, _Direction) ->
+ Msg.
+
+-spec is_chat_message(stanza()) -> boolean().
+is_chat_message(#message{type = chat}) ->
+ true;
+is_chat_message(#message{type = normal, body = [_|_]}) ->
+ true;
+is_chat_message(_) ->
+ false.
+
+-spec is_received_muc_pm(jid(), message(), direction()) -> boolean().
+is_received_muc_pm(#jid{lresource = <<>>}, _Packet, _Direction) ->
+ false;
+is_received_muc_pm(_To, _Packet, sent) ->
+ false;
+is_received_muc_pm(_To, Packet, received) ->
+ xmpp:has_subtag(Packet, #muc_user{}).
+
+-spec list(binary(), binary()) -> [{Resource :: binary(), Namespace :: binary()}].
list(User, Server) ->
- Mod = gen_mod:db_mod(Server, ?MODULE),
- Mod:list(User, Server).
+ lists:filtermap(
+ fun({Resource, Info}) ->
+ case lists:keyfind(carboncopy, 1, Info) of
+ {_, NS} -> {true, {Resource, NS}};
+ false -> false
+ end
+ end, ejabberd_sm:get_user_info(User, Server)).
depends(_Host, _Opts) ->
[].
-mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
-mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
-mod_opt_type(_) -> [db_type, iqdisc].
+mod_options(_) ->
+ [].