blob: d56f380d177544da96c6b54235edc8b92dddf73d [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}
Emily Markovabf24c9e2023-02-08 20:31:11 -0800255 entry, ok := assembledMatches[key]
256 if !ok {
257 entry = request_all_matches_response.MatchT{
258 MatchNumber: match.MatchNumber,
259 SetNumber: match.SetNumber,
260 CompLevel: match.CompLevel,
261 }
262 }
Emily Markovabf24c9e2023-02-08 20:31:11 -0800263 switch match.Alliance {
264 case "R":
265 switch match.AlliancePosition {
266 case 1:
267 entry.R1 = match.TeamNumber
268 case 2:
269 entry.R2 = match.TeamNumber
270 case 3:
271 entry.R3 = match.TeamNumber
272 default:
273 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown red position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
274 return
275 }
276 case "B":
277 switch match.AlliancePosition {
278 case 1:
279 entry.B1 = match.TeamNumber
280 case 2:
281 entry.B2 = match.TeamNumber
282 case 3:
283 entry.B3 = match.TeamNumber
284 default:
285 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown blue position ", strconv.Itoa(int(match.AlliancePosition)), " in match ", strconv.Itoa(int(match.MatchNumber))))
286 return
287 }
288 default:
289 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Unknown alliance ", match.Alliance, " in match ", strconv.Itoa(int(match.AlliancePosition))))
290 return
291 }
292 assembledMatches[key] = entry
293 }
294
295 var response RequestAllMatchesResponseT
296 for _, match := range assembledMatches {
297 copied_match := match
298 response.MatchList = append(response.MatchList, &copied_match)
299 }
300
301 var MATCH_TYPE_ORDERING = []string{"qm", "ef", "qf", "sf", "f"}
302
303 err = nil
304 sort.Slice(response.MatchList, func(i, j int) bool {
305 if err != nil {
306 return false
307 }
308 a := response.MatchList[i]
309 b := response.MatchList[j]
310
Emily Markovaabcac6e2023-02-18 17:50:03 -0800311 aMatchTypeIndex, err2 := findIndexInList(MATCH_TYPE_ORDERING, a.CompLevel)
312 if err2 != nil {
313 err = errors.New(fmt.Sprint("Comp level ", a.CompLevel, " not found in sorting list ", MATCH_TYPE_ORDERING, " : ", err2))
Emily Markovabf24c9e2023-02-08 20:31:11 -0800314 return false
315 }
Emily Markovaabcac6e2023-02-18 17:50:03 -0800316 bMatchTypeIndex, err2 := findIndexInList(MATCH_TYPE_ORDERING, b.CompLevel)
317 if err2 != nil {
318 err = errors.New(fmt.Sprint("Comp level ", b.CompLevel, " not found in sorting list ", MATCH_TYPE_ORDERING, " : ", err2))
Emily Markovabf24c9e2023-02-08 20:31:11 -0800319 return false
320 }
321
322 if aMatchTypeIndex < bMatchTypeIndex {
323 return true
324 }
325 if aMatchTypeIndex > bMatchTypeIndex {
326 return false
327 }
328
329 // Then sort by match number. E.g. in semi finals, all match 1 rounds
330 // are done first. Then come match 2 rounds. And then, if necessary,
331 // the match 3 rounds.
332 aMatchNumber := a.MatchNumber
333 bMatchNumber := b.MatchNumber
334 if aMatchNumber < bMatchNumber {
335 return true
336 }
337 if aMatchNumber > bMatchNumber {
338 return false
339 }
340 // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
341 // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
342 // Semi Final 2 Match 2.
343 aSetNumber := a.SetNumber
344 bSetNumber := b.SetNumber
345 if aSetNumber < bSetNumber {
346 return true
347 }
348 if aSetNumber > bSetNumber {
349 return false
350 }
351 return true
352 })
353
354 if err != nil {
355 // check if error happened during sorting and notify webpage if that
356 respondWithError(w, http.StatusInternalServerError, fmt.Sprint(err))
357 return
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800358 }
359
360 builder := flatbuffers.NewBuilder(50 * 1024)
361 builder.Finish((&response).Pack(builder))
362 w.Write(builder.FinishedBytes())
363}
364
Philipp Schraderacf96232022-03-01 22:03:30 -0800365// Handles a RequestDataScouting request.
366type requestDataScoutingHandler struct {
367 db Database
368}
369
370func (handler requestDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
371 requestBytes, err := io.ReadAll(req.Body)
372 if err != nil {
373 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
374 return
375 }
376
Philipp Schraderb7e75932022-03-26 16:18:34 -0700377 _, success := parseRequest(w, requestBytes, "RequestDataScouting", request_data_scouting.GetRootAsRequestDataScouting)
Philipp Schraderacf96232022-03-01 22:03:30 -0800378 if !success {
379 return
380 }
381
382 stats, err := handler.db.ReturnStats()
383 if err != nil {
384 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
Philipp Schrader2e7eb0002022-03-02 22:52:39 -0800385 return
Philipp Schraderacf96232022-03-01 22:03:30 -0800386 }
387
388 var response RequestDataScoutingResponseT
389 for _, stat := range stats {
390 response.StatsList = append(response.StatsList, &request_data_scouting_response.StatsT{
Philipp Schraderfa45d742022-03-18 19:29:05 -0700391 Team: stat.TeamNumber,
392 Match: stat.MatchNumber,
Philipp Schrader30b4a682022-04-16 14:36:17 -0700393 SetNumber: stat.SetNumber,
Philipp Schrader4535b7e2022-04-08 20:27:00 -0700394 CompLevel: stat.CompLevel,
Philipp Schraderfa45d742022-03-18 19:29:05 -0700395 StartingQuadrant: stat.StartingQuadrant,
396 AutoBall1: stat.AutoBallPickedUp[0],
397 AutoBall2: stat.AutoBallPickedUp[1],
398 AutoBall3: stat.AutoBallPickedUp[2],
399 AutoBall4: stat.AutoBallPickedUp[3],
400 AutoBall5: stat.AutoBallPickedUp[4],
401 MissedShotsAuto: stat.ShotsMissedAuto,
402 UpperGoalAuto: stat.UpperGoalAuto,
403 LowerGoalAuto: stat.LowerGoalAuto,
404 MissedShotsTele: stat.ShotsMissed,
405 UpperGoalTele: stat.UpperGoalShots,
406 LowerGoalTele: stat.LowerGoalShots,
407 DefenseRating: stat.PlayedDefense,
408 DefenseReceivedRating: stat.DefenseReceivedScore,
409 ClimbLevel: request_data_scouting_response.ClimbLevel(stat.Climbing),
410 CollectedBy: stat.CollectedBy,
411 Comment: stat.Comment,
Philipp Schraderacf96232022-03-01 22:03:30 -0800412 })
413 }
414
415 builder := flatbuffers.NewBuilder(50 * 1024)
416 builder.Finish((&response).Pack(builder))
417 w.Write(builder.FinishedBytes())
418}
419
Philipp Schraderd3fac192022-03-02 20:35:46 -0800420func parseTeamKey(teamKey string) (int, error) {
421 // TBA prefixes teams with "frc". Not sure why. Get rid of that.
422 teamKey = strings.TrimPrefix(teamKey, "frc")
Philipp Schrader84fe7782022-11-12 08:10:36 -0800423 magnitude := 0
424 if strings.HasSuffix(teamKey, "A") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800425 magnitude = 0
Philipp Schrader84fe7782022-11-12 08:10:36 -0800426 teamKey = strings.TrimSuffix(teamKey, "A")
427 } else if strings.HasSuffix(teamKey, "B") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800428 magnitude = 9
Philipp Schrader84fe7782022-11-12 08:10:36 -0800429 teamKey = strings.TrimSuffix(teamKey, "B")
430 } else if strings.HasSuffix(teamKey, "C") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800431 magnitude = 8
Philipp Schrader84fe7782022-11-12 08:10:36 -0800432 teamKey = strings.TrimSuffix(teamKey, "C")
433 } else if strings.HasSuffix(teamKey, "D") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800434 magnitude = 7
Philipp Schrader84fe7782022-11-12 08:10:36 -0800435 teamKey = strings.TrimSuffix(teamKey, "D")
436 } else if strings.HasSuffix(teamKey, "E") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800437 magnitude = 6
Philipp Schrader84fe7782022-11-12 08:10:36 -0800438 teamKey = strings.TrimSuffix(teamKey, "E")
439 } else if strings.HasSuffix(teamKey, "F") {
Filip Kujawa08057fa2022-11-12 09:54:49 -0800440 magnitude = 5
Philipp Schrader84fe7782022-11-12 08:10:36 -0800441 teamKey = strings.TrimSuffix(teamKey, "F")
442 }
Filip Kujawa4de6feb2022-11-12 08:41:49 -0800443
444 if magnitude != 0 {
445 teamKey = strconv.Itoa(magnitude) + teamKey
Philipp Schrader84fe7782022-11-12 08:10:36 -0800446 }
Filip Kujawa4de6feb2022-11-12 08:41:49 -0800447
448 result, err := strconv.Atoi(teamKey)
Philipp Schrader84fe7782022-11-12 08:10:36 -0800449 return result, err
Philipp Schraderd3fac192022-03-02 20:35:46 -0800450}
451
452// Parses the alliance data from the specified match and returns the three red
453// teams and the three blue teams.
454func parseTeamKeys(match *scraping.Match) ([3]int32, [3]int32, error) {
455 redKeys := match.Alliances.Red.TeamKeys
456 blueKeys := match.Alliances.Blue.TeamKeys
457
458 if len(redKeys) != 3 || len(blueKeys) != 3 {
459 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
460 "Found %d red teams and %d blue teams.", len(redKeys), len(blueKeys)))
461 }
462
463 var red [3]int32
464 for i, key := range redKeys {
465 team, err := parseTeamKey(key)
466 if err != nil {
467 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
468 "Failed to parse red %d team '%s' as integer: %v", i+1, key, err))
469 }
470 red[i] = int32(team)
471 }
472 var blue [3]int32
473 for i, key := range blueKeys {
474 team, err := parseTeamKey(key)
475 if err != nil {
476 return [3]int32{}, [3]int32{}, errors.New(fmt.Sprintf(
477 "Failed to parse blue %d team '%s' as integer: %v", i+1, key, err))
478 }
479 blue[i] = int32(team)
480 }
481 return red, blue, nil
482}
483
484type refreshMatchListHandler struct {
485 db Database
486 scrape ScrapeMatchList
487}
488
489func (handler refreshMatchListHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
490 requestBytes, err := io.ReadAll(req.Body)
491 if err != nil {
492 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
493 return
494 }
495
Philipp Schraderb7e75932022-03-26 16:18:34 -0700496 request, success := parseRequest(w, requestBytes, "RefreshMatchList", refresh_match_list.GetRootAsRefreshMatchList)
Philipp Schraderd3fac192022-03-02 20:35:46 -0800497 if !success {
498 return
499 }
500
501 matches, err := handler.scrape(request.Year(), string(request.EventCode()))
502 if err != nil {
503 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to scrape match list: ", err))
504 return
505 }
506
507 for _, match := range matches {
508 // Make sure the data is valid.
509 red, blue, err := parseTeamKeys(&match)
510 if err != nil {
511 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
512 "TheBlueAlliance data for match %d is malformed: %v", match.MatchNumber, err))
513 return
514 }
Emily Markovabf24c9e2023-02-08 20:31:11 -0800515
516 team_matches := []db.TeamMatch{
517 {
518 MatchNumber: int32(match.MatchNumber),
519 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
520 Alliance: "R", AlliancePosition: 1, TeamNumber: red[0],
521 },
522 {
523 MatchNumber: int32(match.MatchNumber),
524 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
525 Alliance: "R", AlliancePosition: 2, TeamNumber: red[1],
526 },
527 {
528 MatchNumber: int32(match.MatchNumber),
529 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
530 Alliance: "R", AlliancePosition: 3, TeamNumber: red[2],
531 },
532 {
533 MatchNumber: int32(match.MatchNumber),
534 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
535 Alliance: "B", AlliancePosition: 1, TeamNumber: blue[0],
536 },
537 {
538 MatchNumber: int32(match.MatchNumber),
539 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
540 Alliance: "B", AlliancePosition: 2, TeamNumber: blue[1],
541 },
542 {
543 MatchNumber: int32(match.MatchNumber),
544 SetNumber: int32(match.SetNumber), CompLevel: match.CompLevel,
545 Alliance: "B", AlliancePosition: 3, TeamNumber: blue[2],
546 },
547 }
548
549 for _, match := range team_matches {
550 // Iterate through matches to check they can be added to database.
551 err = handler.db.AddToMatch(match)
552 if err != nil {
553 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf(
554 "Failed to add team %d from match %d to the database: %v", match.TeamNumber, match.MatchNumber, err))
555 return
556 }
Philipp Schrader7365d322022-03-06 16:40:08 -0800557 }
Philipp Schraderd3fac192022-03-02 20:35:46 -0800558 }
559
560 var response RefreshMatchListResponseT
561 builder := flatbuffers.NewBuilder(1024)
562 builder.Finish((&response).Pack(builder))
563 w.Write(builder.FinishedBytes())
564}
565
Alex Perry81f96ba2022-03-13 18:26:19 -0700566type submitNoteScoutingHandler struct {
567 db Database
568}
569
570func (handler submitNoteScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
571 requestBytes, err := io.ReadAll(req.Body)
572 if err != nil {
573 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
574 return
575 }
576
Philipp Schraderb7e75932022-03-26 16:18:34 -0700577 request, success := parseRequest(w, requestBytes, "SubmitNotes", submit_notes.GetRootAsSubmitNotes)
Alex Perry81f96ba2022-03-13 18:26:19 -0700578 if !success {
579 return
580 }
581
Filip Kujawaf947cb42022-11-21 10:00:30 -0800582 err = handler.db.AddNotes(db.NotesData{
583 TeamNumber: request.Team(),
584 Notes: string(request.Notes()),
585 GoodDriving: bool(request.GoodDriving()),
586 BadDriving: bool(request.BadDriving()),
587 SketchyClimb: bool(request.SketchyClimb()),
588 SolidClimb: bool(request.SolidClimb()),
589 GoodDefense: bool(request.GoodDefense()),
590 BadDefense: bool(request.BadDefense()),
591 })
Alex Perry81f96ba2022-03-13 18:26:19 -0700592 if err != nil {
593 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
594 return
595 }
596
597 var response SubmitNotesResponseT
598 builder := flatbuffers.NewBuilder(10)
599 builder.Finish((&response).Pack(builder))
600 w.Write(builder.FinishedBytes())
601}
602
Alex Perry81f96ba2022-03-13 18:26:19 -0700603type requestNotesForTeamHandler struct {
604 db Database
605}
606
607func (handler requestNotesForTeamHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
608 requestBytes, err := io.ReadAll(req.Body)
609 if err != nil {
610 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
611 return
612 }
613
Philipp Schraderb7e75932022-03-26 16:18:34 -0700614 request, success := parseRequest(w, requestBytes, "RequestNotesForTeam", request_notes_for_team.GetRootAsRequestNotesForTeam)
Alex Perry81f96ba2022-03-13 18:26:19 -0700615 if !success {
616 return
617 }
618
Philipp Schradereecb8962022-06-01 21:02:42 -0700619 notes, err := handler.db.QueryNotes(request.Team())
Alex Perry81f96ba2022-03-13 18:26:19 -0700620 if err != nil {
621 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query notes: %v", err))
622 return
623 }
624
625 var response RequestNotesForTeamResponseT
Philipp Schradereecb8962022-06-01 21:02:42 -0700626 for _, data := range notes {
Alex Perry81f96ba2022-03-13 18:26:19 -0700627 response.Notes = append(response.Notes, &request_notes_for_team_response.NoteT{data})
628 }
629
630 builder := flatbuffers.NewBuilder(1024)
631 builder.Finish((&response).Pack(builder))
632 w.Write(builder.FinishedBytes())
633}
634
Milo Lin1d59f0c2022-06-22 20:30:58 -0700635type requestShiftScheduleHandler struct {
636 db Database
637}
638
639func (handler requestShiftScheduleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
640 requestBytes, err := io.ReadAll(req.Body)
641 if err != nil {
642 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
643 return
644 }
645
646 _, success := parseRequest(w, requestBytes, "RequestShiftSchedule", request_shift_schedule.GetRootAsRequestShiftSchedule)
647 if !success {
648 return
649 }
650
651 shiftData, err := handler.db.ReturnAllShifts()
652 if err != nil {
653 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query shift schedule: %v", err))
654 return
655 }
656
657 var response RequestShiftScheduleResponseT
658 for _, shifts := range shiftData {
659 response.ShiftSchedule = append(response.ShiftSchedule, &request_shift_schedule_response.MatchAssignmentT{
660 MatchNumber: shifts.MatchNumber,
661 R1scouter: shifts.R1scouter,
662 R2scouter: shifts.R2scouter,
663 R3scouter: shifts.R3scouter,
664 B1scouter: shifts.B1scouter,
665 B2scouter: shifts.B2scouter,
666 B3scouter: shifts.B3scouter,
667 })
668 }
669
670 builder := flatbuffers.NewBuilder(1024)
671 builder.Finish((&response).Pack(builder))
672 w.Write(builder.FinishedBytes())
673}
674
675type submitShiftScheduleHandler struct {
676 db Database
677}
678
679func (handler submitShiftScheduleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
680 // Get the username of the person submitting the data.
681 username := parseUsername(req)
682
683 requestBytes, err := io.ReadAll(req.Body)
684 if err != nil {
685 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
686 return
687 }
688
689 request, success := parseRequest[SubmitShiftSchedule](w, requestBytes, "SubmitShiftSchedule", submit_shift_schedule.GetRootAsSubmitShiftSchedule)
690 if !success {
691 return
692 }
693
694 log.Println("Got shift schedule from", username)
695 shift_schedule_length := request.ShiftScheduleLength()
696 for i := 0; i < shift_schedule_length; i++ {
697 var match_assignment submit_shift_schedule.MatchAssignment
698 request.ShiftSchedule(&match_assignment, i)
699 current_shift := db.Shift{
700 MatchNumber: match_assignment.MatchNumber(),
701 R1scouter: string(match_assignment.R1scouter()),
702 R2scouter: string(match_assignment.R2scouter()),
703 R3scouter: string(match_assignment.R3scouter()),
704 B1scouter: string(match_assignment.B1scouter()),
705 B2scouter: string(match_assignment.B2scouter()),
706 B3scouter: string(match_assignment.B3scouter()),
707 }
708 err = handler.db.AddToShift(current_shift)
709 if err != nil {
710 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to submit shift schedule: ", err))
711 return
712 }
713 }
714
715 builder := flatbuffers.NewBuilder(50 * 1024)
716 builder.Finish((&SubmitShiftScheduleResponseT{}).Pack(builder))
717 w.Write(builder.FinishedBytes())
718}
719
Filip Kujawa210a03b2022-11-24 14:41:11 -0800720type SubmitDriverRankingHandler struct {
721 db Database
722}
723
724func (handler SubmitDriverRankingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
725 requestBytes, err := io.ReadAll(req.Body)
726 if err != nil {
727 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
728 return
729 }
730
731 request, success := parseRequest(w, requestBytes, "SubmitDriverRanking", submit_driver_ranking.GetRootAsSubmitDriverRanking)
732 if !success {
733 return
734 }
735
736 err = handler.db.AddDriverRanking(db.DriverRankingData{
737 MatchNumber: request.MatchNumber(),
738 Rank1: request.Rank1(),
739 Rank2: request.Rank2(),
740 Rank3: request.Rank3(),
741 })
742
743 if err != nil {
744 respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert driver ranking: %v", err))
745 return
746 }
747
748 var response SubmitDriverRankingResponseT
749 builder := flatbuffers.NewBuilder(10)
750 builder.Finish((&response).Pack(builder))
751 w.Write(builder.FinishedBytes())
752}
753
Filip Kujawaf882e022022-12-14 13:14:08 -0800754type requestAllNotesHandler struct {
755 db Database
756}
757
758func (handler requestAllNotesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
759 requestBytes, err := io.ReadAll(req.Body)
760 if err != nil {
761 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
762 return
763 }
764
765 _, success := parseRequest(w, requestBytes, "RequestAllNotes", request_all_notes.GetRootAsRequestAllNotes)
766 if !success {
767 return
768 }
769
770 notes, err := handler.db.ReturnAllNotes()
771 if err != nil {
772 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
773 return
774 }
775
776 var response RequestAllNotesResponseT
777 for _, note := range notes {
778 response.NoteList = append(response.NoteList, &request_all_notes_response.NoteT{
779 Team: note.TeamNumber,
780 Notes: note.Notes,
781 GoodDriving: note.GoodDriving,
782 BadDriving: note.BadDriving,
783 SketchyClimb: note.SketchyClimb,
784 SolidClimb: note.SolidClimb,
785 GoodDefense: note.GoodDefense,
786 BadDefense: note.BadDefense,
787 })
788 }
789
790 builder := flatbuffers.NewBuilder(50 * 1024)
791 builder.Finish((&response).Pack(builder))
792 w.Write(builder.FinishedBytes())
793}
794
795type requestAllDriverRankingsHandler struct {
796 db Database
797}
798
799func (handler requestAllDriverRankingsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
800 requestBytes, err := io.ReadAll(req.Body)
801 if err != nil {
802 respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
803 return
804 }
805
806 _, success := parseRequest(w, requestBytes, "RequestAllDriverRankings", request_all_driver_rankings.GetRootAsRequestAllDriverRankings)
807 if !success {
808 return
809 }
810
811 rankings, err := handler.db.ReturnAllDriverRankings()
812 if err != nil {
813 respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
814 return
815 }
816
817 var response RequestAllDriverRankingsResponseT
818 for _, ranking := range rankings {
819 response.DriverRankingList = append(response.DriverRankingList, &request_all_driver_rankings_response.RankingT{
820 MatchNumber: ranking.MatchNumber,
821 Rank1: ranking.Rank1,
822 Rank2: ranking.Rank2,
823 Rank3: ranking.Rank3,
824 })
825 }
826
827 builder := flatbuffers.NewBuilder(50 * 1024)
828 builder.Finish((&response).Pack(builder))
829 w.Write(builder.FinishedBytes())
830}
831
Philipp Schraderd3fac192022-03-02 20:35:46 -0800832func HandleRequests(db Database, scrape ScrapeMatchList, scoutingServer server.ScoutingServer) {
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800833 scoutingServer.HandleFunc("/requests", unknown)
Philipp Schrader8747f1b2022-02-23 23:56:22 -0800834 scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
Philipp Schradercbf5c6a2022-02-27 23:25:19 -0800835 scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
Filip Kujawaf882e022022-12-14 13:14:08 -0800836 scoutingServer.Handle("/requests/request/all_notes", requestAllNotesHandler{db})
837 scoutingServer.Handle("/requests/request/all_driver_rankings", requestAllDriverRankingsHandler{db})
Philipp Schraderacf96232022-03-01 22:03:30 -0800838 scoutingServer.Handle("/requests/request/data_scouting", requestDataScoutingHandler{db})
Philipp Schraderd3fac192022-03-02 20:35:46 -0800839 scoutingServer.Handle("/requests/refresh_match_list", refreshMatchListHandler{db, scrape})
Alex Perry81f96ba2022-03-13 18:26:19 -0700840 scoutingServer.Handle("/requests/submit/submit_notes", submitNoteScoutingHandler{db})
841 scoutingServer.Handle("/requests/request/notes_for_team", requestNotesForTeamHandler{db})
Milo Lin1d59f0c2022-06-22 20:30:58 -0700842 scoutingServer.Handle("/requests/submit/shift_schedule", submitShiftScheduleHandler{db})
843 scoutingServer.Handle("/requests/request/shift_schedule", requestShiftScheduleHandler{db})
Filip Kujawa210a03b2022-11-24 14:41:11 -0800844 scoutingServer.Handle("/requests/submit/submit_driver_ranking", SubmitDriverRankingHandler{db})
Philipp Schradercdb5cfc2022-02-20 14:57:07 -0800845}