diff --git a/big_tests/default.spec b/big_tests/default.spec index 3d97476dc9..d1ee2f0d47 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -33,6 +33,7 @@ {suites, "tests", graphql_roster_SUITE}. {suites, "tests", graphql_session_SUITE}. {suites, "tests", graphql_stanza_SUITE}. +{suites, "tests", graphql_vcard_SUITE}. {suites, "tests", inbox_SUITE}. {suites, "tests", inbox_extensions_SUITE}. {suites, "tests", jingle_SUITE}. diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index b5161b532d..1b00f53713 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -49,6 +49,7 @@ {suites, "tests", graphql_roster_SUITE}. {suites, "tests", graphql_session_SUITE}. {suites, "tests", graphql_stanza_SUITE}. +{suites, "tests", graphql_vcard_SUITE}. {suites, "tests", inbox_SUITE}. diff --git a/big_tests/tests/graphql_vcard_SUITE.erl b/big_tests/tests/graphql_vcard_SUITE.erl new file mode 100644 index 0000000000..16a6416e31 --- /dev/null +++ b/big_tests/tests/graphql_vcard_SUITE.erl @@ -0,0 +1,534 @@ +-module(graphql_vcard_SUITE). + +-compile([export_all, nowarn_export_all]). + +-import(distributed_helper, [require_rpc_nodes/1]). +-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("exml/include/exml.hrl"). +-include_lib("escalus/include/escalus.hrl"). +-include_lib("../../include/mod_roster.hrl"). + +suite() -> + require_rpc_nodes([mim]) ++ escalus:suite(). + +all() -> + [{group, user_vcard}, {group, admin_vcard}]. + +groups() -> + [{user_vcard, [], user_vcard_handler()}, + {admin_vcard, [], admin_vcard_handler()}]. + +user_vcard_handler() -> + [user_set_vcard, + user_get_their_vcard, + user_get_their_vcard_no_vcard, + user_get_others_vcard, + user_get_others_vcard_no_user, + user_get_others_vcard_no_vcard]. + +admin_vcard_handler() -> + [admin_set_vcard, + admin_set_vcard_incomplete_fields, + admin_set_vcard_no_user, + admin_get_vcard, + admin_get_vcard_no_vcard, + admin_get_vcard_no_user]. + +init_per_suite(Config) -> + case vcard_helper:is_vcard_ldap() of + true -> + {skip, ldap_vcard_is_not_supported}; + _ -> + Config2 = escalus:init_per_suite(Config), + dynamic_modules:save_modules(domain_helper:host_type(), Config2) + end. + +end_per_suite(Config) -> + dynamic_modules:restore_modules(Config), + escalus:end_per_suite(Config). + +init_per_group(admin_vcard, Config) -> + graphql_helper:init_admin_handler(Config); +init_per_group(user_vcard, Config) -> + [{schema_endpoint, user} | Config]. + +end_per_group(admin_vcard, _Config) -> + escalus_fresh:clean(); +end_per_group(user_vcard, _Config) -> + escalus_fresh:clean(). + +init_per_testcase(CaseName, Config) -> + escalus:init_per_testcase(CaseName, Config). + +end_per_testcase(CaseName, Config) -> + escalus:end_per_testcase(CaseName, Config). + +% User test cases + +user_set_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_set_vcard/2). + +user_set_vcard(Config, Alice) -> + Vcard = complete_vcard_input(), + BodySet = set_vcard_body_user(Vcard), + GraphQlRequestSet = execute_user(BodySet, Alice, Config), + ParsedResultSet = ok_result(<<"vcard">>, <<"setVcard">>, GraphQlRequestSet), + ?assertEqual(Vcard, ParsedResultSet), + QueryGet = user_get_full_vcard_as_result_query(), + BodyGet = #{query => QueryGet, operationName => <<"Q1">>, variables => #{}}, + GraphQlRequestGet = execute_user(BodyGet, Alice, Config), + ParsedResultGet = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequestGet), + ?assertEqual(Vcard, ParsedResultGet). + +user_get_their_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_their_vcard/2). + +user_get_their_vcard(Config, Alice) -> + Client1Fields = [{<<"FN">>, <<"TESTNAME">>}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL">>}, + {<<"HOME">>, []}, {"WORK", []}]}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL2">>}, + {<<"HOME">>, []}]}], + ExpectedResult = #{<<"formattedName">> => <<"TESTNAME">>, + <<"email">> => [#{<<"userId">> => <<"TESTEMAIL">>, <<"tags">> => [<<"HOME">>, <<"WORK">>]}, + #{<<"userId">> => <<"TESTEMAIL2">>, <<"tags">> => [<<"HOME">>]}]}, + escalus_client:send_and_wait(Alice, escalus_stanza:vcard_update(Client1Fields)), + Body = #{query => user_get_query(), operationName => <<"Q1">>, variables => #{}}, + GraphQlRequest = execute_user(Body, Alice, Config), + ParsedResult = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequest), + ?assertEqual(ExpectedResult, ParsedResult). + +user_get_their_vcard_no_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_their_vcard_no_vcard/2). + +user_get_their_vcard_no_vcard(Config, Alice) -> + Body = #{query => user_get_query(), operationName => <<"Q1">>, variables => #{}}, + GraphQlRequest = execute_user(Body, Alice, Config), + ParsedResult = error_result(<<"message">>, GraphQlRequest), + ?assertEqual(<<"Vcard for user not found">>, ParsedResult). + +user_get_others_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_get_others_vcard/3). + +user_get_others_vcard(Config, Alice, Bob) -> + Client1Fields = [{<<"FN">>, <<"TESTNAME">>}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL">>}, + {<<"HOME">>, []}, {"WORK", []}]}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL2">>}, + {<<"HOME">>, []}]}], + ExpectedResult = #{<<"formattedName">> => <<"TESTNAME">>, + <<"email">> => [#{<<"userId">> => <<"TESTEMAIL">>, <<"tags">> => [<<"HOME">>, <<"WORK">>]}, + #{<<"userId">> => <<"TESTEMAIL2">>, <<"tags">> => [<<"HOME">>]}]}, + escalus_client:send_and_wait(Bob, escalus_stanza:vcard_update([{<<"VERSION">>, <<"TESTVERSION">>} | Client1Fields])), + Body = #{query => user_get_query(), operationName => <<"Q1">>, + variables => #{user => user_to_bin(Bob)}}, + GraphQlRequest = execute_user(Body, Alice, Config), + ParsedResult = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequest), + ?assertEqual(ExpectedResult, ParsedResult). + +user_get_others_vcard_no_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_get_others_vcard_no_vcard/3). + +user_get_others_vcard_no_vcard(Config, Alice, Bob) -> + Body = #{query => user_get_query(), operationName => <<"Q1">>, + variables => #{user => user_to_bin(Bob)}}, + GraphQlRequest = execute_user(Body, Alice, Config), + ParsedResult = error_result(<<"message">>, GraphQlRequest), + ?assertEqual(<<"Vcard for user not found">>, ParsedResult). + +user_get_others_vcard_no_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_others_vcard_no_user/2). + +user_get_others_vcard_no_user(Config, Alice) -> + Body = #{query => user_get_query(), operationName => <<"Q1">>, + variables => #{user => <<"AAAAA">>}}, + GraphQlRequest = execute_user(Body, Alice, Config), + ParsedResult = error_result(<<"message">>, GraphQlRequest), + ?assertEqual(<<"User does not exist">>, ParsedResult). + +%% Admin test cases + +admin_set_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_set_vcard/3). + +admin_set_vcard(Config, Alice, _Bob) -> + Vcard = complete_vcard_input(), + BodySet = set_vcard_body_admin(Vcard, user_to_bin(Alice)), + GraphQlRequestSet = execute_auth(BodySet, Config), + ParsedResultSet = ok_result(<<"vcard">>, <<"setVcard">>, GraphQlRequestSet), + ?assertEqual(Vcard, ParsedResultSet), + QueryGet = admin_get_full_vcard_as_result_query(), + VarsGet = #{user => user_to_bin(Alice)}, + BodyGet = #{query => QueryGet, operationName => <<"Q1">>, variables => VarsGet}, + GraphQlRequestGet = execute_auth(BodyGet, Config), + ParsedResultGet = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequestGet), + ?assertEqual(Vcard, ParsedResultGet). + +admin_set_vcard_incomplete_fields(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_set_vcard_incomplete_fields/3). + +admin_set_vcard_incomplete_fields(Config, Alice, _Bob) -> + QuerySet = admin_get_full_vcard_as_result_mutation(), + VcardInput = address_vcard_input(), + [VcardAddress] = maps:get(<<"address">> ,VcardInput), + Vcard = [maps:put(<<"pcode">>, null, VcardAddress)], + VarsSet = #{user => user_to_bin(Alice), vcard => VcardInput}, + BodySet = #{query => QuerySet, operationName => <<"M1">>, variables => VarsSet}, + GraphQlRequestSet = execute_auth(BodySet, Config), + ParsedResultSet = ok_result(<<"vcard">>, <<"setVcard">>, GraphQlRequestSet), + ?assertEqual(Vcard, maps:get(<<"address">>, ParsedResultSet)), + QueryGet = admin_get_full_vcard_as_result_query(), + VarsGet = #{user => user_to_bin(Alice)}, + BodyGet = #{query => QueryGet, operationName => <<"Q1">>, variables => VarsGet}, + GraphQlRequestGet = execute_auth(BodyGet, Config), + ParsedResultGet = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequestGet), + ?assertEqual(Vcard, maps:get(<<"address">>, ParsedResultGet)). + +admin_set_vcard_no_user(Config) -> + Query = admin_get_full_vcard_as_result_mutation(), + OpName = <<"M1">>, + Vcard = complete_vcard_input(), + Vars = #{user => <<"AAAAA">>, vcard => Vcard}, + Body = #{query => Query, operationName => OpName, variables => Vars}, + GraphQlRequest = execute_auth(Body, Config), + ParsedResult = error_result(<<"message">>, GraphQlRequest), + ?assertEqual(<<"User does not exist">>, ParsedResult). + +admin_get_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_get_vcard/3). + +admin_get_vcard(Config, Alice, _Bob) -> + Client1Fields = [{<<"ADR">>, [{<<"POBOX">>, <<"TESTPobox">>}, {<<"EXTADD">>, <<"TESTExtadd">>}, + {<<"STREET">>, <<"TESTStreet">>}, {<<"LOCALITY">>, <<"TESTLocality">>}, + {<<"REGION">>, <<"TESTRegion">>}, {<<"PCODE">>, <<"TESTPCODE">>}, + {<<"CTRY">>, <<"TESTCTRY">>}, {<<"HOME">>, []}, {<<"WORK">>, []}]}], + ExpectedResult = #{<<"address">> => [#{<<"pobox">> => <<"TESTPobox">>, + <<"extadd">> => <<"TESTExtadd">>, <<"street">> => <<"TESTStreet">>, + <<"locality">> => <<"TESTLocality">>, <<"region">> => <<"TESTRegion">>, + <<"pcode">> => <<"TESTPCODE">>, <<"country">> => <<"TESTCTRY">>, + <<"tags">> => [<<"HOME">>, <<"WORK">>]}]}, + escalus_client:send_and_wait(Alice, escalus_stanza:vcard_update(Client1Fields)), + Vars = #{user => user_to_bin(Alice)}, + Body = #{query => admin_get_address_query(), operationName => <<"Q1">>, variables => Vars}, + GraphQlRequest = execute_auth(Body, Config), + ParsedResult = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequest), + ?assertEqual(ExpectedResult, ParsedResult). + +admin_get_vcard_no_vcard(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_get_vcard_no_vcard/2). + +admin_get_vcard_no_vcard(Config, Alice) -> + Query = admin_get_address_query(), + OpName = <<"Q1">>, + Vars = #{user => user_to_bin(Alice)}, + Body = #{query => Query, operationName => OpName, variables => Vars}, + GraphQlRequest = execute_auth(Body, Config), + ParsedResult = error_result(<<"message">>, GraphQlRequest), + ?assertEqual(<<"Vcard for user not found">>, ParsedResult). + +admin_get_vcard_no_user(Config) -> + Query = admin_get_address_query(), + OpName = <<"Q1">>, + Vars = #{user => <<"AAAAA">>}, + Body = #{query => Query, operationName => OpName, variables => Vars}, + GraphQlRequest = execute_auth(Body, Config), + ParsedResult = error_result(<<"message">>, GraphQlRequest), + ?assertEqual(<<"User does not exist">>, ParsedResult). + +%% Helpers + +ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) -> + maps:get(What2, maps:get(What1, Data)). + +error_result(What, {{<<"200">>, <<"OK">>}, #{<<"errors">> := [Data]}}) -> + maps:get(What, Data). + +set_vcard_body_user(Body) -> + QuerySet = user_get_full_vcard_as_result_mutation(), + #{query => QuerySet, operationName => <<"M1">>, variables => #{vcard => Body}}. + +set_vcard_body_admin(Body, User) -> + QuerySet = admin_get_full_vcard_as_result_mutation(), + #{query => QuerySet, operationName => <<"M1">>, variables => #{vcard => Body, user => User}}. + +address_vcard_input() -> + #{<<"formattedName">> => <<"TestName">>, + <<"nameComponents">> => #{}, + <<"address">> => [ + #{ + <<"tags">> => [<<"HOME">>, <<"WORK">>], + <<"pobox">> => <<"poboxTest">>, + <<"extadd">> => <<"extaddTest">>, + <<"street">> => <<"TESTSTREET123">>, + <<"locality">> => <<"LOCALITY123">>, + <<"region">> => <<"REGION777">>, + <<"country">> => <<"COUNTRY123">> + } + ]}. + +complete_vcard_input() -> + #{<<"formattedName">> => <<"TestName">>, + <<"nameComponents">> => #{ + <<"family">> => <<"familyName">>, + <<"givenName">> => <<"givenName">>, + <<"middleName">> => <<"middleName">>, + <<"prefix">> => <<"prefix">>, + <<"suffix">> => <<"sufix">> + }, + <<"nickname">> => [<<"NicknameTest">>, <<"SecondNickname">>], + <<"photo">> => [ + #{<<"type">> => <<"image/jpeg">>, + <<"binValue">> => <<"TestBinaries">>}, + #{<<"extValue">> => <<"External Value">>} + ], + <<"birthday">> => [<<"birthdayTest">>, <<"SecondBirthday">>], + <<"address">> => [ + #{ + <<"tags">> => [<<"HOME">>, <<"WORK">>], + <<"pobox">> => <<"poboxTest">>, + <<"extadd">> => <<"extaddTest">>, + <<"street">> => <<"TESTSTREET123">>, + <<"locality">> => <<"LOCALITY123">>, + <<"region">> => <<"REGION777">>, + <<"pcode">> => <<"PcodeTest">>, + <<"country">> => <<"COUNTRY123">> + }, + #{ + <<"tags">> => [<<"HOME">>, <<"WORK">>, <<"POSTAL">>], + <<"pobox">> => <<"poboxTestSecond">>, + <<"extadd">> => <<"extaddTestSecond">>, + <<"street">> => <<"TESTSTREET123Second">>, + <<"locality">> => <<"LOCALITY123Second">>, + <<"region">> => <<"REGION777TEST">>, + <<"pcode">> => <<"PcodeTestSECOND">>, + <<"country">> => <<"COUNTRY123SECOND">> + } + ], + <<"label">> => [ + #{<<"tags">> => [<<"WORK">>, <<"HOME">>], <<"line">> => [<<"LineTest">>, <<"AAA">>]}, + #{<<"tags">> => [<<"POSTAL">>, <<"WORK">>], <<"line">> => [<<"LineTest2">>, <<"AAA">>]} + ], + <<"telephone">> => [ + #{<<"tags">> => [<<"HOME">>, <<"BBS">>], <<"number">> => <<"590190190">>}, + #{<<"tags">> => [<<"WORK">>, <<"BBS">>], <<"number">> => <<"590190191">>} + ], + <<"email">> => [ + #{<<"tags">> => [<<"PREF">>], <<"userId">> => <<"userIDTEst">>}, + #{<<"tags">> => [<<"PREF">>, <<"HOME">>], <<"userId">> => <<"userIDTEsTETt">>} + ], + <<"jabberId">> => [<<"JabberId">>, <<"JabberIDSecind">>], + <<"mailer">> => [<<"MailerTest">>, <<"MailerSecond">>], + <<"timeZone">> => [<<"TimeZoneTest">>, <<"TimeZOneSecond">>], + <<"geo">> => [ + #{<<"lat">> => <<"LatitudeTest">>, <<"lon">> => <<"LongtitudeTest">>}, + #{<<"lat">> => <<"LatitudeTest2">>, <<"lon">> => <<"LongtitudeTest2">>} + ], + <<"title">> => [<<"TitleTest">>, <<"SEcondTest">>], + <<"role">> => [<<"roleTest">>, <<"SecondRole">>], + <<"logo">> => [ + #{<<"type">> => <<"image/jpeg">>, + <<"binValue">> => <<"TestBinariesLogo">>}, + #{<<"extValue">> => <<"External Value Logo">>} + ], + <<"agent">> => [ + #{<<"vcard">> => agent_vcard_input()}, + #{<<"extValue">> => <<"TESTVALUE">>}, + #{<<"vcard">> => agent_vcard_input()} + ], + <<"org">> => [ + #{<<"orgname">> => <<"TESTNAME">>, <<"orgunit">> => [<<"test1">>, <<"TEST2">>]}, + #{<<"orgname">> => <<"TESTNAME123">>, <<"orgunit">> => [<<"test1">>]} + ], + <<"categories">> => [ + #{<<"keyword">> => [<<"KeywordTest">>]}, + #{<<"keyword">> => [<<"KeywordTest">>, <<"Keyword2">>]} + ], + <<"note">> => [<<"NoteTest">>, <<"NOTE2">>], + <<"prodId">> => [<<"ProdIdTest">>, <<"PRodTEST2">>], + <<"rev">> => [<<"revTest">>, <<"RevTest2">>], + <<"sortString">> => [<<"sortStringTest">>, <<"String2">>], + <<"sound">> => [ + #{<<"binValue">> => <<"TestBinValue">>}, + #{<<"phonetic">> => <<"PhoneticTest">>}, + #{<<"extValue">> => <<"ExtValueTest">>} + ], + <<"uid">> => [<<"UidTest">>, <<"UID2">>], + <<"url">> => [<<"UrlTest">>, <<"URL2">>], + <<"desc">> => [<<"DescTest">>, <<"DESC2">>], + <<"class">> => [ + #{<<"tags">> => [<<"CONFIDENTIAL">>, <<"PRIVATE">>]}, + #{<<"tags">> => [<<"CONFIDENTIAL">>]} + ], + <<"key">> => [ + #{<<"type">> => <<"TYPETEST1">>, <<"credential">> => <<"TESTCREDENTIAL1">>}, + #{<<"type">> => <<"TYPETEST2">>, <<"credential">> => <<"TESTCREDENTIAL2">>} + ] + }. + +agent_vcard_input() -> + #{<<"formattedName">> => <<"TestName">>, + <<"nameComponents">> => #{ + <<"family">> => <<"familyName">>, + <<"givenName">> => <<"givenName">>, + <<"middleName">> => <<"middleName">>, + <<"prefix">> => <<"prefix">>, + <<"suffix">> => <<"sufix">> + }, + <<"nickname">> => [<<"NicknameTest">>, <<"SecondNickname">>], + <<"photo">> => [ + #{<<"type">> => <<"image/jpeg">>, + <<"binValue">> => <<"TestBinaries">>}, + #{<<"extValue">> => <<"External Value">>} + ]}. + +admin_get_address_query() -> + <<"query Q1($user: JID!) + { + vcard + { + getVcard(user: $user) + { + address + { + tags + pobox + extadd + street + locality + region + pcode + country + } + } + } + }">>. + +user_get_query() -> + <<"query Q1($user: JID) + { + vcard + { + getVcard(user: $user) + { + formattedName + email + { + userId + tags + } + } + } + }">>. + +admin_get_full_vcard_as_result_mutation() -> + ResultFormat = get_full_vcard_as_result(), + <<"mutation M1($vcard: VcardInput!, $user: JID!)", + "{vcard{setVcard(vcard: $vcard, user: $user)", ResultFormat/binary, "}}">>. + +user_get_full_vcard_as_result_mutation() -> + ResultFormat = get_full_vcard_as_result(), + <<"mutation M1($vcard: VcardInput!){vcard{setVcard(vcard: $vcard)", + ResultFormat/binary, "}}">>. + +user_get_full_vcard_as_result_query() -> + ResultFormat = get_full_vcard_as_result(), + <<"query Q1($user: JID){vcard{getVcard(user: $user)", + ResultFormat/binary, "}}">>. + +admin_get_full_vcard_as_result_query() -> + ResultFormat = get_full_vcard_as_result(), + <<"query Q1($user: JID!)", + "{vcard{getVcard(user: $user)", ResultFormat/binary, "}}">>. + +get_full_vcard_as_result() -> + <<"{ + formattedName + nameComponents + { family givenName middleName prefix suffix } + nickname + photo + { + ... on ImageData + { type binValue } + ... on External + { extValue } + } + birthday + address + { tags pobox extadd street locality region pcode country } + label + { tags line } + telephone + { tags number } + email + { tags userId } + jabberId + mailer + timeZone + geo + { lat lon } + title + role + logo + { + ... on ImageData + { type binValue } + ... on External + { extValue } + } + agent + { + ... on External + { extValue } + ... on AgentVcard + { + vcard + { + formattedName + nameComponents + { family givenName middleName prefix suffix } + nickname + photo + { + ... on ImageData + { type binValue } + ... on External + { extValue } + } + } + } + } + org + { orgname orgunit } + categories + { keyword } + note + prodId + rev + sortString + sound + { + ... on External + { extValue } + ... on BinValue + { binValue } + ... on Phonetic + { phonetic } + } + uid + url + desc + class + { tags } + key + { type credential } + }">>. diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index 318b2ab951..6ae9192561 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -23,7 +23,9 @@ type AdminQuery{ "MUC Light room management" muc_light: MUCLightAdminQuery "Roster/Contacts management" - roster: RosterAdminQuery + roster: RosterAdminQuery + "Vcard management" + vcard: VcardAdminQuery } """ @@ -45,4 +47,6 @@ type AdminMutation @protected{ muc_light: MUCLightAdminMutation "Roster/Contacts management" roster: RosterAdminMutation + "Vcard management" + vcard: VcardAdminMutation } diff --git a/priv/graphql/schemas/admin/vcard.gql b/priv/graphql/schemas/admin/vcard.gql new file mode 100644 index 0000000000..ad57eac245 --- /dev/null +++ b/priv/graphql/schemas/admin/vcard.gql @@ -0,0 +1,16 @@ +""" +Allow admin to set user's vcard +""" +type VcardAdminMutation @protected{ + "Set a new vcard for a user" + setVcard(user: JID!, vcard: VcardInput!): Vcard + @protected(type: DOMAIN, args: ["user"]) +} + +""" +Allow admin to get user's vcard +""" +type VcardAdminQuery @protected{ + "Get user's vcard" + getVcard(user: JID!): Vcard +} diff --git a/priv/graphql/schemas/global/vcard.gql b/priv/graphql/schemas/global/vcard.gql new file mode 100644 index 0000000000..30810d747b --- /dev/null +++ b/priv/graphql/schemas/global/vcard.gql @@ -0,0 +1,325 @@ +type Vcard{ + "Formatted name from Vcard" + formattedName: String + "Person's name details" + nameComponents: NameComponents + "User's nickname" + nickname: [String] + "User's photo" + photo: [Image] + "Birthday date" + birthday: [String] + address: [Address] + label: [Label] + telephone: [Telephone] + email: [Email] + jabberId: [String] + mailer: [String] + timeZone: [String] + "Geographical position" + geo: [GeographicalPosition] + title: [String] + role: [String] + logo: [Image] + agent: [Agent] + "Organization" + org: [Organization] + categories: [Keyword] + note: [String] + "Identifier of product that generated the vCard property" + prodId: [String] + "Last revised property. The value must be an ISO 8601 formatted UTC date/time" + rev: [String] + "Sort string property" + sortString: [String] + "Formatted name pronunciation property" + sound: [Sound] + "Unique identifier property" + uid: [String] + "Directory URL property" + url: [String] + "Free-form descriptive text" + desc: [String] + "Privacy classification property" + class: [Privacy] + "Authentication credential or encryption key property" + key: [Key] +} + +type Keyword{ + keyword: [String] +} + +type NameComponents{ + family: String + givenName: String + middleName: String + prefix: String + suffix: String +} + +type Address{ + "Address tags" + tags: [AddressTags] + "Post office box" + pobox: String + "Extra address data" + extadd: String + street: String + locality: String + region: String + "Postal code" + pcode: String + country: String +} + +type Label{ + "Label tags" + tags: [AddressTags] + line: [String] +} + +type Telephone{ + "Telephone tags" + tags: [TelephoneTags] + number: String +} + +type Email{ + "Email tags" + tags: [EmailTags] + "Email address" + userId: String +} + +type ImageData{ + type: String + binValue: String +} + +type External{ + extValue: String +} + +type Phonetic { + phonetic: String +} + +type BinValue{ + binValue: String +} + +type AgentVcard{ + vcard: Vcard +} + +union Image = ImageData | External + +union Sound = Phonetic | BinValue | External + +union Agent = AgentVcard | External + +type GeographicalPosition{ + "Geographical latitude" + lat: String + "Geographical longtitude" + lon: String +} + +type Organization{ + "Organization name" + orgname: String + "Organization unit" + orgunit: [String] +} + +type Privacy{ + tags: [PrivacyClassificationTags] +} + +type Key{ + type: String + credential: String +} + +input VcardInput{ + "Formatted name from Vcard" + formattedName: String! + "Person's name details" + nameComponents: NameComponentsInput! + "User's nickname" + nickname: [String] + "User's photo" + photo: [ImageInput!] + "Birthday date" + birthday: [String!] + address: [AddressInput!] + label: [LabelInput!] + telephone: [TelephoneInput!] + email: [EmailInput!] + jabberId: [String!] + mailer: [String!] + timeZone: [String!] + "Geographical position" + geo: [GeographicalPositionInput!] + title: [String!] + role: [String!] + logo: [ImageInput!] + agent: [AgentInput!] + "Organization" + org: [OrganizationInput!] + categories: [KeywordInput!] + note: [String!] + "Identifier of product that generated the vCard property" + prodId: [String!] + "Last revised property. The value must be an ISO 8601 formatted UTC date/time" + rev: [String!] + "Sort string property" + sortString: [String!] + "Formatted name pronunciation property" + sound: [SoundInput!] + "Unique identifier property" + uid: [String!] + "Directory URL property" + url: [String!] + "Free-form descriptive text" + desc: [String!] + "Privacy classification property" + class: [PrivacyInput!] + "Authentication credential or encryption key property" + key: [KeyInput!] +} + +input KeywordInput{ + keyword: [String!] +} + +input NameComponentsInput{ + family: String + givenName: String + middleName: String + prefix: String + suffix: String +} + +input AddressInput{ + "Address tags" + tags: [AddressTags!] + "Post office box" + pobox: String + "Extra address data" + extadd: String + street: String + locality: String + region: String + "Postal code" + pcode: String + country: String +} + +input LabelInput{ + "Label tags" + tags: [AddressTags!] + line: [String!]! +} + +input TelephoneInput{ + "Telephone tags" + tags: [TelephoneTags!] + number: String! +} + +input EmailInput{ + "Email tags" + tags: [EmailTags!] + "Email address" + userId: String! +} + +input GeographicalPositionInput{ + "Geographical latitude" + lat: String! + "Geographical longtitude" + lon: String! +} + +input OrganizationInput{ + "Organization name" + orgname: String! + "Organization unit" + orgunit: [String!] +} + +input PrivacyInput{ + tags: [PrivacyClassificationTags!] +} + +input ImageInput{ + "Format type parameter" + type: String + "Base64 encoded binary image" + binValue: String + "Link to external image" + extValue: String +} + +input SoundInput{ + "Textual phonetic pronunciation" + phonetic: String + "Base64 encoded sound binary value" + binValue: String + "Link to external audio file" + extValue: String +} + +input AgentInput{ + "Link to external vcard" + extValue: String + "Individual vcard container" + vcard: VcardInput +} + +input KeyInput{ + type: String + credential: String! +} + +enum PrivacyClassificationTags{ + PUBLIC + PRIVATE + CONFIDENTIAL +} + +enum AddressTags{ + HOME + WORK + POSTAL + PARCEL + DOM + PREF + INTL +} + +enum TelephoneTags{ + HOME + WORK + VOICE + FAX + PAGER + MSG + CELL + VIDEO + BBS + MODEM + ISDN + PCS + PREF +} + +enum EmailTags{ + HOME + WORK + INTERNET + PREF + X400 +} diff --git a/priv/graphql/schemas/user/user_schema.gql b/priv/graphql/schemas/user/user_schema.gql index f2a421f6e6..0e3c9eae89 100644 --- a/priv/graphql/schemas/user/user_schema.gql +++ b/priv/graphql/schemas/user/user_schema.gql @@ -22,6 +22,8 @@ type UserQuery{ stanza: StanzaUserQuery "Roster/Contacts management" roster: RosterUserQuery + "Vcard management" + vcard: VcardUserQuery } """ @@ -39,4 +41,6 @@ type UserMutation @protected{ stanza: StanzaUserMutation "Roster/Contacts management" roster: RosterUserMutation + "Vcard management" + vcard: VcardUserMutation } diff --git a/priv/graphql/schemas/user/vcard.gql b/priv/graphql/schemas/user/vcard.gql new file mode 100644 index 0000000000..166468ea52 --- /dev/null +++ b/priv/graphql/schemas/user/vcard.gql @@ -0,0 +1,15 @@ +""" +Allow user to set own vcard +""" +type VcardUserMutation @protected{ + "Set user's own vcard" + setVcard(vcard: VcardInput!): Vcard +} + +""" +Allow user to get user's vcard +""" +type VcardUserQuery @protected{ + "Get user's vcard" + getVcard(user: JID): Vcard +} diff --git a/src/graphql/admin/mongoose_graphql_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_admin_mutation.erl index a5433bd04c..5da8a67460 100644 --- a/src/graphql/admin/mongoose_graphql_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_admin_mutation.erl @@ -20,4 +20,6 @@ execute(_Ctx, _Obj, <<"session">>, _Args) -> execute(_Ctx, _Obj, <<"stanza">>, _Args) -> {ok, stanza}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> - {ok, roster}. + {ok, roster}; +execute(_Ctx, _Obj, <<"vcard">>, _Args) -> + {ok, vcard}. diff --git a/src/graphql/admin/mongoose_graphql_admin_query.erl b/src/graphql/admin/mongoose_graphql_admin_query.erl index b80db073c7..969f13955e 100644 --- a/src/graphql/admin/mongoose_graphql_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_admin_query.erl @@ -22,4 +22,6 @@ execute(_Ctx, _Obj, <<"stanza">>, _Args) -> execute(_Ctx, _Obj, <<"roster">>, _Args) -> {ok, roster}; execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> - {ok, admin}. + {ok, admin}; +execute(_Ctx, _Obj, <<"vcard">>, _Args) -> + {ok, vcard}. diff --git a/src/graphql/admin/mongoose_graphql_vcard_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_vcard_admin_mutation.erl new file mode 100644 index 0000000000..636dc427cf --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_vcard_admin_mutation.erl @@ -0,0 +1,21 @@ +-module(mongoose_graphql_vcard_admin_mutation). +-behaviour(mongoose_graphql). + +-include("mod_vcard.hrl"). + +-export([execute/4]). + +-import(mongoose_graphql_helper, [make_error/2]). + +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +execute(_Ctx, vcard, <<"setVcard">>, #{<<"user">> := CallerJID, <<"vcard">> := VcardInput}) -> + case mod_vcard_api:set_vcard(CallerJID, VcardInput) of + {ok, _} = Vcard -> Vcard; + {ErrorCode, ErrorMessage} -> + make_error({ErrorCode, ErrorMessage}, #{user => CallerJID}) + end. diff --git a/src/graphql/admin/mongoose_graphql_vcard_admin_query.erl b/src/graphql/admin/mongoose_graphql_vcard_admin_query.erl new file mode 100644 index 0000000000..ba02b632d9 --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_vcard_admin_query.erl @@ -0,0 +1,19 @@ +-module(mongoose_graphql_vcard_admin_query). +-behaviour(mongoose_graphql). + +-export([execute/4]). + +-import(mongoose_graphql_helper, [make_error/2]). + +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +execute(_Ctx, vcard, <<"getVcard">>, #{<<"user">> := CallerJID}) -> + case mod_vcard_api:get_vcard(CallerJID) of + {ok, _} = Vcard -> Vcard; + {ErrorCode, ErrorMessage} -> + make_error({ErrorCode, ErrorMessage}, #{user => CallerJID}) + end. diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 1d482e5d64..00c9757fa4 100644 --- a/src/graphql/mongoose_graphql.erl +++ b/src/graphql/mongoose_graphql.erl @@ -143,12 +143,15 @@ admin_mapping_rules() -> 'MUCLightAdminMutation' => mongoose_graphql_muc_light_admin_mutation, 'MUCLightAdminQuery' => mongoose_graphql_muc_light_admin_query, 'RosterAdminQuery' => mongoose_graphql_roster_admin_query, + 'VcardAdminMutation' => mongoose_graphql_vcard_admin_mutation, + 'VcardAdminQuery' => mongoose_graphql_vcard_admin_query, 'RosterAdminMutation' => mongoose_graphql_roster_admin_mutation, 'Domain' => mongoose_graphql_domain, default => mongoose_graphql_default}, interfaces => #{default => mongoose_graphql_default}, scalars => #{default => mongoose_graphql_scalar}, - enums => #{default => mongoose_graphql_enum}}. + enums => #{default => mongoose_graphql_enum}, + unions => #{default => mongoose_graphql_union}}. user_mapping_rules() -> #{objects => #{ @@ -162,6 +165,8 @@ user_mapping_rules() -> 'MUCLightUserQuery' => mongoose_graphql_muc_light_user_query, 'RosterUserQuery' => mongoose_graphql_roster_user_query, 'RosterUserMutation' => mongoose_graphql_roster_user_mutation, + 'VcardUserMutation' => mongoose_graphql_vcard_user_mutation, + 'VcardUserQuery' => mongoose_graphql_vcard_user_query, 'SessionUserQuery' => mongoose_graphql_session_user_query, 'StanzaUserMutation' => mongoose_graphql_stanza_user_mutation, 'StanzaUserQuery' => mongoose_graphql_stanza_user_query, @@ -169,7 +174,8 @@ user_mapping_rules() -> default => mongoose_graphql_default}, interfaces => #{default => mongoose_graphql_default}, scalars => #{default => mongoose_graphql_scalar}, - enums => #{default => mongoose_graphql_enum}}. + enums => #{default => mongoose_graphql_enum}, + unions => #{default => mongoose_graphql_union}}. load_multiple_file_schema(Patterns) -> Paths = lists:flatmap(fun(P) -> filelib:wildcard(P) end, Patterns), diff --git a/src/graphql/mongoose_graphql_enum.erl b/src/graphql/mongoose_graphql_enum.erl index 2d1e371de2..d1e73a792d 100644 --- a/src/graphql/mongoose_graphql_enum.erl +++ b/src/graphql/mongoose_graphql_enum.erl @@ -11,10 +11,12 @@ input(<<"PresenceType">>, Type) -> input(<<"Affiliation">>, <<"OWNER">>) -> {ok, owner}; input(<<"Affiliation">>, <<"MEMBER">>) -> {ok, member}; input(<<"Affiliation">>, <<"NONE">>) -> {ok, none}; +input(<<"AddressTags">>, Name) -> {ok, Name}; input(<<"BlockingAction">>, <<"ALLOW">>) -> {ok, allow}; input(<<"BlockingAction">>, <<"DENY">>) -> {ok, deny}; input(<<"BlockedEntityType">>, <<"USER">>) -> {ok, user}; input(<<"BlockedEntityType">>, <<"ROOM">>) -> {ok, room}; +input(<<"EmailTags">>, Name) -> {ok, Name}; input(<<"SubAction">>, <<"INVITE">>) -> {ok, invite}; input(<<"SubAction">>, <<"ACCEPT">>) -> {ok, accept}; input(<<"SubAction">>, <<"DECLINE">>) -> {ok, decline}; @@ -28,7 +30,9 @@ input(<<"MUCAffiliation">>, <<"NONE">>) -> {ok, none}; input(<<"MUCAffiliation">>, <<"MEMBER">>) -> {ok, member}; input(<<"MUCAffiliation">>, <<"OUTCAST">>) -> {ok, outcast}; input(<<"MUCAffiliation">>, <<"ADMIN">>) -> {ok, admin}; -input(<<"MUCAffiliation">>, <<"OWNER">>) -> {ok, owner}. +input(<<"MUCAffiliation">>, <<"OWNER">>) -> {ok, owner}; +input(<<"PrivacyClassificationTags">>, Name) -> {ok, Name}; +input(<<"TelephoneTags">>, Name) -> {ok, Name}. output(<<"PresenceShow">>, Show) -> {ok, list_to_binary(string:to_upper(binary_to_list(Show)))}; @@ -59,4 +63,8 @@ output(<<"ContactAsk">>, Type) when Type =:= subscrube; output(<<"MUCRole">>, Role) -> {ok, list_to_binary(string:to_upper(atom_to_list(Role)))}; output(<<"MUCAffiliation">>, Aff) -> - {ok, list_to_binary(string:to_upper(atom_to_list(Aff)))}. + {ok, list_to_binary(string:to_upper(atom_to_list(Aff)))}; +output(<<"AddressTags">>, Name) -> {ok, Name}; +output(<<"EmailTags">>, Name) -> {ok, Name}; +output(<<"PrivacyClassificationTags">>, Name) -> {ok, Name}; +output(<<"TelephoneTags">>, Name) -> {ok, Name}. diff --git a/src/graphql/mongoose_graphql_union.erl b/src/graphql/mongoose_graphql_union.erl new file mode 100644 index 0000000000..357e830160 --- /dev/null +++ b/src/graphql/mongoose_graphql_union.erl @@ -0,0 +1,12 @@ +-module(mongoose_graphql_union). + +-export([execute/1]). + +-ignore_xref([execute/1]). + +execute(#{<<"type">> := _, <<"binValue">> := _}) -> {ok, <<"ImageData">>}; +execute(#{<<"extValue">> := _}) -> {ok, <<"External">>}; +execute(#{<<"phonetic">> := _}) -> {ok, <<"Phonetic">>}; +execute(#{<<"binValue">> := _}) -> {ok, <<"BinValue">>}; +execute(#{<<"vcard">> := _}) -> {ok, <<"AgentVcard">>}; +execute(_Otherwise) -> {error, unknown_type}. diff --git a/src/graphql/user/mongoose_graphql_user_mutation.erl b/src/graphql/user/mongoose_graphql_user_mutation.erl index ce4098a57d..7363fdc5a0 100644 --- a/src/graphql/user/mongoose_graphql_user_mutation.erl +++ b/src/graphql/user/mongoose_graphql_user_mutation.erl @@ -14,4 +14,6 @@ execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> execute(_Ctx, _Obj, <<"stanza">>, _Args) -> {ok, stanza}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> - {ok, roster}. + {ok, roster}; +execute(_Ctx, _Obj, <<"vcard">>, _Args) -> + {ok, vcard}. diff --git a/src/graphql/user/mongoose_graphql_user_query.erl b/src/graphql/user/mongoose_graphql_user_query.erl index dd452f8487..cb78f3ed94 100644 --- a/src/graphql/user/mongoose_graphql_user_query.erl +++ b/src/graphql/user/mongoose_graphql_user_query.erl @@ -18,4 +18,6 @@ execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> execute(_Ctx, _Obj, <<"stanza">>, _Args) -> {ok, stanza}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> - {ok, roster}. + {ok, roster}; +execute(_Ctx, _Obj, <<"vcard">>, _Args) -> + {ok, vcard}. diff --git a/src/graphql/user/mongoose_graphql_vcard_user_mutation.erl b/src/graphql/user/mongoose_graphql_vcard_user_mutation.erl new file mode 100644 index 0000000000..7dfcb16914 --- /dev/null +++ b/src/graphql/user/mongoose_graphql_vcard_user_mutation.erl @@ -0,0 +1,21 @@ +-module(mongoose_graphql_vcard_user_mutation). +-behaviour(mongoose_graphql). + +-include("mod_vcard.hrl"). + +-export([execute/4]). + +-import(mongoose_graphql_helper, [make_error/2]). + +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +execute(#{user := CallerJID}, vcard, <<"setVcard">>, #{<<"vcard">> := VCARD}) -> + case mod_vcard_api:set_vcard(CallerJID, VCARD) of + {ok, _} = Vcard -> Vcard; + {ErrorCode, ErrorMessage} -> + make_error({ErrorCode, ErrorMessage}, #{user => CallerJID}) + end. diff --git a/src/graphql/user/mongoose_graphql_vcard_user_query.erl b/src/graphql/user/mongoose_graphql_vcard_user_query.erl new file mode 100644 index 0000000000..a2b7a9845d --- /dev/null +++ b/src/graphql/user/mongoose_graphql_vcard_user_query.erl @@ -0,0 +1,20 @@ +-module(mongoose_graphql_vcard_user_query). +-behaviour(mongoose_graphql). + +-export([execute/4]). + +-import(mongoose_graphql_helper, [make_error/2, null_to_default/2]). + +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). +-include("mongoose.hrl"). +-include("jlib.hrl"). + +execute(#{user := CallerJID}, vcard, <<"getVcard">>, #{<<"user">> := UserJID}) -> + UserJID2 = null_to_default(UserJID, CallerJID), + case mod_vcard_api:get_vcard(UserJID2) of + {ok, _} = Vcard -> Vcard; + {ErrorCode, ErrorMessage} -> + make_error({ErrorCode, ErrorMessage}, #{<<"user">> => UserJID2}) + end. diff --git a/src/vcard/mod_vcard.erl b/src/vcard/mod_vcard.erl index 1fe9bbe2d5..ef9e18625e 100644 --- a/src/vcard/mod_vcard.erl +++ b/src/vcard/mod_vcard.erl @@ -76,6 +76,7 @@ -export([default_search_fields/0]). -export([get_results_limit/1]). -export([get_default_reported_fields/1]). +-export([unsafe_set_vcard/3]). %% GDPR related -export([get_personal_data/3]). diff --git a/src/vcard/mod_vcard_api.erl b/src/vcard/mod_vcard_api.erl new file mode 100644 index 0000000000..172d3ca7cb --- /dev/null +++ b/src/vcard/mod_vcard_api.erl @@ -0,0 +1,285 @@ +%% @doc Provide an interface for frontends (like graphql or ctl) to manage vcard. +-module(mod_vcard_api). + +-include("mongoose.hrl"). +-include("jlib.hrl"). +-include("mod_vcard.hrl"). + +-type vcard_map() :: #{binary() => vcard_subelement_binary() | vcard_subelement_map()}. +-type vcard_subelement_binary() :: binary() | [{ok, binary()}]. +-type vcard_subelement_map() :: #{binary() => binary() | [{ok, binary()}]}. + +-export([set_vcard/2, + get_vcard/1]). + +-spec set_vcard(jid:jid(), vcard_map()) -> + {ok, vcard_map()} | {not_found, string()} | {internal, string()} | {vcard_not_found, string()}. +set_vcard(#jid{luser = LUser, lserver = LServer} = UserJID, Vcard) -> + case mongoose_domain_api:get_domain_host_type(LServer) of + {ok, HostType} -> + case set_vcard(HostType, UserJID, Vcard) of + ok -> + get_vcard_from_db(HostType, LUser, LServer); + _ -> + {internal, "Internal server error"} + end; + _ -> + {not_found, "User does not exist"} + end. + +-spec get_vcard(jid:jid()) -> + {ok, vcard_map()} | {not_found, string()} | {internal, string()} | {vcard_not_found, string()}. +get_vcard(#jid{luser = LUser, lserver = LServer}) -> + case mongoose_domain_api:get_domain_host_type(LServer) of + {ok, HostType} -> + get_vcard_from_db(HostType, LUser, LServer); + _ -> + {not_found, "User does not exist"} + end. + +set_vcard(HostType, UserJID, Vcard) -> + mod_vcard:unsafe_set_vcard(HostType, UserJID, transform_from_map(Vcard)). + +get_vcard_from_db(HostType, LUser, LServer) -> + ItemNotFoundError = mongoose_xmpp_errors:item_not_found(), + case mod_vcard_backend:get_vcard(HostType, LUser, LServer) of + {ok, Result} -> + [#xmlel{children = VcardData}] = Result, + {ok, to_map_format(VcardData)}; + {error, ItemNotFoundError} -> + {vcard_not_found, "Vcard for user not found"}; + _ -> + {internal, "Internal server error"} + end. + +transform_from_map(Vcard) -> + #xmlel{name = <<"vCard">>, + attrs = [{<<"xmlns">>, <<"vcard-temp">>}], + children = lists:foldl(fun({Name, Value}, Acc) -> + Acc ++ transform_field_and_value(Name, Value) + end, [], maps:to_list(Vcard))}. + +construct_xmlel(Name, Children) when is_list(Children)-> + [#xmlel{name = Name, + attrs = [], + children = Children}]. + +transform_field_and_value(_Name, null) -> + []; +transform_field_and_value(Name, Value) when is_list(Value) -> + lists:foldl(fun(Element, Acc) -> + Acc ++ transform_field_and_value(Name, Element) + end, [], Value); +transform_field_and_value(Name, Value) when is_map(Value) -> + construct_xmlel(from_map_to_xml(Name), process_child_map(Value)); +transform_field_and_value(Name, Value) -> + construct_xmlel(from_map_to_xml(Name), [{xmlcdata, Value}]). + +transform_subfield_and_value(_Name, null) -> + []; +transform_subfield_and_value(<<"vcard">>, Value) -> + [transform_from_map(Value)]; +transform_subfield_and_value(<<"tags">>, TagsList) -> + lists:foldl(fun(Tag, Acc) -> + Acc ++ construct_xmlel(Tag, []) + end, [], TagsList); +transform_subfield_and_value(Name, Value) when is_list(Value) -> + lists:foldl(fun(Element, Acc) -> + Acc ++ construct_xmlel(from_map_to_xml(Name), [{xmlcdata, Element}]) + end, [], Value); +transform_subfield_and_value(Name, Value) -> + construct_xmlel(from_map_to_xml(Name), [{xmlcdata, Value}]). + +process_child_map(Value) -> + lists:foldl(fun({Name, SubfieldValue}, Acc) -> + Acc ++ transform_subfield_and_value(Name, SubfieldValue) + end, [], maps:to_list(Value)). + +from_map_to_xml(<<"formattedName">>) -> <<"FN">>; +from_map_to_xml(<<"nameComponents">>) -> <<"N">>; +from_map_to_xml(<<"birthday">>) -> <<"BDAY">>; +from_map_to_xml(<<"address">>) -> <<"ADR">>; +from_map_to_xml(<<"telephone">>) -> <<"TEL">>; +from_map_to_xml(<<"timeZone">>) -> <<"TZ">>; +from_map_to_xml(<<"sortString">>) -> <<"SOR">>; +from_map_to_xml(<<"givenName">>) -> <<"GIVEN">>; +from_map_to_xml(<<"middleName">>) -> <<"MIDDLE">>; +from_map_to_xml(<<"credential">>) -> <<"CRED">>; +from_map_to_xml(<<"country">>) -> <<"CTRY">>; +from_map_to_xml(<<"binValue">>) -> <<"BINVAL">>; +from_map_to_xml(<<"extValue">>) -> <<"EXTVAL">>; +from_map_to_xml(Name) -> list_to_binary(string:to_upper(binary_to_list(Name))). + +to_map_format(Vcard) -> + lists:foldl(fun(#xmlel{name = Name, children = Value}, Acc) -> + maps:merge(Acc, transform_from_xml(Name, Value, Acc)) + end, #{}, Vcard). + +transform_from_xml(<<"FN">>, [{_, Value}], _) -> + #{<<"formattedName">> => Value}; +transform_from_xml(<<"N">>, Value, _) -> + #{<<"nameComponents">> => lists:foldl(fun name_components_process/2, #{}, Value)}; +transform_from_xml(<<"NICKNAME">>, Value, Acc) -> + simple_process(<<"nickname">>, Value, Acc); +transform_from_xml(<<"PHOTO">>, Value, Acc) -> + complex_process(<<"photo">>, Value, Acc, fun image_components_process/2); +transform_from_xml(<<"BDAY">>, Value, Acc) -> + simple_process(<<"birthday">>, Value, Acc); +transform_from_xml(<<"ADR">>, Value, Acc) -> + complex_process(<<"address">>, Value, Acc, fun address_components_process/2); +transform_from_xml(<<"LABEL">>, Value, Acc) -> + complex_process(<<"label">>, Value, Acc, fun label_components_process/2); +transform_from_xml(<<"TEL">>, Value, Acc) -> + complex_process(<<"telephone">>, Value, Acc, fun telephone_components_process/2); +transform_from_xml(<<"EMAIL">>, Value, Acc) -> + complex_process(<<"email">>, Value, Acc, fun email_components_process/2); +transform_from_xml(<<"JABBERID">>, Value, Acc) -> + simple_process(<<"jabberId">>, Value, Acc); +transform_from_xml(<<"MAILER">>, Value, Acc) -> + simple_process(<<"mailer">>, Value, Acc); +transform_from_xml(<<"TZ">>, Value, Acc) -> + simple_process(<<"timeZone">>, Value, Acc); +transform_from_xml(<<"GEO">>, Value, Acc) -> + complex_process(<<"geo">>, Value, Acc, fun geo_components_process/2); +transform_from_xml(<<"TITLE">>, Value, Acc) -> + simple_process(<<"title">>, Value, Acc); +transform_from_xml(<<"ROLE">>, Value, Acc) -> + simple_process(<<"role">>, Value, Acc); +transform_from_xml(<<"LOGO">>, Value, Acc) -> + complex_process(<<"logo">>, Value, Acc, fun image_components_process/2); +transform_from_xml(<<"AGENT">>, Value, Acc) -> + complex_process(<<"agent">>, Value, Acc, fun agent_components_process/2); +transform_from_xml(<<"ORG">>, Value, Acc) -> + complex_process(<<"org">>, Value, Acc, fun org_components_process/2); +transform_from_xml(<<"CATEGORIES">>, Value, Acc) -> + complex_process(<<"categories">>, Value, Acc, fun categories_components_process/2); +transform_from_xml(<<"NOTE">>, Value, Acc) -> + simple_process(<<"note">>, Value, Acc); +transform_from_xml(<<"PRODID">>, Value, Acc) -> + simple_process(<<"prodId">>, Value, Acc); +transform_from_xml(<<"REV">>, Value, Acc) -> + simple_process(<<"rev">>, Value, Acc); +transform_from_xml(<<"SOR">>, Value, Acc) -> + simple_process(<<"sortString">>, Value, Acc); +transform_from_xml(<<"SOUND">>, Value, Acc) -> + complex_process(<<"sound">>, Value, Acc, fun sound_components_process/2); +transform_from_xml(<<"UID">>, Value, Acc) -> + simple_process(<<"uid">>, Value, Acc); +transform_from_xml(<<"URL">>, Value, Acc) -> + simple_process(<<"url">>, Value, Acc); +transform_from_xml(<<"DESC">>, Value, Acc) -> + simple_process(<<"desc">>, Value, Acc); +transform_from_xml(<<"CLASS">>, Value, Acc) -> + complex_process(<<"class">>, Value, Acc, fun class_components_process/2); +transform_from_xml(<<"KEY">>, Value, Acc) -> + complex_process(<<"key">>, Value, Acc, fun key_components_process/2); +transform_from_xml(_, _, _) -> + #{}. + +process_value([{_, Value}]) -> + Value; +process_value(_) -> + null. + +simple_process(Name, [{_, Value}], Acc) -> + List = maps:get(Name, Acc, []), + #{Name => List ++ [{ok, Value}]}; +simple_process(_, _, _) -> + #{}. + +complex_process(Name, Value, Acc, Fun) -> + List = maps:get(Name, Acc, []), + #{Name => List ++ [{ok, lists:foldl(fun(Element, Accumulator) -> + Fun(Element, Accumulator) + end, #{}, Value)}]}. + +name_components_process(#xmlel{name = <<"FAMILY">>, children = Value}, Acc) -> + maps:put(<<"family">>, process_value(Value), Acc); +name_components_process(#xmlel{name = <<"GIVEN">>, children = Value}, Acc) -> + maps:put(<<"givenName">>, process_value(Value), Acc); +name_components_process(#xmlel{name = <<"MIDDLE">>, children = Value}, Acc) -> + maps:put(<<"middleName">>, process_value(Value), Acc); +name_components_process(#xmlel{name = <<"PREFIX">>, children = Value}, Acc) -> + maps:put(<<"prefix">>, process_value(Value), Acc); +name_components_process(#xmlel{name = <<"SUFFIX">>, children = Value}, Acc) -> + maps:put(<<"suffix">>, process_value(Value), Acc). + +address_components_process(#xmlel{name = <<"POBOX">>, children = Value}, Acc) -> + maps:put(<<"pobox">>, process_value(Value), Acc); +address_components_process(#xmlel{name = <<"EXTADD">>, children = Value}, Acc) -> + maps:put(<<"extadd">>, process_value(Value), Acc); +address_components_process(#xmlel{name = <<"STREET">>, children = Value}, Acc) -> + maps:put(<<"street">>, process_value(Value), Acc); +address_components_process(#xmlel{name = <<"LOCALITY">>, children = Value}, Acc) -> + maps:put(<<"locality">>, process_value(Value), Acc); +address_components_process(#xmlel{name = <<"REGION">>, children = Value}, Acc) -> + maps:put(<<"region">>, process_value(Value), Acc); +address_components_process(#xmlel{name = <<"PCODE">>, children = Value}, Acc) -> + maps:put(<<"pcode">>, process_value(Value), Acc); +address_components_process(#xmlel{name = <<"CTRY">>, children = Value}, Acc) -> + maps:put(<<"country">>, process_value(Value), Acc); +address_components_process(#xmlel{name = Name, children = []}, Acc) -> + List = maps:get(<<"tags">>, Acc, []), + maps:merge(Acc, #{<<"tags">> => List ++ [{ok, Name}]}). + +label_components_process(#xmlel{name = <<"LINE">>, children = Value}, Acc) -> + List = maps:get(<<"line">>, Acc, []), + maps:merge(Acc, #{<<"line">> => List ++ [{ok, process_value(Value)}]}); +label_components_process(#xmlel{name = Name, children = []}, Acc) -> + List = maps:get(<<"tags">>, Acc, []), + maps:merge(Acc, #{<<"tags">> => List ++ [{ok, Name}]}). + +telephone_components_process(#xmlel{name = <<"NUMBER">>, children = Value}, Acc) -> + maps:put(<<"number">>, process_value(Value), Acc); +telephone_components_process(#xmlel{name = Name, children = []}, Acc) -> + List = maps:get(<<"tags">>, Acc, []), + maps:merge(Acc, #{<<"tags">> => List ++ [{ok, Name}]}). + +email_components_process(#xmlel{name = <<"USERID">>, children = Value}, Acc) -> + maps:put(<<"userId">>, process_value(Value), Acc); +email_components_process(#xmlel{name = Name, children = []}, Acc) -> + List = maps:get(<<"tags">>, Acc, []), + maps:merge(Acc, #{<<"tags">> => List ++ [{ok, Name}]}). + +geo_components_process(#xmlel{name = <<"LAT">>, children = Value}, Acc) -> + maps:put(<<"lat">>, process_value(Value), Acc); +geo_components_process(#xmlel{name = <<"LON">>, children = Value}, Acc) -> + maps:put(<<"lon">>, process_value(Value), Acc). + +org_components_process(#xmlel{name = <<"ORGNAME">>, children = Value}, Acc) -> + maps:put(<<"orgname">>, process_value(Value), Acc); +org_components_process(#xmlel{name = <<"ORGUNIT">>, children = Value}, Acc) -> + List = maps:get(<<"orgunit">>, Acc, []), + maps:merge(Acc, #{<<"orgunit">> => List ++ [{ok, process_value(Value)}]}). + +categories_components_process(#xmlel{name = <<"KEYWORD">>, children = Value}, Acc) -> + List = maps:get(<<"keyword">>, Acc, []), + maps:merge(Acc, #{<<"keyword">> => List ++ [{ok, process_value(Value)}]}). + +key_components_process(#xmlel{name = <<"CRED">>, children = Value}, Acc) -> + maps:put(<<"credential">>, process_value(Value), Acc); +key_components_process(#xmlel{name = <<"TYPE">>, children = Value}, Acc) -> + maps:put(<<"type">>, process_value(Value), Acc). + +class_components_process(#xmlel{name = Name, children = []}, Acc) -> + List = maps:get(<<"tags">>, Acc, []), + maps:merge(Acc, #{<<"tags">> => List ++ [{ok, Name}]}). + +image_components_process(#xmlel{name = <<"TYPE">>, children = Value}, Acc) -> + maps:put(<<"type">>, process_value(Value), Acc); +image_components_process(#xmlel{name = <<"BINVAL">>, children = Value}, Acc) -> + maps:put(<<"binValue">>, process_value(Value), Acc); +image_components_process(#xmlel{name = <<"EXTVAL">>, children = Value}, Acc) -> + maps:put(<<"extValue">>, process_value(Value), Acc). + +sound_components_process(#xmlel{name = <<"PHONETIC">>, children = Value}, Acc) -> + maps:put(<<"phonetic">>, process_value(Value), Acc); +sound_components_process(#xmlel{name = <<"BINVAL">>, children = Value}, Acc) -> + maps:put(<<"binValue">>, process_value(Value), Acc); +sound_components_process(#xmlel{name = <<"EXTVAL">>, children = Value}, Acc) -> + maps:put(<<"extValue">>, process_value(Value), Acc). + +agent_components_process(#xmlel{name = <<"vCard">>, children = Value}, Acc) -> + maps:put(<<"vcard">>, to_map_format(Value), Acc); +agent_components_process(#xmlel{name = <<"EXTVAL">>, children = Value}, Acc) -> + maps:put(<<"extValue">>, process_value(Value), Acc).