From 3ddab1de155768f09c63bda7d96a706e19ee4a29 Mon Sep 17 00:00:00 2001 From: Howl Date: Sun, 3 Apr 2016 19:59:27 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + app/method.go | 63 +++++++++++++++++++++++ app/peppy/user.go | 2 + app/start.go | 33 ++++++++++++ app/tokens.go | 26 ++++++++++ app/v1/404.go | 15 ++++++ app/v1/errors.go | 13 +++++ app/v1/ping.go | 116 ++++++++++++++++++++++++++++++++++++++++++ app/v1/privileges.go | 39 ++++++++++++++ app/v1/user.go | 82 +++++++++++++++++++++++++++++ common/conf.go | 39 ++++++++++++++ common/method_data.go | 15 ++++++ common/privileges.go | 100 ++++++++++++++++++++++++++++++++++++ common/response.go | 7 +++ common/token.go | 8 +++ main.go | 23 +++++++++ 16 files changed, 583 insertions(+) create mode 100644 .gitignore create mode 100644 app/method.go create mode 100644 app/peppy/user.go create mode 100644 app/start.go create mode 100644 app/tokens.go create mode 100644 app/v1/404.go create mode 100644 app/v1/errors.go create mode 100644 app/v1/ping.go create mode 100644 app/v1/privileges.go create mode 100644 app/v1/user.go create mode 100644 common/conf.go create mode 100644 common/method_data.go create mode 100644 common/privileges.go create mode 100644 common/response.go create mode 100644 common/token.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..874eeb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +api +api.conf \ No newline at end of file diff --git a/app/method.go b/app/method.go new file mode 100644 index 0000000..373fb6d --- /dev/null +++ b/app/method.go @@ -0,0 +1,63 @@ +package app + +import ( + "database/sql" + "io/ioutil" + + "github.com/gin-gonic/gin" + "github.com/osuripple/api/common" +) + +// Method wraps an API method to a HandlerFunc. +func Method(f func(md common.MethodData) common.Response, db *sql.DB, privilegesNeeded ...int) gin.HandlerFunc { + return func(c *gin.Context) { + data, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + c.Error(err) + } + c.Request.Body.Close() + + token := "" + switch { + case c.Request.Header.Get("X-Ripple-Token") != "": + token = c.Request.Header.Get("X-Ripple-Token") + case c.Query("token") != "": + token = c.Query("token") + case c.Query("k") != "": + token = c.Query("k") + } + + md := common.MethodData{ + DB: db, + RequestData: data, + C: c, + } + if token != "" { + tokenReal, exists := GetTokenFull(token, db) + if exists { + md.User = tokenReal + } + } + + missingPrivileges := 0 + for _, privilege := range privilegesNeeded { + if int(md.User.Privileges)&privilege == 0 { + missingPrivileges |= privilege + } + } + if missingPrivileges != 0 { + c.IndentedJSON(401, common.Response{ + Code: 401, + Message: "You don't have the privilege(s): " + common.Privileges(missingPrivileges).String() + ".", + }) + return + } + + resp := f(md) + if resp.Code == 0 { + c.IndentedJSON(500, resp) + } else { + c.IndentedJSON(200, resp) + } + } +} diff --git a/app/peppy/user.go b/app/peppy/user.go new file mode 100644 index 0000000..15032f9 --- /dev/null +++ b/app/peppy/user.go @@ -0,0 +1,2 @@ +// Package peppy implements the osu! API as defined on the osu-api repository wiki (https://github.com/ppy/osu-api/wiki). +package peppy diff --git a/app/start.go b/app/start.go new file mode 100644 index 0000000..968e004 --- /dev/null +++ b/app/start.go @@ -0,0 +1,33 @@ +package app + +import ( + "database/sql" + + "github.com/gin-gonic/contrib/gzip" + "github.com/gin-gonic/gin" + "github.com/osuripple/api/app/v1" + "github.com/osuripple/api/common" +) + +// Start begins taking HTTP connections. +func Start(conf common.Conf, db *sql.DB) { + r := gin.Default() + r.Use(gzip.Gzip(gzip.DefaultCompression)) + + api := r.Group("/api") + { + gv1 := api.Group("/v1") + { + gv1.GET("/user/:id", Method(v1.UserGET, db, common.PrivilegeRead)) + gv1.GET("/ping", Method(v1.Ping, db)) + gv1.GET("/surprise_me", Method(v1.SurpriseMe, db)) + gv1.GET("/privileges", Method(v1.PrivilegesGET, db)) + } + } + + r.NoRoute(v1.Handle404) + if conf.Unix { + panic(r.RunUnix(conf.ListenTo)) + } + panic(r.Run(conf.ListenTo)) +} diff --git a/app/tokens.go b/app/tokens.go new file mode 100644 index 0000000..5b5a2c1 --- /dev/null +++ b/app/tokens.go @@ -0,0 +1,26 @@ +package app + +import ( + "database/sql" + + "github.com/osuripple/api/common" +) + +// GetTokenFull retrieves an user ID and their token privileges knowing their API token. +func GetTokenFull(token string, db *sql.DB) (common.Token, bool) { + var uid int + var privs int + err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", token).Scan(&uid, &privs) + switch { + case err == sql.ErrNoRows: + return common.Token{}, false + case err != nil: + panic(err) + default: + return common.Token{ + Value: token, + UserID: uid, + Privileges: common.Privileges(privs), + }, true + } +} diff --git a/app/v1/404.go b/app/v1/404.go new file mode 100644 index 0000000..2833b99 --- /dev/null +++ b/app/v1/404.go @@ -0,0 +1,15 @@ +package v1 + +import ( + "github.com/gin-gonic/gin" + "github.com/osuripple/api/common" +) + +// 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(), + }) +} diff --git a/app/v1/errors.go b/app/v1/errors.go new file mode 100644 index 0000000..bf5965f --- /dev/null +++ b/app/v1/errors.go @@ -0,0 +1,13 @@ +package v1 + +import ( + "github.com/osuripple/api/common" +) + +// Boilerplate errors +var ( + Err500 = common.Response{ + Code: 0, + Message: "An error occurred. Try again, perhaps?", + } +) diff --git a/app/v1/ping.go b/app/v1/ping.go new file mode 100644 index 0000000..f483314 --- /dev/null +++ b/app/v1/ping.go @@ -0,0 +1,116 @@ +package v1 + +import ( + "math/rand" + "time" + + "github.com/osuripple/api/common" +) + +var rn = rand.New(rand.NewSource(time.Now().UnixNano())) + +var kaomojis = [...]string{ + "Σ(ノ°▽°)ノ", + "( ƅ°ਉ°)ƅ", + "ヽ( ・∀・)ノ", + "˭̡̞(◞⁎˃ᆺ˂)◞*✰", + "(p^-^)p", + "(ノ^∇^)ノ゚", + "ヽ(〃・ω・)ノ", + "(۶* ‘ꆚ’)۶”", + "(。>ω<)。", + "(ノ。≧◇≦)ノ", + "ヾ(。・ω・)シ", + "(ノ・д・)ノ", + ".+:。(ノ・ω・)ノ゙", + "Σ(*ノ´>ω<。`)ノ", + "ヾ(〃^∇^)ノ♪", + "\(@ ̄∇ ̄@)/", + "\(^▽^@)ノ", + "ヾ(@^▽^@)ノ", + "(((\(@v@)/)))", + "\(*T▽T*)/", + "\(^▽^)/", + "\(T∇T)/", + "ヽ( ★ω★)ノ", + "ヽ(;▽;)ノ", + "ヾ(。◕ฺ∀◕ฺ)ノ", + "ヾ(@† ▽ †@)ノ", + "ヾ(@^∇^@)ノ", + "ヾ(@^▽^@)ノ", + "ヾ(@^▽^@)ノ", + "ヾ(@゜▽゜@)ノ", + "(.=^・ェ・^=)", + "((≡^⚲͜^≡))", + "(^・o・^)ノ”", + "(^._.^)ノ", + "(^人^)", + "(=;ェ;=)", + "(=`ω´=)", + "(=`ェ´=)", + "(=´∇`=)", + "(=^・^=)", + "(=^・ェ・^=)", + "(=^‥^=)", + "(=TェT=)", + "(=xェx=)", + "\(=^‥^)/’`", + "~(=^‥^)/", + "└(=^‥^=)┐", + "ヾ(=゚・゚=)ノ", + "ヽ(=^・ω・^=)丿", + "d(=^・ω・^=)b", + "o(^・x・^)o", + "V(=^・ω・^=)v", + "(⁎˃ᆺ˂)", + "(,,^・⋏・^,,)", +} + +var randomSentences = [...]string{ + "Proudly sponsored by Kirotuso!", + "The brace is on fire!", + "deverupa ga daisuki!", + "It works!!!!", + "Feelin' groovy!", + "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", +} + +func surpriseMe() string { + return randomSentences[rn.Intn(len(randomSentences))] + " " + kaomojis[rn.Intn(len(kaomojis))] +} + +type pingData struct { + ID int `json:"user_id"` + Privileges int `json:"privileges"` +} + +// Ping is a message to check with the API that we are logged in, and know what are our privileges. +func Ping(md common.MethodData) (r common.Response) { + r.Code = 200 + if md.User.UserID == 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/login " + kaomojis[rn.Intn(len(kaomojis))] + } else { + r.Message = surpriseMe() + } + r.Data = pingData{ + ID: md.User.UserID, + Privileges: int(md.User.Privileges), + } + return +} + +// SurpriseMe generates cute cats. +// +// ... Yes. +func SurpriseMe(md common.MethodData) (r common.Response) { + r.Code = 200 + cats := make([]string, 100) + for i := 0; i < 100; i++ { + cats[i] = surpriseMe() + } + r.Data = cats + return +} diff --git a/app/v1/privileges.go b/app/v1/privileges.go new file mode 100644 index 0000000..b4fb534 --- /dev/null +++ b/app/v1/privileges.go @@ -0,0 +1,39 @@ +package v1 + +import ( + "github.com/osuripple/api/common" +) + +type privilegesData struct { + PrivilegeRead bool `json:"read"` + PrivilegeReadConfidential bool `json:"read_confidential"` + PrivilegeWrite bool `json:"write"` + PrivilegeManageBadges bool `json:"manage_badges"` + PrivilegeBetaKeys bool `json:"beta_keys"` + PrivilegeManageSettings bool `json:"manage_settings"` + PrivilegeViewUserAdvanced bool `json:"view_user_advanced"` + PrivilegeManageUser bool `json:"manage_user"` + PrivilegeManageRoles bool `json:"manage_roles"` + PrivilegeManageAPIKeys bool `json:"manage_api_keys"` + PrivilegeBlog bool `json:"blog"` +} + +// 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. + r.Code = 200 + r.Data = privilegesData{ + PrivilegeRead: md.User.Privileges.HasPrivilegeRead(), + PrivilegeReadConfidential: md.User.Privileges.HasPrivilegeReadConfidential(), + PrivilegeWrite: md.User.Privileges.HasPrivilegeWrite(), + PrivilegeManageBadges: md.User.Privileges.HasPrivilegeManageBadges(), + PrivilegeBetaKeys: md.User.Privileges.HasPrivilegeBetaKeys(), + PrivilegeManageSettings: md.User.Privileges.HasPrivilegeManageSettings(), + PrivilegeViewUserAdvanced: md.User.Privileges.HasPrivilegeViewUserAdvanced(), + PrivilegeManageUser: md.User.Privileges.HasPrivilegeManageUser(), + PrivilegeManageRoles: md.User.Privileges.HasPrivilegeManageRoles(), + PrivilegeManageAPIKeys: md.User.Privileges.HasPrivilegeManageAPIKeys(), + PrivilegeBlog: md.User.Privileges.HasPrivilegeBlog(), + } + return +} diff --git a/app/v1/user.go b/app/v1/user.go new file mode 100644 index 0000000..b5f67db --- /dev/null +++ b/app/v1/user.go @@ -0,0 +1,82 @@ +// Package v1 implements the first version of the Ripple API. +package v1 + +import ( + "database/sql" + "fmt" + "strconv" + "strings" + "time" + + "github.com/osuripple/api/common" +) + +type userData struct { + ID int `json:"id"` + Username string `json:"username"` + UsernameAKA string `json:"username_aka"` + RegisteredOn time.Time `json:"registered_on"` + Rank int `json:"rank"` + LatestActivity time.Time `json:"latest_activity"` + Country string `json:"country"` + Badges []int `json:"badges"` +} + +// UserGET is the API handler for GET /user/:id +func UserGET(md common.MethodData) (r common.Response) { + var err error + var uid int + uidStr := md.C.Param("id") + if uidStr == "self" { + uid = md.User.UserID + } else { + uid, err = strconv.Atoi(uidStr) + if err != nil { + r.Code = 400 + r.Message = fmt.Sprintf("%s ain't a number", uidStr) + return + } + } + + user := userData{} + + registeredOn := int64(0) + latestActivity := int64(0) + var badges string + var showcountry bool + err = md.DB.QueryRow("SELECT users.id, users.username, register_datetime, rank, latest_activity, users_stats.username_aka, users_stats.badges_shown, users_stats.country, users_stats.show_country FROM users LEFT JOIN users_stats ON users.id=users_stats.id WHERE users.id=?", uid).Scan( + &user.ID, &user.Username, ®isteredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &badges, &user.Country, &showcountry) + switch { + case err == sql.ErrNoRows: + r.Code = 404 + r.Message = "No such user was found!" + return + case err != nil: + md.C.Error(err) + r = Err500 + return + } + + user.RegisteredOn = time.Unix(registeredOn, 0) + user.LatestActivity = time.Unix(latestActivity, 0) + + badgesSl := strings.Split(badges, ",") + for _, badge := range badgesSl { + if badge != "" && badge != "0" { + // We are ignoring errors because who really gives a shit if something's gone wrong on our end in this + // particular thing, we can just silently ignore this. + nb, err := strconv.Atoi(badge) + if err == nil && nb != 0 { + user.Badges = append(user.Badges, nb) + } + } + } + + if !showcountry { + user.Country = "XX" + } + + r.Code = 200 + r.Data = user + return +} diff --git a/common/conf.go b/common/conf.go new file mode 100644 index 0000000..9f7d2fb --- /dev/null +++ b/common/conf.go @@ -0,0 +1,39 @@ +package common + +import ( + "fmt" + + "github.com/thehowl/conf" +) + +// Conf is the configuration file data for the ripple API. +// Conf uses https://github.com/thehowl/conf +type Conf struct { + DatabaseType string `description:"At the moment, 'mysql' is the only supported database type."` + DSN string `description:"The Data Source Name for the database. More: https://github.com/go-sql-driver/mysql#dsn-data-source-name"` + ListenTo string `description:"The IP/Port combination from which to take connections, e.g. :8080"` + Unix bool `description:"Bool indicating whether ListenTo is a UNIX socket or an address."` +} + +var cachedConf *Conf + +// Load creates a new Conf, using the data in the file "api.conf". +func Load() (c Conf, halt bool) { + if cachedConf != nil { + c = *cachedConf + return + } + err := conf.Load(&c, "api.conf") + halt = err == conf.ErrNoFile + if halt { + conf.MustExport(Conf{ + DatabaseType: "mysql", + DSN: "root@/ripple", + ListenTo: ":40001", + Unix: false, + }, "api.conf") + fmt.Println("Please compile the configuration file (api.conf).") + } + cachedConf = &c + return +} diff --git a/common/method_data.go b/common/method_data.go new file mode 100644 index 0000000..d2f50eb --- /dev/null +++ b/common/method_data.go @@ -0,0 +1,15 @@ +package common + +import ( + "database/sql" + + "github.com/gin-gonic/gin" +) + +// MethodData is a struct containing the data passed over to an API method. +type MethodData struct { + User Token + DB *sql.DB + RequestData []byte + C *gin.Context +} diff --git a/common/privileges.go b/common/privileges.go new file mode 100644 index 0000000..06bab1a --- /dev/null +++ b/common/privileges.go @@ -0,0 +1,100 @@ +package common + +import "strings" + +// These are the various privileges a token can have. +const ( + PrivilegeRead = 1 << iota // pretty much public data: leaderboard, scores, user profiles (without confidential stuff like email) + PrivilegeReadConfidential // (eventual) private messages, reports... of self + PrivilegeWrite // change user information, write into confidential stuff... + PrivilegeManageBadges // can change various users' badges. + PrivilegeBetaKeys // can add, remove, upgrade/downgrade, make public beta keys. + PrivilegeManageSettings // maintainance, set registrations, global alerts, bancho settings + PrivilegeViewUserAdvanced // can see user email, and perhaps warnings in the future, basically. + PrivilegeManageUser // can change user email, allowed status, userpage, rank, username... + PrivilegeManageRoles // translates as admin, as they can basically assign roles to anyone, even themselves + PrivilegeManageAPIKeys // admin permission to manage user permission, not only self permissions. Only ever do this if you completely trust the application, because this essentially means to put the entire ripple database in the hands of a (potentially evil?) application. + PrivilegeBlog // can do pretty much anything to the blog, and the documentation. +) + +// Privileges is a bitwise enum of the privileges of an user's API key. +type Privileges int + +// HasPrivilegeRead returns whether the Read privilege is included in the privileges. +func (p Privileges) HasPrivilegeRead() bool { + return p&PrivilegeRead != 0 +} + +// HasPrivilegeReadConfidential returns whether the ReadConfidential privilege is included in the privileges. +func (p Privileges) HasPrivilegeReadConfidential() bool { + return p&PrivilegeReadConfidential != 0 +} + +// HasPrivilegeWrite returns whether the Write privilege is included in the privileges. +func (p Privileges) HasPrivilegeWrite() bool { + return p&PrivilegeWrite != 0 +} + +// HasPrivilegeManageBadges returns whether the ManageBadges privilege is included in the privileges. +func (p Privileges) HasPrivilegeManageBadges() bool { + return p&PrivilegeManageBadges != 0 +} + +// HasPrivilegeBetaKeys returns whether the BetaKeys privilege is included in the privileges. +func (p Privileges) HasPrivilegeBetaKeys() bool { + return p&PrivilegeBetaKeys != 0 +} + +// HasPrivilegeManageSettings returns whether the ManageSettings privilege is included in the privileges. +func (p Privileges) HasPrivilegeManageSettings() bool { + return p&PrivilegeManageSettings != 0 +} + +// HasPrivilegeViewUserAdvanced returns whether the ViewUserAdvanced privilege is included in the privileges. +func (p Privileges) HasPrivilegeViewUserAdvanced() bool { + return p&PrivilegeViewUserAdvanced != 0 +} + +// HasPrivilegeManageUser returns whether the ManageUser privilege is included in the privileges. +func (p Privileges) HasPrivilegeManageUser() bool { + return p&PrivilegeManageUser != 0 +} + +// HasPrivilegeManageRoles returns whether the ManageRoles privilege is included in the privileges. +func (p Privileges) HasPrivilegeManageRoles() bool { + return p&PrivilegeManageRoles != 0 +} + +// HasPrivilegeManageAPIKeys returns whether the ManageAPIKeys privilege is included in the privileges. +func (p Privileges) HasPrivilegeManageAPIKeys() bool { + return p&PrivilegeManageAPIKeys != 0 +} + +// HasPrivilegeBlog returns whether the Blog privilege is included in the privileges. +func (p Privileges) HasPrivilegeBlog() bool { + return p&PrivilegeBlog != 0 +} + +var privilegeString = [...]string{ + "Read", + "ReadConfidential", + "Write", + "ManageBadges", + "BetaKeys", + "ManageSettings", + "ViewUserAdvanced", + "ManageUser", + "ManageRoles", + "ManageAPIKeys", + "Blog", +} + +func (p Privileges) String() string { + var pvs []string + for i, v := range privilegeString { + if int(p)&(1<