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 |
:-( |
?LOG_INFO(#{what => push_service_starting, server => Host}), |
43 |
:-( |
start_pool(Host, Opts), |
44 |
|
%% Hooks |
45 |
:-( |
gen_hook:add_handlers(hooks(Host)), |
46 |
:-( |
ok. |
47 |
|
|
48 |
|
-spec start_pool(mongooseim:host_type(), gen_mod:module_opts()) -> term(). |
49 |
|
start_pool(Host, Opts) -> |
50 |
:-( |
{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 |
:-( |
[{strategy, available_worker}, |
55 |
|
{workers, MaxHTTPConnections}]. |
56 |
|
|
57 |
|
-spec stop(Host :: jid:server()) -> ok. |
58 |
|
stop(Host) -> |
59 |
:-( |
gen_hook:delete_handlers(hooks(Host)), |
60 |
:-( |
mongoose_wpool:stop(generic, Host, mongoosepush_service), |
61 |
:-( |
ok. |
62 |
|
|
63 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
64 |
|
config_spec() -> |
65 |
186 |
#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 |
:-( |
[{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 |
:-( |
?LOG_DEBUG(#{what => push_notifications, notifications => Notifications, |
95 |
:-( |
opts => Options, acc => AccIn}), |
96 |
|
|
97 |
:-( |
ProtocolVersion = gen_mod:get_module_opt(Host, ?MODULE, api_version), |
98 |
:-( |
Path = <<ProtocolVersion/binary, "/notification/", DeviceId/binary>>, |
99 |
:-( |
Fun = fun(Notification) -> |
100 |
:-( |
ReqHeaders = [{<<"content-type">>, <<"application/json">>}], |
101 |
:-( |
{ok, JSON} = |
102 |
|
make_notification(Notification, Options), |
103 |
:-( |
Payload = jiffy:encode(JSON), |
104 |
:-( |
call(Host, ?MODULE, http_notification, [Host, post, Path, ReqHeaders, Payload]) |
105 |
|
end, |
106 |
:-( |
{ok, send_push_notifications(Notifications, Fun, ok)}. |
107 |
|
|
108 |
|
send_push_notifications([], _, Result) -> |
109 |
:-( |
Result; |
110 |
|
send_push_notifications([Notification | Notifications], Fun, Result) -> |
111 |
:-( |
case Fun(Notification) of |
112 |
|
{ok, ok} -> |
113 |
:-( |
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 |
:-( |
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 |
:-( |
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 |
:-( |
PoolName = gen_mod:get_module_opt(Host, ?MODULE, pool_name), |
135 |
:-( |
case mongoose_http_client:Method(Host, PoolName, URL, ReqHeaders, Payload) of |
136 |
|
{ok, {BinStatusCode, Body}} -> |
137 |
:-( |
case binary_to_integer(BinStatusCode) of |
138 |
|
StatusCode when StatusCode >= 200 andalso StatusCode < 300 -> |
139 |
:-( |
ok; |
140 |
|
410 -> |
141 |
:-( |
?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 |
:-( |
{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 |
:-( |
?LOG_ERROR(#{what => push_send_failed, |
155 |
|
text => <<"Unable to send push notification">>, |
156 |
:-( |
code => StatusCode, response => Body}), |
157 |
:-( |
{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 |
:-( |
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 |
:-( |
OptionalKeys = [<<"mode">>, <<"priority">>, <<"topic">>, |
178 |
|
<<"mutable_content">>, <<"time_to_live">>], |
179 |
:-( |
OptionalParameters = maps:with(OptionalKeys, Options), |
180 |
:-( |
NotificationParams = maps:merge(RequiredParameters, OptionalParameters), |
181 |
|
|
182 |
:-( |
MessageCount = binary_to_integer(maps:get(<<"message-count">>, Notification)), |
183 |
|
|
184 |
:-( |
DataOrAlert = case Options of |
185 |
|
#{<<"silent">> := <<"true">>} -> |
186 |
:-( |
Data = Notification#{<<"message-count">> := MessageCount}, |
187 |
:-( |
#{data => Data}; |
188 |
|
_ -> |
189 |
:-( |
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 |
:-( |
OptionalAlert = maps:with([<<"click_action">>, <<"sound">>], Options), |
194 |
:-( |
#{alert => maps:merge(BasicAlert, OptionalAlert)} |
195 |
|
end, |
196 |
:-( |
{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 |
:-( |
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 |
:-( |
mongoose_module_metrics:opts_for_module(Host, ?MODULE, [api_version]). |