./ct_report/coverage/mod_mam_muc_elasticsearch_arch.COVER.html

1 %%------------------------------------------------------------------
2 %% Copyright 2018 Erlang Solutions Ltd.
3 %%
4 %% Licensed under the Apache License, Version 2.0 (the "License");
5 %% you may not use this file except in compliance with the License.
6 %% You may obtain a copy of the License at
7 %%
8 %% http://www.apache.org/licenses/LICENSE-2.0
9 %%
10 %% Unless required by applicable law or agreed to in writing, software
11 %% distributed under the License is distributed on an "AS IS" BASIS,
12 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 %% See the License for the specific language governing permissions and
14 %% limitations under the License.
15 %%
16 %% @doc ElasticSearch backend for Message Archive Management for
17 %% group messages.
18 %%------------------------------------------------------------------
19 -module(mod_mam_muc_elasticsearch_arch).
20
21 -behaviour(gen_mod).
22 -behaviour(ejabberd_gen_mam_archive).
23
24 %% gen_mod callbacks
25 -export([start/2]).
26 -export([stop/1]).
27 -export([hooks/1]).
28
29 %% ejabberd_gen_mam_archive callbacks
30 -export([archive_message/3]).
31 -export([lookup_messages/3]).
32 -export([remove_archive/3]).
33 -export([archive_size/3]).
34 -export([get_mam_muc_gdpr_data/3]).
35
36 -include("mongoose.hrl").
37 -include("mongoose_rsm.hrl").
38 -include("mod_mam.hrl").
39 -include("jlib.hrl").
40
41 -define(INDEX_NAME, <<"muc_messages">>).
42 -define(TYPE_NAME, <<"muc">>).
43
44 %%-------------------------------------------------------------------
45 %% gen_mod callbacks
46 %%-------------------------------------------------------------------
47
48 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
49 start(_HostType, _Opts) ->
50 8 ok.
51
52 -spec stop(mongooseim:host_type()) -> ok.
53 stop(_HostType) ->
54 8 ok.
55
56 %%-------------------------------------------------------------------
57 %% ejabberd_gen_mam_archive callbacks
58 %%-------------------------------------------------------------------
59 -spec get_mam_muc_gdpr_data(Acc, Params, Extra) -> {ok | stop, Acc} when
60 Acc :: ejabberd_gen_mam_archive:mam_muc_gdpr_data(),
61 Params :: #{jid := jid:jid()},
62 Extra :: gen_hook:extra().
63 get_mam_muc_gdpr_data(Acc, #{jid := Source}, _Extra) ->
64 34 BinSource = mod_mam_utils:bare_jid(Source),
65 34 Filter = #{term => #{from_jid => BinSource}},
66 34 Sorting = #{mam_id => #{order => asc}},
67 34 SearchQuery = #{query => #{bool => #{filter => Filter}},
68 sort => Sorting},
69 34 case mongoose_elasticsearch:search(?INDEX_NAME, ?TYPE_NAME, SearchQuery) of
70 {ok, #{<<"hits">> := #{<<"hits">> := Hits}}} ->
71 34 Messages = lists:map(fun hit_to_gdpr_mam_message/1, Hits),
72 34 {ok, Messages ++ Acc};
73 {error, _} ->
74
:-(
{ok, Acc}
75 end.
76
77 -spec archive_message(Acc, Params, Extra) -> {ok, Acc} when
78 Acc :: ok | {error, term()},
79 Params :: mod_mam:archive_message_params(),
80 Extra :: gen_hook:extra().
81 archive_message(_Result, Params, #{host_type := HostType}) ->
82 204 #{message_id := MessageId,
83 local_jid := RoomJid,
84 remote_jid := FromJID,
85 source_jid := SourceJid,
86 packet := Packet} = Params,
87 204 Room = mod_mam_utils:bare_jid(RoomJid),
88 204 SourceBinJid = mod_mam_utils:full_jid(SourceJid),
89 204 From = mod_mam_utils:bare_jid(FromJID),
90 204 DocId = make_document_id(Room, MessageId),
91 204 Doc = make_document(MessageId, Room, SourceBinJid, Packet, From),
92 204 case mongoose_elasticsearch:insert_document(?INDEX_NAME, ?TYPE_NAME, DocId, Doc) of
93 {ok, _} ->
94 204 {ok, ok};
95 {error, Reason} = Err ->
96
:-(
?LOG_ERROR(maps:merge(Params,
97 #{what => archive_muc_message_failed, reason => Reason,
98 server => HostType, room => Room, source => SourceBinJid,
99
:-(
message_id => MessageId})),
100
:-(
mongoose_metrics:update(HostType, modMamDropped, 1),
101
:-(
{ok, Err}
102 end.
103
104 -spec lookup_messages(Acc, Params, Extra) -> {ok, Acc} when
105 Acc :: {ok, mod_mam:lookup_result()} | {error, term()},
106 Params :: mam_iq:lookup_params(),
107 Extra :: gen_hook:extra().
108 lookup_messages(Result,
109 #{rsm := #rsm_in{direction = before, id = ID} = RSM} = Params,
110 #{host_type := HostType})
111 when ID =/= undefined ->
112 7 {ok, lookup_message_page(Result, HostType, RSM, Params)};
113 lookup_messages(Result,
114 #{rsm := #rsm_in{direction = aft, id = ID} = RSM} = Params,
115 #{host_type := HostType})
116 when ID =/= undefined ->
117 5 {ok, lookup_message_page(Result, HostType, RSM, Params)};
118 lookup_messages(Result, Params, #{host_type := HostType}) ->
119 35 {ok, do_lookup_messages(Result, HostType, Params)}.
120
121 lookup_message_page(AccResult, Host, RSM, Params) ->
122 12 PageSize = maps:get(page_size, Params),
123 12 case do_lookup_messages(AccResult, Host, Params#{page_size := 1 + PageSize}) of
124 {error, _} = Err ->
125
:-(
Err;
126 {ok, LookupResult} ->
127 12 mod_mam_utils:check_for_item_not_found(RSM, PageSize, LookupResult)
128 end.
129
130 do_lookup_messages(_Result, Host, Params) ->
131 47 SearchQuery0 = build_search_query(Params),
132 47 Sorting = [#{mam_id => #{order => determine_sorting(Params)}}],
133 47 ResultLimit = maps:get(page_size, Params),
134 47 SearchQuery1 = SearchQuery0#{sort => Sorting,
135 size => ResultLimit},
136 47 SearchQuery2 = maybe_add_from_constraint(SearchQuery1, Params),
137 47 case mongoose_elasticsearch:search(?INDEX_NAME, ?TYPE_NAME, SearchQuery2) of
138 {ok, Result} ->
139 47 {ok, search_result_to_mam_lookup_result(Result, Params)};
140 {error, Reason} = Err ->
141
:-(
?LOG_ERROR(maps:merge(Params,
142 #{what => lookup_muc_messages_failed,
143
:-(
server => Host, reason => Reason})),
144
:-(
Err
145 end.
146
147 -spec archive_size(Acc, Params, Extra) -> {ok, Acc} when
148 Acc :: integer(),
149 Params :: #{archive_id := mod_mam:archive_id() | undefined, room := jid:jid()},
150 Extra :: gen_hook:extra().
151 archive_size(_Size, #{room := RoomJid}, _Extra) ->
152 118 SearchQuery = build_search_query(#{owner_jid => RoomJid}),
153 118 {ok, archive_size(SearchQuery)}.
154
155 -spec remove_archive(Acc, Params, Extra) -> {ok, Acc} when
156 Acc :: ok,
157 Params :: #{archive_id := mod_mam:archive_id() | undefined, room := jid:jid()},
158 Extra :: gen_hook:extra().
159 remove_archive(Acc, #{room := RoomJid}, #{host_type := HostType}) ->
160 52 SearchQuery = build_search_query(#{owner_jid => RoomJid}),
161 52 case mongoose_elasticsearch:delete_by_query(?INDEX_NAME, ?TYPE_NAME, SearchQuery) of
162 ok ->
163 52 ok;
164 {error, Reason} ->
165
:-(
?LOG_ERROR(#{what => remove_muc_archive_failed,
166
:-(
server => HostType, room_jid => RoomJid, reason => Reason}),
167
:-(
ok
168 end,
169 52 {ok, Acc}.
170
171 %%-------------------------------------------------------------------
172 %% Helpers
173 %%-------------------------------------------------------------------
174
175 -spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
176 hooks(Host) ->
177 16 [{mam_muc_archive_message, Host, fun ?MODULE:archive_message/3, #{}, 50},
178 {mam_muc_lookup_messages, Host, fun ?MODULE:lookup_messages/3, #{}, 50},
179 {mam_muc_archive_size, Host, fun ?MODULE:archive_size/3, #{}, 50},
180 {mam_muc_remove_archive, Host, fun ?MODULE:remove_archive/3, #{}, 50},
181 {get_mam_muc_gdpr_data, Host, fun ?MODULE:get_mam_muc_gdpr_data/3, #{}, 50}].
182
183 -spec make_document_id(binary(), mod_mam:message_id()) -> binary().
184 make_document_id(Room, MessageId) ->
185 204 <<Room/binary, $$, (integer_to_binary(MessageId))/binary>>.
186
187 -spec make_document(mod_mam:message_id(), binary(), binary(), exml:element(),
188 binary()) -> map().
189 make_document(MessageId, Room, SourceBinJid, Packet, FromJID) ->
190 204 #{mam_id => MessageId,
191 from_jid => FromJID,
192 room => Room,
193 source_jid => SourceBinJid,
194 message => exml:to_binary(Packet),
195 body => exml_query:path(Packet, [{element, <<"body">>}, cdata])
196 }.
197
198 -spec build_search_query(map()) -> mongoose_elasticsearch:query().
199 build_search_query(Params) ->
200 223 Filters = build_filters(Params),
201 223 TextSearchQuery = build_text_search_query(Params),
202 223 #{query =>
203 #{bool =>
204 #{must => TextSearchQuery,
205 filter => Filters}}}.
206
207 -spec build_filters(map()) -> [map()].
208 build_filters(Params) ->
209 223 Builders = [fun room_filter/1,
210 fun with_jid_filter/1,
211 fun range_filter/1],
212 223 lists:flatmap(fun(F) -> F(Params) end, Builders).
213
214 -spec room_filter(map()) -> [map()].
215 room_filter(#{owner_jid := Room}) ->
216 223 BinRoom = mod_mam_utils:bare_jid(Room),
217 223 [#{term => #{room => BinRoom}}].
218
219 -spec with_jid_filter(map()) -> [map()].
220 with_jid_filter(#{with_jid := #jid{} = WithJid}) ->
221 2 [#{term => #{source_jid => mod_mam_utils:full_jid(WithJid)}}];
222 with_jid_filter(_) ->
223 221 [].
224
225 -spec range_filter(map()) -> [map()].
226 range_filter(#{end_ts := End, start_ts := Start, borders := Borders, rsm := RSM}) ->
227 53 {StartId, EndId} = mod_mam_utils:calculate_msg_id_borders(RSM, Borders, Start, End),
228 53 Range1 = maybe_add_end_filter(EndId, #{}),
229 53 Range2 = maybe_add_start_filter(StartId, Range1),
230
231 53 case maps:size(Range2) of
232 0 ->
233 35 [];
234 _ ->
235 18 [#{range => #{mam_id => Range2}}]
236 end;
237 range_filter(_) ->
238 170 [].
239
240 -spec maybe_add_end_filter(undefined | mod_mam:message_id(), map()) -> map().
241 maybe_add_end_filter(undefined, RangeMap) ->
242 43 RangeMap;
243 maybe_add_end_filter(Value, RangeMap) ->
244 10 RangeMap#{le => Value}.
245
246 -spec maybe_add_start_filter(undefined | mod_mam:message_id(), map()) -> map().
247 maybe_add_start_filter(undefined, RangeMap) ->
248 44 RangeMap;
249 maybe_add_start_filter(Value, RangeMap) ->
250 9 RangeMap#{ge => Value}.
251
252 -spec build_text_search_query(map()) -> map().
253 build_text_search_query(#{search_text := SearchText}) when is_binary(SearchText) ->
254 1 #{simple_query_string => #{query => SearchText,
255 fields => [<<"body">>],
256 default_operator => <<"and">>}};
257 build_text_search_query(_) ->
258 222 #{match_all => #{}}.
259
260 -spec determine_sorting(map()) -> asc | desc.
261 determine_sorting(#{rsm := #rsm_in{direction = before}}) ->
262 12 desc;
263 determine_sorting(_) ->
264 35 asc.
265
266 -spec maybe_add_from_constraint(mongoose_elasticsearch:query(), map()) ->
267 mongoose_elasticsearch:query().
268 maybe_add_from_constraint(Query, #{rsm := #rsm_in{index = Offset}}) when is_integer(Offset) ->
269 2 Query#{from => Offset};
270 maybe_add_from_constraint(Query, _) ->
271 45 Query.
272
273 -spec search_result_to_mam_lookup_result(map(), map()) -> mod_mam:lookup_result().
274 search_result_to_mam_lookup_result(Result, Params) ->
275 47 #{<<"hits">> :=
276 #{<<"hits">> := Hits,
277 <<"total">> := TotalCount}} = Result,
278
279 47 Messages = lists:sort(
280 lists:map(fun hit_to_mam_message/1, Hits)),
281
282 47 case maps:get(is_simple, Params) of
283 true ->
284 8 {undefined, undefined, Messages};
285 _ ->
286 39 CorrectedTotalCount = corrected_total_count(TotalCount, Params),
287 39 Count = length(Messages),
288 39 Offset = calculate_offset(TotalCount, Count, Params),
289 39 {CorrectedTotalCount, Offset, Messages}
290 end.
291
292 -spec hit_to_mam_message(map()) -> mod_mam:message_row().
293 hit_to_mam_message(#{<<"_source">> := JSON}) ->
294 228 MessageId = maps:get(<<"mam_id">>, JSON),
295 228 Packet = maps:get(<<"message">>, JSON),
296 228 SourceJid = maps:get(<<"source_jid">>, JSON),
297 228 {ok, Stanza} = exml:parse(Packet),
298 228 #{id => MessageId, jid => jid:from_binary(SourceJid), packet => Stanza}.
299
300 hit_to_gdpr_mam_message(#{<<"_source">> := JSON}) ->
301 13 MessageId = maps:get(<<"mam_id">>, JSON),
302 13 Packet = maps:get(<<"message">>, JSON),
303 13 {integer_to_binary(MessageId), Packet}.
304
305 %% Usage of RSM affects the `"total"' value returned by ElasticSearch. Per RSM spec, the count
306 %% returned by the query should represent the size of the whole result set, which in case of MAM
307 %% is bound only by the MAM filters.
308 %% The solution is to compute the archive size as if the RSM wasn't used. There is an obvious race
309 %% condition here, because a user may send a message between initial request to ElasticSearch and
310 %% the count request issued here.
311 -spec corrected_total_count(non_neg_integer(), mongoose_elasticsearch:query()) ->
312 non_neg_integer().
313 corrected_total_count(_, #{rsm := #rsm_in{id = Id}} = Params) when is_integer(Id) ->
314 4 Query = build_search_query(Params#{rsm := undefined}),
315 4 archive_size(Query);
316 corrected_total_count(Count, _) ->
317 35 Count.
318
319 -spec calculate_offset(non_neg_integer(), non_neg_integer(), map()) -> non_neg_integer().
320 calculate_offset(_, _, #{rsm := #rsm_in{direction = undefined, index = Index}}) when is_integer(Index) ->
321 2 Index;
322 calculate_offset(TotalCount, Count, #{rsm := #rsm_in{direction = before}}) ->
323 7 TotalCount - Count;
324 calculate_offset(_, _, #{rsm := #rsm_in{direction = aft, id = Id}} = Params0) when is_integer(Id) ->
325 %% Not sure how this works..
326 2 Params1 = update_borders(Params0#{rsm := undefined}, Id + 1),
327 2 Query = build_search_query(Params1),
328 2 archive_size(Query);
329 calculate_offset(_, _, _) ->
330 28 0.
331
332 -spec update_borders(map(), non_neg_integer()) -> map().
333 update_borders(#{borders := Borders} = Params, EndId) ->
334 2 Params#{borders := update_borders_to_id(Borders, EndId)}.
335
336 -spec update_borders_to_id(#mam_borders{} | undefined, non_neg_integer()) -> #mam_borders{}.
337 update_borders_to_id(undefined, EndId) ->
338 2 #mam_borders{to_id = EndId};
339 update_borders_to_id(Borders, EndId) ->
340
:-(
Borders#mam_borders{to_id = EndId}.
341
342 -spec archive_size(mongoose_elasticsearch:query()) -> non_neg_integer().
343 archive_size(Query) ->
344 124 case mongoose_elasticsearch:count(?INDEX_NAME, ?TYPE_NAME, Query) of
345 {ok, Count} ->
346 124 Count;
347 {error, Reason} ->
348
:-(
?LOG_ERROR(#{what => archive_size_failed, es_query => Query, reason => Reason}),
349
:-(
0
350 end.
Line Hits Source