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 |
|
%%% Implementation of XEP-0357 |
10 |
|
%%% @end |
11 |
|
%%%------------------------------------------------------------------- |
12 |
|
-module(mod_event_pusher_push). |
13 |
|
-author('rafal.slota@erlang-solutions.com'). |
14 |
|
-behavior(gen_mod). |
15 |
|
-behavior(mod_event_pusher). |
16 |
|
-behaviour(mongoose_module_metrics). |
17 |
|
-xep([{xep, 357}, {version, "0.2.1"}]). |
18 |
|
|
19 |
|
-include("mod_event_pusher_events.hrl"). |
20 |
|
-include("mongoose.hrl"). |
21 |
|
-include("jlib.hrl"). |
22 |
|
-include("mongoose_config_spec.hrl"). |
23 |
|
|
24 |
|
-define(SESSION_KEY, publish_service). |
25 |
|
|
26 |
|
%%-------------------------------------------------------------------- |
27 |
|
%% Exports |
28 |
|
%%-------------------------------------------------------------------- |
29 |
|
|
30 |
|
%% gen_mod behaviour |
31 |
|
-export([start/2, stop/1, config_spec/0]). |
32 |
|
|
33 |
|
%% mongoose_module_metrics behaviour |
34 |
|
-export([config_metrics/1]). |
35 |
|
|
36 |
|
%% mod_event_pusher behaviour |
37 |
|
-export([push_event/2]). |
38 |
|
|
39 |
|
%% Hooks and IQ handlers |
40 |
|
-export([iq_handler/4, |
41 |
|
remove_user/3]). |
42 |
|
|
43 |
|
%% Plugin utils |
44 |
|
-export([cast/3]). |
45 |
|
-export([is_virtual_pubsub_host/3]). |
46 |
|
-export([disable_node/4]). |
47 |
|
|
48 |
|
-ignore_xref([ |
49 |
|
iq_handler/4, remove_user/3 |
50 |
|
]). |
51 |
|
|
52 |
|
%% Types |
53 |
|
-type publish_service() :: {PubSub :: jid:jid(), Node :: pubsub_node(), Form :: form()}. |
54 |
|
-type pubsub_node() :: binary(). |
55 |
|
-type form_field() :: {Name :: binary(), Value :: binary()}. |
56 |
|
-type form() :: [form_field()]. |
57 |
|
|
58 |
|
-export_type([pubsub_node/0, form_field/0, form/0]). |
59 |
|
-export_type([publish_service/0]). |
60 |
|
|
61 |
|
%%-------------------------------------------------------------------- |
62 |
|
%% gen_mod callbacks |
63 |
|
%%-------------------------------------------------------------------- |
64 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any(). |
65 |
|
start(HostType, Opts) -> |
66 |
:-( |
?LOG_INFO(#{what => event_pusher_starting, host_type => HostType}), |
67 |
:-( |
start_pool(HostType, Opts), |
68 |
:-( |
mod_event_pusher_push_backend:init(HostType, Opts), |
69 |
:-( |
mod_event_pusher_push_plugin:init(HostType, Opts), |
70 |
:-( |
init_iq_handlers(HostType, Opts), |
71 |
:-( |
ejabberd_hooks:add(remove_user, HostType, ?MODULE, remove_user, 90), |
72 |
:-( |
ok. |
73 |
|
|
74 |
|
start_pool(HostType, #{wpool := WpoolOpts}) -> |
75 |
:-( |
{ok, _} = mongoose_wpool:start(generic, HostType, pusher_push, maps:to_list(WpoolOpts)). |
76 |
|
|
77 |
|
init_iq_handlers(HostType, #{iqdisc := IQDisc}) -> |
78 |
:-( |
gen_iq_handler:add_iq_handler(ejabberd_local, HostType, ?NS_PUSH, ?MODULE, |
79 |
|
iq_handler, IQDisc), |
80 |
:-( |
gen_iq_handler:add_iq_handler(ejabberd_sm, HostType, ?NS_PUSH, ?MODULE, |
81 |
|
iq_handler, IQDisc). |
82 |
|
|
83 |
|
-spec stop(mongooseim:host_type()) -> ok. |
84 |
|
stop(HostType) -> |
85 |
:-( |
ejabberd_hooks:delete(remove_user, HostType, ?MODULE, remove_user, 90), |
86 |
|
|
87 |
:-( |
gen_iq_handler:remove_iq_handler(ejabberd_sm, HostType, ?NS_PUSH), |
88 |
:-( |
gen_iq_handler:remove_iq_handler(ejabberd_local, HostType, ?NS_PUSH), |
89 |
|
|
90 |
:-( |
mongoose_wpool:stop(generic, HostType, pusher_push), |
91 |
:-( |
ok. |
92 |
|
|
93 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
94 |
|
config_spec() -> |
95 |
152 |
VirtPubSubHost = #option{type = string, validate = subdomain_template, |
96 |
|
process = fun mongoose_subdomain_utils:make_subdomain_pattern/1}, |
97 |
152 |
#section{ |
98 |
|
items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(), |
99 |
|
<<"backend">> => #option{type = atom, validate = {module, ?MODULE}}, |
100 |
|
<<"wpool">> => wpool_spec(), |
101 |
|
<<"plugin_module">> => #option{type = atom, validate = module}, |
102 |
|
<<"virtual_pubsub_hosts">> => #list{items = VirtPubSubHost}}, |
103 |
|
defaults = #{<<"iqdisc">> => one_queue, |
104 |
|
<<"backend">> => mnesia, |
105 |
|
<<"plugin_module">> => mod_event_pusher_push_plugin:default_plugin_module(), |
106 |
|
<<"virtual_pubsub_hosts">> => []}, |
107 |
|
format_items = map |
108 |
|
}. |
109 |
|
|
110 |
|
wpool_spec() -> |
111 |
152 |
WpoolDefaults = mongoose_config_spec:wpool_defaults(), |
112 |
152 |
#section{items = mongoose_config_spec:wpool_items(), |
113 |
|
defaults = WpoolDefaults#{<<"strategy">> := available_worker}, |
114 |
|
format_items = map, |
115 |
|
include = always}. |
116 |
|
|
117 |
|
%%-------------------------------------------------------------------- |
118 |
|
%% mod_event_pusher callbacks |
119 |
|
%%-------------------------------------------------------------------- |
120 |
|
-spec push_event(mongoose_acc:t(), mod_event_pusher:event()) -> mongoose_acc:t(). |
121 |
|
push_event(Acc, Event = #chat_event{direction = out, to = To, type = Type}) |
122 |
|
when Type =:= groupchat; |
123 |
|
Type =:= chat -> |
124 |
:-( |
BareRecipient = jid:to_bare(To), |
125 |
:-( |
do_push_event(Acc, Event, BareRecipient); |
126 |
|
push_event(Acc, Event = #unack_msg_event{to = To}) -> |
127 |
:-( |
BareRecipient = jid:to_bare(To), |
128 |
:-( |
do_push_event(Acc, Event, BareRecipient); |
129 |
|
push_event(Acc, _) -> |
130 |
:-( |
Acc. |
131 |
|
|
132 |
|
%%-------------------------------------------------------------------- |
133 |
|
%% Hooks and IQ handlers |
134 |
|
%%-------------------------------------------------------------------- |
135 |
|
-spec remove_user(Acc :: mongoose_acc:t(), LUser :: jid:luser(), LServer :: jid:lserver()) -> |
136 |
|
mongoose_acc:t(). |
137 |
|
remove_user(Acc, LUser, LServer) -> |
138 |
:-( |
R = mod_event_pusher_push_backend:disable(LServer, jid:make_noprep(LUser, LServer, <<>>)), |
139 |
:-( |
mongoose_lib:log_if_backend_error(R, ?MODULE, ?LINE, {Acc, LUser, LServer}), |
140 |
:-( |
Acc. |
141 |
|
|
142 |
|
-spec iq_handler(From :: jid:jid(), To :: jid:jid(), Acc :: mongoose_acc:t(), |
143 |
|
IQ :: jlib:iq()) -> |
144 |
|
{mongoose_acc:t(), jlib:iq() | ignore}. |
145 |
|
iq_handler(_From, _To, Acc, IQ = #iq{type = get, sub_el = SubEl}) -> |
146 |
:-( |
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}}; |
147 |
|
iq_handler(From, _To, Acc, IQ = #iq{type = set, sub_el = Request}) -> |
148 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
149 |
:-( |
Res = case parse_request(Request) of |
150 |
|
{enable, BarePubSubJID, Node, FormFields} -> |
151 |
:-( |
ok = enable_node(HostType, From, BarePubSubJID, Node, FormFields), |
152 |
:-( |
store_session_info(From, {BarePubSubJID, Node, FormFields}), |
153 |
:-( |
IQ#iq{type = result, sub_el = []}; |
154 |
|
{disable, BarePubsubJID, Node} -> |
155 |
:-( |
ok = disable_node(HostType, From, BarePubsubJID, Node), |
156 |
:-( |
IQ#iq{type = result, sub_el = []}; |
157 |
|
bad_request -> |
158 |
:-( |
IQ#iq{type = error, sub_el = [Request, mongoose_xmpp_errors:bad_request()]} |
159 |
|
end, |
160 |
:-( |
{Acc, Res}. |
161 |
|
|
162 |
|
%%-------------------------------------------------------------------- |
163 |
|
%% Plugin utils API |
164 |
|
%%-------------------------------------------------------------------- |
165 |
|
-spec disable_node(mongooseim:host_type(), UserJID :: jid:jid(), BarePubSubJID :: jid:jid(), |
166 |
|
Node :: pubsub_node()) -> ok | {error, Reason :: term()}. |
167 |
|
disable_node(HostType, UserJID, BarePubSubJID, Node) -> |
168 |
:-( |
BareUserJID = jid:to_bare(UserJID), |
169 |
:-( |
maybe_remove_push_node_from_sessions_info(BareUserJID, BarePubSubJID, Node), |
170 |
:-( |
mod_event_pusher_push_backend:disable(HostType, BareUserJID, BarePubSubJID, Node). |
171 |
|
|
172 |
|
-spec cast(mongooseim:host_type(), F :: function(), A :: [any()]) -> any(). |
173 |
|
cast(HostType, F, A) -> |
174 |
:-( |
mongoose_wpool:cast(generic, HostType, pusher_push, {erlang, apply, [F, A]}). |
175 |
|
|
176 |
|
-spec is_virtual_pubsub_host(HostType :: mongooseim:host_type(), %% recipient host type |
177 |
|
RecipientDomain :: mongooseim:domain(), |
178 |
|
VirtPubsubDomain :: mongooseim:domain()) -> boolean(). |
179 |
|
is_virtual_pubsub_host(HostType, RecipientDomain, VirtPubsubDomain) -> |
180 |
:-( |
Templates = gen_mod:get_module_opt(HostType, ?MODULE, virtual_pubsub_hosts), |
181 |
:-( |
PredFn = fun(Template) -> |
182 |
:-( |
mongoose_subdomain_utils:is_subdomain(Template, |
183 |
|
RecipientDomain, |
184 |
|
VirtPubsubDomain) |
185 |
|
end, |
186 |
:-( |
lists:any(PredFn, Templates). |
187 |
|
|
188 |
|
%%-------------------------------------------------------------------- |
189 |
|
%% local functions |
190 |
|
%%-------------------------------------------------------------------- |
191 |
|
-spec do_push_event(mongoose_acc:t(), mod_event_pusher:event(), jid:jid()) -> mongoose_acc:t(). |
192 |
|
do_push_event(Acc, Event, BareRecipient) -> |
193 |
:-( |
case mod_event_pusher_push_plugin:prepare_notification(Acc, Event) of |
194 |
:-( |
skip -> Acc; |
195 |
|
Payload -> |
196 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
197 |
:-( |
{ok, Services} = mod_event_pusher_push_backend:get_publish_services(HostType, |
198 |
|
BareRecipient), |
199 |
:-( |
FilteredService = mod_event_pusher_push_plugin:should_publish(Acc, Event, Services), |
200 |
:-( |
mod_event_pusher_push_plugin:publish_notification(Acc, Event, Payload, FilteredService) |
201 |
|
end. |
202 |
|
|
203 |
|
-spec parse_request(Request :: exml:element()) -> |
204 |
|
{enable, jid:jid(), pubsub_node(), form()} | |
205 |
|
{disable, jid:jid(), pubsub_node() | undefined} | |
206 |
|
bad_request. |
207 |
|
parse_request(#xmlel{name = <<"enable">>} = Request) -> |
208 |
:-( |
JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)), |
209 |
:-( |
Node = exml_query:attr(Request, <<"node">>, <<>>), %% Treat unset node as empty - both forbidden |
210 |
:-( |
Form = exml_query:subelement(Request, <<"x">>), |
211 |
|
|
212 |
:-( |
case {JID, Node, parse_form(Form)} of |
213 |
:-( |
{_, _, invalid_form} -> bad_request; |
214 |
:-( |
{_, <<>>, _} -> bad_request; |
215 |
:-( |
{error, _, _} -> bad_request; |
216 |
:-( |
{#jid{lserver = <<>>}, _, _} -> bad_request; |
217 |
|
{JID, Node, FormFields} -> |
218 |
:-( |
{enable, jid:to_bare(JID), Node, FormFields} |
219 |
|
end; |
220 |
|
parse_request(#xmlel{name = <<"disable">>} = Request) -> |
221 |
:-( |
JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)), |
222 |
:-( |
Node = exml_query:attr(Request, <<"node">>, undefined), |
223 |
|
|
224 |
:-( |
case {JID, Node} of |
225 |
:-( |
{error, _} -> bad_request; |
226 |
:-( |
{_, <<>>} -> bad_request; %% Node may not be set, but shouldn't be empty |
227 |
:-( |
{#jid{lserver = <<>>}, _} -> bad_request; |
228 |
|
{JID, Node} -> |
229 |
:-( |
{disable, jid:to_bare(JID), Node} |
230 |
|
end; |
231 |
|
parse_request(_) -> |
232 |
:-( |
bad_request. |
233 |
|
|
234 |
|
-spec parse_form(undefined | exml:element()) -> invalid_form | form(). |
235 |
|
parse_form(undefined) -> |
236 |
:-( |
[]; |
237 |
|
parse_form(Form) -> |
238 |
:-( |
IsForm = ?NS_XDATA == exml_query:attr(Form, <<"xmlns">>), |
239 |
:-( |
IsSubmit = <<"submit">> == exml_query:attr(Form, <<"type">>, <<"submit">>), |
240 |
|
|
241 |
:-( |
FieldsXML = exml_query:subelements(Form, <<"field">>), |
242 |
:-( |
Fields = [{exml_query:attr(Field, <<"var">>), |
243 |
:-( |
exml_query:path(Field, [{element, <<"value">>}, cdata])} || Field <- FieldsXML], |
244 |
:-( |
{[{_, FormType}], CustomFields} = lists:partition( |
245 |
|
fun({Name, _}) -> |
246 |
:-( |
Name == <<"FORM_TYPE">> |
247 |
|
end, Fields), |
248 |
:-( |
IsFormTypeCorrect = ?NS_PUBSUB_PUB_OPTIONS == FormType, |
249 |
|
|
250 |
:-( |
case IsForm andalso IsSubmit andalso IsFormTypeCorrect of |
251 |
|
true -> |
252 |
:-( |
CustomFields; |
253 |
|
false -> |
254 |
:-( |
invalid_form |
255 |
|
end. |
256 |
|
|
257 |
|
-spec enable_node(mongooseim:host_type(), jid:jid(), jid:jid(), pubsub_node(), form()) -> |
258 |
|
ok | {error, Reason :: term()}. |
259 |
|
enable_node(HostType, From, BarePubSubJID, Node, FormFields) -> |
260 |
:-( |
mod_event_pusher_push_backend:enable(HostType, jid:to_bare(From), BarePubSubJID, Node, |
261 |
|
FormFields). |
262 |
|
|
263 |
|
-spec store_session_info(jid:jid(), publish_service()) -> any(). |
264 |
|
store_session_info(Jid, Service) -> |
265 |
:-( |
ejabberd_sm:store_info(Jid, ?SESSION_KEY, Service). |
266 |
|
|
267 |
|
-spec maybe_remove_push_node_from_sessions_info(jid:jid(), jid:jid(), pubsub_node() | undefined) -> |
268 |
|
ok. |
269 |
|
maybe_remove_push_node_from_sessions_info(From, PubSubJid, Node) -> |
270 |
:-( |
AllSessions = ejabberd_sm:get_raw_sessions(From), |
271 |
:-( |
find_and_remove_push_node(From, AllSessions, PubSubJid, Node). |
272 |
|
|
273 |
|
-spec find_and_remove_push_node(jid:jid(), [ejabberd_sm:session()], |
274 |
|
jid:jid(), pubsub_node() | undefined) -> ok. |
275 |
|
find_and_remove_push_node(_From, [], _,_) -> |
276 |
:-( |
ok; |
277 |
|
find_and_remove_push_node(From, [RawSession | Rest], PubSubJid, Node) -> |
278 |
:-( |
case my_push_node(RawSession, PubSubJid, Node) of |
279 |
|
true -> |
280 |
:-( |
LResource = mongoose_session:get_resource(RawSession), |
281 |
:-( |
JID = jid:replace_resource(From, LResource), |
282 |
:-( |
ejabberd_sm:remove_info(JID, ?SESSION_KEY), |
283 |
:-( |
find_and_remove_push_node(From, Rest, PubSubJid, Node); |
284 |
|
false -> |
285 |
:-( |
find_and_remove_push_node(From, Rest, PubSubJid, Node) |
286 |
|
end. |
287 |
|
|
288 |
|
-spec my_push_node(ejabberd_sm:session(), jid:jid(), pubsub_node() | undfined) -> boolean(). |
289 |
|
my_push_node(RawSession, PubSubJid, Node) -> |
290 |
:-( |
case mongoose_session:get_info(RawSession, ?SESSION_KEY, undefined) of |
291 |
|
{?SESSION_KEY, {PubSubJid, Node, _}} -> |
292 |
:-( |
true; |
293 |
|
{?SESSION_KEY, {PubSubJid, _, _}} when Node =:= undefined -> |
294 |
|
%% The node is undefined which means that a user wants to |
295 |
|
%% disable all the push nodes for the specified service |
296 |
:-( |
true; |
297 |
:-( |
_ -> false |
298 |
|
end. |
299 |
|
|
300 |
|
-spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}]. |
301 |
|
config_metrics(HostType) -> |
302 |
:-( |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]). |