Implement GET scores in official ripple api

This commit is contained in:
Howl 2016-08-15 19:59:46 +02:00
parent 346f26177c
commit a6ca8de13e
16 changed files with 286 additions and 53 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/gin-gonic/contrib/gzip" "github.com/gin-gonic/contrib/gzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/serenize/snaker"
) )
var ( var (
@ -18,11 +19,23 @@ var (
cf common.Conf cf common.Conf
) )
var commonClusterfucks = map[string]string{
"RegisteredOn": "register_datetime",
"UsernameAKA": "username_aka",
}
// Start begins taking HTTP connections. // Start begins taking HTTP connections.
func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine { func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
db = dbO db = dbO
cf = conf cf = conf
db.MapperFunc(func(s string) string {
if x, ok := commonClusterfucks[s]; ok {
return x
}
return snaker.CamelToSnake(s)
})
setUpLimiter() setUpLimiter()
r := gin.Default() r := gin.Default()
@ -68,6 +81,7 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
gv1.GET("/tokens/self", Method(v1.TokenSelfGET)) gv1.GET("/tokens/self", Method(v1.TokenSelfGET))
gv1.GET("/blog/posts", Method(v1.BlogPostsGET)) gv1.GET("/blog/posts", Method(v1.BlogPostsGET))
gv1.GET("/blog/posts/content", Method(v1.BlogPostsContentGET)) gv1.GET("/blog/posts/content", Method(v1.BlogPostsContentGET))
gv1.GET("/scores", Method(v1.ScoresGET))
// ReadConfidential privilege required // ReadConfidential privilege required
gv1.GET("/friends", Method(v1.FriendsGET, common.PrivilegeReadConfidential)) gv1.GET("/friends", Method(v1.FriendsGET, common.PrivilegeReadConfidential))

View File

@ -24,9 +24,9 @@ func BadgesGET(md common.MethodData) common.CodeMessager {
rows *sql.Rows rows *sql.Rows
err error err error
) )
if md.C.Query("id") != "" { if md.Query("id") != "" {
// TODO(howl): ID validation // TODO(howl): ID validation
rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ?", md.C.Query("id")) rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ?", md.Query("id"))
} else { } else {
rows, err = md.DB.Query("SELECT id, name, icon FROM badges") rows, err = md.DB.Query("SELECT id, name, icon FROM badges")
} }

View File

@ -87,14 +87,14 @@ func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
// BeatmapGET retrieves a beatmap. // BeatmapGET retrieves a beatmap.
func BeatmapGET(md common.MethodData) common.CodeMessager { func BeatmapGET(md common.MethodData) common.CodeMessager {
if md.C.Query("s") == "" && md.C.Query("b") == "" { if md.Query("s") == "" && md.Query("b") == "" {
return common.SimpleResponse(400, "Must pass either querystring param 'b' or 's'") return common.SimpleResponse(400, "Must pass either querystring param 'b' or 's'")
} }
setID := common.Int(md.C.Query("s")) setID := common.Int(md.Query("s"))
if setID != 0 { if setID != 0 {
return getSet(md, setID) return getSet(md, setID)
} }
beatmapID := common.Int(md.C.Query("b")) beatmapID := common.Int(md.Query("b"))
if beatmapID != 0 { if beatmapID != 0 {
return getBeatmap(md, beatmapID) return getBeatmap(md, beatmapID)
} }

View File

@ -23,9 +23,9 @@ type blogPostsResponse struct {
func BlogPostsGET(md common.MethodData) common.CodeMessager { func BlogPostsGET(md common.MethodData) common.CodeMessager {
var and string var and string
var params []interface{} var params []interface{}
if md.C.Query("id") != "" { if md.Query("id") != "" {
and = "b.id = ?" and = "b.id = ?"
params = append(params, md.C.Query("id")) params = append(params, md.Query("id"))
} }
rows, err := md.DB.Query(` rows, err := md.DB.Query(`
SELECT SELECT
@ -37,7 +37,7 @@ func BlogPostsGET(md common.MethodData) common.CodeMessager {
LEFT JOIN users u ON b.author = u.id LEFT JOIN users u ON b.author = u.id
LEFT JOIN users_stats s ON b.author = s.id LEFT JOIN users_stats s ON b.author = s.id
WHERE status = "published" `+and+` WHERE status = "published" `+and+`
ORDER BY b.id DESC `+common.Paginate(md.C.Query("p"), md.C.Query("l"), 50), params...) ORDER BY b.id DESC `+common.Paginate(md.Query("p"), md.Query("l"), 50), params...)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
@ -79,12 +79,12 @@ func BlogPostsContentGET(md common.MethodData) common.CodeMessager {
val string val string
) )
switch { switch {
case md.C.Query("slug") != "": case md.Query("slug") != "":
by = "slug" by = "slug"
val = md.C.Query("slug") val = md.Query("slug")
case md.C.Query("id") != "": case md.Query("id") != "":
by = "id" by = "id"
val = md.C.Query("id") val = md.Query("id")
default: default:
return ErrMissingField("id|slug") return ErrMissingField("id|slug")
} }

View File

@ -21,7 +21,7 @@ type docResponse struct {
// DocGET retrieves a list of documentation files. // DocGET retrieves a list of documentation files.
func DocGET(md common.MethodData) common.CodeMessager { func DocGET(md common.MethodData) common.CodeMessager {
var wc string var wc string
if !md.User.Privileges.HasPrivilegeBlog() || md.C.Query("public") == "1" { if !md.User.Privileges.HasPrivilegeBlog() || md.Query("public") == "1" {
wc = "WHERE public = '1'" wc = "WHERE public = '1'"
} }
rows, err := md.DB.Query("SELECT id, doc_name, public, is_rule FROM docs " + wc) rows, err := md.DB.Query("SELECT id, doc_name, public, is_rule FROM docs " + wc)
@ -50,12 +50,12 @@ type docContentResponse struct {
// DocContentGET retrieves the raw markdown file of a doc file // DocContentGET retrieves the raw markdown file of a doc file
func DocContentGET(md common.MethodData) common.CodeMessager { func DocContentGET(md common.MethodData) common.CodeMessager {
docID := common.Int(md.C.Query("id")) docID := common.Int(md.Query("id"))
if docID == 0 { if docID == 0 {
return common.SimpleResponse(404, "Documentation file not found!") return common.SimpleResponse(404, "Documentation file not found!")
} }
var wc string var wc string
if !md.User.Privileges.HasPrivilegeBlog() || md.C.Query("public") == "1" { if !md.User.Privileges.HasPrivilegeBlog() || md.Query("public") == "1" {
wc = "AND public = '1'" wc = "AND public = '1'"
} }
var r docContentResponse var r docContentResponse

View File

@ -16,6 +16,6 @@ var (
func ErrMissingField(missingFields ...string) common.CodeMessager { func ErrMissingField(missingFields ...string) common.CodeMessager {
return common.ResponseBase{ return common.ResponseBase{
Code: 422, // http://stackoverflow.com/a/10323055/5328069 Code: 422, // http://stackoverflow.com/a/10323055/5328069
Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".", Message: "Missing parameters: " + strings.Join(missingFields, ", ") + ".",
} }
} }

View File

@ -54,7 +54,7 @@ ON users_relationships.user2=users_stats.id
WHERE users_relationships.user1=? WHERE users_relationships.user1=?
ORDER BY users_relationships.id` ORDER BY users_relationships.id`
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.C.Query("p"), md.C.Query("l"), 50), md.ID()) results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.Query("p"), md.Query("l"), 50), md.ID())
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
@ -105,7 +105,7 @@ type friendsWithResponse struct {
func FriendsWithGET(md common.MethodData) common.CodeMessager { func FriendsWithGET(md common.MethodData) common.CodeMessager {
var r friendsWithResponse var r friendsWithResponse
r.Code = 200 r.Code = 200
uid := common.Int(md.C.Query("id")) uid := common.Int(md.Query("id"))
if uid == 0 { if uid == 0 {
return r return r
} }
@ -122,7 +122,7 @@ func FriendsWithGET(md common.MethodData) common.CodeMessager {
// FriendsAddGET is the GET version of FriendsAddPOST. // FriendsAddGET is the GET version of FriendsAddPOST.
func FriendsAddGET(md common.MethodData) common.CodeMessager { func FriendsAddGET(md common.MethodData) common.CodeMessager {
return addFriend(md, common.Int(md.C.Query("id"))) return addFriend(md, common.Int(md.Query("id")))
} }
func addFriend(md common.MethodData, u int) common.CodeMessager { func addFriend(md common.MethodData, u int) common.CodeMessager {
@ -166,7 +166,7 @@ func userExists(md common.MethodData, u int) (r bool) {
// FriendsDelGET is the GET version of FriendDelPOST. // FriendsDelGET is the GET version of FriendDelPOST.
func FriendsDelGET(md common.MethodData) common.CodeMessager { func FriendsDelGET(md common.MethodData) common.CodeMessager {
return delFriend(md, common.Int(md.C.Query("id"))) return delFriend(md, common.Int(md.Query("id")))
} }
func delFriend(md common.MethodData, u int) common.CodeMessager { func delFriend(md common.MethodData, u int) common.CodeMessager {

View File

@ -35,9 +35,9 @@ INNER JOIN users_stats ON users_stats.id = leaderboard_%[1]s.user
// LeaderboardGET gets the leaderboard. // LeaderboardGET gets the leaderboard.
func LeaderboardGET(md common.MethodData) common.CodeMessager { func LeaderboardGET(md common.MethodData) common.CodeMessager {
m := getMode(md.C.Query("mode")) m := getMode(md.Query("mode"))
query := fmt.Sprintf(lbUserQuery, m, `WHERE users.privileges & 1 > 0 ORDER BY leaderboard_`+m+`.position `+ query := fmt.Sprintf(lbUserQuery, m, `WHERE users.privileges & 1 > 0 ORDER BY leaderboard_`+m+`.position `+
common.Paginate(md.C.Query("p"), md.C.Query("l"), 100)) common.Paginate(md.Query("p"), md.Query("l"), 100))
rows, err := md.DB.Query(query) rows, err := md.DB.Query(query)
if err != nil { if err != nil {
md.Err(err) md.Err(err)

View File

@ -50,5 +50,5 @@ LEFT JOIN users_stats
ON users.id=users_stats.id ON users.id=users_stats.id
WHERE users.id=? WHERE users.id=?
LIMIT 1` LIMIT 1`
return userPuts(md, md.DB.QueryRow(query, data.UserID)) return userPuts(md, md.DB.QueryRowx(query, data.UserID))
} }

115
app/v1/score.go Normal file
View File

@ -0,0 +1,115 @@
package v1
import (
"database/sql"
"git.zxq.co/ripple/rippleapi/common"
)
type score struct {
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.OsuTime `json:"time"`
PlayMode int `json:"play_mode"`
Accuracy float64 `json:"accuracy"`
PP float32 `json:"pp"`
Completed int `json:"completed"`
}
// beatmapScore is to differentiate from userScore, as beatmapScore contains
// also an user, while userScore contains the beatmap.
type beatmapScore struct {
score
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 (
beatmapMD5 string
r scoresResponse
)
switch {
case md.Query("md5") != "":
beatmapMD5 = md.Query("md5")
case md.Query("b") != "":
err := md.DB.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", md.Query("b"))
switch {
case err == sql.ErrNoRows:
r.Code = 200
return r
case err != nil:
md.Err(err)
return Err500
}
default:
return ErrMissingField("md5|b")
}
sort := common.Sort(md, common.SortConfiguration{
Default: "scores.pp DESC, scores.score DESC",
Table: "scores",
Allowed: []string{"pp", "score", "accuracy", "time", "id"},
})
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
WHERE scores.beatmap_md5 = ? AND scores.completed = '3'
`+sort+common.Paginate(md.Query("p"), md.Query("l"), 100), beatmapMD5)
if err != nil {
md.Err(err)
return Err500
}
for rows.Next() {
var (
s beatmapScore
u userData
)
err := rows.Scan(
&s.ID, &s.BeatmapMD5, &s.Score,
&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
r.Scores = append(r.Scores, s)
}
r.Code = 200
return r
}

View File

@ -198,8 +198,8 @@ func TokenSelfGET(md common.MethodData) common.CodeMessager {
// TokenFixPrivilegesGET fixes the privileges on the token of the given user, // TokenFixPrivilegesGET fixes the privileges on the token of the given user,
// or of all the users if no user is given. // or of all the users if no user is given.
func TokenFixPrivilegesGET(md common.MethodData) common.CodeMessager { func TokenFixPrivilegesGET(md common.MethodData) common.CodeMessager {
id := common.Int(md.C.Query("id")) id := common.Int(md.Query("id"))
if md.C.Query("id") == "self" { if md.Query("id") == "self" {
id = md.ID() id = md.ID()
} }
go fixPrivileges(id, md.DB) go fixPrivileges(id, md.DB)

View File

@ -6,6 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/jmoiron/sqlx"
"git.zxq.co/ripple/ocl" "git.zxq.co/ripple/ocl"
"git.zxq.co/ripple/rippleapi/common" "git.zxq.co/ripple/rippleapi/common"
) )
@ -36,7 +38,7 @@ LEFT JOIN users_stats
ON users.id=users_stats.id ON users.id=users_stats.id
WHERE ` + whereClause + ` AND users.privileges & 1 > 0 WHERE ` + whereClause + ` AND users.privileges & 1 > 0
LIMIT 1` LIMIT 1`
return userPuts(md, md.DB.QueryRow(query, param)) return userPuts(md, md.DB.QueryRowx(query, param))
} }
type userPutsUserData struct { type userPutsUserData struct {
@ -44,14 +46,11 @@ type userPutsUserData struct {
userData userData
} }
func userPuts(md common.MethodData, row *sql.Row) common.CodeMessager { func userPuts(md common.MethodData, row *sqlx.Row) common.CodeMessager {
var err error var err error
var user userPutsUserData var user userPutsUserData
err = row.Scan( err = row.StructScan(&user.userData)
&user.ID, &user.Username, &user.RegisteredOn, &user.Privileges, &user.LatestActivity,
&user.UsernameAKA, &user.Country,
)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return common.SimpleResponse(404, "No such user was found!") return common.SimpleResponse(404, "No such user was found!")
@ -95,7 +94,7 @@ func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager {
r whatIDResponse r whatIDResponse
privileges int privileges int
) )
err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username = ? LIMIT 1", md.C.Query("name")).Scan(&r.ID, &privileges) err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username = ? LIMIT 1", md.Query("name")).Scan(&r.ID, &privileges)
if err != nil || ((privileges&common.UserPrivilegePublic) == 0 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) { if err != nil || ((privileges&common.UserPrivilegePublic) == 0 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) {
return common.SimpleResponse(404, "That user could not be found!") return common.SimpleResponse(404, "That user could not be found!")
} }
@ -243,17 +242,17 @@ func UserUserpageGET(md common.MethodData) common.CodeMessager {
func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) { func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) {
switch { switch {
case md.C.Query("id") == "self": case md.Query("id") == "self":
return nil, tableName + ".id = ?", md.ID() return nil, tableName + ".id = ?", md.ID()
case md.C.Query("id") != "": case md.Query("id") != "":
id, err := strconv.Atoi(md.C.Query("id")) id, err := strconv.Atoi(md.Query("id"))
if err != nil { if err != nil {
a := common.SimpleResponse(400, "please pass a valid user ID") a := common.SimpleResponse(400, "please pass a valid user ID")
return &a, "", nil return &a, "", nil
} }
return nil, tableName + ".id = ?", id return nil, tableName + ".id = ?", id
case md.C.Query("name") != "": case md.Query("name") != "":
return nil, tableName + ".username = ?", md.C.Query("name") return nil, tableName + ".username = ?", md.Query("name")
} }
a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id") a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id")
return &a, "", nil return &a, "", nil
@ -275,7 +274,7 @@ func UserLookupGET(md common.MethodData) common.CodeMessager {
"%", "\\%", "%", "\\%",
"_", "\\_", "_", "\\_",
"\\", "\\\\", "\\", "\\\\",
).Replace(md.C.Query("name")) ).Replace(md.Query("name"))
if name == "" { if name == "" {
return common.SimpleResponse(400, "please provide an username to start searching") return common.SimpleResponse(400, "please provide an username to start searching")
} }

View File

@ -3,7 +3,6 @@ package v1
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"time"
"git.zxq.co/ripple/rippleapi/common" "git.zxq.co/ripple/rippleapi/common"
) )
@ -46,7 +45,7 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager {
} }
mc := genModeClause(md) mc := genModeClause(md)
// Do not print 0pp scores on std // Do not print 0pp scores on std
if getMode(md.C.Query("mode")) == "std" { if getMode(md.Query("mode")) == "std" {
mc += " AND scores.pp > 0" mc += " AND scores.pp > 0"
} }
return scoresPuts(md, fmt.Sprintf( return scoresPuts(md, fmt.Sprintf(
@ -56,7 +55,7 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager {
%s %s
AND users.privileges & 1 > 0 AND users.privileges & 1 > 0
ORDER BY scores.pp DESC, scores.score DESC %s`, ORDER BY scores.pp DESC, scores.score DESC %s`,
wc, mc, common.Paginate(md.C.Query("p"), md.C.Query("l"), 100), wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
), param) ), param)
} }
@ -72,7 +71,7 @@ func UserScoresRecentGET(md common.MethodData) common.CodeMessager {
%s %s
AND users.privileges & 1 > 0 AND users.privileges & 1 > 0
ORDER BY scores.time DESC %s`, ORDER BY scores.time DESC %s`,
wc, genModeClause(md), common.Paginate(md.C.Query("p"), md.C.Query("l"), 100), wc, genModeClause(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
), param) ), param)
} }
@ -91,8 +90,8 @@ func getMode(m string) string {
func genModeClause(md common.MethodData) string { func genModeClause(md common.MethodData) string {
var modeClause string var modeClause string
if md.C.Query("mode") != "" { if md.Query("mode") != "" {
m, err := strconv.Atoi(md.C.Query("mode")) m, err := strconv.Atoi(md.Query("mode"))
if err == nil && m >= 0 && m <= 3 { if err == nil && m >= 0 && m <= 3 {
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m) modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
} }
@ -110,7 +109,6 @@ func scoresPuts(md common.MethodData, whereClause string, params ...interface{})
for rows.Next() { for rows.Next() {
var ( var (
us userScore us userScore
t string
b beatmap b beatmap
) )
err = rows.Scan( err = rows.Scan(
@ -118,7 +116,7 @@ func scoresPuts(md common.MethodData, whereClause string, params ...interface{})
&us.MaxCombo, &us.FullCombo, &us.Mods, &us.MaxCombo, &us.FullCombo, &us.Mods,
&us.Count300, &us.Count100, &us.Count50, &us.Count300, &us.Count100, &us.Count50,
&us.CountGeki, &us.CountKatu, &us.CountMiss, &us.CountGeki, &us.CountKatu, &us.CountMiss,
&t, &us.PlayMode, &us.Accuracy, &us.PP, &us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
&us.Completed, &us.Completed,
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5, &b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
@ -127,17 +125,11 @@ func scoresPuts(md common.MethodData, whereClause string, params ...interface{})
&b.MaxCombo, &b.HitLength, &b.Ranked, &b.MaxCombo, &b.HitLength, &b.Ranked,
&b.RankedStatusFrozen, &b.LatestUpdate, &b.RankedStatusFrozen, &b.LatestUpdate,
) )
b.Difficulty = b.Diff2.STD
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
// puck feppy b.Difficulty = b.Diff2.STD
us.Time, err = time.Parse(common.OsuTimeFormat, t)
if _, ok := err.(*time.ParseError); !ok && err != nil {
md.Err(err)
return Err500
}
us.Beatmap = b us.Beatmap = b
scores = append(scores, us) scores = append(scores, us)
} }

View File

@ -25,6 +25,11 @@ func (md MethodData) ID() int {
return md.User.UserID return md.User.UserID
} }
// Query is shorthand for md.C.Query.
func (md MethodData) Query(q string) string {
return md.C.Query(q)
}
// RequestData is the body of a request. It is wrapped into this type // RequestData is the body of a request. It is wrapped into this type
// to implement the Unmarshal function, which is just a shorthand to // to implement the Unmarshal function, which is just a shorthand to
// json.Unmarshal. // json.Unmarshal.

View File

@ -1,4 +1,60 @@
package common package common
import (
"errors"
"strconv"
"time"
)
// OsuTimeFormat is the time format for scores in the DB. Can be used with time.Parse etc. // OsuTimeFormat is the time format for scores in the DB. Can be used with time.Parse etc.
const OsuTimeFormat = "060102150405" const OsuTimeFormat = "060102150405"
// OsuTime is simply a time.Time, but can be used to convert an
// osu timestamp in the database into a native time.Time.
type OsuTime time.Time
func (s *OsuTime) setTime(t string) error {
newTime, err := time.Parse(OsuTimeFormat, t)
if _, ok := err.(*time.ParseError); err != nil && !ok {
return err
}
if err == nil {
*s = OsuTime(newTime)
}
return nil
}
// Scan decodes src into an OsuTime.
func (s *OsuTime) Scan(src interface{}) error {
if s == nil {
return errors.New("rippleapi/common: OsuTime is nil")
}
switch src := src.(type) {
case int64:
return s.setTime(strconv.FormatInt(src, 64))
case float64:
return s.setTime(strconv.FormatInt(int64(src), 64))
case string:
return s.setTime(src)
case []byte:
return s.setTime(string(src))
case nil:
// Nothing, leave zero value on timestamp
default:
return errors.New("rippleapi/common: unhandleable type")
}
return nil
}
// MarshalJSON -> time.Time.MarshalJSON
func (s OsuTime) MarshalJSON() ([]byte, error) {
return time.Time(s).MarshalJSON()
}
// UnmarshalJSON -> time.Time.UnmarshalJSON
func (s *OsuTime) UnmarshalJSON(x []byte) error {
t := new(time.Time)
err := t.UnmarshalJSON(x)
*s = OsuTime(*t)
return err
}

52
common/sort.go Normal file
View File

@ -0,0 +1,52 @@
package common
import "strings"
// SortConfiguration is the configuration of Sort.
type SortConfiguration struct {
Allowed []string // Allowed parameters
Default string
DefaultSorting string // if empty, DESC
Table string
}
// Sort allows the request to modify how the query is sorted.
func Sort(md MethodData, config SortConfiguration) string {
if config.DefaultSorting == "" {
config.DefaultSorting = "DESC"
}
if config.Table != "" {
config.Table += "."
}
var sortBy string
for _, s := range md.C.Request.URL.Query()["sort"] {
sortParts := strings.Split(strings.ToLower(s), ",")
if contains(config.Allowed, sortParts[0]) {
if sortBy != "" {
sortBy += ", "
}
sortBy += config.Table + sortParts[0] + " "
if len(sortParts) > 1 && contains([]string{"asc", "desc"}, sortParts[1]) {
sortBy += sortParts[1]
} else {
sortBy += config.DefaultSorting
}
}
}
if sortBy == "" {
sortBy = config.Default
}
if sortBy == "" {
return ""
}
return "ORDER BY " + sortBy
}
func contains(a []string, s string) bool {
for _, el := range a {
if s == el {
return true
}
}
return false
}