The server for Open Course World
1package api
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "smm2_gameserver/nex/id/code"
12 "smm2_gameserver/orm"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/gorilla/mux"
18 "gorm.io/gorm"
19)
20
21func PopulateCourseCommentCounts(courses []*orm.Course) error {
22 if len(courses) == 0 { return nil }
23
24 course_ids := make([]orm.CourseID, len(courses))
25 for i, course := range courses {
26 course_ids[i] = course.ID
27 }
28
29 rows, err := db.
30 Select("data_id, count(*) as comment_count").
31 Table("course_comment").
32 Where("data_id in ?", course_ids).
33 Group("data_id").
34 Rows()
35
36 if err != nil {
37 return err
38 }
39
40 index := make(map[orm.CourseID]int)
41 for rows.Next() {
42 var id orm.CourseID
43 var count int
44 rows.Scan(&id, &count)
45 index[id] = count
46 }
47
48 if len(index) == 0 {
49 return nil
50 }
51
52 for _, course := range courses {
53 course.CommentCount = index[course.ID]
54 }
55
56 return nil
57}
58
59func SearchCoursesByType(w http.ResponseWriter, r *http.Request, user orm.User, typeBy string) {
60 pid, err := parsePid(mux.Vars(r)["pid"])
61 if err != nil {
62 reportError(w, r, err)
63 return
64 }
65
66 size, offset, err := parseRange(r.URL.Query())
67 if err != nil {
68 reportError(w, r, err)
69 return
70 }
71
72 var courses []*orm.Course
73 err = db.
74 Where(fmt.Sprintf("%s = ?", typeBy), pid).
75 Offset(offset).
76 Limit(size).
77 Find(&courses).
78 Error
79
80 if err != nil {
81 reportError(w, r, err)
82 return
83 }
84
85 err = PopulateCourseCommentCounts(courses)
86 if err != nil {
87 reportError(w, r, err)
88 return
89 }
90
91 w.Header().Set("Content-Type", "application/json")
92 json.NewEncoder(w).Encode(courses)
93}
94
95type NinjiLeaderboardEntry struct {
96 ID uint64
97 Rank int
98 Username string
99 ImageURL string
100 LoginProvider string
101 Time int
102 TimeObtained time.Time
103 ReplayID orm.BigInt
104 ClearVideo string
105}
106
107type PutClearVideoParam struct {
108 Link string
109}
110
111func initDatastore() {
112 Secure("/api/user_info/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
113 pid, err := parsePid(mux.Vars(r)["pid"])
114 if err != nil {
115 info, err := dataView.GetUserInfoByCode(stateForUser(user), mux.Vars(r)["pid"])
116 if err != nil {
117 reportError(w, r, err)
118 return
119 }
120 w.Header().Set("Content-Type", "application/json")
121 json.NewEncoder(w).Encode(info)
122 return
123 }
124
125 info, err := dataView.GetUserInfoByPid(stateForUser(user), pid, 0)
126 if err != nil {
127 reportError(w, r, err)
128 return
129 }
130
131 w.Header().Set("Content-Type", "application/json")
132 json.NewEncoder(w).Encode(info)
133 }).Methods("GET")
134
135 Insecure("/api/courses/{pid}", func(w http.ResponseWriter, r *http.Request) {
136 pid, err := parsePid(mux.Vars(r)["pid"])
137 if err != nil {
138 reportError(w, r, err)
139 return
140 }
141
142 var course orm.Course
143 err = db.
144 Preload("Owner").
145 Preload("PlayerResults", func(db *gorm.DB) *gorm.DB {
146 return db.
147 Where("is_world_record = true or is_first_clear = true").
148 Order("is_world_record DESC").
149 Limit(2)
150 }).
151 Where("data_id = ?", pid).
152 First(&course).
153 Error
154 if err != nil {
155 reportError(w, r, err)
156 return
157 }
158
159 err = PopulateCourseCommentCounts([]*orm.Course{&course})
160 if err != nil {
161 reportError(w, r, err)
162 return
163 }
164
165 w.Header().Set("Content-Type", "application/json")
166 err = json.NewEncoder(w).Encode(course)
167 if err != nil {
168 reportError(w, r, err)
169 return
170 }
171 }).Methods("GET")
172
173 InsecureOpt("/api/courses/{code}/comments", func(w http.ResponseWriter, r *http.Request, user *orm.User) {
174 code, err := parsePid(mux.Vars(r)["code"])
175
176 if err != nil {
177 reportError(w, r, err)
178 return
179 }
180
181 size, offset, err := parseRange(r.URL.Query())
182 if err != nil{
183 reportError(w, r, err)
184 return
185 }
186
187 var comments []orm.Comment
188
189 query := db.
190 Where("data_id = ?", code).
191 Joins("Image").
192 Order("time_posted DESC").
193 Offset(offset).
194 Limit(size)
195
196 if user != nil && user.Role.Admin {
197 query = query.InnerJoins("Commenter")
198 } else {
199 query = query.InnerJoins("Commenter", db.Where("NOT shadow_ban"))
200 }
201
202 err = query.Find(&comments).Error
203
204 if err != nil {
205 reportError(w, r, err)
206 return
207 }
208
209 w.Header().Set("Content-Type", "application/json")
210 err = json.NewEncoder(w).Encode(comments)
211 if err != nil {
212 reportError(w, r, err)
213 return
214 }
215 })
216
217 Insecure("/api/courses/{code}/plays", func(w http.ResponseWriter, r *http.Request) {
218 code, err := parsePid(mux.Vars(r)["code"])
219 if err != nil {
220 reportError(w, r, err)
221 return
222 }
223
224 size, offset, err := parseRange(r.URL.Query())
225 if err != nil {
226 reportError(w, r, err)
227 return
228 }
229
230 var plays []orm.PlayerResult
231 err = db.
232 Preload("Player").
233 Where("data_id = ?", code).
234 Order("is_world_record DESC, cleared DESC, best_time ASC, time_obtained DESC").
235 Offset(offset).
236 Limit(size).
237 Find(&plays).
238 Error
239 if err != nil {
240 reportError(w, r, err)
241 return
242 }
243
244 w.Header().Set("Content-Type", "application/json")
245 err = json.NewEncoder(w).Encode(plays)
246 if err != nil {
247 reportError(w, r, err)
248 return
249 }
250
251 }).Methods("GET")
252
253 Secure("/api/course_info/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
254 dataId, err := parseDataId(mux.Vars(r)["data_id"])
255 if err != nil {
256 reportError(w, r, err)
257 return
258 }
259 info, err := dataView.GetCourseInfo(stateForUser(user), dataId)
260 if err != nil {
261 reportError(w, r, err)
262 return
263 }
264
265 w.Header().Set("Content-Type", "application/json")
266 json.NewEncoder(w).Encode(info)
267 }).Methods("GET")
268
269 Insecure("/api/makers/{pid}/courses", func(w http.ResponseWriter, r *http.Request) {
270 pid, err := parsePid(mux.Vars(r)["pid"])
271 if err != nil {
272 reportError(w, r, err)
273 return
274 }
275
276 size, offset, err := parseRange(r.URL.Query())
277 if err != nil {
278 reportError(w, r, err)
279 return
280 }
281
282 var courses []*orm.Course
283 err = db.
284 Model(&orm.Course{}).
285 Preload("Owner").
286 Where("uploader = ? AND deleted = False", pid).
287 Order("time_uploaded DESC").
288 Offset(offset).
289 Limit(size).
290 Find(&courses).
291 Error
292 if err != nil {
293 reportError(w, r, err)
294 return
295 }
296
297 err = PopulateCourseCommentCounts(courses)
298 if err != nil {
299 reportError(w, r, err)
300 return
301 }
302
303 w.Header().Set("Content-Type", "application/json")
304 err = json.NewEncoder(w).Encode(courses)
305 if err != nil {
306 reportError(w, r, err)
307 return
308 }
309 }).Methods("GET")
310
311 Insecure("/api/makers/{pid}/comments", func(w http.ResponseWriter, r *http.Request) {
312 pid, err := parsePid(mux.Vars(r)["pid"])
313 if err != nil {
314 reportError(w, r, err)
315 return
316 }
317
318 size, offset, err := parseRange(r.URL.Query())
319 if err != nil {
320 reportError(w, r, err)
321 return
322 }
323
324 var comments []orm.Comment
325 err = db.
326 Preload("Image").
327 Preload("Commenter").
328 Preload("Course.Owner").
329 Where("pid = ?", pid).
330 Order("time_posted DESC").
331 Offset(offset).
332 Limit(size).
333 Find(&comments).
334 Error
335
336 if err != nil {
337 reportError(w, r, err)
338 return
339 }
340
341 w.Header().Set("Content-Type", "application/json")
342 err = json.NewEncoder(w).Encode(comments)
343 if err != nil {
344 reportError(w, r, err)
345 return
346 }
347 }).Methods("GET")
348
349 Insecure("/api/latest/courses", func(w http.ResponseWriter, r *http.Request) {
350 size, offset, err := parseRange(r.URL.Query())
351 if err != nil {
352 reportError(w, r, err)
353 return
354 }
355
356 var courses []*orm.Course
357 err = db.
358 Model(&orm.Course{}).
359 Preload("Owner").
360 Where("deleted = False").
361 Order("id DESC").
362 Offset(offset).
363 Limit(size).
364 Find(&courses).
365 Error
366 if err != nil {
367 reportError(w, r, err)
368 return
369 }
370
371 err = PopulateCourseCommentCounts(courses)
372 if err != nil {
373 reportError(w, r, err)
374 return
375 }
376
377 w.Header().Set("Content-Type", "application/json")
378 err = json.NewEncoder(w).Encode(courses)
379 if err != nil {
380 reportError(w, r, err)
381 return
382 }
383 }).Methods("GET")
384
385 Insecure("/api/uncleared/courses", func(w http.ResponseWriter, r *http.Request) {
386 size, offset, err := parseRange(r.URL.Query())
387 if err != nil {
388 reportError(w, r, err)
389 return
390 }
391
392 var courses []*orm.Course
393 err = db.
394 Model(&orm.Course{}).
395 Preload("Owner").
396 Where("clears = 0 AND deleted = False").
397 Order("id DESC").
398 Offset(offset).
399 Limit(size).
400 Find(&courses).
401 Error
402 if err != nil {
403 reportError(w, r, err)
404 return
405 }
406
407 err = PopulateCourseCommentCounts(courses)
408 if err != nil {
409 reportError(w, r, err)
410 return
411 }
412
413 w.Header().Set("Content-Type", "application/json")
414 err = json.NewEncoder(w).Encode(courses)
415 if err != nil {
416 reportError(w, r, err)
417 return
418 }
419 }).Methods("GET")
420
421 Insecure("/api/ninjis", func(w http.ResponseWriter, r *http.Request) {
422 size, offset, err := parseRange(r.URL.Query())
423 if err != nil {
424 reportError(w, r, err)
425 return
426 }
427
428 var ninjis []*orm.Ninji
429 err = db.
430 Model(&orm.Ninji{}).
431 Preload("Course").
432 Preload("Course.Owner").
433 Where("deleted = False").
434 Order("id DESC").
435 Offset(offset).
436 Limit(size).
437 Find(&ninjis).
438 Error
439 if err != nil {
440 reportError(w, r, err)
441 return
442 }
443
444 if len(ninjis) > 0 {
445 courses := make([]*orm.Course, len(ninjis))
446 for i, ninji := range ninjis {
447 courses[i] = ninji.Course
448 }
449 err = PopulateCourseCommentCounts(courses)
450 if err != nil {
451 reportError(w, r, err)
452 return
453 }
454 }
455
456 w.Header().Set("Content-Type", "application/json")
457 err = json.NewEncoder(w).Encode(ninjis)
458 if err != nil {
459 reportError(w, r, err)
460 return
461 }
462 }).Methods("GET")
463
464 Insecure("/api/ninjis/{id}", func(w http.ResponseWriter, r *http.Request) {
465 id, err := parsePid(mux.Vars(r)["id"])
466 if err != nil {
467 reportError(w, r, err)
468 return
469 }
470
471 var ninji orm.Ninji
472 err = db.
473 Model(&orm.Ninji{}).
474 Preload("Course").
475 Preload("Course.Owner").
476 Where("data_id = ? AND deleted = False", id).
477 First(&ninji).
478 Error
479
480 if err != nil {
481 reportError(w, r, err)
482 return
483 }
484
485 w.Header().Set("Content-Type", "application/json")
486 err = json.NewEncoder(w).Encode(ninji)
487 if err != nil {
488 reportError(w, r, err)
489 return
490 }
491 }).Methods("GET")
492
493 Insecure("/api/ninjis-leaderboard/{id}", func(w http.ResponseWriter, r *http.Request) {
494 id, err := parsePid(mux.Vars(r)["id"])
495 if err != nil {
496 reportError(w, r, err)
497 return
498 }
499
500 size, offset, err := parseRange(r.URL.Query())
501 if err != nil {
502 reportError(w, r, err)
503 return
504 }
505
506 var results []NinjiLeaderboardEntry
507 db.Raw("select dense_rank() over (order by v.time) as rank, id, username, image_url, login_provider, v.time_obtained, v.time, v.replay_id, v.clear_video from (select pid,time,time_obtained,replay_id,clear_video FROM ninji_time where data_id = ? and active = true) as v, user_info where id = v.pid LIMIT ? OFFSET ?", id, size, offset).Scan(&results)
508
509 out := []map[string]any{}
510
511 for _, result := range results {
512 out = append(out, map[string]any{
513 "rank": result.Rank,
514 "time": result.Time,
515 "time_obtained": result.TimeObtained.Format("2006-01-02 15:04"),
516 "id": result.ReplayID,
517 "link": result.ClearVideo,
518 "player": map[string]any{
519 "username": result.Username,
520 "id": code.IdToMakerCode(result.ID),
521 "image_url": result.ImageURL,
522 "login_provider": result.LoginProvider,
523 },
524 })
525 }
526
527 w.Header().Set("Content-Type", "application/json")
528 err = json.NewEncoder(w).Encode(out)
529 if err != nil {
530 reportError(w, r, err)
531 return
532 }
533 }).Methods("GET")
534
535 Insecure("/api/makers/{pid}/world-records", func(w http.ResponseWriter, r *http.Request) {
536 pid, err := parsePid(mux.Vars(r)["pid"])
537 if err != nil {
538 reportError(w, r, err)
539 return
540 }
541
542 size, offset, err := parseRange(r.URL.Query())
543 if err != nil {
544 reportError(w, r, err)
545 return
546 }
547
548 var playerResults []orm.PlayerResult
549 err = db.
550 Model(&orm.PlayerResult{}).
551 Preload("Course").
552 Preload("Course.Owner").
553 Where("pid = ? and is_world_record = true", pid).
554 Order("time_obtained DESC").
555 Offset(offset).
556 Limit(size).
557 Find(&playerResults).
558 Error
559 if err != nil {
560 reportError(w, r, err)
561 return
562 }
563 w.Header().Set("Content-Type", "application/json")
564 err = json.NewEncoder(w).Encode(playerResults)
565 if err != nil {
566 reportError(w, r, err)
567 return
568 }
569 }).Methods("GET")
570
571 Insecure("/api/makers/{pid}/played", func(w http.ResponseWriter, r *http.Request) {
572 pid, err := parsePid(mux.Vars(r)["pid"])
573 if err != nil {
574 reportError(w, r, err)
575 return
576 }
577
578 size, offset, err := parseRange(r.URL.Query())
579 if err != nil {
580 reportError(w, r, err)
581 return
582 }
583
584 var playerResults []orm.PlayerResult
585 err = db.
586 Model(&orm.PlayerResult{}).
587 Preload("Course").
588 Preload("Course.Owner").
589 Where("pid = ?", pid).
590 Order("id DESC").
591 Offset(offset).
592 Limit(size).
593 Find(&playerResults).
594 Error
595 if err != nil {
596 reportError(w, r, err)
597 return
598 }
599 w.Header().Set("Content-Type", "application/json")
600 err = json.NewEncoder(w).Encode(playerResults)
601 if err != nil {
602 reportError(w, r, err)
603 return
604 }
605 }).Methods("GET")
606
607 Insecure("/api/makers/{pid}/liked", func(w http.ResponseWriter, r *http.Request) {
608 pid, err := parsePid(mux.Vars(r)["pid"])
609 if err != nil {
610 reportError(w, r, err)
611 return
612 }
613
614 size, offset, err := parseRange(r.URL.Query())
615 if err != nil {
616 reportError(w, r, err)
617 return
618 }
619
620 var results []orm.PlayerLikeInfo
621 err = db.
622 Model(&orm.PlayerLikeInfo{}).
623 Preload("Course").
624 Preload("Course.Owner").
625 Where("pid = ? AND liked = True", pid).
626 Order("id DESC").
627 Offset(offset).
628 Limit(size).
629 Find(&results).
630 Error
631 if err != nil {
632 reportError(w, r, err)
633 return
634 }
635 w.Header().Set("Content-Type", "application/json")
636 err = json.NewEncoder(w).Encode(results)
637 if err != nil {
638 reportError(w, r, err)
639 return
640 }
641 }).Methods("GET")
642
643 Insecure("/api/makers/{pid}/follow", func(w http.ResponseWriter, r *http.Request) {
644 pid, err := parsePid(mux.Vars(r)["pid"])
645 if err != nil {
646 reportError(w, r, err)
647 return
648 }
649
650 size, offset, err := parseRange(r.URL.Query())
651 if err != nil {
652 reportError(w, r, err)
653 return
654 }
655
656 var makers []orm.Maker
657 err = db.
658 Model(&orm.Maker{}).
659 Joins("JOIN player_follow ON user_info.id = player_follow.followed_pid").
660 Where("player_follow.pid = ?", pid).
661 Order("player_follow.id DESC").
662 Offset(offset).
663 Limit(size).
664 Find(&makers).
665 Error
666 if err != nil {
667 reportError(w, r, err)
668 return
669 }
670
671 w.Header().Set("Content-Type", "application/json")
672 err = json.NewEncoder(w).Encode(makers)
673 if err != nil {
674 reportError(w, r, err)
675 return
676 }
677 }).Methods("GET")
678
679 Insecure("/api/makers/{pid}/first-clears", func(w http.ResponseWriter, r *http.Request) {
680 pid, err := parsePid(mux.Vars(r)["pid"])
681 if err != nil {
682 reportError(w, r, err)
683 return
684 }
685
686 size, offset, err := parseRange(r.URL.Query())
687 if err != nil {
688 reportError(w, r, err)
689 return
690 }
691
692 var playerResults []orm.PlayerResult
693 err = db.
694 Model(&orm.PlayerResult{}).
695 Preload("Course").
696 Preload("Course.Owner").
697 Where("pid = ? and is_first_clear = true", pid).
698 Order("time_obtained DESC").
699 Offset(offset).
700 Limit(size).
701 Find(&playerResults).
702 Error
703 if err != nil {
704 reportError(w, r, err)
705 return
706 }
707 w.Header().Set("Content-Type", "application/json")
708 err = json.NewEncoder(w).Encode(playerResults)
709 if err != nil {
710 reportError(w, r, err)
711 return
712 }
713 }).Methods("GET")
714
715 Insecure("/api/makers/{pid}", func(w http.ResponseWriter, r *http.Request) {
716 pid, err := parsePid(mux.Vars(r)["pid"])
717 if err != nil {
718 reportError(w, r, err)
719 return
720 }
721
722 var maker orm.Maker
723 err = db.
724 Model(&orm.Maker{}).
725 Preload("Mii").
726 Preload("Stats").
727 Where("id = ?", pid).
728 First(&maker).
729 Error
730
731 if err != nil {
732 reportError(w, r, err)
733 return
734 }
735
736 w.Header().Set("Content-Type", "application/json")
737 err = json.NewEncoder(w).Encode(maker)
738 if err != nil {
739 reportError(w, r, err)
740 return
741 }
742 }).Methods("GET")
743
744 Insecure("/api/makers", func(w http.ResponseWriter, r *http.Request) {
745 size, offset, err := parseRange(r.URL.Query())
746 if err != nil {
747 reportError(w, r, err)
748 return
749 }
750
751 var makers []orm.Maker
752
753 err = db.
754 Model(&orm.Maker{}).
755 Preload("Mii").
756 Preload("Stats").
757 Where("last_active IS NOT NULL and shadow_ban = False").
758 Order("last_active desc, id desc").
759 Offset(offset).
760 Limit(size).
761 Find(&makers).
762 Error
763
764 if err != nil {
765 reportError(w, r, err)
766 return
767 }
768
769 w.Header().Set("Content-Type", "application/json")
770 err = json.NewEncoder(w).Encode(makers)
771 if err != nil {
772 reportError(w, r, err)
773 return
774 }
775 }).Methods("GET")
776
777 Insecure("/api/one_screen_thumbnail/{data_id}", func(w http.ResponseWriter, r *http.Request) {
778 dataId, err := parseDataId(mux.Vars(r)["data_id"])
779 if err != nil {
780 reportError(w, r, err)
781 return
782 }
783 var oneScreenThumbnails []*[]byte
784 err = db.
785 Model(&orm.CourseData{}).
786 Where("data_id = ?", dataId).
787 Pluck("one_screen_thumbnail", &oneScreenThumbnails).
788 Error
789 if err != nil {
790 reportError(w, r, err)
791 return
792 }
793 if len(oneScreenThumbnails) == 0 || oneScreenThumbnails[0] == nil || len(*oneScreenThumbnails[0]) == 0 {
794 w.WriteHeader(http.StatusNotFound)
795 return
796 }
797 w.Header().Set("Cache-Control", "public, max-age=31536000")
798 w.Header().Set("Content-Type", "image/jpeg")
799 w.Write(*oneScreenThumbnails[0])
800 }).Methods("GET")
801
802 Secure("/api/liked_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
803 SearchCoursesByType(w, r, user, "liked_by")
804 })
805
806 Secure("/api/played_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
807 SearchCoursesByType(w, r, user, "played_by")
808 })
809
810 Secure("/api/first_clear_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
811 SearchCoursesByType(w, r, user, "first_clear_by")
812 })
813
814 Secure("/api/world_record_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
815 SearchCoursesByType(w, r, user, "world_record_by")
816 })
817
818 Secure("/api/follows", func(w http.ResponseWriter, r *http.Request, user orm.User) {
819 var follows []orm.PlayerFollow
820 err := db.
821 Model(&orm.PlayerFollow{}).
822 Where("pid = ?", user.ID).
823 Order("id DESC").
824 Find(&follows).
825 Error
826 if err != nil {
827 reportError(w, r, err)
828 return
829 }
830
831 w.Header().Set("Content-Type", "application/json")
832 err = json.NewEncoder(w).Encode(follows)
833 if err != nil {
834 reportError(w, r, err)
835 return
836 }
837 }).Methods("GET")
838
839 Secure("/api/follows/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
840 pid, err := parsePid(mux.Vars(r)["pid"])
841 if err != nil {
842 reportError(w, r, err)
843 return
844 }
845
846 follow := orm.PlayerFollow{
847 FollowerID: orm.MakerID(user.ID),
848 FollowedID: orm.MakerID(pid),
849 }
850
851 // Upsert the follow
852 err = db.
853 Where("pid = ? and followed_pid = ?", follow.FollowerID, follow.FollowedID).
854 Assign(follow).
855 FirstOrCreate(&follow).
856 Error
857 if err != nil {
858 reportError(w, r, err)
859 return
860 }
861
862 w.Header().Set("Content-Type", "application/json")
863 err = json.NewEncoder(w).Encode(follow)
864 if err != nil {
865 reportError(w, r, err)
866 return
867 }
868 }).Methods("PUT")
869
870 Secure("/api/follows/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
871 pid, err := parsePid(mux.Vars(r)["pid"])
872 if err != nil {
873 reportError(w, r, err)
874 return
875 }
876
877 err = db.
878 Where("pid = ? and followed_pid = ?", user.ID, pid).
879 Delete(&orm.PlayerFollow{}).
880 Error
881 if err != nil {
882 reportError(w, r, err)
883 return
884 }
885
886 w.WriteHeader(http.StatusNoContent)
887 }).Methods("DELETE")
888
889 Secure("/api/follows/courses", func(w http.ResponseWriter, r *http.Request, user orm.User) {
890 size, offset, err := parseRange(r.URL.Query())
891 if err != nil {
892 reportError(w, r, err)
893 return
894 }
895
896 var courses []*orm.Course
897 err = db.
898 Model(&orm.Course{}).
899 Preload("Owner").
900 Joins("JOIN player_follow ON course.uploader = player_follow.followed_pid").
901 Where("player_follow.pid = ? and course.deleted = False", user.ID).
902 Order("course.time_uploaded DESC").
903 Offset(offset).
904 Limit(size).
905 Find(&courses).
906 Error
907 if err != nil {
908 reportError(w, r, err)
909 return
910 }
911
912 w.Header().Set("Content-Type", "application/json")
913 err = json.NewEncoder(w).Encode(courses)
914 if err != nil {
915 reportError(w, r, err)
916 return
917 }
918 }).Methods("GET")
919
920 // Bookmarks are just like follows
921 Secure("/api/bookmarks", func(w http.ResponseWriter, r *http.Request, user orm.User) {
922 var bookmarks []orm.Bookmark
923 err := db.
924 Model(&orm.Bookmark{}).
925 Where("pid = ?", user.ID).
926 Order("id DESC").
927 Find(&bookmarks).
928 Error
929 if err != nil {
930 reportError(w, r, err)
931 return
932 }
933
934 w.Header().Set("Content-Type", "application/json")
935 err = json.NewEncoder(w).Encode(bookmarks)
936 if err != nil {
937 reportError(w, r, err)
938 return
939 }
940 }).Methods("GET")
941
942 Secure("/api/bookmarks/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
943 dataId, err := parseDataId(mux.Vars(r)["data_id"])
944 if err != nil {
945 reportError(w, r, err)
946 return
947 }
948
949 bookmark := orm.Bookmark{
950 PlayerID: orm.MakerID(user.ID),
951 CourseID: orm.CourseID(dataId),
952 }
953
954 // Upsert the bookmark
955 err = db.
956 Where("pid = ? and data_id = ?", bookmark.PlayerID, bookmark.CourseID).
957 Assign(bookmark).
958 FirstOrCreate(&bookmark).
959 Error
960 if err != nil {
961 reportError(w, r, err)
962 return
963 }
964
965 w.Header().Set("Content-Type", "application/json")
966 err = json.NewEncoder(w).Encode(bookmark)
967 if err != nil {
968 reportError(w, r, err)
969 return
970 }
971 }).Methods("PUT")
972
973 Secure("/api/bookmarks/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
974 dataId, err := parseDataId(mux.Vars(r)["data_id"])
975 if err != nil {
976 reportError(w, r, err)
977 return
978 }
979
980 err = db.
981 Where("pid = ? and data_id = ?", user.ID, dataId).
982 Delete(&orm.Bookmark{}).
983 Error
984 if err != nil {
985 reportError(w, r, err)
986 return
987 }
988
989 w.WriteHeader(http.StatusNoContent)
990 }).Methods("DELETE")
991
992 Secure("/api/bookmarks/courses", func(w http.ResponseWriter, r *http.Request, user orm.User) {
993 size, offset, err := parseRange(r.URL.Query())
994 if err != nil {
995 reportError(w, r, err)
996 return
997 }
998
999 var courses []*orm.Course
1000 err = db.
1001 Model(&orm.Course{}).
1002 Preload("Owner").
1003 Joins("JOIN bookmark ON course.data_id = bookmark.data_id").
1004 Where("bookmark.pid = ? and course.deleted = False", user.ID).
1005 Order("bookmark.id DESC").
1006 Offset(offset).
1007 Limit(size).
1008 Find(&courses).
1009 Error
1010 if err != nil {
1011 reportError(w, r, err)
1012 return
1013 }
1014
1015 err = PopulateCourseCommentCounts(courses)
1016 if err != nil {
1017 reportError(w, r, err)
1018 return
1019 }
1020
1021 w.Header().Set("Content-Type", "application/json")
1022 err = json.NewEncoder(w).Encode(courses)
1023 if err != nil {
1024 reportError(w, r, err)
1025 return
1026 }
1027 }).Methods("GET")
1028
1029 Secure("/api/follows/makers", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1030 size, offset, err := parseRange(r.URL.Query())
1031 if err != nil {
1032 reportError(w, r, err)
1033 return
1034 }
1035
1036 var makers []orm.Maker
1037 err = db.
1038 Model(&orm.Maker{}).
1039 Joins("JOIN player_follow ON user_info.id = player_follow.followed_pid").
1040 Where("player_follow.pid = ?", user.ID).
1041 Order("player_follow.id DESC").
1042 Offset(offset).
1043 Limit(size).
1044 Find(&makers).
1045 Error
1046 if err != nil {
1047 reportError(w, r, err)
1048 return
1049 }
1050
1051 w.Header().Set("Content-Type", "application/json")
1052 err = json.NewEncoder(w).Encode(makers)
1053 if err != nil {
1054 reportError(w, r, err)
1055 return
1056 }
1057 }).Methods("GET")
1058
1059 Secure("/api/makers/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1060 // Read the Maker model from the body.
1061 // Verify the user is the maker, or the user is an admin.
1062 // Update the maker info.
1063
1064 pid, err := parsePid(mux.Vars(r)["pid"])
1065 if err != nil {
1066 reportError(w, r, err)
1067 return
1068 }
1069
1070 if uint64(pid) != uint64(user.ID) && !user.Role.Admin {
1071 reportError(w, r, errors.New("you do not have permission to edit this maker"))
1072 return
1073 }
1074
1075 var maker orm.Maker
1076 err = json.NewDecoder(r.Body).Decode(&maker)
1077 if err != nil {
1078 reportError(w, r, err)
1079 return
1080 }
1081
1082 // Verify the username is not taken
1083 var count int64
1084 err = db.
1085 Model(&orm.Maker{}).
1086 Where("username = ? and id != ?", maker.Username, pid).
1087 Count(&count).
1088 Error
1089 if err != nil {
1090 reportError(w, r, err)
1091 return
1092 }
1093
1094 if count > 0 {
1095 reportError(w, r, errors.New("username already taken"))
1096 return
1097 }
1098
1099 // [jneen] extra db query here - we need the Maker in the db for the current
1100 // user. This is only to check ReadOnly and ShadowBan, which are on Maker and
1101 // not User.
1102 var userMaker orm.Maker
1103 result := db.Model(&userMaker).Where("id = ?", pid).First(&userMaker)
1104 if result.Error != nil {
1105 reportError(w, r, result.Error)
1106 return
1107 }
1108
1109 if !user.Role.Admin && (userMaker.ReadOnly || userMaker.ShadowBan) {
1110 reportError(w, r, errors.New("too many username changes."))
1111 return
1112 }
1113
1114 q := db.Model(&orm.Maker{}).Where("id = ?", pid)
1115 if user.Role.Admin {
1116 // Allow admins to edit shadow_ban, comments_enabled, tags_enabled, read_only
1117 q = q.
1118 Update("shadow_ban", maker.ShadowBan).
1119 Update("comments_enabled", maker.CommentsEnabled).
1120 Update("tags_enabled", maker.TagsEnabled).
1121 Update("read_only", maker.ReadOnly)
1122 }
1123
1124 if maker.Username != "" {
1125 q = q.Update("username", strings.TrimSpace(strings.ToValidUTF8(maker.Username, "⍰")))
1126 }
1127
1128 err = q.Error
1129 if err != nil {
1130 reportError(w, r, err)
1131 return
1132 }
1133
1134 w.WriteHeader(http.StatusNoContent)
1135 }).Methods("PUT")
1136
1137 Secure("/api/ninji_clear_video/{replayId}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1138 replayId, err := strconv.ParseUint(mux.Vars(r)["replayId"], 0, 64)
1139 if err != nil {
1140 reportError(w, r, err)
1141 return
1142 }
1143
1144 var param PutClearVideoParam
1145 err = json.NewDecoder(r.Body).Decode(¶m)
1146 if err != nil {
1147 reportError(w, r, err)
1148 return
1149 }
1150
1151 if param.Link != "" {
1152 u, err := url.ParseRequestURI(param.Link)
1153 if err != nil {
1154 reportError(w, r, err)
1155 return
1156 }
1157 valid := false
1158 for _, scheme := range []string{"http", "https"} {
1159 if u.Scheme == scheme {
1160 valid = true
1161 }
1162 }
1163 if !valid {
1164 reportError(w, r, fmt.Errorf("invalid url scheme"))
1165 return
1166 }
1167 }
1168
1169 q := db.Model(&orm.NinjiTime{})
1170 if user.Role.Admin {
1171 q = q.Where("replay_id = ?", replayId)
1172 } else {
1173 q = q.Where("replay_id = ? AND pid = ?", replayId, user.ID)
1174 }
1175 q = q.Update("clear_video", param.Link)
1176
1177 err = q.Error
1178 if err != nil {
1179 reportError(w, r, err)
1180 return
1181 }
1182
1183 w.WriteHeader(http.StatusNoContent)
1184 }).Methods("PUT")
1185
1186 // PUT /api/courses/{data_id}
1187 // Update the course
1188 Secure("/api/courses/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1189 dataID, err := parsePid(mux.Vars(r)["data_id"])
1190 if err != nil {
1191 reportError(w, r, err)
1192 return
1193 }
1194
1195 // Copy the body into a buffer so we can read it twice
1196 var bodyBuffer bytes.Buffer
1197 tee := io.TeeReader(r.Body, &bodyBuffer)
1198
1199 // Parse the body into map[string]interface{} to allow partial updates
1200 var body map[string]interface{}
1201 err = json.NewDecoder(tee).Decode(&body)
1202 if err != nil {
1203 reportError(w, r, err)
1204 return
1205 }
1206
1207 var course orm.Course
1208 err = json.NewDecoder(&bodyBuffer).Decode(&course)
1209 if err != nil {
1210 reportError(w, r, err)
1211 return
1212 }
1213
1214 // Verify the course exists
1215 var existing orm.Course
1216 err = db.
1217 Model(&orm.Course{}).
1218 Where("data_id = ?", uint64(dataID)).
1219 First(&existing).
1220 Error
1221 if err != nil {
1222 reportError(w, r, fmt.Errorf("failed to find course: %w", err))
1223 return
1224 }
1225
1226 // Verify the user is the maker, or the user is an admin.
1227 if uint64(existing.OwnerID) != uint64(user.ID) && !user.Role.Admin {
1228 fmt.Printf("user %d is not the owner of course %d (%d)\n", user.ID, dataID, course.OwnerID)
1229 reportError(w, r, errors.New("you do not have permission to edit this course"))
1230 return
1231 }
1232
1233 // Update the course
1234 q := db.
1235 Model(&orm.Course{}).
1236 Where("data_id = ?", dataID)
1237
1238 if course.Title != "" {
1239 q = q.Update("name", strings.TrimSpace(strings.ToValidUTF8(course.Title, "⍰")))
1240 }
1241 if course.Description != "" {
1242 q = q.Update("description", strings.TrimSpace(strings.ToValidUTF8(course.Description, "⍰")))
1243 }
1244
1245 // update endless flag
1246 q = q.Update("endless", course.Endless)
1247
1248 // update the tags
1249 q = q.Update("tag1", course.Tag1)
1250 q = q.Update("tag2", course.Tag2)
1251
1252 // See if deleted is specified in the body
1253 if _, ok := body["deleted"]; ok {
1254 q = q.Update("deleted", course.Deleted)
1255 }
1256
1257 err = q.Error
1258 if err != nil {
1259 reportError(w, r, err)
1260 return
1261 }
1262
1263 w.WriteHeader(http.StatusNoContent)
1264 }).Methods("PUT")
1265
1266 // DELETE /api/courses/{data_id}
1267 // Delete the course
1268 Secure("/api/courses/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1269 dataID, err := parsePid(mux.Vars(r)["data_id"])
1270 if err != nil {
1271 reportError(w, r, err)
1272 return
1273 }
1274
1275 // Verify the course exists
1276 var existing orm.Course
1277 err = db.
1278 Model(&orm.Course{}).
1279 Where("data_id = ?", uint64(dataID)).
1280 First(&existing).
1281 Error
1282 if err != nil {
1283 reportError(w, r, fmt.Errorf("failed to find course: %w", err))
1284 return
1285 }
1286
1287 // Verify the user is the maker, or the user is an admin.
1288 if uint64(existing.OwnerID) != uint64(user.ID) && !user.Role.Admin {
1289 fmt.Printf("user %d is not the owner of course %d (%d)\n", user.ID, dataID, existing.OwnerID)
1290 reportError(w, r, errors.New("you do not have permission to edit this course"))
1291 return
1292 }
1293
1294 // Delete the course by setting delete=true
1295 err = db.
1296 Model(&orm.Course{}).
1297 Where("data_id = ?", dataID).
1298 Update("deleted", true).
1299 Error
1300 if err != nil {
1301 reportError(w, r, err)
1302 return
1303 }
1304
1305 w.WriteHeader(http.StatusNoContent)
1306 }).Methods("DELETE")
1307
1308 // Replicate the TGR API:
1309 // GET /mm2/level_info/{data_id}
1310 Insecure("/mm2/level_info/{data_id}", func(w http.ResponseWriter, r *http.Request) {
1311 fmt.Println("mm2/level_info")
1312 dataID, err := parsePid(mux.Vars(r)["data_id"])
1313 if err != nil {
1314 reportError(w, r, err)
1315 return
1316 }
1317
1318 // Get the course
1319 var course orm.Course
1320 err = db.
1321 Model(&orm.Course{}).
1322 Where("data_id = ?", uint64(dataID)).
1323 First(&course).
1324 Error
1325 if err != nil {
1326 reportError(w, r, fmt.Errorf("failed to find course: %w", err))
1327 return
1328 }
1329
1330 // Get the maker
1331 var maker orm.Maker
1332 err = db.
1333 Model(&orm.Maker{}).
1334 Where("id = ?", course.OwnerID).
1335 First(&maker).
1336 Error
1337 if err != nil {
1338 reportError(w, r, fmt.Errorf("failed to find maker: %w", err))
1339 return
1340 }
1341
1342 tgrApiCourse := TgrApiCourse{
1343 Name: course.Title,
1344 Description: course.Description,
1345 UploadedPretty: course.Created.Format("2006-01-02 15:04:05"),
1346 Uploaded: course.Created.Unix(),
1347 GameStyleName: course.Style.String(),
1348 GameStyle: int(course.Style),
1349 ThemeName: course.Theme.String(),
1350 Theme: int(course.Theme),
1351 CourseId: mux.Vars(r)["data_id"],
1352 Uploader: &TgrApiMaker{
1353 Name: maker.Username,
1354 MiiImage: maker.ImageUrl,
1355 // Code: maker.ID.String(),
1356 },
1357 }
1358
1359 w.Header().Set("Content-Type", "application/json")
1360 err = json.NewEncoder(w).Encode(tgrApiCourse)
1361 if err != nil {
1362 reportError(w, r, err)
1363 return
1364 }
1365 }).Methods("GET")
1366
1367 // Insecure("/mm2/level_data/{data_id}", dataView.ServeLevelData).Methods("GET")
1368 // GET /mm2/level_data/{data_id}
1369 Insecure("/mm2/level_data/{data_id}", func(w http.ResponseWriter, r *http.Request) {
1370 fmt.Println("mm2/level_data")
1371 dataID, err := parsePid(mux.Vars(r)["data_id"])
1372 if err != nil {
1373 reportError(w, r, err)
1374 return
1375 }
1376
1377 // Get the course data
1378 var courseData orm.CourseData
1379 err = db.
1380 Model(&orm.CourseData{}).
1381 Where("data_id = ?", uint64(dataID)).
1382 First(&courseData).
1383 Error
1384 if err != nil {
1385 reportError(w, r, fmt.Errorf("failed to find course data: %w", err))
1386 return
1387 }
1388
1389 bytes, err := courseData.GetBCD()
1390 if err != nil {
1391 reportError(w, r, err)
1392 return
1393 }
1394
1395 // Send the binary level data
1396 w.Header().Set("Content-Type", "application/octet-stream")
1397 _, err = w.Write(bytes)
1398 if err != nil {
1399 reportError(w, r, err)
1400 return
1401 }
1402 }).Methods("GET")
1403
1404 // GET /api/user
1405 // Get the current user
1406 Secure("/api/user", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1407 w.Header().Set("Content-Type", "application/json")
1408 err := json.NewEncoder(w).Encode(user)
1409 if err != nil {
1410 reportError(w, r, err)
1411 return
1412 }
1413 }).Methods("GET")
1414
1415 // PATCH /api/user
1416 // Update the current user
1417 Secure("/api/user", func(w http.ResponseWriter, r *http.Request, user orm.User) {
1418 var patch orm.User
1419 err := json.NewDecoder(r.Body).Decode(&patch)
1420 if err != nil {
1421 reportError(w, r, err)
1422 return
1423 }
1424
1425 // Apply updates
1426 p := db.Model(&user)
1427
1428 if patch.ShowBookmark != nil {
1429 p = p.Update("show_bookmark", *patch.ShowBookmark)
1430 }
1431
1432 err = p.Error
1433
1434 if err != nil {
1435 reportError(w, r, err)
1436 return
1437 }
1438
1439 w.Header().Set("Content-Type", "application/json")
1440 err = json.NewEncoder(w).Encode(user)
1441 if err != nil {
1442 reportError(w, r, err)
1443 return
1444 }
1445 }).Methods("PATCH")
1446}