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