1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Actions for the Web interface of Benitlux
18 module benitlux_controller
21 import nitcorn
::restful
27 import benitlux_social
29 # Server action for REST or Web, for a given location
30 abstract class BenitluxAction
33 # Database used for both the mailing list and the social network
36 # Path to the storage of the last email sent
37 var sample_email_path
= "benitlux_sherbrooke.email"
40 # Web interface to subscribe to the mailing list
41 class BenitluxSubscriptionAction
44 redef fun answer
(request
, turi
)
46 var template
= new BenitluxDocument
48 var sub
= request
.post_args
.keys
.has
("sub")
49 var unsub
= request
.all_args
.keys
.has
("unsub")
52 if request
.all_args
.keys
.has
("email") then email
= request
.all_args
["email"].trim
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!"
64 template
.message_level
= "warning"
65 template
.message_content
= "You've been unsubscribed."
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
76 template
.sample_email_lines
= lines
79 var response
= new HttpResponse(200)
80 response
.body
= template
.write_to_string
85 # RESTful interface for the client app
86 class BenitluxRESTAction
92 # signup?name=a&pass=b&email=c -> LoginResult | BenitluxError
93 fun signup
(name
, pass
, email
: String): HttpResponse
96 if not name
.name_is_ok
then
97 var error
= new BenitluxError("Invalid username")
98 return new HttpResponse.ok
(error
)
101 if not pass
.pass_is_ok
then
102 var error
= new BenitluxError("Invalid password")
103 return new HttpResponse.ok
(error
)
107 var error_message
= db
.signup
(name
, pass
, email
)
109 var object
: nullable Serializable
110 if error_message
== null then
111 object
= db
.login
(name
, pass
)
113 object
= new BenitluxError(error_message
)
116 if object
== null then
117 # There was an error in the call to login
118 return new HttpResponse.server_error
121 # It went ok, may or may not be signed up
122 return new HttpResponse.ok
(object
)
127 # login?name=a&pass=b -> LoginResult | BenitluxError
128 fun login
(name
, pass
: String): HttpResponse
130 var log
: nullable Serializable = db
.login
(name
, pass
)
131 if log
== null then log
= new BenitluxError("Login Failed", "Invalid username and password combination.")
133 return new HttpResponse.ok
(log
)
138 # check_token?token=a -> true | BenitluxError
139 fun check_token
(token
: String): HttpResponse
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)
148 # search?token=b&query=a&offset=0 -> Array[UserAndFollowing] | BenitluxError
149 fun search
(token
: nullable String, query
: String): HttpResponse
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
155 return new HttpResponse.ok
(users
)
158 # List available beers
160 # list?token=a[&offset=0&count=1] -> Array[BeerAndRatings] | BenitluxError
161 fun list
(token
: nullable String): HttpResponse
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
167 return new HttpResponse.ok
(list
)
170 # Post a review of `beer`
172 # review?token=a&beer=b&rating=0 -> true | BenitluxError
173 fun review
(token
: String, rating
, beer
: Int): HttpResponse
175 var user_id
= db
.token_to_id
(token
)
176 if user_id
== null then return new HttpResponse.invalid_token
178 db
.post_review
(user_id
, beer
, rating
, "")
180 return new HttpResponse.ok
(true)
183 # Set whether user of `token` follows `user_to`, by default set as follow
185 # follow?token=a&user_to=0 -> true | BenitluxError
186 fun follow
(token
: String, user_to
: Int, follow
: nullable Bool): HttpResponse
188 var user
= db
.token_to_id
(token
)
189 if user
== null then return new HttpResponse.invalid_token
191 if follow
or else true then
192 db
.add_followed
(user
, user_to
)
193 else db
.remove_followed
(user
, user_to
)
195 return new HttpResponse.ok
(true)
198 # List followers of the user of `token`
200 # followers?token=a -> Array[UserAndFollowing] | BenitluxError | BenitluxError
201 fun followers
(token
: String): HttpResponse
203 var user
= db
.token_to_id
(token
)
204 if user
== null then return new HttpResponse.invalid_token
206 var users
= db
.followers
(user
)
207 if users
== null then return new HttpResponse.server_error
209 return new HttpResponse.ok
(users
)
212 # List users followed by the user of `token`
214 # followed?token=a -> Array[UserAndFollowing] | BenitluxError
215 fun followed
(token
: String): HttpResponse
217 var user
= db
.token_to_id
(token
)
218 if user
== null then return new HttpResponse.invalid_token
220 var users
= db
.followed
(user
)
221 if users
== null then return new HttpResponse.server_error
223 return new HttpResponse.ok
(users
)
226 # List friends of the user of `token`
228 # friends?token=a -> Array[UserAndFollowing] | BenitluxError
229 fun friends
(token
: String, n
: nullable Int): HttpResponse
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
235 return new HttpResponse.ok
(users
)
238 # Check user in or out
240 # checkin?token=a -> true | BenitluxError
241 fun checkin
(token
: String, is_in
: nullable Bool): HttpResponse
243 var id
= db
.token_to_id
(token
)
244 if id
== null then return new HttpResponse.invalid_token
247 db
.checkin
(id
, is_in
or else true)
249 # Update followed_followers
250 var common_followers
= db
.followed_followers
(id
)
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
)
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
270 return new HttpResponse.ok
(true)
273 # List users currently checked in among friends of the user of `token`
275 # checkedin?token=a -> Array[UserAndFollowing]
276 fun checkedin
(token
: String): HttpResponse
278 var user_id
= db
.token_to_id
(token
)
279 if user_id
== null then return new HttpResponse.invalid_token
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
)
286 # List beer changes since `date` with information in relation to the user of `token`
288 # since?token=a&date=date -> BeerEvents
289 fun since
(token
, date
: nullable String): HttpResponse
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
296 return new HttpResponse.ok
(list
)
299 # Fallback answer on errors
300 redef fun answer
(request
, turi
) do return new HttpResponse.bad_request
306 # Benitlux push notification interface
307 class BenitluxPushAction
310 # Intercept the full answer to set aside the connection and complete it later
311 redef fun prepare_respond_and_close
(request
, turi
, connection
)
313 var token
= request
.string_arg
("token")
315 var user
= db
.token_to_id
(token
)
317 # Report errors right away
318 var response
= new HttpResponse.invalid_token
319 connection
.respond response
324 # Set aside the connection
325 push_connections
[user
] = connection
330 # Connections left open for a push notification, organized per user id
331 private var push_connections
= new Map[Int, HttpServer]
337 # Path to the secret used to authenticate admin requests
338 fun secret_path
: String do return "benitlux.secret"
340 # Services reserved to administrators
341 class BenitluxAdminAction
345 private fun server_secret
: String do return secret_path
.to_path
.read_all
347 # Trigger sending daily menu to connected clients
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
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
360 if server_secret
!= secret
then
361 return new HttpResponse.invalid_token
365 var list
= db
.list_beers_and_rating
366 if list
== null then return new HttpResponse.server_error
368 var msg
= new DailyNotification(list
)
371 for conn
in push_connections
.values
.to_a
do
372 if not conn
.closed
then
373 conn
.respond
new HttpResponse.ok
(msg
)
377 push_connections
.clear
379 return new HttpResponse.ok
(true)
382 redef fun answer
(request
, turi
) do return new HttpResponse.bad_request
389 # Rewrite the date represented by `self` in the format expected by SQLite
390 private fun std_date
: String
392 var parts
= self.split
("-")
393 if parts
.length
!= 3 then return "1970-01-01"
395 var y
= parts
[0].to_s
396 var m
= parts
[1].to_s
397 var d
= parts
[2].to_s
399 m
= "0"*(2 - m
.length
) + m
400 d
= "0"*(2 - d
.length
) + d
406 redef class HttpResponse
408 # Respond with `data` in Json and a code 200
409 init ok
(data
: Serializable)
412 body
= data
.serialize_to_json
415 # Respond with a `BenitluxError` in JSON and a code 403
419 var error
= new BenitluxTokenError("Forbidden", "Invalid or outdated token.")
420 body
= error
.serialize_to_json
423 # Respond with a `BenitluxError` in JSON and a code 400
427 var error
= new BenitluxError("Bad Request", "Application error, or it needs to be updated.")
428 body
= error
.serialize_to_json
431 # Respond with a `BenitluxError` in JSON and a code 500
435 var error
= new BenitluxError("Internal Server Error", "Server error, try again later.")
436 body
= error
.serialize_to_json