blob: 207603052b2bd049243e0781d26bdf95152c452c [file] [log] [blame]
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08001package requests
2
3import (
Philipp Schraderfae8a7e2022-03-13 22:51:54 -07004 "encoding/base64"
Philipp Schraderd3fac192022-03-02 20:35:46 -08005 "errors"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08006 "fmt"
7 "io"
Philipp Schraderfae8a7e2022-03-13 22:51:54 -07008 "log"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08009 "net/http"
Philipp Schraderd3fac192022-03-02 20:35:46 -080010 "strconv"
11 "strings"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080012
Philipp Schrader8747f1b2022-02-23 23:56:22 -080013 "github.com/frc971/971-Robot-Code/scouting/db"
Philipp Schraderd3fac192022-03-02 20:35:46 -080014 "github.com/frc971/971-Robot-Code/scouting/scraping"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080015 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
Philipp Schraderd3fac192022-03-02 20:35:46 -080016 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list"
17 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list_response"
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080018 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
19 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
Philipp Schraderacf96232022-03-01 22:03:30 -080020 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting"
21 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting_response"
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080022 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team"
23 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080024 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
Philipp Schrader30005e42022-03-06 13:53:58 -080025 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080026 "github.com/frc971/971-Robot-Code/scouting/webserver/server"
27 flatbuffers "github.com/google/flatbuffers/go"
28)
29
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080030type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
Philipp Schrader30005e42022-03-06 13:53:58 -080031type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080032type RequestAllMatches = request_all_matches.RequestAllMatches
33type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080034type RequestMatchesForTeam = request_matches_for_team.RequestMatchesForTeam
35type RequestMatchesForTeamResponseT = request_matches_for_team_response.RequestMatchesForTeamResponseT
Philipp Schraderacf96232022-03-01 22:03:30 -080036type RequestDataScouting = request_data_scouting.RequestDataScouting
37type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
Philipp Schraderd3fac192022-03-02 20:35:46 -080038type RefreshMatchList = refresh_match_list.RefreshMatchList
39type RefreshMatchListResponseT = refresh_match_list_response.RefreshMatchListResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080040
Philipp Schrader8747f1b2022-02-23 23:56:22 -080041// The interface we expect the database abstraction to conform to.
42// We use an interface here because it makes unit testing easier.
43type Database interface {
44 AddToMatch(db.Match) error
45 AddToStats(db.Stats) error
46 ReturnMatches() ([]db.Match, error)
47 ReturnStats() ([]db.Stats, error)
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080048 QueryMatches(int32) ([]db.Match, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080049 QueryStats(int) ([]db.Stats, error)
50}
51
Philipp Schraderd3fac192022-03-02 20:35:46 -080052type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
53
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080054// Handles unknown requests. Just returns a 404.
55func unknown(w http.ResponseWriter, req *http.Request) {
56 w.WriteHeader(http.StatusNotFound)
57}
58
59func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
60 builder := flatbuffers.NewBuilder(1024)
61 builder.Finish((&error_response.ErrorResponseT{
62 ErrorMessage: errorMessage,
63 }).Pack(builder))
64 w.WriteHeader(statusCode)
65 w.Write(builder.FinishedBytes())
66}
67
68func respondNotImplemented(w http.ResponseWriter) {
69 respondWithError(w, http.StatusNotImplemented, "")
70}
71
72// TODO(phil): Can we turn this into a generic?
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080073func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*SubmitDataScouting, bool) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080074 success := true
75 defer func() {
76 if r := recover(); r != nil {
77 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
78 success = false
79 }
80 }()
81 result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
82 return result, success
83}
84
Philipp Schraderfae8a7e2022-03-13 22:51:54 -070085// Parses the authorization information that the browser inserts into the
86// headers. The authorization follows this format:
87//
88// req.Headers["Authorization"] = []string{"Basic <base64 encoded username:password>"}
89func parseUsername(req *http.Request) string {
90 auth, ok := req.Header["Authorization"]
91 if !ok {
92 return "unknown"
93 }
94
95 parts := strings.Split(auth[0], " ")
96 if !(len(parts) == 2 && parts[0] == "Basic") {
97 return "unknown"
98 }
99
100 info, err := base64.StdEncoding.DecodeString(parts[1])
101 if err != nil {
102 log.Println("ERROR: Failed to parse Basic authentication.")
103 return "unknown"
104 }
105
106 loginParts := strings.Split(string(info), ":")
107 if len(loginParts) != 2 {
108 return "unknown"
109 }
110 return loginParts[0]
111}
112
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800113// Handles a SubmitDataScouting request.
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800114type submitDataScoutingHandler struct {
115 db Database
116}
117
118func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700119 // Get the username of the person submitting the data.
120 username := parseUsername(req)
121 log.Println("Got data scouting data from", username)
122
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800123 requestBytes, err := io.ReadAll(req.Body)
124 if err != nil {
125 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
126 return
127 }
128
Philipp Schrader30005e42022-03-06 13:53:58 -0800129 request, success := parseSubmitDataScouting(w, requestBytes)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800130 if !success {
131 return
132 }
133
Philipp Schrader30005e42022-03-06 13:53:58 -0800134 stats := db.Stats{
135 TeamNumber: request.Team(),
136 MatchNumber: request.Match(),
137 ShotsMissedAuto: request.MissedShotsAuto(),
138 UpperGoalAuto: request.UpperGoalAuto(),
139 LowerGoalAuto: request.LowerGoalAuto(),
140 ShotsMissed: request.MissedShotsTele(),
141 UpperGoalShots: request.UpperGoalTele(),
142 LowerGoalShots: request.LowerGoalTele(),
143 PlayedDefense: request.DefenseRating(),
144 Climbing: request.Climbing(),
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700145 CollectedBy: username,
Philipp Schrader30005e42022-03-06 13:53:58 -0800146 }
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800147
Philipp Schrader30005e42022-03-06 13:53:58 -0800148 err = handler.db.AddToStats(stats)
149 if err != nil {
150 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
151 }
152
153 builder := flatbuffers.NewBuilder(50 * 1024)
154 builder.Finish((&SubmitDataScoutingResponseT{}).Pack(builder))
155 w.Write(builder.FinishedBytes())
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800156}
157
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800158// TODO(phil): Can we turn this into a generic?
159func parseRequestAllMatches(w http.ResponseWriter, buf []byte) (*RequestAllMatches, bool) {
160 success := true
161 defer func() {
162 if r := recover(); r != nil {
163 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
164 success = false
165 }
166 }()
167 result := request_all_matches.GetRootAsRequestAllMatches(buf, 0)
168 return result, success
169}
170
171// Handles a RequestAllMaches request.
172type requestAllMatchesHandler struct {
173 db Database
174}
175
176func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
177 requestBytes, err := io.ReadAll(req.Body)
178 if err != nil {
179 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
180 return
181 }
182
183 _, success := parseRequestAllMatches(w, requestBytes)
184 if !success {
185 return
186 }
187
188 matches, err := handler.db.ReturnMatches()
189 if err != nil {
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700190 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800191 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800192 }
193
194 var response RequestAllMatchesResponseT
195 for _, match := range matches {
196 response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
197 MatchNumber: match.MatchNumber,
198 Round: match.Round,
199 CompLevel: match.CompLevel,
200 R1: match.R1,
201 R2: match.R2,
202 R3: match.R3,
203 B1: match.B1,
204 B2: match.B2,
205 B3: match.B3,
206 })
207 }
208
209 builder := flatbuffers.NewBuilder(50 * 1024)
210 builder.Finish((&response).Pack(builder))
211 w.Write(builder.FinishedBytes())
212}
213
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800214// TODO(phil): Can we turn this into a generic?
215func parseRequestMatchesForTeam(w http.ResponseWriter, buf []byte) (*RequestMatchesForTeam, bool) {
216 success := true
217 defer func() {
218 if r := recover(); r != nil {
219 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
220 success = false
221 }
222 }()
223 result := request_matches_for_team.GetRootAsRequestMatchesForTeam(buf, 0)
224 return result, success
225}
226
227// Handles a RequestMatchesForTeam request.
228type requestMatchesForTeamHandler struct {
229 db Database
230}
231
232func (handler requestMatchesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
233 requestBytes, err := io.ReadAll(req.Body)
234 if err != nil {
235 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
236 return
237 }
238
239 request, success := parseRequestMatchesForTeam(w, requestBytes)
240 if !success {
241 return
242 }
243
244 matches, err := handler.db.QueryMatches(request.Team())
245 if err != nil {
246 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800247 return
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800248 }
249
250 var response RequestAllMatchesResponseT
251 for _, match := range matches {
252 response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
253 MatchNumber: match.MatchNumber,
254 Round: match.Round,
255 CompLevel: match.CompLevel,
256 R1: match.R1,
257 R2: match.R2,
258 R3: match.R3,
259 B1: match.B1,
260 B2: match.B2,
261 B3: match.B3,
262 })
263 }
264
265 builder := flatbuffers.NewBuilder(50 * 1024)
266 builder.Finish((&response).Pack(builder))
267 w.Write(builder.FinishedBytes())
268}
269
Philipp Schraderacf96232022-03-01 22:03:30 -0800270// TODO(phil): Can we turn this into a generic?
271func parseRequestDataScouting(w http.ResponseWriter, buf []byte) (*RequestDataScouting, bool) {
272 success := true
273 defer func() {
274 if r := recover(); r != nil {
275 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
276 success = false
277 }
278 }()
279 result := request_data_scouting.GetRootAsRequestDataScouting(buf, 0)
280 return result, success
281}
282
283// Handles a RequestDataScouting request.
284type requestDataScoutingHandler struct {
285 db Database
286}
287
288func (handler requestDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
289 requestBytes, err := io.ReadAll(req.Body)
290 if err != nil {
291 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
292 return
293 }
294
295 _, success := parseRequestDataScouting(w, requestBytes)
296 if !success {
297 return
298 }
299
300 stats, err := handler.db.ReturnStats()
301 if err != nil {
302 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800303 return
Philipp Schraderacf96232022-03-01 22:03:30 -0800304 }
305
306 var response RequestDataScoutingResponseT
307 for _, stat := range stats {
308 response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
309 Team: stat.TeamNumber,
310 Match: stat.MatchNumber,
311 MissedShotsAuto: stat.ShotsMissedAuto,
312 UpperGoalAuto: stat.UpperGoalAuto,
313 LowerGoalAuto: stat.LowerGoalAuto,
314 MissedShotsTele: stat.ShotsMissed,
315 UpperGoalTele: stat.UpperGoalShots,
316 LowerGoalTele: stat.LowerGoalShots,
317 DefenseRating: stat.PlayedDefense,
318 Climbing: stat.Climbing,
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700319 CollectedBy: stat.CollectedBy,
Philipp Schraderacf96232022-03-01 22:03:30 -0800320 })
321 }
322
323 builder := flatbuffers.NewBuilder(50 * 1024)
324 builder.Finish((&response).Pack(builder))
325 w.Write(builder.FinishedBytes())
326}
327
Philipp Schraderd3fac192022-03-02 20:35:46 -0800328// TODO(phil): Can we turn this into a generic?
329func parseRefreshMatchList(w http.ResponseWriter, buf []byte) (*RefreshMatchList, bool) {
330 success := true
331 defer func() {
332 if r := recover(); r != nil {
333 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
334 success = false
335 }
336 }()
337 result := refresh_match_list.GetRootAsRefreshMatchList(buf, 0)
338 return result, success
339}
340
341func parseTeamKey(teamKey string) (int, error) {
342 // TBA prefixes teams with "frc". Not sure why. Get rid of that.
343 teamKey = strings.TrimPrefix(teamKey, "frc")
344 return strconv.Atoi(teamKey)
345}
346
347// Parses the alliance data from the specified match and returns the three red
348// teams and the three blue teams.
349func parseTeamKeys(match *scraping.Match) ([3]int32, [3]int32, error) {
350 redKeys := match.Alliances.Red.TeamKeys
351 blueKeys := match.Alliances.Blue.TeamKeys
352
353 if len(redKeys) != 3 || len(blueKeys) != 3 {
354 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
355 "Found %d red teams and %d blue teams.", len(redKeys), len(blueKeys)))
356 }
357
358 var red [3]int32
359 for i, key := range redKeys {
360 team, err := parseTeamKey(key)
361 if err != nil {
362 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
363 "Failed to parse red %d team '%s' as integer: %v", i+1, key, err))
364 }
365 red[i] = int32(team)
366 }
367 var blue [3]int32
368 for i, key := range blueKeys {
369 team, err := parseTeamKey(key)
370 if err != nil {
371 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
372 "Failed to parse blue %d team '%s' as integer: %v", i+1, key, err))
373 }
374 blue[i] = int32(team)
375 }
376 return red, blue, nil
377}
378
379type refreshMatchListHandler struct {
380 db Database
381 scrape ScrapeMatchList
382}
383
384func (handler refreshMatchListHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
385 requestBytes, err := io.ReadAll(req.Body)
386 if err != nil {
387 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
388 return
389 }
390
391 request, success := parseRefreshMatchList(w, requestBytes)
392 if !success {
393 return
394 }
395
396 matches, err := handler.scrape(request.Year(), string(request.EventCode()))
397 if err != nil {
398 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to scrape match list: ", err))
399 return
400 }
401
402 for _, match := range matches {
403 // Make sure the data is valid.
404 red, blue, err := parseTeamKeys(&match)
405 if err != nil {
406 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
407 "TheBlueAlliance data for match %d is malformed: %v", match.MatchNumber, err))
408 return
409 }
410 // Add the match to the database.
Philipp Schrader7365d322022-03-06 16:40:08 -0800411 err = handler.db.AddToMatch(db.Match{
Philipp Schraderd3fac192022-03-02 20:35:46 -0800412 MatchNumber: int32(match.MatchNumber),
413 // TODO(phil): What does Round mean?
414 Round: 1,
415 CompLevel: match.CompLevel,
416 R1: red[0],
417 R2: red[1],
418 R3: red[2],
419 B1: blue[0],
420 B2: blue[1],
421 B3: blue[2],
422 })
Philipp Schrader7365d322022-03-06 16:40:08 -0800423 if err != nil {
424 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
425 "Failed to add match %d to the database: %v", match.MatchNumber, err))
426 return
427 }
Philipp Schraderd3fac192022-03-02 20:35:46 -0800428 }
429
430 var response RefreshMatchListResponseT
431 builder := flatbuffers.NewBuilder(1024)
432 builder.Finish((&response).Pack(builder))
433 w.Write(builder.FinishedBytes())
434}
435
436func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800437 scoutingServer.HandleFunc("/requests", unknown)
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800438 scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800439 scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800440 scoutingServer.Handle("/requests/request/matches_for_team", requestMatchesForTeamHandler{db})
Philipp Schraderacf96232022-03-01 22:03:30 -0800441 scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
Philipp Schraderd3fac192022-03-02 20:35:46 -0800442 scoutingServer.Handle("/requests/refresh_match_list", refreshMatchListHandler{db, scrape})
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800443}