package api import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "smm2_gameserver/nex/id/code" "smm2_gameserver/orm" "strconv" "strings" "time" "github.com/gorilla/mux" "gorm.io/gorm" ) func PopulateCourseCommentCounts(courses []*orm.Course) error { if len(courses) == 0 { return nil } course_ids := make([]orm.CourseID, len(courses)) for i, course := range courses { course_ids[i] = course.ID } rows, err := db. Select("data_id, count(*) as comment_count"). Table("course_comment"). Where("data_id in ?", course_ids). Group("data_id"). Rows() if err != nil { return err } index := make(map[orm.CourseID]int) for rows.Next() { var id orm.CourseID var count int rows.Scan(&id, &count) index[id] = count } if len(index) == 0 { return nil } for _, course := range courses { course.CommentCount = index[course.ID] } return nil } func SearchCoursesByType(w http.ResponseWriter, r *http.Request, user orm.User, typeBy string) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var courses []*orm.Course err = db. Where(fmt.Sprintf("%s = ?", typeBy), pid). Offset(offset). Limit(size). Find(&courses). Error if err != nil { reportError(w, r, err) return } err = PopulateCourseCommentCounts(courses) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(courses) } type NinjiLeaderboardEntry struct { ID uint64 Rank int Username string ImageURL string LoginProvider string Time int TimeObtained time.Time ReplayID orm.BigInt ClearVideo string } type PutClearVideoParam struct { Link string } func initDatastore() { Secure("/api/user_info/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { info, err := dataView.GetUserInfoByCode(stateForUser(user), mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(info) return } info, err := dataView.GetUserInfoByPid(stateForUser(user), pid, 0) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(info) }).Methods("GET") Insecure("/api/courses/{pid}", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } var course orm.Course err = db. Preload("Owner"). Preload("PlayerResults", func(db *gorm.DB) *gorm.DB { return db. Where("is_world_record = true or is_first_clear = true"). Order("is_world_record DESC"). Limit(2) }). Where("data_id = ?", pid). First(&course). Error if err != nil { reportError(w, r, err) return } err = PopulateCourseCommentCounts([]*orm.Course{&course}) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(course) if err != nil { reportError(w, r, err) return } }).Methods("GET") InsecureOpt("/api/courses/{code}/comments", func(w http.ResponseWriter, r *http.Request, user *orm.User) { code, err := parsePid(mux.Vars(r)["code"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil{ reportError(w, r, err) return } var comments []orm.Comment query := db. Where("data_id = ?", code). Joins("Image"). Order("time_posted DESC"). Offset(offset). Limit(size) if user != nil && user.Role.Admin { query = query.InnerJoins("Commenter") } else { query = query.InnerJoins("Commenter", db.Where("NOT shadow_ban")) } err = query.Find(&comments).Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(comments) if err != nil { reportError(w, r, err) return } }) Insecure("/api/courses/{code}/plays", func(w http.ResponseWriter, r *http.Request) { code, err := parsePid(mux.Vars(r)["code"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var plays []orm.PlayerResult err = db. Preload("Player"). Where("data_id = ?", code). Order("is_world_record DESC, cleared DESC, best_time ASC, time_obtained DESC"). Offset(offset). Limit(size). Find(&plays). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(plays) if err != nil { reportError(w, r, err) return } }).Methods("GET") Secure("/api/course_info/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) { dataId, err := parseDataId(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } info, err := dataView.GetCourseInfo(stateForUser(user), dataId) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(info) }).Methods("GET") Insecure("/api/makers/{pid}/courses", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var courses []*orm.Course err = db. Model(&orm.Course{}). Preload("Owner"). Where("uploader = ? AND deleted = False", pid). Order("time_uploaded DESC"). Offset(offset). Limit(size). Find(&courses). Error if err != nil { reportError(w, r, err) return } err = PopulateCourseCommentCounts(courses) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(courses) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}/comments", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var comments []orm.Comment err = db. Preload("Image"). Preload("Commenter"). Preload("Course.Owner"). Where("pid = ?", pid). Order("time_posted DESC"). Offset(offset). Limit(size). Find(&comments). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(comments) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/latest/courses", func(w http.ResponseWriter, r *http.Request) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var courses []*orm.Course err = db. Model(&orm.Course{}). Preload("Owner"). Where("deleted = False"). Order("id DESC"). Offset(offset). Limit(size). Find(&courses). Error if err != nil { reportError(w, r, err) return } err = PopulateCourseCommentCounts(courses) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(courses) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/uncleared/courses", func(w http.ResponseWriter, r *http.Request) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var courses []*orm.Course err = db. Model(&orm.Course{}). Preload("Owner"). Where("clears = 0 AND deleted = False"). Order("id DESC"). Offset(offset). Limit(size). Find(&courses). Error if err != nil { reportError(w, r, err) return } err = PopulateCourseCommentCounts(courses) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(courses) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/ninjis", func(w http.ResponseWriter, r *http.Request) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var ninjis []*orm.Ninji err = db. Model(&orm.Ninji{}). Preload("Course"). Preload("Course.Owner"). Where("deleted = False"). Order("id DESC"). Offset(offset). Limit(size). Find(&ninjis). Error if err != nil { reportError(w, r, err) return } if len(ninjis) > 0 { courses := make([]*orm.Course, len(ninjis)) for i, ninji := range ninjis { courses[i] = ninji.Course } err = PopulateCourseCommentCounts(courses) if err != nil { reportError(w, r, err) return } } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(ninjis) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/ninjis/{id}", func(w http.ResponseWriter, r *http.Request) { id, err := parsePid(mux.Vars(r)["id"]) if err != nil { reportError(w, r, err) return } var ninji orm.Ninji err = db. Model(&orm.Ninji{}). Preload("Course"). Preload("Course.Owner"). Where("data_id = ? AND deleted = False", id). First(&ninji). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(ninji) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/ninjis-leaderboard/{id}", func(w http.ResponseWriter, r *http.Request) { id, err := parsePid(mux.Vars(r)["id"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var results []NinjiLeaderboardEntry 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) out := []map[string]any{} for _, result := range results { out = append(out, map[string]any{ "rank": result.Rank, "time": result.Time, "time_obtained": result.TimeObtained.Format("2006-01-02 15:04"), "id": result.ReplayID, "link": result.ClearVideo, "player": map[string]any{ "username": result.Username, "id": code.IdToMakerCode(result.ID), "image_url": result.ImageURL, "login_provider": result.LoginProvider, }, }) } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(out) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}/world-records", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var playerResults []orm.PlayerResult err = db. Model(&orm.PlayerResult{}). Preload("Course"). Preload("Course.Owner"). Where("pid = ? and is_world_record = true", pid). Order("time_obtained DESC"). Offset(offset). Limit(size). Find(&playerResults). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(playerResults) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}/played", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var playerResults []orm.PlayerResult err = db. Model(&orm.PlayerResult{}). Preload("Course"). Preload("Course.Owner"). Where("pid = ?", pid). Order("id DESC"). Offset(offset). Limit(size). Find(&playerResults). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(playerResults) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}/liked", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var results []orm.PlayerLikeInfo err = db. Model(&orm.PlayerLikeInfo{}). Preload("Course"). Preload("Course.Owner"). Where("pid = ? AND liked = True", pid). Order("id DESC"). Offset(offset). Limit(size). Find(&results). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(results) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}/follow", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var makers []orm.Maker err = db. Model(&orm.Maker{}). Joins("JOIN player_follow ON user_info.id = player_follow.followed_pid"). Where("player_follow.pid = ?", pid). Order("player_follow.id DESC"). Offset(offset). Limit(size). Find(&makers). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(makers) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}/first-clears", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var playerResults []orm.PlayerResult err = db. Model(&orm.PlayerResult{}). Preload("Course"). Preload("Course.Owner"). Where("pid = ? and is_first_clear = true", pid). Order("time_obtained DESC"). Offset(offset). Limit(size). Find(&playerResults). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(playerResults) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers/{pid}", func(w http.ResponseWriter, r *http.Request) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } var maker orm.Maker err = db. Model(&orm.Maker{}). Preload("Mii"). Preload("Stats"). Where("id = ?", pid). First(&maker). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(maker) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/makers", func(w http.ResponseWriter, r *http.Request) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var makers []orm.Maker err = db. Model(&orm.Maker{}). Preload("Mii"). Preload("Stats"). Where("last_active IS NOT NULL and shadow_ban = False"). Order("last_active desc, id desc"). Offset(offset). Limit(size). Find(&makers). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(makers) if err != nil { reportError(w, r, err) return } }).Methods("GET") Insecure("/api/one_screen_thumbnail/{data_id}", func(w http.ResponseWriter, r *http.Request) { dataId, err := parseDataId(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } var oneScreenThumbnails []*[]byte err = db. Model(&orm.CourseData{}). Where("data_id = ?", dataId). Pluck("one_screen_thumbnail", &oneScreenThumbnails). Error if err != nil { reportError(w, r, err) return } if len(oneScreenThumbnails) == 0 || oneScreenThumbnails[0] == nil || len(*oneScreenThumbnails[0]) == 0 { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Cache-Control", "public, max-age=31536000") w.Header().Set("Content-Type", "image/jpeg") w.Write(*oneScreenThumbnails[0]) }).Methods("GET") Secure("/api/liked_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { SearchCoursesByType(w, r, user, "liked_by") }) Secure("/api/played_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { SearchCoursesByType(w, r, user, "played_by") }) Secure("/api/first_clear_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { SearchCoursesByType(w, r, user, "first_clear_by") }) Secure("/api/world_record_by/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { SearchCoursesByType(w, r, user, "world_record_by") }) Secure("/api/follows", func(w http.ResponseWriter, r *http.Request, user orm.User) { var follows []orm.PlayerFollow err := db. Model(&orm.PlayerFollow{}). Where("pid = ?", user.ID). Order("id DESC"). Find(&follows). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(follows) if err != nil { reportError(w, r, err) return } }).Methods("GET") Secure("/api/follows/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } follow := orm.PlayerFollow{ FollowerID: orm.MakerID(user.ID), FollowedID: orm.MakerID(pid), } // Upsert the follow err = db. Where("pid = ? and followed_pid = ?", follow.FollowerID, follow.FollowedID). Assign(follow). FirstOrCreate(&follow). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(follow) if err != nil { reportError(w, r, err) return } }).Methods("PUT") Secure("/api/follows/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } err = db. Where("pid = ? and followed_pid = ?", user.ID, pid). Delete(&orm.PlayerFollow{}). Error if err != nil { reportError(w, r, err) return } w.WriteHeader(http.StatusNoContent) }).Methods("DELETE") Secure("/api/follows/courses", func(w http.ResponseWriter, r *http.Request, user orm.User) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var courses []*orm.Course err = db. Model(&orm.Course{}). Preload("Owner"). Joins("JOIN player_follow ON course.uploader = player_follow.followed_pid"). Where("player_follow.pid = ? and course.deleted = False", user.ID). Order("course.time_uploaded DESC"). Offset(offset). Limit(size). Find(&courses). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(courses) if err != nil { reportError(w, r, err) return } }).Methods("GET") // Bookmarks are just like follows Secure("/api/bookmarks", func(w http.ResponseWriter, r *http.Request, user orm.User) { var bookmarks []orm.Bookmark err := db. Model(&orm.Bookmark{}). Where("pid = ?", user.ID). Order("id DESC"). Find(&bookmarks). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(bookmarks) if err != nil { reportError(w, r, err) return } }).Methods("GET") Secure("/api/bookmarks/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) { dataId, err := parseDataId(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } bookmark := orm.Bookmark{ PlayerID: orm.MakerID(user.ID), CourseID: orm.CourseID(dataId), } // Upsert the bookmark err = db. Where("pid = ? and data_id = ?", bookmark.PlayerID, bookmark.CourseID). Assign(bookmark). FirstOrCreate(&bookmark). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(bookmark) if err != nil { reportError(w, r, err) return } }).Methods("PUT") Secure("/api/bookmarks/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) { dataId, err := parseDataId(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } err = db. Where("pid = ? and data_id = ?", user.ID, dataId). Delete(&orm.Bookmark{}). Error if err != nil { reportError(w, r, err) return } w.WriteHeader(http.StatusNoContent) }).Methods("DELETE") Secure("/api/bookmarks/courses", func(w http.ResponseWriter, r *http.Request, user orm.User) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var courses []*orm.Course err = db. Model(&orm.Course{}). Preload("Owner"). Joins("JOIN bookmark ON course.data_id = bookmark.data_id"). Where("bookmark.pid = ? and course.deleted = False", user.ID). Order("bookmark.id DESC"). Offset(offset). Limit(size). Find(&courses). Error if err != nil { reportError(w, r, err) return } err = PopulateCourseCommentCounts(courses) if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(courses) if err != nil { reportError(w, r, err) return } }).Methods("GET") Secure("/api/follows/makers", func(w http.ResponseWriter, r *http.Request, user orm.User) { size, offset, err := parseRange(r.URL.Query()) if err != nil { reportError(w, r, err) return } var makers []orm.Maker err = db. Model(&orm.Maker{}). Joins("JOIN player_follow ON user_info.id = player_follow.followed_pid"). Where("player_follow.pid = ?", user.ID). Order("player_follow.id DESC"). Offset(offset). Limit(size). Find(&makers). Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(makers) if err != nil { reportError(w, r, err) return } }).Methods("GET") Secure("/api/makers/{pid}", func(w http.ResponseWriter, r *http.Request, user orm.User) { // Read the Maker model from the body. // Verify the user is the maker, or the user is an admin. // Update the maker info. pid, err := parsePid(mux.Vars(r)["pid"]) if err != nil { reportError(w, r, err) return } if uint64(pid) != uint64(user.ID) && !user.Role.Admin { reportError(w, r, errors.New("you do not have permission to edit this maker")) return } var maker orm.Maker err = json.NewDecoder(r.Body).Decode(&maker) if err != nil { reportError(w, r, err) return } // Verify the username is not taken var count int64 err = db. Model(&orm.Maker{}). Where("username = ? and id != ?", maker.Username, pid). Count(&count). Error if err != nil { reportError(w, r, err) return } if count > 0 { reportError(w, r, errors.New("username already taken")) return } // [jneen] extra db query here - we need the Maker in the db for the current // user. This is only to check ReadOnly and ShadowBan, which are on Maker and // not User. var userMaker orm.Maker result := db.Model(&userMaker).Where("id = ?", pid).First(&userMaker) if result.Error != nil { reportError(w, r, result.Error) return } if !user.Role.Admin && (userMaker.ReadOnly || userMaker.ShadowBan) { reportError(w, r, errors.New("too many username changes.")) return } q := db.Model(&orm.Maker{}).Where("id = ?", pid) if user.Role.Admin { // Allow admins to edit shadow_ban, comments_enabled, tags_enabled, read_only q = q. Update("shadow_ban", maker.ShadowBan). Update("comments_enabled", maker.CommentsEnabled). Update("tags_enabled", maker.TagsEnabled). Update("read_only", maker.ReadOnly) } if maker.Username != "" { q = q.Update("username", strings.TrimSpace(strings.ToValidUTF8(maker.Username, "⍰"))) } err = q.Error if err != nil { reportError(w, r, err) return } w.WriteHeader(http.StatusNoContent) }).Methods("PUT") Secure("/api/ninji_clear_video/{replayId}", func(w http.ResponseWriter, r *http.Request, user orm.User) { replayId, err := strconv.ParseUint(mux.Vars(r)["replayId"], 0, 64) if err != nil { reportError(w, r, err) return } var param PutClearVideoParam err = json.NewDecoder(r.Body).Decode(¶m) if err != nil { reportError(w, r, err) return } if param.Link != "" { u, err := url.ParseRequestURI(param.Link) if err != nil { reportError(w, r, err) return } valid := false for _, scheme := range []string{"http", "https"} { if u.Scheme == scheme { valid = true } } if !valid { reportError(w, r, fmt.Errorf("invalid url scheme")) return } } q := db.Model(&orm.NinjiTime{}) if user.Role.Admin { q = q.Where("replay_id = ?", replayId) } else { q = q.Where("replay_id = ? AND pid = ?", replayId, user.ID) } q = q.Update("clear_video", param.Link) err = q.Error if err != nil { reportError(w, r, err) return } w.WriteHeader(http.StatusNoContent) }).Methods("PUT") // PUT /api/courses/{data_id} // Update the course Secure("/api/courses/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) { dataID, err := parsePid(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } // Copy the body into a buffer so we can read it twice var bodyBuffer bytes.Buffer tee := io.TeeReader(r.Body, &bodyBuffer) // Parse the body into map[string]interface{} to allow partial updates var body map[string]interface{} err = json.NewDecoder(tee).Decode(&body) if err != nil { reportError(w, r, err) return } var course orm.Course err = json.NewDecoder(&bodyBuffer).Decode(&course) if err != nil { reportError(w, r, err) return } // Verify the course exists var existing orm.Course err = db. Model(&orm.Course{}). Where("data_id = ?", uint64(dataID)). First(&existing). Error if err != nil { reportError(w, r, fmt.Errorf("failed to find course: %w", err)) return } // Verify the user is the maker, or the user is an admin. if uint64(existing.OwnerID) != uint64(user.ID) && !user.Role.Admin { fmt.Printf("user %d is not the owner of course %d (%d)\n", user.ID, dataID, course.OwnerID) reportError(w, r, errors.New("you do not have permission to edit this course")) return } // Update the course q := db. Model(&orm.Course{}). Where("data_id = ?", dataID) if course.Title != "" { q = q.Update("name", strings.TrimSpace(strings.ToValidUTF8(course.Title, "⍰"))) } if course.Description != "" { q = q.Update("description", strings.TrimSpace(strings.ToValidUTF8(course.Description, "⍰"))) } // update endless flag q = q.Update("endless", course.Endless) // update the tags q = q.Update("tag1", course.Tag1) q = q.Update("tag2", course.Tag2) // See if deleted is specified in the body if _, ok := body["deleted"]; ok { q = q.Update("deleted", course.Deleted) } err = q.Error if err != nil { reportError(w, r, err) return } w.WriteHeader(http.StatusNoContent) }).Methods("PUT") // DELETE /api/courses/{data_id} // Delete the course Secure("/api/courses/{data_id}", func(w http.ResponseWriter, r *http.Request, user orm.User) { dataID, err := parsePid(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } // Verify the course exists var existing orm.Course err = db. Model(&orm.Course{}). Where("data_id = ?", uint64(dataID)). First(&existing). Error if err != nil { reportError(w, r, fmt.Errorf("failed to find course: %w", err)) return } // Verify the user is the maker, or the user is an admin. if uint64(existing.OwnerID) != uint64(user.ID) && !user.Role.Admin { fmt.Printf("user %d is not the owner of course %d (%d)\n", user.ID, dataID, existing.OwnerID) reportError(w, r, errors.New("you do not have permission to edit this course")) return } // Delete the course by setting delete=true err = db. Model(&orm.Course{}). Where("data_id = ?", dataID). Update("deleted", true). Error if err != nil { reportError(w, r, err) return } w.WriteHeader(http.StatusNoContent) }).Methods("DELETE") // Replicate the TGR API: // GET /mm2/level_info/{data_id} Insecure("/mm2/level_info/{data_id}", func(w http.ResponseWriter, r *http.Request) { fmt.Println("mm2/level_info") dataID, err := parsePid(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } // Get the course var course orm.Course err = db. Model(&orm.Course{}). Where("data_id = ?", uint64(dataID)). First(&course). Error if err != nil { reportError(w, r, fmt.Errorf("failed to find course: %w", err)) return } // Get the maker var maker orm.Maker err = db. Model(&orm.Maker{}). Where("id = ?", course.OwnerID). First(&maker). Error if err != nil { reportError(w, r, fmt.Errorf("failed to find maker: %w", err)) return } tgrApiCourse := TgrApiCourse{ Name: course.Title, Description: course.Description, UploadedPretty: course.Created.Format("2006-01-02 15:04:05"), Uploaded: course.Created.Unix(), GameStyleName: course.Style.String(), GameStyle: int(course.Style), ThemeName: course.Theme.String(), Theme: int(course.Theme), CourseId: mux.Vars(r)["data_id"], Uploader: &TgrApiMaker{ Name: maker.Username, MiiImage: maker.ImageUrl, // Code: maker.ID.String(), }, } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(tgrApiCourse) if err != nil { reportError(w, r, err) return } }).Methods("GET") // Insecure("/mm2/level_data/{data_id}", dataView.ServeLevelData).Methods("GET") // GET /mm2/level_data/{data_id} Insecure("/mm2/level_data/{data_id}", func(w http.ResponseWriter, r *http.Request) { fmt.Println("mm2/level_data") dataID, err := parsePid(mux.Vars(r)["data_id"]) if err != nil { reportError(w, r, err) return } // Get the course data var courseData orm.CourseData err = db. Model(&orm.CourseData{}). Where("data_id = ?", uint64(dataID)). First(&courseData). Error if err != nil { reportError(w, r, fmt.Errorf("failed to find course data: %w", err)) return } bytes, err := courseData.GetBCD() if err != nil { reportError(w, r, err) return } // Send the binary level data w.Header().Set("Content-Type", "application/octet-stream") _, err = w.Write(bytes) if err != nil { reportError(w, r, err) return } }).Methods("GET") // GET /api/user // Get the current user Secure("/api/user", func(w http.ResponseWriter, r *http.Request, user orm.User) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(user) if err != nil { reportError(w, r, err) return } }).Methods("GET") // PATCH /api/user // Update the current user Secure("/api/user", func(w http.ResponseWriter, r *http.Request, user orm.User) { var patch orm.User err := json.NewDecoder(r.Body).Decode(&patch) if err != nil { reportError(w, r, err) return } // Apply updates p := db.Model(&user) if patch.ShowBookmark != nil { p = p.Update("show_bookmark", *patch.ShowBookmark) } err = p.Error if err != nil { reportError(w, r, err) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(user) if err != nil { reportError(w, r, err) return } }).Methods("PATCH") }