Search code examples
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:

    • user_send_packet - if you want to send the acknowledgement as soon as possible. It will work in your case but if you have something on top that would filter unwanted messages, it could send the acknowledgment too soon.
    • filter_packet - This is called a bit later and again lets you drop unwanted messages. At this time, mod_mam has archived the message, if enabled.
    • user_receive_packet - This is called when the message is routed to the receiver, before any offline logic is called. Could be a good place since you know that for sure, the message has passed through everything on the sender side.
    • store_mam_message - Hook, coming from mod_mam, when a message is being archived. It's possible to put an acknowledgement here although I'm not sure it's appropriate.

    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{}