%%%---------------------------------------------------------------------- %%% File : mod_conversejs.erl %%% Author : Alexey Shchepin %%% Purpose : Serve simple page for Converse.js XMPP web browser client %%% Created : 8 Nov 2021 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2022 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(mod_conversejs). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("translate.hrl"). -include("ejabberd_web_admin.hrl"). start(_Host, _Opts) -> ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. process([], #request{method = 'GET', host = Host, raw_path = RawPath}) -> ExtraOptions = get_auth_options(Host) ++ get_register_options(Host) ++ get_extra_options(Host), 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}, {<<"default_domain">>, Domain}, {<<"domain_placeholder">>, Domain}, {<<"registration_domain">>, Domain}, {<<"assets_path">>, RawPath}, {<<"i18n">>, ejabberd_option:language(Host)}, {<<"view_mode">>, <<"fullscreen">>} | ExtraOptions], Init2 = case mod_host_meta:get_url(?MODULE, websocket, any, Host) of undefined -> Init; WSURL -> [{<<"websocket_url">>, WSURL} | Init] end, Init3 = case mod_host_meta:get_url(?MODULE, bosh, any, Host) of undefined -> Init2; BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2] end, {200, [html], [<<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>]}; 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_auth_options(Domain) -> case {ejabberd_auth_anonymous:is_login_anonymous_enabled(Domain), ejabberd_auth_anonymous:is_sasl_anonymous_enabled(Domain)} of {false, false} -> [{<<"authentication">>, <<"login">>}]; {true, false} -> [{<<"authentication">>, <<"external">>}]; {_, true} -> [{<<"authentication">>, <<"anonymous">>}, {<<"jid">>, Domain}] end. get_register_options(Server) -> AuthSupportsRegister = lists:any( fun(ejabberd_auth_mnesia) -> true; (ejabberd_auth_external) -> true; (ejabberd_auth_sql) -> true; (_) -> false end, ejabberd_auth:auth_modules(Server)), Modules = mod_register_opt:allow_modules(Server), ModRegisterAllowsMe = (Modules == all) orelse lists:member(?MODULE, Modules), [{<<"allow_registration">>, AuthSupportsRegister and ModRegisterAllowsMe}]. get_extra_options(Host) -> RawOpts = gen_mod:get_module_opt(Host, ?MODULE, conversejs_options), lists:map(fun({Name, <<"true">>}) -> {Name, true}; ({Name, <<"false">>}) -> {Name, false}; ({<<"locked_domain">> = Name, Value}) -> {Name, misc:expand_keyword(<<"@HOST@">>, Value, Host)}; ({Name, Value}) -> {Name, Value} end, RawOpts). 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(auto, econf:binary()); mod_opt_type(websocket_url) -> econf:either(auto, econf:binary()); mod_opt_type(conversejs_resources) -> econf:either(undefined, econf:directory()); mod_opt_type(conversejs_options) -> econf:map(econf:binary(), econf:either(econf:binary(), econf:int())); mod_opt_type(conversejs_script) -> econf:binary(); mod_opt_type(conversejs_css) -> econf:binary(); mod_opt_type(default_domain) -> econf:binary(). mod_options(_) -> [{bosh_service_url, auto}, {websocket_url, auto}, {default_domain, <<"@HOST@">>}, {conversejs_resources, undefined}, {conversejs_options, []}, {conversejs_script, auto}, {conversejs_css, auto}]. mod_doc() -> #{desc => [?T("This module serves a simple page for the " "https://conversejs.org/[Converse] XMPP web browser client."), "", ?T("This module is available since ejabberd 21.12."), "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " "http://../listen-options/#request-handlers[request_handlers]."), "", ?T("Make sure either 'mod_bosh' or 'ejabberd_http_ws' " "http://../listen-options/#request-handlers[request_handlers] " "are enabled."), "", ?T("When 'conversejs_css' and 'conversejs_script' are 'auto', " "by default they point to the public Converse client.") ], example => [{?T("Manually setup WebSocket url, and use the public Converse client:"), ["listen:", " -", " port: 5280", " module: ejabberd_http", " request_handlers:", " /bosh: mod_bosh", " /websocket: ejabberd_http_ws", " /conversejs: mod_conversejs", "", "modules:", " mod_bosh: {}", " mod_conversejs:", " websocket_url: \"ws://@HOST@:5280/websocket\""]}, {?T("Host Converse locally and let auto detection of WebSocket and Converse URLs:"), ["listen:", " -", " port: 443", " module: ejabberd_http", " tls: true", " request_handlers:", " /websocket: ejabberd_http_ws", " /conversejs: mod_conversejs", "", "modules:", " mod_conversejs:", " conversejs_resources: \"/home/ejabberd/conversejs-9.0.0/package/dist\""]}, {?T("Configure some additional options for Converse"), ["modules:", " mod_conversejs:", " websocket_url: auto", " conversejs_options:", " auto_away: 30", " clear_cache_on_logout: true", " i18n: \"pt\"", " locked_domain: \"@HOST@\"", " message_archiving: always", " theme: dracula"]} ], opts => [{websocket_url, #{value => ?T("auto | WebSocketURL"), desc => ?T("A WebSocket URL to which Converse can connect to. " "The keyword '@HOST@' is replaced with the real virtual " "host name. " "If set to 'auto', it will build the URL of the first " "configured WebSocket request handler. " "The default value is 'auto'.")}}, {bosh_service_url, #{value => ?T("auto | BoshURL"), desc => ?T("BOSH service URL to which Converse can connect to. " "The keyword '@HOST@' is replaced with the real " "virtual host name. " "If set to 'auto', it will build the URL of the first " "configured BOSH request handler. " "The default value is 'auto'.")}}, {default_domain, #{value => ?T("Domain"), desc => ?T("Specify a domain to act as the default for user JIDs. " "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_options, #{value => "{Name: Value}", desc => ?T("Specify additional options to be passed to Converse. " "See https://conversejs.org/docs/html/configuration.html[Converse configuration]. " "Only boolean, integer and string values are supported; " "lists are not supported.")}}, {conversejs_script, #{value => ?T("auto | URL"), desc => ?T("Converse main script URL. " "The keyword '@HOST@' is replaced with the hostname. " "The default value is 'auto'.")}}, {conversejs_css, #{value => ?T("auto | URL"), desc => ?T("Converse CSS URL. " "The keyword '@HOST@' is replaced with the hostname. " "The default value is 'auto'.")}}] }.