Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 426 lines (400 sloc) 13.935 kB
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
1 %%----------------------------------------------------------------
2 %%% @author Yomi Colledge <yomi@boodah.net>
3 %%% @doc Web interface for Chatterl
4 %%%
5 %%% Chatterl Web Gateway
6 %%%
7 %%% Allows Chatterl to interface with any web-based interface
8 %%% Using JSON and XML, sending the requests off to the chatterl_serv
9 %%% module.
10 %%% @end
11 %%% @copyright 2008 Yomi Akindayini
12 %%%---------------------------------------------------------------
13 -module(chatterl_gateway).
14
15 -behaviour(gen_server).
16
17 -define(OK, <<"ok">>).
18 %% API
8b4032e @baphled Need to clean up XML messages.
authored
19 -export([start/1,dispatch_requests/1,tuple_to_xml/2]).
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
20
21 %% gen_server callbacks
22 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
23 terminate/2, code_change/3]).
24
25 -record(state, {}).
231f468 @baphled Starting to implement the basic JSON functionality, having fun
authored
26 -record(messages, {client,message}).
e62e333 @baphled Refactored to handle JSON messages, will need to do send next.
authored
27 %% Record used to pass our basic messages from the gateway to chatterl_web
8780bb8 @baphled General refactoring so we are passing or JSON around nicely.
authored
28 -record(carrier, {type,message}).
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
29 -define(SERVER, ?MODULE).
30 %%====================================================================
31 %% API
32 %%====================================================================
33 %%--------------------------------------------------------------------
34 %% @doc
35 %% Starts the server
36 %%
37 %% @spec start(Port) -> {ok,Pid} | ignore | {error,Error}
38 %% @end
39 %%--------------------------------------------------------------------
40 start(Port) ->
41 gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).
42
f52d792 @baphled Added missing documentation for dispatch_requests.
authored
43 %%--------------------------------------------------------------------
44 %% @doc
45 %% Dispatches our requests to the relevant handle.
46 %%
47 %% Uses clean_path to determine what the action is.
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
48 %% @spec dispatch_requests(Request) -> void()
f52d792 @baphled Added missing documentation for dispatch_requests.
authored
49 %% @end
50 %%--------------------------------------------------------------------
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
51 dispatch_requests(Req) ->
52 Path = Req:get(path),
53 Action = clean_path(Path),
54 handle(Action, Req).
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
55
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
56 %%====================================================================
57 %% gen_server callbacks
58 %%====================================================================
59
60 %%--------------------------------------------------------------------
61 %% @doc
62 %% Initiates the server
63 %%
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
64 %% Function: init(Port) -> {ok, State} |
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
65 %% {ok, State, Timeout} |
66 %% ignore |
67 %% {stop, Reason}
68
69 %%--------------------------------------------------------------------
70 init([Port]) ->
71 io:format("Initialising Chatterl Web Interface~n"),
72 process_flag(trap_exit, true),
73 mochiweb_http:start([{port, Port}, {loop, fun dispatch_requests/1}]),
74 erlang:monitor(process,mochiweb_http),
75 {ok, #state{}}.
76
77 %%--------------------------------------------------------------------
78 %% @doc
79 %% Description: Handling call messages
80 %%
81 %% @spec handle_call(Request, From, State) -> {reply, Reply, State} |
82 %% {reply, Reply, State, Timeout} |
83 %% {noreply, State} |
84 %% {noreply, State, Timeout} |
85 %% {stop, Reason, Reply, State} |
86 %% {stop, Reason, State}
87 %% @end
88 %%--------------------------------------------------------------------
89 handle_call(_Rquest, _From, State) ->
90 {reply,ok,State}.
91
92 %%--------------------------------------------------------------------
93 %% @doc
94 %% Handling cast messages
95 %%
96 %% @spec handle_cast(Msg, State) -> {noreply, State} |
97 %% {noreply, State, Timeout} |
98 %% {stop, Reason, State}
99 %% @end
100 %%--------------------------------------------------------------------
101 handle_cast(_Msg, State) ->
102 {noreply, State}.
103
104 %%--------------------------------------------------------------------
105 %% @doc
106 %% Handling all non call/cast messages
107 %%
108 %% @spec handle_info(Info, State) -> {noreply, State} |
109 %% {noreply, State, Timeout} |
110 %% {stop, Reason, State}
111 %% @end
112 %%--------------------------------------------------------------------
113 handle_info({'DOWN', _Ref, _Process, {mochiweb_http, Host}, Reason}, State) ->
114 io:format("Unable to start mochiweb on ~s:~nReason: ~s~n",[Host,Reason]),
115 {stop,normal,State};
116 handle_info(_Info, State) ->
117 {noreply, State}.
118
119 %%--------------------------------------------------------------------
120 %% @doc
121 %% Terminates Chatterl Web Interface, making sure to shutdown mochiweb
122 %% along side it.
123 %%
124 %% @spec terminate({node,Reason},State) -> void()
125 %% @todo Needs a time out for when the port is already in use.
126 %% @end
127 %%--------------------------------------------------------------------
128 terminate(_Reason, _State) ->
129 io:format("Shutting down ChatterlWeb on: ~s...~n",[node(self())]),
130 mochiweb_http:stop(),
131 ok.
132
133 %%--------------------------------------------------------------------
134 %% @doc
135 %%
136 %% Convert process state when code is changed
137 %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
138 %%
139 %% @end
140 %%--------------------------------------------------------------------
141 code_change(_OldVsn, State, _Extra) ->
142 {ok, State}.
143
144 %%--------------------------------------------------------------------
145 %%% Internal functions
146 %%--------------------------------------------------------------------
147 %%--------------------------------------------------------------------
148 %% @doc
149 %%
150 %% Handles our RESTful resquests.
151 %% @spec handle(Action, Req) -> {ok, NewState}
152 %%
153 %% @end
154 %%--------------------------------------------------------------------
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
155 %% handle("/send", Req) ->
156 %% Params = Req:parse_qs(),
157 %% Sender = proplists:get_value("nick", Params),
158 %% Group = proplists:get_value("to", Params),
159 %% Message = proplists:get_value("msg", Params),
160 %% chatterl_man:send_message(Sender, Group, Message),
161 %% success(Req, {"text/plain",?OK});
162 %% Need to refactor so the request goes to chatterl_serv
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
163 handle("/connect/" ++ Client,Req) ->
b810026 @baphled Fixed typos.
authored
164 ContentType = "text/xml",
853cfb8 @baphled Renamed error to send_response, it will now handle all of our respons…
authored
165 Record =
166 case gen_server:call({global,chatterl_serv},{connect,Client}) of
167 {ok,_} ->
168 get_record("success",Client ++ " now connected");
169 {error,Error} ->
170 get_record("fail",Error)
171 end,
b810026 @baphled Fixed typos.
authored
172 send_response(Req,{ContentType,Record});
76910a1 @baphled Fixed side effect bug, now using xml_tuple_single for when we know we…
authored
173 handle("/disconnect/" ++ Client,Req) ->
174 ContentType = "text/xml",
175 Record =
176 case gen_server:call({global,chatterl_serv},{disconnect,Client}) of
177 {ok,Message} ->
178 get_record("success",Message);
179 {error,Error} ->
180 get_record("fail",Error)
181 end,
182 send_response(Req,{ContentType,Record});
fbc8846 @baphled Silly typo /users/list should work now.
authored
183 handle("/users/list",Req) ->
7de996a @baphled Refactored so that the message type is passed with the return respons…
authored
184 {Type,Result} =
8bc0db9 @baphled Added list_users handle & refactored the catch all to out put the com…
authored
185 case gen_server:call({global,chatterl_serv},list_users) of
7de996a @baphled Refactored so that the message type is passed with the return respons…
authored
186 [] -> {"success",get_record("users","")};
8bc0db9 @baphled Added list_users handle & refactored the catch all to out put the com…
authored
187 Users ->
188 UsersList = [get_record("user",User)||User <- Users],
7de996a @baphled Refactored so that the message type is passed with the return respons…
authored
189 {"success",get_record("users",UsersList)}
8bc0db9 @baphled Added list_users handle & refactored the catch all to out put the com…
authored
190 end,
1866331 @baphled General cleanup.
authored
191 send_response(Req,{"text/xml",get_record(Type,Result)});
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
192 handle("/groups/list",Req) ->
7de996a @baphled Refactored so that the message type is passed with the return respons…
authored
193 {Type,Result} =
c07d146 @baphled Refactoring so we can retrieve formatted groups list in xml.
authored
194 case gen_server:call({global,chatterl_serv},list_groups) of
7de996a @baphled Refactored so that the message type is passed with the return respons…
authored
195 [] -> {"success",get_record("groups","")};
c07d146 @baphled Refactoring so we can retrieve formatted groups list in xml.
authored
196 Groups ->
197 GroupsList = [get_record("group",Group)||Group <- Groups],
7de996a @baphled Refactored so that the message type is passed with the return respons…
authored
198 {"success",get_record("groups",GroupsList)}
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
199 end,
1866331 @baphled General cleanup.
authored
200 send_response(Req,{"text/xml",get_record(Type,Result)});
8bc0db9 @baphled Added list_users handle & refactored the catch all to out put the com…
authored
201 handle(Unknown, Req) ->
202 send_response(Req,{"text/xml",get_record("error", "Unknown command: " ++Unknown)}).
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
203 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
204 %% @private
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
205 %% @doc
206 %%
a8da56a @baphled Cleaned up documentation.
authored
207 %% Builds and returns our carrier message.
208 %% @spec get_record(Type,Message) -> Record
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
209 %%
210 %% @end
211 %%--------------------------------------------------------------------
493673f @baphled Added get record method, which basically returns our carrier record a…
authored
212 get_record(Type,Message) ->
213 #carrier{ type=Type, message=Message}.
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
214
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
215 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
216 %% @private
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
217 %% @doc
218 %%
219 %% Cleans up a request so we only retrieve the path.
220 %% @spec clean_path(Path) -> [Path]
221 %%
222 %% @end
223 %%--------------------------------------------------------------------
224 clean_path(Path) ->
225 case string:str(Path, "?") of
226 0 ->
227 Path;
228 N ->
229 string:substr(Path, 1, string:len(Path) - (N + 1))
230 end.
231
232 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
233 %% @private
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
234 %% @doc
235 %%
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
236 %% Handles our all responses.
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
237 %%
853cfb8 @baphled Renamed error to send_response, it will now handle all of our respons…
authored
238 %% Sends responses based on the content type, which are JSON and XML.
239 %% @spec send_response(Req,Body) -> tuple()
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
240 %%
241 %% @end
242 %%--------------------------------------------------------------------
853cfb8 @baphled Renamed error to send_response, it will now handle all of our respons…
authored
243 send_response(Req, {ContentType,Record}) when is_list(ContentType) ->
d5242e9 @baphled Added documentation for helper methods and moved success closer to er…
authored
244 Response = get_response_body(ContentType,Record),
8e7e583 @baphled General clean up and refactoring of error, which will eventually be o…
authored
245 Code = get_response_code(Record),
246 %% If we cant construct the code or have an illegal content type, we need to
247 %% send an illegal method message back to the client.
248 Req:respond({Code, [{"Content-Type", ContentType}], list_to_binary(Response)}).
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
249
d5242e9 @baphled Added documentation for helper methods and moved success closer to er…
authored
250 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
251 %% @private
d5242e9 @baphled Added documentation for helper methods and moved success closer to er…
authored
252 %% @doc
253 %%
254 %% Gets the actual response body to return to the client.
255 %%
256 %% Responses are either in JSON or XML, if the wrong content type is passed
257 %% the client will recieve an error in XML format.
258 %% @spec get_response_body(ContentType,Record) -> [ResponseBody]
259 %%
260 %% @end
261 %%--------------------------------------------------------------------
262 get_response_body(ContentType,Record) ->
8e7e583 @baphled General clean up and refactoring of error, which will eventually be o…
authored
263 case ContentType of
264 "text/json" ->
265 to_json(Record);
266 "text/xml" ->
d5242e9 @baphled Added documentation for helper methods and moved success closer to er…
authored
267 xml_message(Record);
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
268 _ -> xml_message(get_record("error","Illegal content type!"))
8e7e583 @baphled General clean up and refactoring of error, which will eventually be o…
authored
269 end.
d5242e9 @baphled Added documentation for helper methods and moved success closer to er…
authored
270
271 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
272 %% @private
d5242e9 @baphled Added documentation for helper methods and moved success closer to er…
authored
273 %% @doc
274 %%
275 %% Gets our response code depending on the type of message passed by
276 %% the carrier record.
277 %%
278 %%
279 %% @spec get_response_code(Record) -> integer()
280 %%
281 %% @end
282 %%--------------------------------------------------------------------
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
283 get_response_code(Record) ->
284 case Record of
285 {carrier,Type,_Message} ->
286 case Type of
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
287 "fail" -> 500;
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
288 "success" -> 200;
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
289 "error" -> 200;
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
290 _ -> 500
2a34c4f @baphled Added basic functionality to allow clients to list groups.
authored
291 end
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
292 end.
44d0dfb @baphled General cleanup, forgot that chatterl's param was a simple int & not …
authored
293
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
294 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
295 %% @private
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
296 %% @doc
297 %%
ed0d4ad @baphled General cleanup.
authored
298 %% Converts our carrier Record into a JSON format.
a8da56a @baphled Cleaned up documentation.
authored
299 %% @spec to_json(Record) -> JSON
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
300 %%
301 %% @end
302 %%--------------------------------------------------------------------
ed0d4ad @baphled General cleanup.
authored
303 to_json(Record) ->
304 case Record of
231f468 @baphled Starting to implement the basic JSON functionality, having fun
authored
305 {carrier, Type, Message} ->
306 mochijson:encode({array, [Type,Message]});
307 _ -> io:format("Illegal message~n")
308 end.
1a3192f @baphled Added xml_to_list, which we'll use to create our XML response, also c…
authored
309
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
310 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
311 %% @private
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
312 %% @doc
313 %%
76910a1 @baphled Fixed side effect bug, now using xml_tuple_single for when we know we…
authored
314 %% Generates out actual XML message.
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
315 %%
316 %% Takes the record and converts it into a tuple which can be further
317 %% converted into a valid XML format using tuple_to_xml.
76910a1 @baphled Fixed side effect bug, now using xml_tuple_single for when we know we…
authored
318 %% Example of XML tuple structure.
319 %% <code>{carrier,"groups",[{carrier,"group","nu"},{carrier,"group","nu2"}]}</code>
320 %%
a8da56a @baphled Cleaned up documentation.
authored
321 %% @spec xml_message(Record) -> XML
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
322 %%
323 %% @end
324 %%--------------------------------------------------------------------
f2676fa @baphled Refactored to further cleanup xml_message, will be able to use loop_c…
authored
325 xml_message(CarrierRecord) ->
326 {carrier, MessageType, Message} = CarrierRecord,
fbe6799 @baphled Finally have functional XML responses for groupslist, should work for…
authored
327 case Message of
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
328 {carrier, Type, Record} ->
329 case Type of
330 "groups" ->
331 RecordList = loop_carrier(Record),
332 tuple_to_xml(xml_tuple(Type,RecordList),[]);
333 "users" ->
334 RecordList = loop_carrier(Record),
335 tuple_to_xml(xml_tuple(Type,RecordList),[]);
336 _ -> io:format("dont know ~s~n",[Type])
337 end;
e1de653 @baphled Removed debug message.
authored
338 _ -> tuple_to_xml(xml_tuple_single(MessageType,Message),[])
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
339 end.
566de6d @baphled General cleanup, will need to implement XML later.
authored
340
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
341 %%--------------------------------------------------------------------
342 %% @private
343 %% @doc
344 %%
345 %% Loops over the record carrier building the tuple structure need to
346 %% build out XML.
347 %% @spec loop_carrier(Carrier) -> XMLTuple
348 %%
349 %% @end
350 %%--------------------------------------------------------------------
f2676fa @baphled Refactored to further cleanup xml_message, will be able to use loop_c…
authored
351 loop_carrier(CarrierRecord) ->
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
352 Result = [loop_xml_tuple(DataType,Data) || {carrier,DataType,Data} <- CarrierRecord],
353 Response = [Result],
354 Response.
355
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
356 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
357 %% @private
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
358 %% @doc
359 %%
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
360 %% Prepares our tuple used to generate our XML.
361 %% @spec xml_tuple(Type,Message) -> {XmlBody}
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
362 %%
363 %% @end
364 %%--------------------------------------------------------------------
f79e8f7 @baphled Added guard to xml_tuple, now we can rename xml_tuple_single and use …
authored
365 xml_tuple(Type,Message) when is_list(Message) ->
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
366 [Data] = Message,
367 {chatterl,[],[{message,[],[{list_to_atom(Type),[],Data}]}]}.
566de6d @baphled General cleanup, will need to implement XML later.
authored
368
1866331 @baphled General cleanup.
authored
369 %%--------------------------------------------------------------------
370 %% @private
371 %% @doc
372 %%
373 %% Prepares our tuple used to generate our sinlge XML elements.
374 %% @spec xml_tuple_single(Type,Message) -> {XmlBody}
375 %%
376 %% @end
377 %%--------------------------------------------------------------------
76910a1 @baphled Fixed side effect bug, now using xml_tuple_single for when we know we…
authored
378 xml_tuple_single(Type,Message) ->
379 {chatterl,[],[{message,[],[{list_to_atom(Type),[],[Message]}]}]}.
6cc9426 @baphled Finally got multiple XML elements, will work on adding functionality …
authored
380 %%--------------------------------------------------------------------
381 %% @private
382 %% @doc
383 %%
384 %% Prepares our tuple used to generate our XML.
385 %% @spec loop_xml_tuple(Type,Message) -> XmlTuple
386 %%
387 %% @end
388 %%--------------------------------------------------------------------
389 loop_xml_tuple(Type,Message) ->
fbe6799 @baphled Finally have functional XML responses for groupslist, should work for…
authored
390 {list_to_atom(Type),[],[Message]}.
76910a1 @baphled Fixed side effect bug, now using xml_tuple_single for when we know we…
authored
391
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
392 %%--------------------------------------------------------------------
a8da56a @baphled Cleaned up documentation.
authored
393 %% @private
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
394 %% @doc
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
395 %% Converts a tuple into XML.
31f3fd0 @baphled General cleanup.
authored
396 %%
397 %% Inspired by the below link.
398 %% @see http://arandomurl.com/post/Simple-XML-in-Erlang
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
399 %% @spec tuple_to_xml(Xml,Prolog) -> [XML]
74cb4b7 @baphled Shelled out functionality to allow us to handle response codes easily…
authored
400 %%
401 %% @end
402 %%--------------------------------------------------------------------
b4e8c22 @baphled Cleaned up documentation from earlier and last night.
authored
403 tuple_to_xml(XmlTuple,Prolog) ->
404 lists:flatten(xmerl:export_simple([XmlTuple],xmerl_xml,[{prolog,Prolog}])).
77f1273 @baphled Added strip_whitespaces method, which we'll use to clean up our XML t…
authored
405
406 %%--------------------------------------------------------------------
407 %% @private
408 %% @doc
409 %% Strips out whitespaces out of out XML tuple.
410 %%
411 %% Extracted from the link below.
412 %% @see http://arandomurl.com/post/Simple-XML-in-Erlang
413 %% @spec tuple_to_xml(Xml,Prolog) -> [XML]
414 %%
415 %% @end
416 %%--------------------------------------------------------------------
417 strip_whitespace({El,Attr,Children}) ->
418 NChild = lists:filter(fun(X) ->
419 case X of
420 " " -> false;
421 _ -> true
422 end
423 end,Children),
424 Ch = lists:map(fun(X) -> strip_whitespace(X) end,NChild),
f79e8f7 @baphled Added guard to xml_tuple, now we can rename xml_tuple_single and use …
authored
425 {El,Attr,Ch}.
Something went wrong with that request. Please try again.