contrib: update clients of `to_json_string`
[nit.git] / contrib / benitlux / src / benitlux_controller.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Actions for the Web interface of Benitlux
18 module benitlux_controller
19
20 import nitcorn
21 import nitcorn::restful
22 private import json::serialization
23
24 import benitlux_model
25 import benitlux_db
26 import benitlux_view
27 import benitlux_social
28
29 # Server action for REST or Web, for a given location
30 abstract class BenitluxAction
31 super Action
32
33 # Database used for both the mailing list and the social network
34 var db: BenitluxDB
35
36 # Path to the storage of the last email sent
37 var sample_email_path = "benitlux_sherbrooke.email"
38 end
39
40 # Web interface to subscribe to the mailing list
41 class BenitluxSubscriptionAction
42 super BenitluxAction
43
44 redef fun answer(request, turi)
45 do
46 var template = new BenitluxDocument
47
48 var sub = request.post_args.keys.has("sub")
49 var unsub = request.all_args.keys.has("unsub")
50
51 var email = null
52 if request.all_args.keys.has("email") then email = request.all_args["email"].trim
53
54 if email != null then
55 if email.is_empty or not email.chars.has('@') or not email.chars.has('.') then
56 template.message_level = "danger"
57 template.message_content = "Invalid email."
58 else if sub and request.post_args.keys.has("email") then
59 template.message_level = "success"
60 template.message_content = "Subscription successful!"
61
62 db.subscribe email
63 else if unsub then
64 template.message_level = "warning"
65 template.message_content = "You've been unsubscribed."
66
67 db.unsubscribe email
68 end
69 end
70
71 if sample_email_path.file_exists then
72 var f = new FileReader.open(sample_email_path)
73 var lines = new Array[String]
74 for line in f.read_all.split_with("\n") do if not line.is_empty then lines.add line
75 f.close
76 template.sample_email_lines = lines
77 end
78
79 var response = new HttpResponse(200)
80 response.body = template.write_to_string
81 return response
82 end
83 end
84
85 # RESTful interface for the client app
86 class BenitluxRESTAction
87 super BenitluxAction
88 super RestfulAction
89
90 # Sign up a new user
91 #
92 # signup?name=a&pass=b&email=c -> LoginResult | BenitluxError
93 fun signup(name, pass, email: String): HttpResponse
94 is restful do
95 # Validate input
96 if not name.name_is_ok then
97 var error = new BenitluxError("Invalid username")
98 return new HttpResponse.ok(error)
99 end
100
101 if not pass.pass_is_ok then
102 var error = new BenitluxError("Invalid password")
103 return new HttpResponse.ok(error)
104 end
105
106 # Query DB
107 var error_message = db.signup(name, pass, email)
108
109 var object: nullable Serializable
110 if error_message == null then
111 object = db.login(name, pass)
112 else
113 object = new BenitluxError(error_message)
114 end
115
116 if object == null then
117 # There was an error in the call to login
118 return new HttpResponse.server_error
119 end
120
121 # It went ok, may or may not be signed up
122 return new HttpResponse.ok(object)
123 end
124
125 # Attempt to login
126 #
127 # login?name=a&pass=b -> LoginResult | BenitluxError
128 fun login(name, pass: String): HttpResponse
129 is restful do
130 var log: nullable Serializable = db.login(name, pass)
131 if log == null then log = new BenitluxError("Login Failed", "Invalid username and password combination.")
132
133 return new HttpResponse.ok(log)
134 end
135
136 # Search a user
137 #
138 # search?token=b&query=a&offset=0 -> Array[UserAndFollowing] | BenitluxError
139 fun search(token: nullable String, query: String): HttpResponse
140 is restful do
141 var user_id = db.token_to_id(token)
142 var users = db.search_users(query, user_id)
143 if users == null then return new HttpResponse.server_error
144
145 return new HttpResponse.ok(users)
146 end
147
148 # List available beers
149 #
150 # list?token=a[&offset=0&count=1] -> Array[BeerAndRatings] | BenitluxError
151 fun list(token: nullable String): HttpResponse
152 is restful do
153 var user_id = db.token_to_id(token)
154 var list = db.list_beers_and_rating(user_id)
155 if list == null then return new HttpResponse.server_error
156
157 return new HttpResponse.ok(list)
158 end
159
160 # Post a review of `beer`
161 #
162 # review?token=a&beer=b&rating=0 -> true | BenitluxError
163 fun review(token: String, rating, beer: Int): HttpResponse
164 is restful do
165 var user_id = db.token_to_id(token)
166 if user_id == null then return new HttpResponse.invalid_token
167
168 db.post_review(user_id, beer, rating, "")
169
170 return new HttpResponse.ok(true)
171 end
172
173 # Set whether user of `token` follows `user_to`, by default set as follow
174 #
175 # follow?token=a&user_to=0 -> true | BenitluxError
176 fun follow(token: String, user_to: Int, follow: nullable Bool): HttpResponse
177 is restful do
178 var user = db.token_to_id(token)
179 if user == null then return new HttpResponse.invalid_token
180
181 if follow or else true then
182 db.add_followed(user, user_to)
183 else db.remove_followed(user, user_to)
184
185 return new HttpResponse.ok(true)
186 end
187
188 # List followers of the user of `token`
189 #
190 # followers?token=a -> Array[UserAndFollowing] | BenitluxError | BenitluxError
191 fun followers(token: String): HttpResponse
192 is restful do
193 var user = db.token_to_id(token)
194 if user == null then return new HttpResponse.invalid_token
195
196 var users = db.followers(user)
197 if users == null then return new HttpResponse.server_error
198
199 return new HttpResponse.ok(users)
200 end
201
202 # List users followed by the user of `token`
203 #
204 # followed?token=a -> Array[UserAndFollowing] | BenitluxError
205 fun followed(token: String): HttpResponse
206 is restful do
207 var user = db.token_to_id(token)
208 if user == null then return new HttpResponse.invalid_token
209
210 var users = db.followed(user)
211 if users == null then return new HttpResponse.server_error
212
213 return new HttpResponse.ok(users)
214 end
215
216 # List friends of the user of `token`
217 #
218 # friends?token=a -> Array[UserAndFollowing] | BenitluxError
219 fun friends(token: String, n: nullable Int): HttpResponse
220 is restful do
221 var user = db.token_to_id(token)
222 var users = db.friends(user, n)
223 if users == null then return new HttpResponse.server_error
224
225 return new HttpResponse.ok(users)
226 end
227
228 # Check user in or out
229 #
230 # checkin?token=a -> true | BenitluxError
231 fun checkin(token: String, is_in: nullable Bool): HttpResponse
232 is restful do
233 var id = db.token_to_id(token)
234 if id == null then return new HttpResponse.invalid_token
235
236 # Register in DB
237 db.checkin(id, is_in or else true)
238
239 # Update followed_followers
240 var common_followers = db.followed_followers(id)
241
242 # Sent push notifications to connected reciprocal friends
243 if common_followers != null then
244 for friend in common_followers do
245 var conn = push_connections.get_or_null(friend.id)
246 if conn != null then
247 push_connections.keys.remove friend.id
248 if not conn.closed then
249 var report = db.checkedin_followed_followers(friend.id)
250 var response = if report == null then
251 new HttpResponse.server_error
252 else new HttpResponse.ok(report)
253 conn.respond response
254 conn.close
255 end
256 end
257 end
258 end
259
260 return new HttpResponse.ok(true)
261 end
262
263 # List users currently checked in among friends of the user of `token`
264 #
265 # checkedin?token=a -> Array[UserAndFollowing]
266 fun checkedin(token: String): HttpResponse
267 is restful do
268 var user_id = db.token_to_id(token)
269 if user_id == null then return new HttpResponse.invalid_token
270
271 var report = db.checkedin_followed_followers(user_id)
272 if report == null then return new HttpResponse.server_error
273 return new HttpResponse.ok(report)
274 end
275
276 # List beer changes since `date` with information in relation to the user of `token`
277 #
278 # since?token=a&date=date -> BeerEvents
279 fun since(token, date: nullable String): HttpResponse
280 is restful do
281 # Query DB
282 var user_id = db.token_to_id(token)
283 var list = db.list_beers_and_rating(user_id, date)
284 if list == null then return new HttpResponse.server_error
285
286 return new HttpResponse.ok(list)
287 end
288
289 # Fallback answer on errors
290 redef fun answer(request, turi) do return new HttpResponse.bad_request
291 end
292
293 # ---
294 # Push notification
295
296 # Benitlux push notification interface
297 class BenitluxPushAction
298 super BenitluxAction
299
300 # Intercept the full answer to set aside the connection and complete it later
301 redef fun prepare_respond_and_close(request, turi, connection)
302 do
303 var token = request.string_arg("token")
304
305 var user = db.token_to_id(token)
306 if user == null then
307 # Report errors right away
308 var response = new HttpResponse.invalid_token
309 connection.respond response
310 connection.close
311 return
312 end
313
314 # Set aside the connection
315 push_connections[user] = connection
316 end
317 end
318
319 redef class Sys
320 # Connections left open for a push notification, organized per user id
321 private var push_connections = new Map[Int, HttpServer]
322 end
323
324 # ---
325 # Misc services
326
327 redef class Text
328 # Rewrite the date represented by `self` in the format expected by SQLite
329 private fun std_date: String
330 do
331 var parts = self.split("-")
332 if parts.length != 3 then return "1970-01-01"
333
334 var y = parts[0].to_s
335 var m = parts[1].to_s
336 var d = parts[2].to_s
337
338 m = "0"*(2 - m.length) + m
339 d = "0"*(2 - d.length) + d
340
341 return "{y}-{m}-{d}"
342 end
343 end
344
345 redef class HttpResponse
346
347 # Respond with `data` in Json and a code 200
348 init ok(data: Serializable)
349 do
350 init 200
351 body = data.serialize_to_json
352 end
353
354 # Respond with a `BenitluxError` in JSON and a code 403
355 init invalid_token
356 do
357 init 403
358 var error = new BenitluxTokenError("Forbidden", "Invalid or outdated token.")
359 body = error.serialize_to_json
360 end
361
362 # Respond with a `BenitluxError` in JSON and a code 400
363 init bad_request
364 do
365 init 400
366 var error = new BenitluxError("Bad Request", "Application error, or it needs to be updated.")
367 body = error.serialize_to_json
368 end
369
370 # Respond with a `BenitluxError` in JSON and a code 500
371 init server_error
372 do
373 init 500
374 var error = new BenitluxError("Internal Server Error", "Server error, try again later.")
375 body = error.serialize_to_json
376 end
377 end