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