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. |