• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.34
/src/mod_adhoc_api.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_adhoc_api.erl
3
%%% Author  : Badlop <badlop@process-one.net>
4
%%% Purpose : Frontend for ejabberd API Commands via XEP-0050 Ad-Hoc Commands
5
%%% Created : 21 Feb 2025 by Badlop <badlop@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License along
21
%%% with this program; if not, write to the Free Software Foundation, Inc.,
22
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
%%%
24
%%%----------------------------------------------------------------------
25

26
%%%% definitions
27
%% @format-begin
28

29
-module(mod_adhoc_api).
30

31
-behaviour(gen_mod).
32

33
-author('badlop@process-one.net').
34

35
%% gen_mod callbacks
36
-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
37
%% hooks
38
-export([adhoc_local_commands/4, adhoc_local_items/4, disco_local_features/5,
39
         disco_local_identity/5, disco_local_items/5]).
40

41
-include("ejabberd_commands.hrl").
42
-include("ejabberd_sm.hrl").
43
-include("logger.hrl").
44
-include("translate.hrl").
45

46
-include_lib("stdlib/include/ms_transform.hrl").
47
-include_lib("xmpp/include/xmpp.hrl").
48

49
-define(DEFAULT_API_VERSION, 1000000).
50

51
%%%==================================
52
%%%% gen_mod
53

54
start(_Host, _Opts) ->
55
    {ok,
90✔
56
     [{hook, adhoc_local_commands, adhoc_local_commands, 40},
57
      {hook, adhoc_local_items, adhoc_local_items, 40},
58
      {hook, disco_local_features, disco_local_features, 40},
59
      {hook, disco_local_identity, disco_local_identity, 40},
60
      {hook, disco_local_items, disco_local_items, 40}]}.
61

62
stop(_Host) ->
63
    ok.
90✔
64

65
reload(_Host, _NewOpts, _OldOpts) ->
66
    ok.
×
67

68
mod_opt_type(default_version) ->
69
    econf:either(
108✔
70
        econf:int(0, 3),
71
        econf:and_then(
72
            econf:binary(),
73
            fun(Binary) ->
74
               case binary_to_list(Binary) of
×
75
                   F when F >= "24.06" ->
76
                       2;
×
77
                   F when (F > "23.10") and (F < "24.06") ->
78
                       1;
×
79
                   F when F =< "23.10" ->
80
                       0
×
81
               end
82
            end)).
83

84
-spec mod_options(binary()) -> [{default_version, integer()}].
85
mod_options(_) ->
86
    [{default_version, ?DEFAULT_API_VERSION}].
108✔
87

88
depends(_Host, _Opts) ->
89
    [{mod_adhoc, hard}, {mod_last, soft}].
108✔
90

91
mod_doc() ->
92
    #{desc =>
×
93
          ?T("Execute (def:API commands) "
94
             "in a XMPP client using "
95
             "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. "
96
             "This module requires _`mod_adhoc`_ (to execute the commands), "
97
             "and recommends _`mod_disco`_ (to discover the commands)."),
98
      note => "added in 25.03",
99
      opts =>
100
          [{default_version,
101
            #{value => "integer() | string()",
102
              desc =>
103
                  ?T("What API version to use. "
104
                     "If setting an ejabberd version, it will use the latest API "
105
                     "version that was available in that (def:c2s) ejabberd version. "
106
                     "For example, setting '\"24.06\"' in this option implies '2'. "
107
                     "The default value is the latest version.")}}],
108
      example =>
109
          ["acl:",
110
           "  admin:",
111
           "    user: jan@localhost",
112
           "",
113
           "api_permissions:",
114
           "  \"adhoc commands\":",
115
           "    from: mod_adhoc_api",
116
           "    who: admin",
117
           "    what:",
118
           "      - \"[tag:roster]\"",
119
           "      - \"[tag:session]\"",
120
           "      - stats",
121
           "      - status",
122
           "",
123
           "modules:",
124
           "  mod_adhoc_api:",
125
           "    default_version: 2"]}.
126

127
%%%==================================
128
%%%% Ad-Hoc Commands (copied from mod_configure)
129

130
-define(INFO_IDENTITY(Category, Type, Name, Lang),
131
        [#identity{category = Category,
132
                   type = Type,
133
                   name = tr(Lang, Name)}]).
134
-define(INFO_COMMAND(Name, Lang),
135
        ?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)).
136
-define(NODE(Name, Node),
137
        #disco_item{jid = jid:make(Server),
138
                    node = Node,
139
                    name = tr(Lang, Name)}).
140

141
-spec tokenize(binary()) -> [binary()].
142
tokenize(Node) ->
143
    str:tokens(Node, <<"/#">>).
70✔
144

145
-spec tr(binary(), binary()) -> binary().
146
tr(Lang, Text) ->
147
    translate:translate(Lang, Text).
219✔
148

149
%%%==================================
150
%%%% - disco identity
151

152
-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) ->
153
                              [identity()].
154
disco_local_identity(Acc, _From, #jid{lserver = LServer} = _To, Node, Lang) ->
155
    case tokenize(Node) of
10✔
156
        [<<"api-commands">>] ->
157
            ?INFO_COMMAND(?T("API Commands"), Lang);
1✔
158
        [<<"api-commands">>, CommandName] ->
159
            ?INFO_COMMAND(get_api_command_desc(CommandName, LServer), Lang);
×
160
        _ ->
161
            Acc
9✔
162
    end.
163

164
get_api_command_desc(NameAtom, Host) ->
165
    iolist_to_binary((get_api_command(NameAtom, Host))#ejabberd_commands.desc).
×
166

167
%%%==================================
168
%%%% - disco features
169

170
-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) ->
171
                              mod_disco:features_acc().
172
disco_local_features(Acc, _From, #jid{lserver = LServer} = _To, Node, _Lang) ->
173
    case gen_mod:is_loaded(LServer, mod_adhoc) of
10✔
174
        false ->
175
            Acc;
×
176
        _ ->
177
            case tokenize(Node) of
10✔
178
                [<<"api-commands">>] ->
179
                    {result, []};
1✔
180
                [<<"api-commands">>, _] ->
181
                    {result, [?NS_COMMANDS]};
×
182
                _ ->
183
                    Acc
9✔
184
            end
185
    end.
186

187
%%%==================================
188
%%%% - adhoc items
189

190
-spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) ->
191
                           mod_disco:items_acc().
192
adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) ->
193
    Items =
×
194
        case Acc of
195
            {result, Its} ->
196
                Its;
×
197
            empty ->
198
                []
×
199
        end,
200
    Nodes = recursively_get_local_items(From, global, LServer, <<"">>, Server, Lang),
×
201
    Nodes1 =
×
202
        lists:filter(fun(#disco_item{node = Nd}) ->
203
                        F = disco_local_features(empty, From, To, Nd, Lang),
×
204
                        case F of
×
205
                            {result, [?NS_COMMANDS]} ->
206
                                true;
×
207
                            _ ->
208
                                false
×
209
                        end
210
                     end,
211
                     Nodes),
212
    {result, Items ++ Nodes1}.
×
213

214
-spec recursively_get_local_items(jid(),
215
                                  global | vhost,
216
                                  binary(),
217
                                  binary(),
218
                                  binary(),
219
                                  binary()) ->
220
                                     [disco_item()].
221
recursively_get_local_items(From, PermLev, LServer, Node, Server, Lang) ->
222
    Items =
×
223
        case get_local_items2(From, {PermLev, LServer}, tokenize(Node), Server, Lang) of
224
            {result, Res} ->
225
                Res;
×
226
            {error, _Error} ->
227
                []
×
228
        end,
229
    lists:flatten(
×
230
        lists:map(fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
231
                     if (S /= Server) or (Nd == <<"">>) ->
×
232
                            [];
×
233
                        true ->
234
                            [Item,
×
235
                             recursively_get_local_items(From, PermLev, LServer, Nd, Server, Lang)]
236
                     end
237
                  end,
238
                  Items)).
239

240
%%%==================================
241
%%%% - disco items
242

243
-spec disco_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) ->
244
                           mod_disco:items_acc().
245
disco_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) ->
246
    case gen_mod:is_loaded(LServer, mod_adhoc) of
2✔
247
        false ->
248
            Acc;
×
249
        _ ->
250
            Items =
2✔
251
                case Acc of
252
                    {result, Its} ->
253
                        Its;
×
254
                    empty ->
255
                        [];
2✔
256
                    Other ->
257
                        Other
×
258
                end,
259
            case tokenize(Node) of
2✔
260
                LNode when (LNode == [<<"api-commands">>]) or (LNode == []) ->
261
                    case get_local_items2(From, {global, LServer}, LNode, jid:encode(To), Lang) of
2✔
262
                        {result, Res} ->
263
                            {result, Res};
2✔
264
                        {error, Error} ->
265
                            {error, Error}
×
266
                    end;
267
                _ ->
268
                    {result, Items}
×
269
            end
270
    end.
271

272
%%%==================================
273
%%%% - get_local_items2
274

275
-spec get_local_items2(jid(),
276
                       {global | vhost, binary()},
277
                       [binary()],
278
                       binary(),
279
                       binary()) ->
280
                          {result, [disco_item()]} | {error, stanza_error()}.
281
get_local_items2(_From, _Host, [], Server, Lang) ->
282
    {result, [?NODE(?T("API Commands"), <<"api-commands">>)]};
1✔
283
get_local_items2(From, {_, Host}, [<<"api-commands">>], _Server, Lang) ->
284
    {result, get_api_commands(From, Host, Lang)};
1✔
285
get_local_items2(_From, {_, _Host}, [<<"api-commands">>, _], _Server, _Lang) ->
286
    {result, []};
×
287
get_local_items2(_From, _Host, _, _Server, _Lang) ->
288
    {error, xmpp:err_item_not_found()}.
×
289

290
-spec get_api_commands(jid(), binary(), binary()) -> [disco_item()].
291
get_api_commands(From, Server, Lang) ->
292
    ApiVersion = mod_adhoc_api_opt:default_version(Server),
1✔
293
    lists:map(fun({Name, _Args, _Desc}) ->
1✔
294
                 NameBin = list_to_binary(atom_to_list(Name)),
217✔
295
                 ?NODE(NameBin, <<"api-commands/", NameBin/binary>>)
217✔
296
              end,
297
              ejabberd_commands:list_commands(ApiVersion, get_caller_info(From))).
298

299
%%%==================================
300
%%%% - adhoc commands
301

302
-define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang),
303
        adhoc_local_commands(From, To, Request)).
304

305
-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) ->
306
                              adhoc_command() | {error, stanza_error()}.
307
adhoc_local_commands(Acc, From, To, #adhoc_command{node = Node} = Request) ->
308
    case tokenize(Node) of
24✔
309
        [<<"api-commands">>, _CommandName] ->
310
            ?COMMANDS_RESULT(LServer, From, To, Request, Lang);
24✔
311
        _ ->
312
            Acc
×
313
    end.
314

315
-spec adhoc_local_commands(jid(), jid(), adhoc_command()) ->
316
                              adhoc_command() | {error, stanza_error()}.
317
adhoc_local_commands(From,
318
                     #jid{lserver = LServer} = _To,
319
                     #adhoc_command{lang = Lang,
320
                                    node = Node,
321
                                    sid = SessionID,
322
                                    action = Action,
323
                                    xdata = XData} =
324
                         Request) ->
325
    LNode = tokenize(Node),
24✔
326
    ActionIsExecute = Action == execute orelse Action == complete,
24✔
327
    if Action == cancel ->
24✔
328
           #adhoc_command{status = canceled,
×
329
                          lang = Lang,
330
                          node = Node,
331
                          sid = SessionID};
332
       XData == undefined, ActionIsExecute ->
333
           case get_form(LServer, LNode, Lang) of
12✔
334
               {result, Form} ->
335
                   xmpp_util:make_adhoc_response(Request,
12✔
336
                                                 #adhoc_command{status = executing, xdata = Form});
337
               {error, Error} ->
338
                   {error, Error}
×
339
           end;
340
       XData /= undefined, ActionIsExecute ->
341
           case set_form(From, LServer, LNode, Lang, XData) of
12✔
342
               {result, Res} ->
343
                   xmpp_util:make_adhoc_response(Request,
12✔
344
                                                 #adhoc_command{xdata = Res, status = completed});
345
               %%{'EXIT', _} -> {error, xmpp:err_bad_request()};
346
               {error, Error} ->
347
                   {error, Error}
×
348
           end;
349
       true ->
350
           {error, xmpp:err_bad_request(?T("Unexpected action"), Lang)}
×
351
    end.
352

353
-spec get_form(binary(), [binary()], binary()) ->
354
                  {result, xdata()} | {error, stanza_error()}.
355
get_form(Host, [<<"api-commands">>, CommandName], Lang) ->
356
    get_form_api_command(CommandName, Host, Lang);
12✔
357
get_form(_Host, _, _Lang) ->
358
    {error, xmpp:err_service_unavailable()}.
×
359

360
-spec set_form(jid(), binary(), [binary()], binary(), xdata()) ->
361
                  {result, xdata() | undefined} | {error, stanza_error()}.
362
set_form(From, Host, [<<"api-commands">>, Command], Lang, XData) ->
363
    set_form_api_command(From, Host, Command, XData, Lang);
12✔
364
set_form(_From, _Host, _, _Lang, _XData) ->
365
    {error, xmpp:err_service_unavailable()}.
×
366

367
%%%==================================
368
%%%% API Commands
369

370
get_api_command(Name, Host) when is_binary(Name) ->
371
    get_api_command(binary_to_existing_atom(Name, latin1), Host);
24✔
372
get_api_command(Name, Host) when is_atom(Name) ->
373
    ApiVersion = mod_adhoc_api_opt:default_version(Host),
24✔
374
    ejabberd_commands:get_command_definition(Name, ApiVersion).
24✔
375

376
get_caller_info(#jid{user = User, server = Server} = From) ->
377
    #{tag => <<>>,
13✔
378
      usr => {User, Server, <<"">>},
379
      caller_server => Server,
380
      ip => get_ip_address(From),
381
      caller_module => ?MODULE}.
382

383
get_ip_address(#jid{user = User,
384
                    server = Server,
385
                    resource = Resource}) ->
386
    case ejabberd_sm:get_user_ip(User, Server, Resource) of
13✔
387
        {IP, _Port} when is_tuple(IP) ->
388
            IP;
13✔
389
        _ ->
390
            error_ip_address
×
391
    end.
392

393
%%%==================================
394
%%%% - get form
395

396
get_form_api_command(NameBin, Host, _Lang) ->
397
    Def = get_api_command(NameBin, Host),
12✔
398
    Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)),
12✔
399
    Instructions = get_instructions(Def),
12✔
400
    FieldsArgs =
12✔
401
        build_fields(Def#ejabberd_commands.args,
402
                     Def#ejabberd_commands.args_desc,
403
                     Def#ejabberd_commands.args_example,
404
                     Def#ejabberd_commands.policy,
405
                     get_replacements(Host),
406
                     true),
407
    FieldsArgsWithHeads =
12✔
408
        case FieldsArgs of
409
            [] ->
410
                [];
3✔
411
            _ ->
412
                [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
9✔
413
        end,
414
    NodeFields = build_node_fields(),
12✔
415
    {result,
12✔
416
     #xdata{title = Title,
417
            type = form,
418
            instructions = Instructions,
419
            fields = FieldsArgsWithHeads ++ NodeFields}}.
420

421
get_replacements(Host) ->
422
    [{user, <<"">>},
12✔
423
     {localuser, <<"">>},
424
     {host, Host},
425
     {localhost, Host},
426
     {password, <<"">>},
427
     {newpass, <<"">>},
428
     {service, mod_muc_admin:find_hosts(Host)}].
429

430
build_node_fields() ->
431
    build_node_fields([node() | nodes()]).
12✔
432

433
build_node_fields([_ThisNode]) ->
434
    [];
12✔
435
build_node_fields(AtomNodes) ->
436
    [ThisNode | _] = Nodes = [atom_to_binary(Atom, latin1) || Atom <- AtomNodes],
×
437
    Options = [#xdata_option{label = N, value = N} || N <- Nodes],
×
438
    [#xdata_field{type = fixed, label = ?T("Clustering")},
×
439
     #xdata_field{type = 'list-single',
440
                  label = <<"ejabberd node">>,
441
                  var = <<"mod_adhoc_api_target_node">>,
442
                  values = [ThisNode],
443
                  options = Options}].
444

445
%%%==================================
446
%%%% - set form
447

448
set_form_api_command(From, Host, CommandNameBin, XData, _Lang) ->
449
    %% Description
450
    Def = get_api_command(CommandNameBin, Host),
12✔
451
    Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)),
12✔
452
    Instructions = get_instructions(Def),
12✔
453

454
    %% Arguments
455
    FieldsArgs0 = [Field || Field <- XData#xdata.fields, Field#xdata_field.type /= fixed],
12✔
456
    FieldsArgs1 =
12✔
457
        lists:map(fun(Arg) ->
458
                     case Arg#xdata_field.values of
10✔
459
                         [_] ->
460
                             Arg;
8✔
461
                         _ ->
462
                             Arg#xdata_field{type = 'text-multi'}
2✔
463
                     end
464
                  end,
465
                  FieldsArgs0),
466

467
    {Node, FieldsArgs} =
12✔
468
        case lists:keytake(<<"mod_adhoc_api_target_node">>, #xdata_field.var, FieldsArgs1) of
469
            {value, #xdata_field{values = [TargetNode]}, FAs} ->
470
                {binary_to_existing_atom(TargetNode, latin1), FAs};
×
471
            false ->
472
                {node(), FieldsArgs1}
12✔
473
        end,
474

475
    FieldsArgsWithHeads =
12✔
476
        case FieldsArgs of
477
            [] ->
478
                [];
3✔
479
            _ ->
480
                [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
9✔
481
        end,
482

483
    %% Execute
484
    Arguments = api_extract_fields(FieldsArgs, Def#ejabberd_commands.args),
12✔
485
    ApiVersion = mod_adhoc_api_opt:default_version(Host),
12✔
486
    CallResult =
12✔
487
        execute(Def,
488
                [Node,
489
                 mod_http_api,
490
                 handle,
491
                 [binary_to_existing_atom(CommandNameBin, latin1),
492
                  get_caller_info(From),
493
                  Arguments,
494
                  ApiVersion]]),
495

496
    %% Command result
497
    FieldsResult2 =
12✔
498
        case CallResult of
499
            {200, RR} ->
500
                build_fields([Def#ejabberd_commands.result],
12✔
501
                             [Def#ejabberd_commands.result_desc],
502
                             [RR],
503
                             restricted,
504
                             [{host, Host}],
505
                             false);
506
            {Code, _ApiErrorCode, MessageBin} ->
507
                [#xdata_field{type = 'text-single',
×
508
                              label = <<"Error ", (integer_to_binary(Code))/binary>>,
509
                              values = encode(MessageBin, irrelevat_type),
510
                              var = <<"error">>}];
511
            {Code, MessageBin} ->
512
                [#xdata_field{type = 'text-single',
×
513
                              label = <<"Error ", (integer_to_binary(Code))/binary>>,
514
                              values = encode(MessageBin, irrelevat_type),
515
                              var = <<"error">>}]
516
        end,
517
    FieldsResultWithHeads =
12✔
518
        [#xdata_field{type = fixed, label = <<"">>},
519
         #xdata_field{type = fixed, label = ?T("Result")}
520
         | FieldsResult2],
521

522
    %% Result stanza
523
    {result,
12✔
524
     #xdata{title = Title,
525
            type = result,
526
            instructions = Instructions,
527
            fields = FieldsArgsWithHeads ++ FieldsResultWithHeads}}.
528

529
execute(Def, Arguments) ->
530
    case lists:member(async, Def#ejabberd_commands.tags) of
12✔
531
        true ->
532
            spawn(ejabberd_cluster, call, Arguments),
×
533
            {200, 0};
×
534
        false ->
535
            apply(ejabberd_cluster, call, Arguments)
12✔
536
    end.
537

538
api_extract_fields(Fields, ArgsDef) ->
539
    lists:map(fun(#xdata_field{values = Values, var = ANameBin}) ->
12✔
540
                 ArgDef = proplists:get_value(binary_to_existing_atom(ANameBin, latin1), ArgsDef),
10✔
541
                 V = case {Values, ArgDef} of
10✔
542
                         {Values, {list, {_ElementName, {tuple, ElementsDef}}}} ->
543
                             [parse_tuple(ElementsDef, Value) || Value <- Values];
1✔
544
                         {[Value], {tuple, ElementsDef}} ->
545
                             parse_tuple(ElementsDef, Value);
1✔
546
                         {[Value], _} ->
547
                             Value;
7✔
548
                         _ ->
549
                             Values
1✔
550
                     end,
551
                 {ANameBin, V}
10✔
552
              end,
553
              Fields).
554

555
parse_tuple(ElementsDef, Value) ->
556
    Values = str:tokens(Value, <<":">>),
4✔
557
    List1 =
4✔
558
        [{atom_to_binary(Name, latin1), Val}
9✔
559
         || {{Name, _Type}, Val} <- lists:zip(ElementsDef, Values)],
4✔
560
    maps:from_list(List1).
4✔
561

562
%%%==================================
563
%%%% - get instructions
564

565
get_instructions(Def) ->
566
    Note2 =
24✔
567
        case Def#ejabberd_commands.note of
568
            [] ->
569
                [];
24✔
570
            Note ->
571
                N = iolist_to_binary(Note),
×
572
                [<<"Note: ", N/binary>>]
×
573
        end,
574
    Tags2 =
24✔
575
        case Def#ejabberd_commands.tags of
576
            [] ->
577
                [];
×
578
            Tags ->
579
                T = str:join([atom_to_binary(Tag, latin1) || Tag <- Tags], <<", ">>),
24✔
580
                [<<"Tags: ", T/binary>>]
24✔
581
        end,
582
    Module2 =
24✔
583
        case Def#ejabberd_commands.definer of
584
            unknown ->
585
                [];
×
586
            DefinerAtom ->
587
                D = atom_to_binary(DefinerAtom, latin1),
24✔
588
                [<<"Module: ", D/binary>>]
24✔
589
        end,
590
    Version2 =
24✔
591
        case Def#ejabberd_commands.version of
592
            0 ->
593
                [];
20✔
594
            Version ->
595
                V = integer_to_binary(Version),
4✔
596
                [<<"API version: ", V/binary>>]
4✔
597
        end,
598
    get_instructions2([Def#ejabberd_commands.desc, Def#ejabberd_commands.longdesc]
24✔
599
                      ++ Note2
600
                      ++ Tags2
601
                      ++ Module2
602
                      ++ Version2).
603

604
get_instructions2(ListStrings) ->
605
    [re:replace(String, "[\t]*[  ]+", " ", [{return, binary}, global])
24✔
606
     || String <- ListStrings, String /= ""].
24✔
607

608
%%%==================================
609
%%%% - build fields
610

611
build_fields(NameTypes, none, Examples, Policy, Replacements, Required) ->
612
    build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
12✔
613
build_fields(NameTypes, Descs, none, Policy, Replacements, Required) ->
614
    build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
×
615
build_fields(NameTypes, [none], Examples, Policy, Replacements, Required) ->
616
    build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
12✔
617
build_fields(NameTypes, Descs, [none], Policy, Replacements, Required) ->
618
    build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
×
619
build_fields(NameTypes, Descs, Examples, Policy, Replacements, Required) ->
620
    {NameTypes2, Descs2, Examples2} =
24✔
621
        case Policy of
622
            user ->
623
                {[{user, binary}, {host, binary} | NameTypes],
×
624
                 ["Username", "Server host" | Descs],
625
                 ["tom", "example.com" | Examples]};
626
            _ ->
627
                {NameTypes, Descs, Examples}
24✔
628
        end,
629
    build_fields2(NameTypes2, Descs2, Examples2, Replacements, Required).
24✔
630

631
build_fields2([{_ArgName, {list, ArgNameType}}] = NameTypes,
632
              Descs,
633
              Examples,
634
              _Replacements,
635
              Required) ->
636
    ArgNameType2 =
4✔
637
        case ArgNameType of
638
            {jid, _} ->
639
                'jid-multi';
×
640
            {_, _} ->
641
                'text-multi'
4✔
642
        end,
643
    Args = lists_zip3_pad(NameTypes, Descs, Examples),
4✔
644
    lists:map(fun({{AName, AType}, ADesc, AExample}) ->
4✔
645
                 ANameBin = list_to_binary(atom_to_list(AName)),
4✔
646
                 #xdata_field{type = ArgNameType2,
4✔
647
                              label = ANameBin,
648
                              desc = list_to_binary(ADesc),
649
                              values = encode(AExample, AType),
650
                              required = Required,
651
                              var = ANameBin}
652
              end,
653
              Args);
654
build_fields2(NameTypes, Descs, Examples, Replacements, Required) ->
655
    Args = lists_zip3_pad(NameTypes, Descs, Examples),
20✔
656
    lists:map(fun({{AName, AType}, ADesc, AExample}) ->
20✔
657
                 ANameBin = list_to_binary(atom_to_list(AName)),
18✔
658
                 AValue = proplists:get_value(AName, Replacements, AExample),
18✔
659
                 Values = encode(AValue, AType),
18✔
660
                 Type =
18✔
661
                     case {AType, Values} of
662
                         {{list, _}, _} ->
663
                             'text-multi';
×
664
                         {string, [_, _ | _]} ->
665
                             'text-multi';
×
666
                         _ ->
667
                             'text-single'
18✔
668
                     end,
669
                 #xdata_field{type = Type,
18✔
670
                              label = ANameBin,
671
                              desc = make_desc(ADesc, AValue),
672
                              values = Values,
673
                              required = Required,
674
                              var = ANameBin}
675
              end,
676
              Args).
677

678
-ifdef(OTP_BELOW_26).
679

680
lists_zip3_pad(As, Bs, Cs) ->
681
    lists_zip3_pad(As, Bs, Cs, []).
682

683
lists_zip3_pad([A | As], [B | Bs], [C | Cs], Xs) ->
684
    lists_zip3_pad(As, Bs, Cs, [{A, B, C} | Xs]);
685
lists_zip3_pad([A | As], [B | Bs], Nil, Xs) when (Nil == none) or (Nil == []) ->
686
    lists_zip3_pad(As, Bs, [], [{A, B, ""} | Xs]);
687
lists_zip3_pad([A | As], Nil, [C | Cs], Xs) when (Nil == none) or (Nil == []) ->
688
    lists_zip3_pad(As, [], Cs, [{A, "", C} | Xs]);
689
lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
690
    lists_zip3_pad(As, [], [], [{A, "", ""} | Xs]);
691
lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
692
    lists:reverse(Xs).
693

694
-endif.
695

696
-ifndef(OTP_BELOW_26).
697

698
lists_zip3_pad(As, Bs, Cs) ->
699
    lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}).
24✔
700

701
-endif.
702

703
make_desc(ADesc, T) when is_tuple(T) ->
704
    T3 = string:join(tuple_to_list(T), " : "),
1✔
705
    iolist_to_binary([ADesc, " {", T3, "}"]);
1✔
706
make_desc(ADesc, M) when is_map(M) ->
707
    M2 = [binary_to_list(V) || V <- maps:keys(M)],
1✔
708
    M3 = string:join(M2, " : "),
1✔
709
    iolist_to_binary([ADesc, " {", M3, "}"]);
1✔
710
make_desc(ADesc, _M) ->
711
    iolist_to_binary(ADesc).
16✔
712

713
%%%==================================
714
%%%% - encode
715

716
encode({[T | _] = List}, Type) when is_tuple(T) ->
717
    encode(List, Type);
×
718
encode([T | _] = List, Type) when is_tuple(T) ->
719
    [encode(Element, Type) || Element <- List];
1✔
720
encode(T, _Type) when is_tuple(T) ->
721
    T2 = [x_to_binary(E) || E <- tuple_to_list(T)],
4✔
722
    T3 = str:join(T2, <<":">>),
4✔
723
    [T3];
4✔
724
encode(M, {tuple, Types}) when is_map(M) ->
725
    M2 = [x_to_list(maps:get(atom_to_binary(Key, latin1), M))
4✔
726
          || {Key, _ElementType} <- Types],
4✔
727
    M3 = string:join(M2, " : "),
4✔
728
    [iolist_to_binary(M3)];
4✔
729
encode([S | _] = SList, _Type) when is_list(S) ->
730
    [iolist_to_binary(A) || A <- SList];
1✔
731
encode([B | _] = BList, _Type) when is_binary(B) ->
732
    BList;
1✔
733
encode(I, _Type) when is_integer(I) ->
734
    [integer_to_binary(I)];
6✔
735
encode([M | _] = List, {list, {_Name, TupleType}}) when is_map(M) ->
736
    [encode(M1, TupleType) || M1 <- List];
1✔
737
encode(S, _Type) when is_list(S) ->
738
    [iolist_to_binary(S)];
6✔
739
encode(B, _Type) when is_binary(B) ->
740
    str:tokens(B, <<"\n">>).
4✔
741

742
x_to_list(B) when is_binary(B) ->
743
    binary_to_list(B);
9✔
744
x_to_list(I) when is_integer(I) ->
745
    integer_to_list(I);
×
746
x_to_list(L) when is_list(L) ->
747
    L.
×
748

749
x_to_binary(B) when is_binary(B) ->
750
    B;
×
751
x_to_binary(I) when is_integer(I) ->
752
    integer_to_binary(I);
×
753
x_to_binary(L) when is_list(L) ->
754
    iolist_to_binary(L).
9✔
755

756
%%%==================================
757

758
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc