• 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

0.0
/src/mod_conversejs.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_conversejs.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : Serve simple page for Converse.js XMPP web browser client
5
%%% Created :  8 Nov 2021 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_conversejs).
27

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

30
-behaviour(gen_mod).
31

32
-export([start/2, stop/1, reload/3, process/2, depends/2,
33
         mod_opt_type/1, mod_options/1, mod_doc/0]).
34
-export([http_handlers_init/2, web_menu_system/3]).
35

36
-include_lib("xmpp/include/xmpp.hrl").
37
-include("logger.hrl").
38
-include("ejabberd_http.hrl").
39
-include("translate.hrl").
40
-include("ejabberd_web_admin.hrl").
41

42
-define(AUTOLOGIN_PATH, <<"conversejs-autologin">>).
43

44
start(_Host, _Opts) ->
45
    {ok, [{hook, http_request_handlers_init, http_handlers_init, 50, global},
×
46
          {hook, webadmin_menu_system_post, web_menu_system, 1000-$c, global}]}.
47

48
stop(_Host) ->
49
    ok.
×
50

51
reload(_Host, _NewOpts, _OldOpts) ->
52
    ok.
×
53

54
depends(_Host, _Opts) ->
55
    [].
×
56

57
process(LocalPath, #request{auth = Auth, path = Path} = Request) ->
58
    AutologinPath = lists:member(?AUTOLOGIN_PATH, Path),
×
59
    case {AutologinPath, Auth} of
×
60
        {true, undefined} ->
61
            ejabberd_web:error(not_found);
×
62
        _ ->
63
            process2(LocalPath, Request)
×
64
    end.
65

66
process2([], #request{method = 'GET', host = Host, auth = Auth, raw_path = RawPath1}) ->
67
    [RawPath | _] = string:split(RawPath1, "?"),
×
68
    ExtraOptions = get_auth_options(Host)
×
69
        ++ get_autologin_options(Auth)
70
        ++ get_register_options(Host)
71
        ++ get_extra_options(Host),
72
    Domain = mod_conversejs_opt:default_domain(Host),
×
73
    Script = get_file_url(Host, conversejs_script,
×
74
                          <<RawPath/binary, "/converse.min.js">>,
75
                          <<"https://cdn.conversejs.org/dist/converse.min.js">>),
76
    CSS = get_file_url(Host, conversejs_css,
×
77
                       <<RawPath/binary, "/converse.min.css">>,
78
                       <<"https://cdn.conversejs.org/dist/converse.min.css">>),
79
    PluginsHtml = get_plugins_html(Host, RawPath),
×
80
    Init = [{<<"discover_connection_methods">>, false},
×
81
            {<<"default_domain">>, Domain},
82
            {<<"domain_placeholder">>, Domain},
83
            {<<"registration_domain">>, Domain},
84
            {<<"assets_path">>, <<RawPath/binary, "/">>},
85
            {<<"view_mode">>, <<"fullscreen">>}
86
           | ExtraOptions],
87
    Init2 =
×
88
        case ejabberd_http:get_url(?MODULE, websocket, any, Host) of
89
            undefined -> Init;
×
90
            WSURL -> [{<<"websocket_url">>, WSURL} | Init]
×
91
        end,
92
    Init3 =
×
93
        case ejabberd_http:get_url(?MODULE, bosh, any, Host) of
94
            undefined -> Init2;
×
95
            BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2]
×
96
        end,
97
    Init4 = maps:from_list(Init3),
×
98
    {200, [html],
×
99
     [<<"<!DOCTYPE html>">>,
100
      <<"<html>">>,
101
      <<"<head>">>,
102
      <<"<meta charset='utf-8'>">>,
103
      <<"<link rel='stylesheet' type='text/css' media='screen' href='">>,
104
      fxml:crypt(CSS), <<"'>">>,
105
      <<"<script src='">>, fxml:crypt(Script), <<"' charset='utf-8'></script>">>
106
     ] ++ PluginsHtml ++ [
107
      <<"</head>">>,
108
      <<"<body>">>,
109
      <<"<script>">>,
110
      <<"converse.initialize(">>, misc:json_encode(Init4), <<");">>,
111
      <<"</script>">>,
112
      <<"</body>">>,
113
      <<"</html>">>]};
114
process2(LocalPath, #request{host = Host}) ->
115
    case is_served_file(LocalPath) of
×
116
        true -> serve(Host, LocalPath);
×
117
        false -> ejabberd_web:error(not_found)
×
118
    end.
119

120
%%----------------------------------------------------------------------
121
%% File server
122
%%----------------------------------------------------------------------
123

124
is_served_file([<<"converse.min.css">>]) -> true;
×
125
is_served_file([<<"converse.min.css.map">>]) -> true;
×
NEW
126
is_served_file([<<"converse.min.js">>]) -> true;
×
NEW
127
is_served_file([<<"converse.min.js.map">>]) -> true;
×
128
is_served_file([<<"emoji.json">>]) -> true;
×
129
is_served_file([<<"emojis.js">>]) -> true;
×
130
is_served_file([<<"images">>, _]) -> true;
×
131
is_served_file([<<"locales">>, <<"dayjs">>, _]) -> true;
×
NEW
132
is_served_file([<<"locales">>, _]) -> true;
×
NEW
133
is_served_file([<<"plugins">>, _]) -> true;
×
UNCOV
134
is_served_file([<<"sounds">>, _]) -> true;
×
UNCOV
135
is_served_file([<<"webfonts">>, _]) -> true;
×
UNCOV
136
is_served_file(_) -> false.
×
137

138
serve(Host, LocalPath) ->
139
    case get_conversejs_resources(Host) of
×
140
        undefined ->
UNCOV
141
            Path = str:join(LocalPath, <<"/">>),
×
UNCOV
142
            {303, [{<<"Location">>, <<"https://cdn.conversejs.org/dist/", Path/binary>>}], <<>>};
×
UNCOV
143
        MainPath -> serve2(LocalPath, MainPath)
×
144
    end.
145

146
get_conversejs_resources(Host) ->
UNCOV
147
    Opts = gen_mod:get_module_opts(Host, ?MODULE),
×
UNCOV
148
    mod_conversejs_opt:conversejs_resources(Opts).
×
149

150
%% Copied from mod_muc_log_http.erl
151

152
serve2(LocalPathBin, MainPathBin) ->
153
    LocalPath = [binary_to_list(LPB) || LPB <- LocalPathBin],
×
UNCOV
154
    MainPath = binary_to_list(MainPathBin),
×
155
    FileName = filename:join(filename:split(MainPath) ++ LocalPath),
×
156
    ContentType = get_content_type(iolist_to_binary(FileName)),
×
UNCOV
157
    case file:read_file(FileName) of
×
158
        {ok, FileContents} ->
UNCOV
159
            ?DEBUG("Delivering content.", []),
×
160
            {200,
×
161
             [{<<"Content-Type">>, ContentType}],
162
             FileContents};
163
        {error, eisdir} ->
164
            {403, [], "Forbidden"};
×
165
        {error, Error} ->
166
            ?DEBUG("Delivering error: ~p", [Error]),
×
UNCOV
167
            case Error of
×
UNCOV
168
                eacces -> {403, [], "Forbidden"};
×
UNCOV
169
                enoent -> {404, [], "Not found"};
×
UNCOV
170
                _Else -> {404, [], atom_to_list(Error)}
×
171
            end
172
    end.
173

174
-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>).
175

176
-spec get_content_type(binary()) -> binary().
177
get_content_type(FileName) ->
UNCOV
178
    ContentTypes = mod_http_fileserver:build_list_content_types([]),
×
UNCOV
179
    mod_http_fileserver:content_type(FileName,
×
180
                                     ?DEFAULT_CONTENT_TYPE,
181
                                     ContentTypes).
182

183
%%----------------------------------------------------------------------
184
%% Options parsing
185
%%----------------------------------------------------------------------
186

187
get_auth_options(Domain) ->
188
    case {ejabberd_auth_anonymous:is_login_anonymous_enabled(Domain),
×
189
          ejabberd_auth_anonymous:is_sasl_anonymous_enabled(Domain)} of
190
        {false, false} ->
UNCOV
191
            [{<<"authentication">>, <<"login">>}];
×
192
        {true, false} ->
UNCOV
193
            [{<<"authentication">>, <<"external">>}];
×
194
        {_, true} ->
UNCOV
195
            [{<<"authentication">>, <<"anonymous">>},
×
196
             {<<"jid">>, Domain}]
197
    end.
198

199
get_autologin_options({Jid, Password}) ->
UNCOV
200
    [{<<"auto_login">>, <<"true">>}, {<<"jid">>, Jid}, {<<"password">>, Password}];
×
201
get_autologin_options(undefined) ->
202
    [].
×
203

204
get_register_options(Server) ->
205
    AuthSupportsRegister =
×
206
        lists:any(
207
          fun(ejabberd_auth_mnesia) -> true;
×
UNCOV
208
             (ejabberd_auth_external) -> true;
×
UNCOV
209
             (ejabberd_auth_sql) -> true;
×
210
             (_) -> false
×
211
          end,
212
          ejabberd_auth:auth_modules(Server)),
UNCOV
213
    Modules = get_register_modules(Server),
×
UNCOV
214
    ModRegisterAllowsMe = (Modules == all) orelse lists:member(?MODULE, Modules),
×
215
    [{<<"allow_registration">>, AuthSupportsRegister and ModRegisterAllowsMe}].
×
216

217
get_register_modules(Server) ->
218
    try mod_register_opt:allow_modules(Server)
×
219
    catch
220
        error:{module_not_loaded, mod_register, _} ->
UNCOV
221
            ?DEBUG("mod_conversejs couldn't get mod_register configuration for "
×
UNCOV
222
                   "vhost ~p: module not loaded in that vhost.", [Server]),
×
UNCOV
223
            []
×
224
    end.
225

226
get_extra_options(Host) ->
UNCOV
227
    RawOpts = gen_mod:get_module_opt(Host, ?MODULE, conversejs_options),
×
228
    lists:map(fun({Name, <<"true">>}) -> {Name, true};
×
UNCOV
229
                 ({Name, <<"false">>}) -> {Name, false};
×
230
                 ({<<"locked_domain">> = Name, Value}) ->
UNCOV
231
                      {Name, misc:expand_keyword(<<"@HOST@">>, Value, Host)};
×
232
                 ({Name, Value}) ->
UNCOV
233
                      {Name, Value}
×
234
              end,
235
              RawOpts).
236

237
get_file_url(Host, Option, Filename, Default) ->
UNCOV
238
    FileRaw = case gen_mod:get_module_opt(Host, ?MODULE, Option) of
×
239
                  auto -> get_auto_file_url(Host, Filename, Default);
×
UNCOV
240
                  F -> F
×
241
              end,
242
    misc:expand_keyword(<<"@HOST@">>, FileRaw, Host).
×
243

244
get_auto_file_url(Host, Filename, Default) ->
UNCOV
245
    case get_conversejs_resources(Host) of
×
UNCOV
246
        undefined -> Default;
×
UNCOV
247
        _ -> Filename
×
248
    end.
249

250
get_plugins_html(Host, RawPath) ->
UNCOV
251
    Resources = get_conversejs_resources(Host),
×
UNCOV
252
    lists:map(fun(F) ->
×
253
                 Plugin =
×
254
                     case {F, Resources} of
255
                         {<<"libsignal">>, undefined} ->
UNCOV
256
                             <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>;
×
257
                         {<<"libsignal">>, Path} ->
258
                             ?WARNING_MSG("~p is configured to use local Converse files "
×
259
                                          "from path ~ts but the public plugin ~ts!",
260
                                          [?MODULE, Path, F]),
×
UNCOV
261
                             <<"https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js">>;
×
262
                         _ ->
UNCOV
263
                             fxml:crypt(<<RawPath/binary, "/plugins/", F/binary>>)
×
264
                     end,
UNCOV
265
                 <<"<script src='", Plugin/binary, "' charset='utf-8'></script>">>
×
266
              end,
267
              gen_mod:get_module_opt(Host, ?MODULE, conversejs_plugins)).
268

269
%%----------------------------------------------------------------------
270
%% WebAdmin link and autologin
271
%%----------------------------------------------------------------------
272

273
%% @format-begin
274

275
http_handlers_init(Handlers, _Opts) ->
UNCOV
276
    Handlers2 =
×
277
        lists:foldl(fun ({Path, ejabberd_web_admin} = Handler, Acc) ->
278
                            [Handler, {lists:append(Path, [?AUTOLOGIN_PATH]), mod_conversejs}
×
279
                             | Acc];
280
                        (Handler, Acc) ->
UNCOV
281
                            [Handler | Acc]
×
282
                    end,
283
                    [],
284
                    Handlers),
285
    lists:reverse(Handlers2).
×
286

287
web_menu_system(Result, #request{tp = Protocol}, Level) ->
UNCOV
288
    Els = ejabberd_web_admin:make_menu_system(?MODULE, "☯️", "Converse", ""),
×
UNCOV
289
    Base = iolist_to_binary(lists:duplicate(Level, "../")),
×
290
    ThisTls =
×
291
        case Protocol of
292
            http ->
UNCOV
293
                false;
×
294
            https ->
UNCOV
295
                true
×
296
        end,
UNCOV
297
    ConverseEl2 =
×
298
        ejabberd_web_admin:make_menu_system_el("☯️",
299
                                               "Converse (autologin)",
300
                                               binary_to_list(?AUTOLOGIN_PATH),
301
                                               {ThisTls, Base}),
UNCOV
302
    lists:flatten([ConverseEl2, Els, Result]).
×
303
%% @format-end
304

305
%%----------------------------------------------------------------------
306
%%
307
%%----------------------------------------------------------------------
308

309
mod_opt_type(bosh_service_url) ->
UNCOV
310
    econf:either(auto, econf:binary());
×
311
mod_opt_type(websocket_url) ->
UNCOV
312
    econf:either(auto, econf:binary());
×
313
mod_opt_type(conversejs_resources) ->
UNCOV
314
    econf:either(undefined, econf:directory());
×
315
mod_opt_type(conversejs_options) ->
UNCOV
316
    econf:map(econf:binary(), econf:either(econf:binary(), econf:int()));
×
317
mod_opt_type(conversejs_script) ->
UNCOV
318
    econf:binary();
×
319
mod_opt_type(conversejs_css) ->
UNCOV
320
    econf:binary();
×
321
mod_opt_type(conversejs_plugins) ->
UNCOV
322
    econf:list(econf:binary());
×
323
mod_opt_type(default_domain) ->
324
    econf:host().
×
325

326
mod_options(Host) ->
UNCOV
327
    [{bosh_service_url, auto},
×
328
     {websocket_url, auto},
329
     {default_domain, Host},
330
     {conversejs_resources, undefined},
331
     {conversejs_options, []},
332
     {conversejs_script, auto},
333
     {conversejs_plugins, []},
334
     {conversejs_css, auto}].
335

336
mod_doc() ->
UNCOV
337
    #{desc =>
×
338
          [?T("This module serves a simple page for the "
339
              "https://conversejs.org/[Converse] XMPP web browser client."), "",
340
           ?T("To use this module, in addition to adding it to the 'modules' "
341
              "section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
342
              "_`listen-options.md#request_handlers|request_handlers`_."), "",
343
           ?T("Make sure either _`mod_bosh`_ or _`listen.md#ejabberd_http_ws|ejabberd_http_ws`_ "
344
              "are enabled in at least one 'request_handlers'."), "",
345
           ?T("When 'conversejs_css' and 'conversejs_script' are 'auto', "
346
              "by default they point to the public Converse client."), "",
347
           ?T("This module is available since ejabberd 21.12.")
348
          ],
349
      note => "improved in 25.07",
350
      example =>
351
          [{?T("Manually setup WebSocket url, and use the public Converse client:"),
352
            ["listen:",
353
             "  -",
354
             "    port: 5280",
355
             "    module: ejabberd_http",
356
             "    request_handlers:",
357
             "      /bosh: mod_bosh",
358
             "      /websocket: ejabberd_http_ws",
359
             "      /conversejs: mod_conversejs",
360
             "",
361
             "modules:",
362
             "  mod_bosh: {}",
363
             "  mod_conversejs:",
364
             "    conversejs_plugins: [\"libsignal\"]",
365
             "    websocket_url: \"ws://@HOST@:5280/websocket\""]},
366
           {?T("Host Converse locally and let auto detection of WebSocket and Converse URLs:"),
367
            ["listen:",
368
             "  -",
369
             "    port: 443",
370
             "    module: ejabberd_http",
371
             "    tls: true",
372
             "    request_handlers:",
373
             "      /websocket: ejabberd_http_ws",
374
             "      /conversejs: mod_conversejs",
375
             "",
376
             "modules:",
377
             "  mod_conversejs:",
378
             "    conversejs_resources: \"/home/ejabberd/conversejs-x.y.z/package/dist\"",
379
             "    conversejs_plugins: [\"libsignal-protocol.min.js\"]",
380
             "    # File path is: /home/ejabberd/conversejs-x.y.z/package/dist/plugins/libsignal-protocol.min.js"]},
381
           {?T("Configure some additional options for Converse"),
382
            ["modules:",
383
             "  mod_conversejs:",
384
             "    websocket_url: auto",
385
             "    conversejs_options:",
386
             "      auto_away: 30",
387
             "      clear_cache_on_logout: true",
388
             "      i18n: \"pt\"",
389
             "      locked_domain: \"@HOST@\"",
390
             "      message_archiving: always",
391
             "      theme: dracula"]}
392
          ],
393
      opts =>
394
          [{websocket_url,
395
            #{value => ?T("auto | WebSocketURL"),
396
              desc =>
397
                  ?T("A WebSocket URL to which Converse can connect to. "
398
                     "The '@HOST@' keyword is replaced with the real virtual "
399
                     "host name. "
400
                     "If set to 'auto', it will build the URL of the first "
401
                     "configured WebSocket request handler. "
402
                     "The default value is 'auto'.")}},
403
           {bosh_service_url,
404
            #{value => ?T("auto | BoshURL"),
405
              desc =>
406
                  ?T("BOSH service URL to which Converse can connect to. "
407
                     "The keyword '@HOST@' is replaced with the real "
408
                     "virtual host name. "
409
                     "If set to 'auto', it will build the URL of the first "
410
                     "configured BOSH request handler. "
411
                     "The default value is 'auto'.")}},
412
           {default_domain,
413
            #{value => ?T("Domain"),
414
              desc =>
415
                  ?T("Specify a domain to act as the default for user JIDs. "
416
                     "The keyword '@HOST@' is replaced with the hostname. "
417
                     "The default value is '@HOST@'.")}},
418
           {conversejs_resources,
419
            #{value => ?T("Path"),
420
              note => "added in 22.05",
421
              desc =>
422
                  ?T("Local path to the Converse files. "
423
                     "If not set, the public Converse client will be used instead.")}},
424
           {conversejs_options,
425
            #{value => "{Name: Value}",
426
              note => "added in 22.05",
427
              desc =>
428
                  ?T("Specify additional options to be passed to Converse. "
429
                     "See https://conversejs.org/docs/html/configuration.html[Converse configuration]. "
430
                     "Only boolean, integer and string values are supported; "
431
                     "lists are not supported.")}},
432
           {conversejs_plugins,
433
            #{value => ?T("[Filename]"),
434
              desc =>
435
                  ?T("List of additional local files to include as scripts in the homepage. "
436
                     "Please make sure those files are available in the path specified in "
437
                     "'conversejs_resources' option, in subdirectory 'plugins/'. "
438
                     "If using the public Converse client, then '\"libsignal\"' "
439
                     "gets replaced with the URL of the public library. "
440
                     "The default value is '[]'.")}},
441
           {conversejs_script,
442
            #{value => ?T("auto | URL"),
443
              desc =>
444
                  ?T("Converse main script URL. "
445
                     "The keyword '@HOST@' is replaced with the hostname. "
446
                     "The default value is 'auto'.")}},
447
           {conversejs_css,
448
            #{value => ?T("auto | URL"),
449
              desc =>
450
                  ?T("Converse CSS URL. "
451
                     "The keyword '@HOST@' is replaced with the hostname. "
452
                     "The default value is 'auto'.")}}]
453
     }.
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