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

processone / ejabberd / 1296

19 Jan 2026 11:25AM UTC coverage: 33.562% (+0.09%) from 33.468%
1296

push

github

badlop
mod_conversejs: Cosmetic change: sort paths alphabetically

0 of 4 new or added lines in 1 file covered. (0.0%)

11245 existing lines in 174 files now uncovered.

15580 of 46421 relevant lines covered (33.56%)

1074.56 hits per line

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

5.26
/src/mod_shared_roster.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_shared_roster.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Shared roster management
5
%%% Created :  5 Mar 2005 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2026   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(mod_shared_roster).
27

28
-author('alexey@process-one.net').
29

30
-behaviour(gen_mod).
31

32
-export([start/2, stop/1, reload/3, export/1,
33
         import_info/0, webadmin_menu/3, webadmin_page/3,
34
         get_user_roster/2,
35
         get_jid_info/4, import/5, process_item/2, import_start/2,
36
         in_subscription/2, out_subscription/1, c2s_self_presence/1,
37
         unset_presence/4, register_user/2, remove_user/2,
38
         list_groups/1, create_group/2, create_group/3,
39
         delete_group/2, get_group_opts/2, set_group_opts/3,
40
         get_group_users/2, get_group_explicit_users/2,
41
         is_user_in_group/3, add_user_to_group/3, opts_to_binary/1,
42
         remove_user_from_group/3, mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]).
43

44
-import(ejabberd_web_admin, [make_command/4, make_command_raw_value/3, make_table/2, make_table/4]).
45

46
-include("logger.hrl").
47

48
-include_lib("xmpp/include/xmpp.hrl").
49

50
-include("mod_roster.hrl").
51

52
-include("ejabberd_http.hrl").
53

54
-include("ejabberd_web_admin.hrl").
55

56
-include("mod_shared_roster.hrl").
57

58
-include("translate.hrl").
59

60
-type group_options() :: [{atom(), any()}].
61
-callback init(binary(), gen_mod:opts()) -> any().
62
-callback import(binary(), binary(), [binary()]) -> ok.
63
-callback list_groups(binary()) -> [binary()].
64
-callback groups_with_opts(binary()) -> [{binary(), group_options()}].
65
-callback create_group(binary(), binary(), group_options()) -> {atomic, any()}.
66
-callback delete_group(binary(), binary()) -> {atomic, any()}.
67
-callback get_group_opts(binary(), binary()) -> group_options() | error.
68
-callback set_group_opts(binary(), binary(), group_options()) -> {atomic, any()}.
69
-callback get_user_groups({binary(), binary()}, binary()) -> [binary()].
70
-callback get_group_explicit_users(binary(), binary()) -> [{binary(), binary()}].
71
-callback get_user_displayed_groups(binary(), binary(), group_options()) ->
72
    [{binary(), group_options()}].
73
-callback is_user_in_group({binary(), binary()}, binary(), binary()) -> boolean().
74
-callback add_user_to_group(binary(), {binary(), binary()}, binary()) -> any().
75
-callback remove_user_from_group(binary(), {binary(), binary()}, binary()) -> {atomic, any()}.
76
-callback use_cache(binary()) -> boolean().
77
-callback cache_nodes(binary()) -> [node()].
78

79
-optional_callbacks([use_cache/1, cache_nodes/1]).
80

81
-define(GROUP_OPTS_CACHE, shared_roster_group_opts_cache).
82
-define(USER_GROUPS_CACHE, shared_roster_user_groups_cache).
83
-define(GROUP_EXPLICIT_USERS_CACHE, shared_roster_group_explicit_cache).
84
-define(SPECIAL_GROUPS_CACHE, shared_roster_special_groups_cache).
85

86
start(Host, Opts) ->
UNCOV
87
    Mod = gen_mod:db_mod(Opts, ?MODULE),
4✔
UNCOV
88
    Mod:init(Host, Opts),
4✔
UNCOV
89
    init_cache(Mod, Host, Opts),
4✔
UNCOV
90
    {ok, [{hook, webadmin_menu_host, webadmin_menu, 70},
4✔
91
          {hook, webadmin_page_host, webadmin_page, 50},
92
          {hook, roster_get, get_user_roster, 70},
93
          {hook, roster_in_subscription, in_subscription, 30},
94
          {hook, roster_out_subscription, out_subscription, 30},
95
          {hook, roster_get_jid_info, get_jid_info, 70},
96
          {hook, roster_process_item, process_item, 50},
97
          {hook, c2s_self_presence, c2s_self_presence, 50},
98
          {hook, unset_presence_hook, unset_presence, 50},
99
          {hook, register_user, register_user, 50},
100
          {hook, remove_user, remove_user, 50}]}.
101

102
stop(_Host) ->
UNCOV
103
    ok.
4✔
104

105
reload(Host, NewOpts, OldOpts) ->
106
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
×
107
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
×
108
    if
×
109
        NewMod /= OldMod ->
110
            NewMod:init(Host, NewOpts);
×
111
       true ->
112
            ok
×
113
    end,
114
    init_cache(NewMod, Host, NewOpts),
×
115
    ok.
×
116

117
depends(_Host, _Opts) ->
UNCOV
118
    [].
4✔
119

120
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
121
init_cache(Mod, Host, Opts) ->
UNCOV
122
    NumHosts = length(ejabberd_option:hosts()),
4✔
UNCOV
123
    ets_cache:new(?SPECIAL_GROUPS_CACHE, [{max_size, NumHosts * 4}]),
4✔
UNCOV
124
    case use_cache(Mod, Host) of
4✔
125
        true ->
UNCOV
126
            CacheOpts = cache_opts(Opts),
4✔
UNCOV
127
            ets_cache:new(?GROUP_OPTS_CACHE, CacheOpts),
4✔
UNCOV
128
            ets_cache:new(?USER_GROUPS_CACHE, CacheOpts),
4✔
UNCOV
129
            ets_cache:new(?GROUP_EXPLICIT_USERS_CACHE, CacheOpts);
4✔
130
        false ->
131
            ets_cache:delete(?GROUP_OPTS_CACHE),
×
132
            ets_cache:delete(?USER_GROUPS_CACHE),
×
133
            ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE)
×
134
    end.
135

136
-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
137
cache_opts(Opts) ->
UNCOV
138
    MaxSize = mod_shared_roster_opt:cache_size(Opts),
4✔
UNCOV
139
    CacheMissed = mod_shared_roster_opt:cache_missed(Opts),
4✔
UNCOV
140
    LifeTime = mod_shared_roster_opt:cache_life_time(Opts),
4✔
UNCOV
141
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
4✔
142

143
-spec use_cache(module(), binary()) -> boolean().
144
use_cache(Mod, Host) ->
UNCOV
145
    case erlang:function_exported(Mod, use_cache, 1) of
4✔
146
        true -> Mod:use_cache(Host);
×
UNCOV
147
        false -> mod_shared_roster_opt:use_cache(Host)
4✔
148
    end.
149

150
-spec cache_nodes(module(), binary()) -> [node()].
151
cache_nodes(Mod, Host) ->
152
    case erlang:function_exported(Mod, cache_nodes, 1) of
×
153
        true -> Mod:cache_nodes(Host);
×
154
        false -> ejabberd_cluster:get_nodes()
×
155
    end.
156

157
-spec get_user_roster([#roster_item{}], {binary(), binary()}) -> [#roster_item{}].
158
get_user_roster(Items, {_, S} = US) ->
159
    {DisplayedGroups, Cache} = get_user_displayed_groups(US),
×
160
    SRUsers = lists:foldl(
×
161
        fun(Group, Acc1) ->
162
            GroupLabel = get_group_label_cached(S, Group, Cache),
×
163
            lists:foldl(
×
164
                fun(User, Acc2) ->
165
                    if User == US -> Acc2;
×
166
                        true ->
167
                            dict:append(User, GroupLabel, Acc2)
×
168
                    end
169
                end,
170
                Acc1, get_group_users_cached(S, Group, Cache))
171
        end,
172
        dict:new(), DisplayedGroups),
173
    {NewItems1, SRUsersRest} = lists:mapfoldl(
×
174
        fun(Item = #roster_item{jid = #jid{luser = User1, lserver = Server1}}, SRUsers1) ->
175
            US1 = {User1, Server1},
×
176
            case dict:find(US1, SRUsers1) of
×
177
                {ok, GroupLabels} ->
178
                    {Item#roster_item{subscription = both,
×
179
                                      groups = Item#roster_item.groups ++ GroupLabels,
180
                                      ask = undefined},
181
                     dict:erase(US1, SRUsers1)};
182
                error ->
183
                    {Item, SRUsers1}
×
184
            end
185
        end,
186
        SRUsers, Items),
187
    SRItems = [#roster_item{jid = jid:make(U1, S1),
×
188
                            name = get_rosteritem_name(U1, S1),
189
                            subscription = both, ask = undefined,
190
                            groups = GroupLabels}
191
               || {{U1, S1}, GroupLabels} <- dict:to_list(SRUsersRest)],
×
192
    SRItems ++ NewItems1.
×
193

194
get_rosteritem_name(U, S) ->
195
    case gen_mod:is_loaded(S, mod_vcard) of
×
196
        true ->
197
            SubEls = mod_vcard:get_vcard(U, S),
×
198
            get_rosteritem_name_vcard(SubEls);
×
199
        false ->
200
            <<"">>
×
201
    end.
202

203
-spec get_rosteritem_name_vcard([xmlel()]) -> binary().
204
get_rosteritem_name_vcard([Vcard|_]) ->
205
    case fxml:get_path_s(Vcard,
×
206
                        [{elem, <<"NICKNAME">>}, cdata])
207
        of
208
      <<"">> ->
209
          fxml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]);
×
210
      Nickname -> Nickname
×
211
    end;
212
get_rosteritem_name_vcard(_) ->
213
    <<"">>.
×
214

215
%% This function rewrites the roster entries when moving or renaming
216
%% them in the user contact list.
217
-spec process_item(#roster{}, binary()) -> #roster{}.
218
process_item(RosterItem, Host) ->
219
    USFrom = {UserFrom, ServerFrom} = RosterItem#roster.us,
×
220
    {UserTo, ServerTo, ResourceTo} = RosterItem#roster.jid,
×
221
    NameTo = RosterItem#roster.name,
×
222
    USTo = {UserTo, ServerTo},
×
223
    {DisplayedGroups, Cache} = get_user_displayed_groups(USFrom),
×
224
    CommonGroups = lists:filter(fun (Group) ->
×
225
                                        is_user_in_group(USTo, Group, Host)
×
226
                                end,
227
                                DisplayedGroups),
228
    case CommonGroups of
×
229
      [] -> RosterItem;
×
230
      %% Roster item cannot be removed: We simply reset the original groups:
231
      _ when RosterItem#roster.subscription == remove ->
232
          GroupLabels = lists:map(fun (Group) ->
×
233
                                         get_group_label_cached(Host, Group, Cache)
×
234
                                 end,
235
                                 CommonGroups),
236
          RosterItem#roster{subscription = both, ask = none,
×
237
                            groups = GroupLabels};
238
      %% Both users have at least a common shared group,
239
      %% So each user can see the other
240
      _ ->
241
          case lists:subtract(RosterItem#roster.groups,
×
242
                              CommonGroups)
243
              of
244
            %% If it doesn't, then remove this user from any
245
            %% existing roster groups.
246
            [] ->
247
                Pres = #presence{from = jid:make(UserTo, ServerTo),
×
248
                                 to = jid:make(UserFrom, ServerFrom),
249
                                 type = unsubscribe},
250
                mod_roster:out_subscription(Pres),
×
251
                mod_roster:in_subscription(false, Pres),
×
252
                RosterItem#roster{subscription = both, ask = none};
×
253
            %% If so, it means the user wants to add that contact
254
            %% to his personal roster
255
            PersonalGroups ->
256
                set_new_rosteritems(UserFrom, ServerFrom, UserTo,
×
257
                                    ServerTo, ResourceTo, NameTo,
258
                                    PersonalGroups)
259
          end
260
    end.
261

262
build_roster_record(User1, Server1, User2, Server2,
263
                    Name2, Groups) ->
264
    USR2 = {User2, Server2, <<"">>},
×
265
    #roster{usj = {User1, Server1, USR2},
×
266
            us = {User1, Server1}, jid = USR2, name = Name2,
267
            subscription = both, ask = none, groups = Groups}.
268

269
set_new_rosteritems(UserFrom, ServerFrom, UserTo,
270
                    ServerTo, ResourceTo, NameTo, GroupsFrom) ->
271
    RIFrom = build_roster_record(UserFrom, ServerFrom,
×
272
                                 UserTo, ServerTo, NameTo, GroupsFrom),
273
    set_item(UserFrom, ServerFrom, ResourceTo, RIFrom),
×
274
    JIDTo = jid:make(UserTo, ServerTo),
×
275
    JIDFrom = jid:make(UserFrom, ServerFrom),
×
276
    RITo = build_roster_record(UserTo, ServerTo, UserFrom,
×
277
                               ServerFrom, UserFrom, []),
278
    set_item(UserTo, ServerTo, <<"">>, RITo),
×
279
    mod_roster:out_subscription(
×
280
      #presence{from = JIDFrom, to = JIDTo, type = subscribe}),
281
    mod_roster:in_subscription(
×
282
      false, #presence{to = JIDTo, from = JIDFrom, type = subscribe}),
283
    mod_roster:out_subscription(
×
284
      #presence{from = JIDTo, to = JIDFrom, type = subscribed}),
285
    mod_roster:in_subscription(
×
286
      false, #presence{to = JIDFrom, from = JIDTo, type = subscribed}),
287
    mod_roster:out_subscription(
×
288
      #presence{from = JIDTo, to = JIDFrom, type = subscribe}),
289
    mod_roster:in_subscription(
×
290
      false, #presence{to = JIDFrom, from = JIDTo, type = subscribe}),
291
    mod_roster:out_subscription(
×
292
      #presence{from = JIDFrom, to = JIDTo, type = subscribed}),
293
    mod_roster:in_subscription(
×
294
      false, #presence{to = JIDTo, from = JIDFrom, type = subscribed}),
295
    RIFrom.
×
296

297
set_item(User, Server, Resource, Item) ->
298
    ResIQ = #iq{from = jid:make(User, Server, Resource),
×
299
                to = jid:make(Server),
300
                type = set, id = <<"push", (p1_rand:get_string())/binary>>,
301
                sub_els = [#roster_query{
302
                              items = [mod_roster:encode_item(Item)]}]},
303
    ejabberd_router:route(ResIQ).
×
304

305
-spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid())
306
      -> {subscription(), ask(), [binary()]}.
307
get_jid_info({Subscription, Ask, Groups}, User, Server,
308
             JID) ->
309
    LUser = jid:nodeprep(User),
×
310
    LServer = jid:nameprep(Server),
×
311
    US = {LUser, LServer},
×
312
    {U1, S1, _} = jid:tolower(JID),
×
313
    US1 = {U1, S1},
×
314
    {DisplayedGroups, Cache} = get_user_displayed_groups(US),
×
315
    SRUsers = lists:foldl(
×
316
        fun(Group, Acc1) ->
317
            GroupLabel = get_group_label_cached(LServer, Group, Cache), %++
×
318
            lists:foldl(
×
319
                fun(User1, Acc2) ->
320
                    dict:append(User1, GroupLabel, Acc2)
×
321
                end, Acc1, get_group_users_cached(LServer, Group, Cache))
322
        end,
323
        dict:new(), DisplayedGroups),
324
    case dict:find(US1, SRUsers) of
×
325
      {ok, GroupLabels} ->
326
          NewGroups = if Groups == [] -> GroupLabels;
×
327
                         true -> Groups
×
328
                      end,
329
          {both, none, NewGroups};
×
330
      error -> {Subscription, Ask, Groups}
×
331
    end.
332

333
-spec in_subscription(boolean(), presence()) -> boolean().
334
in_subscription(Acc, #presence{to = To, from = JID, type = Type}) ->
335
    #jid{user = User, server = Server} = To,
×
336
    process_subscription(in, User, Server, JID, Type, Acc).
×
337

338
-spec out_subscription(presence()) -> boolean().
339
out_subscription(#presence{from = From, to = To, type = unsubscribed} = Pres) ->
340
    #jid{user = User, server = Server} = From,
×
341
    mod_roster:out_subscription(Pres#presence{type = unsubscribe}),
×
342
    mod_roster:in_subscription(false, xmpp:set_from_to(
×
343
                                        Pres#presence{type = unsubscribe},
344
                                        To, From)),
345
    process_subscription(out, User, Server, To, unsubscribed, false);
×
346
out_subscription(#presence{from = From, to = To, type = Type}) ->
347
    #jid{user = User, server = Server} = From,
×
348
    process_subscription(out, User, Server, To, Type, false).
×
349

350
process_subscription(Direction, User, Server, JID,
351
                     _Type, Acc) ->
352
    LUser = jid:nodeprep(User),
×
353
    LServer = jid:nameprep(Server),
×
354
    US = {LUser, LServer},
×
355
    {U1, S1, _} =
×
356
        jid:tolower(jid:remove_resource(JID)),
357
    US1 = {U1, S1},
×
358
    {DisplayedGroups, _} = get_user_displayed_groups(US),
×
359
    SRUsers = lists:usort(lists:flatmap(fun (Group) ->
×
360
                                                get_group_users(LServer, Group)
×
361
                                        end,
362
                                        DisplayedGroups)),
363
    case lists:member(US1, SRUsers) of
×
364
      true ->
365
          case Direction of
×
366
            in -> {stop, false};
×
367
            out -> stop
×
368
          end;
369
      false -> Acc
×
370
    end.
371

372
list_groups(Host) ->
373
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
374
    Mod:list_groups(Host).
×
375

376
groups_with_opts(Host) ->
377
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
378
    Mod:groups_with_opts(Host).
×
379

380
create_group(Host, Group) ->
381
    create_group(Host, Group, []).
×
382

383
create_group(Host, Group, Opts) ->
384
    case jid:nameprep(Group) of
×
385
        error ->
386
            {error, invalid_group_name};
×
387
        LGroup ->
388
            case jid:nameprep(Host) of
×
389
                error ->
390
                    {error, invalid_group_host};
×
391
                LHost ->
392
                    create_group2(LHost, LGroup, Opts)
×
393
            end
394
    end.
395

396
create_group2(Host, Group, Opts) ->
397
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
398
    case proplists:get_value(all_users, Opts, false) orelse
×
399
         proplists:get_value(online_users, Opts, false) of
×
400
        true ->
401
            update_wildcard_cache(Host, Group, Opts);
×
402
        _ ->
403
            ok
×
404
    end,
405
    case use_cache(Mod, Host) of
×
406
        true ->
407
            ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)),
×
408
            ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host));
×
409
        _ ->
410
            ok
×
411
    end,
412
    Mod:create_group(Host, Group, Opts).
×
413

414
delete_group(Host, Group) ->
415
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
416
    update_wildcard_cache(Host, Group, []),
×
417
    case use_cache(Mod, Host) of
×
418
        true ->
419
            ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)),
×
420
            ets_cache:clear(?USER_GROUPS_CACHE, cache_nodes(Mod, Host)),
×
421
            ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host));
×
422
        _ ->
423
            ok
×
424
    end,
425
    Mod:delete_group(Host, Group).
×
426

427
get_groups_opts_cached(Host1, Group1, Cache) ->
428
    {Host, Group} = split_grouphost(Host1, Group1),
×
429
    Key = {Group, Host},
×
430
    case Cache of
×
431
        #{Key := Opts} ->
432
            {Opts, Cache};
×
433
        _ ->
434
            Opts = get_group_opts_int(Host, Group),
×
435
            {Opts, Cache#{Key => Opts}}
×
436
    end.
437

438
get_group_opts(Host1, Group1) ->
439
    {Host, Group} = split_grouphost(Host1, Group1),
×
440
    get_group_opts_int(Host, Group).
×
441

442
get_group_opts_int(Host, Group) ->
443
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
444
    Res = case use_cache(Mod, Host) of
×
445
        true ->
446
            ets_cache:lookup(
×
447
                ?GROUP_OPTS_CACHE, {Host, Group},
448
                fun() ->
449
                    case Mod:get_group_opts(Host, Group) of
×
450
                        error -> error;
×
451
                        V -> {cache, V}
×
452
                    end
453
                end);
454
        false ->
455
            Mod:get_group_opts(Host, Group)
×
456
    end,
457
    case Res of
×
458
        {ok, Opts} -> Opts;
×
459
        error -> error
×
460
    end.
461

462
set_group_opts(Host, Group, Opts) ->
463
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
464
    update_wildcard_cache(Host, Group, Opts),
×
465
    case use_cache(Mod, Host) of
×
466
        true ->
467
            ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)),
×
468
            ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host));
×
469
        _ ->
470
            ok
×
471
    end,
472
    Mod:set_group_opts(Host, Group, Opts).
×
473

474
get_user_groups(US) ->
475
    Host = element(2, US),
×
476
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
477
    UG = case use_cache(Mod, Host) of
×
478
             true ->
479
                 ets_cache:lookup(
×
480
                     ?USER_GROUPS_CACHE, {Host, US},
481
                     fun() ->
482
                         {cache, Mod:get_user_groups(US, Host)}
×
483
                     end);
484
             false ->
485
                 Mod:get_user_groups(US, Host)
×
486
         end,
487
    UG ++ get_groups_with_wildcards(Host, both).
×
488

489
get_group_opt_cached(Host, Group, Opt, Default, Cache) ->
490
    case get_groups_opts_cached(Host, Group, Cache) of
×
491
        {error, _} -> Default;
×
492
        {Opts, _} ->
493
            proplists:get_value(Opt, Opts, Default)
×
494
    end.
495

496
-spec get_group_opt(Host::binary(), Group::binary(), displayed_groups | label, Default) ->
497
    OptValue::any() | Default.
498
get_group_opt(Host, Group, Opt, Default) ->
499
    case get_group_opts(Host, Group) of
×
500
      error -> Default;
×
501
      Opts ->
502
          proplists:get_value(Opt, Opts, Default)
×
503
    end.
504

505
get_online_users(Host) ->
506
    lists:usort([{U, S}
×
507
                 || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host)]).
×
508

509
get_group_users_cached(Host1, Group1, Cache) ->
510
    {Host, Group} = split_grouphost(Host1, Group1),
×
511
    {Opts, _} = get_groups_opts_cached(Host, Group, Cache),
×
512
    get_group_users(Host, Group, Opts).
×
513

514
get_group_users(Host1, Group1) ->
515
    {Host, Group} = split_grouphost(Host1, Group1),
×
516
    get_group_users(Host, Group, get_group_opts(Host, Group)).
×
517

518
get_group_users(Host, Group, GroupOpts) ->
519
    case proplists:get_value(all_users, GroupOpts, false) of
520
      true -> ejabberd_auth:get_users(Host);
×
521
      false -> []
×
522
    end
523
      ++
×
524
      case proplists:get_value(online_users, GroupOpts, false)
525
          of
526
        true -> get_online_users(Host);
×
527
        false -> []
×
528
      end
529
        ++ get_group_explicit_users(Host, Group).
530

531
get_group_explicit_users(Host, Group) ->
532
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
533
    case use_cache(Mod, Host) of
×
534
        true ->
535
            ets_cache:lookup(
×
536
                ?GROUP_EXPLICIT_USERS_CACHE, {Host, Group},
537
                fun() ->
538
                    {cache, Mod:get_group_explicit_users(Host, Group)}
×
539
                end);
540
        false ->
541
            Mod:get_group_explicit_users(Host, Group)
×
542
    end.
543

544
get_group_label_cached(Host, Group, Cache) ->
545
    get_group_opt_cached(Host, Group, label, Group, Cache).
×
546

547
-spec update_wildcard_cache(binary(), binary(), list()) -> ok.
548
update_wildcard_cache(Host, Group, NewOpts) ->
549
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
550
    Online = get_groups_with_wildcards(Host, online),
×
551
    Both = get_groups_with_wildcards(Host, both),
×
552
    IsOnline = proplists:get_value(online_users, NewOpts, false),
×
553
    IsAll = proplists:get_value(all_users, NewOpts, false),
×
554

555
    OnlineUpdated = lists:member(Group, Online) /= IsOnline,
×
556
    BothUpdated = lists:member(Group, Both) /= (IsOnline orelse IsAll),
×
557

558
    if
×
559
        OnlineUpdated ->
560
            NewOnline = case IsOnline of
×
561
                            true -> [Group | Online];
×
562
                            _ -> Online -- [Group]
×
563
                        end,
564
            ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, online},
×
565
                             {ok, NewOnline}, fun() -> ok end, cache_nodes(Mod, Host));
×
566
        true -> ok
×
567
    end,
568
    if
×
569
        BothUpdated ->
570
            NewBoth = case IsOnline orelse IsAll of
×
571
                            true -> [Group | Both];
×
572
                            _ -> Both -- [Group]
×
573
                        end,
574
            ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, both},
×
575
                             {ok, NewBoth}, fun() -> ok end, cache_nodes(Mod, Host));
×
576
        true -> ok
×
577
    end,
578
    ok.
×
579

580
-spec get_groups_with_wildcards(binary(), online | both) -> list(binary()).
581
get_groups_with_wildcards(Host, Type) ->
582
    Res =
×
583
    ets_cache:lookup(
584
        ?SPECIAL_GROUPS_CACHE, {Host, Type},
585
        fun() ->
586
            Res = lists:filtermap(
×
587
                fun({Group, Opts}) ->
588
                    case proplists:get_value(online_users, Opts, false) orelse
×
589
                         (Type == both andalso proplists:get_value(all_users, Opts, false)) of
×
590
                        true -> {true, Group};
×
591
                        false -> false
×
592
                    end
593
                end,
594
                groups_with_opts(Host)),
595
            {cache, {ok, Res}}
×
596
        end),
597
    case Res of
×
598
        {ok, List} -> List;
×
599
        _ -> []
×
600
    end.
601

602
%% Given two lists of groupnames and their options,
603
%% return the list of displayed groups to the second list
604
displayed_groups(GroupsOpts, SelectedGroupsOpts) ->
605
    DisplayedGroups = lists:usort(lists:flatmap(
×
606
        fun
607
            ({_Group, Opts}) ->
608
                [G || G <- proplists:get_value(displayed_groups, Opts, []),
×
609
                 not lists:member(disabled, Opts)]
×
610
        end, SelectedGroupsOpts)),
611
    [G || G <- DisplayedGroups, not lists:member(disabled, proplists:get_value(G, GroupsOpts, []))].
×
612

613
%% Given a list of group names with options,
614
%% for those that have @all@ in memberlist,
615
%% get the list of groups displayed
616
get_special_displayed_groups(GroupsOpts) ->
617
    Groups = lists:filter(fun ({_Group, Opts}) ->
×
618
                                  proplists:get_value(all_users, Opts, false)
×
619
                          end,
620
                          GroupsOpts),
621
    displayed_groups(GroupsOpts, Groups).
×
622

623
%% Given a username and server, and a list of group names with options,
624
%% for the list of groups of that server that user is member
625
%% get the list of groups displayed
626
get_user_displayed_groups(LUser, LServer, GroupsOpts) ->
627
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
628
    Groups = Mod:get_user_displayed_groups(LUser, LServer, GroupsOpts),
×
629
    displayed_groups(GroupsOpts, Groups).
×
630

631
%% @doc Get the list of groups that are displayed to this user
632
get_user_displayed_groups(US) ->
633
    Host = element(2, US),
×
634
    {Groups, Cache} =
×
635
    lists:foldl(
636
        fun(Group, {Groups, Cache}) ->
637
            case get_groups_opts_cached(Host, Group, Cache) of
×
638
                {error, Cache2} ->
639
                    {Groups, Cache2};
×
640
                {Opts, Cache3} ->
641
                    case lists:member(disabled, Opts) of
×
642
                        false ->
643
                            {proplists:get_value(displayed_groups, Opts, []) ++ Groups, Cache3};
×
644
                        _ ->
645
                            {Groups, Cache3}
×
646
                    end
647
            end
648
        end, {[], #{}}, get_user_groups(US)),
649
    lists:foldl(
×
650
        fun(Group, {Groups0, Cache0}) ->
651
            case get_groups_opts_cached(Host, Group, Cache0) of
×
652
                {error, Cache1} ->
653
                    {Groups0, Cache1};
×
654
                {Opts, Cache2} ->
655
                    case lists:member(disabled, Opts) of
×
656
                        false ->
657
                            {[Group|Groups0], Cache2};
×
658
                        _ ->
659
                            {Groups0, Cache2}
×
660
                    end
661
            end
662
        end, {[], Cache}, lists:usort(Groups)).
663

664
is_user_in_group(US, Group, Host) ->
665
    Mod = gen_mod:db_mod(Host, ?MODULE),
×
666
    case Mod:is_user_in_group(US, Group, Host) of
×
667
        false ->
668
            lists:member(US, get_group_users(Host, Group));
×
669
        true ->
670
            true
×
671
    end.
672

673
-spec add_user_to_group(Host::binary(), {User::binary(), Server::binary()},
674
                        Group::binary()) -> {atomic, ok} | error.
675
add_user_to_group(Host, US, Group) ->
676
    {_LUser, LServer} = US,
×
677
    case lists:member(LServer, ejabberd_config:get_option(hosts)) of
×
678
        true -> add_user_to_group2(Host, US, Group);
×
679
        false ->
680
            ?INFO_MSG("Attempted adding to shared roster user of inexistent vhost ~ts", [LServer]),
×
681
            error
×
682
    end.
683
add_user_to_group2(Host, US, Group) ->
684
    {LUser, LServer} = US,
×
685
    case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of
×
686
      match ->
687
          GroupOpts = get_group_opts(Host, Group),
×
688
          MoreGroupOpts = case LUser of
×
689
                            <<"@all@">> -> [{all_users, true}];
×
690
                            <<"@online@">> -> [{online_users, true}];
×
691
                            _ -> []
×
692
                          end,
693
          set_group_opts(Host, Group,
×
694
                         GroupOpts ++ MoreGroupOpts);
695
      nomatch ->
696
          DisplayedToGroups = displayed_to_groups(Group, Host),
×
697
          DisplayedGroups = get_displayed_groups(Group, LServer),
×
698
          push_user_to_displayed(LUser, LServer, Group, Host, both, DisplayedToGroups),
×
699
          push_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups),
×
700
          Mod = gen_mod:db_mod(Host, ?MODULE),
×
701
          Mod:add_user_to_group(Host, US, Group),
×
702
          case use_cache(Mod, Host) of
×
703
              true ->
704
                  ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)),
×
705
                  ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host));
×
706
              false ->
707
                  ok
×
708
          end
709
    end.
710

711
get_displayed_groups(Group, LServer) ->
712
    get_group_opt(LServer, Group, displayed_groups, []).
×
713

714
push_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) ->
715
    [push_members_to_user(LUser, LServer, DGroup, Host,
×
716
                          Subscription)
717
     || DGroup <- DisplayedGroups].
×
718

719
remove_user_from_group(Host, US, Group) ->
720
    {LUser, LServer} = US,
×
721
    case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of
×
722
      match ->
723
          GroupOpts = get_group_opts(Host, Group),
×
724
          NewGroupOpts = case LUser of
×
725
                           <<"@all@">> ->
726
                               lists:filter(fun (X) -> X /= {all_users, true}
×
727
                                            end,
728
                                            GroupOpts);
729
                           <<"@online@">> ->
730
                               lists:filter(fun (X) -> X /= {online_users, true}
×
731
                                            end,
732
                                            GroupOpts)
733
                         end,
734
          set_group_opts(Host, Group, NewGroupOpts);
×
735
      nomatch ->
736
          Mod = gen_mod:db_mod(Host, ?MODULE),
×
737
          Result = Mod:remove_user_from_group(Host, US, Group),
×
738
          case use_cache(Mod, Host) of
×
739
              true ->
740
                  ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)),
×
741
                  ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host));
×
742
              false ->
743
                  ok
×
744
          end,
745
          DisplayedToGroups = displayed_to_groups(Group, Host),
×
746
          DisplayedGroups = get_displayed_groups(Group, LServer),
×
747
          push_user_to_displayed(LUser, LServer, Group, Host, remove, DisplayedToGroups),
×
748
          push_displayed_to_user(LUser, LServer, Host, remove, DisplayedGroups),
×
749
          Result
×
750
    end.
751

752
push_members_to_user(LUser, LServer, Group, Host,
753
                     Subscription) ->
754
    GroupOpts = get_group_opts(LServer, Group),
×
755
    GroupLabel = proplists:get_value(label, GroupOpts, Group), %++
×
756
    Members = get_group_users(Host, Group),
×
757
    lists:foreach(fun ({U, S}) ->
×
758
                          N = get_rosteritem_name(U, S),
×
759
                          push_roster_item(LUser, LServer, U, S, N, GroupLabel,
×
760
                                           Subscription)
761
                  end,
762
                  Members).
763

764
-spec register_user(binary(), binary()) -> ok.
765
register_user(User, Server) ->
766
    Groups = get_user_groups({User, Server}),
×
767
    [push_user_to_displayed(User, Server, Group, Server,
×
768
                            both, displayed_to_groups(Group, Server))
769
     || Group <- Groups],
×
770
    ok.
×
771

772
-spec remove_user(binary(), binary()) -> ok.
773
remove_user(User, Server) ->
774
    push_user_to_members(User, Server, remove).
×
775

776
push_user_to_members(User, Server, Subscription) ->
777
    LUser = jid:nodeprep(User),
×
778
    LServer = jid:nameprep(Server),
×
779
    RosterName = get_rosteritem_name(LUser, LServer),
×
780
    GroupsOpts = groups_with_opts(LServer),
×
781
    SpecialGroups =
×
782
        get_special_displayed_groups(GroupsOpts),
783
    UserGroups = get_user_displayed_groups(LUser, LServer,
×
784
                                           GroupsOpts),
785
    lists:foreach(fun (Group) ->
×
786
                          remove_user_from_group(LServer, {LUser, LServer},
×
787
                                                 Group),
788
                          GroupOpts = proplists:get_value(Group, GroupsOpts,
×
789
                                                          []),
790
                          GroupLabel = proplists:get_value(label, GroupOpts,
×
791
                                                          Group),
792
                          lists:foreach(fun ({U, S}) ->
×
793
                                                push_roster_item(U, S, LUser,
×
794
                                                                 LServer,
795
                                                                 RosterName,
796
                                                                 GroupLabel,
797
                                                                 Subscription)
798
                                        end,
799
                                        get_group_users(LServer, Group,
800
                                                        GroupOpts))
801
                  end,
802
                  lists:usort(SpecialGroups ++ UserGroups)).
803

804
push_user_to_displayed(LUser, LServer, Group, Host, Subscription, DisplayedToGroupsOpts) ->
805
    GroupLabel = get_group_opt(Host, Group, label, Group), %++
×
806
    [push_user_to_group(LUser, LServer, GroupD, Host,
×
807
                        GroupLabel, Subscription)
808
     || GroupD <- DisplayedToGroupsOpts].
×
809

810
push_user_to_group(LUser, LServer, Group, Host,
811
                   GroupLabel, Subscription) ->
812
    RosterName = get_rosteritem_name(LUser, LServer),
×
813
    lists:foreach(fun ({U, S})
×
814
                          when (U == LUser) and (S == LServer) ->
815
                          ok;
×
816
                      ({U, S}) ->
817
                          case lists:member(S, ejabberd_option:hosts()) of
×
818
                              true ->
819
                                  push_roster_item(U, S, LUser, LServer, RosterName, GroupLabel,
×
820
                                                   Subscription);
821
                              _ ->
822
                                  ok
×
823
                          end
824
                  end,
825
                  get_group_users(Host, Group)).
826

827
%% Get list of groups to which this group is displayed
828
displayed_to_groups(GroupName, LServer) ->
829
    GroupsOpts = groups_with_opts(LServer),
×
830
    Gs = lists:filter(fun ({_Group, Opts}) ->
×
831
                         lists:member(GroupName,
×
832
                                      proplists:get_value(displayed_groups,
833
                                                          Opts, []))
834
                 end,
835
                 GroupsOpts),
836
    [Name || {Name, _} <- Gs].
×
837

838
push_item(User, Server, Item) ->
839
    mod_roster:push_item(jid:make(User, Server),
×
840
                         Item#roster_item{subscription = none},
841
                         Item).
842

843
push_roster_item(User, Server, ContactU, ContactS, ContactN,
844
                 GroupLabel, Subscription) ->
845
    Item = #roster_item{jid = jid:make(ContactU, ContactS),
×
846
                        name = ContactN, subscription = Subscription, ask = undefined,
847
                        groups = [GroupLabel]},
848
    push_item(User, Server, Item).
×
849

850
-spec c2s_self_presence({presence(), ejabberd_c2s:state()})
851
      -> {presence(), ejabberd_c2s:state()}.
852
c2s_self_presence(Acc) ->
853
    Acc.
×
854

855
-spec unset_presence(binary(), binary(), binary(), binary()) -> ok.
856
unset_presence(User, Server, Resource, Status) ->
857
    LUser = jid:nodeprep(User),
×
858
    LServer = jid:nameprep(Server),
×
859
    LResource = jid:resourceprep(Resource),
×
860
    Resources = ejabberd_sm:get_user_resources(LUser,
×
861
                                               LServer),
862
    ?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p "
×
863
           "(~p resources)",
864
           [LUser, LServer, LResource, Status, length(Resources)]),
×
865
    case length(Resources) of
×
866
      0 ->
867
          lists:foreach(
×
868
              fun(OG) ->
869
                  DisplayedToGroups = displayed_to_groups(OG, LServer),
×
870
                  push_user_to_displayed(LUser, LServer, OG,
×
871
                                         LServer, remove, DisplayedToGroups),
872
                  push_displayed_to_user(LUser, LServer,
×
873
                                         LServer, remove, DisplayedToGroups)
874
              end, get_groups_with_wildcards(LServer, online));
875
      _ -> ok
×
876
    end.
877

878
%%---------------------
879
%% Web Admin: Page Frontend
880
%%---------------------
881

882
%% @format-begin
883

884
webadmin_menu(Acc, _Host, Lang) ->
885
    [{<<"shared-roster">>, translate:translate(Lang, ?T("Shared Roster Groups"))} | Acc].
×
886

887
webadmin_page(_,
888
              Host,
889
              #request{us = _US,
890
                       path = [<<"shared-roster">> | RPath],
891
                       lang = Lang} =
892
                  R) ->
893
    PageTitle = translate:translate(Lang, ?T("Shared Roster Groups")),
×
894
    Head = ?H1GL(PageTitle, <<"modules/#mod_shared_roster">>, <<"mod_shared_roster">>),
×
895
    Level = length(RPath),
×
896
    Res = case check_group_exists(Host, RPath) of
×
897
              true ->
898
                  webadmin_page_backend(Host, RPath, R, Lang, Level);
×
899
              false ->
900
                  [?XREST(<<"Group does not exist.">>)]
×
901
          end,
902
    {stop, Head ++ Res};
×
903
webadmin_page(Acc, _, _) ->
904
    Acc.
×
905

906
check_group_exists(Host, [<<"group">>, Id | _]) ->
907
    case get_group_opts(Host, Id) of
×
908
        error ->
909
            false;
×
910
        _ ->
911
            true
×
912
    end;
913
check_group_exists(_, _) ->
914
    true.
×
915

916
%%---------------------
917
%% Web Admin: Page Backend
918
%%---------------------
919

920
webadmin_page_backend(Host, [<<"group">>, Id, <<"info">> | RPath], R, _Lang, Level) ->
921
    Breadcrumb =
×
922
        make_breadcrumb({group_section,
923
                         Level,
924
                         <<"Groups of ", Host/binary>>,
925
                         Id,
926
                         <<"Information">>,
927
                         RPath}),
928
    SetLabel =
×
929
        make_command(srg_set_info,
930
                     R,
931
                     [{<<"host">>, Host}, {<<"group">>, Id}, {<<"key">>, <<"label">>}],
932
                     [{only, without_presentation}, {input_name_append, [Id, Host, <<"label">>]}]),
933
    SetDescription =
×
934
        make_command(srg_set_info,
935
                     R,
936
                     [{<<"host">>, Host}, {<<"group">>, Id}, {<<"key">>, <<"description">>}],
937
                     [{only, without_presentation},
938
                      {input_name_append, [Id, Host, <<"description">>]}]),
939
    SetAll =
×
940
        make_command(srg_set_info,
941
                     R,
942
                     [{<<"host">>, Host},
943
                      {<<"group">>, Id},
944
                      {<<"key">>, <<"all_users">>},
945
                      {<<"value">>, <<"true">>}],
946
                     [{only, button},
947
                      {input_name_append, [Id, Host, <<"all_users">>, <<"true">>]}]),
948
    UnsetAll =
×
949
        make_command(srg_set_info,
950
                     R,
951
                     [{<<"host">>, Host},
952
                      {<<"group">>, Id},
953
                      {<<"key">>, <<"all_users">>},
954
                      {<<"value">>, <<"false">>}],
955
                     [{only, button},
956
                      {input_name_append, [Id, Host, <<"all_users">>, <<"false">>]}]),
957
    SetOnline =
×
958
        make_command(srg_set_info,
959
                     R,
960
                     [{<<"host">>, Host},
961
                      {<<"group">>, Id},
962
                      {<<"key">>, <<"online_users">>},
963
                      {<<"value">>, <<"true">>}],
964
                     [{only, button},
965
                      {input_name_append, [Id, Host, <<"online_users">>, <<"true">>]}]),
966
    UnsetOnline =
×
967
        make_command(srg_set_info,
968
                     R,
969
                     [{<<"host">>, Host},
970
                      {<<"group">>, Id},
971
                      {<<"key">>, <<"online_users">>},
972
                      {<<"value">>, <<"false">>}],
973
                     [{only, button},
974
                      {input_name_append, [Id, Host, <<"online_users">>, <<"false">>]}]),
975
    GetInfo =
×
976
        make_command_raw_value(srg_get_info, R, [{<<"group">>, Id}, {<<"host">>, Host}]),
977
    AllElement =
×
978
        case proplists:get_value(<<"all_users">>, GetInfo, not_found) of
979
            "true" ->
980
                {?C("Unset @all@: "), UnsetAll};
×
981
            _ ->
982
                {?C("Set @all@: "), SetAll}
×
983
        end,
984
    OnlineElement =
×
985
        case proplists:get_value(<<"online_users">>, GetInfo, not_found) of
986
            "true" ->
987
                {?C("Unset @online@: "), UnsetOnline};
×
988
            _ ->
989
                {?C("Set @online@: "), SetOnline}
×
990
        end,
991
    Types =
×
992
        [{?C("Set label: "), SetLabel},
993
         {?C("Set description: "), SetDescription},
994
         AllElement,
995
         OnlineElement],
996
    Get = [?BR,
×
997
           make_command(srg_get_info, R, [{<<"host">>, Host}, {<<"group">>, Id}], []),
998
           make_command(srg_set_info, R, [], [{only, presentation}]),
999
           make_table(20, [], [{<<"">>, right}, <<"">>], Types)],
1000
    Breadcrumb ++ Get;
×
1001
webadmin_page_backend(Host,
1002
                      [<<"group">>, Id, <<"displayed">> | RPath],
1003
                      R,
1004
                      _Lang,
1005
                      Level) ->
1006
    Breadcrumb =
×
1007
        make_breadcrumb({group_section,
1008
                         Level,
1009
                         <<"Groups of ", Host/binary>>,
1010
                         Id,
1011
                         <<"Displayed Groups">>,
1012
                         RPath}),
1013
    AddDisplayed =
×
1014
        make_command(srg_add_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}], []),
1015
    _ = make_webadmin_displayed_table(Host, Id, R),
×
1016
    DisplayedTable = make_webadmin_displayed_table(Host, Id, R),
×
1017
    Get = [?BR,
×
1018
           make_command(srg_get_displayed, R, [], [{only, presentation}]),
1019
           make_command(srg_del_displayed, R, [], [{only, presentation}]),
1020
           ?XE(<<"blockquote">>, [DisplayedTable]),
1021
           AddDisplayed],
1022
    Breadcrumb ++ Get;
×
1023
webadmin_page_backend(Host, [<<"group">>, Id, <<"members">> | RPath], R, _Lang, Level) ->
1024
    Breadcrumb =
×
1025
        make_breadcrumb({group_section,
1026
                         Level,
1027
                         <<"Groups of ", Host/binary>>,
1028
                         Id,
1029
                         <<"Members">>,
1030
                         RPath}),
1031
    UserAdd = make_command(srg_user_add, R, [{<<"grouphost">>, Host}, {<<"group">>, Id}], []),
×
1032
    _ = make_webadmin_members_table(Host, Id, R),
×
1033
    MembersTable = make_webadmin_members_table(Host, Id, R),
×
1034
    Get = [make_command(srg_get_members, R, [], [{only, presentation}]),
×
1035
           make_command(srg_user_del, R, [], [{only, presentation}]),
1036
           ?XE(<<"blockquote">>, [MembersTable]),
1037
           UserAdd],
1038
    Breadcrumb ++ Get;
×
1039
webadmin_page_backend(Host, [<<"group">>, Id, <<"delete">> | RPath], R, _Lang, Level) ->
1040
    Breadcrumb =
×
1041
        make_breadcrumb({group_section,
1042
                         Level,
1043
                         <<"Groups of ", Host/binary>>,
1044
                         Id,
1045
                         <<"Delete">>,
1046
                         RPath}),
1047
    Get = [make_command(srg_delete,
×
1048
                        R,
1049
                        [{<<"host">>, Host}, {<<"group">>, Id}],
1050
                        [{style, danger}])],
1051
    Breadcrumb ++ Get;
×
1052
webadmin_page_backend(Host, [<<"group">>, Id | _RPath], _R, _Lang, Level) ->
1053
    Breadcrumb = make_breadcrumb({group, Level, <<"Groups of ", Host/binary>>, Id}),
×
1054
    MenuItems =
×
1055
        [{<<"info/">>, <<"Information">>},
1056
         {<<"members/">>, <<"Members">>},
1057
         {<<"displayed/">>, <<"Displayed Groups">>},
1058
         {<<"delete/">>, <<"Delete">>}],
1059
    Get = [?XE(<<"ul">>, [?LI([?AC(MIU, MIN)]) || {MIU, MIN} <- MenuItems])],
×
1060
    Breadcrumb ++ Get;
×
1061
webadmin_page_backend(Host, RPath, R, _Lang, Level) ->
1062
    Breadcrumb = make_breadcrumb({groups, <<"Groups of ", Host/binary>>}),
×
1063
    _ = make_webadmin_srg_table(Host, R, 3 + Level, RPath),
×
1064
    Set = [make_command(srg_add, R, [{<<"host">>, Host}], []),
×
1065
           make_command(srg_create, R, [{<<"host">>, Host}], [])],
1066
    RV2 = make_webadmin_srg_table(Host, R, 3 + Level, RPath),
×
1067
    Get = [make_command(srg_list, R, [{<<"host">>, Host}], [{only, presentation}]),
×
1068
           make_command(srg_get_info, R, [{<<"host">>, Host}], [{only, presentation}]),
1069
           make_command(srg_delete, R, [{<<"host">>, Host}], [{only, presentation}]),
1070
           ?XE(<<"blockquote">>, [RV2])],
1071
    Breadcrumb ++ Get ++ Set.
×
1072

1073
%%---------------------
1074
%% Web Admin: Table Generation
1075
%%---------------------
1076

1077
make_webadmin_srg_table(Host, R, Level, RPath) ->
1078
    Groups =
×
1079
        case make_command_raw_value(srg_list, R, [{<<"host">>, Host}]) of
1080
            Gs when is_list(Gs) ->
1081
                Gs;
×
1082
            _ ->
1083
                []
×
1084
        end,
1085
    Columns =
×
1086
        [<<"id">>,
1087
         <<"label">>,
1088
         <<"description">>,
1089
         <<"all">>,
1090
         <<"online">>,
1091
         {<<"members">>, right},
1092
         {<<"displayed">>, right},
1093
         <<"">>],
1094
    Rows =
×
1095
        [{make_command(echo3,
×
1096
                       R,
1097
                       [{<<"first">>, Id}, {<<"second">>, Host}, {<<"sentence">>, Id}],
1098
                       [{only, value}, {result_links, [{sentence, shared_roster, Level, <<"">>}]}]),
1099
          make_command(echo3,
1100
                       R,
1101
                       [{<<"first">>, Id},
1102
                        {<<"second">>, Host},
1103
                        {<<"sentence">>,
1104
                         iolist_to_binary(proplists:get_value(<<"label">>,
1105
                                                              make_command_raw_value(srg_get_info,
1106
                                                                                     R,
1107
                                                                                     [{<<"group">>,
1108
                                                                                       Id},
1109
                                                                                      {<<"host">>,
1110
                                                                                       Host}]),
1111
                                                              ""))}],
1112
                       [{only, value},
1113
                        {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
1114
          make_command(echo3,
1115
                       R,
1116
                       [{<<"first">>, Id},
1117
                        {<<"second">>, Host},
1118
                        {<<"sentence">>,
1119
                         iolist_to_binary(proplists:get_value(<<"description">>,
1120
                                                              make_command_raw_value(srg_get_info,
1121
                                                                                     R,
1122
                                                                                     [{<<"group">>,
1123
                                                                                       Id},
1124
                                                                                      {<<"host">>,
1125
                                                                                       Host}]),
1126
                                                              ""))}],
1127
                       [{only, value},
1128
                        {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
1129
          make_command(echo3,
1130
                       R,
1131
                       [{<<"first">>, Id},
1132
                        {<<"second">>, Host},
1133
                        {<<"sentence">>,
1134
                         iolist_to_binary(proplists:get_value(<<"all_users">>,
1135
                                                              make_command_raw_value(srg_get_info,
1136
                                                                                     R,
1137
                                                                                     [{<<"group">>,
1138
                                                                                       Id},
1139
                                                                                      {<<"host">>,
1140
                                                                                       Host}]),
1141
                                                              ""))}],
1142
                       [{only, value},
1143
                        {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
1144
          make_command(echo3,
1145
                       R,
1146
                       [{<<"first">>, Id},
1147
                        {<<"second">>, Host},
1148
                        {<<"sentence">>,
1149
                         iolist_to_binary(proplists:get_value(<<"online_users">>,
1150
                                                              make_command_raw_value(srg_get_info,
1151
                                                                                     R,
1152
                                                                                     [{<<"group">>,
1153
                                                                                       Id},
1154
                                                                                      {<<"host">>,
1155
                                                                                       Host}]),
1156
                                                              ""))}],
1157
                       [{only, value},
1158
                        {result_links, [{sentence, shared_roster, Level, <<"info">>}]}]),
1159
          make_command(echo3,
1160
                       R,
1161
                       [{<<"first">>, Id},
1162
                        {<<"second">>, Host},
1163
                        {<<"sentence">>,
1164
                         integer_to_binary(length(make_command_raw_value(srg_get_members,
1165
                                                                         R,
1166
                                                                         [{<<"group">>, Id},
1167
                                                                          {<<"host">>, Host}])))}],
1168
                       [{only, value},
1169
                        {result_links, [{sentence, shared_roster, Level, <<"members">>}]}]),
1170
          make_command(echo3,
1171
                       R,
1172
                       [{<<"first">>, Id},
1173
                        {<<"second">>, Host},
1174
                        {<<"sentence">>,
1175
                         integer_to_binary(length(make_command_raw_value(srg_get_displayed,
1176
                                                                         R,
1177
                                                                         [{<<"group">>, Id},
1178
                                                                          {<<"host">>, Host}])))}],
1179
                       [{only, value},
1180
                        {result_links, [{sentence, shared_roster, Level, <<"displayed">>}]}]),
1181
          make_command(srg_delete,
1182
                       R,
1183
                       [{<<"group">>, Id}, {<<"host">>, Host}],
1184
                       [{only, button}, {style, danger}, {input_name_append, [Id, Host]}])}
1185
         || Id <- Groups],
×
1186
    make_table(20, RPath, Columns, Rows).
×
1187

1188
make_webadmin_members_table(Host, Id, R) ->
1189
    Members =
×
1190
        case make_command_raw_value(srg_get_members, R, [{<<"host">>, Host}, {<<"group">>, Id}])
1191
        of
1192
            Ms when is_list(Ms) ->
1193
                Ms;
×
1194
            _ ->
1195
                []
×
1196
        end,
1197
    make_table([<<"member">>, <<"">>],
×
1198
               [{make_command(echo,
×
1199
                              R,
1200
                              [{<<"sentence">>, Jid}],
1201
                              [{only, value}, {result_links, [{sentence, user, 6, <<"">>}]}]),
1202
                 make_command(srg_user_del,
1203
                              R,
1204
                              [{<<"user">>,
1205
                                element(1,
1206
                                        jid:split(
1207
                                            jid:decode(Jid)))},
1208
                               {<<"host">>,
1209
                                element(2,
1210
                                        jid:split(
1211
                                            jid:decode(Jid)))},
1212
                               {<<"group">>, Id},
1213
                               {<<"grouphost">>, Host}],
1214
                              [{only, button},
1215
                               {style, danger},
1216
                               {input_name_append,
1217
                                [element(1,
1218
                                         jid:split(
1219
                                             jid:decode(Jid))),
1220
                                 element(2,
1221
                                         jid:split(
1222
                                             jid:decode(Jid))),
1223
                                 Id,
1224
                                 Host]}])}
1225
                || Jid <- Members]).
×
1226

1227
make_webadmin_displayed_table(Host, Id, R) ->
1228
    Displayed =
×
1229
        case make_command_raw_value(srg_get_displayed, R, [{<<"host">>, Host}, {<<"group">>, Id}])
1230
        of
1231
            Ms when is_list(Ms) ->
1232
                Ms;
×
1233
            _ ->
1234
                []
×
1235
        end,
1236
    make_table([<<"group">>, <<"">>],
×
1237
               [{make_command(echo3,
×
1238
                              R,
1239
                              [{<<"first">>, ThisId},
1240
                               {<<"second">>, Host},
1241
                               {<<"sentence">>, ThisId}],
1242
                              [{only, value},
1243
                               {result_links, [{sentence, shared_roster, 6, <<"">>}]}]),
1244
                 make_command(srg_del_displayed,
1245
                              R,
1246
                              [{<<"group">>, Id}, {<<"host">>, Host}, {<<"del">>, ThisId}],
1247
                              [{only, button},
1248
                               {style, danger},
1249
                               {input_name_append, [Id, Host, ThisId]}])}
1250
                || ThisId <- Displayed]).
×
1251

1252
make_breadcrumb({groups, Service}) ->
1253
    make_breadcrumb([Service]);
×
1254
make_breadcrumb({group, Level, Service, Name}) ->
1255
    make_breadcrumb([{Level, Service}, separator, Name]);
×
1256
make_breadcrumb({group_section, Level, Service, Name, Section, RPath}) ->
1257
    make_breadcrumb([{Level, Service}, separator, {Level - 2, Name}, separator, Section
×
1258
                     | RPath]);
1259
make_breadcrumb(Elements) ->
1260
    lists:map(fun ({xmlel, _, _, _} = Xmlel) ->
×
1261
                      Xmlel;
×
1262
                  (<<"sort">>) ->
1263
                      ?C(<<" +">>);
×
1264
                  (<<"page">>) ->
1265
                      ?C(<<" #">>);
×
1266
                  (separator) ->
1267
                      ?C(<<" > ">>);
×
1268
                  (Bin) when is_binary(Bin) ->
1269
                      ?C(Bin);
×
1270
                  ({Level, Bin}) when is_integer(Level) and is_binary(Bin) ->
1271
                      ?AC(binary:copy(<<"../">>, Level), Bin)
×
1272
              end,
1273
              Elements).
1274
%% @format-end
1275

1276
split_grouphost(Host, Group) ->
1277
    case str:tokens(Group, <<"@">>) of
×
1278
      [GroupName, HostName] -> {HostName, GroupName};
×
1279
      [_] -> {Host, Group}
×
1280
    end.
1281

1282
opts_to_binary(Opts) ->
1283
    lists:map(
×
1284
      fun({label, Label}) ->
1285
              {label, iolist_to_binary(Label)};
×
1286
         ({name, Label}) -> % For SQL backwards compat with ejabberd 20.03 and older
1287
              {label, iolist_to_binary(Label)};
×
1288
         ({description, Desc}) ->
1289
              {description, iolist_to_binary(Desc)};
×
1290
         ({displayed_groups, Gs}) ->
1291
              {displayed_groups, [iolist_to_binary(G) || G <- Gs]};
×
1292
         (Opt) ->
1293
              Opt
×
1294
      end, Opts).
1295

1296
export(LServer) ->
1297
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
1298
    Mod:export(LServer).
×
1299

1300
import_info() ->
1301
    [{<<"sr_group">>, 3}, {<<"sr_user">>, 3}].
×
1302

1303
import_start(LServer, DBType) ->
1304
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1305
    Mod:init(LServer, []).
×
1306

1307
import(LServer, {sql, _}, DBType, Tab, L) ->
1308
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
1309
    Mod:import(LServer, Tab, L).
×
1310

1311
mod_opt_type(db_type) ->
UNCOV
1312
    econf:db_type(?MODULE);
4✔
1313
mod_opt_type(use_cache) ->
UNCOV
1314
    econf:bool();
4✔
1315
mod_opt_type(cache_size) ->
UNCOV
1316
    econf:pos_int(infinity);
4✔
1317
mod_opt_type(cache_missed) ->
UNCOV
1318
    econf:bool();
4✔
1319
mod_opt_type(cache_life_time) ->
UNCOV
1320
    econf:timeout(second, infinity).
4✔
1321

1322
mod_options(Host) ->
UNCOV
1323
    [{db_type, ejabberd_config:default_db(Host, ?MODULE)},
4✔
1324
     {use_cache, ejabberd_option:use_cache(Host)},
1325
     {cache_size, ejabberd_option:cache_size(Host)},
1326
     {cache_missed, ejabberd_option:cache_missed(Host)},
1327
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
1328

1329
mod_doc() ->
1330
    #{desc =>
×
1331
          [?T("This module enables you to create shared roster groups: "
1332
              "groups of accounts that can see members from (other) groups "
1333
              "in their rosters."), "",
1334
           ?T("The big advantages of this feature are that end users do not "
1335
              "need to manually add all users to their rosters, and that they "
1336
              "cannot permanently delete users from the shared roster groups. "
1337
              "A shared roster group can have members from any XMPP server, "
1338
              "but the presence will only be available from and to members of "
1339
              "the same virtual host where the group is created. It still "
1340
              "allows the users to have / add their own contacts, as it does "
1341
              "not replace the standard roster. Instead, the shared roster "
1342
              "contacts are merged to the relevant users at retrieval time. "
1343
              "The standard user rosters thus stay unmodified."), "",
1344
           ?T("Shared roster groups can be edited via the Web Admin, "
1345
              "and some API commands called 'srg_', for example _`srg_add`_ API. "
1346
              "Each group has a unique name and those parameters:"), "",
1347
           ?T("- Label: Used in the rosters where this group is displayed."),"",
1348
           ?T("- Description: of the group, which has no effect."), "",
1349
           ?T("- Members: A list of JIDs of group members, entered one per "
1350
              "line in the Web Admin. The special member directive '@all@' "
1351
              "represents all the registered users in the virtual host; "
1352
              "which is only recommended for a small server with just a few "
1353
              "hundred users. The special member directive '@online@' "
1354
              "represents the online users in the virtual host. With those "
1355
              "two directives, the actual list of members in those shared "
1356
              "rosters is generated dynamically at retrieval time."), "",
1357
           ?T("- Displayed: A list of groups that will be in the "
1358
              "rosters of this group's members. A group of other vhost can "
1359
              "be identified with 'groupid@vhost'."), "",
1360
           ?T("This module depends on _`mod_roster`_. "
1361
              "If not enabled, roster queries will return 503 errors.")],
1362
      opts =>
1363
          [{db_type,
1364
            #{value => "mnesia | sql",
1365
              desc =>
1366
                  ?T("Same as top-level _`default_db`_ option, "
1367
                     "but applied to this module only.")}},
1368
           {use_cache,
1369
            #{value => "true | false",
1370
              desc =>
1371
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
1372
           {cache_size,
1373
            #{value => "pos_integer() | infinity",
1374
              desc =>
1375
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
1376
           {cache_missed,
1377
            #{value => "true | false",
1378
              desc =>
1379
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
1380
           {cache_life_time,
1381
            #{value => "timeout()",
1382
              desc =>
1383
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
1384
      example =>
1385
          [{?T("Take the case of a computer club that wants all its members "
1386
               "seeing each other in their rosters. To achieve this, they "
1387
               "need to create a shared roster group similar to this one:"),
1388
            ["Name: club_members",
1389
             "Label: Club Members",
1390
             "Description: Members from the computer club",
1391
             "Members: member1@example.org, member2@example.org, member3@example.org",
1392
             "Displayed Groups: club_members"]},
1393
           {?T("In another case we have a company which has three divisions: "
1394
               "Management, Marketing and Sales. All group members should see "
1395
               "all other members in their rosters. Additionally, all managers "
1396
               "should have all marketing and sales people in their roster. "
1397
               "Simultaneously, all marketeers and the whole sales team "
1398
               "should see all managers. This scenario can be achieved by "
1399
               "creating shared roster groups as shown in the following lists:"),
1400
            ["First list:",
1401
             "Name: management",
1402
             "Label: Management",
1403
             "Description: Management",
1404
             "Members: manager1@example.org, manager2@example.org",
1405
             "Displayed: management, marketing, sales",
1406
             "",
1407
             "Second list:",
1408
             "Name: marketing",
1409
             "Label: Marketing",
1410
             "Description: Marketing",
1411
             "Members: marketeer1@example.org, marketeer2@example.org, marketeer3@example.org",
1412
             "Displayed: management, marketing",
1413
             "",
1414
             "Third list:",
1415
             "Name: sales",
1416
             "Label: Sales",
1417
             "Description: Sales",
1418
             "Members: salesman1@example.org, salesman2@example.org, salesman3@example.org",
1419
             "Displayed: management, sales"
1420
            ]}
1421
          ]}.
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