diff --git a/include/pubsub_serverinfo_codec.hrl b/include/pubsub_serverinfo_codec.hrl new file mode 100644 index 000000000..81b975798 --- /dev/null +++ b/include/pubsub_serverinfo_codec.hrl @@ -0,0 +1,17 @@ +%% Created automatically by XML generator (fxml_gen.erl) +%% Source: pubsub_serverinfo_codec.spec + +-record(pubsub_serverinfo_remote_domain, {name = <<>> :: binary(), + type = [] :: ['bidi' | 'incoming' | 'outgoing']}). +-type pubsub_serverinfo_remote_domain() :: #pubsub_serverinfo_remote_domain{}. + +-record(pubsub_serverinfo_domain, {name = <<>> :: binary(), + remote_domain :: 'undefined' | [#pubsub_serverinfo_remote_domain{}]}). +-type pubsub_serverinfo_domain() :: #pubsub_serverinfo_domain{}. + +-record(pubsub_serverinfo, {domain = [] :: [#pubsub_serverinfo_domain{}]}). +-type pubsub_serverinfo() :: #pubsub_serverinfo{}. + +-type pubsub_serverinfo_codec() :: pubsub_serverinfo() | + pubsub_serverinfo_domain() | + pubsub_serverinfo_remote_domain(). diff --git a/specs/pubsub_serverinfo_codec.spec b/specs/pubsub_serverinfo_codec.spec new file mode 100644 index 000000000..44966e9f1 --- /dev/null +++ b/specs/pubsub_serverinfo_codec.spec @@ -0,0 +1,52 @@ +-xml(pubsub_serverinfo, + #elem{name = <<"serverinfo">>, + xmlns = <<"urn:xmpp:serverinfo:0">>, + module = pubsub_serverinfo_codec, + result = {pubsub_serverinfo, '$domain'}, + refs = [#ref{name = pubsub_serverinfo_domain, + label = '$domain', + min = 0}]}). + +-xml(pubsub_serverinfo_domain, + #elem{name = <<"domain">>, + xmlns = <<"urn:xmpp:serverinfo:0">>, + module = pubsub_serverinfo_codec, + result = {pubsub_serverinfo_domain, '$name', '$remote_domain'}, + attrs = [#attr{name = <<"name">>, + label = '$name', + required = true}], + refs = [#ref{name = pubsub_serverinfo_federation, + label = '$remote_domain', + min = 0, max = 1}]}). + +-xml(pubsub_serverinfo_federation, + #elem{name = <<"federation">>, + xmlns = <<"urn:xmpp:serverinfo:0">>, + module = pubsub_serverinfo_codec, + result = '$remote_domain', + refs = [#ref{name = pubsub_serverinfo_remote_domain, + label = '$remote_domain', + min = 0}]}). + +-xml(pubsub_serverinfo_remote_domain, + #elem{name = <<"remote-domain">>, + xmlns = <<"urn:xmpp:serverinfo:0">>, + module = pubsub_serverinfo_codec, + result = {pubsub_serverinfo_remote_domain, '$name', '$type'}, + attrs = [#attr{name = <<"name">>, + label = '$name', + required = true}], + refs = [#ref{name = pubsub_serverinfo_connection, + label = '$type', + min = 0}]}). + +-xml(pubsub_serverinfo_connection, + #elem{name = <<"connection">>, + xmlns = <<"urn:xmpp:serverinfo:0">>, + module = pubsub_serverinfo_codec, + result = '$type', + attrs = [#attr{name = <<"type">>, + label = '$type', + required = true, + enc = {enc_enum, []}, + dec = {dec_enum, [[incoming, outgoing, bidi]]}}]}). diff --git a/src/mod_pubsub_serverinfo.erl b/src/mod_pubsub_serverinfo.erl new file mode 100644 index 000000000..4ebc93732 --- /dev/null +++ b/src/mod_pubsub_serverinfo.erl @@ -0,0 +1,312 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_pubsub_serverinfo.erl +%%% Author : Stefan Strigler +%%% Purpose : Exposes server information over Pub/Sub +%%% Created : 26 Dec 2023 by Guus der Kinderen +%%% +%%% +%%% ejabberd, Copyright (C) 2023 - 2025 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. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_pubsub_serverinfo). +-author('stefan@strigler.de'). +-behaviour(gen_mod). +-behaviour(gen_server). + +-include("pubsub_serverinfo_codec.hrl"). +-include("logger.hrl"). +-include_lib("xmpp/include/xmpp.hrl"). + +%% gen_mod callbacks. +-export([start/2, stop/1, depends/2, mod_options/1, mod_opt_type/1, get_local_features/5, mod_doc/0]). +-export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2]). +-export([in_auth_result/3, out_auth_result/2, get_info/5]). + +-define(NS_URN_SERVERINFO, <<"urn:xmpp:serverinfo:0">>). +-define(PUBLIC_HOSTS_URL, <<"https://data.xmpp.net/providers/v2/providers-Ds.json">>). + +-record(state, {host, pubsub_host, node, monitors = #{}, timer = undefined, public_hosts = []}). + +start(Host, Opts) -> + case pubsub_host(Host, Opts) of + {error, _Reason} = Error -> + Error; + PubsubHost -> + xmpp:register_codec(pubsub_serverinfo_codec), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 50), + ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 50), + ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, out_auth_result, 50), + ejabberd_hooks:add(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50), + gen_mod:start_child(?MODULE, Host, PubsubHost) + end. + +stop(Host) -> + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 50), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50), + ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, out_auth_result, 50), + ejabberd_hooks:delete(s2s_in_auth_result, Host, ?MODULE, in_auth_result, 50), + gen_mod:stop_child(?MODULE, Host). + +init([Host, PubsubHost]) -> + TRef = timer:send_interval(timer:minutes(5), self(), update_pubsub), + Monitors = init_monitors(Host), + PublicHosts = fetch_public_hosts(), + State = #state{host = Host, + pubsub_host = PubsubHost, + node = <<"serverinfo">>, + timer = TRef, + monitors = Monitors, + public_hosts = PublicHosts}, + self() ! update_pubsub, + {ok, State}. + +-spec init_monitors(binary()) -> map(). +init_monitors(Host) -> + lists:foldl( + fun(Domain, Monitors) -> + RefIn = make_ref(), % just dummies + RefOut = make_ref(), + maps:merge(#{RefIn => {incoming, {Host, Domain, true}}, + RefOut => {outgoing, {Host, Domain, true}}}, + Monitors) + end, + #{}, + ejabberd_option:hosts() -- [Host]). + +-spec fetch_public_hosts() -> list(). +fetch_public_hosts() -> + try + {ok, {{_, 200, _}, _Headers, Body}} = httpc:request(get, {?PUBLIC_HOSTS_URL, []}, [{timeout, 1000}], [{body_format, binary}]), + case misc:json_decode(Body) of + PublicHosts when is_list(PublicHosts) -> PublicHosts; + Other -> + ?WARNING_MSG("Parsed JSON for public hosts was not a list: ~p", [Other]), + [] + end + catch E:R -> + ?WARNING_MSG("Failed fetching public hosts (~p): ~p", [E, R]), + [] + end. + +handle_cast({Event, Domain, Pid}, #state{host = Host, monitors = Mons} = State) + when Event == register_in; Event == register_out -> + Ref = monitor(process, Pid), + IsPublic = check_if_public(Domain, State), + NewMons = maps:put(Ref, {event_to_dir(Event), {Host, Domain, IsPublic}}, Mons), + {noreply, State#state{monitors = NewMons}}; +handle_cast(_, State) -> + {noreply, State}. + +event_to_dir(register_in) -> incoming; +event_to_dir(register_out) -> outgoing. + +handle_call(_Request, _From, State) -> + {noreply, State}. + +handle_info({iq_reply, IQReply, {LServer, RServer}}, + #state{monitors = Mons} = State) -> + case IQReply of + #iq{type = result, sub_els = [El]} -> + case xmpp:decode(El) of + #disco_info{features = Features} -> + case lists:member(?NS_URN_SERVERINFO, Features) of + true -> + NewMons = maps:fold(fun(Ref, {Dir, {LServer0, RServer0, _}}, Acc) + when LServer == LServer0, RServer == RServer0 -> + maps:put(Ref, {Dir, {LServer, RServer, true}}, Acc); + (Ref, Other, Acc) -> + maps:put(Ref, Other, Acc) + end, + #{}, + Mons), + {noreply, State#state{monitors = NewMons}}; + _ -> + {noreply, State} + end; + _ -> + {noreply, State} + end; + _ -> + {noreply, State} + end; +handle_info(update_pubsub, State) -> + update_pubsub(State), + {noreply, State}; +handle_info({'DOWN', Mon, process, _Pid, _Info}, #state{monitors = Mons} = State) -> + {noreply, State#state{monitors = maps:remove(Mon, Mons)}}; +handle_info(_Request, State) -> + {noreply, State}. + +terminate(_Reason, #state{monitors = Mons, timer = Timer}) -> + case is_reference(Timer) of + true -> + case erlang:cancel_timer(Timer) of + false -> + receive {timeout, Timer, _} -> ok + after 0 -> ok + end; + _ -> + ok + end; + _ -> + ok + end, + maps:fold( + fun(Mon, _, _) -> + demonitor(Mon) + end, ok, Mons). + +depends(_Host, _Opts) -> + [{mod_pubsub, hard}]. + +mod_options(_Host) -> + [{pubsub_host, undefined}]. + +mod_opt_type(pubsub_host) -> + econf:either(undefined, econf:host()). + +mod_doc() -> #{}. + +in_auth_result(#{server_host := Host, remote_server := RServer} = State, true, _Server) -> + gen_server:cast(gen_mod:get_module_proc(Host, ?MODULE), {register_in, RServer, self()}), + State; +in_auth_result(State, _, _) -> + State. + +out_auth_result(#{server_host := Host, remote_server := RServer} = State, true) -> + gen_server:cast(gen_mod:get_module_proc(Host, ?MODULE), {register_out, RServer, self()}), + State; +out_auth_result(State, _) -> + State. + +check_if_public(Domain, State) -> + maybe_send_disco_info(is_public(Domain, State) orelse is_monitored(Domain, State), Domain, State). + +is_public(Domain, #state{public_hosts = PublicHosts}) -> + lists:member(Domain, PublicHosts). + +is_monitored(Domain, #state{host = Host, monitors = Mons}) -> + maps:size( + maps:filter( + fun(_Ref, {_Dir, {LServer, RServer, IsPublic}}) + when LServer == Host, RServer == Domain -> IsPublic; + (_Ref, _Other) -> false + end, + Mons)) =/= 0. + +maybe_send_disco_info(true, _Domain, _State) -> true; +maybe_send_disco_info(false, Domain, #state{host = Host}) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + IQ = #iq{type = get, from = jid:make(Host), + to = jid:make(Domain), sub_els = [#disco_info{}]}, + ejabberd_router:route_iq(IQ, {Host, Domain}, Proc), + false. + +update_pubsub(#state{host = Host, pubsub_host = PubsubHost, node = Node, monitors = Mons}) -> + Map = maps:fold( + fun(_, {Dir, {MyDomain, Target, IsPublic}}, Acc) -> + maps:update_with(MyDomain, + fun(Acc2) -> + maps:update_with(Target, + fun({Types, _}) -> {Types#{Dir => true}, IsPublic} end, + {#{Dir => true}, IsPublic}, Acc2) + end, #{Target => {#{Dir => true}, IsPublic}}, Acc) + end, #{}, Mons), + Domains = maps:fold( + fun(MyDomain, Targets, Acc) -> + Remote = maps:fold( + fun(Remote, {Types, true}, Acc2) -> + [#pubsub_serverinfo_remote_domain{name = Remote, type = maps:keys(Types)} | Acc2]; + (_HiddenRemote, {Types, false}, Acc2) -> + [#pubsub_serverinfo_remote_domain{type = maps:keys(Types)} | Acc2] + end, [], Targets), + [#pubsub_serverinfo_domain{name = MyDomain, remote_domain = Remote} | Acc] + end, [], Map), + + PubOpts = [{persist_items, true}, {max_items, 1}, {access_model, open}], + ?DEBUG("Publishing serverinfo pubsub item on ~s: ~p", [PubsubHost, Domains]), + mod_pubsub:publish_item( + PubsubHost, Host, Node, jid:make(Host), + <<"current">>, [xmpp:encode(#pubsub_serverinfo{domain = Domains})], PubOpts, all). + +get_local_features({error, _} = Acc, _From, _To, _Node, _Lang) -> + Acc; +get_local_features(Acc, _From, _To, Node, _Lang) when Node == <<>> -> + case Acc of + {result, Features} -> + {result, [?NS_URN_SERVERINFO | Features]}; + empty -> {result, [?NS_URN_SERVERINFO]} + end; +get_local_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +get_info(Acc, Host, Mod, Node, Lang) when (Mod == undefined orelse Mod == mod_disco), Node == <<"">> -> + case mod_disco:get_info(Acc, Host, Mod, Node, Lang) of + [#xdata{fields = Fields} = XD | Rest] -> + PubsubHost = pubsub_host(Host), + NodeField = #xdata_field{var = <<"serverinfo-pubsub-node">>, + values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}, + {stop, [XD#xdata{fields = Fields ++ [NodeField]} | Rest]}; + _ -> + Acc + end; +get_info(Acc, Host, Mod, Node, _Lang) when Node == <<"">>, is_atom(Mod) -> + PubsubHost = pubsub_host(Host), + [#xdata{type = result, + fields = [ + #xdata_field{type = hidden, + var = <<"FORM_TYPE">>, + values = [?NS_SERVERINFO]}, + #xdata_field{var = <<"serverinfo-pubsub-node">>, + values = [<<"xmpp:", PubsubHost/binary, "?;node=serverinfo">>]}]} | Acc]; +get_info(Acc, _Host, _Mod, _Node, _Lang) -> + Acc. + +pubsub_host(Host) -> + case pubsub_host(Host, gen_mod:get_module_opts(Host, ?MODULE)) of + {error, _Reason} = Error -> + throw(Error); + PubsubHost -> + PubsubHost + end. + +pubsub_host(Host, Opts) -> + case gen_mod:get_opt(pubsub_host, Opts) of + undefined -> + PubsubHost = hd(get_mod_pubsub_hosts(Host)), + ?INFO_MSG("No pubsub_host in configuration for ~p, choosing ~s", [?MODULE, PubsubHost]), + PubsubHost; + PubsubHost -> + case check_pubsub_host_exists(Host, PubsubHost) of + true -> + PubsubHost; + false -> + {error, {pubsub_host_does_not_exist, PubsubHost}} + end + end. + +check_pubsub_host_exists(Host, PubsubHost) -> + lists:member(PubsubHost, get_mod_pubsub_hosts(Host)). + +get_mod_pubsub_hosts(Host) -> + case gen_mod:get_module_opt(Host, mod_pubsub, hosts) of + [] -> + [gen_mod:get_module_opt(Host, mod_pubsub, host)]; + PubsubHosts -> + PubsubHosts + end. diff --git a/src/pubsub_serverinfo_codec.erl b/src/pubsub_serverinfo_codec.erl new file mode 100644 index 000000000..9b16edeaf --- /dev/null +++ b/src/pubsub_serverinfo_codec.erl @@ -0,0 +1,734 @@ +%% Created automatically by XML generator (fxml_gen.erl) +%% Source: pubsub_serverinfo_codec.spec + +-module(pubsub_serverinfo_codec). + +-compile(export_all). + +decode(El) -> decode(El, <<>>, []). + +decode(El, Opts) -> decode(El, <<>>, Opts). + +decode({xmlel, Name, Attrs, _} = El, TopXMLNS, Opts) -> + XMLNS = get_attr(<<"xmlns">>, Attrs, TopXMLNS), + case get_mod(Name, XMLNS) of + undefined when XMLNS == <<>> -> + erlang:error({pubsub_serverinfo_codec, + {missing_tag_xmlns, Name}}); + undefined -> + erlang:error({pubsub_serverinfo_codec, + {unknown_tag, Name, XMLNS}}); + Mod -> Mod:do_decode(Name, XMLNS, El, Opts) + end. + +encode(El) -> encode(El, <<>>). + +encode({xmlel, _, _, _} = El, _) -> El; +encode({xmlcdata, _} = CData, _) -> CData; +encode(El, TopXMLNS) -> + Mod = get_mod(El), + Mod:do_encode(El, TopXMLNS). + + +get_name(El) -> + Mod = get_mod(El), + Mod:do_get_name(El). + +get_ns(El) -> + Mod = get_mod(El), + Mod:do_get_ns(El). + +is_known_tag({xmlel, Name, Attrs, _}, TopXMLNS) -> + XMLNS = get_attr(<<"xmlns">>, Attrs, TopXMLNS), + get_mod(Name, XMLNS) /= undefined. + +get_els(Term) -> + Mod = get_mod(Term), + Mod:get_els(Term). + +set_els(Term, Els) -> + Mod = get_mod(Term), + Mod:set_els(Term, Els). + +do_decode(<<"connection">>, <<"urn:xmpp:serverinfo:0">>, + El, Opts) -> + decode_pubsub_serverinfo_connection(<<"urn:xmpp:serverinfo:0">>, + Opts, + El); +do_decode(<<"remote-domain">>, + <<"urn:xmpp:serverinfo:0">>, El, Opts) -> + decode_pubsub_serverinfo_remote_domain(<<"urn:xmpp:serverinfo:0">>, + Opts, + El); +do_decode(<<"federation">>, <<"urn:xmpp:serverinfo:0">>, + El, Opts) -> + decode_pubsub_serverinfo_federation(<<"urn:xmpp:serverinfo:0">>, + Opts, + El); +do_decode(<<"domain">>, <<"urn:xmpp:serverinfo:0">>, El, + Opts) -> + decode_pubsub_serverinfo_domain(<<"urn:xmpp:serverinfo:0">>, + Opts, + El); +do_decode(<<"serverinfo">>, <<"urn:xmpp:serverinfo:0">>, + El, Opts) -> + decode_pubsub_serverinfo(<<"urn:xmpp:serverinfo:0">>, + Opts, + El); +do_decode(Name, <<>>, _, _) -> + erlang:error({pubsub_serverinfo_codec, + {missing_tag_xmlns, Name}}); +do_decode(Name, XMLNS, _, _) -> + erlang:error({pubsub_serverinfo_codec, + {unknown_tag, Name, XMLNS}}). + +tags() -> + [{<<"connection">>, <<"urn:xmpp:serverinfo:0">>}, + {<<"remote-domain">>, <<"urn:xmpp:serverinfo:0">>}, + {<<"federation">>, <<"urn:xmpp:serverinfo:0">>}, + {<<"domain">>, <<"urn:xmpp:serverinfo:0">>}, + {<<"serverinfo">>, <<"urn:xmpp:serverinfo:0">>}]. + +do_encode({pubsub_serverinfo, _} = Serverinfo, + TopXMLNS) -> + encode_pubsub_serverinfo(Serverinfo, TopXMLNS); +do_encode({pubsub_serverinfo_domain, _, _} = Domain, + TopXMLNS) -> + encode_pubsub_serverinfo_domain(Domain, TopXMLNS); +do_encode({pubsub_serverinfo_remote_domain, _, _} = + Remote_domain, + TopXMLNS) -> + encode_pubsub_serverinfo_remote_domain(Remote_domain, + TopXMLNS). + +do_get_name({pubsub_serverinfo, _}) -> <<"serverinfo">>; +do_get_name({pubsub_serverinfo_domain, _, _}) -> + <<"domain">>; +do_get_name({pubsub_serverinfo_remote_domain, _, _}) -> + <<"remote-domain">>. + +do_get_ns({pubsub_serverinfo, _}) -> + <<"urn:xmpp:serverinfo:0">>; +do_get_ns({pubsub_serverinfo_domain, _, _}) -> + <<"urn:xmpp:serverinfo:0">>; +do_get_ns({pubsub_serverinfo_remote_domain, _, _}) -> + <<"urn:xmpp:serverinfo:0">>. + +register_module(Mod) -> + register_module(Mod, pubsub_serverinfo_codec_external). + +unregister_module(Mod) -> + unregister_module(Mod, + pubsub_serverinfo_codec_external). + +format_error({bad_attr_value, Attr, Tag, XMLNS}) -> + <<"Bad value of attribute '", Attr/binary, "' in tag <", + Tag/binary, "/> qualified by namespace '", XMLNS/binary, + "'">>; +format_error({bad_cdata_value, <<>>, Tag, XMLNS}) -> + <<"Bad value of cdata in tag <", Tag/binary, + "/> qualified by namespace '", XMLNS/binary, "'">>; +format_error({missing_tag, Tag, XMLNS}) -> + <<"Missing tag <", Tag/binary, + "/> qualified by namespace '", XMLNS/binary, "'">>; +format_error({missing_attr, Attr, Tag, XMLNS}) -> + <<"Missing attribute '", Attr/binary, "' in tag <", + Tag/binary, "/> qualified by namespace '", XMLNS/binary, + "'">>; +format_error({missing_cdata, <<>>, Tag, XMLNS}) -> + <<"Missing cdata in tag <", Tag/binary, + "/> qualified by namespace '", XMLNS/binary, "'">>; +format_error({unknown_tag, Tag, XMLNS}) -> + <<"Unknown tag <", Tag/binary, + "/> qualified by namespace '", XMLNS/binary, "'">>; +format_error({missing_tag_xmlns, Tag}) -> + <<"Missing namespace for tag <", Tag/binary, "/>">>. + +io_format_error({bad_attr_value, Attr, Tag, XMLNS}) -> + {<<"Bad value of attribute '~s' in tag <~s/> " + "qualified by namespace '~s'">>, + [Attr, Tag, XMLNS]}; +io_format_error({bad_cdata_value, <<>>, Tag, XMLNS}) -> + {<<"Bad value of cdata in tag <~s/> qualified " + "by namespace '~s'">>, + [Tag, XMLNS]}; +io_format_error({missing_tag, Tag, XMLNS}) -> + {<<"Missing tag <~s/> qualified by namespace " + "'~s'">>, + [Tag, XMLNS]}; +io_format_error({missing_attr, Attr, Tag, XMLNS}) -> + {<<"Missing attribute '~s' in tag <~s/> " + "qualified by namespace '~s'">>, + [Attr, Tag, XMLNS]}; +io_format_error({missing_cdata, <<>>, Tag, XMLNS}) -> + {<<"Missing cdata in tag <~s/> qualified " + "by namespace '~s'">>, + [Tag, XMLNS]}; +io_format_error({unknown_tag, Tag, XMLNS}) -> + {<<"Unknown tag <~s/> qualified by namespace " + "'~s'">>, + [Tag, XMLNS]}; +io_format_error({missing_tag_xmlns, Tag}) -> + {<<"Missing namespace for tag <~s/>">>, [Tag]}. + +get_attr(Attr, Attrs, Default) -> + case lists:keyfind(Attr, 1, Attrs) of + {_, Val} -> Val; + false -> Default + end. + +enc_xmlns_attrs(XMLNS, XMLNS) -> []; +enc_xmlns_attrs(XMLNS, _) -> [{<<"xmlns">>, XMLNS}]. + +choose_top_xmlns(<<>>, NSList, TopXMLNS) -> + case lists:member(TopXMLNS, NSList) of + true -> TopXMLNS; + false -> hd(NSList) + end; +choose_top_xmlns(XMLNS, _, _) -> XMLNS. + +register_module(Mod, ResolverMod) -> + MD5Sum = try Mod:module_info(md5) of + Val -> Val + catch + error:badarg -> + {ok, {Mod, Val}} = beam_lib:md5(code:which(Mod)), + Val + end, + case orddict:find(Mod, ResolverMod:modules()) of + {ok, MD5Sum} -> ok; + _ -> + Mods = orddict:store(Mod, + MD5Sum, + ResolverMod:modules()), + recompile_resolver(Mods, ResolverMod) + end. + +unregister_module(Mod, ResolverMod) -> + case orddict:find(Mod, ResolverMod:modules()) of + {ok, _} -> + Mods = orddict:erase(Mod, ResolverMod:modules()), + recompile_resolver(Mods, ResolverMod); + error -> ok + end. + +recompile_resolver(Mods, ResolverMod) -> + Tags = lists:flatmap(fun (M) -> + [{Name, XMLNS, M} || {Name, XMLNS} <- M:tags()] + end, + orddict:fetch_keys(Mods)), + Records = lists:flatmap(fun (M) -> + [{RecName, RecSize, M} + || {RecName, RecSize} <- M:records()] + end, + orddict:fetch_keys(Mods)), + Lookup1 = string:join(lists:map(fun ({RecName, + RecSize, + M}) -> + io_lib:format("lookup({~s}) -> '~s'", + [string:join([io_lib:format("'~s'", + [RecName]) + | ["_" + || _ + <- lists:seq(1, + RecSize)]], + ","), + M]) + end, + Records) + ++ + ["lookup(Term) -> erlang:error(badarg, " + "[Term])."], + ";" ++ io_lib:nl()), + Lookup2 = string:join(lists:map(fun ({Name, + XMLNS, + M}) -> + io_lib:format("lookup(~w, ~w) -> '~s'", + [Name, XMLNS, M]) + end, + Tags) + ++ ["lookup(_, _) -> undefined."], + ";" ++ io_lib:nl()), + Modules = io_lib:format("modules() -> [~s].", + [string:join([io_lib:format("{'~s', ~w}", [M, S]) + || {M, S} <- Mods], + ",")]), + Module = io_lib:format("-module(~s).", [ResolverMod]), + Compile = "-compile(export_all).", + Forms = lists:map(fun (Expr) -> + {ok, Tokens, _} = + erl_scan:string(lists:flatten(Expr)), + {ok, Form} = erl_parse:parse_form(Tokens), + Form + end, + [Module, Compile, Modules, Lookup1, Lookup2]), + {ok, Code} = case compile:forms(Forms, []) of + {ok, ResolverMod, Bin} -> {ok, Bin}; + {ok, ResolverMod, Bin, _Warnings} -> {ok, Bin}; + Error -> Error + end, + {module, ResolverMod} = code:load_binary(ResolverMod, + "nofile", + Code), + ok. + +dec_enum(Val, Enums) -> + AtomVal = erlang:binary_to_existing_atom(Val, utf8), + case lists:member(AtomVal, Enums) of + true -> AtomVal + end. + +enc_enum(Atom) -> erlang:atom_to_binary(Atom, utf8). + +pp(pubsub_serverinfo, 1) -> [domain]; +pp(pubsub_serverinfo_domain, 2) -> + [name, remote_domain]; +pp(pubsub_serverinfo_remote_domain, 2) -> [name, type]; +pp(xmlel, 3) -> [name, attrs, children]; +pp(Name, Arity) -> + try get_mod(erlang:make_tuple(Arity + 1, + undefined, + [{1, Name}])) + of + Mod -> Mod:pp(Name, Arity) + catch + error:badarg -> no + end. + +records() -> + [{pubsub_serverinfo, 1}, + {pubsub_serverinfo_domain, 2}, + {pubsub_serverinfo_remote_domain, 2}]. + +get_mod(<<"serverinfo">>, + <<"urn:xmpp:serverinfo:0">>) -> + pubsub_serverinfo_codec; +get_mod(<<"remote-domain">>, + <<"urn:xmpp:serverinfo:0">>) -> + pubsub_serverinfo_codec; +get_mod(<<"federation">>, + <<"urn:xmpp:serverinfo:0">>) -> + pubsub_serverinfo_codec; +get_mod(<<"domain">>, <<"urn:xmpp:serverinfo:0">>) -> + pubsub_serverinfo_codec; +get_mod(<<"connection">>, + <<"urn:xmpp:serverinfo:0">>) -> + pubsub_serverinfo_codec; +get_mod(Name, XMLNS) -> + pubsub_serverinfo_codec_external:lookup(Name, XMLNS). + +get_mod({pubsub_serverinfo, _}) -> + pubsub_serverinfo_codec; +get_mod({pubsub_serverinfo_domain, _, _}) -> + pubsub_serverinfo_codec; +get_mod({pubsub_serverinfo_remote_domain, _, _}) -> + pubsub_serverinfo_codec; +get_mod(Record) -> + pubsub_serverinfo_codec_external:lookup(Record). + +decode_pubsub_serverinfo_connection(__TopXMLNS, __Opts, + {xmlel, <<"connection">>, _attrs, _els}) -> + Type = + decode_pubsub_serverinfo_connection_attrs(__TopXMLNS, + _attrs, + undefined), + Type. + +decode_pubsub_serverinfo_connection_attrs(__TopXMLNS, + [{<<"type">>, _val} | _attrs], + _Type) -> + decode_pubsub_serverinfo_connection_attrs(__TopXMLNS, + _attrs, + _val); +decode_pubsub_serverinfo_connection_attrs(__TopXMLNS, + [_ | _attrs], Type) -> + decode_pubsub_serverinfo_connection_attrs(__TopXMLNS, + _attrs, + Type); +decode_pubsub_serverinfo_connection_attrs(__TopXMLNS, + [], Type) -> + decode_pubsub_serverinfo_connection_attr_type(__TopXMLNS, + Type). + +encode_pubsub_serverinfo_connection(Type, __TopXMLNS) -> + __NewTopXMLNS = + pubsub_serverinfo_codec:choose_top_xmlns(<<"urn:xmpp:serverinfo:0">>, + [], + __TopXMLNS), + _els = [], + _attrs = + encode_pubsub_serverinfo_connection_attr_type(Type, + pubsub_serverinfo_codec:enc_xmlns_attrs(__NewTopXMLNS, + __TopXMLNS)), + {xmlel, <<"connection">>, _attrs, _els}. + +decode_pubsub_serverinfo_connection_attr_type(__TopXMLNS, + undefined) -> + erlang:error({pubsub_serverinfo_codec, + {missing_attr, + <<"type">>, + <<"connection">>, + __TopXMLNS}}); +decode_pubsub_serverinfo_connection_attr_type(__TopXMLNS, + _val) -> + case catch dec_enum(_val, [incoming, outgoing, bidi]) of + {'EXIT', _} -> + erlang:error({pubsub_serverinfo_codec, + {bad_attr_value, + <<"type">>, + <<"connection">>, + __TopXMLNS}}); + _res -> _res + end. + +encode_pubsub_serverinfo_connection_attr_type(_val, + _acc) -> + [{<<"type">>, enc_enum(_val)} | _acc]. + +decode_pubsub_serverinfo_remote_domain(__TopXMLNS, + __Opts, + {xmlel, + <<"remote-domain">>, + _attrs, + _els}) -> + Type = + decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, + _els, + []), + Name = + decode_pubsub_serverinfo_remote_domain_attrs(__TopXMLNS, + _attrs, + undefined), + {pubsub_serverinfo_remote_domain, Name, Type}. + +decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, [], Type) -> + lists:reverse(Type); +decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, + [{xmlel, + <<"connection">>, + _attrs, + _} = + _el + | _els], + Type) -> + case pubsub_serverinfo_codec:get_attr(<<"xmlns">>, + _attrs, + __TopXMLNS) + of + <<"urn:xmpp:serverinfo:0">> -> + decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, + _els, + [decode_pubsub_serverinfo_connection(<<"urn:xmpp:serverinfo:0">>, + __Opts, + _el) + | Type]); + _ -> + decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, + _els, + Type) + end; +decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, [_ | _els], Type) -> + decode_pubsub_serverinfo_remote_domain_els(__TopXMLNS, + __Opts, + _els, + Type). + +decode_pubsub_serverinfo_remote_domain_attrs(__TopXMLNS, + [{<<"name">>, _val} | _attrs], + _Name) -> + decode_pubsub_serverinfo_remote_domain_attrs(__TopXMLNS, + _attrs, + _val); +decode_pubsub_serverinfo_remote_domain_attrs(__TopXMLNS, + [_ | _attrs], Name) -> + decode_pubsub_serverinfo_remote_domain_attrs(__TopXMLNS, + _attrs, + Name); +decode_pubsub_serverinfo_remote_domain_attrs(__TopXMLNS, + [], Name) -> + decode_pubsub_serverinfo_remote_domain_attr_name(__TopXMLNS, + Name). + +encode_pubsub_serverinfo_remote_domain({pubsub_serverinfo_remote_domain, + Name, + Type}, + __TopXMLNS) -> + __NewTopXMLNS = + pubsub_serverinfo_codec:choose_top_xmlns(<<"urn:xmpp:serverinfo:0">>, + [], + __TopXMLNS), + _els = + lists:reverse('encode_pubsub_serverinfo_remote_domain_$type'(Type, + __NewTopXMLNS, + [])), + _attrs = + encode_pubsub_serverinfo_remote_domain_attr_name(Name, + pubsub_serverinfo_codec:enc_xmlns_attrs(__NewTopXMLNS, + __TopXMLNS)), + {xmlel, <<"remote-domain">>, _attrs, _els}. + +'encode_pubsub_serverinfo_remote_domain_$type'([], + __TopXMLNS, _acc) -> + _acc; +'encode_pubsub_serverinfo_remote_domain_$type'([Type + | _els], + __TopXMLNS, _acc) -> + 'encode_pubsub_serverinfo_remote_domain_$type'(_els, + __TopXMLNS, + [encode_pubsub_serverinfo_connection(Type, + __TopXMLNS) + | _acc]). + +decode_pubsub_serverinfo_remote_domain_attr_name(__TopXMLNS, + undefined) -> + erlang:error({pubsub_serverinfo_codec, + {missing_attr, + <<"name">>, + <<"remote-domain">>, + __TopXMLNS}}); +decode_pubsub_serverinfo_remote_domain_attr_name(__TopXMLNS, + _val) -> + _val. + +encode_pubsub_serverinfo_remote_domain_attr_name(_val, + _acc) -> + [{<<"name">>, _val} | _acc]. + +decode_pubsub_serverinfo_federation(__TopXMLNS, __Opts, + {xmlel, <<"federation">>, _attrs, _els}) -> + Remote_domain = + decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, + _els, + []), + Remote_domain. + +decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, [], Remote_domain) -> + lists:reverse(Remote_domain); +decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, + [{xmlel, + <<"remote-domain">>, + _attrs, + _} = + _el + | _els], + Remote_domain) -> + case pubsub_serverinfo_codec:get_attr(<<"xmlns">>, + _attrs, + __TopXMLNS) + of + <<"urn:xmpp:serverinfo:0">> -> + decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, + _els, + [decode_pubsub_serverinfo_remote_domain(<<"urn:xmpp:serverinfo:0">>, + __Opts, + _el) + | Remote_domain]); + _ -> + decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, + _els, + Remote_domain) + end; +decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, [_ | _els], Remote_domain) -> + decode_pubsub_serverinfo_federation_els(__TopXMLNS, + __Opts, + _els, + Remote_domain). + +encode_pubsub_serverinfo_federation(Remote_domain, + __TopXMLNS) -> + __NewTopXMLNS = + pubsub_serverinfo_codec:choose_top_xmlns(<<"urn:xmpp:serverinfo:0">>, + [], + __TopXMLNS), + _els = + lists:reverse('encode_pubsub_serverinfo_federation_$remote_domain'(Remote_domain, + __NewTopXMLNS, + [])), + _attrs = + pubsub_serverinfo_codec:enc_xmlns_attrs(__NewTopXMLNS, + __TopXMLNS), + {xmlel, <<"federation">>, _attrs, _els}. + +'encode_pubsub_serverinfo_federation_$remote_domain'([], + __TopXMLNS, _acc) -> + _acc; +'encode_pubsub_serverinfo_federation_$remote_domain'([Remote_domain + | _els], + __TopXMLNS, _acc) -> + 'encode_pubsub_serverinfo_federation_$remote_domain'(_els, + __TopXMLNS, + [encode_pubsub_serverinfo_remote_domain(Remote_domain, + __TopXMLNS) + | _acc]). + +decode_pubsub_serverinfo_domain(__TopXMLNS, __Opts, + {xmlel, <<"domain">>, _attrs, _els}) -> + Remote_domain = + decode_pubsub_serverinfo_domain_els(__TopXMLNS, + __Opts, + _els, + undefined), + Name = decode_pubsub_serverinfo_domain_attrs(__TopXMLNS, + _attrs, + undefined), + {pubsub_serverinfo_domain, Name, Remote_domain}. + +decode_pubsub_serverinfo_domain_els(__TopXMLNS, __Opts, + [], Remote_domain) -> + Remote_domain; +decode_pubsub_serverinfo_domain_els(__TopXMLNS, __Opts, + [{xmlel, <<"federation">>, _attrs, _} = _el + | _els], + Remote_domain) -> + case pubsub_serverinfo_codec:get_attr(<<"xmlns">>, + _attrs, + __TopXMLNS) + of + <<"urn:xmpp:serverinfo:0">> -> + decode_pubsub_serverinfo_domain_els(__TopXMLNS, + __Opts, + _els, + decode_pubsub_serverinfo_federation(<<"urn:xmpp:serverinfo:0">>, + __Opts, + _el)); + _ -> + decode_pubsub_serverinfo_domain_els(__TopXMLNS, + __Opts, + _els, + Remote_domain) + end; +decode_pubsub_serverinfo_domain_els(__TopXMLNS, __Opts, + [_ | _els], Remote_domain) -> + decode_pubsub_serverinfo_domain_els(__TopXMLNS, + __Opts, + _els, + Remote_domain). + +decode_pubsub_serverinfo_domain_attrs(__TopXMLNS, + [{<<"name">>, _val} | _attrs], _Name) -> + decode_pubsub_serverinfo_domain_attrs(__TopXMLNS, + _attrs, + _val); +decode_pubsub_serverinfo_domain_attrs(__TopXMLNS, + [_ | _attrs], Name) -> + decode_pubsub_serverinfo_domain_attrs(__TopXMLNS, + _attrs, + Name); +decode_pubsub_serverinfo_domain_attrs(__TopXMLNS, [], + Name) -> + decode_pubsub_serverinfo_domain_attr_name(__TopXMLNS, + Name). + +encode_pubsub_serverinfo_domain({pubsub_serverinfo_domain, + Name, + Remote_domain}, + __TopXMLNS) -> + __NewTopXMLNS = + pubsub_serverinfo_codec:choose_top_xmlns(<<"urn:xmpp:serverinfo:0">>, + [], + __TopXMLNS), + _els = + lists:reverse('encode_pubsub_serverinfo_domain_$remote_domain'(Remote_domain, + __NewTopXMLNS, + [])), + _attrs = encode_pubsub_serverinfo_domain_attr_name(Name, + pubsub_serverinfo_codec:enc_xmlns_attrs(__NewTopXMLNS, + __TopXMLNS)), + {xmlel, <<"domain">>, _attrs, _els}. + +'encode_pubsub_serverinfo_domain_$remote_domain'(undefined, + __TopXMLNS, _acc) -> + _acc; +'encode_pubsub_serverinfo_domain_$remote_domain'(Remote_domain, + __TopXMLNS, _acc) -> + [encode_pubsub_serverinfo_federation(Remote_domain, + __TopXMLNS) + | _acc]. + +decode_pubsub_serverinfo_domain_attr_name(__TopXMLNS, + undefined) -> + erlang:error({pubsub_serverinfo_codec, + {missing_attr, <<"name">>, <<"domain">>, __TopXMLNS}}); +decode_pubsub_serverinfo_domain_attr_name(__TopXMLNS, + _val) -> + _val. + +encode_pubsub_serverinfo_domain_attr_name(_val, _acc) -> + [{<<"name">>, _val} | _acc]. + +decode_pubsub_serverinfo(__TopXMLNS, __Opts, + {xmlel, <<"serverinfo">>, _attrs, _els}) -> + Domain = decode_pubsub_serverinfo_els(__TopXMLNS, + __Opts, + _els, + []), + {pubsub_serverinfo, Domain}. + +decode_pubsub_serverinfo_els(__TopXMLNS, __Opts, [], + Domain) -> + lists:reverse(Domain); +decode_pubsub_serverinfo_els(__TopXMLNS, __Opts, + [{xmlel, <<"domain">>, _attrs, _} = _el | _els], + Domain) -> + case pubsub_serverinfo_codec:get_attr(<<"xmlns">>, + _attrs, + __TopXMLNS) + of + <<"urn:xmpp:serverinfo:0">> -> + decode_pubsub_serverinfo_els(__TopXMLNS, + __Opts, + _els, + [decode_pubsub_serverinfo_domain(<<"urn:xmpp:serverinfo:0">>, + __Opts, + _el) + | Domain]); + _ -> + decode_pubsub_serverinfo_els(__TopXMLNS, + __Opts, + _els, + Domain) + end; +decode_pubsub_serverinfo_els(__TopXMLNS, __Opts, + [_ | _els], Domain) -> + decode_pubsub_serverinfo_els(__TopXMLNS, + __Opts, + _els, + Domain). + +encode_pubsub_serverinfo({pubsub_serverinfo, Domain}, + __TopXMLNS) -> + __NewTopXMLNS = + pubsub_serverinfo_codec:choose_top_xmlns(<<"urn:xmpp:serverinfo:0">>, + [], + __TopXMLNS), + _els = + lists:reverse('encode_pubsub_serverinfo_$domain'(Domain, + __NewTopXMLNS, + [])), + _attrs = + pubsub_serverinfo_codec:enc_xmlns_attrs(__NewTopXMLNS, + __TopXMLNS), + {xmlel, <<"serverinfo">>, _attrs, _els}. + +'encode_pubsub_serverinfo_$domain'([], __TopXMLNS, + _acc) -> + _acc; +'encode_pubsub_serverinfo_$domain'([Domain | _els], + __TopXMLNS, _acc) -> + 'encode_pubsub_serverinfo_$domain'(_els, + __TopXMLNS, + [encode_pubsub_serverinfo_domain(Domain, + __TopXMLNS) + | _acc]). diff --git a/src/pubsub_serverinfo_codec_external.erl b/src/pubsub_serverinfo_codec_external.erl new file mode 100644 index 000000000..2f2df47e3 --- /dev/null +++ b/src/pubsub_serverinfo_codec_external.erl @@ -0,0 +1,12 @@ +%% Created automatically by XML generator (fxml_gen.erl) +%% Source: pubsub_serverinfo_codec.spec + +-module(pubsub_serverinfo_codec_external). + +-compile(export_all). + +modules() -> []. + +lookup(_, _) -> undefined. + +lookup(Term) -> erlang:error(badarg, [Term]).