./ct_report/coverage/mod_push_service_mongoosepush.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% @author Rafal Slota
3 %%% @copyright (C) 2017 Erlang Solutions Ltd.
4 %%% This software is released under the Apache License, Version 2.0
5 %%% cited in 'LICENSE.txt'.
6 %%% @end
7 %%%-------------------------------------------------------------------
8 %%% @doc
9 %%% Adapter for MongoosePush service.
10 %%% @end
11 %%%-------------------------------------------------------------------
12 -module(mod_push_service_mongoosepush).
13 -author('rafal.slota@erlang-solutions.com').
14 -behavior(gen_mod).
15 -behaviour(mongoose_module_metrics).
16
17 -include("mongoose.hrl").
18 -include("mongoose_config_spec.hrl").
19
20 %%--------------------------------------------------------------------
21 %% Exports
22 %%--------------------------------------------------------------------
23
24 %% gen_mod handlers
25 -export([start/2, stop/1, config_spec/0]).
26
27 %% Hooks and IQ handlers
28 -export([push_notifications/4]).
29
30 -export([http_notification/5]).
31
32 -export([config_metrics/1]).
33
34 -ignore_xref([http_notification/5, push_notifications/4]).
35
36 %%--------------------------------------------------------------------
37 %% Module callbacks
38 %%--------------------------------------------------------------------
39
40 -spec start(Host :: mongooseim:host_type(), Opts :: gen_mod:module_opts()) -> any().
41 start(Host, Opts) ->
42 14 ?LOG_INFO(#{what => push_service_starting, server => Host}),
43 14 start_pool(Host, Opts),
44 %% Hooks
45 14 ejabberd_hooks:add(push_notifications, Host, ?MODULE, push_notifications, 10),
46 14 ok.
47
48 -spec start_pool(mongooseim:host_type(), gen_mod:module_opts()) -> term().
49 start_pool(Host, Opts) ->
50 14 {ok, _} = mongoose_wpool:start(generic, Host, mongoosepush_service, pool_opts(Opts)).
51
52 -spec pool_opts(gen_mod:module_opts()) -> mongoose_wpool:pool_opts().
53 pool_opts(#{max_http_connections := MaxHTTPConnections}) ->
54 14 [{strategy, available_worker},
55 {workers, MaxHTTPConnections}].
56
57 -spec stop(Host :: jid:server()) -> ok.
58 stop(Host) ->
59 14 ejabberd_hooks:delete(push_notifications, Host, ?MODULE, push_notifications, 10),
60 14 mongoose_wpool:stop(generic, Host, mongoosepush_service),
61 14 ok.
62
63 -spec config_spec() -> mongoose_config_spec:config_section().
64 config_spec() ->
65 164 #section{
66 items = #{<<"pool_name">> => #option{type = atom,
67 validate = pool_name},
68 <<"api_version">> => #option{type = binary,
69 validate = {enum, [<<"v2">>, <<"v3">>]}},
70 <<"max_http_connections">> => #option{type = integer,
71 validate = non_negative}
72 },
73 defaults = #{<<"pool_name">> => undefined,
74 <<"api_version">> => <<"v3">>,
75 <<"max_http_connections">> => 100},
76 format_items = map
77 }.
78
79 %%--------------------------------------------------------------------
80 %% Hooks
81 %%--------------------------------------------------------------------
82
83 %% Hook 'push_notifications'
84 -spec push_notifications(AccIn :: ok | mongoose_acc:t(), Host :: jid:server(),
85 Notifications :: [#{binary() => binary()}],
86 Options :: #{binary() => binary()}) ->
87 ok | {error, Reason :: term()}.
88 push_notifications(AccIn, Host, Notifications, Options = #{<<"device_id">> := DeviceId}) ->
89 72 ?LOG_DEBUG(#{what => push_notifications, notifications => Notifications,
90 72 opts => Options, acc => AccIn}),
91
92 72 ProtocolVersion = gen_mod:get_module_opt(Host, ?MODULE, api_version),
93 72 Path = <<ProtocolVersion/binary, "/notification/", DeviceId/binary>>,
94 72 Fun = fun(Notification) ->
95 72 ReqHeaders = [{<<"content-type">>, <<"application/json">>}],
96 72 {ok, JSON} =
97 make_notification(Notification, Options),
98 72 Payload = jiffy:encode(JSON),
99 72 call(Host, ?MODULE, http_notification, [Host, post, Path, ReqHeaders, Payload])
100 end,
101 72 send_push_notifications(Notifications, Fun, ok).
102
103 send_push_notifications([], _, Result) ->
104 70 Result;
105 send_push_notifications([Notification | Notifications], Fun, Result) ->
106 72 case Fun(Notification) of
107 {ok, ok} ->
108 64 send_push_notifications(Notifications, Fun, Result);
109 {ok, {error, device_not_registered} = Err} ->
110 %% In this case there is no point in sending other push notifications
111 %% so we can finish immediately
112 2 Err;
113 {ok, Other} ->
114 %% The publish IQ allows to put more notifications into one request
115 %% but this is currently not used in MongooseIM.
116 %% In case it's used we try sending other notifications
117 6 send_push_notifications(Notifications, Fun, Other)
118 end.
119
120
121 %%--------------------------------------------------------------------
122 %% Module API
123 %%--------------------------------------------------------------------
124
125 -spec http_notification(Host :: jid:server(), post,
126 binary(), proplists:proplist(), binary()) ->
127 ok | {error, Reason :: term()}.
128 http_notification(Host, Method, URL, ReqHeaders, Payload) ->
129 72 PoolName = gen_mod:get_module_opt(Host, ?MODULE, pool_name),
130 72 case mongoose_http_client:Method(Host, PoolName, URL, ReqHeaders, Payload) of
131 {ok, {BinStatusCode, Body}} ->
132 72 case binary_to_integer(BinStatusCode) of
133 StatusCode when StatusCode >= 200 andalso StatusCode < 300 ->
134 64 ok;
135 410 ->
136 2 ?LOG_WARNING(#{what => push_send_failed,
137 text => <<"Unable to send push notification. "
138 "Device not registered.">>,
139
:-(
code => 410, reason => device_not_registered}),
140 2 {error, device_not_registered};
141 StatusCode when StatusCode >= 400 andalso StatusCode < 500 ->
142
:-(
?LOG_ERROR(#{what => push_send_failed,
143 text => <<"Unable to send push notification. "
144 "Possible API mismatch">>,
145 code => StatusCode, url => URL,
146
:-(
response => Body, payload => Payload}),
147
:-(
{error, {invalid_status_code, StatusCode}};
148 StatusCode ->
149 6 ?LOG_ERROR(#{what => push_send_failed,
150 text => <<"Unable to send push notification">>,
151
:-(
code => StatusCode, response => Body}),
152 6 {error, {invalid_status_code, StatusCode}}
153 end;
154 {error, Reason} ->
155
:-(
?LOG_ERROR(#{what => connection_error, reason => Reason}),
156
:-(
{error, Reason}
157 end.
158
159 %%--------------------------------------------------------------------
160 %% Helper functions
161 %%--------------------------------------------------------------------
162
163 %% Create notification for API v2 and v3
164 make_notification(Notification, Options) ->
165 72 RequiredParameters = #{service => maps:get(<<"service">>, Options)},
166 %% The full list of supported optional parameters can be found here:
167 %% https://github.com/esl/MongoosePush/blob/master/README.md#request
168 %%
169 %% Note that <<"tags">> parameter is explicitly excluded to avoid any
170 %% security issues. User should not be allowed to select pools other than
171 %% prod and dev (see <<"mode">> parameter description).
172 72 OptionalKeys = [<<"mode">>, <<"priority">>, <<"topic">>,
173 <<"mutable_content">>, <<"time_to_live">>],
174 72 OptionalParameters = maps:with(OptionalKeys, Options),
175 72 NotificationParams = maps:merge(RequiredParameters, OptionalParameters),
176
177 72 MessageCount = binary_to_integer(maps:get(<<"message-count">>, Notification)),
178
179 72 DataOrAlert = case Options of
180 #{<<"silent">> := <<"true">>} ->
181 17 Data = Notification#{<<"message-count">> := MessageCount},
182 17 #{data => Data};
183 _ ->
184 55 BasicAlert = #{body => maps:get(<<"last-message-body">>, Notification),
185 title => maps:get(<<"last-message-sender">>, Notification),
186 tag => maps:get(<<"last-message-sender">>, Notification),
187 badge => MessageCount},
188 55 OptionalAlert = maps:with([<<"click_action">>, <<"sound">>], Options),
189 55 #{alert => maps:merge(BasicAlert, OptionalAlert)}
190 end,
191 72 {ok, maps:merge(NotificationParams, DataOrAlert)}.
192
193 -spec call(Host :: jid:server(), M :: atom(), F :: atom(), A :: [any()]) -> any().
194 call(Host, M, F, A) ->
195 72 mongoose_wpool:call(generic, Host, mongoosepush_service, {M, F, A}).
196
197 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
198 config_metrics(Host) ->
199
:-(
mongoose_module_metrics:opts_for_module(Host, ?MODULE, [api_version]).
Line Hits Source