2016-08-15 17:59:46 +00:00
|
|
|
package v1
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
2018-05-03 20:28:38 +00:00
|
|
|
"encoding/json"
|
2016-08-15 18:07:40 +00:00
|
|
|
"fmt"
|
|
|
|
"strconv"
|
2017-04-18 19:08:06 +00:00
|
|
|
"strings"
|
2016-08-15 17:59:46 +00:00
|
|
|
|
2017-04-18 19:08:06 +00:00
|
|
|
"gopkg.in/thehowl/go-osuapi.v1"
|
2017-01-14 17:06:16 +00:00
|
|
|
"zxq.co/ripple/rippleapi/common"
|
2017-04-18 19:08:06 +00:00
|
|
|
"zxq.co/x/getrank"
|
2016-08-15 17:59:46 +00:00
|
|
|
)
|
|
|
|
|
2017-02-19 17:19:59 +00:00
|
|
|
// Score is a score done on Ripple.
|
|
|
|
type Score struct {
|
2016-08-19 15:02:51 +00:00
|
|
|
ID int `json:"id"`
|
|
|
|
BeatmapMD5 string `json:"beatmap_md5"`
|
|
|
|
Score int64 `json:"score"`
|
|
|
|
MaxCombo int `json:"max_combo"`
|
|
|
|
FullCombo bool `json:"full_combo"`
|
|
|
|
Mods int `json:"mods"`
|
|
|
|
Count300 int `json:"count_300"`
|
|
|
|
Count100 int `json:"count_100"`
|
|
|
|
Count50 int `json:"count_50"`
|
|
|
|
CountGeki int `json:"count_geki"`
|
|
|
|
CountKatu int `json:"count_katu"`
|
|
|
|
CountMiss int `json:"count_miss"`
|
|
|
|
Time common.UnixTimestamp `json:"time"`
|
|
|
|
PlayMode int `json:"play_mode"`
|
|
|
|
Accuracy float64 `json:"accuracy"`
|
|
|
|
PP float32 `json:"pp"`
|
2017-04-18 19:08:06 +00:00
|
|
|
Rank string `json:"rank"`
|
2016-08-19 15:02:51 +00:00
|
|
|
Completed int `json:"completed"`
|
2016-08-15 17:59:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// beatmapScore is to differentiate from userScore, as beatmapScore contains
|
|
|
|
// also an user, while userScore contains the beatmap.
|
|
|
|
type beatmapScore struct {
|
2017-02-19 17:19:59 +00:00
|
|
|
Score
|
2016-08-15 17:59:46 +00:00
|
|
|
User userData `json:"user"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type scoresResponse struct {
|
|
|
|
common.ResponseBase
|
|
|
|
Scores []beatmapScore `json:"scores"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ScoresGET retrieves the top scores for a certain beatmap.
|
|
|
|
func ScoresGET(md common.MethodData) common.CodeMessager {
|
|
|
|
var (
|
2018-07-19 13:43:58 +00:00
|
|
|
where = new(common.WhereClause)
|
|
|
|
r scoresResponse
|
2016-08-15 17:59:46 +00:00
|
|
|
)
|
2018-07-19 13:43:58 +00:00
|
|
|
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
|
2016-08-15 17:59:46 +00:00
|
|
|
switch {
|
|
|
|
case md.Query("md5") != "":
|
2018-07-19 13:43:58 +00:00
|
|
|
where.In("beatmap_md5", pm("md5")...)
|
2016-08-15 17:59:46 +00:00
|
|
|
case md.Query("b") != "":
|
2018-07-19 13:43:58 +00:00
|
|
|
var md5 string
|
|
|
|
err := md.DB.Get(&md5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", md.Query("b"))
|
2016-08-15 17:59:46 +00:00
|
|
|
switch {
|
|
|
|
case err == sql.ErrNoRows:
|
|
|
|
r.Code = 200
|
|
|
|
return r
|
|
|
|
case err != nil:
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
2018-07-19 13:43:58 +00:00
|
|
|
where.Where("beatmap_md5 = ?", md5)
|
2016-08-15 17:59:46 +00:00
|
|
|
}
|
2018-07-19 13:43:58 +00:00
|
|
|
where.In("scores.id", pm("id")...)
|
2016-08-15 17:59:46 +00:00
|
|
|
|
|
|
|
sort := common.Sort(md, common.SortConfiguration{
|
|
|
|
Default: "scores.pp DESC, scores.score DESC",
|
|
|
|
Table: "scores",
|
2016-10-29 12:27:55 +00:00
|
|
|
Allowed: []string{"pp", "score", "accuracy", "id"},
|
2016-08-15 17:59:46 +00:00
|
|
|
})
|
2018-07-19 13:58:35 +00:00
|
|
|
if where.Clause == "" {
|
|
|
|
return ErrMissingField("must specify at least one queried item")
|
|
|
|
}
|
2016-08-15 17:59:46 +00:00
|
|
|
|
2018-07-19 13:43:58 +00:00
|
|
|
where.Where(` scores.completed = '3' AND `+md.User.OnlyUserPublic(false)+` `+
|
2018-07-19 13:52:19 +00:00
|
|
|
genModeClause(md)+` `+sort+common.Paginate(md.Query("p"), md.Query("l"), 100), "FIF")
|
2018-07-19 13:43:58 +00:00
|
|
|
where.Params = where.Params[:len(where.Params)-1]
|
|
|
|
|
2016-08-15 17:59:46 +00:00
|
|
|
rows, err := md.DB.Query(`
|
|
|
|
SELECT
|
|
|
|
scores.id, scores.beatmap_md5, scores.score,
|
|
|
|
scores.max_combo, scores.full_combo, scores.mods,
|
|
|
|
scores.300_count, scores.100_count, scores.50_count,
|
|
|
|
scores.gekis_count, scores.katus_count, scores.misses_count,
|
|
|
|
scores.time, scores.play_mode, scores.accuracy, scores.pp,
|
|
|
|
scores.completed,
|
|
|
|
|
|
|
|
users.id, users.username, users.register_datetime, users.privileges,
|
|
|
|
users.latest_activity, users_stats.username_aka, users_stats.country
|
|
|
|
FROM scores
|
|
|
|
INNER JOIN users ON users.id = scores.userid
|
|
|
|
INNER JOIN users_stats ON users_stats.id = scores.userid
|
2018-07-19 13:43:58 +00:00
|
|
|
`+where.Clause, where.Params...)
|
2016-08-15 17:59:46 +00:00
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
for rows.Next() {
|
|
|
|
var (
|
|
|
|
s beatmapScore
|
|
|
|
u userData
|
|
|
|
)
|
|
|
|
err := rows.Scan(
|
2017-02-19 18:01:13 +00:00
|
|
|
&s.ID, &s.BeatmapMD5, &s.Score.Score,
|
2016-08-15 17:59:46 +00:00
|
|
|
&s.MaxCombo, &s.FullCombo, &s.Mods,
|
|
|
|
&s.Count300, &s.Count100, &s.Count50,
|
|
|
|
&s.CountGeki, &s.CountKatu, &s.CountMiss,
|
|
|
|
&s.Time, &s.PlayMode, &s.Accuracy, &s.PP,
|
|
|
|
&s.Completed,
|
|
|
|
|
|
|
|
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges,
|
|
|
|
&u.LatestActivity, &u.UsernameAKA, &u.Country,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
s.User = u
|
2017-04-18 19:08:06 +00:00
|
|
|
s.Rank = strings.ToUpper(getrank.GetRank(
|
|
|
|
osuapi.Mode(s.PlayMode),
|
|
|
|
osuapi.Mods(s.Mods),
|
|
|
|
s.Accuracy,
|
|
|
|
s.Count300,
|
|
|
|
s.Count100,
|
|
|
|
s.Count50,
|
|
|
|
s.CountMiss,
|
|
|
|
))
|
2016-08-15 17:59:46 +00:00
|
|
|
r.Scores = append(r.Scores, s)
|
|
|
|
}
|
|
|
|
r.Code = 200
|
|
|
|
return r
|
|
|
|
}
|
2016-08-15 18:07:40 +00:00
|
|
|
|
2018-05-03 20:28:38 +00:00
|
|
|
type scoreReportData struct {
|
|
|
|
ScoreID int `json:"score_id"`
|
|
|
|
Data json.RawMessage `json:"data"`
|
|
|
|
Anticheat string `json:"anticheat"`
|
|
|
|
Severity float32 `json:"severity"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type scoreReport struct {
|
|
|
|
ID int `json:"id"`
|
|
|
|
scoreReportData
|
|
|
|
}
|
|
|
|
|
|
|
|
type scoreReportResponse struct {
|
|
|
|
common.ResponseBase
|
|
|
|
scoreReport
|
|
|
|
}
|
|
|
|
|
|
|
|
// ScoreReportPOST creates a new report for a score
|
|
|
|
func ScoreReportPOST(md common.MethodData) common.CodeMessager {
|
|
|
|
var data scoreReportData
|
|
|
|
err := md.Unmarshal(&data)
|
|
|
|
if err != nil {
|
|
|
|
return ErrBadJSON
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if there are any missing fields
|
|
|
|
var missingFields []string
|
|
|
|
if data.ScoreID == 0 {
|
|
|
|
missingFields = append(missingFields, "score_id")
|
|
|
|
}
|
|
|
|
if data.Anticheat == "" {
|
|
|
|
missingFields = append(missingFields, "anticheat")
|
|
|
|
}
|
|
|
|
if len(missingFields) > 0 {
|
|
|
|
return ErrMissingField(missingFields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
tx, err := md.DB.Beginx()
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get anticheat ID
|
|
|
|
var id int
|
|
|
|
err = tx.Get(&id, "SELECT id FROM anticheats WHERE name = ? LIMIT 1", data.Anticheat)
|
|
|
|
switch err {
|
|
|
|
case nil: // carry on
|
|
|
|
case sql.ErrNoRows:
|
|
|
|
// Create anticheat!
|
|
|
|
res, err := tx.Exec("INSERT INTO anticheats (name) VALUES (?);", data.Anticheat)
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
lid, err := res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
id = int(lid)
|
|
|
|
default:
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
|
|
|
|
d := sql.NullString{String: string(data.Data), Valid: true}
|
|
|
|
if d.String == "null" || d.String == `""` ||
|
|
|
|
d.String == "[]" || d.String == "{}" || d.String == "0" {
|
|
|
|
d.Valid = false
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := tx.Exec("INSERT INTO anticheat_reports (score_id, anticheat_id, data, severity) VALUES (?, ?, ?, ?)",
|
|
|
|
data.ScoreID, id, d, data.Severity)
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
|
|
|
|
lid, err := res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit()
|
|
|
|
if err != nil {
|
|
|
|
md.Err(err)
|
|
|
|
return Err500
|
|
|
|
}
|
|
|
|
|
|
|
|
if !d.Valid {
|
|
|
|
data.Data = json.RawMessage("null")
|
|
|
|
}
|
|
|
|
|
|
|
|
repData := scoreReportResponse{
|
|
|
|
scoreReport: scoreReport{
|
|
|
|
ID: int(lid),
|
|
|
|
scoreReportData: data,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
repData.Code = 200
|
|
|
|
return repData
|
|
|
|
}
|
|
|
|
|
2016-08-15 18:07:40 +00:00
|
|
|
func getMode(m string) string {
|
|
|
|
switch m {
|
|
|
|
case "1":
|
|
|
|
return "taiko"
|
|
|
|
case "2":
|
|
|
|
return "ctb"
|
|
|
|
case "3":
|
|
|
|
return "mania"
|
|
|
|
default:
|
|
|
|
return "std"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func genModeClause(md common.MethodData) string {
|
|
|
|
var modeClause string
|
|
|
|
if md.Query("mode") != "" {
|
|
|
|
m, err := strconv.Atoi(md.Query("mode"))
|
|
|
|
if err == nil && m >= 0 && m <= 3 {
|
|
|
|
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return modeClause
|
|
|
|
}
|