diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3268dac..a58b25c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: name: OTP ${{matrix.otp_vsn}} strategy: matrix: - otp_vsn: ['28', '27', '26'] + otp_vsn: ['28', '27'] rebar_vsn: ['3.25'] runs-on: 'ubuntu-24.04' env: diff --git a/include/escalus_xmlns.hrl b/include/escalus_xmlns.hrl index 0b766a9..4976ca9 100644 --- a/include/escalus_xmlns.hrl +++ b/include/escalus_xmlns.hrl @@ -552,12 +552,18 @@ % Defined by XEP-0297: Stanza Forwarding -define(NS_FORWARD_0, <<"urn:xmpp:forward:0">>). +% Defined by XEP-0300: Use of Cryptographic Hash Functions in XMPP +-define(NS_HASH, <<"urn:xmpp:hashes">>). + % Defined by XEP-0313: Message Archive Management (MAM) -define(NS_MAM, <<"urn:xmpp:mam:0">>). % Defined by XEP-0333: Chat Markers -define(NS_CHAT_MARKERS, <<"urn:xmpp:chat-markers:0">>). +% Defined by XEP-0390: Entity Capabilities 2.0. +-define(NS_CAPS_2, <<"urn:xmpp:caps">>). + % Defined by XEP-0402: PEP Native Bookmarks -define(NS_PEP_BOOKMARKS, <<"urn:xmpp:bookmarks:1">>). diff --git a/src/escalus_session.erl b/src/escalus_session.erl index 8c3d939..94b9afe 100644 --- a/src/escalus_session.erl +++ b/src/escalus_session.erl @@ -345,15 +345,9 @@ get_sasl_channel_bindings(Features) -> {element, <<"channel-binding">>}, {attr, <<"type">>}]). --spec get_server_caps(exml:element()) -> undefined | map(). +-spec get_server_caps(exml:element()) -> [exml:element()]. get_server_caps(Features) -> - case exml_query:subelement(Features, <<"c">>) of - #xmlel{attrs = Attrs} -> - Attrs; - _ -> - undefined - end. - + exml_query:subelements(Features, ~"c"). -spec stream_start_to_element(exml_stream:element()) -> exml:element(). stream_start_to_element(#xmlel{name = <<"open">>} = Open) -> Open; diff --git a/src/escalus_stanza.erl b/src/escalus_stanza.erl index d4f77d4..b4d13e1 100644 --- a/src/escalus_stanza.erl +++ b/src/escalus_stanza.erl @@ -71,10 +71,14 @@ x_data_form/2, field_el/3]). +%% XEP-0030: Service Discovery -export([disco_info/1, disco_info/2, disco_items/1, - disco_items/2 + disco_items/2, + feature/1, + identity/3, + identity/4 ]). -export([vcard_update/1, @@ -132,6 +136,12 @@ -export([remove_account/0]). +%% XEP-0115: Entity Capabilities +%% XEP-0390: Entity Capabilities 2.0 +-export([caps/3, + caps_to_node/1, + ns_caps/1]). + %% Stanzas from inline XML -export([from_template/2, from_xml/1]). @@ -629,6 +639,8 @@ privacy_list_jid_item(Order, Action, Who, Contents) -> privacy_list_item(Order, Action, <<"jid">>, escalus_utils:get_jid(Who), Contents). +%% XEP-0030 Service Discovery +%% -spec disco_info(escalus_utils:jid_spec()) -> exml:element(). disco_info(JID) -> Query = query_el(?NS_DISCO_INFO, []), @@ -649,6 +661,21 @@ disco_items(JID, Node) -> ItemsQuery = query_el(?NS_DISCO_ITEMS, #{<<"node">> => Node}, []), iq(JID, <<"get">>, [ItemsQuery]). +-spec feature(binary()) -> exml:element(). +feature(Feature) -> + #xmlel{name = ~"feature", attrs = #{~"var" => Feature}}. + +-spec identity(binary() | undefined, binary() | undefined, binary() | undefined) -> exml:element(). +identity(Category, Type, Name) -> + identity(Category, Type, Name, #{}). + +-spec identity(binary() | undefined, binary() | undefined, binary() | undefined, + #{binary() => binary()}) -> exml:element(). +identity(Category, Type, Name, ExtraAttrs) -> + BasicAttrs = #{~"category" => Category, ~"type" => Type, ~"name" => Name}, + Attrs = skip_undefined(maps:merge(BasicAttrs, ExtraAttrs)), + #xmlel{name = ~"identity", attrs = Attrs}. + -spec search_fields([null | {binary(), binary()} | term()]) -> [exml:element()]. search_fields([]) -> []; @@ -933,6 +960,37 @@ marker_el(MarkerName, MessageId) when MarkerName =:= <<"received">> orelse is_binary(MessageId) -> #xmlel{name = MarkerName, attrs = #{<<"xmlns">> => ?NS_CHAT_MARKERS, <<"id">> => MessageId}}. +%% XEP-0115 Entity Capabilities (v1) +%% XEP-0390 Entity Capabilities 2.0 (v2) +%% +-type caps_version() :: v1 | v2. + +-spec caps(binary(), binary(), caps_version()) -> exml:element(). +caps(HashAlg, HashValue, v1) -> + #xmlel{name = ~"c", attrs = #{~"xmlns" => ?NS_CAPS, + ~"hash" => HashAlg, + ~"node" => id(), + ~"ver" => HashValue}}; +caps(HashAlg, HashValue, v2) -> + #xmlel{name = ~"c", + attrs = #{~"xmlns" => ?NS_CAPS_2}, + children = [#xmlel{name = ~"hash", + attrs = #{~"xmlns" => ?NS_HASH, ~"algo" => HashAlg}, + children = [#xmlcdata{content = HashValue}]}]}. + +-spec caps_to_node(exml:element()) -> binary(). +caps_to_node(#xmlel{name = ~"c", attrs = #{~"xmlns" := ?NS_CAPS, ~"node" := Node, ~"ver" := Ver}}) -> + <>; +caps_to_node(#xmlel{name = ~"c", attrs = #{~"xmlns" := ?NS_CAPS_2}} = Caps) -> + [HashEl | _] = exml_query:subelements_with_name_and_ns(Caps, ~"hash", ?NS_HASH), + HashAlg = exml_query:attr(HashEl, ~"algo"), + HashVal = exml_query:cdata(HashEl), + <<(?NS_CAPS_2)/binary, $#, HashAlg/binary, $., HashVal/binary>>. + +-spec ns_caps(caps_version()) -> binary(). +ns_caps(v1) -> ?NS_CAPS; +ns_caps(v2) -> ?NS_CAPS_2. + -spec id() -> binary(). id() -> binary:encode_hex(crypto:strong_rand_bytes(16), lowercase).