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