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 |
|
%%% Default plugin module for mod_event_pusher_push. |
10 |
|
%%% This module allows for some dynamic customizations. |
11 |
|
%%% @end |
12 |
|
%%%------------------------------------------------------------------- |
13 |
|
-module(mod_event_pusher_push_plugin_defaults). |
14 |
|
-behavior(mod_event_pusher_push_plugin). |
15 |
|
-author('rafal.slota@erlang-solutions.com'). |
16 |
|
|
17 |
|
-include("jlib.hrl"). |
18 |
|
-include("mongoose.hrl"). |
19 |
|
-include("mod_event_pusher_events.hrl"). |
20 |
|
|
21 |
|
%% Callback API |
22 |
|
-export([prepare_notification/2, |
23 |
|
should_publish/3, |
24 |
|
publish_notification/4]). |
25 |
|
|
26 |
|
-define(PUSH_FORM_TYPE, <<"urn:xmpp:push:summary">>). |
27 |
|
%%-------------------------------------------------------------------- |
28 |
|
%% mod_event_pusher_push_plugin callbacks |
29 |
|
%%-------------------------------------------------------------------- |
30 |
|
-spec should_publish(Acc :: mongoose_acc:t(), |
31 |
|
Event :: mod_event_pusher:event(), |
32 |
|
Services :: [mod_event_pusher_push:publish_service()]) -> |
33 |
|
[mod_event_pusher_push:publish_service()]. |
34 |
|
should_publish(Acc, #chat_event{to = To}, Services) -> |
35 |
166 |
PublishedServices = mongoose_acc:get(event_pusher, published_services, [], Acc), |
36 |
166 |
case should_publish(Acc, To) of |
37 |
84 |
true -> Services -- PublishedServices; |
38 |
82 |
false -> [] |
39 |
|
end; |
40 |
:-( |
should_publish(_Acc, _Event, _Services) -> []. |
41 |
|
|
42 |
|
-spec prepare_notification(Acc :: mongoose_acc:t(), |
43 |
|
Event :: mod_event_pusher:event()) -> |
44 |
|
mod_event_pusher_push_plugin:push_payload() | skip. |
45 |
|
prepare_notification(Acc, _) -> |
46 |
220 |
{From, To, Packet} = mongoose_acc:packet(Acc), |
47 |
220 |
case exml_query:subelement(Packet, <<"body">>) of |
48 |
:-( |
undefined -> skip; |
49 |
|
Body -> |
50 |
220 |
BodyCData = exml_query:cdata(Body), |
51 |
220 |
MessageCount = get_unread_count(Acc, To), |
52 |
220 |
SenderId = sender_id(From, Packet), |
53 |
220 |
push_content_fields(SenderId, BodyCData, MessageCount) |
54 |
|
end. |
55 |
|
|
56 |
|
-spec publish_notification(Acc :: mongoose_acc:t(), |
57 |
|
Event :: mod_event_pusher:event(), |
58 |
|
Payload :: mod_event_pusher_push_plugin:push_payload(), |
59 |
|
Services :: [mod_event_pusher_push:publish_service()]) -> |
60 |
|
mongoose_acc:t(). |
61 |
|
publish_notification(Acc, _, Payload, Services) -> |
62 |
76 |
To = mongoose_acc:to_jid(Acc), |
63 |
76 |
HostType = mongoose_acc:host_type(Acc), |
64 |
76 |
lists:foreach(fun({PubsubJID, _Node, _Form} = Service) -> |
65 |
76 |
case mod_event_pusher_push:is_virtual_pubsub_host(HostType, |
66 |
|
To#jid.lserver, |
67 |
|
PubsubJID#jid.lserver) of |
68 |
|
true -> |
69 |
38 |
publish_via_hook(Acc, HostType, To, Service, Payload); |
70 |
|
false -> |
71 |
38 |
publish_via_pubsub(HostType, To, Service, Payload) |
72 |
|
end |
73 |
|
end, Services), |
74 |
|
|
75 |
76 |
mongoose_acc:append(event_pusher, published_services, Services, Acc). |
76 |
|
|
77 |
|
%%-------------------------------------------------------------------- |
78 |
|
%% local functions |
79 |
|
%%-------------------------------------------------------------------- |
80 |
|
|
81 |
|
-spec should_publish(Acc :: mongoose_acc:t(), To :: jid:jid()) -> boolean(). |
82 |
|
should_publish(Acc, #jid{} = To) -> |
83 |
166 |
HostType = mongoose_acc:host_type(Acc), |
84 |
166 |
try ejabberd_auth:does_user_exist(HostType, To, stored) of |
85 |
|
false -> |
86 |
32 |
false; |
87 |
|
true -> |
88 |
134 |
ejabberd_sm:is_offline(To) |
89 |
|
catch |
90 |
|
_:_ -> |
91 |
:-( |
ejabberd_sm:is_offline(To) |
92 |
|
end. |
93 |
|
|
94 |
|
-spec get_unread_count(mongoose_acc:t(), jid:jid()) -> pos_integer(). |
95 |
|
get_unread_count(Acc, To) -> |
96 |
220 |
NewAcc = mongoose_hooks:inbox_unread_count(To#jid.lserver, Acc, To), |
97 |
220 |
mongoose_acc:get(inbox, unread_count, 1, NewAcc). |
98 |
|
|
99 |
|
-spec sender_id(jid:jid(), exml:element()) -> binary(). |
100 |
|
sender_id(From, Packet) -> |
101 |
220 |
case exml_query:attr(Packet, <<"type">>) of |
102 |
|
<<"chat">> -> |
103 |
70 |
jid:to_bare_binary(jid:to_lower(From)); |
104 |
|
<<"groupchat">> -> |
105 |
150 |
jid:to_binary(jid:to_lower(From)) |
106 |
|
end. |
107 |
|
|
108 |
|
-spec push_content_fields(binary(), binary(), non_neg_integer()) -> |
109 |
|
mod_event_pusher_push_plugin:push_payload() | skip. |
110 |
|
push_content_fields(_SenderId, <<"">>, _MessageCount) -> |
111 |
44 |
skip; |
112 |
|
push_content_fields(SenderId, BodyCData, MessageCount) -> |
113 |
176 |
[ |
114 |
|
{<<"message-count">>, integer_to_binary(MessageCount)}, |
115 |
|
{<<"last-message-sender">>, SenderId}, |
116 |
|
{<<"last-message-body">>, BodyCData} |
117 |
|
]. |
118 |
|
|
119 |
|
-spec publish_via_hook(Acc :: mongoose_acc:t(), |
120 |
|
HostType :: mongooseim:host_type(), |
121 |
|
To :: jid:jid(), |
122 |
|
Service :: mod_event_pusher_push:publish_service(), |
123 |
|
PushPayload :: mod_event_pusher_push_plugin:push_payload()) -> |
124 |
|
any(). |
125 |
|
publish_via_hook(Acc0, HostType, To, {PubsubJID, Node, Form}, PushPayload) -> |
126 |
|
%% Acc is ignored by mod_push_service_mongoosepush, added here only for |
127 |
|
%% traceability purposes and push_SUITE code unification |
128 |
38 |
Acc = mongoose_acc:set(push_notifications, pubsub_jid, PubsubJID, Acc0), |
129 |
38 |
case mongoose_hooks:push_notifications(HostType, Acc, [maps:from_list(PushPayload)], Form) of |
130 |
|
{error, device_not_registered} -> |
131 |
|
%% We disable the push node in case the error type is device_not_registered |
132 |
1 |
mod_event_pusher_push:disable_node(HostType, To, PubsubJID, Node); |
133 |
37 |
_ -> ok |
134 |
|
end. |
135 |
|
|
136 |
|
-spec publish_via_pubsub(mongooseim:host_type(), To :: jid:jid(), |
137 |
|
Service :: mod_event_pusher_push:publish_service(), |
138 |
|
PushPayload :: mod_event_pusher_push_plugin:push_payload()) -> |
139 |
|
any(). |
140 |
|
publish_via_pubsub(HostType, To, {PubsubJID, Node, Form}, PushPayload) -> |
141 |
38 |
Stanza = push_notification_iq(Node, Form, PushPayload), |
142 |
38 |
Acc = mongoose_acc:new(#{ location => ?LOCATION, |
143 |
|
host_type => HostType, |
144 |
|
lserver => To#jid.lserver, |
145 |
|
element => jlib:iq_to_xml(Stanza), |
146 |
|
from_jid => To, |
147 |
|
to_jid => PubsubJID }), |
148 |
|
|
149 |
38 |
ResponseHandler = |
150 |
|
fun(_From, _To, FAcc, Response) -> |
151 |
38 |
mod_event_pusher_push:cast(HostType, fun handle_publish_response/5, |
152 |
|
[HostType, To, PubsubJID, Node, Response]), |
153 |
38 |
FAcc |
154 |
|
end, |
155 |
|
%% The IQ is routed from the recipient's server JID to pubsub JID |
156 |
|
%% This is recommended in the XEP and also helps process replies to this IQ |
157 |
38 |
NotificationFrom = jid:make(<<>>, To#jid.lserver, <<>>), |
158 |
38 |
mod_event_pusher_push:cast(HostType, fun ejabberd_local:route_iq/5, |
159 |
|
[NotificationFrom, PubsubJID, Acc, Stanza, ResponseHandler]). |
160 |
|
|
161 |
|
-spec handle_publish_response(mongooseim:host_type(), |
162 |
|
Recipient :: jid:jid(), PubsubJID :: jid:jid(), |
163 |
|
Node :: mod_event_pusher_push:pubsub_node(), |
164 |
|
Result :: timeout | jlib:iq()) -> ok. |
165 |
|
handle_publish_response(_HostType, _Recipient, _PubsubJID, _Node, timeout) -> |
166 |
2 |
ok; |
167 |
|
handle_publish_response(_HostType, _Recipient, _PubsubJID, _Node, #iq{type = result}) -> |
168 |
30 |
ok; |
169 |
|
handle_publish_response(HostType, Recipient, PubsubJID, Node, #iq{type = error, sub_el = Els}) -> |
170 |
4 |
[Error | _ ] = [Err || #xmlel{name = <<"error">>} = Err <- Els], |
171 |
4 |
case exml_query:attr(Error, <<"type">>) of |
172 |
|
<<"cancel">> -> |
173 |
|
%% We disable the push node in case the error type is cancel |
174 |
1 |
mod_event_pusher_push:disable_node(HostType, Recipient, PubsubJID, Node); |
175 |
|
_ -> |
176 |
3 |
ok |
177 |
|
end. |
178 |
|
|
179 |
|
-spec push_notification_iq(Node :: mod_event_pusher_push:pubsub_node(), |
180 |
|
Form :: mod_event_pusher_push:form(), |
181 |
|
PushPayload :: mod_event_pusher_push_plugin:push_payload()) -> |
182 |
|
jlib:iq(). |
183 |
|
push_notification_iq(Node, Form, PushPayload) -> |
184 |
38 |
#iq{type = set, sub_el = [ |
185 |
|
#xmlel{name = <<"pubsub">>, attrs = [{<<"xmlns">>, ?NS_PUBSUB}], children = [ |
186 |
|
#xmlel{name = <<"publish">>, attrs = [{<<"node">>, Node}], children = [ |
187 |
|
#xmlel{name = <<"item">>, children = [ |
188 |
|
#xmlel{name = <<"notification">>, |
189 |
|
attrs = [{<<"xmlns">>, ?NS_PUSH}], |
190 |
|
children = [make_form(?PUSH_FORM_TYPE, PushPayload)]} |
191 |
|
]} |
192 |
|
]} |
193 |
|
] ++ maybe_publish_options(maps:to_list(Form))} |
194 |
|
]}. |
195 |
|
|
196 |
|
-spec make_form(binary(), mod_event_pusher_push_plugin:push_payload()) -> exml:element(). |
197 |
|
make_form(FormType, FieldKVs) -> |
198 |
74 |
Fields = [#{var => Name, values => [Value]} || {Name, Value} <- FieldKVs], |
199 |
74 |
mongoose_data_forms:form(#{ns => FormType, type => <<"submit">>, fields => Fields}). |
200 |
|
|
201 |
|
-spec maybe_publish_options([{binary(), binary()}]) -> [exml:element()]. |
202 |
|
maybe_publish_options([]) -> |
203 |
2 |
[]; |
204 |
|
maybe_publish_options(FormFields) -> |
205 |
36 |
Children = [make_form(?NS_PUBSUB_PUB_OPTIONS, FormFields)], |
206 |
36 |
[#xmlel{name = <<"publish-options">>, children = Children}]. |