Packages
ejabberd
26.4.0
26.4.0
26.3.0
26.2.0
26.1.0
25.10.0
25.8.0
25.7.0
25.4.0
25.3.0
24.12.0
24.10.0
24.7.0
24.6.0
24.2.6
23.10.0
23.4.0
23.1.0
22.10.0
22.5.0
21.12.0
21.7.0
21.4.0
21.1.0
20.12.0
20.7.0
20.4.0
20.3.0
20.2.0
20.1.0
19.9.1
19.9.0
19.8.0
19.5.0
19.2.0
18.12.1
18.12.0
18.6.0
18.4.0
18.3.0
18.1.0
17.11.0
17.9.0
17.6.0
17.3.0
17.1.0
16.12.0-beta1
16.9.0
16.8.0
16.6.2
16.6.1
16.6.0
16.4.1
16.4.0
16.3.0
16.2.0
16.1.0-beta1
Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server)
Current section
Files
Jump to
Current section
Files
src/mod_adhoc_api.erl
%%%----------------------------------------------------------------------
%%% File : mod_adhoc_api.erl
%%% Author : Badlop <badlop@process-one.net>
%%% Purpose : Frontend for ejabberd API Commands via XEP-0050 Ad-Hoc Commands
%%% Created : 21 Feb 2025 by Badlop <badlop@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2026 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
%%%% definitions
%% @format-begin
-module(mod_adhoc_api).
-behaviour(gen_mod).
-author('badlop@process-one.net').
%% gen_mod callbacks
-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
%% hooks
-export([adhoc_local_commands/4, adhoc_local_items/4, disco_local_features/5,
disco_local_identity/5, disco_local_items/5]).
-include("ejabberd_commands.hrl").
-include("ejabberd_sm.hrl").
-include("logger.hrl").
-include("translate.hrl").
-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-define(DEFAULT_API_VERSION, 1000000).
%%%==================================
%%%% gen_mod
start(_Host, _Opts) ->
{ok,
[{hook, adhoc_local_commands, adhoc_local_commands, 40},
{hook, adhoc_local_items, adhoc_local_items, 40},
{hook, disco_local_features, disco_local_features, 40},
{hook, disco_local_identity, disco_local_identity, 40},
{hook, disco_local_items, disco_local_items, 40}]}.
stop(_Host) ->
ok.
reload(_Host, _NewOpts, _OldOpts) ->
ok.
mod_opt_type(default_version) ->
econf:either(
econf:int(0, 3),
econf:and_then(
econf:binary(),
fun(Binary) ->
case binary_to_list(Binary) of
F when F >= "24.06" ->
2;
F when (F > "23.10") and (F < "24.06") ->
1;
F when F =< "23.10" ->
0
end
end)).
-spec mod_options(binary()) -> [{default_version, integer()}].
mod_options(_) ->
[{default_version, ?DEFAULT_API_VERSION}].
depends(_Host, _Opts) ->
[{mod_adhoc, hard}, {mod_last, soft}].
mod_doc() ->
#{desc =>
?T("Execute (def:API commands) "
"in a XMPP client using "
"https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. "
"This module requires _`mod_adhoc`_ (to execute the commands), "
"and recommends _`mod_disco`_ (to discover the commands)."),
note => "added in 25.03",
opts =>
[{default_version,
#{value => "integer() | string()",
desc =>
?T("What API version to use. "
"If setting an ejabberd version, it will use the latest API "
"version that was available in that (def:c2s) ejabberd version. "
"For example, setting '\"24.06\"' in this option implies '2'. "
"The default value is the latest version.")}}],
example =>
["acl:",
" admin:",
" user: jan@localhost",
"",
"api_permissions:",
" \"adhoc commands\":",
" from: mod_adhoc_api",
" who: admin",
" what:",
" - \"[tag:roster]\"",
" - \"[tag:session]\"",
" - stats",
" - status",
"",
"modules:",
" mod_adhoc_api:",
" default_version: 2"]}.
%%%==================================
%%%% Ad-Hoc Commands (copied from mod_configure)
-define(INFO_IDENTITY(Category, Type, Name, Lang),
[#identity{category = Category,
type = Type,
name = tr(Lang, Name)}]).
-define(INFO_COMMAND(Name, Lang),
?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)).
-define(NODE(Name, Node),
#disco_item{jid = jid:make(Server),
node = Node,
name = tr(Lang, Name)}).
-spec tokenize(binary()) -> [binary()].
tokenize(Node) ->
str:tokens(Node, <<"/#">>).
-spec tr(binary(), binary()) -> binary().
tr(Lang, Text) ->
translate:translate(Lang, Text).
%%%==================================
%%%% - disco identity
-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) ->
[identity()].
disco_local_identity(Acc, _From, #jid{lserver = LServer} = _To, Node, Lang) ->
case tokenize(Node) of
[<<"api-commands">>] ->
?INFO_COMMAND(?T("API Commands"), Lang);
[<<"api-commands">>, CommandName] ->
?INFO_COMMAND(get_api_command_desc(CommandName, LServer), Lang);
_ ->
Acc
end.
get_api_command_desc(NameAtom, Host) ->
iolist_to_binary((get_api_command(NameAtom, Host))#ejabberd_commands.desc).
%%%==================================
%%%% - disco features
-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) ->
mod_disco:features_acc().
disco_local_features(Acc, _From, #jid{lserver = LServer} = _To, Node, _Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false ->
Acc;
_ ->
case tokenize(Node) of
[<<"api-commands">>] ->
{result, []};
[<<"api-commands">>, _] ->
{result, [?NS_COMMANDS]};
_ ->
Acc
end
end.
%%%==================================
%%%% - adhoc items
-spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) ->
mod_disco:items_acc().
adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) ->
Items =
case Acc of
{result, Its} ->
Its;
empty ->
[]
end,
Nodes = recursively_get_local_items(From, global, LServer, <<"">>, Server, Lang),
Nodes1 =
lists:filter(fun(#disco_item{node = Nd}) ->
F = disco_local_features(empty, From, To, Nd, Lang),
case F of
{result, [?NS_COMMANDS]} ->
true;
_ ->
false
end
end,
Nodes),
{result, Items ++ Nodes1}.
-spec recursively_get_local_items(jid(),
global | vhost,
binary(),
binary(),
binary(),
binary()) ->
[disco_item()].
recursively_get_local_items(From, PermLev, LServer, Node, Server, Lang) ->
Items =
case get_local_items2(From, {PermLev, LServer}, tokenize(Node), Server, Lang) of
{result, Res} ->
Res;
{error, _Error} ->
[]
end,
lists:flatten(
lists:map(fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
if (S /= Server) or (Nd == <<"">>) ->
[];
true ->
[Item,
recursively_get_local_items(From, PermLev, LServer, Nd, Server, Lang)]
end
end,
Items)).
%%%==================================
%%%% - disco items
-spec disco_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) ->
mod_disco:items_acc().
disco_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) ->
case gen_mod:is_loaded(LServer, mod_adhoc) of
false ->
Acc;
_ ->
Items =
case Acc of
{result, Its} ->
Its;
empty ->
[];
Other ->
Other
end,
case tokenize(Node) of
LNode when (LNode == [<<"api-commands">>]) or (LNode == []) ->
case get_local_items2(From, {global, LServer}, LNode, jid:encode(To), Lang) of
{result, Res} ->
{result, Res};
{error, Error} ->
{error, Error}
end;
_ ->
{result, Items}
end
end.
%%%==================================
%%%% - get_local_items2
-spec get_local_items2(jid(),
{global | vhost, binary()},
[binary()],
binary(),
binary()) ->
{result, [disco_item()]} | {error, stanza_error()}.
get_local_items2(_From, _Host, [], Server, Lang) ->
{result, [?NODE(?T("API Commands"), <<"api-commands">>)]};
get_local_items2(From, {_, Host}, [<<"api-commands">>], _Server, Lang) ->
{result, get_api_commands(From, Host, Lang)};
get_local_items2(_From, {_, _Host}, [<<"api-commands">>, _], _Server, _Lang) ->
{result, []};
get_local_items2(_From, _Host, _, _Server, _Lang) ->
{error, xmpp:err_item_not_found()}.
-spec get_api_commands(jid(), binary(), binary()) -> [disco_item()].
get_api_commands(From, Server, Lang) ->
ApiVersion = mod_adhoc_api_opt:default_version(Server),
lists:map(fun({Name, _Args, _Desc}) ->
NameBin = list_to_binary(atom_to_list(Name)),
NiceNameBin = ejabberd_web_admin:nice_this(NameBin),
?NODE(NiceNameBin, <<"api-commands/", NameBin/binary>>)
end,
ejabberd_commands:list_commands(ApiVersion, get_caller_info(From))).
%%%==================================
%%%% - adhoc commands
-define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang),
adhoc_local_commands(From, To, Request)).
-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) ->
adhoc_command() | {error, stanza_error()}.
adhoc_local_commands(Acc, From, To, #adhoc_command{node = Node} = Request) ->
case tokenize(Node) of
[<<"api-commands">>, _CommandName] ->
?COMMANDS_RESULT(LServer, From, To, Request, Lang);
_ ->
Acc
end.
-spec adhoc_local_commands(jid(), jid(), adhoc_command()) ->
adhoc_command() | {error, stanza_error()}.
adhoc_local_commands(From,
#jid{lserver = LServer} = _To,
#adhoc_command{lang = Lang,
node = Node,
sid = SessionID,
action = Action,
xdata = XData} =
Request) ->
LNode = tokenize(Node),
ActionIsExecute = Action == execute orelse Action == complete,
if Action == cancel ->
#adhoc_command{status = canceled,
lang = Lang,
node = Node,
sid = SessionID};
XData == undefined, ActionIsExecute ->
case get_form(LServer, LNode, Lang) of
{result, Form} ->
xmpp_util:make_adhoc_response(Request,
#adhoc_command{status = executing, xdata = Form});
{error, Error} ->
{error, Error}
end;
XData /= undefined, ActionIsExecute ->
case set_form(From, LServer, LNode, Lang, XData) of
{result, Res} ->
xmpp_util:make_adhoc_response(Request,
#adhoc_command{xdata = Res, status = completed});
%%{'EXIT', _} -> {error, xmpp:err_bad_request()};
{error, Error} ->
{error, Error}
end;
true ->
{error, xmpp:err_bad_request(?T("Unexpected action"), Lang)}
end.
-spec get_form(binary(), [binary()], binary()) ->
{result, xdata()} | {error, stanza_error()}.
get_form(Host, [<<"api-commands">>, CommandName], Lang) ->
get_form_api_command(CommandName, Host, Lang);
get_form(_Host, _, _Lang) ->
{error, xmpp:err_service_unavailable()}.
-spec set_form(jid(), binary(), [binary()], binary(), xdata()) ->
{result, xdata() | undefined} | {error, stanza_error()}.
set_form(From, Host, [<<"api-commands">>, Command], Lang, XData) ->
set_form_api_command(From, Host, Command, XData, Lang);
set_form(_From, _Host, _, _Lang, _XData) ->
{error, xmpp:err_service_unavailable()}.
%%%==================================
%%%% API Commands
get_api_command(Name, Host) when is_binary(Name) ->
get_api_command(binary_to_existing_atom(Name, latin1), Host);
get_api_command(Name, Host) when is_atom(Name) ->
ApiVersion = mod_adhoc_api_opt:default_version(Host),
ejabberd_commands:get_command_definition(Name, ApiVersion).
get_caller_info(#jid{user = User, server = Server} = From) ->
#{tag => <<>>,
usr => {User, Server, <<"">>},
caller_server => Server,
ip => get_ip_address(From),
caller_module => ?MODULE}.
get_ip_address(#jid{user = User,
server = Server,
resource = Resource}) ->
case ejabberd_sm:get_user_ip(User, Server, Resource) of
{IP, _Port} when is_tuple(IP) ->
IP;
_ ->
error_ip_address
end.
%%%==================================
%%%% - get form
get_form_api_command(NameBin, Host, _Lang) ->
Def = get_api_command(NameBin, Host),
Title = ejabberd_web_admin:nice_this(Def#ejabberd_commands.name),
Instructions = get_instructions(Def),
FieldsArgs =
build_fields(Def#ejabberd_commands.args,
Def#ejabberd_commands.args_desc,
Def#ejabberd_commands.args_example,
Def#ejabberd_commands.policy,
get_replacements(Host),
true),
FieldsArgsWithHeads =
case FieldsArgs of
[] ->
[];
_ ->
[#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
end,
NodeFields = build_node_fields(),
{result,
#xdata{title = Title,
type = form,
instructions = Instructions,
fields = FieldsArgsWithHeads ++ NodeFields}}.
get_replacements(Host) ->
[{user, <<"">>},
{localuser, <<"">>},
{host, Host},
{localhost, Host},
{password, <<"">>},
{newpass, <<"">>},
{service, mod_muc_admin:find_hosts(Host)}].
build_node_fields() ->
build_node_fields([node() | nodes()]).
build_node_fields([_ThisNode]) ->
[];
build_node_fields(AtomNodes) ->
[ThisNode | _] = Nodes = [atom_to_binary(Atom, latin1) || Atom <- AtomNodes],
Options = [#xdata_option{label = N, value = N} || N <- Nodes],
[#xdata_field{type = fixed, label = ?T("Clustering")},
#xdata_field{type = 'list-single',
label = <<"ejabberd node">>,
var = <<"mod_adhoc_api_target_node">>,
values = [ThisNode],
options = Options}].
%%%==================================
%%%% - set form
set_form_api_command(From, Host, CommandNameBin, XData, _Lang) ->
%% Description
Def = get_api_command(CommandNameBin, Host),
Title = ejabberd_web_admin:nice_this(Def#ejabberd_commands.name),
Instructions = get_instructions(Def),
%% Arguments
FieldsArgs0 = [Field || Field <- XData#xdata.fields, Field#xdata_field.type /= fixed],
FieldsArgs1 =
lists:map(fun(Arg) ->
case Arg#xdata_field.values of
[_] ->
Arg;
_ ->
Arg#xdata_field{type = 'text-multi'}
end
end,
FieldsArgs0),
{Node, FieldsArgs} =
case lists:keytake(<<"mod_adhoc_api_target_node">>, #xdata_field.var, FieldsArgs1) of
{value, #xdata_field{values = [TargetNode]}, FAs} ->
{binary_to_existing_atom(TargetNode, latin1), FAs};
false ->
{node(), FieldsArgs1}
end,
FieldsArgsWithHeads =
case FieldsArgs of
[] ->
[];
_ ->
[#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
end,
%% Execute
Arguments = api_extract_fields(FieldsArgs, Def#ejabberd_commands.args),
ApiVersion = mod_adhoc_api_opt:default_version(Host),
CallResult =
execute(Def,
[Node,
mod_http_api,
handle,
[binary_to_existing_atom(CommandNameBin, latin1),
get_caller_info(From),
Arguments,
ApiVersion]]),
%% Command result
FieldsResult2 =
case CallResult of
{200, RR} ->
build_fields([Def#ejabberd_commands.result],
[Def#ejabberd_commands.result_desc],
[RR],
restricted,
[{host, Host}],
false);
{Code, _ApiErrorCode, MessageBin} ->
[#xdata_field{type = 'text-single',
label = <<"Error ", (integer_to_binary(Code))/binary>>,
values = encode(MessageBin, irrelevat_type),
var = <<"error">>}];
{Code, MessageBin} ->
[#xdata_field{type = 'text-single',
label = <<"Error ", (integer_to_binary(Code))/binary>>,
values = encode(MessageBin, irrelevat_type),
var = <<"error">>}]
end,
FieldsResultWithHeads =
[#xdata_field{type = fixed, label = <<"">>},
#xdata_field{type = fixed, label = ?T("Result")}
| FieldsResult2],
%% Result stanza
{result,
#xdata{title = Title,
type = result,
instructions = Instructions,
fields = FieldsArgsWithHeads ++ FieldsResultWithHeads}}.
execute(Def, Arguments) ->
case lists:member(async, Def#ejabberd_commands.tags) of
true ->
spawn(ejabberd_cluster, call, Arguments),
{200, 0};
false ->
apply(ejabberd_cluster, call, Arguments)
end.
api_extract_fields(Fields, ArgsDef) ->
lists:map(fun(#xdata_field{values = Values, var = ANameBin}) ->
ArgDef = proplists:get_value(binary_to_existing_atom(ANameBin, latin1), ArgsDef),
V = case {Values, ArgDef} of
{Values, {list, {_ElementName, {tuple, ElementsDef}}}} ->
[parse_tuple(ElementsDef, Value) || Value <- Values];
{[Value], {tuple, ElementsDef}} ->
parse_tuple(ElementsDef, Value);
{[Value], _} ->
Value;
_ ->
Values
end,
{ANameBin, V}
end,
Fields).
parse_tuple(ElementsDef, Value) ->
Values = str:tokens(Value, <<":">>),
List1 =
[{atom_to_binary(Name, latin1), Val}
|| {{Name, _Type}, Val} <- lists:zip(ElementsDef, Values)],
maps:from_list(List1).
%%%==================================
%%%% - get instructions
get_instructions(Def) ->
Note2 =
case Def#ejabberd_commands.note of
[] ->
[];
Note ->
N = iolist_to_binary(Note),
[<<"Note: ", N/binary>>]
end,
Tags2 =
case Def#ejabberd_commands.tags of
[] ->
[];
Tags ->
T = str:join([atom_to_binary(Tag, latin1) || Tag <- Tags], <<", ">>),
[<<"Tags: ", T/binary>>]
end,
Module2 =
case Def#ejabberd_commands.definer of
unknown ->
[];
DefinerAtom ->
D = atom_to_binary(DefinerAtom, latin1),
[<<"Module: ", D/binary>>]
end,
Version2 =
case Def#ejabberd_commands.version of
0 ->
[];
Version ->
V = integer_to_binary(Version),
[<<"API version: ", V/binary>>]
end,
get_instructions2([Def#ejabberd_commands.desc, Def#ejabberd_commands.longdesc]
++ Note2
++ Tags2
++ Module2
++ Version2).
get_instructions2(ListStrings) ->
[re:replace(String, "[\t]*[ ]+", " ", [{return, binary}, global])
|| String <- ListStrings, String /= ""].
%%%==================================
%%%% - build fields
build_fields(NameTypes, none, Examples, Policy, Replacements, Required) ->
build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
build_fields(NameTypes, Descs, none, Policy, Replacements, Required) ->
build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
build_fields(NameTypes, [none], Examples, Policy, Replacements, Required) ->
build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
build_fields(NameTypes, Descs, [none], Policy, Replacements, Required) ->
build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
build_fields(NameTypes, Descs, Examples, Policy, Replacements, Required) ->
{NameTypes2, Descs2, Examples2} =
case Policy of
user ->
{[{user, binary}, {host, binary} | NameTypes],
["Username", "Server host" | Descs],
["tom", "example.com" | Examples]};
_ ->
{NameTypes, Descs, Examples}
end,
build_fields2(NameTypes2, Descs2, Examples2, Replacements, Required).
build_fields2([{_ArgName, {list, ArgNameType}}] = NameTypes,
Descs,
Examples,
_Replacements,
Required) ->
ArgNameType2 =
case ArgNameType of
{jid, _} ->
'jid-multi';
{_, _} ->
'text-multi'
end,
Args = lists_zip3_pad(NameTypes, Descs, Examples),
lists:map(fun({{AName, AType}, ADesc, AExample}) ->
ANameBin = list_to_binary(atom_to_list(AName)),
#xdata_field{type = ArgNameType2,
label = ANameBin,
desc = list_to_binary(ADesc),
values = encode(AExample, AType),
required = Required,
var = ANameBin}
end,
Args);
build_fields2(NameTypes, Descs, Examples, Replacements, Required) ->
Args = lists_zip3_pad(NameTypes, Descs, Examples),
lists:map(fun({{AName, AType}, ADesc, AExample}) ->
ANameBin = list_to_binary(atom_to_list(AName)),
AValue = proplists:get_value(AName, Replacements, AExample),
Values = encode(AValue, AType),
Type =
case {AType, Values} of
{{list, _}, _} ->
'text-multi';
{string, [_, _ | _]} ->
'text-multi';
_ ->
'text-single'
end,
#xdata_field{type = Type,
label = ANameBin,
desc = make_desc(ADesc, AValue),
values = Values,
required = Required,
var = ANameBin}
end,
Args).
-ifdef(OTP_BELOW_26).
lists_zip3_pad(As, Bs, Cs) ->
lists_zip3_pad(As, Bs, Cs, []).
lists_zip3_pad([A | As], [B | Bs], [C | Cs], Xs) ->
lists_zip3_pad(As, Bs, Cs, [{A, B, C} | Xs]);
lists_zip3_pad([A | As], [B | Bs], Nil, Xs) when (Nil == none) or (Nil == []) ->
lists_zip3_pad(As, Bs, [], [{A, B, ""} | Xs]);
lists_zip3_pad([A | As], Nil, [C | Cs], Xs) when (Nil == none) or (Nil == []) ->
lists_zip3_pad(As, [], Cs, [{A, "", C} | Xs]);
lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
lists_zip3_pad(As, [], [], [{A, "", ""} | Xs]);
lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
lists:reverse(Xs).
-endif.
-ifndef(OTP_BELOW_26).
lists_zip3_pad(As, Bs, Cs) ->
lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}).
-endif.
make_desc(ADesc, T) when is_tuple(T) ->
T3 = string:join(tuple_to_list(T), " : "),
iolist_to_binary([ADesc, " {", T3, "}"]);
make_desc(ADesc, M) when is_map(M) ->
M2 = [binary_to_list(V) || V <- maps:keys(M)],
M3 = string:join(M2, " : "),
iolist_to_binary([ADesc, " {", M3, "}"]);
make_desc(ADesc, _M) ->
iolist_to_binary(ADesc).
%%%==================================
%%%% - encode
encode({[T | _] = List}, Type) when is_tuple(T) ->
encode(List, Type);
encode([T | _] = List, Type) when is_tuple(T) ->
[encode(Element, Type) || Element <- List];
encode(T, _Type) when is_tuple(T) ->
T2 = [x_to_binary(E) || E <- tuple_to_list(T)],
T3 = str:join(T2, <<":">>),
[T3];
encode(M, {tuple, Types}) when is_map(M) ->
M2 = [x_to_list(maps:get(atom_to_binary(Key, latin1), M))
|| {Key, _ElementType} <- Types],
M3 = string:join(M2, " : "),
[iolist_to_binary(M3)];
encode([S | _] = SList, _Type) when is_list(S) ->
[iolist_to_binary(A) || A <- SList];
encode([B | _] = BList, _Type) when is_binary(B) ->
BList;
encode(I, _Type) when is_integer(I) ->
[integer_to_binary(I)];
encode([M | _] = List, {list, {_Name, TupleType}}) when is_map(M) ->
[encode(M1, TupleType) || M1 <- List];
encode(S, _Type) when is_list(S) ->
[iolist_to_binary(S)];
encode(B, _Type) when is_binary(B) ->
str:tokens(B, <<"\n">>).
x_to_list(B) when is_binary(B) ->
binary_to_list(B);
x_to_list(I) when is_integer(I) ->
integer_to_list(I);
x_to_list(L) when is_list(L) ->
L.
x_to_binary(B) when is_binary(B) ->
B;
x_to_binary(I) when is_integer(I) ->
integer_to_binary(I);
x_to_binary(L) when is_list(L) ->
iolist_to_binary(L).
%%%==================================
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: