core: fix typos in doc of CircularArray
[nit.git] / contrib / benitlux / src / server / 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
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 # Is `token` valid?
137 #
138 # check_token?token=a -> true | BenitluxError
139 fun check_token(token: String): HttpResponse
140 is restful do
141 var user_id = db.token_to_id(token)
142 if user_id == null then return new HttpResponse.invalid_token
143 return new HttpResponse.ok(true)
144 end
145
146 # Search a user
147 #
148 # search?token=b&query=a&offset=0 -> Array[UserAndFollowing] | BenitluxError
149 fun search(token: nullable String, query: String): HttpResponse
150 is restful do
151 var user_id = db.token_to_id(token)
152 var users = db.search_users(query, user_id)
153 if users == null then return new HttpResponse.server_error
154
155 return new HttpResponse.ok(users)
156 end
157
158 # List available beers
159 #
160 # list?token=a[&offset=0&count=1] -> Array[BeerAndRatings] | BenitluxError
161 fun list(token: nullable String): HttpResponse
162 is restful do
163 var user_id = db.token_to_id(token)
164 var list = db.list_beers_and_rating(user_id)
165 if list == null then return new HttpResponse.server_error
166
167 return new HttpResponse.ok(list)
168 end
169
170 # Post a review of `beer`
171 #
172 # review?token=a&beer=b&rating=0 -> true | BenitluxError
173 fun review(token: String, rating, beer: Int): HttpResponse
174 is restful do
175 var user_id = db.token_to_id(token)
176 if user_id == null then return new HttpResponse.invalid_token
177
178 db.post_review(user_id, beer, rating, "")
179
180 return new HttpResponse.ok(true)
181 end
182
183 # Set whether user of `token` follows `user_to`, by default set as follow
184 #
185 # follow?token=a&user_to=0 -> true | BenitluxError
186 fun follow(token: String, user_to: Int, follow: nullable Bool): HttpResponse
187 is restful do
188 var user = db.token_to_id(token)
189 if user == null then return new HttpResponse.invalid_token
190
191 if follow or else true then
192 db.add_followed(user, user_to)
193 else db.remove_followed(user, user_to)
194
195 return new HttpResponse.ok(true)
196 end
197
198 # List followers of the user of `token`
199 #
200 # followers?token=a -> Array[UserAndFollowing] | BenitluxError | BenitluxError
201 fun followers(token: String): HttpResponse
202 is restful do
203 var user = db.token_to_id(token)
204 if user == null then return new HttpResponse.invalid_token
205
206 var users = db.followers(user)
207 if users == null then return new HttpResponse.server_error
208
209 return new HttpResponse.ok(users)
210 end
211
212 # List users followed by the user of `token`
213 #
214 # followed?token=a -> Array[UserAndFollowing] | BenitluxError
215 fun followed(token: String): HttpResponse
216 is restful do
217 var user = db.token_to_id(token)
218 if user == null then return new HttpResponse.invalid_token
219
220 var users = db.followed(user)
221 if users == null then return new HttpResponse.server_error
222
223 return new HttpResponse.ok(users)
224 end
225
226 # List friends of the user of `token`
227 #
228 # friends?token=a -> Array[UserAndFollowing] | BenitluxError
229 fun friends(token: String, n: nullable Int): HttpResponse
230 is restful do
231 var user = db.token_to_id(token)
232 var users = db.friends(user, n)
233 if users == null then return new HttpResponse.server_error
234
235 return new HttpResponse.ok(users)
236 end
237
238 # Check user in or out
239 #
240 # checkin?token=a -> true | BenitluxError
241 fun checkin(token: String, is_in: nullable Bool): HttpResponse
242 is restful do
243 var id = db.token_to_id(token)
244 if id == null then return new HttpResponse.invalid_token
245
246 # Register in DB
247 db.checkin(id, is_in or else true)
248
249 # Update followed_followers
250 var common_followers = db.followed_followers(id)
251
252 # Sent push notifications to connected reciprocal friends
253 if common_followers != null then
254 for friend in common_followers do
255 var conn = push_connections.get_or_null(friend.id)
256 if conn != null then
257 push_connections.keys.remove friend.id
258 if not conn.closed then
259 var report = db.checkedin_followed_followers(friend.id)
260 var response = if report == null then
261 new HttpResponse.server_error
262 else new HttpResponse.ok(report)
263 conn.respond response
264 conn.close
265 end
266 end
267 end
268 end
269
270 return new HttpResponse.ok(true)
271 end
272
273 # List users currently checked in among friends of the user of `token`
274 #
275 # checkedin?token=a -> Array[UserAndFollowing]
276 fun checkedin(token: String): HttpResponse
277 is restful do
278 var user_id = db.token_to_id(token)
279 if user_id == null then return new HttpResponse.invalid_token
280
281 var report = db.checkedin_followed_followers(user_id)
282 if report == null then return new HttpResponse.server_error
283 return new HttpResponse.ok(report)
284 end
285
286 # List beer changes since `date` with information in relation to the user of `token`
287 #
288 # since?token=a&date=date -> BeerEvents
289 fun since(token, date: nullable String): HttpResponse
290 is restful do
291 # Query DB
292 var user_id = db.token_to_id(token)
293 var list = db.list_beers_and_rating(user_id, date)
294 if list == null then return new HttpResponse.server_error
295
296 return new HttpResponse.ok(list)
297 end
298
299 # Fallback answer on errors
300 redef fun answer(request, turi) do return new HttpResponse.bad_request
301 end
302
303 # ---
304 # Push notification
305
306 # Benitlux push notification interface
307 class BenitluxPushAction
308 super BenitluxAction
309
310 # Intercept the full answer to set aside the connection and complete it later
311 redef fun prepare_respond_and_close(request, turi, connection)
312 do
313 var token = request.string_arg("token")
314
315 var user = db.token_to_id(token)
316 if user == null then
317 # Report errors right away
318 var response = new HttpResponse.invalid_token
319 connection.respond response
320 connection.close
321 return
322 end
323
324 # Set aside the connection
325 push_connections[user] = connection
326 end
327 end
328
329 redef class Sys
330 # Connections left open for a push notification, organized per user id
331 private var push_connections = new Map[Int, HttpServer]
332 end
333
334 # ---
335 # Administration
336
337 # Path to the secret used to authenticate admin requests
338 fun secret_path: String do return "benitlux.secret"
339
340 # Services reserved to administrators
341 class BenitluxAdminAction
342 super BenitluxAction
343 super RestfulAction
344
345 private fun server_secret: String do return secret_path.to_path.read_all
346
347 # Trigger sending daily menu to connected clients
348 #
349 # This should usually be called by an external cron program.
350 # send_daily_updates?secret=shared_secret -> true | BenitluxError
351 fun send_daily_updates(secret: nullable String): HttpResponse
352 is restful do
353 # Check secrets
354 var server_secret = server_secret
355 if server_secret.is_empty then
356 print_error "The admin interface needs a secret at '{secret_path}'"
357 return new HttpResponse.server_error
358 end
359
360 if server_secret != secret then
361 return new HttpResponse.invalid_token
362 end
363
364 # Load beer menu
365 var list = db.list_beers_and_rating
366 if list == null then return new HttpResponse.server_error
367
368 var msg = new DailyNotification(list)
369
370 # Broadcast updates
371 for conn in push_connections.values.to_a do
372 if not conn.closed then
373 conn.respond new HttpResponse.ok(msg)
374 conn.close
375 end
376 end
377 push_connections.clear
378
379 return new HttpResponse.ok(true)
380 end
381
382 redef fun answer(request, turi) do return new HttpResponse.bad_request
383 end
384
385 # ---
386 # Misc services
387
388 redef class Text
389 # Rewrite the date represented by `self` in the format expected by SQLite
390 private fun std_date: String
391 do
392 var parts = self.split("-")
393 if parts.length != 3 then return "1970-01-01"
394
395 var y = parts[0].to_s
396 var m = parts[1].to_s
397 var d = parts[2].to_s
398
399 m = "0"*(2 - m.length) + m
400 d = "0"*(2 - d.length) + d
401
402 return "{y}-{m}-{d}"
403 end
404 end
405
406 redef class HttpResponse
407
408 # Respond with `data` in Json and a code 200
409 init ok(data: Serializable)
410 do
411 init 200
412 body = data.serialize_to_json
413 end
414
415 # Respond with a `BenitluxError` in JSON and a code 403
416 init invalid_token
417 do
418 init 403
419 var error = new BenitluxTokenError("Forbidden", "Invalid or outdated token.")
420 body = error.serialize_to_json
421 end
422
423 # Respond with a `BenitluxError` in JSON and a code 400
424 init bad_request
425 do
426 init 400
427 var error = new BenitluxError("Bad Request", "Application error, or it needs to be updated.")
428 body = error.serialize_to_json
429 end
430
431 # Respond with a `BenitluxError` in JSON and a code 500
432 init server_error
433 do
434 init 500
435 var error = new BenitluxError("Internal Server Error", "Server error, try again later.")
436 body = error.serialize_to_json
437 end
438 end