contrib/tnitter: add REST interface
[nit.git] / contrib / tnitter / src / action.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 # View and controller of Tnitter
18 module action
19
20 import nitcorn
21 import json::serialization
22
23 import model
24 import database
25
26 # Path to the Sqlite3 database
27 fun tnitter_db_path: String do return "tnitter.db"
28
29 redef class Session
30 # User logged in
31 var user: nullable String = null
32 end
33
34 # Main Tnitter Action
35 class TnitterWeb
36 super Action
37
38 # Header on pages served by this `Action`
39 #
40 # Keywords to `Text::replace`:
41 # * `%app_path%` is the main URL to reach this `Action`
42 # * `%nav_right%` is the pulled right part of the header, used for login form
43 var header = """
44 <nav class="navbar navbar-default" role="navigation">
45 <div class="container-fluid">
46 <div class="navbar-header">
47 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
48 <span class="sr-only">Toggle navigation</span>
49 <span class="icon-bar"></span>
50 <span class="icon-bar"></span>
51 <span class="icon-bar"></span>
52 </button>
53 <a class="navbar-brand" href="%app_path%">Tnitter</a>
54 </div>
55
56 <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
57 <ul class="nav navbar-nav">
58 <li><a href="https://github.com/nitlang/nit/">Nit repository</a></li>
59 </ul>
60
61 <ul class="nav navbar-nav pull-right">
62 %header_right%
63 </ul>
64 </div>
65 </div>
66 </nav>"""
67
68 # Template of the pages served by this `Action`
69 #
70 # Keywords to `Text::replace`:
71 # * The `%header%`, first thing in the `<body>`
72 # * The main page `%content%` within a `<div class="container">`
73 var template = """
74 <!DOCTYPE html>
75 <html>
76 <head>
77 <title>Tnitter</title>
78 <meta charset="utf-8">
79 <meta http-equiv="X-UA-Compatible" content="IE=edge">
80 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
81
82 <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
83 <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
84 <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
85 </head>
86 <body>
87
88 %header%
89
90 <div class="container">
91 %content%
92 </div>
93 </body>
94 </html>"""
95
96 redef fun answer(request, turi)
97 do
98 # Get existing session
99 var session = request.session
100
101 # Error to display on page as a dismissable panel
102 var error = null
103
104 var db = new DB.open(tnitter_db_path)
105
106 # Login/logout
107 if turi == "/login" and request.post_args.keys.has("user") and
108 request.post_args.keys.has("pass") then
109
110 var user = request.post_args["user"].trim
111 var pass = request.post_args["pass"]
112
113 var original_user = db.check_login(user, pass)
114 if original_user != null then
115 # Log in successful
116 if session == null then session = new Session
117 session.user = original_user
118 else
119 # Check for basic requirements
120 if user.is_empty then
121 error = "Username must have at least 1 character"
122 else if user.chars.has(' ') or user.chars.has('\n') then
123 error = "Username cannot contain white spaces"
124 else if db.sign_up(user, pass) then
125 # Sign up successful
126 if session == null then session = new Session
127 session.user = user
128 else
129 # Invalid user/pass
130 error = "Invalid combination of username and password"
131 session = null
132 end
133 end
134 else if turi == "/logout" then
135 # Logging out
136 session = null
137 else if turi == "/post" and request.post_args.keys.has("text") and session != null then
138 var user = session.user
139 var text = request.post_args["text"].trim
140 if user != null and not text.is_empty then
141 # Post a Tnit!
142 db.post(user, text)
143 db.close
144
145 # Redirect the user to avoid double posting
146 var response = new HttpResponse(303)
147 response.header["Location"] = request.uri
148 response.session = session
149 return response
150 end
151 end
152
153 var login_or_out
154 var content
155 if session == null or session.user == null then
156 # Log in form in the navbar
157 login_or_out = """
158 <li>
159 <form class="navbar-form" role="form" action="login" method="POST">
160 <div class="form-group">
161 <input type="text" placeholder="Username" class="form-control" name="user">
162 </div>
163 <div class="form-group">
164 <input type="password" placeholder="Password" class="form-control" name="pass">
165 </div>
166 <button type="submit" class="btn btn-default">Log in (or sign up)</button>
167 </form>
168 </li>
169 """
170
171 # Cannot post when not logged in
172 content = ""
173 else
174 # Log out form in the navbar
175 login_or_out = """
176 <li><p class="navbar-text">Signed in as @{{{session.user.html_escape}}}</p></li>
177 <li>
178 <form class="navbar-form" role="form" action="logout" method="POST">
179 <button type="submit" class="btn btn-default">Log out</button>
180 </form>
181 </li>
182 """
183
184 # Post form
185 content = """
186 <form class="form" role="form" action="post" method="POST">
187 <div class="form-group">
188 <div class="input-group">
189 <div class="input-group-addon">Share your thoughts</div>
190 <input class="form-control" type="text" placeholder="..." name="text">
191 <span class="input-group-btn">
192 <button class="btn btn-default" type="submit">Tnit!</button>
193 </span>
194 </div><!-- /input-group -->
195 </div>
196 </form>
197 """
198 end
199
200 # Show error if any
201 var error_html
202 if error != null then
203 error_html = """
204 <div class="alert alert-danger alert-dismissible" role="alert">
205 <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
206 {{{error}}}
207 </div>
208 """
209 else error_html = ""
210
211 # Load the last 16 Tnits
212 var posts = db.list_posts(0, 16)
213 db.close
214
215 var html_posts = new Array[String]
216 for post in posts do
217 html_posts.add "<tr><td>@{post.user.html_escape}</td><td>{post.text.html_escape}</td></tr>"
218 end
219
220 content += """
221 <div class="panel panel-default">
222 <div class="panel-heading">Latest Tnits</div>
223 <table class="table table-striped">
224 {{{html_posts.join("\n")}}}
225 </table>
226 </div>
227 </div>
228 """
229
230 # Get page from template, we replace the header first so we can replace
231 # everything on the same body afterwards
232 var body = template.
233 replace("%header%", header).
234 replace("%app_path%", request.uri.strip_extension(turi) + "/").
235 replace("%header_right%", login_or_out).
236 replace("%content%", error_html + content)
237
238 # Build response
239 var response = new HttpResponse(200)
240 response.body = body
241 response.session = session
242 return response
243 end
244 end
245
246 # Tnitter RESTful interface
247 class TnitterREST
248 super Action
249
250 redef fun answer(request, turi)
251 do
252 if turi == "/list" then
253 # list?from=1&count=2 -> Error | Array[Post]
254
255 var from = request.int_arg("from") or else 0
256 var count = request.int_arg("count") or else 8
257
258 var db = new DB.open(tnitter_db_path)
259 var posts = db.list_posts(from, count)
260 db.close
261
262 var response = new HttpResponse(200)
263 response.body = posts.to_json_string
264 return response
265 end
266
267 # Format not recognized
268 var error = new Error("Bad Request")
269 var response = new HttpResponse(400)
270 response.body = error.to_json_string
271 return response
272 end
273 end