./ct_report/coverage/mongoose_client_api_contacts.COVER.html

1 -module(mongoose_client_api_contacts).
2
3 -behaviour(mongoose_client_api).
4 -export([routes/0]).
5
6 -behaviour(cowboy_rest).
7 -export([trails/0,
8 init/2,
9 is_authorized/2,
10 content_types_provided/2,
11 content_types_accepted/2,
12 allowed_methods/2,
13 to_json/2,
14 from_json/2,
15 delete_resource/2]).
16
17 -ignore_xref([from_json/2, to_json/2, trails/0]).
18
19 -import(mongoose_client_api, [parse_body/1, try_handle_request/3, throw_error/2]).
20
21 -type req() :: cowboy_req:req().
22 -type state() :: map().
23
24 -spec routes() -> mongoose_http_handler:routes().
25 routes() ->
26 108 [{"/contacts/[:jid]", ?MODULE, #{}}].
27
28 trails() ->
29 108 mongoose_client_api_contacts_doc:trails().
30
31 -spec init(req(), state()) -> {cowboy_rest, req(), state()}.
32 init(Req, Opts) ->
33 34 mongoose_client_api:init(Req, Opts).
34
35 -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}.
36 is_authorized(Req, State) ->
37 34 mongoose_client_api:is_authorized(Req, State).
38
39 -spec content_types_provided(req(), state()) ->
40 {[{{binary(), binary(), '*'}, atom()}], req(), state()}.
41 content_types_provided(Req, State) ->
42 32 {[
43 {{<<"application">>, <<"json">>, '*'}, to_json}
44 ], Req, State}.
45
46 -spec content_types_accepted(req(), state()) ->
47 {[{{binary(), binary(), '*'}, atom()}], req(), state()}.
48 content_types_accepted(Req, State) ->
49 16 {[
50 {{<<"application">>, <<"json">>, '*'}, from_json}
51 ], Req, State}.
52
53 -spec allowed_methods(req(), state()) -> {[binary()], req(), state()}.
54 allowed_methods(Req, State) ->
55 34 {[<<"OPTIONS">>, <<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>],
56 Req, State}.
57
58 %% @doc Called for a method of type "GET"
59 -spec to_json(req(), state()) -> {iodata() | stop, req(), state()}.
60 to_json(Req, State) ->
61 12 try_handle_request(Req, State, fun handle_get/2).
62
63 %% @doc Called for a method of type "POST" or "PUT"
64 -spec from_json(req(), state()) -> {true | stop, req(), state()}.
65 from_json(Req, State) ->
66 16 F = case cowboy_req:method(Req) of
67 11 <<"POST">> -> fun handle_post/2;
68 5 <<"PUT">> -> fun handle_put/2
69 end,
70 16 try_handle_request(Req, State, F).
71
72 %% @doc Called for a method of type "DELETE"
73 -spec delete_resource(req(), state()) -> {true | stop, req(), state()}.
74 delete_resource(Req, State) ->
75 4 try_handle_request(Req, State, fun handle_delete/2).
76
77 %% Internal functions
78
79 handle_get(Req, State = #{jid := UserJid}) ->
80 12 Bindings = cowboy_req:bindings(Req),
81 12 assert_no_jid(Bindings),
82 11 {ok, Contacts} = mod_roster_api:list_contacts(UserJid),
83 11 {jiffy:encode(lists:map(fun roster_info/1, Contacts)), Req, State}.
84
85 handle_post(Req, State = #{jid := UserJid}) ->
86 11 Args = parse_body(Req),
87 11 ContactJid = get_jid(Args),
88 9 case mod_roster_api:add_contact(UserJid, ContactJid, <<>>, []) of
89 {user_not_exist, Reason} ->
90 1 throw_error(not_found, Reason);
91 {ok, _} ->
92 8 {true, Req, State}
93 end.
94
95 handle_put(Req, State = #{jid := UserJid}) ->
96 5 Bindings = cowboy_req:bindings(Req),
97 5 ContactJid = get_jid(Bindings),
98 5 Args = parse_body(Req),
99 5 Action = get_action(Args),
100 3 assert_contact_exists(UserJid, ContactJid),
101 2 {ok, _} = mod_roster_api:subscription(UserJid, ContactJid, Action),
102 2 {true, Req, State}.
103
104 handle_delete(Req, State = #{jid := UserJid}) ->
105 4 Bindings = cowboy_req:bindings(Req),
106 4 case try_get_jid(Bindings) of
107 undefined ->
108 2 Args = parse_body(Req),
109 2 ContactJids = get_jids_to_delete(Args),
110 2 NotDeleted = delete_contacts(UserJid, ContactJids),
111 2 RespBody = #{not_deleted => lists:map(fun jid:to_binary/1, NotDeleted)},
112 2 Req2 = cowboy_req:set_resp_body(jiffy:encode(RespBody), Req),
113 2 Req3 = cowboy_req:set_resp_header(<<"content-type">>, <<"application/json">>, Req2),
114 2 {true, Req3, State};
115 ContactJid ->
116 2 case mod_roster_api:delete_contact(UserJid, ContactJid) of
117 {contact_not_found, Reason} ->
118 1 throw_error(not_found, Reason);
119 {ok, _} ->
120 1 {true, Req, State}
121 end
122 end.
123
124 -spec roster_info(mod_roster:roster()) -> jiffy:json_value(). %% returns jiffy:json_object()
125 roster_info(Roster) ->
126 7 #{jid := Jid, subscription := Sub, ask := Ask} = mod_roster:item_to_map(Roster),
127 7 #{jid => jid:to_binary(Jid), subscription => Sub, ask => Ask}.
128
129 -spec delete_contacts(jid:jid(), [jid:jid()]) -> [jid:jid()].
130 delete_contacts(UserJid, ContactJids) ->
131 2 lists:filter(fun(ContactJid) ->
132 5 case mod_roster_api:delete_contact(UserJid, ContactJid) of
133 {contact_not_found, _Reason} ->
134 1 true;
135 {ok, _} ->
136 4 false
137 end
138 end, ContactJids).
139
140 get_jid(#{jid := JidBin}) ->
141 15 parse_jid(JidBin);
142 get_jid(#{}) ->
143 1 throw_error(bad_request, <<"Missing JID">>).
144
145 try_get_jid(#{jid := JidBin}) ->
146 2 parse_jid(JidBin);
147 try_get_jid(#{}) ->
148 2 undefined.
149
150 assert_no_jid(#{jid := _}) ->
151 1 throw_error(not_found, <<"JID provided but not supported">>);
152 assert_no_jid(#{}) ->
153 11 ok.
154
155 assert_contact_exists(UserJid, ContactJid) ->
156 3 case mod_roster_api:get_contact(UserJid, ContactJid) of
157 2 {ok, _} -> ok;
158 1 {_Error, Msg} -> throw_error(not_found, Msg)
159 end.
160
161 get_jids_to_delete(#{to_delete := JidsBin}) ->
162 2 lists:map(fun parse_jid/1, JidsBin).
163
164 parse_jid(JidBin) ->
165 22 case jid:from_binary(JidBin) of
166 1 error -> throw_error(bad_request, <<"Invalid JID: ", JidBin/binary>>);
167 21 Jid -> Jid
168 end.
169
170 get_action(#{action := ActionBin}) ->
171 4 decode_action(ActionBin);
172 get_action(#{}) ->
173 1 throw_error(bad_request, <<"Missing action">>).
174
175 2 decode_action(<<"invite">>) -> subscribe;
176 1 decode_action(<<"accept">>) -> subscribed;
177 1 decode_action(_) -> throw_error(bad_request, <<"Invalid action">>).
Line Hits Source