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