Completely change response structure

This commit is contained in:
Howl 2016-04-16 18:05:24 +02:00
parent 14d926e31d
commit 7387ed4295
12 changed files with 280 additions and 277 deletions

View File

@ -10,13 +10,13 @@ import (
) )
// Method wraps an API method to a HandlerFunc. // Method wraps an API method to a HandlerFunc.
func Method(f func(md common.MethodData) common.Response, db *sql.DB, privilegesNeeded ...int) gin.HandlerFunc { func Method(f func(md common.MethodData) common.CodeMessager, db *sql.DB, privilegesNeeded ...int) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
initialCaretaker(c, f, db, privilegesNeeded...) initialCaretaker(c, f, db, privilegesNeeded...)
} }
} }
func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.Response, db *sql.DB, privilegesNeeded ...int) { func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMessager, db *sql.DB, privilegesNeeded ...int) {
data, err := ioutil.ReadAll(c.Request.Body) data, err := ioutil.ReadAll(c.Request.Body)
if err != nil { if err != nil {
c.Error(err) c.Error(err)
@ -52,22 +52,25 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.Respon
} }
} }
if missingPrivileges != 0 { if missingPrivileges != 0 {
c.IndentedJSON(401, common.Response{ c.IndentedJSON(401, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
Code: 401,
Message: "You don't have the privilege(s): " + common.Privileges(missingPrivileges).String() + ".",
})
return return
} }
resp := f(md) resp := f(md)
if resp.Code == 0 { if resp.GetCode() == 0 {
resp.Code = 500 // Dirty hack to set the code
type setCoder interface {
SetCode(int)
}
if newver, can := resp.(setCoder); can {
newver.SetCode(500)
}
} }
if _, exists := c.GetQuery("pls200"); exists { if _, exists := c.GetQuery("pls200"); exists {
c.Writer.WriteHeader(200) c.Writer.WriteHeader(200)
} else { } else {
c.Writer.WriteHeader(resp.Code) c.Writer.WriteHeader(resp.GetCode())
} }
if _, exists := c.GetQuery("callback"); exists { if _, exists := c.GetQuery("callback"); exists {

View File

@ -5,11 +5,18 @@ import (
"github.com/osuripple/api/common" "github.com/osuripple/api/common"
) )
type response404 struct {
common.ResponseBase
Cats string `json:"cats"`
}
// Handle404 handles requests with no implemented handlers. // Handle404 handles requests with no implemented handlers.
func Handle404(c *gin.Context) { func Handle404(c *gin.Context) {
c.IndentedJSON(404, common.Response{ c.IndentedJSON(404, response404{
Code: 404, ResponseBase: common.ResponseBase{
Message: "Oh dear... that API request could not be found! Perhaps the API is not up-to-date? Either way, have a surprise!", Code: 404,
Data: surpriseMe(), Message: "Oh dear... that API request could not be found! Perhaps the API is not up-to-date? Either way, have a surprise!",
},
Cats: surpriseMe(),
}) })
} }

View File

@ -6,53 +6,57 @@ import (
"github.com/osuripple/api/common" "github.com/osuripple/api/common"
) )
type badgeData struct { type singleBadge struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Icon string `json:"icon"` Icon string `json:"icon"`
} }
type badgeData struct {
common.ResponseBase
singleBadge
}
// BadgeByIDGET is the handler for /badge/:id // BadgeByIDGET is the handler for /badge/:id
func BadgeByIDGET(md common.MethodData) (r common.Response) { func BadgeByIDGET(md common.MethodData) common.CodeMessager {
b := badgeData{} var b badgeData
err := md.DB.QueryRow("SELECT id, name, icon FROM badges WHERE id=? LIMIT 1", md.C.Param("id")).Scan(&b.ID, &b.Name, &b.Icon) err := md.DB.QueryRow("SELECT id, name, icon FROM badges WHERE id=? LIMIT 1", md.C.Param("id")).Scan(&b.ID, &b.Name, &b.Icon)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
r.Code = 404 return common.SimpleResponse(404, "No such badge was found")
r.Message = "No such badge was found"
return
case err != nil: case err != nil:
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
r.Code = 200 b.Code = 200
r.Data = b return b
return }
type multiBadgeData struct {
common.ResponseBase
Badges []singleBadge `json:"badges"`
} }
// BadgesGET retrieves all the badges on this ripple instance. // BadgesGET retrieves all the badges on this ripple instance.
func BadgesGET(md common.MethodData) (r common.Response) { func BadgesGET(md common.MethodData) common.CodeMessager {
var badges []badgeData var r multiBadgeData
rows, err := md.DB.Query("SELECT id, name, icon FROM badges") rows, err := md.DB.Query("SELECT id, name, icon FROM badges")
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
nb := badgeData{} nb := singleBadge{}
err = rows.Scan(&nb.ID, &nb.Name, &nb.Icon) err = rows.Scan(&nb.ID, &nb.Name, &nb.Icon)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
} }
badges = append(badges, nb) r.Badges = append(r.Badges, nb)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
md.Err(err) md.Err(err)
} }
r.Code = 200 r.ResponseBase.Code = 200
r.Data = badges return r
return
} }

View File

@ -8,19 +8,13 @@ import (
// Boilerplate errors // Boilerplate errors
var ( var (
Err500 = common.Response{ Err500 = common.SimpleResponse(500, "An error occurred. Trying again may work. If it doesn't, yell at this Ripple instance admin and tell them to fix the API.")
Code: 500, ErrBadJSON = common.SimpleResponse(400, "Your JSON for this request is invalid.")
Message: "An error occurred. Try again, perhaps?",
}
ErrBadJSON = common.Response{
Code: 400,
Message: "There was an error processing your JSON data.",
}
) )
// ErrMissingField generates a response to a request when some fields in the JSON are missing. // ErrMissingField generates a response to a request when some fields in the JSON are missing.
func ErrMissingField(missingFields ...string) common.Response { func ErrMissingField(missingFields ...string) common.CodeMessager {
return common.Response{ 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 fields: " + strings.Join(missingFields, ", ") + ".",
} }

View File

@ -13,15 +13,19 @@ type friendData struct {
IsMutual bool `json:"is_mutual"` IsMutual bool `json:"is_mutual"`
} }
type friendsGETResponse struct {
common.ResponseBase
Friends []friendData `json:"friends"`
}
// FriendsGET is the API request handler for GET /friends. // FriendsGET is the API request handler for GET /friends.
// It retrieves an user's friends, and whether the friendship is mutual or not. // It retrieves an user's friends, and whether the friendship is mutual or not.
func FriendsGET(md common.MethodData) (r common.Response) { func FriendsGET(md common.MethodData) common.CodeMessager {
var myFrienders []int var myFrienders []int
myFriendersRaw, err := md.DB.Query("SELECT user1 FROM users_relationships WHERE user2 = ?", md.ID()) myFriendersRaw, err := md.DB.Query("SELECT user1 FROM users_relationships WHERE user2 = ?", md.ID())
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
defer myFriendersRaw.Close() defer myFriendersRaw.Close()
for myFriendersRaw.Next() { for myFriendersRaw.Next() {
@ -55,8 +59,7 @@ ORDER BY users_relationships.id`
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.C.Query("p"), md.C.Query("l")), md.ID()) results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.C.Query("p"), md.C.Query("l")), md.ID())
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
var myFriends []friendData var myFriends []friendData
@ -76,9 +79,10 @@ ORDER BY users_relationships.id`
md.Err(err) md.Err(err)
} }
r := friendsGETResponse{}
r.Code = 200 r.Code = 200
r.Data = myFriends r.Friends = myFriends
return return r
} }
func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) { func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) {
@ -104,39 +108,37 @@ func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) {
return return
} }
type friendsWithData struct { type friendsWithResponse struct {
common.ResponseBase
Friends bool `json:"friend"` Friends bool `json:"friend"`
Mutual bool `json:"mutual"` Mutual bool `json:"mutual"`
} }
// FriendsWithGET checks the current user is friends with the one passed in the request path. // FriendsWithGET checks the current user is friends with the one passed in the request path.
func FriendsWithGET(md common.MethodData) (r common.Response) { func FriendsWithGET(md common.MethodData) common.CodeMessager {
r := friendsWithResponse{}
r.Code = 200 r.Code = 200
var d friendsWithData
uid, err := strconv.Atoi(md.C.Param("id")) uid, err := strconv.Atoi(md.C.Param("id"))
if err != nil { if err != nil {
r.Data = d return common.SimpleResponse(400, "That is not a number!")
return
} }
err = md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users_relationships WHERE user1 = ? AND user2 = ? LIMIT 1), EXISTS(SELECT 1 FROM users_relationships WHERE user2 = ? AND user1 = ? LIMIT 1)", md.ID(), uid, md.ID(), uid).Scan(&d.Friends, &d.Mutual) err = md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users_relationships WHERE user1 = ? AND user2 = ? LIMIT 1), EXISTS(SELECT 1 FROM users_relationships WHERE user2 = ? AND user1 = ? LIMIT 1)", md.ID(), uid, md.ID(), uid).Scan(&r.Friends, &r.Mutual)
if err != sql.ErrNoRows && err != nil { if err != sql.ErrNoRows && err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
r.Data = d if !r.Friends {
return r.Mutual = false
}
return r
} }
// FriendsAddGET is the GET version of FriendsAddPOST. // FriendsAddGET is the GET version of FriendsAddPOST.
func FriendsAddGET(md common.MethodData) common.Response { func FriendsAddGET(md common.MethodData) common.CodeMessager {
uidS := md.C.Param("id") uidS := md.C.Param("id")
uid, err := strconv.Atoi(uidS) uid, err := strconv.Atoi(uidS)
if err != nil { if err != nil {
return common.Response{ return common.SimpleResponse(400, "Nope. That's not a number.")
Code: 400,
Message: "Nope. That's not a number.",
}
} }
return addFriend(md, uid) return addFriend(md, uid)
} }
@ -146,26 +148,21 @@ type friendAddPOSTData struct {
} }
// FriendsAddPOST allows for adding friends. Yup. Easy as that. // FriendsAddPOST allows for adding friends. Yup. Easy as that.
func FriendsAddPOST(md common.MethodData) (r common.Response) { func FriendsAddPOST(md common.MethodData) common.CodeMessager {
d := friendAddPOSTData{} d := friendAddPOSTData{}
err := md.RequestData.Unmarshal(&d) err := md.RequestData.Unmarshal(&d)
if err != nil { if err != nil {
r = ErrBadJSON return ErrBadJSON
return
} }
return addFriend(md, d.UserID) return addFriend(md, d.UserID)
} }
func addFriend(md common.MethodData, u int) (r common.Response) { func addFriend(md common.MethodData, u int) common.CodeMessager {
if md.ID() == u { if md.ID() == u {
r.Code = 400 return common.SimpleResponse(406, "Just so you know: you can't add yourself to your friends.")
r.Message = "Just so you know: you can't add yourself to your friends."
return
} }
if !userExists(md, u) { if !userExists(md, u) {
r.Code = 404 return common.SimpleResponse(404, "I'd also like to be friends with someone who doesn't even exist (???), however that's NOT POSSIBLE.")
r.Message = "I'd also like to be friends with someone who doesn't even exist (???), however that's NOT POSSIBLE."
return
} }
var ( var (
relExists bool relExists bool
@ -174,23 +171,20 @@ func addFriend(md common.MethodData, u int) (r common.Response) {
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users_relationships WHERE user1 = ? AND user2 = ?), EXISTS(SELECT 1 FROM users_relationships WHERE user2 = ? AND user1 = ?)", md.ID(), u, md.ID(), u).Scan(&relExists, &isMutual) err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users_relationships WHERE user1 = ? AND user2 = ?), EXISTS(SELECT 1 FROM users_relationships WHERE user2 = ? AND user1 = ?)", md.ID(), u, md.ID(), u).Scan(&relExists, &isMutual)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
if !relExists { if !relExists {
_, err := md.DB.Exec("INSERT INTO users_relationships(user1, user2) VALUES (?, ?)", md.User.UserID, u) _, err := md.DB.Exec("INSERT INTO users_relationships(user1, user2) VALUES (?, ?)", md.User.UserID, u)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
} }
var r friendsWithResponse
r.Code = 200 r.Code = 200
r.Data = friendsWithData{ r.Friends = true
Friends: true, r.Mutual = isMutual
Mutual: isMutual, return r
}
return
} }
// userExists makes sure an user exists. // userExists makes sure an user exists.
@ -203,40 +197,35 @@ 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.Response { func FriendsDelGET(md common.MethodData) common.CodeMessager {
uidS := md.C.Param("id") uidS := md.C.Param("id")
uid, err := strconv.Atoi(uidS) uid, err := strconv.Atoi(uidS)
if err != nil { if err != nil {
return common.Response{ return common.SimpleResponse(400, "Nope. That's not a number.")
Code: 400,
Message: "Nope. That's not a number.",
}
} }
return delFriend(md, uid) return delFriend(md, uid)
} }
// FriendsDelPOST allows for deleting friends. // FriendsDelPOST allows for deleting friends.
func FriendsDelPOST(md common.MethodData) (r common.Response) { func FriendsDelPOST(md common.MethodData) common.CodeMessager {
d := friendAddPOSTData{} d := friendAddPOSTData{}
err := md.RequestData.Unmarshal(&d) err := md.RequestData.Unmarshal(&d)
if err != nil { if err != nil {
r = ErrBadJSON return ErrBadJSON
return
} }
return delFriend(md, d.UserID) return delFriend(md, d.UserID)
} }
func delFriend(md common.MethodData, u int) common.Response { func delFriend(md common.MethodData, u int) common.CodeMessager {
_, err := md.DB.Exec("DELETE FROM users_relationships WHERE user1 = ? AND user2 = ?", md.ID(), u) _, err := md.DB.Exec("DELETE FROM users_relationships WHERE user1 = ? AND user2 = ?", md.ID(), u)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
return Err500 return Err500
} }
return common.Response{ r := friendsWithResponse{
Code: 200, Friends: false,
Data: friendsWithData{ Mutual: false,
Friends: false,
Mutual: false,
},
} }
r.Code = 200
return r
} }

View File

@ -8,22 +8,18 @@ type setAllowedData struct {
} }
// UserManageSetAllowedPOST allows to set the allowed status of an user. // UserManageSetAllowedPOST allows to set the allowed status of an user.
func UserManageSetAllowedPOST(md common.MethodData) (r common.Response) { func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager {
data := setAllowedData{} data := setAllowedData{}
if err := md.RequestData.Unmarshal(&data); err != nil { if err := md.RequestData.Unmarshal(&data); err != nil {
r = ErrBadJSON return ErrBadJSON
return
} }
if data.Allowed < 0 || data.Allowed > 2 { if data.Allowed < 0 || data.Allowed > 2 {
r.Code = 400 return common.SimpleResponse(400, "Allowed status must be between 0 and 2")
r.Message = "Allowed status must be between 0 and 2"
return
} }
_, err := md.DB.Exec("UPDATE users SET allowed = ? WHERE id = ?", data.Allowed, data.UserID) _, err := md.DB.Exec("UPDATE users SET allowed = ? WHERE id = ?", data.Allowed, data.UserID)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
query := ` query := `
SELECT users.id, users.username, register_datetime, rank, SELECT users.id, users.username, register_datetime, rank,
@ -34,6 +30,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`
r = userPuts(md, md.DB.QueryRow(query, data.UserID)) return userPuts(md, md.DB.QueryRow(query, data.UserID))
return
} }

View File

@ -14,60 +14,59 @@ import (
) )
// MetaRestartGET restarts the API with Zero Downtime™. // MetaRestartGET restarts the API with Zero Downtime™.
func MetaRestartGET(md common.MethodData) (r common.Response) { func MetaRestartGET(md common.MethodData) common.CodeMessager {
proc, err := os.FindProcess(syscall.Getpid()) proc, err := os.FindProcess(syscall.Getpid())
if err != nil { if err != nil {
r.Code = 500 return common.SimpleResponse(500, "couldn't find process. what the fuck?")
r.Message = "couldn't find process. what the fuck?"
return
} }
r.Code = 200
r.Message = "brb"
go func() { go func() {
time.Sleep(time.Second) time.Sleep(time.Second)
proc.Signal(syscall.SIGUSR2) proc.Signal(syscall.SIGUSR2)
}() }()
return return common.SimpleResponse(200, "brb")
} }
// MetaKillGET kills the API process. NOTE TO EVERYONE: NEVER. EVER. USE IN PROD. // MetaKillGET kills the API process. NOTE TO EVERYONE: NEVER. EVER. USE IN PROD.
// Mainly created because I couldn't bother to fire up a terminal, do htop and kill the API each time. // Mainly created because I couldn't bother to fire up a terminal, do htop and kill the API each time.
func MetaKillGET(md common.MethodData) (r common.Response) { func MetaKillGET(md common.MethodData) common.CodeMessager {
proc, err := os.FindProcess(syscall.Getpid()) proc, err := os.FindProcess(syscall.Getpid())
if err != nil { if err != nil {
r.Code = 500 return common.SimpleResponse(500, "couldn't find process. what the fuck?")
r.Message = "couldn't find process. what the fuck?"
return
} }
const form = "02/01/2006" const form = "02/01/2006"
r.Code = 200 r := common.ResponseBase{
Code: 200,
Message: fmt.Sprintf("RIP ripple API %s - %s", upSince.Format(form), time.Now().Format(form)),
}
// yes // yes
r.Message = fmt.Sprintf("RIP ripple API %s - %s", upSince.Format(form), time.Now().Format(form))
go func() { go func() {
time.Sleep(time.Second) time.Sleep(time.Second)
proc.Kill() proc.Kill()
}() }()
return return r
} }
var upSince = time.Now() var upSince = time.Now()
type metaUpSinceResponse struct {
common.ResponseBase
Code int `json:"code"`
Since int64 `json:"since"`
}
// MetaUpSinceGET retrieves the moment the API application was started. // MetaUpSinceGET retrieves the moment the API application was started.
// Mainly used to get if the API was restarted. // Mainly used to get if the API was restarted.
func MetaUpSinceGET(md common.MethodData) common.Response { func MetaUpSinceGET(md common.MethodData) common.CodeMessager {
return common.Response{ return metaUpSinceResponse{
Code: 200, Code: 200,
Data: upSince.UnixNano(), Since: int64(upSince.UnixNano()),
} }
} }
// MetaUpdateGET updates the API to the latest version, and restarts it. // MetaUpdateGET updates the API to the latest version, and restarts it.
func MetaUpdateGET(md common.MethodData) common.Response { func MetaUpdateGET(md common.MethodData) common.CodeMessager {
if f, err := os.Stat(".git"); err == os.ErrNotExist || !f.IsDir() { if f, err := os.Stat(".git"); err == os.ErrNotExist || !f.IsDir() {
return common.Response{ return common.SimpleResponse(500, "instance is not using git")
Code: 500,
Message: "repo is not using git",
}
} }
go func() { go func() {
// go get // go get
@ -87,10 +86,7 @@ func MetaUpdateGET(md common.MethodData) common.Response {
} }
proc.Signal(syscall.SIGUSR2) proc.Signal(syscall.SIGUSR2)
}() }()
return common.Response{ return common.SimpleResponse(200, "Started updating! "+surpriseMe())
Code: 200,
Message: "Started updating! " + surpriseMe(),
}
} }
func execCommand(command string, args ...string) bool { func execCommand(command string, args ...string) bool {

View File

@ -75,42 +75,52 @@ var randomSentences = [...]string{
"sudo rm -rf /", "sudo rm -rf /",
"Hi! I'm Flowey! Flowey the flower!", "Hi! I'm Flowey! Flowey the flower!",
"Ripple devs are actually cats", "Ripple devs are actually cats",
"Support Howl's fund for buying a power supply for his SSD", "Support Howl's fund for buying a power supply for his SSD!",
"Superman dies",
"PP when?",
"RWC hype",
} }
func surpriseMe() string { func surpriseMe() string {
return randomSentences[rn.Intn(len(randomSentences))] + " " + kaomojis[rn.Intn(len(kaomojis))] return randomSentences[rn.Intn(len(randomSentences))] + " " + kaomojis[rn.Intn(len(kaomojis))]
} }
type pingData struct { type pingResponse struct {
common.ResponseBase
ID int `json:"user_id"` ID int `json:"user_id"`
Privileges int `json:"privileges"` Privileges int `json:"privileges"`
} }
// PingGET is a message to check with the API that we are logged in, and know what are our privileges. // PingGET is a message to check with the API that we are logged in, and know what are our privileges.
func PingGET(md common.MethodData) (r common.Response) { func PingGET(md common.MethodData) common.CodeMessager {
var r pingResponse
r.Code = 200 r.Code = 200
if md.ID() == 0 { if md.ID() == 0 {
r.Message = "You have not given us a token, so we don't know who you are! But you can still login with /api/v1/tokens/new " + kaomojis[rn.Intn(len(kaomojis))] r.Message = "You have not given us a token, so we don't know who you are! But you can still login with /api/v1/tokens/new " + kaomojis[rn.Intn(len(kaomojis))]
} else { } else {
r.Message = surpriseMe() r.Message = surpriseMe()
} }
r.Data = pingData{
ID: md.ID(), r.ID = md.ID()
Privileges: int(md.User.Privileges), r.Privileges = int(md.User.Privileges)
}
return return r
}
type surpriseMeResponse struct {
common.ResponseBase
Cats [100]string `json:"cats"`
} }
// SurpriseMeGET generates cute cats. // SurpriseMeGET generates cute cats.
// //
// ... Yes. // ... Yes.
func SurpriseMeGET(md common.MethodData) (r common.Response) { func SurpriseMeGET(md common.MethodData) common.CodeMessager {
var r surpriseMeResponse
r.Code = 200 r.Code = 200
cats := make([]string, 100)
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
cats[i] = surpriseMe() r.Cats[i] = surpriseMe()
} }
r.Data = cats return r
return
} }

View File

@ -5,6 +5,7 @@ import (
) )
type privilegesData struct { type privilegesData struct {
common.ResponseBase
Read bool `json:"read"` Read bool `json:"read"`
ReadConfidential bool `json:"read_confidential"` ReadConfidential bool `json:"read_confidential"`
Write bool `json:"write"` Write bool `json:"write"`
@ -20,22 +21,21 @@ type privilegesData struct {
} }
// PrivilegesGET returns an explaination for the privileges, telling the client what they can do with this token. // PrivilegesGET returns an explaination for the privileges, telling the client what they can do with this token.
func PrivilegesGET(md common.MethodData) (r common.Response) { func PrivilegesGET(md common.MethodData) common.CodeMessager {
// This code sucks. r := privilegesData{}
r.Code = 200 r.Code = 200
r.Data = privilegesData{ // This code sucks.
Read: md.User.Privileges.HasPrivilegeRead(), r.Read = md.User.Privileges.HasPrivilegeRead()
ReadConfidential: md.User.Privileges.HasPrivilegeReadConfidential(), r.ReadConfidential = md.User.Privileges.HasPrivilegeReadConfidential()
Write: md.User.Privileges.HasPrivilegeWrite(), r.Write = md.User.Privileges.HasPrivilegeWrite()
ManageBadges: md.User.Privileges.HasPrivilegeManageBadges(), r.ManageBadges = md.User.Privileges.HasPrivilegeManageBadges()
BetaKeys: md.User.Privileges.HasPrivilegeBetaKeys(), r.BetaKeys = md.User.Privileges.HasPrivilegeBetaKeys()
ManageSettings: md.User.Privileges.HasPrivilegeManageSettings(), r.ManageSettings = md.User.Privileges.HasPrivilegeManageSettings()
ViewUserAdvanced: md.User.Privileges.HasPrivilegeViewUserAdvanced(), r.ViewUserAdvanced = md.User.Privileges.HasPrivilegeViewUserAdvanced()
ManageUser: md.User.Privileges.HasPrivilegeManageUser(), r.ManageUser = md.User.Privileges.HasPrivilegeManageUser()
ManageRoles: md.User.Privileges.HasPrivilegeManageRoles(), r.ManageRoles = md.User.Privileges.HasPrivilegeManageRoles()
ManageAPIKeys: md.User.Privileges.HasPrivilegeManageAPIKeys(), r.ManageAPIKeys = md.User.Privileges.HasPrivilegeManageAPIKeys()
Blog: md.User.Privileges.HasPrivilegeBlog(), r.Blog = md.User.Privileges.HasPrivilegeBlog()
APIMeta: md.User.Privileges.HasPrivilegeAPIMeta(), r.APIMeta = md.User.Privileges.HasPrivilegeAPIMeta()
} return r
return
} }

View File

@ -19,7 +19,8 @@ type tokenNewInData struct {
Description string `json:"description"` Description string `json:"description"`
} }
type tokenNewOutData struct { type tokenNewResponse struct {
common.ResponseBase
Username string `json:"username"` Username string `json:"username"`
ID int `json:"id"` ID int `json:"id"`
Privileges int `json:"privileges"` Privileges int `json:"privileges"`
@ -28,12 +29,12 @@ type tokenNewOutData struct {
} }
// TokenNewPOST is the handler for POST /token/new. // TokenNewPOST is the handler for POST /token/new.
func TokenNewPOST(md common.MethodData) (r common.Response) { func TokenNewPOST(md common.MethodData) common.CodeMessager {
var r tokenNewResponse
data := tokenNewInData{} data := tokenNewInData{}
err := md.RequestData.Unmarshal(&data) err := md.RequestData.Unmarshal(&data)
if err != nil { if err != nil {
r = ErrBadJSON return ErrBadJSON
return
} }
var miss []string var miss []string
@ -44,8 +45,7 @@ func TokenNewPOST(md common.MethodData) (r common.Response) {
miss = append(miss, "password") miss = append(miss, "password")
} }
if len(miss) != 0 { if len(miss) != 0 {
r = ErrMissingField(miss...) return ErrMissingField(miss...)
return
} }
var q *sql.Row var q *sql.Row
@ -56,7 +56,6 @@ func TokenNewPOST(md common.MethodData) (r common.Response) {
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username) q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username)
} }
ret := tokenNewOutData{}
var ( var (
rank int rank int
pw string pw string
@ -64,48 +63,37 @@ func TokenNewPOST(md common.MethodData) (r common.Response) {
allowed int allowed int
) )
err = q.Scan(&ret.ID, &ret.Username, &rank, &pw, &pwVersion, &allowed) err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &allowed)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
r.Code = 404 return common.SimpleResponse(404, "No user with that username/id was found.")
r.Message = "No user with that username/id was found."
return
case err != nil: case err != nil:
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
if nFailedAttempts(ret.ID) > 20 { if nFailedAttempts(r.ID) > 20 {
r.Code = 429 return common.SimpleResponse(429, "You've made too many login attempts. Try again later.")
r.Message = "You've made too many login attempts. Try again later."
return
} }
if pwVersion == 1 { if pwVersion == 1 {
r.Code = 418 // Teapots! return common.SimpleResponse(418, "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website.")
r.Message = "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website."
return
} }
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword { if err == bcrypt.ErrMismatchedHashAndPassword {
go addFailedAttempt(ret.ID) go addFailedAttempt(r.ID)
r.Code = 403 return common.SimpleResponse(403, "That password doesn't match!")
r.Message = "That password doesn't match!"
return
} }
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
if allowed == 0 { if allowed == 0 {
r.Code = 200 r.Code = 200
r.Message = "That user is banned." r.Message = "That user is banned."
ret.Banned = true r.Banned = true
r.Data = ret return r
return
} }
ret.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank)) r.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank))
var ( var (
tokenStr string tokenStr string
@ -114,7 +102,7 @@ func TokenNewPOST(md common.MethodData) (r common.Response) {
for { for {
tokenStr = common.RandomString(32) tokenStr = common.RandomString(32)
tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr))) tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
ret.Token = tokenStr r.Token = tokenStr
id := 0 id := 0
err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=? LIMIT 1", tokenMD5).Scan(&id) err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=? LIMIT 1", tokenMD5).Scan(&id)
@ -123,18 +111,15 @@ func TokenNewPOST(md common.MethodData) (r common.Response) {
} }
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
} }
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token) VALUES (?, ?, ?, ?)", ret.ID, ret.Privileges, data.Description, tokenMD5) _, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token) VALUES (?, ?, ?, ?)", r.ID, r.Privileges, data.Description, tokenMD5)
if err != nil { if err != nil {
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
r.Code = 200 r.Code = 200
r.Data = ret return r
return
} }

View File

@ -13,6 +13,7 @@ import (
) )
type userData struct { type userData struct {
common.ResponseBase
ID int `json:"id"` ID int `json:"id"`
Username string `json:"username"` Username string `json:"username"`
UsernameAKA string `json:"username_aka"` UsernameAKA string `json:"username_aka"`
@ -23,7 +24,7 @@ type userData struct {
} }
// UserByIDGET is the API handler for GET /users/id/:id // UserByIDGET is the API handler for GET /users/id/:id
func UserByIDGET(md common.MethodData) (r common.Response) { func UserByIDGET(md common.MethodData) common.CodeMessager {
var err error var err error
var uid int var uid int
uidStr := md.C.Param("id") uidStr := md.C.Param("id")
@ -32,9 +33,7 @@ func UserByIDGET(md common.MethodData) (r common.Response) {
} else { } else {
uid, err = strconv.Atoi(uidStr) uid, err = strconv.Atoi(uidStr)
if err != nil { if err != nil {
r.Code = 400 return common.SimpleResponse(400, fmt.Sprintf("%s ain't a number", uidStr))
r.Message = fmt.Sprintf("%s ain't a number", uidStr)
return
} }
} }
@ -47,12 +46,11 @@ LEFT JOIN users_stats
ON users.id=users_stats.id ON users.id=users_stats.id
WHERE users.id=? AND users.allowed='1' WHERE users.id=? AND users.allowed='1'
LIMIT 1` LIMIT 1`
r = userPuts(md, md.DB.QueryRow(query, uid)) return userPuts(md, md.DB.QueryRow(query, uid))
return
} }
// UserByNameGET is the API handler for GET /users/name/:name // UserByNameGET is the API handler for GET /users/name/:name
func UserByNameGET(md common.MethodData) (r common.Response) { func UserByNameGET(md common.MethodData) common.CodeMessager {
username := md.C.Param("name") username := md.C.Param("name")
query := ` query := `
@ -64,11 +62,10 @@ LEFT JOIN users_stats
ON users.id=users_stats.id ON users.id=users_stats.id
WHERE users.username=? AND users.allowed='1' WHERE users.username=? AND users.allowed='1'
LIMIT 1` LIMIT 1`
r = userPuts(md, md.DB.QueryRow(query, username)) return userPuts(md, md.DB.QueryRow(query, username))
return
} }
func userPuts(md common.MethodData, row *sql.Row) (r common.Response) { func userPuts(md common.MethodData, row *sql.Row) common.CodeMessager {
var err error var err error
var user userData var user userData
@ -80,13 +77,10 @@ func userPuts(md common.MethodData, row *sql.Row) (r common.Response) {
err = row.Scan(&user.ID, &user.Username, &registeredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showCountry) err = row.Scan(&user.ID, &user.Username, &registeredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showCountry)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
r.Code = 404 return common.SimpleResponse(404, "No such user was found!")
r.Message = "No such user was found!"
return
case err != nil: case err != nil:
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
user.RegisteredOn = time.Unix(registeredOn, 0) user.RegisteredOn = time.Unix(registeredOn, 0)
@ -94,9 +88,8 @@ func userPuts(md common.MethodData, row *sql.Row) (r common.Response) {
user.Country = genCountry(md, user.ID, showCountry, user.Country) user.Country = genCountry(md, user.ID, showCountry, user.Country)
r.Code = 200 user.Code = 200
r.Data = user return user
return
} }
func badgesToArray(badges string) []int { func badgesToArray(badges string) []int {
@ -125,7 +118,7 @@ func genCountry(md common.MethodData, uid int, showCountry bool, country string)
} }
// UserSelfGET is a shortcut for /users/id/self. (/users/self) // UserSelfGET is a shortcut for /users/id/self. (/users/self)
func UserSelfGET(md common.MethodData) common.Response { func UserSelfGET(md common.MethodData) common.CodeMessager {
md.C.Params = append(md.C.Params, gin.Param{ md.C.Params = append(md.C.Params, gin.Param{
Key: "id", Key: "id",
Value: "self", Value: "self",
@ -133,23 +126,23 @@ func UserSelfGET(md common.MethodData) common.Response {
return UserByIDGET(md) return UserByIDGET(md)
} }
type whatIDResponse struct {
common.ResponseBase
ID int `json:"id"`
}
// UserWhatsTheIDGET is an API request that only returns an user's ID. // UserWhatsTheIDGET is an API request that only returns an user's ID.
func UserWhatsTheIDGET(md common.MethodData) common.Response { func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager {
var ( var (
id int r whatIDResponse
allowed int allowed int
) )
err := md.DB.QueryRow("SELECT id, allowed FROM users WHERE username = ? LIMIT 1", md.C.Param("username")).Scan(&id, &allowed) err := md.DB.QueryRow("SELECT id, allowed FROM users WHERE username = ? LIMIT 1", md.C.Param("username")).Scan(&r.ID, &allowed)
if err != nil || (allowed != 1 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) { if err != nil || (allowed != 1 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) {
return common.Response{ return common.SimpleResponse(404, "That user could not be found!")
Code: 404,
Message: "That user could not be found!",
}
}
return common.Response{
Code: 200,
Data: id,
} }
r.Code = 200
return r
} }
type modeData struct { type modeData struct {
@ -162,7 +155,8 @@ type modeData struct {
Accuracy float64 `json:"accuracy"` Accuracy float64 `json:"accuracy"`
GlobalLeaderboardRank int `json:"global_leaderboard_rank"` GlobalLeaderboardRank int `json:"global_leaderboard_rank"`
} }
type userFullData struct { type userFullResponse struct {
common.ResponseBase
userData userData
STD modeData `json:"std"` STD modeData `json:"std"`
Taiko modeData `json:"taiko"` Taiko modeData `json:"taiko"`
@ -174,7 +168,7 @@ type userFullData struct {
} }
// UserFullGET gets all of an user's information, with one exception: their userpage. // UserFullGET gets all of an user's information, with one exception: their userpage.
func UserFullGET(md common.MethodData) (r common.Response) { func UserFullGET(md common.MethodData) common.CodeMessager {
// Hellest query I've ever done. // Hellest query I've ever done.
query := ` query := `
SELECT SELECT
@ -214,7 +208,7 @@ WHERE users.id=? AND users.allowed = '1'
LIMIT 1 LIMIT 1
` `
// Fuck. // Fuck.
fd := userFullData{} r := userFullResponse{}
var ( var (
badges string badges string
country string country string
@ -223,63 +217,61 @@ LIMIT 1
latestActivity int64 latestActivity int64
) )
err := md.DB.QueryRow(query, md.C.Param("id")).Scan( err := md.DB.QueryRow(query, md.C.Param("id")).Scan(
&fd.ID, &fd.Username, &registeredOn, &fd.Rank, &latestActivity, &r.ID, &r.Username, &registeredOn, &r.Rank, &latestActivity,
&fd.UsernameAKA, &badges, &country, &showCountry, &r.UsernameAKA, &badges, &country, &showCountry,
&fd.PlayStyle, &fd.FavouriteMode, &r.PlayStyle, &r.FavouriteMode,
&fd.STD.RankedScore, &fd.STD.TotalScore, &fd.STD.PlayCount, &r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
&fd.STD.ReplaysWatched, &fd.STD.TotalHits, &fd.STD.Level, &r.STD.ReplaysWatched, &r.STD.TotalHits, &r.STD.Level,
&fd.STD.Accuracy, &fd.STD.GlobalLeaderboardRank, &r.STD.Accuracy, &r.STD.GlobalLeaderboardRank,
&fd.Taiko.RankedScore, &fd.Taiko.TotalScore, &fd.Taiko.PlayCount, &r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
&fd.Taiko.ReplaysWatched, &fd.Taiko.TotalHits, &fd.Taiko.Level, &r.Taiko.ReplaysWatched, &r.Taiko.TotalHits, &r.Taiko.Level,
&fd.Taiko.Accuracy, &fd.Taiko.GlobalLeaderboardRank, &r.Taiko.Accuracy, &r.Taiko.GlobalLeaderboardRank,
&fd.CTB.RankedScore, &fd.CTB.TotalScore, &fd.CTB.PlayCount, &r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
&fd.CTB.ReplaysWatched, &fd.CTB.TotalHits, &fd.CTB.Level, &r.CTB.ReplaysWatched, &r.CTB.TotalHits, &r.CTB.Level,
&fd.CTB.Accuracy, &fd.CTB.GlobalLeaderboardRank, &r.CTB.Accuracy, &r.CTB.GlobalLeaderboardRank,
&fd.Mania.RankedScore, &fd.Mania.TotalScore, &fd.Mania.PlayCount, &r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
&fd.Mania.ReplaysWatched, &fd.Mania.TotalHits, &fd.Mania.Level, &r.Mania.ReplaysWatched, &r.Mania.TotalHits, &r.Mania.Level,
&fd.Mania.Accuracy, &fd.Mania.GlobalLeaderboardRank, &r.Mania.Accuracy, &r.Mania.GlobalLeaderboardRank,
) )
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
r.Code = 404 return common.SimpleResponse(404, "That user could not be found!")
r.Message = "That user could not be found!"
return
case err != nil: case err != nil:
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
fd.Country = genCountry(md, fd.ID, showCountry, country) r.Country = genCountry(md, r.ID, showCountry, country)
fd.Badges = badgesToArray(badges) r.Badges = badgesToArray(badges)
fd.RegisteredOn = time.Unix(registeredOn, 0) r.RegisteredOn = time.Unix(registeredOn, 0)
fd.LatestActivity = time.Unix(latestActivity, 0) r.LatestActivity = time.Unix(latestActivity, 0)
r.Code = 200 r.Code = 200
r.Data = fd return r
return }
type userpageResponse struct {
common.ResponseBase
Userpage string `json:"userpage"`
} }
// UserUserpageGET gets an user's userpage, as in the customisable thing. // UserUserpageGET gets an user's userpage, as in the customisable thing.
func UserUserpageGET(md common.MethodData) (r common.Response) { func UserUserpageGET(md common.MethodData) common.CodeMessager {
var userpage string var r userpageResponse
err := md.DB.QueryRow("SELECT userpage_content FROM users_stats WHERE id = ? LIMIT 1", md.C.Param("id")).Scan(&userpage) err := md.DB.QueryRow("SELECT userpage_content FROM users_stats WHERE id = ? LIMIT 1", md.C.Param("id")).Scan(&r.Userpage)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
r.Code = 404 return common.SimpleResponse(404, "No user with that user ID!")
r.Message = "No user with that user ID!"
case err != nil: case err != nil:
md.Err(err) md.Err(err)
r = Err500 return Err500
return
} }
r.Code = 200 r.Code = 200
r.Data = userpage return r
return
} }

View File

@ -1,8 +1,36 @@
package common package common
// Response is the data that is always returned with an API request. // ResponseBase is the data that is always returned with an API request.
type Response struct { type ResponseBase struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Data interface{} `json:"data"` }
// GetCode retrieves the response code.
func (r ResponseBase) GetCode() int {
return r.Code
}
// SetCode changes the response code.
func (r *ResponseBase) SetCode(i int) {
r.Code = i
}
// GetMessage retrieves the response message.
func (r ResponseBase) GetMessage() string {
return r.Message
}
// CodeMessager is something that has the Code() and Message() methods.
type CodeMessager interface {
GetMessage() string
GetCode() int
}
// SimpleResponse returns the most basic response.
func SimpleResponse(code int, message string) CodeMessager {
return ResponseBase{
Code: code,
Message: message,
}
} }