The server for Open Course World
at main 1446 lines 33 kB view raw
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(&param) 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}