erlangejabberd

Ejabberd custom module


I am trying to create a custom module for my Ejabberd server. I am using relatively newer version so ejabberd.hrl is not available. The module is supposed to do two things:

  1. When user A sends a message to user B a message is sent back to A saying that the message has arrived at the server (sort of like one check mark with Whatsapp) The message has a unique stanza structure which is what my Java client code is expecting in the stanza listener.
  2. If user B is offline then a message is sent to my appserver so a push notification can be generated.

The module kind of works but not really. I found that the acknowledgment message is generated only if B is offline but I need it to be generated no matter what. The first part works just fine. I think the issue is with the message hook. Here is the code:

-module(mod_http_offline).
-author("Allen Turing").

-behaviour(gen_mod).

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

%% Record definitions
-record(jid, {user = <<"">>, server = <<"">>, resource = <<"">>,
              luser = <<"">>, lserver = <<"">>, lresource = <<>>}).

-record(xmlel, {name, attrs = [], children = []}).
-record(xmlcdata, {content}).

%% Start function
start(Host, _Opts) ->
    inets:start(),
    ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, create_message, 50),
    ok.

%% Stop function
stop(Host) ->
    ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, create_message, 50),
    ok.

%% Create message function
create_message({archived, {message, StanzaId, Type, _, FromJID, ToJID, _, _, _, _, _}}) ->
    handle_message(Type, FromJID, ToJID, StanzaId);
create_message({_, {message, StanzaId, Type, _, FromJID, ToJID, _, _, _, _, _}}) ->
    handle_message(Type, FromJID, ToJID, StanzaId);
create_message(_Msg) ->
    ok.

%% Handle message function
handle_message(Type, FromJID, ToJID, StanzaId) ->
    case Type of
        chat ->
            ToLUser = ToJID#jid.luser,
            ToLServer = ToJID#jid.lserver,
            %% send HTTP notification
            post_offline_message(ToLUser, ToLServer),
            %% send acknowledgment
            send_acknowledgment(FromJID, ToJID, StanzaId),
            ok;
        _ ->
            ok
    end.

%% Function to send acknowledgment
send_acknowledgment(FromJID, ToJID, StanzaId) ->
    %% Build the 'arrived' extension element
    ArrivedExtension = #xmlel{
        name = <<"arrived">>,
        attrs = [{<<"xmlns">>, <<"urn:xmpp:ack">>}],
        children = []
    },
    %% Build the message packet
    AckPacket = #xmlel{
        name = <<"message">>,
        attrs = [{<<"to">>, jid_to_binary(FromJID)},
                 {<<"from">>, jid_to_binary(ToJID)},
                 {<<"type">>, <<"chat">>}],
        children = [
            %% Include the stanza ID in the message body
            #xmlel{
                name = <<"body">>,
                attrs = [],
                children = [#xmlcdata{content = StanzaId}]
            },
            %% Add the 'arrived' extension
            ArrivedExtension
        ]
    },
    %% Send acknowledgment
    ejabberd_router:route(ToJID, FromJID, AckPacket),
    ok.

%% Helper function to convert JID to binary
jid_to_binary(#jid{user = User, server = Server, resource = Resource}) ->
    case Resource of
        <<>> -> <<User/binary, "@", Server/binary>>;
        _ -> <<User/binary, "@", Server/binary, "/", Resource/binary>>
    end.

%% function to post offline message via HTTP
post_offline_message(ToLUser, ToLServer) ->
    JID = <<ToLUser/binary, "@", ToLServer/binary>>,
    Data = lists:flatten(io_lib:format("jid=~s", [JID])),
    Headers = [
        {"API-Key", generate_api_key()}
    ],
    case httpc:request(post, {"https://www.myappserver.com/notify", Headers, "application/x-www-form-urlencoded", Data}, [], []) of
        {ok, {{_Version, StatusCode, _ReasonPhrase}, _Headers, _Body}} ->
            ok;
        {error, Reason} ->
            ok
    end.

%% API key generation
generate_api_key() ->
    %% Return a fixed, placeholder API key
    <<"DUMMY_API_KEY_1234567890">>.

%% Optional callbacks for gen_mod behaviour
depends(_Host, _Opts) -> [].

mod_doc() ->
    "mod_http_offline: This module sends HTTP requests for offline messages and sends acknowledgments.".

mod_options(_Host) -> [].

I was thinking about maybe breaking the task into two separate modules one that checks if B is offline and then communicates with the appserver and another to send an acknowledgment message back to A but I would rather keep it all in one module.


Solution

  • offline_message_hook, as the name suggests, is only called when a message is received by an offline user. You need a different hook to handle all incoming messages. Candidates are:

    You can find examples of the hooks in the ejabberd repository (https://github.com/processone/ejabberd) or the modules contrib repository (https://github.com/processone/ejabberd-contrib)

    A bit unrelated to the question, but I also suggest that you don't work directly with xmlel records but rather use the xmpp records, such as #message{}. This also applies to the arguments of your hook handler. If you need to specify custom XML elements outside the XMPP protocol, you can still include them in the sub_els property of #message{}