1 |
|
%% @doc MongooseIM/Ejabberd module for (a subset of) XEP-0079 support. |
2 |
|
%% This work was sponsored by Grindr LLC |
3 |
|
%% @reference <a href="http://xmpp.org/extensions/xep-0079.html">XEP-0079</a> |
4 |
|
%% @author <mongooseim@erlang-solutions.com> |
5 |
|
%% @copyright 2014 Erlang Solutions, Ltd. |
6 |
|
-module(mod_amp). |
7 |
|
|
8 |
|
-behavior(gen_mod). |
9 |
|
-behaviour(mongoose_module_metrics). |
10 |
|
-xep([{xep, 79}, {version, "1.2"}, {status, partial}]). |
11 |
|
-export([start/2, stop/1, supported_features/0]). |
12 |
|
-export([user_send_message/3, |
13 |
|
check_packet/2, |
14 |
|
disco_local_features/3, |
15 |
|
c2s_stream_features/3, |
16 |
|
xmpp_send_element/3]). |
17 |
|
|
18 |
|
-export_type([amp_event/0, amp_rule/0, amp_rules/0, amp_rule_support/0, |
19 |
|
amp_match_result/0, amp_strategy/0]). |
20 |
|
|
21 |
|
-include("amp.hrl"). |
22 |
|
-include("mongoose.hrl"). |
23 |
|
-include("jlib.hrl"). |
24 |
|
|
25 |
|
-define(AMP_FEATURE, |
26 |
|
#xmlel{name = <<"amp">>, attrs = [{<<"xmlns">>, ?NS_AMP_FEATURE}]}). |
27 |
|
|
28 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
29 |
|
start(HostType, _Opts) -> |
30 |
172 |
gen_hook:add_handlers(hooks(HostType)). |
31 |
|
|
32 |
|
-spec stop(mongooseim:host_type()) -> ok. |
33 |
|
stop(HostType) -> |
34 |
168 |
gen_hook:delete_handlers(hooks(HostType)). |
35 |
|
|
36 |
|
-spec supported_features() -> [atom()]. |
37 |
86 |
supported_features() -> [dynamic_domains]. |
38 |
|
|
39 |
|
-spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:fn()). |
40 |
|
c2s_hooks(HostType) -> |
41 |
340 |
[{user_send_message, HostType, fun ?MODULE:user_send_message/3, #{}, 5}]. |
42 |
|
|
43 |
|
hooks(HostType) -> |
44 |
340 |
[ |
45 |
|
{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}, |
46 |
|
{c2s_stream_features, HostType, fun ?MODULE:c2s_stream_features/3, #{}, 50}, |
47 |
|
{xmpp_send_element, HostType, fun ?MODULE:xmpp_send_element/3, #{}, 10}, |
48 |
|
{amp_verify_support, HostType, fun amp_resolver:verify_support/3, #{}, 10}, |
49 |
|
{amp_check_condition, HostType, fun amp_resolver:check_condition/3, #{}, 10}, |
50 |
|
{amp_determine_strategy, HostType, fun amp_strategy:determine_strategy/3, #{}, 10} |
51 |
|
| c2s_hooks(HostType)]. |
52 |
|
|
53 |
|
%% API |
54 |
|
|
55 |
|
-spec user_send_message(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) -> |
56 |
|
mongoose_c2s_hooks:result(). |
57 |
|
user_send_message(Acc, _, _) -> |
58 |
3693 |
{From, To, Element} = mongoose_acc:packet(Acc), |
59 |
3693 |
run_initial_check(Acc, From, To, Element). |
60 |
|
|
61 |
|
-spec check_packet(mongoose_acc:t(), amp_event()) -> mongoose_acc:t(). |
62 |
|
check_packet(Acc, Event) -> |
63 |
67321 |
case mongoose_acc:get(amp, rules, none, Acc) of |
64 |
67024 |
none -> Acc; |
65 |
297 |
Rules -> process_event(Acc, Rules, Event) |
66 |
|
end. |
67 |
|
|
68 |
|
-spec disco_local_features(mongoose_disco:feature_acc(), |
69 |
|
map(), |
70 |
|
map()) -> {ok, mongoose_disco:feature_acc()}. |
71 |
|
disco_local_features(Acc = #{node := Node}, _, _) -> |
72 |
118 |
NewAcc = case amp_features(Node) of |
73 |
4 |
[] -> Acc; |
74 |
114 |
Features -> mongoose_disco:add_features(Features, Acc) |
75 |
|
end, |
76 |
118 |
{ok, NewAcc}. |
77 |
|
|
78 |
|
-spec c2s_stream_features(Acc, Params, Extra) -> {ok, Acc} when |
79 |
|
Acc :: [exml:element()], |
80 |
|
Params :: map(), |
81 |
|
Extra :: gen_hook:extra(). |
82 |
|
c2s_stream_features(Acc, _, _) -> |
83 |
11938 |
{ok, lists:keystore(<<"amp">>, #xmlel.name, Acc, ?AMP_FEATURE)}. |
84 |
|
|
85 |
|
-spec xmpp_send_element(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) -> |
86 |
|
gen_hook:hook_fn_ret(mongoose_acc:t()). |
87 |
|
xmpp_send_element(Acc, _Params, _Extra) -> |
88 |
55582 |
Event = case mongoose_acc:get(c2s, send_result, undefined, Acc) of |
89 |
55460 |
ok -> delivered; |
90 |
122 |
_ -> delivery_failed |
91 |
|
end, |
92 |
55582 |
{ok, check_packet(Acc, Event)}. |
93 |
|
|
94 |
|
%% Internal |
95 |
|
-spec run_initial_check(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> |
96 |
|
mongoose_c2s_hooks:result(). |
97 |
|
run_initial_check(Acc, From, To, Packet) -> |
98 |
3693 |
Result = case amp:extract_requested_rules(Packet) of |
99 |
3421 |
none -> nothing_to_do; |
100 |
271 |
{rules, Rules} -> validate_and_process_rules(Packet, From, Rules, Acc); |
101 |
1 |
{errors, Errors} -> send_errors_and_drop(Packet, From, Errors, Acc) |
102 |
|
end, |
103 |
3693 |
case Result of |
104 |
|
nothing_to_do -> |
105 |
3421 |
{ok, Acc}; |
106 |
|
drop -> |
107 |
63 |
{stop, Acc}; |
108 |
|
NewRules -> |
109 |
209 |
Acc1 = mongoose_acc:set_permanent(amp, rules, NewRules, Acc), |
110 |
209 |
Acc2 = mongoose_acc:update_stanza(#{element => amp:strip_amp_el(Packet), |
111 |
|
from_jid => From, to_jid => To}, Acc1), |
112 |
209 |
{ok, Acc2} |
113 |
|
end. |
114 |
|
|
115 |
|
-spec validate_and_process_rules(exml:element(), jid:jid(), amp_rules(), mongoose_acc:t()) -> |
116 |
|
amp_rules() | drop. |
117 |
|
validate_and_process_rules(Packet, From, Rules, Acc) -> |
118 |
271 |
VerifiedRules = verify_support(mongoose_acc:host_type(Acc), Rules), |
119 |
271 |
{Good, Bad} = lists:partition(fun is_supported_rule/1, VerifiedRules), |
120 |
271 |
ValidRules = [ Rule || {supported, Rule} <- Good ], |
121 |
271 |
case Bad of |
122 |
|
[{error, ValidationError, InvalidRule} | _] -> |
123 |
2 |
send_error_and_drop(Packet, From, ValidationError, InvalidRule, Acc); |
124 |
|
[] -> |
125 |
269 |
process_rules(Packet, From, initial_check, ValidRules, Acc) |
126 |
|
end. |
127 |
|
|
128 |
|
-spec process_event(mongoose_acc:t(), amp_rules(), amp_event()) -> mongoose_acc:t(). |
129 |
|
process_event(Acc, Rules, Event) when Event =/= initial_check -> |
130 |
297 |
Packet = mongoose_acc:element(Acc), |
131 |
297 |
From = mongoose_acc:from_jid(Acc), |
132 |
297 |
NewRules = process_rules(Packet, From, Event, Rules, Acc), |
133 |
297 |
mongoose_acc:set_permanent(amp, rules, NewRules, Acc). |
134 |
|
|
135 |
|
-spec amp_features(binary()) -> [mongoose_disco:feature()]. |
136 |
|
amp_features(?NS_AMP) -> |
137 |
1 |
[<<?NS_AMP/binary, Suffix/binary>> || Suffix <- amp_feature_suffixes()]; |
138 |
|
amp_features(<<>>) -> |
139 |
113 |
[?NS_AMP]; |
140 |
|
amp_features(_) -> |
141 |
4 |
[]. |
142 |
|
|
143 |
|
%% @doc This may eventually be configurable, but for now we return a constant list. |
144 |
|
amp_feature_suffixes() -> |
145 |
1 |
[<<>>, |
146 |
|
<<"?action=drop">>, |
147 |
|
<<"?action=notify">>, |
148 |
|
<<"?action=error">>, |
149 |
|
<<"?condition=deliver">>, |
150 |
|
<<"?condition=match-resource">>]. |
151 |
|
|
152 |
|
-spec process_rules(exml:element(), jid:jid(), amp_event(), amp_rules(), mongoose_acc:t()) -> |
153 |
|
amp_rules() | drop. |
154 |
|
process_rules(Packet, From, Event, Rules, Acc) -> |
155 |
566 |
HostType = mongoose_acc:host_type(Acc), |
156 |
566 |
To = mongoose_acc:to_jid(Acc), |
157 |
566 |
Strategy = determine_strategy(HostType, Packet, From, To, Event), |
158 |
566 |
RulesWithResults = apply_rules(fun(Rule) -> |
159 |
544 |
resolve_condition(HostType, Strategy, Event, Rule) |
160 |
|
end, Rules), |
161 |
566 |
PacketResult = take_action(Packet, From, RulesWithResults, Acc), |
162 |
566 |
return_result(PacketResult, Event, RulesWithResults). |
163 |
|
|
164 |
|
%% @doc hooks helpers |
165 |
|
-spec verify_support(mongooseim:host_type(), amp_rules()) -> [amp_rule_support()]. |
166 |
|
verify_support(HostType, Rules) -> |
167 |
271 |
mongoose_hooks:amp_verify_support(HostType, Rules). |
168 |
|
|
169 |
|
-spec determine_strategy(mongooseim:host_type(), exml:element(), jid:jid(), jid:jid(), |
170 |
|
amp_event()) -> |
171 |
|
amp_strategy(). |
172 |
|
determine_strategy(HostType, Packet, From, To, Event) -> |
173 |
566 |
mongoose_hooks:amp_determine_strategy(HostType, From, To, Packet, Event). |
174 |
|
|
175 |
|
apply_rules(F, Rules) -> |
176 |
566 |
[Rule#amp_rule{result = F(Rule)} || Rule <- Rules]. |
177 |
|
|
178 |
|
-spec resolve_condition(mongooseim:host_type(), amp_strategy(), amp_event(), amp_rule()) -> |
179 |
|
amp_match_result(). |
180 |
|
resolve_condition(HostType, Strategy, Event, Rule) -> |
181 |
544 |
Result = mongoose_hooks:amp_check_condition(HostType, Strategy, Rule), |
182 |
544 |
match_undecided_for_final_event(Rule, Event, Result). |
183 |
|
|
184 |
|
match_undecided_for_final_event(#amp_rule{condition = deliver}, Event, undecided) |
185 |
|
when Event =:= delivered; |
186 |
16 |
Event =:= delivery_failed -> match; |
187 |
528 |
match_undecided_for_final_event(_, _, Result) -> Result. |
188 |
|
|
189 |
|
-spec take_action(exml:element(), jid:jid(), amp_rules(), mongoose_acc:t()) -> pass | drop. |
190 |
|
take_action(Packet, From, Rules, Acc) -> |
191 |
566 |
case find(fun(#amp_rule{result = Result}) -> Result =:= match end, Rules) of |
192 |
429 |
not_found -> pass; |
193 |
137 |
{found, Rule} -> take_action_for_matched_rule(Packet, From, Rule, Acc) |
194 |
|
end. |
195 |
|
|
196 |
60 |
return_result(drop, initial_check, _Rules) -> drop; |
197 |
|
return_result(pass, _Event, Rules) -> |
198 |
506 |
lists:filter(fun(#amp_rule{result = Result}) -> |
199 |
424 |
Result =:= undecided |
200 |
|
end, Rules). |
201 |
|
|
202 |
|
-spec take_action_for_matched_rule(exml:element(), jid:jid(), amp_rule(), mongoose_acc:t()) -> |
203 |
|
pass | drop. |
204 |
|
take_action_for_matched_rule(Packet, From, #amp_rule{action = notify} = Rule, _Acc) -> |
205 |
77 |
reply_to_sender(Rule, server_jid(From), From, Packet), |
206 |
77 |
pass; |
207 |
|
take_action_for_matched_rule(Packet, From, #amp_rule{action = error} = Rule, Acc) -> |
208 |
30 |
send_error_and_drop(Packet, From, 'undefined-condition', Rule, Acc); |
209 |
|
take_action_for_matched_rule(Packet, From, #amp_rule{action = drop}, Acc) -> |
210 |
30 |
update_metric_and_drop(Packet, From, Acc). |
211 |
|
|
212 |
|
-spec reply_to_sender(amp_rule(), jid:jid(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
213 |
|
reply_to_sender(MatchedRule, ServerJid, OriginalSender, OriginalPacket) -> |
214 |
77 |
Response = amp:make_response(MatchedRule, OriginalSender, OriginalPacket), |
215 |
77 |
ejabberd_router:route(ServerJid, OriginalSender, Response). |
216 |
|
|
217 |
|
-spec send_error_and_drop(exml:element(), jid:jid(), amp_error(), amp_rule(), mongoose_acc:t()) -> drop. |
218 |
|
send_error_and_drop(Packet, From, AmpError, MatchedRule, Acc) -> |
219 |
32 |
send_errors_and_drop(Packet, From, [{AmpError, MatchedRule}], Acc). |
220 |
|
|
221 |
|
-spec send_errors_and_drop(exml:element(), jid:jid(), [{amp_error(), amp_rule()}], mongoose_acc:t()) -> drop. |
222 |
|
send_errors_and_drop(Packet, From, [], Acc) -> |
223 |
:-( |
?LOG_ERROR(#{what => empty_list_of_errors_generated, |
224 |
|
text => <<"This shouldn't happen!">>, |
225 |
:-( |
exml_packet => Packet, from => From}), |
226 |
:-( |
update_metric_and_drop(Packet, From, Acc); |
227 |
|
send_errors_and_drop(Packet, From, ErrorRules, Acc) -> |
228 |
33 |
{Errors, Rules} = lists:unzip(ErrorRules), |
229 |
33 |
ErrorResponse = amp:make_error_response(Errors, Rules, From, Packet), |
230 |
33 |
ejabberd_router:route(server_jid(From), From, ErrorResponse), |
231 |
33 |
update_metric_and_drop(Packet, From, Acc). |
232 |
|
|
233 |
|
-spec update_metric_and_drop(exml:element(), jid:jid(), mongoose_acc:t()) -> drop. |
234 |
|
update_metric_and_drop(Packet, From, Acc) -> |
235 |
63 |
mongoose_hooks:xmpp_stanza_dropped(Acc, From, mongoose_acc:to_jid(Acc), Packet), |
236 |
63 |
drop. |
237 |
|
|
238 |
|
-spec is_supported_rule(amp_rule_support()) -> boolean(). |
239 |
403 |
is_supported_rule({supported, _}) -> true; |
240 |
2 |
is_supported_rule(_) -> false. |
241 |
|
|
242 |
|
server_jid(#jid{lserver = Host}) -> |
243 |
110 |
jid:from_binary(Host). |
244 |
|
|
245 |
429 |
find(_Pred, []) -> not_found; |
246 |
|
find(Pred, [H|T]) -> |
247 |
504 |
case Pred(H) of |
248 |
137 |
true -> {found, H}; |
249 |
367 |
false -> find(Pred, T) |
250 |
|
end. |