blob: 1fc0ae4f4627641c4839558a856fc4faa8dd58d7 [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"
Emily Markovabf24c9e2023-02-08 20:31:11 -080010 "sort"
Philipp Schraderd3fac192022-03-02 20:35:46 -080011 "strconv"
12 "strings"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080013
Philipp Schrader8747f1b2022-02-23 23:56:22 -080014 "github.com/frc971/971-Robot-Code/scouting/db"
Philipp Schraderd3fac192022-03-02 20:35:46 -080015 "github.com/frc971/971-Robot-Code/scouting/scraping"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080016 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
Philipp Schraderd3fac192022-03-02 20:35:46 -080017 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list"
18 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/refresh_match_list_response"
Filip Kujawaf882e022022-12-14 13:14:08 -080019 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings"
20 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_driver_rankings_response"
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080021 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
22 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
Filip Kujawaf882e022022-12-14 13:14:08 -080023 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes"
24 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_notes_response"
Philipp Schraderacf96232022-03-01 22:03:30 -080025 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting"
26 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_data_scouting_response"
Alex Perry81f96ba2022-03-13 18:26:19 -070027 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team"
28 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
Milo Lin1d59f0c2022-06-22 20:30:58 -070029 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule"
30 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_shift_schedule_response"
Sabina Leaver759090b2023-01-14 20:42:56 -080031 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions"
32 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_actions_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080033 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
Philipp Schrader30005e42022-03-06 13:53:58 -080034 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
Filip Kujawa210a03b2022-11-24 14:41:11 -080035 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking"
36 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_driver_ranking_response"
Alex Perry81f96ba2022-03-13 18:26:19 -070037 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes"
38 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
Milo Lin1d59f0c2022-06-22 20:30:58 -070039 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule"
40 "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_shift_schedule_response"
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080041 "github.com/frc971/971-Robot-Code/scouting/webserver/server"
42 flatbuffers "github.com/google/flatbuffers/go"
43)
44
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080045type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
Philipp Schrader30005e42022-03-06 13:53:58 -080046type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080047type RequestAllMatches = request_all_matches.RequestAllMatches
48type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
Filip Kujawaf882e022022-12-14 13:14:08 -080049type RequestAllDriverRankings = request_all_driver_rankings.RequestAllDriverRankings
50type RequestAllDriverRankingsResponseT = request_all_driver_rankings_response.RequestAllDriverRankingsResponseT
51type RequestAllNotes = request_all_notes.RequestAllNotes
52type RequestAllNotesResponseT = request_all_notes_response.RequestAllNotesResponseT
Philipp Schraderacf96232022-03-01 22:03:30 -080053type RequestDataScouting = request_data_scouting.RequestDataScouting
54type RequestDataScoutingResponseT = request_data_scouting_response.RequestDataScoutingResponseT
Philipp Schraderd3fac192022-03-02 20:35:46 -080055type RefreshMatchList = refresh_match_list.RefreshMatchList
56type RefreshMatchListResponseT = refresh_match_list_response.RefreshMatchListResponseT
Alex Perry81f96ba2022-03-13 18:26:19 -070057type SubmitNotes = submit_notes.SubmitNotes
58type SubmitNotesResponseT = submit_notes_response.SubmitNotesResponseT
59type RequestNotesForTeam = request_notes_for_team.RequestNotesForTeam
60type RequestNotesForTeamResponseT = request_notes_for_team_response.RequestNotesForTeamResponseT
Milo Lin1d59f0c2022-06-22 20:30:58 -070061type RequestShiftSchedule = request_shift_schedule.RequestShiftSchedule
62type RequestShiftScheduleResponseT = request_shift_schedule_response.RequestShiftScheduleResponseT
63type SubmitShiftSchedule = submit_shift_schedule.SubmitShiftSchedule
64type SubmitShiftScheduleResponseT = submit_shift_schedule_response.SubmitShiftScheduleResponseT
Filip Kujawa210a03b2022-11-24 14:41:11 -080065type SubmitDriverRanking = submit_driver_ranking.SubmitDriverRanking
66type SubmitDriverRankingResponseT = submit_driver_ranking_response.SubmitDriverRankingResponseT
Sabina Leaver759090b2023-01-14 20:42:56 -080067type SubmitActions = submit_actions.SubmitActions
68type SubmitActionsResponseT = submit_actions_response.SubmitActionsResponseT
Philipp Schradercbf5c6a2022-02-27 23:25:19 -080069
Philipp Schrader8747f1b2022-02-23 23:56:22 -080070// The interface we expect the database abstraction to conform to.
71// We use an interface here because it makes unit testing easier.
72type Database interface {
Emily Markovabf24c9e2023-02-08 20:31:11 -080073 AddToMatch(db.TeamMatch) error
Milo Lin1d59f0c2022-06-22 20:30:58 -070074 AddToShift(db.Shift) error
Philipp Schrader8747f1b2022-02-23 23:56:22 -080075 AddToStats(db.Stats) error
Emily Markovabf24c9e2023-02-08 20:31:11 -080076 ReturnMatches() ([]db.TeamMatch, error)
Filip Kujawaf882e022022-12-14 13:14:08 -080077 ReturnAllNotes() ([]db.NotesData, error)
78 ReturnAllDriverRankings() ([]db.DriverRankingData, error)
Milo Lin1d59f0c2022-06-22 20:30:58 -070079 ReturnAllShifts() ([]db.Shift, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080080 ReturnStats() ([]db.Stats, error)
Milo Lin1d59f0c2022-06-22 20:30:58 -070081 QueryAllShifts(int) ([]db.Shift, error)
Philipp Schrader8747f1b2022-02-23 23:56:22 -080082 QueryStats(int) ([]db.Stats, error)
Philipp Schradereecb8962022-06-01 21:02:42 -070083 QueryNotes(int32) ([]string, error)
Filip Kujawaf947cb42022-11-21 10:00:30 -080084 AddNotes(db.NotesData) error
Filip Kujawa210a03b2022-11-24 14:41:11 -080085 AddDriverRanking(db.DriverRankingData) error
Philipp Schrader8747f1b2022-02-23 23:56:22 -080086}
87
Philipp Schraderd3fac192022-03-02 20:35:46 -080088type ScrapeMatchList func(int32, string) ([]scraping.Match, error)
89
Philipp Schradercdb5cfc2022-02-20 14:57:07 -080090// Handles unknown requests. Just returns a 404.
91func unknown(w http.ResponseWriter, req *http.Request) {
92 w.WriteHeader(http.StatusNotFound)
93}
94
95func respondWithError(w http.ResponseWriter, statusCode int, errorMessage string) {
96 builder := flatbuffers.NewBuilder(1024)
97 builder.Finish((&error_response.ErrorResponseT{
98 ErrorMessage: errorMessage,
99 }).Pack(builder))
100 w.WriteHeader(statusCode)
101 w.Write(builder.FinishedBytes())
102}
103
104func respondNotImplemented(w http.ResponseWriter) {
105 respondWithError(w, http.StatusNotImplemented, "")
106}
107
Philipp Schraderb7e75932022-03-26 16:18:34 -0700108func parseRequest[T interface{}](w http.ResponseWriter, buf []byte, requestName string, parser func([]byte, flatbuffers.UOffsetT) *T) (*T, bool) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800109 success := true
110 defer func() {
111 if r := recover(); r != nil {
Philipp Schraderb7e75932022-03-26 16:18:34 -0700112 respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse %s: %v", requestName, r))
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800113 success = false
114 }
115 }()
Philipp Schraderb7e75932022-03-26 16:18:34 -0700116 result := parser(buf, 0)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800117 return result, success
118}
119
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700120// Parses the authorization information that the browser inserts into the
121// headers. The authorization follows this format:
122//
123// req.Headers["Authorization"] = []string{"Basic <base64 encoded username:password>"}
124func parseUsername(req *http.Request) string {
125 auth, ok := req.Header["Authorization"]
126 if !ok {
127 return "unknown"
128 }
129
130 parts := strings.Split(auth[0], " ")
131 if !(len(parts) == 2 && parts[0] == "Basic") {
132 return "unknown"
133 }
134
135 info, err := base64.StdEncoding.DecodeString(parts[1])
136 if err != nil {
137 log.Println("ERROR: Failed to parse Basic authentication.")
138 return "unknown"
139 }
140
141 loginParts := strings.Split(string(info), ":")
142 if len(loginParts) != 2 {
143 return "unknown"
144 }
145 return loginParts[0]
146}
147
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800148// Handles a SubmitDataScouting request.
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800149type submitDataScoutingHandler struct {
150 db Database
151}
152
153func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700154 // Get the username of the person submitting the data.
155 username := parseUsername(req)
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700156
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800157 requestBytes, err := io.ReadAll(req.Body)
158 if err != nil {
159 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
160 return
161 }
162
Philipp Schraderb7e75932022-03-26 16:18:34 -0700163 request, success := parseRequest[SubmitDataScouting](w, requestBytes, "SubmitDataScouting", submit_data_scouting.GetRootAsSubmitDataScouting)
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800164 if !success {
165 return
166 }
167
Philipp Schraderd7b6eba2022-03-18 22:21:25 -0700168 log.Println("Got data scouting data for match", request.Match(), "team", request.Team(), "from", username)
169
Philipp Schrader30005e42022-03-06 13:53:58 -0800170 stats := db.Stats{
Philipp Schraderfee07e12022-03-17 22:19:47 -0700171 TeamNumber: request.Team(),
172 MatchNumber: request.Match(),
Philipp Schrader30b4a682022-04-16 14:36:17 -0700173 SetNumber: request.SetNumber(),
Philipp Schrader4535b7e2022-04-08 20:27:00 -0700174 CompLevel: string(request.CompLevel()),
Philipp Schraderfee07e12022-03-17 22:19:47 -0700175 StartingQuadrant: request.StartingQuadrant(),
176 AutoBallPickedUp: [5]bool{
177 request.AutoBall1(), request.AutoBall2(), request.AutoBall3(),
178 request.AutoBall4(), request.AutoBall5(),
179 },
Philipp Schraderfa45d742022-03-18 19:29:05 -0700180 ShotsMissedAuto: request.MissedShotsAuto(),
181 UpperGoalAuto: request.UpperGoalAuto(),
182 LowerGoalAuto: request.LowerGoalAuto(),
183 ShotsMissed: request.MissedShotsTele(),
184 UpperGoalShots: request.UpperGoalTele(),
185 LowerGoalShots: request.LowerGoalTele(),
186 PlayedDefense: request.DefenseRating(),
187 DefenseReceivedScore: request.DefenseReceivedRating(),
188 Climbing: int32(request.ClimbLevel()),
189 CollectedBy: username,
190 Comment: string(request.Comment()),
Philipp Schrader30005e42022-03-06 13:53:58 -0800191 }
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800192
Philipp Schraderfee07e12022-03-17 22:19:47 -0700193 // Do some error checking.
194 if stats.StartingQuadrant < 1 || stats.StartingQuadrant > 4 {
195 respondWithError(w, http.StatusBadRequest, fmt.Sprint(
196 "Invalid starting_quadrant field value of ", stats.StartingQuadrant))
197 return
198 }
199
Philipp Schrader30005e42022-03-06 13:53:58 -0800200 err = handler.db.AddToStats(stats)
201 if err != nil {
202 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit datascouting data: ", err))
Philipp Schraderfee07e12022-03-17 22:19:47 -0700203 return
Philipp Schrader30005e42022-03-06 13:53:58 -0800204 }
205
206 builder := flatbuffers.NewBuilder(50 * 1024)
207 builder.Finish((&SubmitDataScoutingResponseT{}).Pack(builder))
208 w.Write(builder.FinishedBytes())
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800209}
210
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800211// Handles a RequestAllMaches request.
212type requestAllMatchesHandler struct {
213 db Database
214}
215
Emily Markovabf24c9e2023-02-08 20:31:11 -0800216func findIndexInList(list []string, comp_level string) (int, error) {
217 for index, value := range list {
218 if value == comp_level {
219 return index, nil
220 }
221 }
222 return -1, errors.New(fmt.Sprint("Failed to find comp level ", comp_level, " in list ", list))
223}
224
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800225func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
226 requestBytes, err := io.ReadAll(req.Body)
227 if err != nil {
228 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
229 return
230 }
231
Philipp Schraderb7e75932022-03-26 16:18:34 -0700232 _, success := parseRequest(w, requestBytes, "RequestAllMatches", request_all_matches.GetRootAsRequestAllMatches)
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800233 if !success {
234 return
235 }
236
237 matches, err := handler.db.ReturnMatches()
238 if err != nil {
Philipp Schraderfae8a7e2022-03-13 22:51:54 -0700239 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800240 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800241 }
242
Emily Markovabf24c9e2023-02-08 20:31:11 -0800243 // Change structure of match objects in the database(1 per team) to
244 // the old match structure(1 per match) that the webserver uses.
245 type Key struct {
246 MatchNumber int32
247 SetNumber int32
248 CompLevel string
249 }
250
251 assembledMatches := map[Key]request_all_matches_response.MatchT{}
252
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800253 for _, match := range matches {
Emily Markovabf24c9e2023-02-08 20:31:11 -0800254 key := Key{match.MatchNumber, match.SetNumber, match.CompLevel}
255 log.Println("Key : ", key)
256 entry, ok := assembledMatches[key]
257 if !ok {
258 entry = request_all_matches_response.MatchT{
259 MatchNumber: match.MatchNumber,
260 SetNumber: match.SetNumber,
261 CompLevel: match.CompLevel,
262 }
263 }
264 log.Println("Entry : ", entry)
265 switch match.Alliance {
266 case "R":
267 switch match.AlliancePosition {
268 case 1:
269 entry.R1 = match.TeamNumber
270 case 2:
271 entry.R2 = match.TeamNumber
272 case 3:
273 entry.R3 = match.TeamNumber
274 default:
275 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown red position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
276 return
277 }
278 case "B":
279 switch match.AlliancePosition {
280 case 1:
281 entry.B1 = match.TeamNumber
282 case 2:
283 entry.B2 = match.TeamNumber
284 case 3:
285 entry.B3 = match.TeamNumber
286 default:
287 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown blue position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
288 return
289 }
290 default:
291 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown alliance ", match.Alliance, " in match ", strconv.Itoa(int(match.AlliancePosition))))
292 return
293 }
294 assembledMatches[key] = entry
295 }
296
297 var response RequestAllMatchesResponseT
298 for _, match := range assembledMatches {
299 copied_match := match
300 response.MatchList = append(response.MatchList, &copied_match)
301 }
302
303 var MATCH_TYPE_ORDERING = []string{"qm", "ef", "qf", "sf", "f"}
304
305 err = nil
306 sort.Slice(response.MatchList, func(i, j int) bool {
307 if err != nil {
308 return false
309 }
310 a := response.MatchList[i]
311 b := response.MatchList[j]
312
313 aMatchTypeIndex, err := findIndexInList(MATCH_TYPE_ORDERING, a.CompLevel)
314 if err != nil {
315 err = errors.New(fmt.Sprint("Comp level ", a.CompLevel, " not found in sorting list ", MATCH_TYPE_ORDERING, " : ", err))
316 return false
317 }
318 bMatchTypeIndex, err := findIndexInList(MATCH_TYPE_ORDERING, b.CompLevel)
319 if err != nil {
320 err = errors.New(fmt.Sprint("Comp level ", b.CompLevel, " not found in sorting list ", MATCH_TYPE_ORDERING, " : ", err))
321 return false
322 }
323
324 if aMatchTypeIndex < bMatchTypeIndex {
325 return true
326 }
327 if aMatchTypeIndex > bMatchTypeIndex {
328 return false
329 }
330
331 // Then sort by match number. E.g. in semi finals, all match 1 rounds
332 // are done first. Then come match 2 rounds. And then, if necessary,
333 // the match 3 rounds.
334 aMatchNumber := a.MatchNumber
335 bMatchNumber := b.MatchNumber
336 if aMatchNumber < bMatchNumber {
337 return true
338 }
339 if aMatchNumber > bMatchNumber {
340 return false
341 }
342 // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
343 // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
344 // Semi Final 2 Match 2.
345 aSetNumber := a.SetNumber
346 bSetNumber := b.SetNumber
347 if aSetNumber < bSetNumber {
348 return true
349 }
350 if aSetNumber > bSetNumber {
351 return false
352 }
353 return true
354 })
355
356 if err != nil {
357 // check if error happened during sorting and notify webpage if that
358 respondWithError(w, http.StatusInternalServerError, fmt.Sprint(err))
359 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800360 }
361
362 builder := flatbuffers.NewBuilder(50 * 1024)
363 builder.Finish((&response).Pack(builder))
364 w.Write(builder.FinishedBytes())
365}
366
Philipp Schraderacf96232022-03-01 22:03:30 -0800367// Handles a RequestDataScouting request.
368type requestDataScoutingHandler struct {
369 db Database
370}
371
372func (handler requestDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
373 requestBytes, err := io.ReadAll(req.Body)
374 if err != nil {
375 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
376 return
377 }
378
Philipp Schraderb7e75932022-03-26 16:18:34 -0700379 _, success := parseRequest(w, requestBytes, "RequestDataScouting", request_data_scouting.GetRootAsRequestDataScouting)
Philipp Schraderacf96232022-03-01 22:03:30 -0800380 if !success {
381 return
382 }
383
384 stats, err := handler.db.ReturnStats()
385 if err != nil {
386 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800387 return
Philipp Schraderacf96232022-03-01 22:03:30 -0800388 }
389
390 var response RequestDataScoutingResponseT
391 for _, stat := range stats {
392 response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
Philipp Schraderfa45d742022-03-18 19:29:05 -0700393 Team: stat.TeamNumber,
394 Match: stat.MatchNumber,
Philipp Schrader30b4a682022-04-16 14:36:17 -0700395 SetNumber: stat.SetNumber,
Philipp Schrader4535b7e2022-04-08 20:27:00 -0700396 CompLevel: stat.CompLevel,
Philipp Schraderfa45d742022-03-18 19:29:05 -0700397 StartingQuadrant: stat.StartingQuadrant,
398 AutoBall1: stat.AutoBallPickedUp[0],
399 AutoBall2: stat.AutoBallPickedUp[1],
400 AutoBall3: stat.AutoBallPickedUp[2],
401 AutoBall4: stat.AutoBallPickedUp[3],
402 AutoBall5: stat.AutoBallPickedUp[4],
403 MissedShotsAuto: stat.ShotsMissedAuto,
404 UpperGoalAuto: stat.UpperGoalAuto,
405 LowerGoalAuto: stat.LowerGoalAuto,
406 MissedShotsTele: stat.ShotsMissed,
407 UpperGoalTele: stat.UpperGoalShots,
408 LowerGoalTele: stat.LowerGoalShots,
409 DefenseRating: stat.PlayedDefense,
410 DefenseReceivedRating: stat.DefenseReceivedScore,
411 ClimbLevel: request_data_scouting_response.ClimbLevel(stat.Climbing),
412 CollectedBy: stat.CollectedBy,
413 Comment: stat.Comment,
Philipp Schraderacf96232022-03-01 22:03:30 -0800414 })
415 }
416
417 builder := flatbuffers.NewBuilder(50 * 1024)
418 builder.Finish((&response).Pack(builder))
419 w.Write(builder.FinishedBytes())
420}
421
Philipp Schraderd3fac192022-03-02 20:35:46 -0800422func parseTeamKey(teamKey string) (int, error) {
423 // TBA prefixes teams with "frc". Not sure why. Get rid of that.
424 teamKey = strings.TrimPrefix(teamKey, "frc")
Philipp Schrader84fe7782022-11-12 08:10:36 -0800425 magnitude := 0
426 if strings.HasSuffix(teamKey, "A") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800427 magnitude = 0
Philipp Schrader84fe7782022-11-12 08:10:36 -0800428 teamKey = strings.TrimSuffix(teamKey, "A")
429 } else if strings.HasSuffix(teamKey, "B") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800430 magnitude = 9
Philipp Schrader84fe7782022-11-12 08:10:36 -0800431 teamKey = strings.TrimSuffix(teamKey, "B")
432 } else if strings.HasSuffix(teamKey, "C") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800433 magnitude = 8
Philipp Schrader84fe7782022-11-12 08:10:36 -0800434 teamKey = strings.TrimSuffix(teamKey, "C")
435 } else if strings.HasSuffix(teamKey, "D") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800436 magnitude = 7
Philipp Schrader84fe7782022-11-12 08:10:36 -0800437 teamKey = strings.TrimSuffix(teamKey, "D")
438 } else if strings.HasSuffix(teamKey, "E") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800439 magnitude = 6
Philipp Schrader84fe7782022-11-12 08:10:36 -0800440 teamKey = strings.TrimSuffix(teamKey, "E")
441 } else if strings.HasSuffix(teamKey, "F") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800442 magnitude = 5
Philipp Schrader84fe7782022-11-12 08:10:36 -0800443 teamKey = strings.TrimSuffix(teamKey, "F")
444 }
Filip Kujawa4de6feb2022-11-12 08:41:49 -0800445
446 if magnitude != 0 {
447 teamKey = strconv.Itoa(magnitude) + teamKey
Philipp Schrader84fe7782022-11-12 08:10:36 -0800448 }
Filip Kujawa4de6feb2022-11-12 08:41:49 -0800449
450 result, err := strconv.Atoi(teamKey)
Philipp Schrader84fe7782022-11-12 08:10:36 -0800451 return result, err
Philipp Schraderd3fac192022-03-02 20:35:46 -0800452}
453
454// Parses the alliance data from the specified match and returns the three red
455// teams and the three blue teams.
456func parseTeamKeys(match *scraping.Match) ([3]int32, [3]int32, error) {
457 redKeys := match.Alliances.Red.TeamKeys
458 blueKeys := match.Alliances.Blue.TeamKeys
459
460 if len(redKeys) != 3 || len(blueKeys) != 3 {
461 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
462 "Found %d red teams and %d blue teams.", len(redKeys), len(blueKeys)))
463 }
464
465 var red [3]int32
466 for i, key := range redKeys {
467 team, err := parseTeamKey(key)
468 if err != nil {
469 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
470 "Failed to parse red %d team '%s' as integer: %v", i+1, key, err))
471 }
472 red[i] = int32(team)
473 }
474 var blue [3]int32
475 for i, key := range blueKeys {
476 team, err := parseTeamKey(key)
477 if err != nil {
478 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
479 "Failed to parse blue %d team '%s' as integer: %v", i+1, key, err))
480 }
481 blue[i] = int32(team)
482 }
483 return red, blue, nil
484}
485
486type refreshMatchListHandler struct {
487 db Database
488 scrape ScrapeMatchList
489}
490
491func (handler refreshMatchListHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
492 requestBytes, err := io.ReadAll(req.Body)
493 if err != nil {
494 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
495 return
496 }
497
Philipp Schraderb7e75932022-03-26 16:18:34 -0700498 request, success := parseRequest(w, requestBytes, "RefreshMatchList", refresh_match_list.GetRootAsRefreshMatchList)
Philipp Schraderd3fac192022-03-02 20:35:46 -0800499 if !success {
500 return
501 }
502
503 matches, err := handler.scrape(request.Year(), string(request.EventCode()))
504 if err != nil {
505 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to scrape match list: ", err))
506 return
507 }
508
509 for _, match := range matches {
510 // Make sure the data is valid.
511 red, blue, err := parseTeamKeys(&match)
512 if err != nil {
513 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
514 "TheBlueAlliance data for match %d is malformed: %v", match.MatchNumber, err))
515 return
516 }
Emily Markovabf24c9e2023-02-08 20:31:11 -0800517
518 team_matches := []db.TeamMatch{
519 {
520 MatchNumber: int32(match.MatchNumber),
521 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
522 Alliance: "R", AlliancePosition: 1, TeamNumber: red[0],
523 },
524 {
525 MatchNumber: int32(match.MatchNumber),
526 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
527 Alliance: "R", AlliancePosition: 2, TeamNumber: red[1],
528 },
529 {
530 MatchNumber: int32(match.MatchNumber),
531 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
532 Alliance: "R", AlliancePosition: 3, TeamNumber: red[2],
533 },
534 {
535 MatchNumber: int32(match.MatchNumber),
536 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
537 Alliance: "B", AlliancePosition: 1, TeamNumber: blue[0],
538 },
539 {
540 MatchNumber: int32(match.MatchNumber),
541 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
542 Alliance: "B", AlliancePosition: 2, TeamNumber: blue[1],
543 },
544 {
545 MatchNumber: int32(match.MatchNumber),
546 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
547 Alliance: "B", AlliancePosition: 3, TeamNumber: blue[2],
548 },
549 }
550
551 for _, match := range team_matches {
552 // Iterate through matches to check they can be added to database.
553 err = handler.db.AddToMatch(match)
554 if err != nil {
555 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
556 "Failed to add team %d from match %d to the database: %v", match.TeamNumber, match.MatchNumber, err))
557 return
558 }
Philipp Schrader7365d322022-03-06 16:40:08 -0800559 }
Philipp Schraderd3fac192022-03-02 20:35:46 -0800560 }
561
562 var response RefreshMatchListResponseT
563 builder := flatbuffers.NewBuilder(1024)
564 builder.Finish((&response).Pack(builder))
565 w.Write(builder.FinishedBytes())
566}
567
Alex Perry81f96ba2022-03-13 18:26:19 -0700568type submitNoteScoutingHandler struct {
569 db Database
570}
571
572func (handler submitNoteScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
573 requestBytes, err := io.ReadAll(req.Body)
574 if err != nil {
575 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
576 return
577 }
578
Philipp Schraderb7e75932022-03-26 16:18:34 -0700579 request, success := parseRequest(w, requestBytes, "SubmitNotes", submit_notes.GetRootAsSubmitNotes)
Alex Perry81f96ba2022-03-13 18:26:19 -0700580 if !success {
581 return
582 }
583
Filip Kujawaf947cb42022-11-21 10:00:30 -0800584 err = handler.db.AddNotes(db.NotesData{
585 TeamNumber: request.Team(),
586 Notes: string(request.Notes()),
587 GoodDriving: bool(request.GoodDriving()),
588 BadDriving: bool(request.BadDriving()),
589 SketchyClimb: bool(request.SketchyClimb()),
590 SolidClimb: bool(request.SolidClimb()),
591 GoodDefense: bool(request.GoodDefense()),
592 BadDefense: bool(request.BadDefense()),
593 })
Alex Perry81f96ba2022-03-13 18:26:19 -0700594 if err != nil {
595 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
596 return
597 }
598
599 var response SubmitNotesResponseT
600 builder := flatbuffers.NewBuilder(10)
601 builder.Finish((&response).Pack(builder))
602 w.Write(builder.FinishedBytes())
603}
604
Alex Perry81f96ba2022-03-13 18:26:19 -0700605type requestNotesForTeamHandler struct {
606 db Database
607}
608
609func (handler requestNotesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
610 requestBytes, err := io.ReadAll(req.Body)
611 if err != nil {
612 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
613 return
614 }
615
Philipp Schraderb7e75932022-03-26 16:18:34 -0700616 request, success := parseRequest(w, requestBytes, "RequestNotesForTeam", request_notes_for_team.GetRootAsRequestNotesForTeam)
Alex Perry81f96ba2022-03-13 18:26:19 -0700617 if !success {
618 return
619 }
620
Philipp Schradereecb8962022-06-01 21:02:42 -0700621 notes, err := handler.db.QueryNotes(request.Team())
Alex Perry81f96ba2022-03-13 18:26:19 -0700622 if err != nil {
623 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query notes: %v", err))
624 return
625 }
626
627 var response RequestNotesForTeamResponseT
Philipp Schradereecb8962022-06-01 21:02:42 -0700628 for _, data := range notes {
Alex Perry81f96ba2022-03-13 18:26:19 -0700629 response.Notes = append(response.Notes, &request_notes_for_team_response.NoteT{data})
630 }
631
632 builder := flatbuffers.NewBuilder(1024)
633 builder.Finish((&response).Pack(builder))
634 w.Write(builder.FinishedBytes())
635}
636
Milo Lin1d59f0c2022-06-22 20:30:58 -0700637type requestShiftScheduleHandler struct {
638 db Database
639}
640
641func (handler requestShiftScheduleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
642 requestBytes, err := io.ReadAll(req.Body)
643 if err != nil {
644 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
645 return
646 }
647
648 _, success := parseRequest(w, requestBytes, "RequestShiftSchedule", request_shift_schedule.GetRootAsRequestShiftSchedule)
649 if !success {
650 return
651 }
652
653 shiftData, err := handler.db.ReturnAllShifts()
654 if err != nil {
655 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query shift schedule: %v", err))
656 return
657 }
658
659 var response RequestShiftScheduleResponseT
660 for _, shifts := range shiftData {
661 response.ShiftSchedule = append(response.ShiftSchedule, &request_shift_schedule_response.MatchAssignmentT{
662 MatchNumber: shifts.MatchNumber,
663 R1scouter: shifts.R1scouter,
664 R2scouter: shifts.R2scouter,
665 R3scouter: shifts.R3scouter,
666 B1scouter: shifts.B1scouter,
667 B2scouter: shifts.B2scouter,
668 B3scouter: shifts.B3scouter,
669 })
670 }
671
672 builder := flatbuffers.NewBuilder(1024)
673 builder.Finish((&response).Pack(builder))
674 w.Write(builder.FinishedBytes())
675}
676
677type submitShiftScheduleHandler struct {
678 db Database
679}
680
681func (handler submitShiftScheduleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
682 // Get the username of the person submitting the data.
683 username := parseUsername(req)
684
685 requestBytes, err := io.ReadAll(req.Body)
686 if err != nil {
687 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
688 return
689 }
690
691 request, success := parseRequest[SubmitShiftSchedule](w, requestBytes, "SubmitShiftSchedule", submit_shift_schedule.GetRootAsSubmitShiftSchedule)
692 if !success {
693 return
694 }
695
696 log.Println("Got shift schedule from", username)
697 shift_schedule_length := request.ShiftScheduleLength()
698 for i := 0; i < shift_schedule_length; i++ {
699 var match_assignment submit_shift_schedule.MatchAssignment
700 request.ShiftSchedule(&match_assignment, i)
701 current_shift := db.Shift{
702 MatchNumber: match_assignment.MatchNumber(),
703 R1scouter: string(match_assignment.R1scouter()),
704 R2scouter: string(match_assignment.R2scouter()),
705 R3scouter: string(match_assignment.R3scouter()),
706 B1scouter: string(match_assignment.B1scouter()),
707 B2scouter: string(match_assignment.B2scouter()),
708 B3scouter: string(match_assignment.B3scouter()),
709 }
710 err = handler.db.AddToShift(current_shift)
711 if err != nil {
712 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit shift schedule: ", err))
713 return
714 }
715 }
716
717 builder := flatbuffers.NewBuilder(50 * 1024)
718 builder.Finish((&SubmitShiftScheduleResponseT{}).Pack(builder))
719 w.Write(builder.FinishedBytes())
720}
721
Filip Kujawa210a03b2022-11-24 14:41:11 -0800722type SubmitDriverRankingHandler struct {
723 db Database
724}
725
726func (handler SubmitDriverRankingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
727 requestBytes, err := io.ReadAll(req.Body)
728 if err != nil {
729 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
730 return
731 }
732
733 request, success := parseRequest(w, requestBytes, "SubmitDriverRanking", submit_driver_ranking.GetRootAsSubmitDriverRanking)
734 if !success {
735 return
736 }
737
738 err = handler.db.AddDriverRanking(db.DriverRankingData{
739 MatchNumber: request.MatchNumber(),
740 Rank1: request.Rank1(),
741 Rank2: request.Rank2(),
742 Rank3: request.Rank3(),
743 })
744
745 if err != nil {
746 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert driver ranking: %v", err))
747 return
748 }
749
750 var response SubmitDriverRankingResponseT
751 builder := flatbuffers.NewBuilder(10)
752 builder.Finish((&response).Pack(builder))
753 w.Write(builder.FinishedBytes())
754}
755
Filip Kujawaf882e022022-12-14 13:14:08 -0800756type requestAllNotesHandler struct {
757 db Database
758}
759
760func (handler requestAllNotesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
761 requestBytes, err := io.ReadAll(req.Body)
762 if err != nil {
763 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
764 return
765 }
766
767 _, success := parseRequest(w, requestBytes, "RequestAllNotes", request_all_notes.GetRootAsRequestAllNotes)
768 if !success {
769 return
770 }
771
772 notes, err := handler.db.ReturnAllNotes()
773 if err != nil {
774 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
775 return
776 }
777
778 var response RequestAllNotesResponseT
779 for _, note := range notes {
780 response.NoteList = append(response.NoteList, &request_all_notes_response.NoteT{
781 Team: note.TeamNumber,
782 Notes: note.Notes,
783 GoodDriving: note.GoodDriving,
784 BadDriving: note.BadDriving,
785 SketchyClimb: note.SketchyClimb,
786 SolidClimb: note.SolidClimb,
787 GoodDefense: note.GoodDefense,
788 BadDefense: note.BadDefense,
789 })
790 }
791
792 builder := flatbuffers.NewBuilder(50 * 1024)
793 builder.Finish((&response).Pack(builder))
794 w.Write(builder.FinishedBytes())
795}
796
797type requestAllDriverRankingsHandler struct {
798 db Database
799}
800
801func (handler requestAllDriverRankingsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
802 requestBytes, err := io.ReadAll(req.Body)
803 if err != nil {
804 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
805 return
806 }
807
808 _, success := parseRequest(w, requestBytes, "RequestAllDriverRankings", request_all_driver_rankings.GetRootAsRequestAllDriverRankings)
809 if !success {
810 return
811 }
812
813 rankings, err := handler.db.ReturnAllDriverRankings()
814 if err != nil {
815 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
816 return
817 }
818
819 var response RequestAllDriverRankingsResponseT
820 for _, ranking := range rankings {
821 response.DriverRankingList = append(response.DriverRankingList, &request_all_driver_rankings_response.RankingT{
822 MatchNumber: ranking.MatchNumber,
823 Rank1: ranking.Rank1,
824 Rank2: ranking.Rank2,
825 Rank3: ranking.Rank3,
826 })
827 }
828
829 builder := flatbuffers.NewBuilder(50 * 1024)
830 builder.Finish((&response).Pack(builder))
831 w.Write(builder.FinishedBytes())
832}
833
Philipp Schraderd3fac192022-03-02 20:35:46 -0800834func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800835 scoutingServer.HandleFunc("/requests", unknown)
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800836 scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800837 scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
Filip Kujawaf882e022022-12-14 13:14:08 -0800838 scoutingServer.Handle("/requests/request/all_notes", requestAllNotesHandler{db})
839 scoutingServer.Handle("/requests/request/all_driver_rankings", requestAllDriverRankingsHandler{db})
Philipp Schraderacf96232022-03-01 22:03:30 -0800840 scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
Philipp Schraderd3fac192022-03-02 20:35:46 -0800841 scoutingServer.Handle("/requests/refresh_match_list", refreshMatchListHandler{db, scrape})
Alex Perry81f96ba2022-03-13 18:26:19 -0700842 scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
843 scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
Milo Lin1d59f0c2022-06-22 20:30:58 -0700844 scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
845 scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
Filip Kujawa210a03b2022-11-24 14:41:11 -0800846 scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800847}