1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : mod_vcard.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : Vcard management in Mnesia |
5 |
|
%%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@process-one.net> |
6 |
|
%%% |
7 |
|
%%% Store vCards in mnesia to provide "XEP-0054: vcard-temp" |
8 |
|
%%% and "XEP-0055: Jabber Search" |
9 |
|
%%% |
10 |
|
%%% Most of this is now using binaries. The search fields l* in vcard_search |
11 |
|
%%% are still stored as lists to allow string prefix search using the match |
12 |
|
%%% spec with a trailing element String ++ '_'. |
13 |
|
%%% |
14 |
|
%%%---------------------------------------------------------------------- |
15 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
16 |
|
%%% |
17 |
|
%%% This program is free software; you can redistribute it and/or |
18 |
|
%%% modify it under the terms of the GNU General Public License as |
19 |
|
%%% published by the Free Software Foundation; either version 2 of the |
20 |
|
%%% License, or (at your option) any later version. |
21 |
|
%%% |
22 |
|
%%% This program is distributed in the hope that it will be useful, |
23 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
24 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
25 |
|
%%% General Public License for more details. |
26 |
|
%%% |
27 |
|
%%% You should have received a copy of the GNU General Public License |
28 |
|
%%% along with this program; if not, write to the Free Software |
29 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
30 |
|
%%% |
31 |
|
%%%---------------------------------------------------------------------- |
32 |
|
|
33 |
|
-module(mod_vcard). |
34 |
|
-author('alexey@process-one.net'). |
35 |
|
-xep([{xep, 54}, {version, "1.2"}]). |
36 |
|
-xep([{xep, 55}, {version, "1.3"}]). |
37 |
|
-behaviour(gen_mod). |
38 |
|
-behaviour(gen_server). |
39 |
|
-behaviour(mongoose_module_metrics). |
40 |
|
|
41 |
|
-include("mongoose.hrl"). |
42 |
|
-include("jlib.hrl"). |
43 |
|
-include("mod_vcard.hrl"). |
44 |
|
-include("mongoose_rsm.hrl"). |
45 |
|
-include("mongoose_config_spec.hrl"). |
46 |
|
|
47 |
|
%% gen_mod handlers |
48 |
|
-export([start/2, stop/1, |
49 |
|
supported_features/0]). |
50 |
|
|
51 |
|
%% config_spec |
52 |
|
-export([config_spec/0, |
53 |
|
process_map_spec/1, |
54 |
|
process_search_spec/1, |
55 |
|
process_search_reported_spec/1]). |
56 |
|
|
57 |
|
%% gen_server handlers |
58 |
|
-export([init/1, |
59 |
|
handle_info/2, |
60 |
|
handle_call/3, |
61 |
|
handle_cast/2, |
62 |
|
terminate/2, |
63 |
|
code_change/3]). |
64 |
|
|
65 |
|
%% mongoose_packet_handler export |
66 |
|
-export([process_packet/5]). |
67 |
|
|
68 |
|
%% Hook handlers |
69 |
|
-export([process_local_iq/5, |
70 |
|
process_sm_iq/5, |
71 |
|
remove_user/3, |
72 |
|
remove_domain/3, |
73 |
|
set_vcard/4]). |
74 |
|
|
75 |
|
-export([start_link/2]). |
76 |
|
-export([default_search_fields/0]). |
77 |
|
-export([get_results_limit/1]). |
78 |
|
-export([get_default_reported_fields/1]). |
79 |
|
-export([default_host/0]). |
80 |
|
|
81 |
|
%% GDPR related |
82 |
|
-export([get_personal_data/3]). |
83 |
|
|
84 |
|
-export([config_metrics/1]). |
85 |
|
|
86 |
|
-ignore_xref([ |
87 |
|
behaviour_info/1, process_packet/5, |
88 |
|
get_personal_data/3, remove_user/3, remove_domain/3, set_vcard/4, start_link/2 |
89 |
|
]). |
90 |
|
|
91 |
|
-define(PROCNAME, ejabberd_mod_vcard). |
92 |
|
|
93 |
|
-record(state, {search :: boolean(), |
94 |
|
host_type :: mongooseim:host_type()}). |
95 |
|
|
96 |
|
-type error() :: error | {error, any()}. |
97 |
|
|
98 |
|
%%-------------------------------------------------------------------- |
99 |
|
%% gdpr callback |
100 |
|
%%-------------------------------------------------------------------- |
101 |
|
|
102 |
|
-spec get_personal_data(gdpr:personal_data(), mongooseim:host_type(), jid:jid()) -> gdpr:personal_data(). |
103 |
|
get_personal_data(Acc, HostType, #jid{luser = LUser, lserver = LServer}) -> |
104 |
35 |
Jid = jid:to_binary({LUser, LServer}), |
105 |
35 |
Schema = ["jid", "vcard"], |
106 |
35 |
Entries = case mod_vcard_backend:get_vcard(HostType, LUser, LServer) of |
107 |
|
{ok, Record} -> |
108 |
1 |
SerializedRecords = exml:to_binary(Record), |
109 |
1 |
[{Jid, SerializedRecords}]; |
110 |
34 |
_ -> [] |
111 |
|
end, |
112 |
35 |
[{vcard, Schema, Entries} | Acc]. |
113 |
|
|
114 |
|
-spec default_search_fields() -> list(). |
115 |
|
default_search_fields() -> |
116 |
2 |
[{<<"User">>, <<"user">>}, |
117 |
|
{<<"Full Name">>, <<"fn">>}, |
118 |
|
{<<"Given Name">>, <<"first">>}, |
119 |
|
{<<"Middle Name">>, <<"middle">>}, |
120 |
|
{<<"Family Name">>, <<"last">>}, |
121 |
|
{<<"Nickname">>, <<"nick">>}, |
122 |
|
{<<"Birthday">>, <<"bday">>}, |
123 |
|
{<<"Country">>, <<"ctry">>}, |
124 |
|
{<<"City">>, <<"locality">>}, |
125 |
|
{<<"Email">>, <<"email">>}, |
126 |
|
{<<"Organization Name">>, <<"orgname">>}, |
127 |
|
{<<"Organization Unit">>, <<"orgunit">>}]. |
128 |
|
|
129 |
|
-spec get_results_limit(mongooseim:host_type()) -> non_neg_integer() | infinity. |
130 |
|
get_results_limit(HostType) -> |
131 |
17 |
case gen_mod:get_module_opt(HostType, mod_vcard, matches, ?JUD_MATCHES) of |
132 |
|
infinity -> |
133 |
:-( |
infinity; |
134 |
|
Val when is_integer(Val) and (Val > 0) -> |
135 |
17 |
Val; |
136 |
|
Val -> |
137 |
:-( |
?LOG_ERROR(#{what => illegal_option_error, value => {matches, Val}, |
138 |
:-( |
default_value => ?JUD_MATCHES}), |
139 |
:-( |
?JUD_MATCHES |
140 |
|
end. |
141 |
|
|
142 |
|
-spec default_host() -> mongoose_subdomain_utils:subdomain_pattern(). |
143 |
|
default_host() -> |
144 |
646 |
mongoose_subdomain_utils:make_subdomain_pattern(<<"vjud.@HOST@">>). |
145 |
|
|
146 |
|
%%-------------------------------------------------------------------- |
147 |
|
%% gen_mod callbacks |
148 |
|
%%-------------------------------------------------------------------- |
149 |
|
|
150 |
|
start(HostType, Opts) -> |
151 |
325 |
mod_vcard_backend:init(HostType, Opts), |
152 |
325 |
start_hooks(HostType), |
153 |
325 |
start_iq_handlers(HostType, Opts), |
154 |
325 |
Proc = gen_mod:get_module_proc(HostType, ?PROCNAME), |
155 |
325 |
ChildSpec = {Proc, {?MODULE, start_link, [HostType, Opts]}, |
156 |
|
transient, 1000, worker, [?MODULE]}, |
157 |
325 |
ejabberd_sup:start_child(ChildSpec). |
158 |
|
|
159 |
|
-spec stop(mongooseim:host_type()) -> ok. |
160 |
|
stop(HostType) -> |
161 |
325 |
Proc = gen_mod:get_module_proc(HostType, ?PROCNAME), |
162 |
325 |
stop_hooks(HostType), |
163 |
325 |
stop_iq_handlers(HostType), |
164 |
325 |
stop_backend(HostType), |
165 |
325 |
gen_server:call(Proc, stop), |
166 |
325 |
ejabberd_sup:stop_child(Proc), |
167 |
325 |
ok. |
168 |
|
|
169 |
142 |
supported_features() -> [dynamic_domains]. |
170 |
|
|
171 |
|
start_hooks(HostType) -> |
172 |
325 |
ejabberd_hooks:add(hooks(HostType)). |
173 |
|
|
174 |
|
stop_hooks(HostType) -> |
175 |
325 |
ejabberd_hooks:delete(hooks(HostType)). |
176 |
|
|
177 |
|
hooks(HostType) -> |
178 |
650 |
[{Hook, HostType, ?MODULE, Function, Priority} |
179 |
650 |
|| {Hook, Function, Priority} <- hooks2()]. |
180 |
|
|
181 |
|
hooks2() -> |
182 |
650 |
[{remove_user, remove_user, 50}, |
183 |
|
{anonymous_purge_hook, remove_user, 50}, |
184 |
|
{remove_domain, remove_domain, 50}, |
185 |
|
{set_vcard, set_vcard, 50}, |
186 |
|
{get_personal_data, get_personal_data, 50}]. |
187 |
|
|
188 |
|
start_iq_handlers(HostType, Opts) -> |
189 |
325 |
IQDisc = gen_mod:get_opt(iqdisc, Opts, parallel), |
190 |
325 |
gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_sm, |
191 |
|
fun ?MODULE:process_sm_iq/5, #{}, IQDisc), |
192 |
325 |
gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_local, |
193 |
|
fun ?MODULE:process_local_iq/5, #{}, IQDisc). |
194 |
|
|
195 |
|
stop_iq_handlers(HostType) -> |
196 |
325 |
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_local), |
197 |
325 |
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_VCARD, ejabberd_sm). |
198 |
|
|
199 |
|
stop_backend(HostType) -> |
200 |
325 |
mod_vcard_backend:tear_down(HostType). |
201 |
|
|
202 |
|
%% Domain registration |
203 |
|
maybe_register_search(false, _HostType, _Opts) -> |
204 |
2 |
ok; |
205 |
|
maybe_register_search(true, HostType, Opts) -> |
206 |
323 |
SubdomainPattern = gen_mod:get_opt(host, Opts, default_host()), |
207 |
323 |
PacketHandler = mongoose_packet_handler:new(?MODULE, #{pid => self()}), |
208 |
|
%% Always register, even if search functionality is disabled. |
209 |
|
%% So, we can send 503 error, instead of 404 error. |
210 |
323 |
mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler). |
211 |
|
|
212 |
|
maybe_unregister_search(false, _HostType) -> |
213 |
2 |
ok; |
214 |
|
maybe_unregister_search(true, HostType) -> |
215 |
323 |
SubdomainPattern = gen_mod:get_module_opt(HostType, ?MODULE, host, default_host()), |
216 |
323 |
mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern). |
217 |
|
|
218 |
|
%%-------------------------------------------------------------------- |
219 |
|
%% config_spec |
220 |
|
%%-------------------------------------------------------------------- |
221 |
|
|
222 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
223 |
|
config_spec() -> |
224 |
160 |
#section{ |
225 |
|
items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(), |
226 |
|
<<"host">> => #option{type = string, |
227 |
|
validate = subdomain_template, |
228 |
|
process = fun mongoose_subdomain_utils:make_subdomain_pattern/1}, |
229 |
|
<<"search">> => #option{type = boolean}, |
230 |
|
<<"backend">> => #option{type = atom, |
231 |
|
validate = {module, mod_vcard}}, |
232 |
|
<<"matches">> => #option{type = int_or_infinity, |
233 |
|
validate = non_negative}, |
234 |
|
<<"ldap_pool_tag">> => #option{type = atom, |
235 |
|
validate = pool_name}, |
236 |
|
<<"ldap_base">> => #option{type = string}, |
237 |
|
<<"ldap_uids">> => #list{items = mongoose_ldap_config:uids()}, |
238 |
|
<<"ldap_filter">> => #option{type = binary}, |
239 |
|
<<"ldap_deref">> => #option{type = atom, |
240 |
|
validate = {enum, [never, always, finding, searching]}}, |
241 |
|
<<"ldap_vcard_map">> => #list{items = ldap_vcard_map_spec()}, |
242 |
|
<<"ldap_search_fields">> => #list{items = ldap_search_fields_spec()}, |
243 |
|
<<"ldap_search_reported">> => #list{items = ldap_search_reported_spec()}, |
244 |
|
<<"ldap_search_operator">> => #option{type = atom, |
245 |
|
validate = {enum, ['or', 'and']}}, |
246 |
|
<<"ldap_binary_search_fields">> => #list{items = #option{type = binary, |
247 |
|
validate = non_empty}}, |
248 |
|
<<"riak">> => riak_config_spec() |
249 |
|
} |
250 |
|
}. |
251 |
|
|
252 |
|
ldap_vcard_map_spec() -> |
253 |
160 |
#section{ |
254 |
|
items = #{<<"vcard_field">> => #option{type = binary, |
255 |
|
validate = non_empty}, |
256 |
|
<<"ldap_pattern">> => #option{type = binary, |
257 |
|
validate = non_empty}, |
258 |
|
<<"ldap_field">> => #option{type = binary, |
259 |
|
validate = non_empty} |
260 |
|
}, |
261 |
|
required = all, |
262 |
|
process = fun ?MODULE:process_map_spec/1 |
263 |
|
}. |
264 |
|
|
265 |
|
ldap_search_fields_spec() -> |
266 |
160 |
#section{ |
267 |
|
items = #{<<"search_field">> => #option{type = binary, |
268 |
|
validate = non_empty}, |
269 |
|
<<"ldap_field">> => #option{type = binary, |
270 |
|
validate = non_empty} |
271 |
|
}, |
272 |
|
required = all, |
273 |
|
process = fun ?MODULE:process_search_spec/1 |
274 |
|
}. |
275 |
|
|
276 |
|
ldap_search_reported_spec() -> |
277 |
160 |
#section{ |
278 |
|
items = #{<<"search_field">> => #option{type = binary, |
279 |
|
validate = non_empty}, |
280 |
|
<<"vcard_field">> => #option{type = binary, |
281 |
|
validate = non_empty} |
282 |
|
}, |
283 |
|
required = all, |
284 |
|
process = fun ?MODULE:process_search_reported_spec/1 |
285 |
|
}. |
286 |
|
|
287 |
|
riak_config_spec() -> |
288 |
160 |
#section{ |
289 |
|
items = #{<<"bucket_type">> => #option{type = binary, |
290 |
|
validate = non_empty}, |
291 |
|
<<"search_index">> => #option{type = binary, |
292 |
|
validate = non_empty} |
293 |
|
}, |
294 |
|
wrap = none |
295 |
|
}. |
296 |
|
|
297 |
|
process_map_spec(KVs) -> |
298 |
:-( |
{[[{vcard_field, VF}], [{ldap_pattern, LP}], [{ldap_field, LF}]], []} = |
299 |
|
proplists:split(KVs, [vcard_field, ldap_pattern, ldap_field]), |
300 |
:-( |
{VF, LP, [LF]}. |
301 |
|
|
302 |
|
process_search_spec(KVs) -> |
303 |
:-( |
{[[{search_field, SF}], [{ldap_field, LF}]], []} = |
304 |
|
proplists:split(KVs, [search_field, ldap_field]), |
305 |
:-( |
{SF, LF}. |
306 |
|
|
307 |
|
process_search_reported_spec(KVs) -> |
308 |
:-( |
{[[{search_field, SF}], [{vcard_field, VF}]], []} = |
309 |
|
proplists:split(KVs, [search_field, vcard_field]), |
310 |
:-( |
{SF, VF}. |
311 |
|
|
312 |
|
%%-------------------------------------------------------------------- |
313 |
|
%% mongoose_packet_handler callbacks for search |
314 |
|
%%-------------------------------------------------------------------- |
315 |
|
|
316 |
|
-spec process_packet(Acc :: mongoose_acc:t(), From ::jid:jid(), To ::jid:jid(), |
317 |
|
Packet :: exml:element(), #{}) -> mongoose_acc:t(). |
318 |
|
process_packet(Acc, From, To, _Packet, _Extra) -> |
319 |
23 |
handle_route(Acc, From, To), |
320 |
23 |
Acc. |
321 |
|
|
322 |
|
handle_route(Acc, From, To) -> |
323 |
23 |
HostType = mongoose_acc:host_type(Acc), |
324 |
23 |
{IQ, Acc1} = mongoose_iq:info(Acc), |
325 |
23 |
LServer = directory_jid_to_server_host(To), |
326 |
23 |
try do_route(HostType, LServer, From, To, Acc1, IQ) |
327 |
|
catch |
328 |
|
Class:Reason:Stacktrace -> |
329 |
:-( |
?LOG_ERROR(#{what => vcard_route_failed, acc => Acc, |
330 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}) |
331 |
|
end. |
332 |
|
|
333 |
|
%%-------------------------------------------------------------------- |
334 |
|
%% gen_server callbacks for search |
335 |
|
%%-------------------------------------------------------------------- |
336 |
|
start_link(HostType, Opts) -> |
337 |
325 |
Proc = gen_mod:get_module_proc(HostType, ?PROCNAME), |
338 |
325 |
gen_server:start_link({local, Proc}, ?MODULE, [HostType, Opts], []). |
339 |
|
|
340 |
|
init([HostType, Opts]) -> |
341 |
325 |
process_flag(trap_exit, true), |
342 |
325 |
Search = gen_mod:get_opt(search, Opts, true), |
343 |
325 |
maybe_register_search(Search, HostType, Opts), |
344 |
325 |
{ok, #state{host_type = HostType, search = Search}}. |
345 |
|
|
346 |
|
handle_call(get_state, _From, State) -> |
347 |
:-( |
{reply, {ok, State}, State}; |
348 |
|
handle_call(stop, _From, State) -> |
349 |
325 |
{stop, normal, ok, State}; |
350 |
|
handle_call(_Request, _From, State) -> |
351 |
:-( |
{reply, bad_request, State}. |
352 |
|
|
353 |
|
handle_info(_, State) -> |
354 |
:-( |
{noreply, State}. |
355 |
|
|
356 |
|
handle_cast(_Request, State) -> |
357 |
:-( |
{noreply, State}. |
358 |
|
|
359 |
|
code_change(_OldVsn, State, _Extra) -> |
360 |
:-( |
{ok, State}. |
361 |
|
|
362 |
|
terminate(_Reason, #state{host_type = HostType, search = Search}) -> |
363 |
325 |
maybe_unregister_search(Search, HostType). |
364 |
|
|
365 |
|
%%-------------------------------------------------------------------- |
366 |
|
%% Hook handlers |
367 |
|
%%-------------------------------------------------------------------- |
368 |
|
process_local_iq(Acc, _From, _To, IQ = #iq{type = set, sub_el = SubEl}, _Extra) -> |
369 |
:-( |
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}}; |
370 |
|
process_local_iq(Acc, _From, _To, IQ = #iq{type = get}, _Extra) -> |
371 |
1 |
DescCData = #xmlcdata{content = [<<"MongooseIM XMPP Server">>, |
372 |
|
<<"\nCopyright (c) Erlang Solutions Ltd.">>]}, |
373 |
1 |
{Acc, IQ#iq{type = result, |
374 |
|
sub_el = [#xmlel{name = <<"vCard">>, attrs = [{<<"xmlns">>, ?NS_VCARD}], |
375 |
|
children = [#xmlel{name = <<"FN">>, |
376 |
|
children = [#xmlcdata{content = <<"MongooseIM">>}]}, |
377 |
|
#xmlel{name = <<"URL">>, |
378 |
|
children = [#xmlcdata{content = ?MONGOOSE_URI}]}, |
379 |
|
#xmlel{name = <<"DESC">>, |
380 |
|
children = [DescCData]} |
381 |
|
]}]}}. |
382 |
|
|
383 |
|
-spec process_sm_iq(Acc :: mongoose_acc:t(), |
384 |
|
From :: jid:jid(), |
385 |
|
To :: jid:jid(), |
386 |
|
IQ :: jlib:iq(), |
387 |
|
Extra :: map()) -> |
388 |
|
{stop, mongoose_acc:t()} | {mongoose_acc:t(), jlib:iq()}. |
389 |
|
process_sm_iq(Acc, From, To, IQ = #iq{type = set, sub_el = VCARD}, _Extra) -> |
390 |
28 |
HostType = mongoose_acc:host_type(Acc), |
391 |
28 |
process_sm_iq_set(HostType, From, To, Acc, IQ, VCARD); |
392 |
|
process_sm_iq(Acc, From, To, IQ = #iq{type = get, sub_el = VCARD}, _Extra) -> |
393 |
25 |
HostType = mongoose_acc:host_type(Acc), |
394 |
25 |
process_sm_iq_get(HostType, From, To, Acc, IQ, VCARD). |
395 |
|
|
396 |
|
process_sm_iq_set(HostType, From, To, Acc, IQ, VCARD) -> |
397 |
28 |
#jid{user = FromUser, lserver = FromVHost} = From, |
398 |
28 |
#jid{user = ToUser, lserver = ToVHost, resource = ToResource} = To, |
399 |
28 |
Local = ((FromUser == ToUser) andalso (FromVHost == ToVHost) andalso (ToResource == <<>>)) |
400 |
2 |
orelse ((ToUser == <<>>) andalso (ToVHost == <<>>)), |
401 |
28 |
Res = case Local of |
402 |
|
true -> |
403 |
26 |
try unsafe_set_vcard(HostType, From, VCARD) of |
404 |
|
ok -> |
405 |
24 |
IQ#iq{type = result, sub_el = []}; |
406 |
|
{error, {invalid_input, {Field, Value}}} -> |
407 |
2 |
?LOG_WARNING(#{what => vcard_sm_iq_set_failed, value => Value, |
408 |
:-( |
reason => invalid_input, field => Field, acc => Acc}), |
409 |
2 |
Text = io_lib:format("Invalid input for vcard field ~s", [Field]), |
410 |
2 |
ReasonEl = mongoose_xmpp_errors:bad_request(<<"en">>, erlang:iolist_to_binary(Text)), |
411 |
2 |
vcard_error(IQ, ReasonEl); |
412 |
|
{error, Reason} -> |
413 |
:-( |
?LOG_WARNING(#{what => vcard_sm_iq_set_failed, |
414 |
:-( |
reason => Reason, acc => Acc}), |
415 |
:-( |
vcard_error(IQ, mongoose_xmpp_errors:unexpected_request_cancel()) |
416 |
|
catch |
417 |
|
E:R:Stack -> |
418 |
:-( |
?LOG_ERROR(#{what => vcard_sm_iq_set_failed, |
419 |
:-( |
class => E, reason => R, stacktrace => Stack, acc => Acc}), |
420 |
:-( |
vcard_error(IQ, mongoose_xmpp_errors:internal_server_error()) |
421 |
|
end; |
422 |
|
_ -> |
423 |
2 |
?LOG_WARNING(#{what => vcard_sm_iq_get_failed, |
424 |
:-( |
reason => not_allowed, acc => Acc}), |
425 |
2 |
vcard_error(IQ, mongoose_xmpp_errors:not_allowed()) |
426 |
|
end, |
427 |
28 |
{Acc, Res}. |
428 |
|
|
429 |
|
process_sm_iq_get(HostType, _From, To, Acc, IQ, SubEl) -> |
430 |
25 |
#jid{luser = LUser, lserver = LServer} = To, |
431 |
25 |
Res = try mod_vcard_backend:get_vcard(HostType, LUser, LServer) of |
432 |
|
{ok, VCARD} -> |
433 |
21 |
IQ#iq{type = result, sub_el = VCARD}; |
434 |
|
{error, Reason} -> |
435 |
4 |
IQ#iq{type = error, sub_el = [SubEl, Reason]} |
436 |
|
catch E:R:Stack -> |
437 |
:-( |
?LOG_ERROR(#{what => vcard_sm_iq_get_failed, |
438 |
:-( |
class => E, reason => R, stacktrace => Stack, acc => Acc}), |
439 |
:-( |
vcard_error(IQ, mongoose_xmpp_errors:internal_server_error()) |
440 |
|
end, |
441 |
25 |
{Acc, Res}. |
442 |
|
|
443 |
|
unsafe_set_vcard(HostType, From, VCARD) -> |
444 |
26 |
#jid{user = FromUser, lserver = FromVHost} = From, |
445 |
26 |
case parse_vcard(FromUser, FromVHost, VCARD) of |
446 |
|
{ok, VcardSearch} -> |
447 |
24 |
mod_vcard_backend:set_vcard(HostType, FromUser, FromVHost, VCARD, VcardSearch); |
448 |
|
{error, Reason} -> |
449 |
2 |
{error, Reason} |
450 |
|
end. |
451 |
|
|
452 |
|
|
453 |
|
-spec set_vcard(HandlerAcc, HostType, From, VCARD) -> Result when |
454 |
|
HandlerAcc :: ok | error(), |
455 |
|
HostType :: mongooseim:host_type(), |
456 |
|
From ::jid:jid(), |
457 |
|
VCARD :: exml:element(), |
458 |
|
Result :: ok | error(). |
459 |
|
set_vcard(ok, _HostType, _From, _VCARD) -> |
460 |
:-( |
?LOG_DEBUG(#{what => hook_call_already_handled}), |
461 |
:-( |
ok; |
462 |
|
set_vcard({error, no_handler_defined}, HostType, From, VCARD) -> |
463 |
:-( |
try unsafe_set_vcard(HostType, From, VCARD) of |
464 |
:-( |
ok -> ok; |
465 |
|
{error, Reason} -> |
466 |
:-( |
?LOG_ERROR(#{what => unsafe_set_vcard_failed, reason => Reason}), |
467 |
:-( |
{error, Reason} |
468 |
|
catch |
469 |
:-( |
E:R:S -> ?LOG_ERROR(#{what => unsafe_set_vcard_failed, class => E, |
470 |
:-( |
reason => R, stacktrace => S}), |
471 |
:-( |
{error, {E, R}} |
472 |
|
end; |
473 |
:-( |
set_vcard({error, _} = E, _HostType, _From, _VCARD) -> E. |
474 |
|
|
475 |
|
-spec remove_domain(mongoose_hooks:simple_acc(), |
476 |
|
mongooseim:host_type(), jid:lserver()) -> |
477 |
|
mongoose_hooks:simple_acc(). |
478 |
|
remove_domain(Acc, HostType, Domain) -> |
479 |
:-( |
mod_vcard_backend:remove_domain(HostType, Domain), |
480 |
:-( |
Acc. |
481 |
|
|
482 |
|
%% #rh |
483 |
|
remove_user(Acc, User, Server) -> |
484 |
2298 |
HostType = mongoose_acc:host_type(Acc), |
485 |
2298 |
LUser = jid:nodeprep(User), |
486 |
2298 |
LServer = jid:nodeprep(Server), |
487 |
2298 |
mod_vcard_backend:remove_user(HostType, LUser, LServer), |
488 |
2298 |
Acc. |
489 |
|
|
490 |
|
%% ------------------------------------------------------------------ |
491 |
|
%% Internal |
492 |
|
%% ------------------------------------------------------------------ |
493 |
|
do_route(_HostType, _LServer, From, |
494 |
|
#jid{user = User, resource = Resource} = To, Acc, _IQ) |
495 |
|
when (User /= <<>>) or (Resource /= <<>>) -> |
496 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:service_unavailable()), |
497 |
:-( |
ejabberd_router:route(To, From, Acc1, Err); |
498 |
|
do_route(HostType, LServer, From, To, Acc, |
499 |
|
#iq{type = set, xmlns = ?NS_SEARCH, lang = Lang, sub_el = SubEl} = IQ) -> |
500 |
19 |
route_search_iq_set(HostType, LServer, From, To, Acc, Lang, SubEl, IQ); |
501 |
|
do_route(HostType, LServer, From, To, Acc, |
502 |
|
#iq{type = get, xmlns = ?NS_SEARCH, lang = Lang} = IQ) -> |
503 |
2 |
Form = ?FORM(To, mod_vcard_backend:search_fields(HostType, LServer), Lang), |
504 |
2 |
ResIQ = make_search_form_result_iq(IQ, Form), |
505 |
2 |
ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)); |
506 |
|
do_route(_HostType, _LServer, From, To, Acc, |
507 |
|
#iq{type = set, xmlns = ?NS_DISCO_INFO}) -> |
508 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:not_allowed()), |
509 |
:-( |
ejabberd_router:route(To, From, Acc1, Err); |
510 |
|
do_route(HostType, _LServer, From, To, Acc, |
511 |
|
#iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ) -> |
512 |
1 |
IdentityXML = mongoose_disco:identities_to_xml([identity(Lang)]), |
513 |
1 |
FeatureXML = mongoose_disco:features_to_xml(features()), |
514 |
1 |
InfoXML = mongoose_disco:get_info(HostType, ?MODULE, <<>>, <<>>), |
515 |
1 |
ResIQ = IQ#iq{type = result, |
516 |
|
sub_el = [#xmlel{name = <<"query">>, |
517 |
|
attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], |
518 |
|
children = IdentityXML ++ FeatureXML ++ InfoXML}]}, |
519 |
1 |
ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)); |
520 |
|
do_route(_HostType, _LServer, From, To, Acc, |
521 |
|
#iq{type = set, xmlns = ?NS_DISCO_ITEMS}) -> |
522 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:not_allowed()), |
523 |
:-( |
ejabberd_router:route(To, From, Acc1, Err); |
524 |
|
do_route(_HostType, _LServer, From, To, Acc, |
525 |
|
#iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ) -> |
526 |
:-( |
ResIQ = |
527 |
|
IQ#iq{type = result, |
528 |
|
sub_el = [#xmlel{name = <<"query">>, |
529 |
|
attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}]}]}, |
530 |
:-( |
ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)); |
531 |
|
do_route(_HostType, _LServer, From, To, Acc, |
532 |
|
#iq{type = get, xmlns = ?NS_VCARD} = IQ) -> |
533 |
1 |
ResIQ = |
534 |
|
IQ#iq{type = result, |
535 |
|
sub_el = [#xmlel{name = <<"vCard">>, |
536 |
|
attrs = [{<<"xmlns">>, ?NS_VCARD}], |
537 |
|
children = iq_get_vcard()}]}, |
538 |
1 |
ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)); |
539 |
|
do_route(_HostType, _LServer, From, To, Acc, _IQ) -> |
540 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:service_unavailable()), |
541 |
:-( |
ejabberd_router:route(To, From, Acc1, Err). |
542 |
|
|
543 |
|
make_search_form_result_iq(IQ, Form) -> |
544 |
2 |
IQ#iq{type = result, |
545 |
|
sub_el = [#xmlel{name = <<"query">>, |
546 |
|
attrs = [{<<"xmlns">>, ?NS_SEARCH}], |
547 |
|
children = Form |
548 |
|
}]}. |
549 |
|
|
550 |
|
route_search_iq_set(HostType, LServer, From, To, Acc, Lang, SubEl, IQ) -> |
551 |
19 |
XDataEl = find_xdata_el(SubEl), |
552 |
19 |
RSMIn = jlib:rsm_decode(IQ), |
553 |
19 |
case XDataEl of |
554 |
|
false -> |
555 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()), |
556 |
:-( |
ejabberd_router:route(To, From, Acc1, Err); |
557 |
|
_ -> |
558 |
19 |
XData = jlib:parse_xdata_submit(XDataEl), |
559 |
19 |
case XData of |
560 |
|
invalid -> |
561 |
:-( |
{Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:bad_request()), |
562 |
:-( |
ejabberd_router:route(To, From, Acc1, Err); |
563 |
|
_ -> |
564 |
19 |
{SearchResult, RSMOutEls} = search_result(HostType, LServer, Lang, To, XData, RSMIn), |
565 |
19 |
ResIQ = make_search_result_iq(IQ, SearchResult, RSMOutEls), |
566 |
19 |
ejabberd_router:route(To, From, Acc, jlib:iq_to_xml(ResIQ)) |
567 |
|
end |
568 |
|
end. |
569 |
|
|
570 |
|
make_search_result_iq(IQ, SearchResult, RSMOutEls) -> |
571 |
19 |
IQ#iq{ |
572 |
|
type = result, |
573 |
|
sub_el = [#xmlel{name = <<"query">>, |
574 |
|
attrs = [{<<"xmlns">>, ?NS_SEARCH}], |
575 |
|
children = [#xmlel{name = <<"x">>, |
576 |
|
attrs = [{<<"xmlns">>, ?NS_XDATA}, |
577 |
|
{<<"type">>, <<"result">>}], |
578 |
|
children = SearchResult} |
579 |
|
] ++ RSMOutEls} |
580 |
|
]}. |
581 |
|
|
582 |
|
iq_get_vcard() -> |
583 |
1 |
[#xmlel{name = <<"FN">>, |
584 |
|
children = [#xmlcdata{content = <<"MongooseIM/mod_vcard">>}]}, |
585 |
|
#xmlel{name = <<"URL">>, children = [#xmlcdata{content = ?MONGOOSE_URI}]}, |
586 |
|
#xmlel{name = <<"DESC">>, |
587 |
|
children = [#xmlcdata{content = [<<"MongooseIM vCard module">>, |
588 |
|
<<"\nCopyright (c) Erlang Solutions Ltd.">>]}]}]. |
589 |
|
find_xdata_el(#xmlel{children = SubEls}) -> |
590 |
19 |
find_xdata_el1(SubEls). |
591 |
|
|
592 |
|
find_xdata_el1([]) -> |
593 |
:-( |
false; |
594 |
|
find_xdata_el1([XE = #xmlel{attrs = Attrs} | Els]) -> |
595 |
19 |
case xml:get_attr_s(<<"xmlns">>, Attrs) of |
596 |
|
?NS_XDATA -> |
597 |
19 |
XE; |
598 |
|
_ -> |
599 |
:-( |
find_xdata_el1(Els) |
600 |
|
end; |
601 |
|
find_xdata_el1([_ | Els]) -> |
602 |
:-( |
find_xdata_el1(Els). |
603 |
|
|
604 |
|
features() -> |
605 |
1 |
[?NS_DISCO_INFO, ?NS_SEARCH, ?NS_VCARD]. |
606 |
|
|
607 |
|
identity(Lang) -> |
608 |
1 |
#{category => <<"directory">>, |
609 |
|
type => <<"user">>, |
610 |
|
name => translate:translate(Lang, <<"vCard User Search">>)}. |
611 |
|
|
612 |
|
search_result(HostType, LServer, Lang, JID, Data, RSMIn) -> |
613 |
19 |
Text = translate:translate(Lang, <<"Search Results for ">>), |
614 |
19 |
TitleEl = #xmlel{name = <<"title">>, |
615 |
|
children = [#xmlcdata{content = [Text, jid:to_binary(JID)]}]}, |
616 |
19 |
ReportedFields = mod_vcard_backend:search_reported_fields(HostType, LServer, Lang), |
617 |
19 |
Results1 = mod_vcard_backend:search(HostType, LServer, Data), |
618 |
19 |
Results2 = lists:filtermap( |
619 |
|
fun(Result) -> |
620 |
25 |
case search_result_get_jid(Result) of |
621 |
|
{ok, ResultJID} -> |
622 |
25 |
{true, {ResultJID, Result}}; |
623 |
|
undefined -> |
624 |
:-( |
false |
625 |
|
end |
626 |
|
end, |
627 |
|
Results1), |
628 |
|
%% mnesia does not guarantee sorting order |
629 |
19 |
Results3 = lists:sort(Results2), |
630 |
19 |
{Results4, RSMOutEls} = |
631 |
|
apply_rsm_to_search_results(Results3, RSMIn, none), |
632 |
19 |
Results5 = [Result || {_, Result} <- Results4], |
633 |
19 |
{[TitleEl, ReportedFields | Results5], RSMOutEls}. |
634 |
|
|
635 |
|
%% No RSM input, create empty |
636 |
|
apply_rsm_to_search_results(Results, none, RSMOut) -> |
637 |
10 |
apply_rsm_to_search_results(Results, #rsm_in{}, RSMOut); |
638 |
|
|
639 |
|
%% Create RSM output |
640 |
|
apply_rsm_to_search_results(Results, #rsm_in{} = RSMIn, none) -> |
641 |
19 |
RSMOut = #rsm_out{count = length(Results)}, |
642 |
19 |
apply_rsm_to_search_results(Results, RSMIn, RSMOut); |
643 |
|
|
644 |
|
%% Skip by <after>$id</after> |
645 |
|
apply_rsm_to_search_results(Results1, #rsm_in{direction = aft, |
646 |
|
id = After} = RSMIn, RSMOut) |
647 |
|
when is_binary(After) -> |
648 |
2 |
Results2 = lists:dropwhile( |
649 |
|
fun({JID, _Result}) -> |
650 |
3 |
JID == After |
651 |
|
end, |
652 |
|
lists:dropwhile( |
653 |
|
fun({JID, _Result}) -> |
654 |
3 |
JID =/= After |
655 |
|
end, |
656 |
|
Results1 |
657 |
|
)), |
658 |
2 |
Index = length(Results1) - length(Results2), |
659 |
2 |
apply_rsm_to_search_results( |
660 |
|
Results2, |
661 |
|
RSMIn#rsm_in{direction = undefined, id = undefined}, |
662 |
|
RSMOut#rsm_out{index = Index} |
663 |
|
); |
664 |
|
|
665 |
|
%% Seek by <before>$id</before> |
666 |
|
apply_rsm_to_search_results(Results1, #rsm_in{max = Max, |
667 |
|
direction = before, |
668 |
|
id = Before} = RSMIn, RSMOut) |
669 |
|
when is_binary(Before) -> |
670 |
3 |
Results2 = lists:takewhile( |
671 |
|
fun({JID, _Result}) -> |
672 |
5 |
JID =/= Before |
673 |
|
end, Results1), |
674 |
3 |
if |
675 |
|
is_integer(Max) -> |
676 |
3 |
Index = max(0, length(Results2) - Max), |
677 |
3 |
Results3 = lists:nthtail(Index, Results2); |
678 |
|
true -> |
679 |
:-( |
Index = 0, |
680 |
:-( |
Results3 = Results2 |
681 |
|
end, |
682 |
3 |
apply_rsm_to_search_results( |
683 |
|
Results3, |
684 |
|
RSMIn#rsm_in{direction = undefined, id = undefined}, |
685 |
|
RSMOut#rsm_out{index = Index} |
686 |
|
); |
687 |
|
|
688 |
|
%% Skip by page number <index>371</index> |
689 |
|
apply_rsm_to_search_results(Results1, |
690 |
|
#rsm_in{max = Max, |
691 |
|
index = Index} = RSMIn1, |
692 |
|
RSMOut) |
693 |
|
when is_integer(Max), is_integer(Index) -> |
694 |
1 |
Results2 = lists:nthtail(min(Index, length(Results1)), Results1), |
695 |
1 |
RSMIn2 = RSMIn1#rsm_in{index = undefined}, |
696 |
1 |
apply_rsm_to_search_results( |
697 |
|
Results2, |
698 |
|
RSMIn2, |
699 |
|
RSMOut#rsm_out{index = Index} |
700 |
|
); |
701 |
|
|
702 |
|
%% Limit to <max>10</max> items |
703 |
|
apply_rsm_to_search_results(Results1, #rsm_in{max = Max} = RSMIn, RSMOut) |
704 |
|
when is_integer(Max) -> |
705 |
9 |
Results2 = lists:sublist(Results1, Max), |
706 |
9 |
apply_rsm_to_search_results(Results2, |
707 |
|
RSMIn#rsm_in{max = undefined}, RSMOut); |
708 |
|
|
709 |
|
%% Encode RSM output |
710 |
|
apply_rsm_to_search_results([_ | _] = Results, _, #rsm_out{} = RSMOut1) -> |
711 |
12 |
{FirstJID, _} = hd(Results), |
712 |
12 |
{LastJID, _} = lists:last(Results), |
713 |
12 |
RSMOut2 = RSMOut1#rsm_out{first = FirstJID, |
714 |
|
last = LastJID}, |
715 |
12 |
{Results, jlib:rsm_encode(RSMOut2)}; |
716 |
|
|
717 |
|
apply_rsm_to_search_results([], _, #rsm_out{} = RSMOut1) -> |
718 |
|
%% clear `index' without `after' |
719 |
7 |
RSMOut2 = RSMOut1#rsm_out{index = undefined}, |
720 |
7 |
{[], jlib:rsm_encode(RSMOut2)}. |
721 |
|
|
722 |
|
search_result_get_jid(#xmlel{name = <<"item">>, |
723 |
|
children = Children}) -> |
724 |
25 |
Fields = jlib:parse_xdata_fields(Children), |
725 |
25 |
case lists:keysearch(<<"jid">>, 1, Fields) of |
726 |
|
{value, {<<"jid">>, JID}} -> |
727 |
25 |
{ok, list_to_binary(JID)}; |
728 |
|
false -> |
729 |
:-( |
undefined |
730 |
|
end. |
731 |
|
|
732 |
|
parse_vcard(User, VHost, VCARD) -> |
733 |
26 |
FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), |
734 |
26 |
Family = xml:get_path_s(VCARD, [{elem, <<"N">>}, |
735 |
|
{elem, <<"FAMILY">>}, cdata]), |
736 |
26 |
Given = xml:get_path_s(VCARD, [{elem, <<"N">>}, |
737 |
|
{elem, <<"GIVEN">>}, cdata]), |
738 |
26 |
Middle = xml:get_path_s(VCARD, [{elem, <<"N">>}, |
739 |
|
{elem, <<"MIDDLE">>}, cdata]), |
740 |
26 |
Nickname = xml:get_path_s(VCARD, [{elem, <<"NICKNAME">>}, cdata]), |
741 |
26 |
BDay = xml:get_path_s(VCARD, [{elem, <<"BDAY">>}, cdata]), |
742 |
26 |
CTRY = xml:get_path_s(VCARD, [{elem, <<"ADR">>}, |
743 |
|
{elem, <<"CTRY">>}, cdata]), |
744 |
26 |
Locality = xml:get_path_s(VCARD, [{elem, <<"ADR">>}, |
745 |
|
{elem, <<"LOCALITY">>}, cdata]), |
746 |
26 |
EMail1 = xml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, |
747 |
|
{elem, <<"USERID">>}, cdata]), |
748 |
26 |
EMail2 = xml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, cdata]), |
749 |
26 |
OrgName = xml:get_path_s(VCARD, [{elem, <<"ORG">>}, |
750 |
|
{elem, <<"ORGNAME">>}, cdata]), |
751 |
26 |
OrgUnit = xml:get_path_s(VCARD, [{elem, <<"ORG">>}, |
752 |
|
{elem, <<"ORGUNIT">>}, cdata]), |
753 |
26 |
EMail = case EMail1 of |
754 |
23 |
<<"">> -> EMail2; |
755 |
3 |
_ -> EMail1 |
756 |
|
end, |
757 |
26 |
try |
758 |
26 |
LUser = jid:nodeprep(User), |
759 |
26 |
LFN = prepare_index(<<"FN">>, FN), |
760 |
24 |
LFamily = prepare_index(<<"FAMILY">>, Family), |
761 |
24 |
LGiven = prepare_index(<<"GIVEN">>, Given), |
762 |
24 |
LMiddle = prepare_index(<<"MIDDLE">>, Middle), |
763 |
24 |
LNickname = prepare_index_allow_emoji(<<"NICKNAME">>, Nickname), |
764 |
24 |
LBDay = prepare_index(<<"BDAY">>, BDay), |
765 |
24 |
LCTRY = prepare_index(<<"CTRY">>, CTRY), |
766 |
24 |
LLocality = prepare_index(<<"LOCALITY">>, Locality), |
767 |
24 |
LEMail = prepare_index(<<"EMAIL">>, EMail), |
768 |
24 |
LOrgName = prepare_index(<<"ORGNAME">>, OrgName), |
769 |
24 |
LOrgUnit = prepare_index(<<"ORGUNIT">>, OrgUnit), |
770 |
|
|
771 |
24 |
US = {LUser, VHost}, |
772 |
|
|
773 |
24 |
{ok, #vcard_search{us = US, |
774 |
|
user = {User, VHost}, |
775 |
|
luser = LUser, |
776 |
|
fn = FN, lfn = LFN, |
777 |
|
family = Family, lfamily = LFamily, |
778 |
|
given = Given, lgiven = LGiven, |
779 |
|
middle = Middle, lmiddle = LMiddle, |
780 |
|
nickname = Nickname, lnickname = LNickname, |
781 |
|
bday = BDay, lbday = LBDay, |
782 |
|
ctry = CTRY, lctry = LCTRY, |
783 |
|
locality = Locality, llocality = LLocality, |
784 |
|
email = EMail, lemail = LEMail, |
785 |
|
orgname = OrgName, lorgname = LOrgName, |
786 |
|
orgunit = OrgUnit, lorgunit = LOrgUnit |
787 |
|
}} |
788 |
|
catch |
789 |
|
throw:{invalid_input, Info} -> |
790 |
2 |
{error, {invalid_input, Info}} |
791 |
|
end. |
792 |
|
|
793 |
|
prepare_index(FieldName, Value) -> |
794 |
266 |
case jid:str_tolower(Value) of |
795 |
|
error -> |
796 |
2 |
throw({invalid_input, {FieldName, Value}}); |
797 |
|
LValue -> |
798 |
264 |
LValue |
799 |
|
end. |
800 |
|
|
801 |
|
prepare_index_allow_emoji(FieldName, Value) -> |
802 |
24 |
{ok, Re} = re:compile(<<"[^[:alnum:][:space:][:punct:]]">>, [unicode, ucp]), |
803 |
24 |
Sanitized = re:replace(Value, Re, <<"">>, [global]), |
804 |
24 |
prepare_index(FieldName, Sanitized). |
805 |
|
|
806 |
|
|
807 |
|
-spec get_default_reported_fields(binary()) -> exml:element(). |
808 |
|
get_default_reported_fields(Lang) -> |
809 |
19 |
#xmlel{name = <<"reported">>, |
810 |
|
children = [ |
811 |
|
?TLFIELD(<<"jid-single">>, <<"Jabber ID">>, <<"jid">>), |
812 |
|
?TLFIELD(<<"text-single">>, <<"Full Name">>, <<"fn">>), |
813 |
|
?TLFIELD(<<"text-single">>, <<"Name">>, <<"first">>), |
814 |
|
?TLFIELD(<<"text-single">>, <<"Middle Name">>, <<"middle">>), |
815 |
|
?TLFIELD(<<"text-single">>, <<"Family Name">>, <<"last">>), |
816 |
|
?TLFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>), |
817 |
|
?TLFIELD(<<"text-single">>, <<"Birthday">>, <<"bday">>), |
818 |
|
?TLFIELD(<<"text-single">>, <<"Country">>, <<"ctry">>), |
819 |
|
?TLFIELD(<<"text-single">>, <<"City">>, <<"locality">>), |
820 |
|
?TLFIELD(<<"text-single">>, <<"Email">>, <<"email">>), |
821 |
|
?TLFIELD(<<"text-single">>, <<"Organization Name">>, <<"orgname">>), |
822 |
|
?TLFIELD(<<"text-single">>, <<"Organization Unit">>, <<"orgunit">>) |
823 |
|
]}. |
824 |
|
|
825 |
|
config_metrics(Host) -> |
826 |
222 |
OptsToReport = [{backend, mnesia}], %list of tuples {option, defualt_value} |
827 |
222 |
mongoose_module_metrics:opts_for_module(Host, ?MODULE, OptsToReport). |
828 |
|
|
829 |
|
vcard_error(IQ = #iq{sub_el = VCARD}, ReasonEl) -> |
830 |
4 |
IQ#iq{type = error, sub_el = [VCARD, ReasonEl]}. |
831 |
|
|
832 |
|
directory_jid_to_server_host(#jid{lserver = DirHost}) -> |
833 |
23 |
case mongoose_domain_api:get_subdomain_info(DirHost) of |
834 |
|
{ok, #{parent_domain := ServerHost}} when is_binary(ServerHost) -> |
835 |
23 |
ServerHost; |
836 |
|
Other -> |
837 |
:-( |
error({dir_jid_to_server_host_failed, DirHost, Other}) |
838 |
|
end. |