./ct_report/coverage/mod_event_pusher_push.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 %%% 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]).
Line Hits Source