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