Ejabberd: error in simple module to handle offline

2019-07-24 15:58发布

问题:

I have an Ejabberd 17.01 installation where I need to push a notification in case a recipient is offline. This seems the be a common task and solutions using a customized Ejabberd module can be found everywhere. However, I just don't get it running. First, here's me script:

    -module(mod_offline_push).
    -behaviour(gen_mod).

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

    -include("ejabberd.hrl").
    -include("logger.hrl").
    -include("jlib.hrl").

    start(Host, _Opts) ->
            ?INFO_MSG("mod_offline_push loading", []),
            ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, push_message, 10),
            ok.

    stop(Host) ->
            ?INFO_MSG("mod_offline_push stopping", []),
            ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, push_message, 10),
            ok.

    push_message(From, To, Packet) ->
            ?INFO_MSG("mod_offline_push -> push_message", [To]),
            Type = fxml:get_tag_attr_s(<<"type">>, Packet), % Supposedly since 16.04
            %Type = xml:get_tag_attr_s(<<"type">>, Packet), % Supposedly since 13.XX
            %Type = xml:get_tag_attr_s("type", Packet),
            %Type = xml:get_tag_attr_s(list_to_binary("type"), Packet),
            ?INFO_MSG("mod_offline_push -> push_message", []),
            ok.

The problem is the line Type = ... line in method push_message; without that line the last info message is logged (so the hook definitely works). When browsing online, I can find all kinds of function calls to extract elements from Packet. As far as I understand it changed over time with new releases. But it's not good, all variants lead in some kind of error. The current way returns:

2017-01-25 20:38:08.701 [error] <0.21678.0>@ejabberd_hooks:run1:332 {function_clause,[{fxml,get_tag_attr_s,[<<"type">>,{message,<<>>,normal,<<>>,{jid,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>},{jid,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>},[],[{text,<<>>,<<"sfsdfsdf">>}],undefined,[],#{}}],[{file,"src/fxml.erl"},{line,169}]},{mod_offline_push,push_message,3,[{file,"mod_offline_push.erl"},{line,33}]},{ejabberd_hooks,safe_apply,3,[{file,"src/ejabberd_hooks.erl"},{line,382}]},{ejabberd_hooks,run1,3,[{file,"src/ejabberd_hooks.erl"},{line,329}]},{ejabberd_sm,route,3,[{file,"src/ejabberd_sm.erl"},{line,126}]},{ejabberd_local,route,3,[{file,"src/ejabberd_local.erl"},{line,110}]},{ejabberd_router,route,3,[{file,"src/ejabberd_router.erl"},{line,87}]},{ejabberd_c2s,check_privacy_route,5,[{file,"src/ejabberd_c2s.erl"},{line,1886}]}]}

running hook: {offline_message_hook,[{jid,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>},{jid,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>},{message,<<>>,normal,<<>>,{jid,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>},{jid,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>},[],[{text,<<>>,<<"sfsdfsdf">>}],undefined,[],#{}}]}

I'm new Ejabberd and Erlang, so I cannot really interpret the error, but the Line 33 as mentioned in {mod_offline_push,push_message,3,[{file,"mod_offline_push.erl"}, {line,33}]} is definitely the line calling get_tag_attr_s.

UPDATE 2017/01/27: Since this cost me a lot of headache -- and I'm still not perfectly happy -- I post here my current working module in the hopes it might help others. My setup is Ejabberd 17.01 running on Ubuntu 16.04. Most stuff I tried and failed with seem to for older versions of Ejabberd:

-module(mod_fcm_fork).
-behaviour(gen_mod).

%% public methods for this module
-export([start/2, stop/1]).
-export([push_notification/3]).

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

%% Copied this record definition from jlib.hrl
%% Including "xmpp_codec.hrl" and "jlib.hrl" resulted in errors ("XYZ already defined")
-record(jid, {user = <<"">> :: binary(),
              server = <<"">> :: binary(),
              resource = <<"">> :: binary(),
              luser = <<"">> :: binary(),
              lserver = <<"">> :: binary(),
              lresource = <<"">> :: binary()}).


start(Host, _Opts) ->
    ?INFO_MSG("mod_fcm_fork loading", []),
    % Providing the most basic API to the clients and servers that are part of the Inets application
    inets:start(),
    % Add hook to handle message to user who are offline
    ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, push_notification, 10),
    ok.


stop(Host) ->
    ?INFO_MSG("mod_fcm_fork stopping", []),
    ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, push_notification, 10),
    ok.


push_notification(From, To, Packet) ->
    % Generate JID of sender and receiver
    FromJid = lists:concat([binary_to_list(From#jid.user), "@", binary_to_list(From#jid.server), "/", binary_to_list(From#jid.resource)]),
    ToJid = lists:concat([binary_to_list(To#jid.user), "@", binary_to_list(To#jid.server), "/", binary_to_list(To#jid.resource)]),
    % Get message body
    MessageBody = Packet#message.body,
    % Check of MessageBody is not empty
    case MessageBody/=[] of
        true ->
            % Get first element (no idea when this list can have more elements)
            [First | _ ] = MessageBody,
            % Get message data and convert to string
            MessageBodyText = binary_to_list(First#text.data),
            send_post_request(FromJid, ToJid, MessageBodyText);
        false ->
            ?INFO_MSG("mod_fcm_fork -> push_notification: MessageBody is empty",[])
    end,    
    ok.


send_post_request(FromJid, ToJid, MessageBodyText) ->
    %?INFO_MSG("mod_fcm_fork -> send_post_request -> MessageBodyText = ~p", [Demo]),    
    Method = post,
    PostURL = gen_mod:get_module_opt(global, ?MODULE, post_url,fun(X) -> X end, all),
    % Add data as query string. Not nice, query body would be preferable
    % Problem: message body itself can be in a JSON string, and I couldn't figure out the correct encoding.
    URL = lists:concat([binary_to_list(PostURL), "?", "fromjid=", FromJid,"&tojid=", ToJid,"&body=", edoc_lib:escape_uri(MessageBodyText)]),   
    Header = [],
    ContentType = "application/json",
    Body = [],
    ?INFO_MSG("mod_fcm_fork -> send_post_request -> URL = ~p", [URL]),    
    % ADD SSL CONFIG BELOW!
    %HTTPOptions = [{ssl,[{versions, ['tlsv1.2']}]}],
    HTTPOptions = [], 
    Options = [],
    httpc:request(Method, {URL, Header, ContentType, Body}, HTTPOptions, Options),
    ok.

回答1:

Actually it fails with second arg Packet you pass to fxml:get_tag_attr_s in push_message function

{message,<<>>,normal,<<>>,
         {jid,<<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>,
              <<"homer">>,<<"xxx.xxx.xxx.xxx">>,<<"conference">>},
         {jid,<<"carl">>,<<"xxx.xxx.xxx.xxx">>,<<>>,<<"carl">>,
              <<"xxx.xxx.xxx.xxx">>,<<>>},
         [],
         [{text,<<>>,<<"sfsdfsdf">>}],
         undefined,[],#{}}

because it is not xmlel

Looks like it is record "message" defined in tools/xmpp_codec.hrl with <<>> id and type 'normal'

xmpp_codec.hrl
    -record(message, {id :: binary(),
                      type = normal :: 'chat' | 'error' | 'groupchat' | 'headline' | 'normal',
                      lang :: binary(),
                      from :: any(),
                      to :: any(),
                      subject = [] :: [#text{}],
                      body = [] :: [#text{}],
                      thread :: binary(),
                      error :: #error{},
                      sub_els = [] :: [any()]}).

Include this file and use just

Type =  Packet#message.type

or, if you expect binary value

 Type =  erlang:atom_to_binary(Packet#message.type, utf8)


回答2:

The newest way to do that seems to be with xmpp:get_type/1:

Type = xmpp:get_type(Packet),

It returns an atom, in this case normal.