• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.45
/src/mod_carboncopy.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_carboncopy.erl
3
%%% Author  : Eric Cestari <ecestari@process-one.net>
4
%%% Purpose : Message Carbons XEP-0280 0.8
5
%%% Created : 5 May 2008 by Mickael Remond <mremond@process-one.net>
6
%%% Usage   : Add the following line in modules section of ejabberd.yml:
7
%%%              {mod_carboncopy, []}
8
%%%
9
%%%
10
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
11
%%%
12
%%% This program is free software; you can redistribute it and/or
13
%%% modify it under the terms of the GNU General Public License as
14
%%% published by the Free Software Foundation; either version 2 of the
15
%%% License, or (at your option) any later version.
16
%%%
17
%%% This program is distributed in the hope that it will be useful,
18
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
19
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
%%% General Public License for more details.
21
%%%
22
%%% You should have received a copy of the GNU General Public License along
23
%%% with this program; if not, write to the Free Software Foundation, Inc.,
24
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25
%%%
26
%%%----------------------------------------------------------------------
27
-module (mod_carboncopy).
28

29
-author ('ecestari@process-one.net').
30
-protocol({xep, 280, '1.0.1', '13.06', "complete", ""}).
31

32
-behaviour(gen_mod).
33

34
%% API:
35
-export([start/2, stop/1, reload/3]).
36

37
-export([user_send_packet/1, user_receive_packet/1,
38
         iq_handler/1, disco_features/5,
39
         depends/2, mod_options/1, mod_doc/0]).
40
-export([c2s_copy_session/2, c2s_session_opened/1, c2s_session_resumed/1,
41
         c2s_inline_features/3, c2s_handle_bind2_inline/1]).
42
%% For debugging purposes
43
-export([list/2]).
44

45
-include("logger.hrl").
46
-include_lib("xmpp/include/xmpp.hrl").
47
-include("translate.hrl").
48

49
-type direction() :: sent | received.
50
-type c2s_state() :: ejabberd_c2s:state().
51

52
start(_Host, _Opts) ->
53
    {ok, [{hook, disco_local_features, disco_features, 50},
90✔
54
          %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90)
55
          {hook, user_send_packet, user_send_packet, 89},
56
          {hook, user_receive_packet, user_receive_packet, 89},
57
          {hook, c2s_copy_session, c2s_copy_session, 50},
58
          {hook, c2s_session_resumed, c2s_session_resumed, 50},
59
          {hook, c2s_session_opened, c2s_session_opened, 50},
60
          {hook, c2s_inline_features, c2s_inline_features, 50},
61
          {hook, c2s_handle_bind2_inline, c2s_handle_bind2_inline, 50},
62
          {iq_handler, ejabberd_sm, ?NS_CARBONS_2, iq_handler}]}.
63

64
stop(_Host) ->
65
    ok.
90✔
66

67
reload(_Host, _NewOpts, _OldOpts) ->
68
    ok.
×
69

70
-spec disco_features({error, stanza_error()} | {result, [binary()]} | empty,
71
                     jid(), jid(), binary(), binary()) ->
72
                            {error, stanza_error()} | {result, [binary()]}.
73
disco_features(empty, From, To, <<"">>, Lang) ->
74
    disco_features({result, []}, From, To, <<"">>, Lang);
9✔
75
disco_features({result, Feats}, _From, _To, <<"">>, _Lang) ->
76
    {result, [?NS_CARBONS_2,?NS_CARBONS_RULES_0|Feats]};
9✔
77
disco_features(Acc, _From, _To, _Node, _Lang) ->
78
    Acc.
1✔
79

80
-spec iq_handler(iq()) -> iq().
81
iq_handler(#iq{type = set, lang = Lang, from = From,
82
               sub_els = [El]} = IQ) when is_record(El, carbons_enable);
83
                                          is_record(El, carbons_disable) ->
84
    {U, S, R} = jid:tolower(From),
5✔
85
    Result = case El of
5✔
86
                 #carbons_enable{} -> enable(S, U, R, ?NS_CARBONS_2);
4✔
87
                 #carbons_disable{} -> disable(S, U, R)
1✔
88
             end,
89
    case Result of
5✔
90
        ok ->
91
            xmpp:make_iq_result(IQ);
5✔
92
        {error, _} ->
93
            Txt = ?T("Database failure"),
×
94
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
95
    end;
96
iq_handler(#iq{type = set, lang = Lang} = IQ) ->
97
    Txt = ?T("Only <enable/> or <disable/> tags are allowed"),
3✔
98
    xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
3✔
99
iq_handler(#iq{type = get, lang = Lang} = IQ)->
100
    Txt = ?T("Value 'get' of 'type' attribute is not allowed"),
5✔
101
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
5✔
102

103
-spec user_send_packet({stanza(), ejabberd_c2s:state()})
104
      -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}.
105
user_send_packet({#message{meta = #{carbon_copy := true}}, _C2SState} = Acc) ->
106
    %% Stop the hook chain, we don't want logging modules to duplicate this
107
    %% message.
108
    {stop, Acc};
×
109
user_send_packet({#message{from = From, to = To} = Msg, C2SState}) ->
110
    {check_and_forward(From, To, Msg, sent), C2SState};
154✔
111
user_send_packet(Acc) ->
112
    Acc.
405✔
113

114
-spec user_receive_packet({stanza(), ejabberd_c2s:state()})
115
      -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}.
116
user_receive_packet({#message{meta = #{carbon_copy := true}}, _C2SState} = Acc) ->
117
    %% Stop the hook chain, we don't want logging modules to duplicate this
118
    %% message.
119
    {stop, Acc};
6✔
120
user_receive_packet({#message{to = To} = Msg, #{jid := JID} = C2SState}) ->
121
    {check_and_forward(JID, To, Msg, received), C2SState};
399✔
122
user_receive_packet(Acc) ->
123
    Acc.
715✔
124

125
-spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state().
126
c2s_copy_session(State, #{user := U, server := S, resource := R}) ->
127
    case ejabberd_sm:get_user_info(U, S, R) of
1✔
128
        offline -> State;
×
129
        Info ->
130
            case lists:keyfind(carboncopy, 1, Info) of
1✔
131
                {_, CC} -> State#{carboncopy => CC};
×
132
                false -> State
1✔
133
            end
134
    end.
135

136
-spec c2s_session_resumed(c2s_state()) -> c2s_state().
137
c2s_session_resumed(#{user := U, server := S, resource := R,
138
                      carboncopy := CC} = State) ->
139
    ejabberd_sm:set_user_info(U, S, R, carboncopy, CC),
×
140
    maps:remove(carboncopy, State);
×
141
c2s_session_resumed(State) ->
142
    State.
1✔
143

144
-spec c2s_session_opened(c2s_state()) -> c2s_state().
145
c2s_session_opened(State) ->
146
    maps:remove(carboncopy, State).
185✔
147

148
c2s_inline_features({Sasl, Bind, Extra} = Acc, Host, _State) ->
149
    case gen_mod:is_loaded(Host, ?MODULE) of
×
150
        true ->
151
            {Sasl, [#bind2_feature{var = ?NS_CARBONS_2} | Bind], Extra};
×
152
        false ->
153
            Acc
×
154
    end.
155

156
c2s_handle_bind2_inline({#{user := U, server := S, resource := R} = State, Els, Results}) ->
157
    case lists:keyfind(carbons_enable, 1, Els) of
×
158
        #carbons_enable{} ->
159
            enable(S, U, R, ?NS_CARBONS_2),
×
160
            {State, Els, Results};
×
161
        _ ->
162
        {State, Els, Results}
×
163
    end.
164

165
% Modified from original version:
166
%    - registered to the user_send_packet hook, to be called only once even for multicast
167
%    - do not support "private" message mode, and do not modify the original packet in any way
168
%    - we also replicate "read" notifications
169
-spec check_and_forward(jid(), jid(), message(), direction()) -> message().
170
check_and_forward(JID, To, Msg, Direction)->
171
    case (is_chat_message(Msg) orelse
553✔
172
          is_received_muc_invite(Msg, Direction)) andalso
445✔
173
        not is_received_muc_pm(To, Msg, Direction) andalso
109✔
174
        not xmpp:has_subtag(Msg, #carbons_private{}) andalso
95✔
175
        not xmpp:has_subtag(Msg, #hint{type = 'no-copy'}) of
83✔
176
        true ->
177
            send_copies(JID, To, Msg, Direction);
71✔
178
        false ->
179
            ok
482✔
180
    end,
181
    Msg.
553✔
182

183
%%% Internal
184
%% Direction = received | sent <received xmlns='urn:xmpp:carbons:1'/>
185
-spec send_copies(jid(), jid(), message(), direction()) -> ok.
186
send_copies(JID, To, Msg, Direction)->
187
    {U, S, R} = jid:tolower(JID),
71✔
188
    PrioRes = ejabberd_sm:get_user_present_resources(U, S),
71✔
189
    {_, AvailRs} = lists:unzip(PrioRes),
71✔
190
    {MaxPrio, _MaxRes} = case catch lists:max(PrioRes) of
71✔
191
        {Prio, Res} -> {Prio, Res};
71✔
192
        _ -> {0, undefined}
×
193
    end,
194

195
    %% unavailable resources are handled like bare JIDs
196
    IsBareTo = case {Direction, To} of
71✔
197
        {received, #jid{lresource = <<>>}} -> true;
×
198
        {received, #jid{lresource = LRes}} -> not lists:member(LRes, AvailRs);
29✔
199
        _ -> false
42✔
200
    end,
201
    %% list of JIDs that should receive a carbon copy of this message (excluding the
202
    %% receiver(s) of the original message
203
    TargetJIDs = case {IsBareTo, Msg} of
71✔
204
        {true, #message{meta = #{sm_copy := true}}} ->
205
            %% The message was sent to our bare JID, and we currently have
206
            %% multiple resources with the same highest priority, so the session
207
            %% manager routes the message to each of them. We create carbon
208
            %% copies only from one of those resources in order to avoid
209
            %% duplicates.
210
            [];
×
211
        {true, _} ->
212
            OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end,
12✔
213
            [ {jid:make({U, S, CCRes}), CC_Version}
12✔
214
             || {CCRes, CC_Version} <- list(U, S),
12✔
215
                lists:member(CCRes, AvailRs), not OrigTo(CCRes) ];
×
216
        {false, _} ->
217
            [ {jid:make({U, S, CCRes}), CC_Version}
59✔
218
             || {CCRes, CC_Version} <- list(U, S),
59✔
219
                lists:member(CCRes, AvailRs), CCRes /= R ]
6✔
220
            %TargetJIDs = lists:delete(JID, [ jid:make({U, S, CCRes}) || CCRes <- list(U, S) ]),
221
    end,
222

223
    lists:foreach(
71✔
224
      fun({Dest, _Version}) ->
225
              {_, _, Resource} = jid:tolower(Dest),
6✔
226
              ?DEBUG("Sending:  ~p =/= ~p", [R, Resource]),
6✔
227
              Sender = jid:make({U, S, <<>>}),
6✔
228
              New = build_forward_packet(Msg, Sender, Dest, Direction),
6✔
229
              ejabberd_router:route(xmpp:set_from_to(New, Sender, Dest))
6✔
230
      end, TargetJIDs).
231

232
-spec build_forward_packet(message(), jid(), jid(), direction()) -> message().
233
build_forward_packet(#message{type = T} = Msg, Sender, Dest, Direction) ->
234
    Forwarded = #forwarded{sub_els = [Msg]},
6✔
235
    Carbon = case Direction of
6✔
236
                 sent -> #carbons_sent{forwarded = Forwarded};
3✔
237
                 received -> #carbons_received{forwarded = Forwarded}
3✔
238
             end,
239
    #message{from = Sender, to = Dest, type = T, sub_els = [Carbon],
6✔
240
             meta = #{carbon_copy => true}}.
241

242
-spec enable(binary(), binary(), binary(), binary()) -> ok | {error, any()}.
243
enable(Host, U, R, CC)->
244
    ?DEBUG("Enabling carbons for ~ts@~ts/~ts", [U, Host, R]),
4✔
245
    case ejabberd_sm:set_user_info(U, Host, R, carboncopy, CC) of
4✔
246
        ok -> ok;
4✔
247
        {error, Reason} = Err ->
248
            ?ERROR_MSG("Failed to enable carbons for ~ts@~ts/~ts: ~p",
×
249
                       [U, Host, R, Reason]),
×
250
            Err
×
251
    end.
252

253
-spec disable(binary(), binary(), binary()) -> ok | {error, any()}.
254
disable(Host, U, R)->
255
    ?DEBUG("Disabling carbons for ~ts@~ts/~ts", [U, Host, R]),
1✔
256
    case ejabberd_sm:del_user_info(U, Host, R, carboncopy) of
1✔
257
        ok -> ok;
1✔
258
        {error, notfound} -> ok;
×
259
        {error, Reason} = Err ->
260
            ?ERROR_MSG("Failed to disable carbons for ~ts@~ts/~ts: ~p",
×
261
                       [U, Host, R, Reason]),
×
262
            Err
×
263
    end.
264

265
-spec is_chat_message(message()) -> boolean().
266
is_chat_message(#message{type = chat}) ->
267
    true;
46✔
268
is_chat_message(#message{type = normal, body = [_|_]}) ->
269
    true;
62✔
270
is_chat_message(#message{type = Type} = Msg) when Type == chat;
271
                                                  Type == normal ->
272
    has_chatstate(Msg) orelse xmpp:has_subtag(Msg, #receipt_response{});
35✔
273
is_chat_message(_) ->
274
    false.
410✔
275

276
-spec is_received_muc_invite(message(), direction()) -> boolean().
277
is_received_muc_invite(_Msg, sent) ->
278
    false;
100✔
279
is_received_muc_invite(Msg, received) ->
280
    case xmpp:get_subtag(Msg, #muc_user{}) of
345✔
281
        #muc_user{invites = [_|_]} ->
282
            true;
1✔
283
        _ ->
284
            xmpp:has_subtag(Msg, #x_conference{jid = jid:make(<<"">>)})
344✔
285
    end.
286

287
-spec is_received_muc_pm(jid(), message(), direction()) -> boolean().
288
is_received_muc_pm(#jid{lresource = <<>>}, _Msg, _Direction) ->
289
    false;
19✔
290
is_received_muc_pm(_To, _Msg, sent) ->
291
    false;
35✔
292
is_received_muc_pm(_To, Msg, received) ->
293
    xmpp:has_subtag(Msg, #muc_user{}).
55✔
294

295
-spec has_chatstate(message()) -> boolean().
296
has_chatstate(#message{sub_els = Els}) ->
297
    lists:any(fun(El) -> xmpp:get_ns(El) == ?NS_CHATSTATES end, Els).
35✔
298

299
-spec list(binary(), binary()) -> [{Resource :: binary(), Namespace :: binary()}].
300
list(User, Server) ->
301
    lists:filtermap(
71✔
302
      fun({Resource, Info}) ->
303
              case lists:keyfind(carboncopy, 1, Info) of
83✔
304
                  {_, NS} -> {true, {Resource, NS}};
6✔
305
                  false -> false
77✔
306
              end
307
      end, ejabberd_sm:get_user_info(User, Server)).
308

309
depends(_Host, _Opts) ->
310
    [].
108✔
311

312
mod_options(_) ->
313
    [].
108✔
314

315
mod_doc() ->
316
    #{desc =>
×
317
          ?T("The module implements https://xmpp.org/extensions/xep-0280.html"
318
             "[XEP-0280: Message Carbons]. "
319
             "The module broadcasts messages on all connected "
320
             "user resources (devices).")}.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc