aboutsummaryrefslogtreecommitdiff
path: root/src/mod_applepush_service.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_applepush_service.erl')
-rw-r--r--src/mod_applepush_service.erl704
1 files changed, 704 insertions, 0 deletions
diff --git a/src/mod_applepush_service.erl b/src/mod_applepush_service.erl
new file mode 100644
index 000000000..995447a0d
--- /dev/null
+++ b/src/mod_applepush_service.erl
@@ -0,0 +1,704 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_applepush_service.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : Central push infrastructure
+%%% Created : 5 Jun 2009 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%% ejabberd, Copyright (C) 2002-2009 ProcessOne
+%%%----------------------------------------------------------------------
+
+-module(mod_applepush_service).
+-author('alexey@process-one.net').
+
+-behaviour(gen_server).
+-behaviour(gen_mod).
+
+%% API
+-export([start_link/2, start/2, stop/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+
+-include("ejabberd.hrl").
+-include("jlib.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-record(state, {host = <<"">> :: binary(),
+ socket :: ssl:sslsocket(),
+ gateway = "" :: string(),
+ port = 2195 :: inet:port_number(),
+ feedback_socket :: ssl:sslsocket(),
+ feedback :: string(),
+ feedback_port = 2196 :: inet:port_number(),
+ feedback_buf = <<>> :: binary(),
+ certfile = "" :: string(),
+ certfile_mtime :: file:date_time(),
+ failure_script :: string(),
+ queue = {0, queue:new()} :: {non_neg_integer(), queue()},
+ soundfile = <<"">> :: binary(),
+ cmd_id = 0 :: non_neg_integer(),
+ cmd_cache = dict:new() :: dict(),
+ device_cache = dict:new() :: dict()}).
+
+-define(PROCNAME, ejabberd_mod_applepush_service).
+-define(RECONNECT_TIMEOUT, 5000).
+-define(FEEDBACK_RECONNECT_TIMEOUT, 30000).
+-define(HANDSHAKE_TIMEOUT, 60000).
+-define(SSL_TIMEOUT, 5000).
+-define(MAX_QUEUE_SIZE, 1000).
+-define(CACHE_SIZE, 4096).
+-define(MAX_PAYLOAD_SIZE, 255).
+
+-define(NS_P1_PUSH, <<"p1:push">>).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
+%% Description: Starts the server
+%%--------------------------------------------------------------------
+start_link(Host, Opts) ->
+ Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
+ gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []).
+
+start(Host, Opts) ->
+ ssl:start(),
+ MyHosts = case catch gen_mod:get_opt(
+ hosts, Opts,
+ fun(L) when is_list(L) ->
+ [{iolist_to_binary(H), O} || {H, O}<-L]
+ end, []) of
+ {'EXIT', _} ->
+ [{gen_mod:get_opt_host(Host, Opts,
+ <<"applepush.@HOST@">>), Opts}];
+ Hs ->
+ Hs
+ end,
+ lists:foreach(
+ fun({MyHost, MyOpts}) ->
+ Proc = gen_mod:get_module_proc(MyHost, ?PROCNAME),
+ ChildSpec =
+ {Proc,
+ {?MODULE, start_link, [MyHost, MyOpts]},
+ transient,
+ 1000,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, ChildSpec)
+ end, MyHosts).
+
+stop(Host) ->
+ MyHosts = case gen_mod:get_module_opt(
+ Host, ?MODULE, hosts,
+ fun(Hs) when is_list(Hs) ->
+ [iolist_to_binary(H) || {H, _} <- Hs]
+ end, []) of
+ [] ->
+ [gen_mod:get_module_opt_host(
+ Host, ?MODULE, <<"applepush.@HOST@">>)];
+ Hs ->
+ Hs
+ end,
+ lists:foreach(
+ fun(MyHost) ->
+ Proc = gen_mod:get_module_proc(MyHost, ?PROCNAME),
+ gen_server:call(Proc, stop),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc)
+ end, MyHosts).
+
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% Description: Initiates the server
+%%--------------------------------------------------------------------
+init([MyHost, Opts]) ->
+ CertFile = gen_mod:get_opt(certfile, Opts,
+ fun iolist_to_string/1,
+ ""),
+ SoundFile = gen_mod:get_opt(sound_file, Opts,
+ fun iolist_to_binary/1,
+ <<"pushalert.wav">>),
+ Gateway = gen_mod:get_opt(gateway, Opts,
+ fun iolist_to_string/1,
+ "gateway.push.apple.com"),
+ Feedback = gen_mod:get_opt(feedback, Opts,
+ fun iolist_to_string/1,
+ undefined),
+ Port = gen_mod:get_opt(port, Opts,
+ fun(I) when is_integer(I), I>0, I<65536 -> I end,
+ 2195),
+ FeedbackPort = gen_mod:get_opt(feedback_port, Opts,
+ fun(I) when is_integer(I), I>0, I<65536 -> I end,
+ 2196),
+ FailureScript = gen_mod:get_opt(failure_script, Opts,
+ fun iolist_to_string/1,
+ undefined),
+ self() ! connect,
+ case Feedback of
+ undefined ->
+ ok;
+ _ ->
+ self() ! connect_feedback
+ end,
+ ejabberd_router:register_route(MyHost),
+ {ok, #state{host = MyHost,
+ gateway = Gateway,
+ port = Port,
+ feedback = Feedback,
+ feedback_port = FeedbackPort,
+ certfile = CertFile,
+ failure_script = FailureScript,
+ queue = {0, queue:new()},
+ soundfile = SoundFile}}.
+
+%%--------------------------------------------------------------------
+%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} |
+%% {stop, Reason, State}
+%% Description: Handling call messages
+%%--------------------------------------------------------------------
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_cast(Msg, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling cast messages
+%%--------------------------------------------------------------------
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% Function: handle_info(Info, State) -> {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State}
+%% Description: Handling all non call/cast messages
+%%--------------------------------------------------------------------
+handle_info({route, From, To, Packet}, State) ->
+ case catch do_route(From, To, Packet, State) of
+ {'EXIT', Reason} ->
+ ?ERROR_MSG("~p", [Reason]),
+ {noreply, State};
+ Res ->
+ Res
+ end;
+handle_info(connect, State) ->
+ connect(State);
+handle_info(connect_feedback, #state{certfile_mtime = MTime} = State)
+ when MTime /= undefined ->
+ erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
+ connect_feedback),
+ {noreply, State};
+handle_info(connect_feedback, State)
+ when State#state.feedback /= undefined,
+ State#state.feedback_socket == undefined ->
+ Feedback = State#state.feedback,
+ FeedbackPort = State#state.feedback_port,
+ CertFile = State#state.certfile,
+ case ssl:connect(Feedback, FeedbackPort,
+ [{certfile, CertFile},
+ {active, true},
+ binary], ?SSL_TIMEOUT) of
+ {ok, Socket} ->
+ ?INFO_MSG("(~p) Connected to ~p:~p",
+ [State#state.host, Feedback, FeedbackPort]),
+ {noreply, State#state{feedback_socket = Socket}};
+ {error, Reason} ->
+ ?ERROR_MSG("(~p) Connection to ~p:~p failed: ~p, "
+ "retrying after ~p seconds",
+ [State#state.host, Feedback, FeedbackPort,
+ Reason, ?FEEDBACK_RECONNECT_TIMEOUT div 1000]),
+ erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
+ connect_feedback),
+ {noreply, State}
+ end;
+handle_info({ssl, Socket, Packet}, State)
+ when Socket == State#state.socket ->
+ case Packet of
+ <<8, Status, CmdID:32>> when Status /= 0 ->
+ case dict:find(CmdID, State#state.cmd_cache) of
+ {ok, {JID, _DeviceID}} ->
+ ?ERROR_MSG("PUSH ERROR for ~p: ~p", [JID, Status]),
+ if
+ Status == 8 ->
+ From = jlib:make_jid(<<"">>, State#state.host, <<"">>),
+ ejabberd_router:route(
+ From, JID,
+ #xmlel{name = <<"message">>, attrs = [],
+ children =
+ [#xmlel{name = <<"disable">>,
+ attrs = [{<<"xmlns">>, ?NS_P1_PUSH},
+ {<<"status">>,
+ jlib:integer_to_binary(Status)}],
+ children = []}]});
+ true ->
+ ok
+ end,
+ ok;
+ error ->
+ ?ERROR_MSG("Unknown cmd ID ~p~n", [CmdID]),
+ ok
+ end;
+ _ ->
+ ?ERROR_MSG("Received unknown packet ~p~n", [Packet])
+ end,
+ {noreply, State};
+handle_info({ssl, Socket, Packet}, State)
+ when Socket == State#state.feedback_socket ->
+ ?INFO_MSG("(~p) feedback: ~p", [State#state.host, Packet]),
+ Buf = <<(State#state.feedback_buf)/binary, Packet/binary>>,
+ Buf2 = parse_feedback_buf(Buf, State),
+ {noreply, State#state{feedback_buf = Buf2}};
+handle_info({ssl_closed, Socket}, State)
+ when Socket == State#state.feedback_socket ->
+ ssl:close(Socket),
+ erlang:send_after(?FEEDBACK_RECONNECT_TIMEOUT, self(),
+ connect_feedback),
+ {noreply, State#state{feedback_socket = undefined,
+ feedback_buf = <<>>}};
+handle_info(_Info, State) ->
+ %io:format("got info: ~p~n", [_Info]),
+ {noreply, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description: This function is called by a gen_server when it is about to
+%% terminate. It should be the opposite of Module:init/1 and do any necessary
+%% cleaning up. When it returns, the gen_server terminates with Reason.
+%% The return value is ignored.
+%%--------------------------------------------------------------------
+terminate(_Reason, State) ->
+ ejabberd_router:unregister_route(State#state.host),
+ ok.
+
+%%--------------------------------------------------------------------
+%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%%----------------------------------------------------------------------
+%%% Internal functions
+%%%----------------------------------------------------------------------
+
+do_route(From, To, Packet, State) ->
+ #jid{user = User, resource = Resource} = To,
+ if
+ (User /= <<"">>) or (Resource /= <<"">>) ->
+ Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE),
+ ejabberd_router:route(To, From, Err),
+ {noreply, State};
+ true ->
+ case Packet of
+ #xmlel{name = <<"iq">>} ->
+ IQ = jlib:iq_query_info(Packet),
+ case IQ of
+ #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS,
+ sub_el = _SubEl, lang = Lang} = IQ ->
+ Res = IQ#iq{type = result,
+ sub_el =
+ [#xmlel{name = <<"query">>,
+ attrs = [{<<"xmlns">>, XMLNS}],
+ children = iq_disco(Lang)}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res)),
+ {noreply, State};
+ #iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS} = IQ ->
+ Res = IQ#iq{type = result,
+ sub_el =
+ [#xmlel{name = <<"query">>,
+ attrs = [{<<"xmlns">>, XMLNS}],
+ children = []}]},
+ ejabberd_router:route(To,
+ From,
+ jlib:iq_to_xml(Res)),
+ {noreply, State};
+ _ ->
+ Err = jlib:make_error_reply(Packet,
+ ?ERR_SERVICE_UNAVAILABLE),
+ ejabberd_router:route(To, From, Err),
+ {noreply, State}
+ end;
+ #xmlel{name = <<"message">>, children = Els} ->
+ case xml:remove_cdata(Els) of
+ [#xmlel{name = <<"push">>}] ->
+ NewState = handle_message(From, To, Packet, State),
+ {noreply, NewState};
+ [#xmlel{name = <<"disable">>}] ->
+ {noreply, State};
+ _ ->
+ {noreply, State}
+ end;
+ _ ->
+ {noreply, State}
+ end
+ end.
+
+get_custom_fields(Packet) ->
+ case xml:get_subtag(xml:get_subtag(Packet, <<"push">>), <<"custom">>) of
+ false -> [];
+ #xmlel{name = <<"custom">>, attrs = [], children = Children} ->
+ [ {xml:get_tag_attr_s(<<"name">>, C), xml:get_tag_cdata(C)} ||
+ C <- xml:remove_cdata(Children) ]
+ end.
+
+handle_message(From, To, Packet, #state{socket = undefined} = State) ->
+ queue_message(From, To, Packet, State);
+handle_message(From, To, Packet, State) ->
+ DeviceID =
+ xml:get_path_s(Packet,
+ [{elem, <<"push">>}, {elem, <<"id">>}, cdata]),
+ Msg =
+ xml:get_path_s(Packet,
+ [{elem, <<"push">>}, {elem, <<"msg">>}, cdata]),
+ Badge =
+ xml:get_path_s(Packet,
+ [{elem, <<"push">>}, {elem, <<"badge">>}, cdata]),
+ Sound =
+ xml:get_path_s(Packet,
+ [{elem, <<"push">>}, {elem, <<"sound">>}, cdata]),
+ Sender =
+ xml:get_path_s(Packet,
+ [{elem, <<"push">>}, {elem, <<"from">>}, cdata]),
+ Receiver =
+ xml:get_path_s(Packet,
+ [{elem, <<"push">>}, {elem, <<"to">>}, cdata]),
+ CustomFields = get_custom_fields(Packet),
+ Payload = make_payload(State, Msg, Badge, Sound, Sender, CustomFields),
+ ID =
+ case catch erlang:list_to_integer(binary_to_list(DeviceID), 16) of
+ ID1 when is_integer(ID1) ->
+ ID1;
+ _ ->
+ false
+ end,
+ if
+ is_integer(ID) ->
+ Command = 1,
+ CmdID = State#state.cmd_id,
+ {MegaSecs, Secs, _MicroSecs} = now(),
+ Expiry = MegaSecs * 1000000 + Secs + 24 * 60 * 60,
+ BDeviceID = <<ID:256>>,
+ IDLen = size(BDeviceID),
+ PayloadLen = size(Payload),
+ Notification =
+ <<Command:8,
+ CmdID:32,
+ Expiry:32,
+ IDLen:16,
+ BDeviceID/binary,
+ PayloadLen:16,
+ Payload/binary>>,
+ ?INFO_MSG("(~p) sending notification for ~s~n~p~npayload:~n~s~n"
+ "Sender: ~s~n"
+ "Receiver: ~s~n"
+ "Device ID: ~s~n",
+ [State#state.host, erlang:integer_to_list(ID, 16),
+ Notification, Payload,
+ Sender,
+ Receiver, DeviceID]),
+ case ssl:send(State#state.socket, Notification) of
+ ok ->
+ cache(From, ID, State);
+ {error, Reason} ->
+ ?INFO_MSG("(~p) Connection closed: ~p, reconnecting",
+ [State#state.host, Reason]),
+ ssl:close(State#state.socket),
+ self() ! connect,
+ queue_message(From, To, Packet,
+ State#state{socket = undefined})
+ end;
+ true ->
+ State
+ end.
+
+make_payload(State, Msg, Badge, Sound, Sender, CustomFields) ->
+ Msg2 = json_escape(Msg),
+ AlertPayload =
+ case Msg2 of
+ <<"">> -> <<"">>;
+ _ -> <<"\"alert\":\"", Msg2/binary, "\"">>
+ end,
+ BadgePayload =
+ case catch jlib:binary_to_integer(Badge) of
+ B when is_integer(B) ->
+ <<"\"badge\":", Badge/binary>>;
+ _ -> <<"">>
+ end,
+ SoundPayload =
+ case Sound of
+ <<"true">> ->
+ SoundFile = State#state.soundfile,
+ <<"\"sound\":\"", (json_escape(SoundFile))/binary, "\"">>;
+ _ -> <<"">>
+ end,
+ Payloads = lists:filter(
+ fun(S) -> S /= <<"">> end,
+ [AlertPayload, BadgePayload, SoundPayload]),
+
+ CustomPayloadFields =
+ [<<"\"", (json_escape(Name))/binary, "\":\"",
+ (json_escape(Value))/binary, "\"">>
+ || {Name, Value} <- CustomFields] ++
+ [<<"\"from\":\"", (json_escape(Sender))/binary, "\"">> || Sender /= <<"">>],
+
+ Payload1 =
+ [<<"{">>,
+ str:join(
+ [<<"\"aps\":{", (str:join(Payloads, <<",">>))/binary, "}">> | CustomPayloadFields],
+ <<",">>),
+ <<"}">>],
+ Payload = list_to_binary(Payload1),
+ PayloadLen = size(Payload),
+ if
+ PayloadLen > ?MAX_PAYLOAD_SIZE ->
+ Delta = PayloadLen - ?MAX_PAYLOAD_SIZE,
+ MsgLen = size(Msg),
+ if
+ MsgLen /= 0 ->
+ CutMsg =
+ if
+ MsgLen > Delta ->
+ {CMsg, _} = split_binary(Msg, MsgLen - Delta),
+ CMsg;
+ true ->
+ <<"">>
+ end,
+ make_payload(State, CutMsg, Badge, Sound, Sender, CustomFields);
+ true ->
+ Payload2 =
+ <<"{\"aps\":{", (str:join(Payloads, <<",">>))/binary, "}}">>,
+ Payload2
+ end;
+ true ->
+ Payload
+ end.
+
+connect(#state{socket = undefined, certfile_mtime = undefined} = State) ->
+ Gateway = State#state.gateway,
+ Port = State#state.port,
+ CertFile = State#state.certfile,
+ case ssl:connect(Gateway, Port, [{certfile, CertFile},
+ {active, true},
+ binary],
+ ?SSL_TIMEOUT) of
+ {ok, Socket} ->
+ {noreply, resend_messages(State#state{socket = Socket})};
+ {error, Reason} ->
+ {Timeout, State2} =
+ case Reason of
+ esslconnect ->
+ MTime = get_mtime(CertFile),
+ case State#state.failure_script of
+ undefined ->
+ ok;
+ FailureScript ->
+ os:cmd(FailureScript ++ " " ++ Gateway)
+ end,
+ {?HANDSHAKE_TIMEOUT,
+ State#state{certfile_mtime = MTime}};
+ _ ->
+ {?RECONNECT_TIMEOUT, State}
+ end,
+ ?ERROR_MSG("(~p) Connection to ~p:~p failed: ~p, "
+ "retrying after ~p seconds",
+ [State2#state.host, Gateway, Port,
+ Reason, Timeout div 1000]),
+ erlang:send_after(Timeout, self(), connect),
+ {noreply, State2}
+ end;
+connect(#state{socket = undefined, certfile_mtime = MTime} = State) ->
+ CertFile = State#state.certfile,
+ case get_mtime(CertFile) of
+ MTime ->
+ Gateway = State#state.gateway,
+ Port = State#state.port,
+ Timeout = ?HANDSHAKE_TIMEOUT,
+ ?ERROR_MSG("(~p) Connection to ~p:~p postponed: "
+ "waiting for ~p update, "
+ "retrying after ~p seconds",
+ [State#state.host, Gateway, Port,
+ CertFile, Timeout div 1000]),
+ erlang:send_after(Timeout, self(), connect),
+ {noreply, State};
+ _ ->
+ connect(State#state{certfile_mtime = undefined})
+ end;
+connect(State) ->
+ {noreply, State}.
+
+get_mtime(File) ->
+ case file:read_file_info(File) of
+ {ok, FileInfo} ->
+ FileInfo#file_info.mtime;
+ {error, _} ->
+ no_certfile
+ end.
+
+bounce_message(From, To, Packet, Reason) ->
+ #xmlel{attrs = Attrs} = Packet,
+ Type = xml:get_attr_s(<<"type">>, Attrs),
+ if Type /= <<"error">>; Type /= <<"result">> ->
+ ejabberd_router:route(
+ To, From,
+ jlib:make_error_reply(
+ Packet,
+ ?ERRT_INTERNAL_SERVER_ERROR(
+ xml:get_attr_s(<<"xml:lang">>, Attrs),
+ Reason)));
+ true ->
+ ok
+ end.
+
+queue_message(From, To, Packet, State) ->
+ case State#state.queue of
+ {?MAX_QUEUE_SIZE, Queue} ->
+ {{value, {From1, To1, Packet1}}, Queue1} = queue:out(Queue),
+ bounce_message(From1, To1, Packet1,
+ <<"Unable to connect to push service">>),
+ Queue2 = queue:in({From, To, Packet}, Queue1),
+ State#state{queue = {?MAX_QUEUE_SIZE, Queue2}};
+ {Size, Queue} ->
+ Queue1 = queue:in({From, To, Packet}, Queue),
+ State#state{queue = {Size+1, Queue1}}
+ end.
+
+resend_messages(#state{queue = {_, Queue}} = State) ->
+ lists:foldl(
+ fun({From, To, Packet}, AccState) ->
+ case catch handle_message(From, To, Packet, AccState) of
+ {'EXIT', _} = Err ->
+ ?ERROR_MSG("error while processing message:~n"
+ "** From: ~p~n"
+ "** To: ~p~n"
+ "** Packet: ~p~n"
+ "** Reason: ~p",
+ [From, To, Packet, Err]),
+ AccState;
+ NewAccState ->
+ NewAccState
+ end
+ end, State#state{queue = {0, queue:new()}}, queue:to_list(Queue)).
+
+cache(JID, DeviceID, State) ->
+ CmdID = State#state.cmd_id,
+ Key = CmdID rem ?CACHE_SIZE,
+ C1 = State#state.cmd_cache,
+ D1 = State#state.device_cache,
+ D2 = case dict:find(Key, C1) of
+ {ok, {_, OldDeviceID}} ->
+ del_device_cache(D1, OldDeviceID);
+ error ->
+ D1
+ end,
+ D3 = add_device_cache(D2, DeviceID, JID),
+ C2 = dict:store(Key, {JID, DeviceID}, C1),
+ State#state{cmd_id = CmdID + 1,
+ cmd_cache = C2,
+ device_cache = D3}.
+
+add_device_cache(DeviceCache, DeviceID, JID) ->
+ dict:update(
+ DeviceID,
+ fun({Counter, _}) -> {Counter + 1, JID} end,
+ {1, JID},
+ DeviceCache).
+
+del_device_cache(DeviceCache, DeviceID) ->
+ case dict:find(DeviceID, DeviceCache) of
+ {ok, {Counter, JID}} ->
+ case Counter of
+ 1 ->
+ dict:erase(DeviceID, DeviceCache);
+ _ ->
+ dict:store(DeviceID, {Counter - 1, JID}, DeviceCache)
+ end;
+ error ->
+ DeviceCache
+ end.
+
+json_escape(S) ->
+ json_escape(S, <<>>).
+
+json_escape(<<>>, Res) ->
+ Res;
+json_escape(<<C, S/binary>>, Res) ->
+ case C of
+ $" -> json_escape(S, <<Res/binary, "\\\"">>);
+ $\\ -> json_escape(S, <<Res/binary, "\\\\">>);
+ _ when C < 16 ->
+ B = list_to_binary(erlang:integer_to_list(C, 16)),
+ json_escape(S, <<Res/binary, "\\u000", B/binary>>);
+ _ when C < 32 ->
+ B = list_to_binary(erlang:integer_to_list(C, 16)),
+ json_escape(S, <<Res/binary, "\\u00", B/binary>>);
+ _ -> json_escape(S, <<Res/binary, C>>)
+ end.
+
+
+iq_disco(Lang) ->
+ [#xmlel{name = <<"identity">>,
+ attrs = [{<<"category">>, <<"gateway">>},
+ {<<"type">>, <<"apple">>},
+ {<<"name">>, translate:translate(
+ Lang, <<"Apple Push Service">>)}],
+ children = []},
+ #xmlel{name = <<"feature">>,
+ attrs = [{<<"var">>, ?NS_DISCO_INFO}],
+ children = []}].
+
+
+parse_feedback_buf(Buf, State) ->
+ case Buf of
+ <<TimeStamp:32, IDLen:16, BDeviceID:IDLen/binary, Rest/binary>> ->
+ IDLen8 = IDLen * 8,
+ <<DeviceID:IDLen8>> = BDeviceID,
+ ?INFO_MSG("(~p) received feedback for ~s~n",
+ [State#state.host, erlang:integer_to_list(DeviceID, 16)]),
+ case dict:find(DeviceID, State#state.device_cache) of
+ {ok, {_Counter, JID}} ->
+ ?INFO_MSG("(~p) sending feedback for ~s to ~s~n",
+ [State#state.host,
+ erlang:integer_to_list(DeviceID, 16),
+ jlib:jid_to_string(JID)]),
+ From = jlib:make_jid(<<"">>, State#state.host, <<"">>),
+ ejabberd_router:route(
+ From, JID,
+ #xmlel{name = <<"iq">>,
+ attrs = [{<<"id">>, <<"disable">>},
+ {<<"type">>, <<"set">>}],
+ children =
+ [#xmlel{name = <<"disable">>,
+ attrs =
+ [{<<"xmlns">>, ?NS_P1_PUSH},
+ {<<"status">>, <<"feedback">>},
+ {<<"ts">>, jlib:integer_to_binary(TimeStamp)},
+ {<<"id">>, jlib:integer_to_binary(DeviceID, 16)}],
+ children = []}]});
+ error ->
+ ok
+ end,
+ parse_feedback_buf(Rest, State);
+ _ ->
+ Buf
+ end.
+
+iolist_to_string(S) ->
+ binary_to_list(iolist_to_binary(S)).