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