contrib/nitrpg: return 404 on bad urls
[nit.git] / contrib / nitrpg / src / web.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
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 # Display `nitrpg` data as a website.
18 module web
19
20 import nitcorn
21 import templates
22
23 # A custom action forn `nitrpg`.
24 class RpgAction
25 super Action
26
27 # Root URL is used as a prefix for all URL generated by the actions.
28 var root_url: String
29
30 # Github oauth token used for GithubAPI.
31 var auth: String is lazy do return get_github_oauth
32
33 # API client used to import data from Github.
34 var api: GithubAPI is lazy do
35 var api = new GithubAPI(auth)
36 return api
37 end
38
39 init do
40 super
41 if auth.is_empty then
42 print "Error: Invalid Github oauth token!"
43 exit 1
44 end
45 end
46
47 # Return an Error reponse page.
48 fun bad_request(msg: String): HttpResponse do
49 var rsp = new HttpResponse(400)
50 var page = new NitRpgPage(root_url)
51 var error = new ErrorPanel(msg)
52 page.flow_panels.add error
53 rsp.body = page.write_to_string
54 return rsp
55 end
56
57 # Returns the game with `name` or null if no game exists with this name.
58 fun load_game(name: String): nullable Game do
59 var repo = api.load_repo(name)
60 if api.was_error or repo == null then return null
61 var game = new Game(api, repo)
62 game.root_url = root_url
63 return game
64 end
65
66 # Returns the list of saved games from NitRPG data.
67 fun load_games: Array[Game] do
68 var res = new Array[Game]
69 var rpgdir = "nitrpg_data"
70 if not rpgdir.file_exists then return res
71 for user in rpgdir.files do
72 for repo in "{rpgdir}/{user}".files do
73 var game = load_game("{user}/{repo}")
74 if game != null then res.add game
75 end
76 end
77 return res
78 end
79 end
80
81 # Repo overview page.
82 class RpgHome
83 super RpgAction
84
85 # Response page stub.
86 var page: NitRpgPage is noinit
87
88 redef fun answer(request, url) do
89 var readme = load_readme
90 var games = load_games
91 var response = new HttpResponse(200)
92 page = new NitRpgPage(root_url)
93 page.side_panels.add new GamesShortListPanel(root_url, games)
94 page.flow_panels.add new MDPanel(readme)
95 response.body = page.write_to_string
96 return response
97 end
98
99 # Load the string content of the nitrpg readme file.
100 private fun load_readme: String do
101 var readme = "README.md"
102 if not readme.file_exists then
103 return "Unable to locate README file."
104 end
105 var file = new FileReader.open(readme)
106 var text = file.read_all
107 file.close
108 return text
109 end
110 end
111
112 # Display the list of active game.
113 class ListGames
114 super RpgAction
115
116 # Response page stub.
117 var page: NitRpgPage is noinit
118
119 redef fun answer(request, url) do
120 var games = load_games
121 var response = new HttpResponse(200)
122 page = new NitRpgPage(root_url)
123 page.breadcrumbs = new Breadcrumbs
124 page.breadcrumbs.add_link(root_url / "games", "games")
125 page.flow_panels.add new GamesListPanel(root_url, games)
126 response.body = page.write_to_string
127 return response
128 end
129 end
130
131 # An action that require a game.
132 class GameAction
133 super RpgAction
134
135 # Response page stub.
136 var page: NitRpgPage is noinit
137
138 # Target game.
139 var game: Game is noinit
140
141 redef fun answer(request, url) is abstract
142
143 # Check errors and prepare response.
144 private fun prepare_response(request: HttpRequest, url: String): HttpResponse do
145 var owner = request.param("owner")
146 var repo_name = request.param("repo")
147 if owner == null or repo_name == null then
148 var msg = "Bad request: should look like /games/:owner/:repo."
149 return bad_request(msg)
150 end
151 var game = load_game("{owner}/{repo_name}")
152 if game == null then
153 var msg = api.last_error.message
154 return bad_request("Repo Error: {msg}")
155 end
156 self.game = game
157 var response = new HttpResponse(200)
158 page = new NitRpgPage(root_url)
159 page.side_panels.add new GameStatusPanel(game)
160 page.breadcrumbs = new Breadcrumbs
161 page.breadcrumbs.add_link(game.url, game.name)
162 prepare_pagination(request)
163 return response
164 end
165
166 # Parse pagination related parameters.
167 private fun prepare_pagination(request: HttpRequest) do
168 var args = request.get_args
169 list_from = args.get_or_default("pfrom", "0").to_i
170 list_limit = args.get_or_default("plimit", "10").to_i
171 end
172
173 # Limit of events to display in lists.
174 var list_limit = 10
175
176 # From where to start the display of events related lists.
177 var list_from = 0
178
179 # TODO should also check 201, 203 ...
180 private fun is_response_error(response: HttpResponse): Bool do
181 return response.status_code != 200
182 end
183 end
184
185 # Repo overview page.
186 class RepoHome
187 super GameAction
188
189 redef fun answer(request, url) do
190 var rsp = prepare_response(request, url)
191 if is_response_error(rsp) then return rsp
192 page.side_panels.add new ShortListPlayersPanel(game)
193 page.flow_panels.add new PodiumPanel(game)
194 page.flow_panels.add new EventListPanel(game, list_limit, list_from)
195 page.flow_panels.add new AchievementsListPanel(game)
196 rsp.body = page.write_to_string
197 return rsp
198 end
199 end
200
201 # Repo players list.
202 class ListPlayers
203 super GameAction
204
205 redef fun answer(request, url) do
206 var rsp = prepare_response(request, url)
207 if is_response_error(rsp) then return rsp
208 page.breadcrumbs.add_link(game.url / "players", "players")
209 page.flow_panels.add new ListPlayersPanel(game)
210 rsp.body = page.write_to_string
211 return rsp
212 end
213 end
214
215 # Player details page.
216 class PlayerHome
217 super GameAction
218
219 redef fun answer(request, url) do
220 var rsp = prepare_response(request, url)
221 if is_response_error(rsp) then return rsp
222 var name = request.param("player")
223 if name == null then
224 var msg = "Bad request: should look like /:owner/:repo/:players/:name."
225 return bad_request(msg)
226 end
227 var player = game.load_player(name)
228 if player == null then
229 return bad_request("Request Error: unknown player {name}.")
230 end
231 page.breadcrumbs.add_link(game.url / "players", "players")
232 page.breadcrumbs.add_link(player.url, name)
233 page.side_panels.clear
234 page.side_panels.add new PlayerStatusPanel(game, player)
235 page.flow_panels.add new PlayerReviewsPanel(game, player)
236 page.flow_panels.add new AchievementsListPanel(player)
237 page.flow_panels.add new EventListPanel(player, list_limit, list_from)
238 rsp.body = page.write_to_string
239 return rsp
240 end
241 end
242
243 # Display the list of achievements unlocked for this game.
244 class ListAchievements
245 super GameAction
246
247 redef fun answer(request, url) do
248 var rsp = prepare_response(request, url)
249 if is_response_error(rsp) then return rsp
250 page.breadcrumbs.add_link(game.url / "achievements", "achievements")
251 page.flow_panels.add new AchievementsListPanel(game)
252 rsp.body = page.write_to_string
253 return rsp
254 end
255 end
256
257 # Player details page.
258 class AchievementHome
259 super GameAction
260
261 redef fun answer(request, url) do
262 var rsp = prepare_response(request, url)
263 if is_response_error(rsp) then return rsp
264 var name = request.param("achievement")
265 if name == null then
266 var msg = "Bad request: should look like /:owner/:repo/achievements/:achievement."
267 return bad_request(msg)
268 end
269 var achievement = game.load_achievement(name)
270 if achievement == null then
271 return bad_request("Request Error: unknown achievement {name}.")
272 end
273 page.breadcrumbs.add_link(game.url / "achievements", "achievements")
274 page.breadcrumbs.add_link(achievement.url, achievement.name)
275 page.flow_panels.add new AchievementPanel(achievement)
276 page.flow_panels.add new EventListPanel(achievement, list_limit, list_from)
277 rsp.body = page.write_to_string
278 return rsp
279 end
280 end
281
282 if args.length != 3 then
283 print "Error: missing argument"
284 print ""
285 print "Usage:"
286 print "web <host> <port> <root_url>"
287 exit 1
288 end
289
290 var host = args[0]
291 var port = args[1]
292 var root = args[2]
293
294 var iface = "{host}:{port}"
295 var vh = new VirtualHost(iface)
296 vh.routes.add new Route("/styles/", new FileServer("www/styles"))
297 vh.routes.add new Route("/games/:owner/:repo/players/:player", new PlayerHome(root))
298 vh.routes.add new Route("/games/:owner/:repo/players", new ListPlayers(root))
299 vh.routes.add new Route("/games/:owner/:repo/achievements/:achievement", new AchievementHome(root))
300 vh.routes.add new Route("/games/:owner/:repo/achievements", new ListAchievements(root))
301 vh.routes.add new Route("/games/:owner/:repo", new RepoHome(root))
302 vh.routes.add new Route("/games", new ListGames(root))
303 vh.routes.add new Route("/", new RpgHome(root))
304
305 var fac = new HttpFactory.and_libevent
306 fac.config.virtual_hosts.add vh
307
308 print "Launching server on http://{iface}/"
309 fac.run