2
votes

Original

I was trying for days to adapt an open source Ejabberd module (mod_offline_http_post), working for previous versions, to the current version (20.04). For example, some parts are now important to be exported like mod_options and mod_depends. They were not part of the module source code. I added them. I still got an error.

.ejabberd-modules/sources/mod_offline_http_post/src/mod_offline_http_post.erl

%% name of module must match file name
%% Update: [email protected]
-module(mod_offline_http_post).
-author("[email protected]").

-behaviour(gen_mod).

-export([start/2, stop/1, depends/2, mod_options/1, create_message/1, create_message/3]).

%% Required by ?INFO_MSG macros
-include("logger.hrl").

-include("scram.hrl").
-include("xmpp.hrl").

start(_Host, _Opt) ->
  ?INFO_MSG("mod_offline_http_post loading", []),
  inets:start(),
  ?INFO_MSG("HTTP client started", []),
  ejabberd_hooks:add(offline_message_hook, _Host, ?MODULE, create_message, 1).

stop (_Host) ->
  ?INFO_MSG("stopping mod_offline_http_post", []),
  ejabberd_hooks:delete(offline_message_hook, _Host, ?MODULE, create_message, 1).

depends(_Host, _Opts) ->
  [].

mod_options(_Host) ->
  [].

create_message({Action, Packet} = Acc) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
        [{text, _, Body}] = Packet#message.body,
        post_offline_message(Packet#message.from, Packet#message.to, Body, Packet#message.id),
  Acc;

create_message(Acc) ->
  Acc.

create_message(_From, _To, Packet) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
  Body = fxml:get_path_s(Packet, [{elem, list_to_binary("body")}, cdata]),
  MessageId = fxml:get_tag_attr_s(list_to_binary("id"), Packet),
  post_offline_message(_From, _To, Body, MessageId),
  ok.

post_offline_message(From, To, Body, MessageId) ->
  ?INFO_MSG("Posting From ~p To ~p Body ~p ID ~p~n",[From, To, Body, MessageId]),
  Token = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, auth_token, fun(S) -> iolist_to_binary(S) end, list_to_binary("")),
  PostUrl = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, post_url, fun(S) -> iolist_to_binary(S) end, list_to_binary("")),
  ToUser = To#jid.luser,
  FromUser = From#jid.luser,
  Vhost = To#jid.lserver,
  case gen_mod:get_module_opt(To#jid.lserver, ?MODULE, confidential, false) of
    true -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&messageId=", binary_to_list(MessageId)], "");
    false -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&body=", binary_to_list(Body), "&messageId=", binary_to_list(MessageId)], "")
  end,
  Request = {binary_to_list(PostUrl), [{"Authorization", binary_to_list(Token)}], "application/x-www-form-urlencoded", Data},
  httpc:request(post, Request,[],[]),
  ?INFO_MSG("post request sent", []).

.ejabberd-modules/sources/mod_offline_http_post/conf/mod_offline_http_post.yml

modules:
  mod_offline_http_post:
    auth_token: "secret"
    post_url: "http://SERVER_IP_ADDRESS/end_of_url"
    confidential: false
    
### Local Variables:
### mode: yaml
### End:
### vim: set filetype=yaml tabstop=8

I can provide more details if needed.

Thank's in advance for your help.

EDIT 1

It seems it is working now... There is a log when a message is sent to an offline user and options are now recognized. I will post what I did to make it work, as an answer, once there is no more problem. This, for the other people.

The remaining problem is that I doubt that the module is called at all. The log I talked about is not from the module but probably from the Ejabberd router. I talked about it because there was nothing like this in the past. There is something positive going on, then. To make the matter more certain, it displays "Bad module" when I uninstall the module.

Here is the new erl code:

%% name of module must match file name
%% Update: [email protected]
-module(mod_offline_http_post).
-author("[email protected]").

-behaviour(gen_mod).

-export([start/2,
        stop/1,
        depends/2,
        mod_options/1,
        mod_opt_type/1,
        create_message/1,
        create_message/3]).

%% Required by ?INFO_MSG macros
-include("logger.hrl").

-include("scram.hrl").
-include("xmpp.hrl").

start(_Host, _Opt) ->
  ?INFO_MSG("mod_offline_http_post loading", []),
  inets:start(),
  ?INFO_MSG("HTTP client started", []),
  ejabberd_hooks:add(offline_message_hook, _Host, ?MODULE, create_message, 1).

stop (_Host) ->
  ?INFO_MSG("stopping mod_offline_http_post", []),
  ejabberd_hooks:delete(offline_message_hook, _Host, ?MODULE, create_message, 1).

depends(_Host, _Opts) ->
  [].

mod_options(_Host) ->
  [{auth_token, <<"secret">>},
  {post_url, <<"http://example.com/test">>},
  {confidential, false}].

mod_opt_type(auth_token) ->
  fun iolist_to_binary/1;
mod_opt_type(post_url) ->
  fun iolist_to_binary/1;
mod_opt_type(confidential) ->
  fun (B) when is_boolean(B) -> B end.

create_message({Action, Packet} = Acc) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
	[{text, _, Body}] = Packet#message.body,
	post_offline_message(Packet#message.from, Packet#message.to, Body, Packet#message.id),
  Acc;

create_message(Acc) ->
  Acc.

create_message(_From, _To, Packet) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
  Body = fxml:get_path_s(Packet, [{elem, list_to_binary("body")}, cdata]),
  MessageId = fxml:get_tag_attr_s(list_to_binary("id"), Packet),
  post_offline_message(_From, _To, Body, MessageId),
  ok.

post_offline_message(From, To, Body, MessageId) ->
  ?DEBUG("Posting From ~p To ~p Body ~p ID ~p~n",[From, To, Body, MessageId]),
  Token = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, auth_token, fun(S) -> iolist_to_binary(S) end, list_to_binary("")),
  PostUrl = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, post_url, fun(S) -> iolist_to_binary(S) end, list_to_binary("")),
  ToUser = To#jid.luser,
  FromUser = From#jid.luser,
  Vhost = To#jid.lserver,
  case gen_mod:get_module_opt(To#jid.lserver, ?MODULE, confidential, false) of
    true -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&messageId=", binary_to_list(MessageId)], "");
    false -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&body=", binary_to_list(Body), "&messageId=", binary_to_list(MessageId)], "")
  end,
  Request = {binary_to_list(PostUrl), [{"Authorization", binary_to_list(Token)}], "application/x-www-form-urlencoded", Data},
  httpc:request(post, Request,[],[]),
  ?DEBUG("post request sent", []).

EDIT 2

Here is a very promising case. The module is called. It's just that it crashes which is another very interesting case. Here is the error log:

2020-05-21 11:53:35.897 [error] <0.508.0>@ejabberd_hooks:safe_apply:240 Hook offline_message_hook crashed when running mod_offline_http_post:create_message/1:
** exception error: undefined function gen_mod:get_module_opt/5
   in function  mod_offline_http_post:post_offline_message/4 (/opt/ejabberd/.ejabberd-modules/sources/mod_offline_http_post/src/mod_offline_http_post.erl, line 63)
   in call from mod_offline_http_post:create_message/1 (/opt/ejabberd/.ejabberd-modules/sources/mod_offline_http_post/src/mod_offline_http_post.erl, line 49)
   in call from ejabberd_hooks:safe_apply/4 (src/ejabberd_hooks.erl, line 236)
   in call from ejabberd_hooks:run_fold1/4 (src/ejabberd_hooks.erl, line 217)
   in call from ejabberd_sm:route/1 (src/ejabberd_sm.erl, line 146)
   in call from ejabberd_router:do_route/1 (src/ejabberd_router.erl, line 399)
   in call from ejabberd_router:route/1 (src/ejabberd_router.erl, line 92)
1

1 Answers

4
votes

IT WORKS !!

Amazing. It's now working. Notifications are sent to offline users. Well, I mean that messages to offline users are forwarded to the backend that, in turn, sends FCM notification to the users.

Since I spent 3 days on this and having little to almost no infos about current version 20.04 about this problem, I will go in details about the solution for whoever meets that problem.

First and foremost, in addition of start/2 and stop/1, export these 3 functions if your custom module supports options: depends/2 (probably a function that indicates dependencies i.e you list required modules there if I'm not wrong), mod_opt_type/1 (each option should be validated) and mod_options/2 (where you list the options and their default value).

Second, many custom modules from the internet are made for previous versions where these were not required. Then, if they use gen_mod:get_module_opt, they add the validation and the default value. You end up having gen_mod:get_module_opt/4 or gen_mod:get_module_opt/5. Now, since you already got the validation and the default value for each option in mod_options/2 and mod_opt_type/1, mod:get_module_opt becomes mod:get_module_opt/3. You should keep only the 3 first args.

If you installed Ejabberd via the RPM package like in my case, you might have the server compiled without LAGER enabled. Well, or an explaination like that and this, at least for custom modules. I mean, you see the logs for modules embedded in the package but not yours from your custom module. To enable LAGER or your infos (please, correct these statements if I didn't use the correct terms), do this right after export:

-ifndef(LAGER).
-define(LAGER, 1).
-endif.

Even though this piece of code seems to be related only to debugging processes, the custom module was not called at all when I didn't have it in the code. Interestingly strange.

I saw some people having problems with that module "mod_offline_http_post" with the error like ebin directory not found. The thing is, once you make a git clone in the .ejabberd-modules/sources directory, the module will be there but Ejabberd might lack permissions over this. Just do this after that: chown -R ejabberd:ejabberd your_path_to_.ejabberd-modules/sources After that, hit ejabberdctl install mod_offline_http_post. If you see warning like ArgumentX not used, ignore this. It just means what it said. To prevent displaying this, just put an underscore in front of the arg that is not used in your erl file i.e _ArgumentX.

Some other people don't even have the .ejabberd-modules directory. Just install a random contrib module like mod_cron and this directory will be created. Go where ejabberdctl is, mine is in /opt/ejabberd-20.04/bin and hit: ejabberdctl module_install mod_cron Once done, don't remove it but go to /opt/ejabberd/.ejabberd-modules/sources and git clone the custom module there. After that, uninstall mod_cron by doing ejabberdctl module_uninstall mod_cron.

Your directory .ejabberd-modules might be somewhere else though. After these commands, you don't need to restart Ejabberd. As a matter of fact, module_install compiles, installs and starts the module.

Well, since you need to put the module inside ejabberd.yml, you have to restart ejabberd but once done, if some things don't work and you end up uninstalling the custom module, once reinstalled, you don't need to restart Ejabberd.

I will post the original code that is working for Ejabberd 19.02 and the edited code that works with 20.04

For 19.02

%% name of module must match file name
%% Update: [email protected]
-module(mod_offline_http_post).
-author("[email protected]").

-behaviour(gen_mod).

-export([start/2, stop/1, create_message/1, create_message/3]).

-include("scram.hrl").
-include("xmpp.hrl").
-include("logger.hrl").

start(_Host, _Opt) ->
  ?INFO_MSG("mod_offline_http_post loading", []),
  inets:start(),
  ?INFO_MSG("HTTP client started", []),
  ejabberd_hooks:add(offline_message_hook, _Host, ?MODULE, create_message, 1).

stop (_Host) ->
  ?INFO_MSG("stopping mod_offline_http_post", []),
  ejabberd_hooks:delete(offline_message_hook, _Host, ?MODULE, create_message, 1).

create_message({Action, Packet} = Acc) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
    [{text, _, Body}] = Packet#message.body,
    post_offline_message(Packet#message.from, Packet#message.to, Body, Packet#message.id),
  Acc;

create_message(Acc) ->
  Acc.

create_message(_From, _To, Packet) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
  Body = fxml:get_path_s(Packet, [{elem, list_to_binary("body")}, cdata]),
  MessageId = fxml:get_tag_attr_s(list_to_binary("id"), Packet),
  post_offline_message(_From, _To, Body, MessageId),
  ok.

post_offline_message(From, To, Body, MessageId) ->
  ?INFO_MSG("Posting From ~p To ~p Body ~p ID ~p~n",[From, To, Body, MessageId]),
  Token = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, auth_token, fun(S) -> iolist_to_binary(S) end, list_to_binary("")),
  PostUrl = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, post_url, fun(S) -> iolist_to_binary(S) end, list_to_binary("")),
  ToUser = To#jid.luser,
  FromUser = From#jid.luser,
  Vhost = To#jid.lserver,
  case gen_mod:get_module_opt(To#jid.lserver, ?MODULE, confidential, false) of
    true -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&messageId=", binary_to_list(MessageId)], "");
    false -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&body=", binary_to_list(Body), "&messageId=", binary_to_list(MessageId)], "")
  end,
  Request = {binary_to_list(PostUrl), [{"Authorization", binary_to_list(Token)}], "application/x-www-form-urlencoded", Data},
  httpc:request(post, Request,[],[]),
  ?INFO_MSG("post request sent", []).

For 20.04

%% name of module must match file name
%% Update: [email protected]
-module(mod_offline_http_post).
-author("[email protected]").

-behaviour(gen_mod).

-export([start/2,
        stop/1,
        depends/2,
        mod_options/1,
        mod_opt_type/1,
        create_message/1,
        create_message/3]).

-ifndef(LAGER).
-define(LAGER, 1).
-endif.

-include("logger.hrl").
-include("xmpp.hrl").

start(_Host, _Opt) ->
  ?INFO_MSG("mod_offline_http_post loading", []),
  inets:start(),
  ?INFO_MSG("HTTP client started", []),
  ejabberd_hooks:add(offline_message_hook, _Host, ?MODULE, create_message, 50).

stop (_Host) ->
  ?INFO_MSG("stopping mod_offline_http_post", []),
  ejabberd_hooks:delete(offline_message_hook, _Host, ?MODULE, create_message, 50).

depends(_Host, _Opts) ->
  [].

mod_options(_Host) ->
  [{auth_token, <<"secret">>},
  {post_url, <<"http://example.com/test">>},
  {confidential, false}].

mod_opt_type(auth_token) ->
  fun iolist_to_binary/1;
mod_opt_type(post_url) ->
  fun iolist_to_binary/1;
mod_opt_type(confidential) ->
  fun (B) when is_boolean(B) -> B end.

create_message({Action, Packet} = Acc) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
    [{text, _, Body}] = Packet#message.body,
    post_offline_message(Packet#message.from, Packet#message.to, Body, Packet#message.id),
  Acc;

create_message(Acc) ->
  Acc.

create_message(_From, _To, Packet) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
  Body = fxml:get_path_s(Packet, [{elem, list_to_binary("body")}, cdata]),
  MessageId = fxml:get_tag_attr_s(list_to_binary("id"), Packet),
  post_offline_message(_From, _To, Body, MessageId),
  ok.

post_offline_message(From, To, Body, MessageId) ->
  ?INFO_MSG("Posting From ~p To ~p Body ~p ID ~p~n",[From, To, Body, MessageId]),
  Token = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, auth_token),
  PostUrl = gen_mod:get_module_opt(To#jid.lserver, ?MODULE, post_url),
  ToUser = To#jid.luser,
  FromUser = From#jid.luser,
  Vhost = To#jid.lserver,
  case gen_mod:get_module_opt(To#jid.lserver, ?MODULE, confidential) of
    true -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&messageId=", binary_to_list(MessageId)], "");
    false -> Data = string:join(["to=", binary_to_list(ToUser), "&from=", binary_to_list(FromUser), "&vhost=", binary_to_list(Vhost), "&body=", binary_to_list(Body), "&messageId=", binary_to_list(MessageId)], "")
  end,
  Request = {binary_to_list(PostUrl), [{"Authorization", binary_to_list(Token)}, {"Logged-Out", "logged-out"}], "application/x-www-form-urlencoded", Data},
  httpc:request(post, Request,[],[]),
  ?INFO_MSG("post request sent", []).

Have an easier life.