From 7387ed4295a2e23635c9ed5b8b4dfd0dbc43138a Mon Sep 17 00:00:00 2001 From: Howl Date: Sat, 16 Apr 2016 18:05:24 +0200 Subject: [PATCH] Completely change response structure --- app/method.go | 21 ++++--- app/v1/404.go | 15 +++-- app/v1/badge.go | 44 ++++++++------- app/v1/errors.go | 14 ++--- app/v1/friend.go | 101 +++++++++++++++------------------ app/v1/manage_user.go | 15 ++--- app/v1/meta.go | 50 ++++++++--------- app/v1/ping.go | 36 +++++++----- app/v1/privileges.go | 34 +++++------ app/v1/token.go | 61 ++++++++------------ app/v1/user.go | 128 ++++++++++++++++++++---------------------- common/response.go | 38 +++++++++++-- 12 files changed, 280 insertions(+), 277 deletions(-) diff --git a/app/method.go b/app/method.go index 7ee9ddf..e1475c2 100644 --- a/app/method.go +++ b/app/method.go @@ -10,13 +10,13 @@ import ( ) // 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) { 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) if err != nil { c.Error(err) @@ -52,22 +52,25 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.Respon } } if missingPrivileges != 0 { - c.IndentedJSON(401, common.Response{ - Code: 401, - Message: "You don't have the privilege(s): " + common.Privileges(missingPrivileges).String() + ".", - }) + c.IndentedJSON(401, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+".")) return } resp := f(md) - if resp.Code == 0 { - resp.Code = 500 + if resp.GetCode() == 0 { + // 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 { c.Writer.WriteHeader(200) } else { - c.Writer.WriteHeader(resp.Code) + c.Writer.WriteHeader(resp.GetCode()) } if _, exists := c.GetQuery("callback"); exists { diff --git a/app/v1/404.go b/app/v1/404.go index 2833b99..b8408df 100644 --- a/app/v1/404.go +++ b/app/v1/404.go @@ -5,11 +5,18 @@ import ( "github.com/osuripple/api/common" ) +type response404 struct { + common.ResponseBase + Cats string `json:"cats"` +} + // Handle404 handles requests with no implemented handlers. func Handle404(c *gin.Context) { - c.IndentedJSON(404, common.Response{ - Code: 404, - Message: "Oh dear... that API request could not be found! Perhaps the API is not up-to-date? Either way, have a surprise!", - Data: surpriseMe(), + c.IndentedJSON(404, response404{ + ResponseBase: common.ResponseBase{ + Code: 404, + 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(), }) } diff --git a/app/v1/badge.go b/app/v1/badge.go index 001d8b4..5510bde 100644 --- a/app/v1/badge.go +++ b/app/v1/badge.go @@ -6,53 +6,57 @@ import ( "github.com/osuripple/api/common" ) -type badgeData struct { +type singleBadge struct { ID int `json:"id"` Name string `json:"name"` Icon string `json:"icon"` } +type badgeData struct { + common.ResponseBase + singleBadge +} + // BadgeByIDGET is the handler for /badge/:id -func BadgeByIDGET(md common.MethodData) (r common.Response) { - b := badgeData{} +func BadgeByIDGET(md common.MethodData) common.CodeMessager { + 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) switch { case err == sql.ErrNoRows: - r.Code = 404 - r.Message = "No such badge was found" - return + return common.SimpleResponse(404, "No such badge was found") case err != nil: md.Err(err) - r = Err500 - return + return Err500 } - r.Code = 200 - r.Data = b - return + b.Code = 200 + return b +} + +type multiBadgeData struct { + common.ResponseBase + Badges []singleBadge `json:"badges"` } // BadgesGET retrieves all the badges on this ripple instance. -func BadgesGET(md common.MethodData) (r common.Response) { - var badges []badgeData +func BadgesGET(md common.MethodData) common.CodeMessager { + var r multiBadgeData rows, err := md.DB.Query("SELECT id, name, icon FROM badges") if err != nil { md.Err(err) - r = Err500 - return + return Err500 } defer rows.Close() for rows.Next() { - nb := badgeData{} + nb := singleBadge{} err = rows.Scan(&nb.ID, &nb.Name, &nb.Icon) if err != nil { md.Err(err) } - badges = append(badges, nb) + r.Badges = append(r.Badges, nb) } if err := rows.Err(); err != nil { md.Err(err) } - r.Code = 200 - r.Data = badges - return + r.ResponseBase.Code = 200 + return r } diff --git a/app/v1/errors.go b/app/v1/errors.go index 4d7fabe..7cf4fb2 100644 --- a/app/v1/errors.go +++ b/app/v1/errors.go @@ -8,19 +8,13 @@ import ( // Boilerplate errors var ( - Err500 = common.Response{ - Code: 500, - Message: "An error occurred. Try again, perhaps?", - } - ErrBadJSON = common.Response{ - Code: 400, - Message: "There was an error processing your JSON data.", - } + 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.") + ErrBadJSON = common.SimpleResponse(400, "Your JSON for this request is invalid.") ) // ErrMissingField generates a response to a request when some fields in the JSON are missing. -func ErrMissingField(missingFields ...string) common.Response { - return common.Response{ +func ErrMissingField(missingFields ...string) common.CodeMessager { + return common.ResponseBase{ Code: 422, // http://stackoverflow.com/a/10323055/5328069 Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".", } diff --git a/app/v1/friend.go b/app/v1/friend.go index badc3ae..c4dc2b4 100644 --- a/app/v1/friend.go +++ b/app/v1/friend.go @@ -13,15 +13,19 @@ type friendData struct { IsMutual bool `json:"is_mutual"` } +type friendsGETResponse struct { + common.ResponseBase + Friends []friendData `json:"friends"` +} + // FriendsGET is the API request handler for GET /friends. // 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 myFriendersRaw, err := md.DB.Query("SELECT user1 FROM users_relationships WHERE user2 = ?", md.ID()) if err != nil { md.Err(err) - r = Err500 - return + return Err500 } defer myFriendersRaw.Close() 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()) if err != nil { md.Err(err) - r = Err500 - return + return Err500 } var myFriends []friendData @@ -76,9 +79,10 @@ ORDER BY users_relationships.id` md.Err(err) } + r := friendsGETResponse{} r.Code = 200 - r.Data = myFriends - return + r.Friends = myFriends + return r } 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 } -type friendsWithData struct { +type friendsWithResponse struct { + common.ResponseBase Friends bool `json:"friend"` Mutual bool `json:"mutual"` } // 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 - var d friendsWithData uid, err := strconv.Atoi(md.C.Param("id")) if err != nil { - r.Data = d - return + return common.SimpleResponse(400, "That is not a number!") } - 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 { md.Err(err) - r = Err500 - return + return Err500 } - r.Data = d - return + if !r.Friends { + r.Mutual = false + } + return r } // 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") uid, err := strconv.Atoi(uidS) if err != nil { - return common.Response{ - Code: 400, - Message: "Nope. That's not a number.", - } + return common.SimpleResponse(400, "Nope. That's not a number.") } return addFriend(md, uid) } @@ -146,26 +148,21 @@ type friendAddPOSTData struct { } // 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{} err := md.RequestData.Unmarshal(&d) if err != nil { - r = ErrBadJSON - return + return ErrBadJSON } 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 { - r.Code = 400 - r.Message = "Just so you know: you can't add yourself to your friends." - return + return common.SimpleResponse(406, "Just so you know: you can't add yourself to your friends.") } if !userExists(md, u) { - r.Code = 404 - r.Message = "I'd also like to be friends with someone who doesn't even exist (???), however that's NOT POSSIBLE." - return + return common.SimpleResponse(404, "I'd also like to be friends with someone who doesn't even exist (???), however that's NOT POSSIBLE.") } var ( 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) if err != nil && err != sql.ErrNoRows { md.Err(err) - r = Err500 - return + return Err500 } if !relExists { _, err := md.DB.Exec("INSERT INTO users_relationships(user1, user2) VALUES (?, ?)", md.User.UserID, u) if err != nil { md.Err(err) - r = Err500 - return + return Err500 } } + var r friendsWithResponse r.Code = 200 - r.Data = friendsWithData{ - Friends: true, - Mutual: isMutual, - } - return + r.Friends = true + r.Mutual = isMutual + return r } // 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. -func FriendsDelGET(md common.MethodData) common.Response { +func FriendsDelGET(md common.MethodData) common.CodeMessager { uidS := md.C.Param("id") uid, err := strconv.Atoi(uidS) if err != nil { - return common.Response{ - Code: 400, - Message: "Nope. That's not a number.", - } + return common.SimpleResponse(400, "Nope. That's not a number.") } return delFriend(md, uid) } // FriendsDelPOST allows for deleting friends. -func FriendsDelPOST(md common.MethodData) (r common.Response) { +func FriendsDelPOST(md common.MethodData) common.CodeMessager { d := friendAddPOSTData{} err := md.RequestData.Unmarshal(&d) if err != nil { - r = ErrBadJSON - return + return ErrBadJSON } 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) if err != nil { md.Err(err) return Err500 } - return common.Response{ - Code: 200, - Data: friendsWithData{ - Friends: false, - Mutual: false, - }, + r := friendsWithResponse{ + Friends: false, + Mutual: false, } + r.Code = 200 + return r } diff --git a/app/v1/manage_user.go b/app/v1/manage_user.go index 0d83082..37f9eae 100644 --- a/app/v1/manage_user.go +++ b/app/v1/manage_user.go @@ -8,22 +8,18 @@ type setAllowedData struct { } // 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{} if err := md.RequestData.Unmarshal(&data); err != nil { - r = ErrBadJSON - return + return ErrBadJSON } if data.Allowed < 0 || data.Allowed > 2 { - r.Code = 400 - r.Message = "Allowed status must be between 0 and 2" - return + return common.SimpleResponse(400, "Allowed status must be between 0 and 2") } _, err := md.DB.Exec("UPDATE users SET allowed = ? WHERE id = ?", data.Allowed, data.UserID) if err != nil { md.Err(err) - r = Err500 - return + return Err500 } query := ` SELECT users.id, users.username, register_datetime, rank, @@ -34,6 +30,5 @@ LEFT JOIN users_stats ON users.id=users_stats.id WHERE users.id=? LIMIT 1` - r = userPuts(md, md.DB.QueryRow(query, data.UserID)) - return + return userPuts(md, md.DB.QueryRow(query, data.UserID)) } diff --git a/app/v1/meta.go b/app/v1/meta.go index 5d9a9c9..bde04f5 100644 --- a/app/v1/meta.go +++ b/app/v1/meta.go @@ -14,60 +14,59 @@ import ( ) // 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()) if err != nil { - r.Code = 500 - r.Message = "couldn't find process. what the fuck?" - return + return common.SimpleResponse(500, "couldn't find process. what the fuck?") } - r.Code = 200 - r.Message = "brb" go func() { time.Sleep(time.Second) proc.Signal(syscall.SIGUSR2) }() - return + return common.SimpleResponse(200, "brb") } // 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. -func MetaKillGET(md common.MethodData) (r common.Response) { +func MetaKillGET(md common.MethodData) common.CodeMessager { proc, err := os.FindProcess(syscall.Getpid()) if err != nil { - r.Code = 500 - r.Message = "couldn't find process. what the fuck?" - return + return common.SimpleResponse(500, "couldn't find process. what the fuck?") } 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 - r.Message = fmt.Sprintf("RIP ripple API %s - %s", upSince.Format(form), time.Now().Format(form)) go func() { time.Sleep(time.Second) proc.Kill() }() - return + return r } 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. // Mainly used to get if the API was restarted. -func MetaUpSinceGET(md common.MethodData) common.Response { - return common.Response{ - Code: 200, - Data: upSince.UnixNano(), +func MetaUpSinceGET(md common.MethodData) common.CodeMessager { + return metaUpSinceResponse{ + Code: 200, + Since: int64(upSince.UnixNano()), } } // 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() { - return common.Response{ - Code: 500, - Message: "repo is not using git", - } + return common.SimpleResponse(500, "instance is not using git") } go func() { // go get @@ -87,10 +86,7 @@ func MetaUpdateGET(md common.MethodData) common.Response { } proc.Signal(syscall.SIGUSR2) }() - return common.Response{ - Code: 200, - Message: "Started updating! " + surpriseMe(), - } + return common.SimpleResponse(200, "Started updating! "+surpriseMe()) } func execCommand(command string, args ...string) bool { diff --git a/app/v1/ping.go b/app/v1/ping.go index 392b7be..f4eaf33 100644 --- a/app/v1/ping.go +++ b/app/v1/ping.go @@ -75,42 +75,52 @@ var randomSentences = [...]string{ "sudo rm -rf /", "Hi! I'm Flowey! Flowey the flower!", "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 { return randomSentences[rn.Intn(len(randomSentences))] + " " + kaomojis[rn.Intn(len(kaomojis))] } -type pingData struct { +type pingResponse struct { + common.ResponseBase ID int `json:"user_id"` Privileges int `json:"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 + 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))] } else { r.Message = surpriseMe() } - r.Data = pingData{ - ID: md.ID(), - Privileges: int(md.User.Privileges), - } - return + + r.ID = md.ID() + r.Privileges = int(md.User.Privileges) + + return r +} + +type surpriseMeResponse struct { + common.ResponseBase + Cats [100]string `json:"cats"` } // SurpriseMeGET generates cute cats. // // ... Yes. -func SurpriseMeGET(md common.MethodData) (r common.Response) { +func SurpriseMeGET(md common.MethodData) common.CodeMessager { + var r surpriseMeResponse r.Code = 200 - cats := make([]string, 100) for i := 0; i < 100; i++ { - cats[i] = surpriseMe() + r.Cats[i] = surpriseMe() } - r.Data = cats - return + return r } diff --git a/app/v1/privileges.go b/app/v1/privileges.go index 8410356..f2072d9 100644 --- a/app/v1/privileges.go +++ b/app/v1/privileges.go @@ -5,6 +5,7 @@ import ( ) type privilegesData struct { + common.ResponseBase Read bool `json:"read"` ReadConfidential bool `json:"read_confidential"` 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. -func PrivilegesGET(md common.MethodData) (r common.Response) { - // This code sucks. +func PrivilegesGET(md common.MethodData) common.CodeMessager { + r := privilegesData{} r.Code = 200 - r.Data = privilegesData{ - Read: md.User.Privileges.HasPrivilegeRead(), - ReadConfidential: md.User.Privileges.HasPrivilegeReadConfidential(), - Write: md.User.Privileges.HasPrivilegeWrite(), - ManageBadges: md.User.Privileges.HasPrivilegeManageBadges(), - BetaKeys: md.User.Privileges.HasPrivilegeBetaKeys(), - ManageSettings: md.User.Privileges.HasPrivilegeManageSettings(), - ViewUserAdvanced: md.User.Privileges.HasPrivilegeViewUserAdvanced(), - ManageUser: md.User.Privileges.HasPrivilegeManageUser(), - ManageRoles: md.User.Privileges.HasPrivilegeManageRoles(), - ManageAPIKeys: md.User.Privileges.HasPrivilegeManageAPIKeys(), - Blog: md.User.Privileges.HasPrivilegeBlog(), - APIMeta: md.User.Privileges.HasPrivilegeAPIMeta(), - } - return + // This code sucks. + r.Read = md.User.Privileges.HasPrivilegeRead() + r.ReadConfidential = md.User.Privileges.HasPrivilegeReadConfidential() + r.Write = md.User.Privileges.HasPrivilegeWrite() + r.ManageBadges = md.User.Privileges.HasPrivilegeManageBadges() + r.BetaKeys = md.User.Privileges.HasPrivilegeBetaKeys() + r.ManageSettings = md.User.Privileges.HasPrivilegeManageSettings() + r.ViewUserAdvanced = md.User.Privileges.HasPrivilegeViewUserAdvanced() + r.ManageUser = md.User.Privileges.HasPrivilegeManageUser() + r.ManageRoles = md.User.Privileges.HasPrivilegeManageRoles() + r.ManageAPIKeys = md.User.Privileges.HasPrivilegeManageAPIKeys() + r.Blog = md.User.Privileges.HasPrivilegeBlog() + r.APIMeta = md.User.Privileges.HasPrivilegeAPIMeta() + return r } diff --git a/app/v1/token.go b/app/v1/token.go index 64608d2..78a6212 100644 --- a/app/v1/token.go +++ b/app/v1/token.go @@ -19,7 +19,8 @@ type tokenNewInData struct { Description string `json:"description"` } -type tokenNewOutData struct { +type tokenNewResponse struct { + common.ResponseBase Username string `json:"username"` ID int `json:"id"` Privileges int `json:"privileges"` @@ -28,12 +29,12 @@ type tokenNewOutData struct { } // 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{} err := md.RequestData.Unmarshal(&data) if err != nil { - r = ErrBadJSON - return + return ErrBadJSON } var miss []string @@ -44,8 +45,7 @@ func TokenNewPOST(md common.MethodData) (r common.Response) { miss = append(miss, "password") } if len(miss) != 0 { - r = ErrMissingField(miss...) - return + return ErrMissingField(miss...) } 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) } - ret := tokenNewOutData{} var ( rank int pw string @@ -64,48 +63,37 @@ func TokenNewPOST(md common.MethodData) (r common.Response) { 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 { case err == sql.ErrNoRows: - r.Code = 404 - r.Message = "No user with that username/id was found." - return + return common.SimpleResponse(404, "No user with that username/id was found.") case err != nil: md.Err(err) - r = Err500 - return + return Err500 } - if nFailedAttempts(ret.ID) > 20 { - r.Code = 429 - r.Message = "You've made too many login attempts. Try again later." - return + if nFailedAttempts(r.ID) > 20 { + return common.SimpleResponse(429, "You've made too many login attempts. Try again later.") } if pwVersion == 1 { - r.Code = 418 // Teapots! - 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 + 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.") } if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil { if err == bcrypt.ErrMismatchedHashAndPassword { - go addFailedAttempt(ret.ID) - r.Code = 403 - r.Message = "That password doesn't match!" - return + go addFailedAttempt(r.ID) + return common.SimpleResponse(403, "That password doesn't match!") } md.Err(err) - r = Err500 - return + return Err500 } if allowed == 0 { r.Code = 200 r.Message = "That user is banned." - ret.Banned = true - r.Data = ret - return + r.Banned = true + return r } - ret.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank)) + r.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank)) var ( tokenStr string @@ -114,7 +102,7 @@ func TokenNewPOST(md common.MethodData) (r common.Response) { for { tokenStr = common.RandomString(32) tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr))) - ret.Token = tokenStr + r.Token = tokenStr id := 0 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 { md.Err(err) - r = Err500 - return + return Err500 } } - _, 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 { md.Err(err) - r = Err500 - return + return Err500 } r.Code = 200 - r.Data = ret - return + return r } diff --git a/app/v1/user.go b/app/v1/user.go index e96d2ae..2cb4926 100644 --- a/app/v1/user.go +++ b/app/v1/user.go @@ -13,6 +13,7 @@ import ( ) type userData struct { + common.ResponseBase ID int `json:"id"` Username string `json:"username"` UsernameAKA string `json:"username_aka"` @@ -23,7 +24,7 @@ type userData struct { } // 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 uid int uidStr := md.C.Param("id") @@ -32,9 +33,7 @@ func UserByIDGET(md common.MethodData) (r common.Response) { } else { uid, err = strconv.Atoi(uidStr) if err != nil { - r.Code = 400 - r.Message = fmt.Sprintf("%s ain't a number", uidStr) - return + return common.SimpleResponse(400, fmt.Sprintf("%s ain't a number", uidStr)) } } @@ -47,12 +46,11 @@ LEFT JOIN users_stats ON users.id=users_stats.id WHERE users.id=? AND users.allowed='1' LIMIT 1` - r = userPuts(md, md.DB.QueryRow(query, uid)) - return + return userPuts(md, md.DB.QueryRow(query, uid)) } // 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") query := ` @@ -64,11 +62,10 @@ LEFT JOIN users_stats ON users.id=users_stats.id WHERE users.username=? AND users.allowed='1' LIMIT 1` - r = userPuts(md, md.DB.QueryRow(query, username)) - return + return userPuts(md, md.DB.QueryRow(query, username)) } -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 user userData @@ -80,13 +77,10 @@ func userPuts(md common.MethodData, row *sql.Row) (r common.Response) { err = row.Scan(&user.ID, &user.Username, ®isteredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showCountry) switch { case err == sql.ErrNoRows: - r.Code = 404 - r.Message = "No such user was found!" - return + return common.SimpleResponse(404, "No such user was found!") case err != nil: md.Err(err) - r = Err500 - return + return Err500 } 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) - r.Code = 200 - r.Data = user - return + user.Code = 200 + return user } 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) -func UserSelfGET(md common.MethodData) common.Response { +func UserSelfGET(md common.MethodData) common.CodeMessager { md.C.Params = append(md.C.Params, gin.Param{ Key: "id", Value: "self", @@ -133,23 +126,23 @@ func UserSelfGET(md common.MethodData) common.Response { return UserByIDGET(md) } +type whatIDResponse struct { + common.ResponseBase + ID int `json:"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 ( - id int + r whatIDResponse 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()) { - return common.Response{ - Code: 404, - Message: "That user could not be found!", - } - } - return common.Response{ - Code: 200, - Data: id, + return common.SimpleResponse(404, "That user could not be found!") } + r.Code = 200 + return r } type modeData struct { @@ -162,7 +155,8 @@ type modeData struct { Accuracy float64 `json:"accuracy"` GlobalLeaderboardRank int `json:"global_leaderboard_rank"` } -type userFullData struct { +type userFullResponse struct { + common.ResponseBase userData STD modeData `json:"std"` Taiko modeData `json:"taiko"` @@ -174,7 +168,7 @@ type userFullData struct { } // 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. query := ` SELECT @@ -214,7 +208,7 @@ WHERE users.id=? AND users.allowed = '1' LIMIT 1 ` // Fuck. - fd := userFullData{} + r := userFullResponse{} var ( badges string country string @@ -223,63 +217,61 @@ LIMIT 1 latestActivity int64 ) err := md.DB.QueryRow(query, md.C.Param("id")).Scan( - &fd.ID, &fd.Username, ®isteredOn, &fd.Rank, &latestActivity, + &r.ID, &r.Username, ®isteredOn, &r.Rank, &latestActivity, - &fd.UsernameAKA, &badges, &country, &showCountry, - &fd.PlayStyle, &fd.FavouriteMode, + &r.UsernameAKA, &badges, &country, &showCountry, + &r.PlayStyle, &r.FavouriteMode, - &fd.STD.RankedScore, &fd.STD.TotalScore, &fd.STD.PlayCount, - &fd.STD.ReplaysWatched, &fd.STD.TotalHits, &fd.STD.Level, - &fd.STD.Accuracy, &fd.STD.GlobalLeaderboardRank, + &r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount, + &r.STD.ReplaysWatched, &r.STD.TotalHits, &r.STD.Level, + &r.STD.Accuracy, &r.STD.GlobalLeaderboardRank, - &fd.Taiko.RankedScore, &fd.Taiko.TotalScore, &fd.Taiko.PlayCount, - &fd.Taiko.ReplaysWatched, &fd.Taiko.TotalHits, &fd.Taiko.Level, - &fd.Taiko.Accuracy, &fd.Taiko.GlobalLeaderboardRank, + &r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount, + &r.Taiko.ReplaysWatched, &r.Taiko.TotalHits, &r.Taiko.Level, + &r.Taiko.Accuracy, &r.Taiko.GlobalLeaderboardRank, - &fd.CTB.RankedScore, &fd.CTB.TotalScore, &fd.CTB.PlayCount, - &fd.CTB.ReplaysWatched, &fd.CTB.TotalHits, &fd.CTB.Level, - &fd.CTB.Accuracy, &fd.CTB.GlobalLeaderboardRank, + &r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount, + &r.CTB.ReplaysWatched, &r.CTB.TotalHits, &r.CTB.Level, + &r.CTB.Accuracy, &r.CTB.GlobalLeaderboardRank, - &fd.Mania.RankedScore, &fd.Mania.TotalScore, &fd.Mania.PlayCount, - &fd.Mania.ReplaysWatched, &fd.Mania.TotalHits, &fd.Mania.Level, - &fd.Mania.Accuracy, &fd.Mania.GlobalLeaderboardRank, + &r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount, + &r.Mania.ReplaysWatched, &r.Mania.TotalHits, &r.Mania.Level, + &r.Mania.Accuracy, &r.Mania.GlobalLeaderboardRank, ) switch { case err == sql.ErrNoRows: - r.Code = 404 - r.Message = "That user could not be found!" - return + return common.SimpleResponse(404, "That user could not be found!") case err != nil: md.Err(err) - r = Err500 - return + return Err500 } - fd.Country = genCountry(md, fd.ID, showCountry, country) - fd.Badges = badgesToArray(badges) + r.Country = genCountry(md, r.ID, showCountry, country) + r.Badges = badgesToArray(badges) - fd.RegisteredOn = time.Unix(registeredOn, 0) - fd.LatestActivity = time.Unix(latestActivity, 0) + r.RegisteredOn = time.Unix(registeredOn, 0) + r.LatestActivity = time.Unix(latestActivity, 0) r.Code = 200 - r.Data = fd - return + return r +} + +type userpageResponse struct { + common.ResponseBase + Userpage string `json:"userpage"` } // UserUserpageGET gets an user's userpage, as in the customisable thing. -func UserUserpageGET(md common.MethodData) (r common.Response) { - var userpage string - err := md.DB.QueryRow("SELECT userpage_content FROM users_stats WHERE id = ? LIMIT 1", md.C.Param("id")).Scan(&userpage) +func UserUserpageGET(md common.MethodData) common.CodeMessager { + var r userpageResponse + err := md.DB.QueryRow("SELECT userpage_content FROM users_stats WHERE id = ? LIMIT 1", md.C.Param("id")).Scan(&r.Userpage) switch { case err == sql.ErrNoRows: - r.Code = 404 - r.Message = "No user with that user ID!" + return common.SimpleResponse(404, "No user with that user ID!") case err != nil: md.Err(err) - r = Err500 - return + return Err500 } r.Code = 200 - r.Data = userpage - return + return r } diff --git a/common/response.go b/common/response.go index f96e5be..b17ad6b 100644 --- a/common/response.go +++ b/common/response.go @@ -1,8 +1,36 @@ package common -// Response is the data that is always returned with an API request. -type Response struct { - Code int `json:"code"` - Message string `json:"message,omitempty"` - Data interface{} `json:"data"` +// ResponseBase is the data that is always returned with an API request. +type ResponseBase struct { + Code int `json:"code"` + Message string `json:"message,omitempty"` +} + +// 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, + } }