1 |
|
%%============================================================================== |
2 |
|
%% Copyright 2016 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 |
|
|
17 |
|
-module(mod_mam_meta). |
18 |
|
-behaviour(gen_mod). |
19 |
|
-behaviour(mongoose_module_metrics). |
20 |
|
|
21 |
|
-type deps() :: #{module() => proplists:proplist()}. |
22 |
|
|
23 |
|
-export([start/2, stop/1, config_spec/0, supported_features/0, deps/2]). |
24 |
|
|
25 |
|
-export([config_metrics/1]). |
26 |
|
|
27 |
|
-include("mongoose_config_spec.hrl"). |
28 |
|
|
29 |
|
%%-------------------------------------------------------------------- |
30 |
|
%% API |
31 |
|
%%-------------------------------------------------------------------- |
32 |
|
|
33 |
|
-spec supported_features() -> [atom()]. |
34 |
|
supported_features() -> |
35 |
13 |
[dynamic_domains]. |
36 |
|
|
37 |
|
-spec start(Host :: jid:server(), Opts :: list()) -> any(). |
38 |
|
start(_Host, _Opts) -> |
39 |
13 |
ok. |
40 |
|
|
41 |
|
|
42 |
|
-spec stop(Host :: jid:server()) -> any(). |
43 |
|
stop(_Host) -> |
44 |
13 |
ok. |
45 |
|
|
46 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
47 |
|
config_spec() -> |
48 |
146 |
Items = config_items(), |
49 |
146 |
#section{ |
50 |
|
items = Items#{<<"pm">> => #section{items = maps:merge(Items, pm_config_items())}, |
51 |
|
<<"muc">> => #section{items = maps:merge(Items, muc_config_items())}, |
52 |
|
<<"riak">> => riak_config_spec()} |
53 |
|
}. |
54 |
|
|
55 |
|
config_items() -> |
56 |
146 |
#{%% General options |
57 |
|
<<"backend">> => #option{type = atom, |
58 |
|
validate = {enum, [rdbms, riak, cassandra, elasticsearch]}}, |
59 |
|
<<"no_stanzaid_element">> => #option{type = boolean}, |
60 |
|
<<"is_archivable_message">> => #option{type = atom, |
61 |
|
validate = module}, |
62 |
|
<<"send_message">> => #option{type = atom, |
63 |
|
validate = module}, |
64 |
|
<<"archive_chat_markers">> => #option{type = boolean}, |
65 |
|
<<"message_retraction">> => #option{type = boolean}, |
66 |
|
|
67 |
|
%% Common backend options |
68 |
|
<<"user_prefs_store">> => #option{type = atom, |
69 |
|
validate = {enum, [rdbms, cassandra, mnesia]}}, |
70 |
|
<<"full_text_search">> => #option{type = boolean}, |
71 |
|
|
72 |
|
%% RDBMS-specific options |
73 |
|
<<"cache_users">> => #option{type = boolean}, |
74 |
|
<<"cache">> => mongoose_user_cache:config_spec(), |
75 |
|
<<"rdbms_message_format">> => #option{type = atom, |
76 |
|
validate = {enum, [simple, internal]}}, |
77 |
|
<<"async_writer">> => mod_mam_rdbms_arch_async:config_spec(), |
78 |
|
|
79 |
|
%% Low-level options |
80 |
|
<<"default_result_limit">> => #option{type = integer, |
81 |
|
validate = non_negative}, |
82 |
|
<<"max_result_limit">> => #option{type = integer, |
83 |
|
validate = non_negative}, |
84 |
|
<<"db_jid_format">> => #option{type = atom, |
85 |
|
validate = module}, |
86 |
|
<<"db_message_format">> => #option{type = atom, |
87 |
|
validate = module}, |
88 |
|
<<"simple">> => #option{type = boolean}, |
89 |
|
<<"extra_fin_element">> => #option{type = atom, |
90 |
|
validate = module}, |
91 |
|
<<"extra_lookup_params">> => #option{type = atom, |
92 |
|
validate = module} |
93 |
|
}. |
94 |
|
|
95 |
|
pm_config_items() -> |
96 |
146 |
#{<<"archive_groupchats">> => #option{type = boolean}, |
97 |
|
<<"same_mam_id_for_peers">> => #option{type = boolean}}. |
98 |
|
|
99 |
|
muc_config_items() -> |
100 |
146 |
#{<<"host">> => #option{type = string, |
101 |
|
validate = subdomain_template, |
102 |
|
process = fun mongoose_subdomain_utils:make_subdomain_pattern/1}}. |
103 |
|
|
104 |
|
riak_config_spec() -> |
105 |
146 |
#section{ |
106 |
|
items = #{<<"search_index">> => #option{type = binary, |
107 |
|
validate = non_empty}, |
108 |
|
<<"bucket_type">> => #option{type = binary, |
109 |
|
validate = non_empty}}, |
110 |
|
wrap = none |
111 |
|
}. |
112 |
|
|
113 |
|
-spec deps(_Host :: jid:server(), Opts :: proplists:proplist()) -> |
114 |
|
gen_mod:deps_list(). |
115 |
|
deps(_Host, Opts0) -> |
116 |
101 |
Opts = normalize(Opts0), |
117 |
|
|
118 |
101 |
DepsWithPm = handle_nested_opts(pm, Opts, false, #{}), |
119 |
101 |
DepsWithPmAndMuc = handle_nested_opts(muc, Opts, false, DepsWithPm), |
120 |
|
|
121 |
101 |
[{Dep, Args, hard} || {Dep, Args} <- maps:to_list(DepsWithPmAndMuc)]. |
122 |
|
|
123 |
|
%%-------------------------------------------------------------------- |
124 |
|
%% Helpers |
125 |
|
%%-------------------------------------------------------------------- |
126 |
|
|
127 |
|
-spec handle_nested_opts(Key :: atom(), RootOpts :: proplists:proplist(), |
128 |
|
Default :: term(), deps()) -> deps(). |
129 |
|
handle_nested_opts(Key, RootOpts, Default, Deps) -> |
130 |
202 |
case proplists:get_value(Key, RootOpts, Default) of |
131 |
33 |
false -> Deps; |
132 |
|
Opts0 -> |
133 |
169 |
Opts = normalize(Opts0), |
134 |
169 |
FullOpts = lists:ukeymerge(1, Opts, RootOpts), |
135 |
169 |
parse_opts(Key, FullOpts, Deps) |
136 |
|
end. |
137 |
|
|
138 |
|
-spec parse_opts(Type :: pm | muc, Opts :: proplists:proplist(), deps()) -> deps(). |
139 |
|
parse_opts(Type, Opts, Deps) -> |
140 |
|
%% Opts are merged root options with options inside pm or muc section |
141 |
169 |
CoreMod = mam_type_to_core_mod(Type), |
142 |
169 |
CoreModOpts = filter_opts(Opts, valid_core_mod_opts(CoreMod)), |
143 |
169 |
WithCoreDeps = add_dep(CoreMod, CoreModOpts, Deps), |
144 |
169 |
Backend = proplists:get_value(backend, Opts, rdbms), |
145 |
169 |
parse_backend_opts(Backend, Type, Opts, WithCoreDeps). |
146 |
|
|
147 |
|
-spec mam_type_to_core_mod(atom()) -> module(). |
148 |
101 |
mam_type_to_core_mod(pm) -> mod_mam; |
149 |
68 |
mam_type_to_core_mod(muc) -> mod_mam_muc. |
150 |
|
|
151 |
|
filter_opts(Opts, ValidOpts) -> |
152 |
169 |
lists:filtermap( |
153 |
|
fun(Key) -> |
154 |
2061 |
case proplists:lookup(Key, Opts) of |
155 |
1915 |
none -> false; |
156 |
146 |
Opt -> {true, Opt} |
157 |
|
end |
158 |
|
end, ValidOpts). |
159 |
|
|
160 |
|
%% Get a list of options to pass into the two modules. |
161 |
|
%% They don't required to be defined in pm or muc sections, |
162 |
|
%% the root section is enough. |
163 |
|
-spec valid_core_mod_opts(module()) -> [atom()]. |
164 |
|
valid_core_mod_opts(mod_mam) -> |
165 |
101 |
[no_stanzaid_element, archive_groupchats, same_mam_id_for_peers] ++ common_opts(); |
166 |
|
valid_core_mod_opts(mod_mam_muc) -> |
167 |
68 |
[host] ++ common_opts(). |
168 |
|
|
169 |
|
common_opts() -> |
170 |
169 |
[async_writer, |
171 |
|
is_archivable_message, |
172 |
|
send_message, |
173 |
|
archive_chat_markers, |
174 |
|
extra_fin_element, |
175 |
|
extra_lookup_params, |
176 |
|
full_text_search, |
177 |
|
message_retraction, |
178 |
|
default_result_limit, |
179 |
|
max_result_limit]. |
180 |
|
|
181 |
|
-spec parse_backend_opts(rdbms | cassandra | riak | elasticsearch, Type :: pm | muc, |
182 |
|
Opts :: proplists:proplist(), deps()) -> deps(). |
183 |
|
parse_backend_opts(cassandra, Type, Opts, Deps0) -> |
184 |
:-( |
ModArch = |
185 |
|
case Type of |
186 |
:-( |
pm -> mod_mam_cassandra_arch; |
187 |
:-( |
muc -> mod_mam_muc_cassandra_arch |
188 |
|
end, |
189 |
|
|
190 |
:-( |
Opts1 = filter_opts(Opts, [db_message_format, pool_name, simple]), |
191 |
:-( |
Deps = add_dep(ModArch, Opts1, Deps0), |
192 |
|
|
193 |
:-( |
case proplists:get_value(user_prefs_store, Opts, false) of |
194 |
:-( |
cassandra -> add_dep(mod_mam_cassandra_prefs, [Type], Deps); |
195 |
:-( |
mnesia -> add_dep(mod_mam_mnesia_prefs, [Type], Deps); |
196 |
:-( |
_ -> Deps |
197 |
|
end; |
198 |
|
|
199 |
|
parse_backend_opts(riak, Type, Opts, Deps0) -> |
200 |
:-( |
Opts1 = filter_opts(Opts, [db_message_format, search_index, bucket_type]), |
201 |
:-( |
Deps = add_dep(mod_mam_riak_timed_arch_yz, [Type | Opts1], Deps0), |
202 |
|
|
203 |
:-( |
case proplists:get_value(user_prefs_store, Opts, false) of |
204 |
:-( |
mnesia -> add_dep(mod_mam_mnesia_prefs, [Type], Deps); |
205 |
:-( |
_ -> Deps |
206 |
|
end; |
207 |
|
|
208 |
|
parse_backend_opts(rdbms, Type, Opts0, Deps0) -> |
209 |
169 |
Opts1 = add_rdbms_async_opts(Opts0), |
210 |
169 |
Opts = add_rdbms_cache_opts(Opts1), |
211 |
|
|
212 |
169 |
{ModRDBMSArch, ModAsyncWriter} = |
213 |
|
case Type of |
214 |
101 |
pm -> {mod_mam_rdbms_arch, mod_mam_rdbms_arch_async}; |
215 |
68 |
muc -> {mod_mam_muc_rdbms_arch, mod_mam_rdbms_arch_async} |
216 |
|
end, |
217 |
|
|
218 |
169 |
Deps1 = add_dep(ModRDBMSArch, [Type], Deps0), |
219 |
169 |
Deps = add_dep(mod_mam_rdbms_user, user_db_types(Type), Deps1), |
220 |
|
|
221 |
169 |
lists:foldl( |
222 |
|
pa:bind(fun parse_rdbms_opt/5, Type, ModRDBMSArch, ModAsyncWriter), |
223 |
|
Deps, Opts); |
224 |
|
|
225 |
|
parse_backend_opts(elasticsearch, Type, Opts, Deps0) -> |
226 |
:-( |
ModArch = |
227 |
|
case Type of |
228 |
:-( |
pm -> mod_mam_elasticsearch_arch; |
229 |
:-( |
muc -> mod_mam_muc_elasticsearch_arch |
230 |
|
end, |
231 |
|
|
232 |
:-( |
Deps = add_dep(ModArch, Deps0), |
233 |
|
|
234 |
:-( |
case proplists:get_value(user_prefs_store, Opts, false) of |
235 |
:-( |
mnesia -> add_dep(mod_mam_mnesia_prefs, [Type], Deps); |
236 |
:-( |
_ -> Deps |
237 |
|
end. |
238 |
|
|
239 |
|
% muc backend requires both pm and muc user DB to populate sender_id column |
240 |
|
-spec user_db_types(pm | muc) -> [pm | muc]. |
241 |
101 |
user_db_types(pm) -> [pm]; |
242 |
68 |
user_db_types(muc) -> [pm, muc]. |
243 |
|
|
244 |
|
-spec normalize(proplists:proplist()) -> [{atom(), term()}]. |
245 |
|
normalize(Opts) -> |
246 |
270 |
lists:ukeysort(1, proplists:unfold(Opts)). |
247 |
|
|
248 |
|
|
249 |
|
-spec add_dep(Dep :: module(), deps()) -> deps(). |
250 |
|
add_dep(Dep, Deps) -> |
251 |
:-( |
add_dep(Dep, [], Deps). |
252 |
|
|
253 |
|
|
254 |
|
-spec add_dep(Dep :: module(), Args :: proplists:proplist(), deps()) -> deps(). |
255 |
|
add_dep(Dep, Args, Deps) -> |
256 |
1024 |
PrevArgs = maps:get(Dep, Deps, []), |
257 |
1024 |
NewArgs = lists:usort(Args ++ PrevArgs), |
258 |
1024 |
maps:put(Dep, NewArgs, Deps). |
259 |
|
|
260 |
|
|
261 |
|
-spec add_rdbms_async_opts(proplists:proplist()) -> proplists:proplist(). |
262 |
|
add_rdbms_async_opts(Opts) -> |
263 |
169 |
case lists:keyfind(async_writer, 1, Opts) of |
264 |
|
{async_writer, AsyncOpts} -> |
265 |
:-( |
case lists:keyfind(enabled, 1, AsyncOpts) of |
266 |
:-( |
{enabled, false} -> lists:keydelete(async_writer, 1, Opts); |
267 |
:-( |
_ -> Opts |
268 |
|
end; |
269 |
|
false -> |
270 |
169 |
[{async_writer, []} | Opts] |
271 |
|
end. |
272 |
|
|
273 |
|
|
274 |
|
add_rdbms_cache_opts(Opts) -> |
275 |
169 |
case {lists:keyfind(cache_users, 1, Opts), lists:keyfind(cache, 1, Opts)} of |
276 |
|
{{cache_users, false}, _} -> |
277 |
:-( |
lists:keydelete(cache, 1, Opts); |
278 |
|
{{cache_users, true}, false} -> |
279 |
:-( |
[{cache, []} | Opts]; |
280 |
|
{false, false} -> |
281 |
169 |
[{cache, []} | Opts]; |
282 |
|
{false, {cache, _}} -> |
283 |
:-( |
Opts |
284 |
|
end. |
285 |
|
|
286 |
|
-spec parse_rdbms_opt(Type :: pm | muc, module(), module(), |
287 |
|
Option :: {module(), term()}, deps()) -> deps(). |
288 |
|
parse_rdbms_opt(Type, ModRDBMSArch, ModAsyncWriter, Option, Deps) -> |
289 |
978 |
case Option of |
290 |
|
{cache, CacheOpts} -> |
291 |
169 |
Deps1 = case gen_mod:get_opt(module, CacheOpts, internal) of |
292 |
169 |
internal -> Deps; |
293 |
:-( |
mod_cache_users -> add_dep(mod_cache_users, Deps) |
294 |
|
end, |
295 |
169 |
add_dep(mod_mam_cache_user, [Type | CacheOpts], Deps1); |
296 |
|
{user_prefs_store, rdbms} -> |
297 |
:-( |
add_dep(mod_mam_rdbms_prefs, [Type], Deps); |
298 |
|
{user_prefs_store, mnesia} -> |
299 |
:-( |
add_dep(mod_mam_mnesia_prefs, [Type], Deps); |
300 |
|
{rdbms_message_format, simple} -> |
301 |
10 |
add_dep(ModRDBMSArch, rdbms_simple_opts(), Deps); |
302 |
|
{async_writer, Opts} -> |
303 |
169 |
DepsWithNoWriter = add_dep(ModRDBMSArch, [no_writer], Deps), |
304 |
169 |
add_dep(ModAsyncWriter, [{Type, Opts}], DepsWithNoWriter); |
305 |
630 |
_ -> Deps |
306 |
|
end. |
307 |
|
|
308 |
|
-spec rdbms_simple_opts() -> list(). |
309 |
10 |
rdbms_simple_opts() -> [{db_jid_format, mam_jid_rfc}, {db_message_format, mam_message_xml}]. |
310 |
|
|
311 |
|
config_metrics(Host) -> |
312 |
:-( |
OptsToReport = [{backend, rdbms}], %list of tuples {option, defualt_value} |
313 |
:-( |
mongoose_module_metrics:opts_for_module(Host, ?MODULE, OptsToReport). |