25

I have ejabberd setup to be the xmpp server between mobile apps, ie. custom iPhone and Android app.

But I've seemingly run into a limitation of the way ejabberd handles online status's.

Scenario:

  • User A is messaging User B via their mobiles.
  • User B loses all connectivity, so client can't disconnect from server.
  • ejabberd still lists User B as online.
  • Since ejabberd assumes User B is still online, any message from User A gets passed on to the dead connection.
  • So user B won't get the message, nor does it get saved as an offline message, as ejabberd assumes the user is online.
  • Message lost.
  • Until ejabberd realises that the connection is stale, it treats it as an online user.

And throw in data connection changes (wifi to 3G to 4G to...) and you'll find this happening quite a lot.

mod_ping:

I tried to implement mod_ping on a 10 second interval.
https://www.process-one.net/docs/ejabberd/guide_en.html#modping
But as the documentation states, the ping will wait 32 seconds for a response before disconnecting the user.
This means there will be a 42 second window where the user can lose their messages.

Ideal Solution:

Even if the ping wait time could be reduce, it's still not a perfect solution.
Is there a way that ejabberd can wait for a 200 response from the client before discarding the message? If no response then save it offline.
Is it possible to write a hook to solve this problem?
Or is there a simple setting I've missed somewhere?

FYI: I am not using BOSH.

Johan Vorster
  • 473
  • 6
  • 16
  • Not quite an answer, but may be useful to someone: I've solved a problem by using prosody jabber server that has (community contributed) plugin for XEP-198 (called smacks). [Wikipedia lists several other servers with support for 198](https://en.wikipedia.org/wiki/Comparison_of_XMPP_server_software#Comparison_by_XEP_implementation_status), but prosody was only one in the default Debian repo. On the (Android) client side, I've used Yaxim – Slartibartfast Apr 12 '14 at 06:29

6 Answers6

14

Here is the mod I wrote that fixes my problem.

To make it work you'll need receipts to be activated client side and the client should be able to handle duplicate messages.

Firstly I created a table called confirm_delivery. I save every 'chat' message to that table. I set a 10 second timer, if I receive a confirmation back, I delete the table entry.

If I don't get a confirmation back, I save the message manually to the offline_msg table and try and resend it again (this might be over the top, but for you to decide) and then delete it from our confirm_delivery table

I've chopped out all the code I perceive as unnecessary, so I hope this will still compile.

Hope this is of help to other ejabberd devs out there!

https://github.com/johanvorster/ejabberd_confirm_delivery.git


%% name of module must match file name
-module(mod_confirm_delivery).

-author("Johan Vorster").

%% Every ejabberd module implements the gen_mod behavior
%% The gen_mod behavior requires two functions: start/2 and stop/1
-behaviour(gen_mod).

%% public methods for this module
-export([start/2, stop/1, send_packet/3, receive_packet/4, get_session/5, set_offline_message/5]).

%% included for writing to ejabberd log file
-include("ejabberd.hrl").

-record(session, {sid, usr, us, priority, info}).
-record(offline_msg, {us, timestamp, expire, from, to, packet}).

-record(confirm_delivery, {messageid, timerref}).

start(_Host, _Opt) -> 

        ?INFO_MSG("mod_confirm_delivery loading", []),
        mnesia:create_table(confirm_delivery, 
            [{attributes, record_info(fields, confirm_delivery)}]),
        mnesia:clear_table(confirm_delivery),
        ?INFO_MSG("created timer ref table", []),

        ?INFO_MSG("start user_send_packet hook", []),
        ejabberd_hooks:add(user_send_packet, _Host, ?MODULE, send_packet, 50),   
        ?INFO_MSG("start user_receive_packet hook", []),
        ejabberd_hooks:add(user_receive_packet, _Host, ?MODULE, receive_packet, 50).   

stop(_Host) -> 
        ?INFO_MSG("stopping mod_confirm_delivery", []),
        ejabberd_hooks:delete(user_send_packet, _Host, ?MODULE, send_packet, 50),
        ejabberd_hooks:delete(user_receive_packet, _Host, ?MODULE, receive_packet, 50). 

send_packet(From, To, Packet) ->    
    ?INFO_MSG("send_packet FromJID ~p ToJID ~p Packet ~p~n",[From, To, Packet]),

    Type = xml:get_tag_attr_s("type", Packet),
    ?INFO_MSG("Message Type ~p~n",[Type]),

    Body = xml:get_path_s(Packet, [{elem, "body"}, cdata]), 
    ?INFO_MSG("Message Body ~p~n",[Body]),

    MessageId = xml:get_tag_attr_s("id", Packet),
    ?INFO_MSG("send_packet MessageId ~p~n",[MessageId]), 

    LUser = element(2, To),
    ?INFO_MSG("send_packet LUser ~p~n",[LUser]), 

    LServer = element(3, To), 
    ?INFO_MSG("send_packet LServer ~p~n",[LServer]), 

    Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us),
    ?INFO_MSG("Session: ~p~n",[Sessions]),

    case Type =:= "chat" andalso Body =/= [] andalso Sessions =/= [] of
        true ->                

        {ok, Ref} = timer:apply_after(10000, mod_confirm_delivery, get_session, [LUser, LServer, From, To, Packet]),

        ?INFO_MSG("Saving To ~p Ref ~p~n",[MessageId, Ref]),

        F = fun() ->
            mnesia:write(#confirm_delivery{messageid=MessageId, timerref=Ref})
        end,

        mnesia:transaction(F);

    _ ->
        ok
    end.   

receive_packet(_JID, From, To, Packet) ->
    ?INFO_MSG("receive_packet JID: ~p From: ~p To: ~p Packet: ~p~n",[_JID, From, To, Packet]), 

    Received = xml:get_subtag(Packet, "received"), 
    ?INFO_MSG("receive_packet Received Tag ~p~n",[Received]),    

    if Received =/= false andalso Received =/= [] ->
        MessageId = xml:get_tag_attr_s("id", Received),
        ?INFO_MSG("receive_packet MessageId ~p~n",[MessageId]);       
    true ->
        MessageId = []
    end, 

    if MessageId =/= [] ->
        Record = mnesia:dirty_read(confirm_delivery, MessageId),
        ?INFO_MSG("receive_packet Record: ~p~n",[Record]);       
    true ->
        Record = []
    end, 

    if Record =/= [] ->
        [R] = Record,
        ?INFO_MSG("receive_packet Record Elements ~p~n",[R]), 

        Ref = element(3, R),

        ?INFO_MSG("receive_packet Cancel Timer ~p~n",[Ref]), 
        timer:cancel(Ref),

        mnesia:dirty_delete(confirm_delivery, MessageId),
        ?INFO_MSG("confirm_delivery clean up",[]);     
    true ->
        ok
    end.


get_session(User, Server, From, To, Packet) ->   
    ?INFO_MSG("get_session User: ~p Server: ~p From: ~p To ~p Packet ~p~n",[User, Server, From, To, Packet]),   

    ejabberd_router:route(From, To, Packet),
    ?INFO_MSG("Resend message",[]),

    set_offline_message(User, Server, From, To, Packet),
    ?INFO_MSG("Set offline message",[]),

    MessageId = xml:get_tag_attr_s("id", Packet), 
    ?INFO_MSG("get_session MessageId ~p~n",[MessageId]),    

    case MessageId =/= [] of
        true ->        

        mnesia:dirty_delete(confirm_delivery, MessageId),
        ?INFO_MSG("confirm_delivery clean up",[]);

     _ ->
        ok
    end.

set_offline_message(User, Server, From, To, Packet) ->
    ?INFO_MSG("set_offline_message User: ~p Server: ~p From: ~p To ~p Packet ~p~n",[User, Server, From, To, Packet]),    

    F = fun() ->
        mnesia:write(#offline_msg{us = {User, Server}, timestamp = now(), expire = "never", from = From, to = To, packet = Packet})
    end,

    mnesia:transaction(F).    
Johan Vorster
  • 473
  • 6
  • 16
  • ,thanks for the above code,can you tell me will this module work with ejabberd 2.1.11??and how shoul i compile this module??say how to create ejabberd_confirm_delivery.beam??hope you will replay soon! – Dev Sep 22 '14 at 12:24
  • 1
    Yip, this works on ejabberd 2.1.11. Run erlang shell, point to the dir where the file is saved and use the c(mod_confirm_delivery). command. This should produce a beam file for you. http://www.erlang.org/documentation/doc-5.3/doc/getting_started/getting_started.html – Johan Vorster Sep 23 '14 at 13:40
  • @JohanVorster I tried this code but I am getting errors.http://prntscr.com/5jt2hi how can I resolve them ? – Tolgay Toklar Dec 22 '14 at 19:51
  • @TolgayToklar unfortunately I don't have the erlang/ejabberd environments at my disposal anymore. So it's not possible to check whats happening. Where are you compiling from? Try compiling it from the ebin folder. – Johan Vorster Jan 21 '15 at 12:18
  • It seems ejabberd changed INFO_MSG macro and send_packet arguments from version 13.10 onwards. Include '-include("logger.hrl").' and make sure you compile from source (ebin). Or alternatively delete the INFO_MSG methods – Johan Vorster Feb 13 '15 at 15:03
  • I am new to ejabbered. i am using mod_offline_odbc How to store the offline msgs to odbc instead of mnesia? – Vishnu Pradeep Apr 02 '15 at 12:56
  • @vishnu best to post that as a separate question. Word of warning though, last time I checked ejabberd (process-one) only supported Mnesia DB. That's if you have a support agreement with them. – Johan Vorster Apr 08 '15 at 10:34
  • @JohanVorster I don't know how you interpreted my question, i want to know how the above ejabberd module can be changed to use odbc instead of Mnesia db. – Vishnu Pradeep May 07 '15 at 13:50
  • @JohanVorster This module is great. But i am not sure if this works with Ejabberd 14.07. Because i dont see binarizations anywhere. Could you make changes to it as per the new ejabberd syntax? thanks – bit_by_bit Jun 29 '15 at 05:15
  • @Heisenberg Hi Heisenberg, I actually don't have any ejabberd environments available at my disposal anymore. So I can't really comment on whats happening on 14.07. It might be worth posting a question regarding your problem. Or contacting some of the others on this thread as they might have had the same issue. – Johan Vorster Jun 29 '15 at 08:31
  • Sure. thanks! @JohanVorster would your module work with an architecture of the sort: Client1 -->http request to server carrying message --> server sends xmpp messages (multicast) to members of the group. – bit_by_bit Jun 29 '15 at 11:09
  • @Heisenberg it could. Can't see why not, multicast is just another from of sending a stanza. And this module hooks on too all messages, so in theory, it should work. But this is obviously a hypothetical call, since I've never done it that way. But if it works for you, let me know. – Johan Vorster Jun 29 '15 at 13:04
  • @JohanVorster Sure would check on that. Meantime, about configuring this module of yours. An entry like mod_confirm_delivery: [] in the modules section of the cfg/yml file is sufficient. right? What config did you do if anything different? – bit_by_bit Jun 30 '15 at 05:58
  • @Heisenberg yip, just add the module to the module section of the config file. {mod_confirm_delivery, []}, – Johan Vorster Jun 30 '15 at 10:09
  • Hi @JohanVorster I have been able to bring the module up. But sadly none of my xmpp/http requests reach the hooks you mentioned. It's like the module seems to be not triggering at the moment. I have enabled xep-184 at client end too. Any specific format that this module expects for it to be activated? And where do the output of ?INFO_MSG statements go? There is nothing in ejabberd.log file. – bit_by_bit Jun 30 '15 at 10:23
  • @Heisenberg Is the mod file in the correct folder? You should set the logging to log debug - {loglevel, 5}. Try and get the logging to work first. Thats the reason I have so much logging, it was my way of debugging the code – Johan Vorster Jun 30 '15 at 10:57
  • Works fine with my xmpp messages. Cant get to work with http messages :( – bit_by_bit Jul 02 '15 at 13:24
  • @Heisenberg Great news! I didn't try it with http messages, but maybe it's just a matter of adding another hook to the mod. Or duplicating this mod to handle http messages. – Johan Vorster Jul 06 '15 at 11:45
  • @ankitrana_ how you solved the hooks problem? Seeing the log the module starts fine but when i try to send a message i got a hooks error – MattC Oct 11 '16 at 09:32
  • @JohanVorster do you know if is possible work with your mod on Ejabberd 16.03? I've some problems, please if you can check my post http://stackoverflow.com/questions/40382698/erlang-module-mod-confirm-delivery-doesnt-works-with-ejabberd-16-03/40434668#40434668 – MattC Nov 07 '16 at 08:48
5

This is well known limitation of TCP connections. You need to introduce some acknowledgment functionality.

One of options in xep-0184. A message may carry receipt request and when it is delivered the receipt goes back to sender.

Another option is xep-0198. This is stream management which acknowledges stanzas.

You can also implement it entirely in application layer and send messages from recipient to sender. Act accordingly when acknowledgment is not delivered. Mind that Sender -> Server connection also may be severed in that way.

I am not aware of implementation of those xeps and features in ejabberd. I implemented them on my own depending on project requirements.

user425720
  • 3,578
  • 1
  • 21
  • 23
  • 2
    Unfortunately ejabberd doesn't support xep-0198. We have implement xep-0184, but ejabberd server doesn't actually verify the receipt, it just passes it back to the sender. So no server validation to see if message has been received. I might need to ping the client every time before sending a message too see if they are still connected. This might actually be less overhead than pinging all connected clients every 10 seconds. – Johan Vorster Jul 03 '13 at 13:05
  • Im agree with Johan Vorster – Chathura Wijesinghe Dec 03 '13 at 13:32
2

ejabberd supports stream management as default in latest version. It is implemented in most mobile libraries like Smack for Android and XMPPFramework for iOS.

This is the state of the art in XMPP specification at the moment.

Mickaël Rémond
  • 9,035
  • 1
  • 24
  • 44
1

Implementing XEP-198 on ejabberd is quite involved.

Erlang Solutions (I work for them) has an XEP-184 module for ejabberd, with enhanced functionality, that solves this problem. It does the buffering and validation on the server side. As long as client sends messages carrying receipt request and when it is delivered the receipt goes back to sender.

The module validates receipts to see if message has been received. If it hasn't within timeout, it gets saved as an offline message.

  • We use receipts (XEP-184) to verify that the receiver got the message. But the sender usually are none the wiser if a message has been lost or saved offline. – Johan Vorster Dec 12 '13 at 16:41
  • 1
    I created a mod and hooked up to the send_packet and receive_packet events. Save the message ID to a table. Start a 10 sec waiting thread. If the receive_packet hook gets the message ID back under 10 sec I kill the thread, else I manually store the message in the offline table. Worst case now is, I might have the msg twice in the offline table. But it will have the same ID, our clients know not to duplicate messages. – Johan Vorster Dec 12 '13 at 16:49
  • @JohanVorster Hi Johan, I'm having a similar issue with ejabberd. Would you consider sharing your mod? I'd appreciate it, it would save me a lot of time. Thanks! – Chris McCabe Mar 21 '14 at 15:09
  • @ChrisMcCabe I've added the code in as an answer, see if that works for you. – Johan Vorster Mar 24 '14 at 17:13
1

I think the better way is that if a message has not be received make user offline and then store message in offline message table and use a push service and configure it for offline message.

Then a push will be send and if there are more message they will be stored on offline message, and for understanding on server that message has not received you can use this https://github.com/Mingism/ejabberd-stanza-ack.

I think Facebook has the same way when a message doesn't deliver it makes user offline until he become online again

Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
0

Ejabberd supports stream management as default in latest version. After set stream manager config in ejabberd_c2s, You should set some config in your client. Please see this post for this config in client. https://community.igniterealtime.org/thread/55715

M.Rezaei
  • 992
  • 3
  • 11
  • 28