aboutsummaryrefslogtreecommitdiff
path: root/backend/handlers/record.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/handlers/record.go')
-rw-r--r--backend/handlers/record.go292
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 @@
1package handlers
2
3import (
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
22type 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
29type 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]
50func 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]
212func 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
258func 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
273func 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
290func deleteFile(service *drive.Service, fileId string) {
291 service.Files.Delete(fileId)
292}