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.4.1"}]). |
18 |
|
|
19 |
|
-include("mod_event_pusher_events.hrl"). |
20 |
|
-include("mongoose.hrl"). |
21 |
|
-include("session.hrl"). |
22 |
|
-include("jlib.hrl"). |
23 |
|
-include("mongoose_config_spec.hrl"). |
24 |
|
|
25 |
|
-define(SESSION_KEY, publish_service). |
26 |
|
|
27 |
|
%%-------------------------------------------------------------------- |
28 |
|
%% Exports |
29 |
|
%%-------------------------------------------------------------------- |
30 |
|
|
31 |
|
%% gen_mod behaviour |
32 |
|
-export([start/2, stop/1, config_spec/0]). |
33 |
|
|
34 |
|
%% mongoose_module_metrics behaviour |
35 |
|
-export([config_metrics/1]). |
36 |
|
|
37 |
|
%% mod_event_pusher behaviour |
38 |
|
-export([push_event/2]). |
39 |
|
|
40 |
|
%% Hooks and IQ handlers |
41 |
|
-export([iq_handler/4, |
42 |
|
remove_user/3]). |
43 |
|
|
44 |
|
%% Plugin utils |
45 |
|
-export([cast/3]). |
46 |
|
-export([is_virtual_pubsub_host/3]). |
47 |
|
-export([disable_node/4]). |
48 |
|
|
49 |
|
-ignore_xref([iq_handler/4]). |
50 |
|
|
51 |
|
%% Types |
52 |
|
-type publish_service() :: {PubSub :: jid:jid(), Node :: pubsub_node(), Form :: form()}. |
53 |
|
-type pubsub_node() :: binary(). |
54 |
|
-type form() :: #{binary() => binary()}. |
55 |
|
|
56 |
|
-export_type([pubsub_node/0, form/0]). |
57 |
|
-export_type([publish_service/0]). |
58 |
|
|
59 |
|
%%-------------------------------------------------------------------- |
60 |
|
%% gen_mod callbacks |
61 |
|
%%-------------------------------------------------------------------- |
62 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any(). |
63 |
|
start(HostType, Opts) -> |
64 |
:-( |
?LOG_INFO(#{what => event_pusher_starting, host_type => HostType}), |
65 |
:-( |
start_pool(HostType, Opts), |
66 |
:-( |
mod_event_pusher_push_backend:init(HostType, Opts), |
67 |
:-( |
mod_event_pusher_push_plugin:init(HostType, Opts), |
68 |
:-( |
init_iq_handlers(HostType, Opts), |
69 |
:-( |
gen_hook:add_handler(remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 90), |
70 |
:-( |
ok. |
71 |
|
|
72 |
|
start_pool(HostType, #{wpool := WpoolOpts}) -> |
73 |
:-( |
{ok, _} = mongoose_wpool:start(generic, HostType, pusher_push, maps:to_list(WpoolOpts)). |
74 |
|
|
75 |
|
init_iq_handlers(HostType, #{iqdisc := IQDisc}) -> |
76 |
:-( |
gen_iq_handler:add_iq_handler(ejabberd_local, HostType, ?NS_PUSH, ?MODULE, |
77 |
|
iq_handler, IQDisc), |
78 |
:-( |
gen_iq_handler:add_iq_handler(ejabberd_sm, HostType, ?NS_PUSH, ?MODULE, |
79 |
|
iq_handler, IQDisc). |
80 |
|
|
81 |
|
-spec stop(mongooseim:host_type()) -> ok. |
82 |
|
stop(HostType) -> |
83 |
:-( |
gen_hook:delete_handler(remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 90), |
84 |
|
|
85 |
:-( |
gen_iq_handler:remove_iq_handler(ejabberd_sm, HostType, ?NS_PUSH), |
86 |
:-( |
gen_iq_handler:remove_iq_handler(ejabberd_local, HostType, ?NS_PUSH), |
87 |
|
|
88 |
:-( |
mongoose_wpool:stop(generic, HostType, pusher_push), |
89 |
:-( |
ok. |
90 |
|
|
91 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
92 |
|
config_spec() -> |
93 |
186 |
VirtPubSubHost = #option{type = string, validate = subdomain_template, |
94 |
|
process = fun mongoose_subdomain_utils:make_subdomain_pattern/1}, |
95 |
186 |
#section{ |
96 |
|
items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(), |
97 |
|
<<"backend">> => #option{type = atom, validate = {module, ?MODULE}}, |
98 |
|
<<"wpool">> => wpool_spec(), |
99 |
|
<<"plugin_module">> => #option{type = atom, validate = module}, |
100 |
|
<<"virtual_pubsub_hosts">> => #list{items = VirtPubSubHost}}, |
101 |
|
defaults = #{<<"iqdisc">> => one_queue, |
102 |
|
<<"backend">> => mnesia, |
103 |
|
<<"plugin_module">> => mod_event_pusher_push_plugin:default_plugin_module(), |
104 |
|
<<"virtual_pubsub_hosts">> => []} |
105 |
|
}. |
106 |
|
|
107 |
|
wpool_spec() -> |
108 |
186 |
Wpool = mongoose_config_spec:wpool(#{<<"strategy">> => available_worker}), |
109 |
186 |
Wpool#section{include = always}. |
110 |
|
|
111 |
|
%%-------------------------------------------------------------------- |
112 |
|
%% mod_event_pusher callbacks |
113 |
|
%%-------------------------------------------------------------------- |
114 |
|
-spec push_event(mongoose_acc:t(), mod_event_pusher:event()) -> mongoose_acc:t(). |
115 |
|
push_event(Acc, Event = #chat_event{direction = out, to = To, type = Type}) |
116 |
|
when Type =:= groupchat; |
117 |
|
Type =:= chat -> |
118 |
:-( |
BareRecipient = jid:to_bare(To), |
119 |
:-( |
do_push_event(Acc, Event, BareRecipient); |
120 |
|
push_event(Acc, Event = #unack_msg_event{to = To}) -> |
121 |
:-( |
BareRecipient = jid:to_bare(To), |
122 |
:-( |
do_push_event(Acc, Event, BareRecipient); |
123 |
|
push_event(Acc, _) -> |
124 |
:-( |
Acc. |
125 |
|
|
126 |
|
%%-------------------------------------------------------------------- |
127 |
|
%% Hooks and IQ handlers |
128 |
|
%%-------------------------------------------------------------------- |
129 |
|
-spec remove_user(Acc, Params, Extra) -> {ok, Acc} when |
130 |
|
Acc :: mongoose_acc:t(), |
131 |
|
Params :: #{jid := jid:jid()}, |
132 |
|
Extra :: map(). |
133 |
|
remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, _) -> |
134 |
:-( |
R = mod_event_pusher_push_backend:disable(LServer, jid:make_noprep(LUser, LServer, <<>>)), |
135 |
:-( |
mongoose_lib:log_if_backend_error(R, ?MODULE, ?LINE, {Acc, LUser, LServer}), |
136 |
:-( |
{ok, 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 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
145 |
:-( |
Res = case parse_request(Request) of |
146 |
|
{enable, BarePubSubJID, Node, FormFields} -> |
147 |
:-( |
ok = enable_node(HostType, From, BarePubSubJID, Node, FormFields), |
148 |
:-( |
store_session_info(From, {BarePubSubJID, Node, FormFields}, Acc), |
149 |
:-( |
IQ#iq{type = result, sub_el = []}; |
150 |
|
{disable, BarePubsubJID, Node} -> |
151 |
:-( |
ok = disable_node(HostType, From, BarePubsubJID, Node), |
152 |
:-( |
IQ#iq{type = result, sub_el = []}; |
153 |
|
bad_request -> |
154 |
:-( |
IQ#iq{type = error, sub_el = [Request, mongoose_xmpp_errors:bad_request()]} |
155 |
|
end, |
156 |
:-( |
{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 |
:-( |
BareUserJID = jid:to_bare(UserJID), |
165 |
:-( |
maybe_remove_push_node_from_sessions_info(BareUserJID, BarePubSubJID, Node), |
166 |
:-( |
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 |
:-( |
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_name(), |
174 |
|
VirtPubsubDomain :: mongooseim:domain_name()) -> boolean(). |
175 |
|
is_virtual_pubsub_host(HostType, RecipientDomain, VirtPubsubDomain) -> |
176 |
:-( |
Templates = gen_mod:get_module_opt(HostType, ?MODULE, virtual_pubsub_hosts), |
177 |
:-( |
PredFn = fun(Template) -> |
178 |
:-( |
mongoose_subdomain_utils:is_subdomain(Template, |
179 |
|
RecipientDomain, |
180 |
|
VirtPubsubDomain) |
181 |
|
end, |
182 |
:-( |
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 |
:-( |
case mod_event_pusher_push_plugin:prepare_notification(Acc, Event) of |
190 |
:-( |
skip -> Acc; |
191 |
|
Payload -> |
192 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
193 |
:-( |
{ok, Services} = mod_event_pusher_push_backend:get_publish_services(HostType, |
194 |
|
BareRecipient), |
195 |
:-( |
FilteredService = mod_event_pusher_push_plugin:should_publish(Acc, Event, Services), |
196 |
:-( |
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 |
:-( |
JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)), |
205 |
:-( |
Node = exml_query:attr(Request, <<"node">>, <<>>), %% Treat unset node as empty - both forbidden |
206 |
:-( |
Form = mongoose_data_forms:find_form(Request), |
207 |
|
|
208 |
:-( |
case {JID, Node, parse_form(Form)} of |
209 |
:-( |
{_, _, invalid_form} -> bad_request; |
210 |
:-( |
{_, <<>>, _} -> bad_request; |
211 |
:-( |
{error, _, _} -> bad_request; |
212 |
:-( |
{#jid{lserver = <<>>}, _, _} -> bad_request; |
213 |
|
{JID, Node, FormFields} -> |
214 |
:-( |
{enable, jid:to_bare(JID), Node, FormFields} |
215 |
|
end; |
216 |
|
parse_request(#xmlel{name = <<"disable">>} = Request) -> |
217 |
:-( |
JID = jid:from_binary(exml_query:attr(Request, <<"jid">>, <<>>)), |
218 |
:-( |
Node = exml_query:attr(Request, <<"node">>, undefined), |
219 |
|
|
220 |
:-( |
case {JID, Node} of |
221 |
:-( |
{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 |
:-( |
{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 |
:-( |
#{}; |
233 |
|
parse_form(Form) -> |
234 |
:-( |
parse_form_fields(Form). |
235 |
|
|
236 |
|
-spec parse_form_fields(exml:element()) -> invalid_form | form(). |
237 |
|
parse_form_fields(Form) -> |
238 |
:-( |
case mongoose_data_forms:parse_form_fields(Form) of |
239 |
|
#{type := <<"submit">>, ns := ?NS_PUBSUB_PUB_OPTIONS, kvs := KVs} -> |
240 |
:-( |
case maps:filtermap(fun(_, [V]) -> {true, V}; |
241 |
:-( |
(_, _) -> false |
242 |
|
end, KVs) of |
243 |
|
ParsedKVs when map_size(ParsedKVs) < map_size(KVs) -> |
244 |
:-( |
invalid_form; |
245 |
|
ParsedKVs -> |
246 |
:-( |
ParsedKVs |
247 |
|
end; |
248 |
|
_ -> |
249 |
:-( |
invalid_form |
250 |
|
end. |
251 |
|
|
252 |
|
-spec enable_node(mongooseim:host_type(), jid:jid(), jid:jid(), pubsub_node(), form()) -> |
253 |
|
ok | {error, Reason :: term()}. |
254 |
|
enable_node(HostType, From, BarePubSubJID, Node, FormFields) -> |
255 |
:-( |
mod_event_pusher_push_backend:enable(HostType, jid:to_bare(From), BarePubSubJID, Node, |
256 |
|
FormFields). |
257 |
|
|
258 |
|
-spec store_session_info(jid:jid(), publish_service(), mongoose_acc:t()) -> any(). |
259 |
|
store_session_info(Jid, Service, Acc) -> |
260 |
:-( |
OriginSid = mongoose_acc:get(c2s, origin_sid, undefined, Acc), |
261 |
:-( |
ejabberd_sm:store_info(Jid, OriginSid, ?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 |
:-( |
AllSessions = ejabberd_sm:get_raw_sessions(From), |
267 |
:-( |
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 |
:-( |
ok; |
273 |
|
find_and_remove_push_node(From, [RawSession | Rest], PubSubJid, Node) -> |
274 |
:-( |
case my_push_node(RawSession, PubSubJid, Node) of |
275 |
|
true -> |
276 |
:-( |
LResource = mongoose_session:get_resource(RawSession), |
277 |
:-( |
JID = jid:replace_resource(From, LResource), |
278 |
:-( |
Sid = RawSession#session.sid, |
279 |
:-( |
ejabberd_sm:remove_info(JID, Sid, ?SESSION_KEY), |
280 |
:-( |
find_and_remove_push_node(From, Rest, PubSubJid, Node); |
281 |
|
false -> |
282 |
:-( |
find_and_remove_push_node(From, Rest, PubSubJid, Node) |
283 |
|
end. |
284 |
|
|
285 |
|
-spec my_push_node(ejabberd_sm:session(), jid:jid(), pubsub_node() | undfined) -> boolean(). |
286 |
|
my_push_node(RawSession, PubSubJid, Node) -> |
287 |
:-( |
case mongoose_session:get_info(RawSession, ?SESSION_KEY, undefined) of |
288 |
|
{?SESSION_KEY, {PubSubJid, Node, _}} -> |
289 |
:-( |
true; |
290 |
|
{?SESSION_KEY, {PubSubJid, _, _}} when Node =:= undefined -> |
291 |
|
%% The node is undefined which means that a user wants to |
292 |
|
%% disable all the push nodes for the specified service |
293 |
:-( |
true; |
294 |
:-( |
_ -> false |
295 |
|
end. |
296 |
|
|
297 |
|
-spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}]. |
298 |
|
config_metrics(HostType) -> |
299 |
:-( |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]). |