blob: 07b7d8a59c03c7582495fe061f8a1733b7c7a328 [file] [log] [blame]
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08001package requests
2
3import (
Philipp Schraderd3fac192022-03-02 20:35:46 -08004 "errors"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -08005 "fmt"
6 "io"
7 "net/http"
Philipp Schraderd3fac192022-03-02 20:35:46 -08008 "strconv"
9 "strings"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080010
Philipp Schrader8747f1b2022-02-23 23:56:22 -080011 "github.com/frc971/971-Robot-Code/scouting/db"
Philipp Schraderd3fac192022-03-02 20:35:46 -080012 "github.com/frc971/971-Robot-Code/scouting/scraping"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080013 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
Philipp Schraderd3fac192022-03-02 20:35:46 -080014 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list"
15 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list_response"
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080016 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
17 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
Philipp Schraderacf96232022-03-01 22:03:30 -080018 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting"
19 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting_response"
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080020 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team"
21 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_matches_for_team_response"
Alex Perry81f96ba2022-03-13 18:26:19 -070022 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
23 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_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"
Alex Perry81f96ba2022-03-13 18:26:19 -070026 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
27 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080028 "github.com/frc971/971-Robot-Code/scouting/webserver/server"
29 flatbuffers "github.com/google/flatbuffers/go"
30)
31
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080032type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
Philipp Schrader30005e42022-03-06 13:53:58 -080033type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080034type RequestAllMatches = request_all_matches.RequestAllMatches
35type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080036type RequestMatchesForTeam = request_matches_for_team.RequestMatchesForTeam
37type RequestMatchesForTeamResponseT = request_matches_for_team_response.RequestMatchesForTeamResponseT
Philipp Schraderacf96232022-03-01 22:03:30 -080038type RequestDataScouting = request_data_scouting.RequestDataScouting
39type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
Philipp Schraderd3fac192022-03-02 20:35:46 -080040type RefreshMatchList = refresh_match_list.RefreshMatchList
41type RefreshMatchListResponseT = refresh_match_list_response.RefreshMatchListResponseT
Alex Perry81f96ba2022-03-13 18:26:19 -070042type SubmitNotes = submit_notes.SubmitNotes
43type SubmitNotesResponseT = submit_notes_response.SubmitNotesResponseT
44type RequestNotesForTeam = request_notes_for_team.RequestNotesForTeam
45type RequestNotesForTeamResponseT = request_notes_for_team_response.RequestNotesForTeamResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080046
Philipp Schrader8747f1b2022-02-23 23:56:22 -080047// The interface we expect the database abstraction to conform to.
48// We use an interface here because it makes unit testing easier.
49type Database interface {
50 AddToMatch(db.Match) error
51 AddToStats(db.Stats) error
52 ReturnMatches() ([]db.Match, error)
53 ReturnStats() ([]db.Stats, error)
Philipp Schraderd1c4bef2022-02-28 22:51:30 -080054 QueryMatches(int32) ([]db.Match, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080055 QueryStats(int) ([]db.Stats, error)
Alex Perry81f96ba2022-03-13 18:26:19 -070056 QueryNotes(int32) (db.NotesData, error)
57 AddNotes(db.NotesData) error
Philipp Schrader8747f1b2022-02-23 23:56:22 -080058}
59
Philipp Schraderd3fac192022-03-02 20:35:46 -080060type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
61
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080062// Handles unknown requests. Just returns a 404.
63func unknown(w http.ResponseWriter, req *http.Request) {
64 w.WriteHeader(http.StatusNotFound)
65}
66
67func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
68 builder := flatbuffers.NewBuilder(1024)
69 builder.Finish((&error_response.ErrorResponseT{
70 ErrorMessage: errorMessage,
71 }).Pack(builder))
72 w.WriteHeader(statusCode)
73 w.Write(builder.FinishedBytes())
74}
75
76func respondNotImplemented(w http.ResponseWriter) {
77 respondWithError(w, http.StatusNotImplemented, "")
78}
79
80// TODO(phil): Can we turn this into a generic?
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080081func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*SubmitDataScouting, bool) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080082 success := true
83 defer func() {
84 if r := recover(); r != nil {
85 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
86 success = false
87 }
88 }()
89 result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
90 return result, success
91}
92
93// Handles a SubmitDataScouting request.
Philipp Schrader8747f1b2022-02-23 23:56:22 -080094type submitDataScoutingHandler struct {
95 db Database
96}
97
98func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080099 requestBytes, err := io.ReadAll(req.Body)
100 if err != nil {
101 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
102 return
103 }
104
Philipp Schrader30005e42022-03-06 13:53:58 -0800105 request, success := parseSubmitDataScouting(w, requestBytes)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800106 if !success {
107 return
108 }
109
Philipp Schrader30005e42022-03-06 13:53:58 -0800110 stats := db.Stats{
111 TeamNumber: request.Team(),
112 MatchNumber: request.Match(),
113 ShotsMissedAuto: request.MissedShotsAuto(),
114 UpperGoalAuto: request.UpperGoalAuto(),
115 LowerGoalAuto: request.LowerGoalAuto(),
116 ShotsMissed: request.MissedShotsTele(),
117 UpperGoalShots: request.UpperGoalTele(),
118 LowerGoalShots: request.LowerGoalTele(),
119 PlayedDefense: request.DefenseRating(),
120 Climbing: request.Climbing(),
121 }
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800122
Philipp Schrader30005e42022-03-06 13:53:58 -0800123 err = handler.db.AddToStats(stats)
124 if err != nil {
125 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
126 }
127
128 builder := flatbuffers.NewBuilder(50 * 1024)
129 builder.Finish((&SubmitDataScoutingResponseT{}).Pack(builder))
130 w.Write(builder.FinishedBytes())
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800131}
132
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800133// TODO(phil): Can we turn this into a generic?
134func parseRequestAllMatches(w http.ResponseWriter, buf []byte) (*RequestAllMatches, bool) {
135 success := true
136 defer func() {
137 if r := recover(); r != nil {
138 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
139 success = false
140 }
141 }()
142 result := request_all_matches.GetRootAsRequestAllMatches(buf, 0)
143 return result, success
144}
145
146// Handles a RequestAllMaches request.
147type requestAllMatchesHandler struct {
148 db Database
149}
150
151func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
152 requestBytes, err := io.ReadAll(req.Body)
153 if err != nil {
154 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
155 return
156 }
157
158 _, success := parseRequestAllMatches(w, requestBytes)
159 if !success {
160 return
161 }
162
163 matches, err := handler.db.ReturnMatches()
164 if err != nil {
165 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800166 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800167 }
168
169 var response RequestAllMatchesResponseT
170 for _, match := range matches {
171 response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
172 MatchNumber: match.MatchNumber,
173 Round: match.Round,
174 CompLevel: match.CompLevel,
175 R1: match.R1,
176 R2: match.R2,
177 R3: match.R3,
178 B1: match.B1,
179 B2: match.B2,
180 B3: match.B3,
181 })
182 }
183
184 builder := flatbuffers.NewBuilder(50 * 1024)
185 builder.Finish((&response).Pack(builder))
186 w.Write(builder.FinishedBytes())
187}
188
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800189// TODO(phil): Can we turn this into a generic?
190func parseRequestMatchesForTeam(w http.ResponseWriter, buf []byte) (*RequestMatchesForTeam, bool) {
191 success := true
192 defer func() {
193 if r := recover(); r != nil {
194 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
195 success = false
196 }
197 }()
198 result := request_matches_for_team.GetRootAsRequestMatchesForTeam(buf, 0)
199 return result, success
200}
201
202// Handles a RequestMatchesForTeam request.
203type requestMatchesForTeamHandler struct {
204 db Database
205}
206
207func (handler requestMatchesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
208 requestBytes, err := io.ReadAll(req.Body)
209 if err != nil {
210 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
211 return
212 }
213
214 request, success := parseRequestMatchesForTeam(w, requestBytes)
215 if !success {
216 return
217 }
218
219 matches, err := handler.db.QueryMatches(request.Team())
220 if err != nil {
221 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800222 return
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800223 }
224
225 var response RequestAllMatchesResponseT
226 for _, match := range matches {
227 response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
228 MatchNumber: match.MatchNumber,
229 Round: match.Round,
230 CompLevel: match.CompLevel,
231 R1: match.R1,
232 R2: match.R2,
233 R3: match.R3,
234 B1: match.B1,
235 B2: match.B2,
236 B3: match.B3,
237 })
238 }
239
240 builder := flatbuffers.NewBuilder(50 * 1024)
241 builder.Finish((&response).Pack(builder))
242 w.Write(builder.FinishedBytes())
243}
244
Philipp Schraderacf96232022-03-01 22:03:30 -0800245// TODO(phil): Can we turn this into a generic?
246func parseRequestDataScouting(w http.ResponseWriter, buf []byte) (*RequestDataScouting, bool) {
247 success := true
248 defer func() {
249 if r := recover(); r != nil {
250 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
251 success = false
252 }
253 }()
254 result := request_data_scouting.GetRootAsRequestDataScouting(buf, 0)
255 return result, success
256}
257
258// Handles a RequestDataScouting request.
259type requestDataScoutingHandler struct {
260 db Database
261}
262
263func (handler requestDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
264 requestBytes, err := io.ReadAll(req.Body)
265 if err != nil {
266 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
267 return
268 }
269
270 _, success := parseRequestDataScouting(w, requestBytes)
271 if !success {
272 return
273 }
274
275 stats, err := handler.db.ReturnStats()
276 if err != nil {
277 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800278 return
Philipp Schraderacf96232022-03-01 22:03:30 -0800279 }
280
281 var response RequestDataScoutingResponseT
282 for _, stat := range stats {
283 response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
284 Team: stat.TeamNumber,
285 Match: stat.MatchNumber,
286 MissedShotsAuto: stat.ShotsMissedAuto,
287 UpperGoalAuto: stat.UpperGoalAuto,
288 LowerGoalAuto: stat.LowerGoalAuto,
289 MissedShotsTele: stat.ShotsMissed,
290 UpperGoalTele: stat.UpperGoalShots,
291 LowerGoalTele: stat.LowerGoalShots,
292 DefenseRating: stat.PlayedDefense,
293 Climbing: stat.Climbing,
294 })
295 }
296
297 builder := flatbuffers.NewBuilder(50 * 1024)
298 builder.Finish((&response).Pack(builder))
299 w.Write(builder.FinishedBytes())
300}
301
Philipp Schraderd3fac192022-03-02 20:35:46 -0800302// TODO(phil): Can we turn this into a generic?
303func parseRefreshMatchList(w http.ResponseWriter, buf []byte) (*RefreshMatchList, bool) {
304 success := true
305 defer func() {
306 if r := recover(); r != nil {
307 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
308 success = false
309 }
310 }()
311 result := refresh_match_list.GetRootAsRefreshMatchList(buf, 0)
312 return result, success
313}
314
315func parseTeamKey(teamKey string) (int, error) {
316 // TBA prefixes teams with "frc". Not sure why. Get rid of that.
317 teamKey = strings.TrimPrefix(teamKey, "frc")
318 return strconv.Atoi(teamKey)
319}
320
321// Parses the alliance data from the specified match and returns the three red
322// teams and the three blue teams.
323func parseTeamKeys(match *scraping.Match) ([3]int32, [3]int32, error) {
324 redKeys := match.Alliances.Red.TeamKeys
325 blueKeys := match.Alliances.Blue.TeamKeys
326
327 if len(redKeys) != 3 || len(blueKeys) != 3 {
328 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
329 "Found %d red teams and %d blue teams.", len(redKeys), len(blueKeys)))
330 }
331
332 var red [3]int32
333 for i, key := range redKeys {
334 team, err := parseTeamKey(key)
335 if err != nil {
336 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
337 "Failed to parse red %d team '%s' as integer: %v", i+1, key, err))
338 }
339 red[i] = int32(team)
340 }
341 var blue [3]int32
342 for i, key := range blueKeys {
343 team, err := parseTeamKey(key)
344 if err != nil {
345 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
346 "Failed to parse blue %d team '%s' as integer: %v", i+1, key, err))
347 }
348 blue[i] = int32(team)
349 }
350 return red, blue, nil
351}
352
353type refreshMatchListHandler struct {
354 db Database
355 scrape ScrapeMatchList
356}
357
358func (handler refreshMatchListHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
359 requestBytes, err := io.ReadAll(req.Body)
360 if err != nil {
361 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
362 return
363 }
364
365 request, success := parseRefreshMatchList(w, requestBytes)
366 if !success {
367 return
368 }
369
370 matches, err := handler.scrape(request.Year(), string(request.EventCode()))
371 if err != nil {
372 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to scrape match list: ", err))
373 return
374 }
375
376 for _, match := range matches {
377 // Make sure the data is valid.
378 red, blue, err := parseTeamKeys(&match)
379 if err != nil {
380 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
381 "TheBlueAlliance data for match %d is malformed: %v", match.MatchNumber, err))
382 return
383 }
384 // Add the match to the database.
385 handler.db.AddToMatch(db.Match{
386 MatchNumber: int32(match.MatchNumber),
387 // TODO(phil): What does Round mean?
388 Round: 1,
389 CompLevel: match.CompLevel,
390 R1: red[0],
391 R2: red[1],
392 R3: red[2],
393 B1: blue[0],
394 B2: blue[1],
395 B3: blue[2],
396 })
397 }
398
399 var response RefreshMatchListResponseT
400 builder := flatbuffers.NewBuilder(1024)
401 builder.Finish((&response).Pack(builder))
402 w.Write(builder.FinishedBytes())
403}
404
Alex Perry81f96ba2022-03-13 18:26:19 -0700405func parseSubmitNotes(w http.ResponseWriter, buf []byte) (*SubmitNotes, bool) {
406 success := true
407 defer func() {
408 if r := recover(); r != nil {
409 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
410 success = false
411 }
412 }()
413 result := submit_notes.GetRootAsSubmitNotes(buf, 0)
414 return result, success
415}
416
417type submitNoteScoutingHandler struct {
418 db Database
419}
420
421func (handler submitNoteScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
422 requestBytes, err := io.ReadAll(req.Body)
423 if err != nil {
424 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
425 return
426 }
427
428 request, success := parseSubmitNotes(w, requestBytes)
429 if !success {
430 return
431 }
432
433 err = handler.db.AddNotes(db.NotesData{
434 TeamNumber: request.Team(),
435 Notes: []string{string(request.Notes())},
436 })
437 if err != nil {
438 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
439 return
440 }
441
442 var response SubmitNotesResponseT
443 builder := flatbuffers.NewBuilder(10)
444 builder.Finish((&response).Pack(builder))
445 w.Write(builder.FinishedBytes())
446}
447
448func parseRequestNotesForTeam(w http.ResponseWriter, buf []byte) (*RequestNotesForTeam, bool) {
449 success := true
450 defer func() {
451 if r := recover(); r != nil {
452 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
453 success = false
454 }
455 }()
456 result := request_notes_for_team.GetRootAsRequestNotesForTeam(buf, 0)
457 return result, success
458}
459
460type requestNotesForTeamHandler struct {
461 db Database
462}
463
464func (handler requestNotesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
465 requestBytes, err := io.ReadAll(req.Body)
466 if err != nil {
467 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
468 return
469 }
470
471 request, success := parseRequestNotesForTeam(w, requestBytes)
472 if !success {
473 return
474 }
475
476 notesData, err := handler.db.QueryNotes(request.Team())
477 if err != nil {
478 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query notes: %v", err))
479 return
480 }
481
482 var response RequestNotesForTeamResponseT
483 for _, data := range notesData.Notes {
484 response.Notes = append(response.Notes, &request_notes_for_team_response.NoteT{data})
485 }
486
487 builder := flatbuffers.NewBuilder(1024)
488 builder.Finish((&response).Pack(builder))
489 w.Write(builder.FinishedBytes())
490}
491
Philipp Schraderd3fac192022-03-02 20:35:46 -0800492func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800493 scoutingServer.HandleFunc("/requests", unknown)
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800494 scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800495 scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
Philipp Schraderd1c4bef2022-02-28 22:51:30 -0800496 scoutingServer.Handle("/requests/request/matches_for_team", requestMatchesForTeamHandler{db})
Philipp Schraderacf96232022-03-01 22:03:30 -0800497 scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
Philipp Schraderd3fac192022-03-02 20:35:46 -0800498 scoutingServer.Handle("/requests/refresh_match_list", refreshMatchListHandler{db, scrape})
Alex Perry81f96ba2022-03-13 18:26:19 -0700499 scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
500 scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800501}