diff options
| author | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2023-08-26 08:53:24 +0300 |
|---|---|---|
| committer | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2023-08-26 08:53:24 +0300 |
| commit | f1b7589b2936335957a6a1da1eea3d66233ad0ce (patch) | |
| tree | 1975af217c190f5dbdb23b96015cef45206302d4 /backend/handlers/record.go | |
| parent | docs: profile improvement swagger (#51) (diff) | |
| download | lphub-f1b7589b2936335957a6a1da1eea3d66233ad0ce.tar.gz lphub-f1b7589b2936335957a6a1da1eea3d66233ad0ce.tar.bz2 lphub-f1b7589b2936335957a6a1da1eea3d66233ad0ce.zip | |
refactor: reorganizing packages
Former-commit-id: 99410223654c2a5ffc15fdab6ec3e921b5410cba
Diffstat (limited to 'backend/handlers/record.go')
| -rw-r--r-- | backend/handlers/record.go | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/backend/handlers/record.go b/backend/handlers/record.go new file mode 100644 index 0000000..00c9b7d --- /dev/null +++ b/backend/handlers/record.go | |||
| @@ -0,0 +1,292 @@ | |||
| 1 | package handlers | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "encoding/base64" | ||
| 6 | "io" | ||
| 7 | "log" | ||
| 8 | "mime/multipart" | ||
| 9 | "net/http" | ||
| 10 | "os" | ||
| 11 | |||
| 12 | "github.com/gin-gonic/gin" | ||
| 13 | "github.com/google/uuid" | ||
| 14 | "github.com/pektezol/leastportalshub/backend/database" | ||
| 15 | "github.com/pektezol/leastportalshub/backend/models" | ||
| 16 | "github.com/pektezol/leastportalshub/backend/parser" | ||
| 17 | "golang.org/x/oauth2/google" | ||
| 18 | "golang.org/x/oauth2/jwt" | ||
| 19 | "google.golang.org/api/drive/v3" | ||
| 20 | ) | ||
| 21 | |||
| 22 | type RecordRequest struct { | ||
| 23 | HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` | ||
| 24 | PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` | ||
| 25 | IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` | ||
| 26 | PartnerID string `json:"partner_id" form:"partner_id"` | ||
| 27 | } | ||
| 28 | |||
| 29 | type RecordResponse struct { | ||
| 30 | ScoreCount int `json:"score_count"` | ||
| 31 | ScoreTime int `json:"score_time"` | ||
| 32 | } | ||
| 33 | |||
| 34 | // POST Record | ||
| 35 | // | ||
| 36 | // @Description Post record with demo of a specific map. | ||
| 37 | // @Tags maps | ||
| 38 | // @Accept mpfd | ||
| 39 | // @Produce json | ||
| 40 | // @Param id path int true "Map ID" | ||
| 41 | // @Param Authorization header string true "JWT Token" | ||
| 42 | // @Param host_demo formData file true "Host Demo" | ||
| 43 | // @Param partner_demo formData file false "Partner Demo" | ||
| 44 | // @Param is_partner_orange formData boolean false "Is Partner Orange" | ||
| 45 | // @Param partner_id formData string false "Partner ID" | ||
| 46 | // @Success 200 {object} models.Response{data=RecordResponse} | ||
| 47 | // @Failure 400 {object} models.Response | ||
| 48 | // @Failure 401 {object} models.Response | ||
| 49 | // @Router /maps/{id}/record [post] | ||
| 50 | func CreateRecordWithDemo(c *gin.Context) { | ||
| 51 | mapId := c.Param("id") | ||
| 52 | // Check if user exists | ||
| 53 | user, exists := c.Get("user") | ||
| 54 | if !exists { | ||
| 55 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 56 | return | ||
| 57 | } | ||
| 58 | // Check if map is sp or mp | ||
| 59 | var gameName string | ||
| 60 | var isCoop bool | ||
| 61 | var isDisabled bool | ||
| 62 | sql := `SELECT g.name, m.is_disabled FROM maps m INNER JOIN games g ON m.game_id=g.id WHERE m.id = $1` | ||
| 63 | err := database.DB.QueryRow(sql, mapId).Scan(&gameName, &isDisabled) | ||
| 64 | if err != nil { | ||
| 65 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 66 | return | ||
| 67 | } | ||
| 68 | if isDisabled { | ||
| 69 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) | ||
| 70 | return | ||
| 71 | } | ||
| 72 | if gameName == "Portal 2 - Cooperative" { | ||
| 73 | isCoop = true | ||
| 74 | } | ||
| 75 | // Get record request | ||
| 76 | var record RecordRequest | ||
| 77 | if err := c.ShouldBind(&record); err != nil { | ||
| 78 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 79 | return | ||
| 80 | } | ||
| 81 | if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { | ||
| 82 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) | ||
| 83 | return | ||
| 84 | } | ||
| 85 | // Demo files | ||
| 86 | demoFiles := []*multipart.FileHeader{record.HostDemo} | ||
| 87 | if isCoop { | ||
| 88 | demoFiles = append(demoFiles, record.PartnerDemo) | ||
| 89 | } | ||
| 90 | var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string | ||
| 91 | var hostDemoScoreCount, hostDemoScoreTime int | ||
| 92 | client := serviceAccount() | ||
| 93 | srv, err := drive.New(client) | ||
| 94 | if err != nil { | ||
| 95 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 96 | return | ||
| 97 | } | ||
| 98 | // Create database transaction for inserts | ||
| 99 | tx, err := database.DB.Begin() | ||
| 100 | if err != nil { | ||
| 101 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) | ||
| 102 | return | ||
| 103 | } | ||
| 104 | // Defer to a rollback in case anything fails | ||
| 105 | defer tx.Rollback() | ||
| 106 | for i, header := range demoFiles { | ||
| 107 | uuid := uuid.New().String() | ||
| 108 | // Upload & insert into demos | ||
| 109 | err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") | ||
| 110 | if err != nil { | ||
| 111 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 112 | return | ||
| 113 | } | ||
| 114 | defer os.Remove("backend/parser/" + uuid + ".dem") | ||
| 115 | f, err := os.Open("backend/parser/" + uuid + ".dem") | ||
| 116 | if err != nil { | ||
| 117 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 118 | return | ||
| 119 | } | ||
| 120 | defer f.Close() | ||
| 121 | file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) | ||
| 122 | if err != nil { | ||
| 123 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 124 | return | ||
| 125 | } | ||
| 126 | hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") | ||
| 127 | if err != nil { | ||
| 128 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 129 | return | ||
| 130 | } | ||
| 131 | if i == 0 { | ||
| 132 | hostDemoFileID = file.Id | ||
| 133 | hostDemoUUID = uuid | ||
| 134 | } else if i == 1 { | ||
| 135 | partnerDemoFileID = file.Id | ||
| 136 | partnerDemoUUID = uuid | ||
| 137 | } | ||
| 138 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) | ||
| 139 | if err != nil { | ||
| 140 | deleteFile(srv, file.Id) | ||
| 141 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 142 | return | ||
| 143 | } | ||
| 144 | } | ||
| 145 | // Insert into records | ||
| 146 | if isCoop { | ||
| 147 | sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id) | ||
| 148 | VALUES($1, $2, $3, $4, $5, $6, $7)` | ||
| 149 | var hostID string | ||
| 150 | var partnerID string | ||
| 151 | if record.IsPartnerOrange { | ||
| 152 | hostID = user.(models.User).SteamID | ||
| 153 | partnerID = record.PartnerID | ||
| 154 | } else { | ||
| 155 | partnerID = user.(models.User).SteamID | ||
| 156 | hostID = record.PartnerID | ||
| 157 | } | ||
| 158 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID) | ||
| 159 | if err != nil { | ||
| 160 | deleteFile(srv, hostDemoFileID) | ||
| 161 | deleteFile(srv, partnerDemoFileID) | ||
| 162 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 163 | return | ||
| 164 | } | ||
| 165 | // If a new world record based on portal count | ||
| 166 | // if record.ScoreCount < wrScore { | ||
| 167 | // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId) | ||
| 168 | // if err != nil { | ||
| 169 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 170 | // return | ||
| 171 | // } | ||
| 172 | // } | ||
| 173 | } else { | ||
| 174 | sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) | ||
| 175 | VALUES($1, $2, $3, $4, $5)` | ||
| 176 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) | ||
| 177 | if err != nil { | ||
| 178 | deleteFile(srv, hostDemoFileID) | ||
| 179 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 180 | return | ||
| 181 | } | ||
| 182 | // If a new world record based on portal count | ||
| 183 | // if record.ScoreCount < wrScore { | ||
| 184 | // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId) | ||
| 185 | // if err != nil { | ||
| 186 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 187 | // return | ||
| 188 | // } | ||
| 189 | // } | ||
| 190 | } | ||
| 191 | if err = tx.Commit(); err != nil { | ||
| 192 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) | ||
| 193 | return | ||
| 194 | } | ||
| 195 | c.JSON(http.StatusOK, models.Response{ | ||
| 196 | Success: true, | ||
| 197 | Message: "Successfully created record.", | ||
| 198 | Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, | ||
| 199 | }) | ||
| 200 | } | ||
| 201 | |||
| 202 | // GET Demo | ||
| 203 | // | ||
| 204 | // @Description Get demo with specified demo uuid. | ||
| 205 | // @Tags demo | ||
| 206 | // @Accept json | ||
| 207 | // @Produce octet-stream | ||
| 208 | // @Param uuid query string true "Demo UUID" | ||
| 209 | // @Success 200 {file} binary "Demo File" | ||
| 210 | // @Failure 400 {object} models.Response | ||
| 211 | // @Router /demos [get] | ||
| 212 | func DownloadDemoWithID(c *gin.Context) { | ||
| 213 | uuid := c.Query("uuid") | ||
| 214 | var locationID string | ||
| 215 | if uuid == "" { | ||
| 216 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given.")) | ||
| 217 | return | ||
| 218 | } | ||
| 219 | err := database.DB.QueryRow(`SELECT location_id FROM demos WHERE id = $1`, uuid).Scan(&locationID) | ||
| 220 | if err != nil { | ||
| 221 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 222 | return | ||
| 223 | } | ||
| 224 | if locationID == "" { | ||
| 225 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given.")) | ||
| 226 | return | ||
| 227 | } | ||
| 228 | url := "https://drive.google.com/uc?export=download&id=" + locationID | ||
| 229 | fileName := uuid + ".dem" | ||
| 230 | output, err := os.Create(fileName) | ||
| 231 | if err != nil { | ||
| 232 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 233 | return | ||
| 234 | } | ||
| 235 | defer os.Remove(fileName) | ||
| 236 | defer output.Close() | ||
| 237 | response, err := http.Get(url) | ||
| 238 | if err != nil { | ||
| 239 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 240 | return | ||
| 241 | } | ||
| 242 | defer response.Body.Close() | ||
| 243 | _, err = io.Copy(output, response.Body) | ||
| 244 | if err != nil { | ||
| 245 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 246 | return | ||
| 247 | } | ||
| 248 | // Downloaded file | ||
| 249 | c.Header("Content-Description", "File Transfer") | ||
| 250 | c.Header("Content-Transfer-Encoding", "binary") | ||
| 251 | c.Header("Content-Disposition", "attachment; filename="+fileName) | ||
| 252 | c.Header("Content-Type", "application/octet-stream") | ||
| 253 | c.File(fileName) | ||
| 254 | // c.FileAttachment() | ||
| 255 | } | ||
| 256 | |||
| 257 | // Use Service account | ||
| 258 | func serviceAccount() *http.Client { | ||
| 259 | privateKey, _ := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64")) | ||
| 260 | config := &jwt.Config{ | ||
| 261 | Email: os.Getenv("GOOGLE_CLIENT_EMAIL"), | ||
| 262 | PrivateKey: []byte(privateKey), | ||
| 263 | Scopes: []string{ | ||
| 264 | drive.DriveScope, | ||
| 265 | }, | ||
| 266 | TokenURL: google.JWTTokenURL, | ||
| 267 | } | ||
| 268 | client := config.Client(context.Background()) | ||
| 269 | return client | ||
| 270 | } | ||
| 271 | |||
| 272 | // Create Gdrive file | ||
| 273 | func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { | ||
| 274 | f := &drive.File{ | ||
| 275 | MimeType: mimeType, | ||
| 276 | Name: name, | ||
| 277 | Parents: []string{parentId}, | ||
| 278 | } | ||
| 279 | file, err := service.Files.Create(f).Media(content).Do() | ||
| 280 | |||
| 281 | if err != nil { | ||
| 282 | log.Println("Could not create file: " + err.Error()) | ||
| 283 | return nil, err | ||
| 284 | } | ||
| 285 | |||
| 286 | return file, nil | ||
| 287 | } | ||
| 288 | |||
| 289 | // Delete Gdrive file | ||
| 290 | func deleteFile(service *drive.Service, fileId string) { | ||
| 291 | service.Files.Delete(fileId) | ||
| 292 | } | ||