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.go303
1 files changed, 303 insertions, 0 deletions
diff --git a/backend/handlers/record.go b/backend/handlers/record.go
new file mode 100644
index 0000000..3d29eb8
--- /dev/null
+++ b/backend/handlers/record.go
@@ -0,0 +1,303 @@
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 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
70 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards."))
71 return
72 }
73 if gameName == "Portal 2 - Cooperative" {
74 isCoop = true
75 }
76 // Get record request
77 var record RecordRequest
78 if err := c.ShouldBind(&record); err != nil {
79 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
80 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
81 return
82 }
83 if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") {
84 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
85 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission."))
86 return
87 }
88 // Demo files
89 demoFiles := []*multipart.FileHeader{record.HostDemo}
90 if isCoop {
91 demoFiles = append(demoFiles, record.PartnerDemo)
92 }
93 var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string
94 var hostDemoScoreCount, hostDemoScoreTime int
95 client := serviceAccount()
96 srv, err := drive.New(client)
97 if err != nil {
98 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
99 return
100 }
101 // Create database transaction for inserts
102 tx, err := database.DB.Begin()
103 if err != nil {
104 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
105 return
106 }
107 // Defer to a rollback in case anything fails
108 defer tx.Rollback()
109 for i, header := range demoFiles {
110 uuid := uuid.New().String()
111 // Upload & insert into demos
112 err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem")
113 if err != nil {
114 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailSaveDemo)
115 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
116 return
117 }
118 defer os.Remove("backend/parser/" + uuid + ".dem")
119 f, err := os.Open("backend/parser/" + uuid + ".dem")
120 if err != nil {
121 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailOpenDemo)
122 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
123 return
124 }
125 defer f.Close()
126 file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID"))
127 if err != nil {
128 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailCreateDemo)
129 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
130 return
131 }
132 hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem")
133 if err != nil {
134 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailProcessDemo)
135 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
136 return
137 }
138 if i == 0 {
139 hostDemoFileID = file.Id
140 hostDemoUUID = uuid
141 } else if i == 1 {
142 partnerDemoFileID = file.Id
143 partnerDemoUUID = uuid
144 }
145 _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id)
146 if err != nil {
147 deleteFile(srv, file.Id)
148 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertDemo)
149 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
150 return
151 }
152 }
153 // Insert into records
154 if isCoop {
155 sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id)
156 VALUES($1, $2, $3, $4, $5, $6, $7)`
157 var hostID string
158 var partnerID string
159 if record.IsPartnerOrange {
160 hostID = user.(models.User).SteamID
161 partnerID = record.PartnerID
162 } else {
163 partnerID = user.(models.User).SteamID
164 hostID = record.PartnerID
165 }
166 _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID)
167 if err != nil {
168 deleteFile(srv, hostDemoFileID)
169 deleteFile(srv, partnerDemoFileID)
170 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord)
171 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
172 return
173 }
174 // If a new world record based on portal count
175 // if record.ScoreCount < wrScore {
176 // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId)
177 // if err != nil {
178 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
179 // return
180 // }
181 // }
182 } else {
183 sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id)
184 VALUES($1, $2, $3, $4, $5)`
185 _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID)
186 if err != nil {
187 deleteFile(srv, hostDemoFileID)
188 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord)
189 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
190 return
191 }
192 // If a new world record based on portal count
193 // if record.ScoreCount < wrScore {
194 // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId)
195 // if err != nil {
196 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
197 // return
198 // }
199 // }
200 }
201 if err = tx.Commit(); err != nil {
202 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
203 return
204 }
205 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordSuccess)
206 c.JSON(http.StatusOK, models.Response{
207 Success: true,
208 Message: "Successfully created record.",
209 Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime},
210 })
211}
212
213// GET Demo
214//
215// @Description Get demo with specified demo uuid.
216// @Tags demo
217// @Accept json
218// @Produce octet-stream
219// @Param uuid query string true "Demo UUID"
220// @Success 200 {file} binary "Demo File"
221// @Failure 400 {object} models.Response
222// @Router /demos [get]
223func DownloadDemoWithID(c *gin.Context) {
224 uuid := c.Query("uuid")
225 var locationID string
226 if uuid == "" {
227 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given."))
228 return
229 }
230 err := database.DB.QueryRow(`SELECT location_id FROM demos WHERE id = $1`, uuid).Scan(&locationID)
231 if err != nil {
232 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
233 return
234 }
235 if locationID == "" {
236 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given."))
237 return
238 }
239 url := "https://drive.google.com/uc?export=download&id=" + locationID
240 fileName := uuid + ".dem"
241 output, err := os.Create(fileName)
242 if err != nil {
243 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
244 return
245 }
246 defer os.Remove(fileName)
247 defer output.Close()
248 response, err := http.Get(url)
249 if err != nil {
250 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
251 return
252 }
253 defer response.Body.Close()
254 _, err = io.Copy(output, response.Body)
255 if err != nil {
256 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
257 return
258 }
259 // Downloaded file
260 c.Header("Content-Description", "File Transfer")
261 c.Header("Content-Transfer-Encoding", "binary")
262 c.Header("Content-Disposition", "attachment; filename="+fileName)
263 c.Header("Content-Type", "application/octet-stream")
264 c.File(fileName)
265 // c.FileAttachment()
266}
267
268// Use Service account
269func serviceAccount() *http.Client {
270 privateKey, _ := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64"))
271 config := &jwt.Config{
272 Email: os.Getenv("GOOGLE_CLIENT_EMAIL"),
273 PrivateKey: []byte(privateKey),
274 Scopes: []string{
275 drive.DriveScope,
276 },
277 TokenURL: google.JWTTokenURL,
278 }
279 client := config.Client(context.Background())
280 return client
281}
282
283// Create Gdrive file
284func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) {
285 f := &drive.File{
286 MimeType: mimeType,
287 Name: name,
288 Parents: []string{parentId},
289 }
290 file, err := service.Files.Create(f).Media(content).Do()
291
292 if err != nil {
293 log.Println("Could not create file: " + err.Error())
294 return nil, err
295 }
296
297 return file, nil
298}
299
300// Delete Gdrive file
301func deleteFile(service *drive.Service, fileId string) {
302 service.Files.Delete(fileId)
303}