diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl index 3b9efef67..48e0c2556 100644 --- a/src/mod_conversejs.erl +++ b/src/mod_conversejs.erl @@ -50,15 +50,22 @@ reload(_Host, _NewOpts, _OldOpts) -> depends(_Host, _Opts) -> []. -process([], #request{method = 'GET'}) -> - Host = ejabberd_config:get_myname(), - Domain = gen_mod:get_module_opt(Host, ?MODULE, default_domain), - Script = gen_mod:get_module_opt(Host, ?MODULE, conversejs_script), - CSS = gen_mod:get_module_opt(Host, ?MODULE, conversejs_css), +process([], #request{method = 'GET', host = Host, raw_path = RawPath}) -> + DomainRaw = gen_mod:get_module_opt(Host, ?MODULE, default_domain), + Domain = misc:expand_keyword(<<"@HOST@">>, DomainRaw, Host), + Script = get_file_url(Host, conversejs_script, + <>, + <<"https://cdn.conversejs.org/dist/converse.min.js">>), + CSS = get_file_url(Host, conversejs_css, + <>, + <<"https://cdn.conversejs.org/dist/converse.min.css">>), Init = [{<<"discover_connection_methods">>, false}, {<<"jid">>, Domain}, {<<"default_domain">>, Domain}, {<<"domain_placeholder">>, Domain}, + {<<"registration_domain">>, Domain}, + {<<"assets_path">>, RawPath}, + {<<"i18n">>, ejabberd_option:language(Host)}, {<<"view_mode">>, <<"fullscreen">>}], Init2 = case gen_mod:get_module_opt(Host, ?MODULE, websocket_url) of @@ -85,13 +92,96 @@ process([], #request{method = 'GET'}) -> <<"">>, <<"">>, <<"">>]}; -process(_, _) -> - ejabberd_web:error(not_found). +process(LocalPath, #request{host = Host}) -> + case is_served_file(LocalPath) of + true -> serve(Host, LocalPath); + false -> ejabberd_web:error(not_found) + end. + +%%---------------------------------------------------------------------- +%% File server +%%---------------------------------------------------------------------- + +is_served_file([<<"converse.min.js">>]) -> true; +is_served_file([<<"converse.min.css">>]) -> true; +is_served_file([<<"converse.min.js.map">>]) -> true; +is_served_file([<<"converse.min.css.map">>]) -> true; +is_served_file([<<"emojis.js">>]) -> true; +is_served_file([<<"locales">>, _]) -> true; +is_served_file([<<"locales">>, <<"dayjs">>, _]) -> true; +is_served_file([<<"webfonts">>, _]) -> true; +is_served_file(_) -> false. + +serve(Host, LocalPath) -> + case get_conversejs_resources(Host) of + undefined -> ejabberd_web:error(not_found); + MainPath -> serve2(LocalPath, MainPath) + end. + +get_conversejs_resources(Host) -> + Opts = gen_mod:get_module_opts(Host, ?MODULE), + mod_conversejs_opt:conversejs_resources(Opts). + +%% Copied from mod_muc_log_http.erl + +serve2(LocalPathBin, MainPathBin) -> + LocalPath = [binary_to_list(LPB) || LPB <- LocalPathBin], + MainPath = binary_to_list(MainPathBin), + FileName = filename:join(filename:split(MainPath) ++ LocalPath), + case file:read_file(FileName) of + {ok, FileContents} -> + ?DEBUG("Delivering content.", []), + {200, + [{<<"Content-Type">>, content_type(FileName)}], + FileContents}; + {error, eisdir} -> + {403, [], "Forbidden"}; + {error, Error} -> + ?DEBUG("Delivering error: ~p", [Error]), + case Error of + eacces -> {403, [], "Forbidden"}; + enoent -> {404, [], "Not found"}; + _Else -> {404, [], atom_to_list(Error)} + end + end. + +content_type(Filename) -> + case string:to_lower(filename:extension(Filename)) of + ".css" -> "text/css"; + ".js" -> "text/javascript"; + ".map" -> "application/json"; + ".ttf" -> "font/ttf"; + ".woff" -> "font/woff"; + ".woff2" -> "font/woff2" + end. + +%%---------------------------------------------------------------------- +%% Options parsing +%%---------------------------------------------------------------------- + +get_file_url(Host, Option, Filename, Default) -> + FileRaw = case gen_mod:get_module_opt(Host, ?MODULE, Option) of + auto -> get_auto_file_url(Host, Filename, Default); + F -> F + end, + misc:expand_keyword(<<"@HOST@">>, FileRaw, Host). + +get_auto_file_url(Host, Filename, Default) -> + case get_conversejs_resources(Host) of + undefined -> Default; + _ -> Filename + end. + +%%---------------------------------------------------------------------- +%% +%%---------------------------------------------------------------------- mod_opt_type(bosh_service_url) -> econf:either(undefined, econf:binary()); mod_opt_type(websocket_url) -> econf:either(undefined, econf:binary()); +mod_opt_type(conversejs_resources) -> + econf:either(undefined, econf:directory()); mod_opt_type(conversejs_script) -> econf:binary(); mod_opt_type(conversejs_css) -> @@ -102,9 +192,10 @@ mod_opt_type(default_domain) -> mod_options(_) -> [{bosh_service_url, undefined}, {websocket_url, undefined}, - {default_domain, ejabberd_config:get_myname()}, - {conversejs_script, <<"https://cdn.conversejs.org/dist/converse.min.js">>}, - {conversejs_css, <<"https://cdn.conversejs.org/dist/converse.min.css">>}]. + {default_domain, <<"@HOST@">>}, + {conversejs_resources, undefined}, + {conversejs_script, auto}, + {conversejs_css, auto}]. mod_doc() -> #{desc => @@ -115,9 +206,8 @@ mod_doc() -> "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " "http://../listen-options/#request-handlers[request_handlers]."), "", ?T("You must also setup either the option 'websocket_url' or 'bosh_service_url'."), "", - ?T("By default, the options 'conversejs_css' and 'conversejs_script'" - " point to the public Converse.js client. Alternatively, you can" - " host the client locally using _`mod_http_fileserver`_.") + ?T("When 'conversejs_css' and 'conversejs_script' are 'auto', " + "by default they point to the public Converse client.") ], example => ["listen:", @@ -130,6 +220,7 @@ mod_doc() -> "", "modules:", " mod_conversejs:", + " conversejs_resources: \"/home/ejabberd/conversejs-9.0.0/package/dist\"", " websocket_url: \"ws://example.org:5280/websocket\""], opts => [{websocket_url, @@ -144,14 +235,23 @@ mod_doc() -> #{value => ?T("Domain"), desc => ?T("Specify a domain to act as the default for user JIDs. " - "The default value is the first domain defined in the " - "ejabberd configuration file.")}}, + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is '@HOST@'.")}}, + {conversejs_resources, + #{value => ?T("Path"), + desc => + ?T("Local path to the Converse files. " + "If not set, the public Converse client will be used instead.")}}, {conversejs_script, - #{value => ?T("URL"), + #{value => ?T("auto | URL"), desc => - ?T("Converse.js main script URL.")}}, + ?T("Converse main script URL. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is 'auto'.")}}, {conversejs_css, - #{value => ?T("URL"), + #{value => ?T("auto | URL"), desc => - ?T("Converse.js CSS URL.")}}] + ?T("Converse CSS URL. " + "The keyword '@HOST@' is replaced with the hostname. " + "The default value is 'auto'.")}}] }. diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl index 9e53978ea..f43b3e830 100644 --- a/src/mod_conversejs_opt.erl +++ b/src/mod_conversejs_opt.erl @@ -5,6 +5,7 @@ -export([bosh_service_url/1]). -export([conversejs_css/1]). +-export([conversejs_resources/1]). -export([conversejs_script/1]). -export([default_domain/1]). -export([websocket_url/1]). @@ -15,13 +16,19 @@ bosh_service_url(Opts) when is_map(Opts) -> bosh_service_url(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, bosh_service_url). --spec conversejs_css(gen_mod:opts() | global | binary()) -> binary(). +-spec conversejs_css(gen_mod:opts() | global | binary()) -> 'auto' | binary(). conversejs_css(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_css, Opts); conversejs_css(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, conversejs_css). --spec conversejs_script(gen_mod:opts() | global | binary()) -> binary(). +-spec conversejs_resources(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +conversejs_resources(Opts) when is_map(Opts) -> + gen_mod:get_opt(conversejs_resources, Opts); +conversejs_resources(Host) -> + gen_mod:get_module_opt(Host, mod_conversejs, conversejs_resources). + +-spec conversejs_script(gen_mod:opts() | global | binary()) -> 'auto' | binary(). conversejs_script(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_script, Opts); conversejs_script(Host) ->