replace zxq.co/ripple/hanayo
This commit is contained in:
29
vendor/zxq.co/ripple/rippleapi/app/v1/404.go
vendored
Normal file
29
vendor/zxq.co/ripple/rippleapi/app/v1/404.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type response404 struct {
|
||||
common.ResponseBase
|
||||
Cats string `json:"cats"`
|
||||
}
|
||||
|
||||
// Handle404 handles requests with no implemented handlers.
|
||||
func Handle404(c *fasthttp.RequestCtx) {
|
||||
c.Response.Header.Add("X-Real-404", "yes")
|
||||
data, err := json.MarshalIndent(response404{
|
||||
ResponseBase: common.ResponseBase{
|
||||
Code: 404,
|
||||
},
|
||||
Cats: surpriseMe(),
|
||||
}, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.SetStatusCode(404)
|
||||
c.Write(data)
|
||||
}
|
82
vendor/zxq.co/ripple/rippleapi/app/v1/badge.go
vendored
Normal file
82
vendor/zxq.co/ripple/rippleapi/app/v1/badge.go
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type singleBadge struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type multiBadgeData struct {
|
||||
common.ResponseBase
|
||||
Badges []singleBadge `json:"badges"`
|
||||
}
|
||||
|
||||
// BadgesGET retrieves all the badges on this ripple instance.
|
||||
func BadgesGET(md common.MethodData) common.CodeMessager {
|
||||
var (
|
||||
r multiBadgeData
|
||||
rows *sql.Rows
|
||||
err error
|
||||
)
|
||||
if md.Query("id") != "" {
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ? LIMIT 1", md.Query("id"))
|
||||
} else {
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges " + common.Paginate(md.Query("p"), md.Query("l"), 50))
|
||||
}
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
nb := singleBadge{}
|
||||
err = rows.Scan(&nb.ID, &nb.Name, &nb.Icon)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
r.Badges = append(r.Badges, nb)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
r.ResponseBase.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type badgeMembersData struct {
|
||||
common.ResponseBase
|
||||
Members []userData `json:"members"`
|
||||
}
|
||||
|
||||
// BadgeMembersGET retrieves the people who have a certain badge.
|
||||
func BadgeMembersGET(md common.MethodData) common.CodeMessager {
|
||||
i := common.Int(md.Query("id"))
|
||||
if i == 0 {
|
||||
return ErrMissingField("id")
|
||||
}
|
||||
|
||||
var members badgeMembersData
|
||||
|
||||
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, register_datetime, users.privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM user_badges ub
|
||||
INNER JOIN users ON users.id = ub.user
|
||||
INNER JOIN users_stats ON users_stats.id = ub.user
|
||||
WHERE badge = ?
|
||||
ORDER BY id ASC `+common.Paginate(md.Query("p"), md.Query("l"), 50), i)
|
||||
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
members.Code = 200
|
||||
return members
|
||||
}
|
228
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap.go
vendored
Normal file
228
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap.go
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type difficulty struct {
|
||||
STD float64 `json:"std"`
|
||||
Taiko float64 `json:"taiko"`
|
||||
CTB float64 `json:"ctb"`
|
||||
Mania float64 `json:"mania"`
|
||||
}
|
||||
|
||||
type beatmap struct {
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
SongName string `json:"song_name"`
|
||||
AR float32 `json:"ar"`
|
||||
OD float32 `json:"od"`
|
||||
Difficulty float64 `json:"difficulty"`
|
||||
Diff2 difficulty `json:"difficulty2"` // fuck nyo
|
||||
MaxCombo int `json:"max_combo"`
|
||||
HitLength int `json:"hit_length"`
|
||||
Ranked int `json:"ranked"`
|
||||
RankedStatusFrozen int `json:"ranked_status_frozen"`
|
||||
LatestUpdate common.UnixTimestamp `json:"latest_update"`
|
||||
}
|
||||
|
||||
type beatmapResponse struct {
|
||||
common.ResponseBase
|
||||
beatmap
|
||||
}
|
||||
type beatmapSetResponse struct {
|
||||
common.ResponseBase
|
||||
Beatmaps []beatmap `json:"beatmaps"`
|
||||
}
|
||||
|
||||
type beatmapSetStatusData struct {
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
RankedStatus int `json:"ranked_status"`
|
||||
Frozen int `json:"frozen"`
|
||||
}
|
||||
|
||||
// BeatmapSetStatusPOST changes the ranked status of a beatmap, and whether
|
||||
// the beatmap ranked status is frozen. Or freezed. Freezed best meme 2k16
|
||||
func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
|
||||
var req beatmapSetStatusData
|
||||
md.Unmarshal(&req)
|
||||
|
||||
var miss []string
|
||||
if req.BeatmapsetID <= 0 && req.BeatmapID <= 0 {
|
||||
miss = append(miss, "beatmapset_id or beatmap_id")
|
||||
}
|
||||
if len(miss) != 0 {
|
||||
return ErrMissingField(miss...)
|
||||
}
|
||||
|
||||
if req.Frozen != 0 && req.Frozen != 1 {
|
||||
return common.SimpleResponse(400, "frozen status must be either 0 or 1")
|
||||
}
|
||||
if req.RankedStatus > 4 || -1 > req.RankedStatus {
|
||||
return common.SimpleResponse(400, "ranked status must be 5 < x < -2")
|
||||
}
|
||||
|
||||
param := req.BeatmapsetID
|
||||
if req.BeatmapID != 0 {
|
||||
err := md.DB.QueryRow("SELECT beatmapset_id FROM beatmaps WHERE beatmap_id = ? LIMIT 1", req.BeatmapID).Scan(¶m)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That beatmap could not be found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
|
||||
md.DB.Exec(`UPDATE beatmaps
|
||||
SET ranked = ?, ranked_status_freezed = ?
|
||||
WHERE beatmapset_id = ?`, req.RankedStatus, req.Frozen, param)
|
||||
|
||||
if req.BeatmapID > 0 {
|
||||
md.Ctx.Request.URI().QueryArgs().SetUint("bb", req.BeatmapID)
|
||||
} else {
|
||||
md.Ctx.Request.URI().QueryArgs().SetUint("s", req.BeatmapsetID)
|
||||
}
|
||||
return getMultipleBeatmaps(md)
|
||||
}
|
||||
|
||||
// BeatmapGET retrieves a beatmap.
|
||||
func BeatmapGET(md common.MethodData) common.CodeMessager {
|
||||
beatmapID := common.Int(md.Query("b"))
|
||||
if beatmapID != 0 {
|
||||
return getBeatmapSingle(md, beatmapID)
|
||||
}
|
||||
return getMultipleBeatmaps(md)
|
||||
}
|
||||
|
||||
const baseBeatmapSelect = `
|
||||
SELECT
|
||||
beatmap_id, beatmapset_id, beatmap_md5,
|
||||
song_name, ar, od, difficulty_std, difficulty_taiko,
|
||||
difficulty_ctb, difficulty_mania, max_combo,
|
||||
hit_length, ranked, ranked_status_freezed,
|
||||
latest_update
|
||||
FROM beatmaps
|
||||
`
|
||||
|
||||
func getMultipleBeatmaps(md common.MethodData) common.CodeMessager {
|
||||
sort := common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"beatmapset_id",
|
||||
"beatmap_id",
|
||||
"id",
|
||||
"ar",
|
||||
"od",
|
||||
"difficulty_std",
|
||||
"difficulty_taiko",
|
||||
"difficulty_ctb",
|
||||
"difficulty_mania",
|
||||
"max_combo",
|
||||
"latest_update",
|
||||
"playcount",
|
||||
"passcount",
|
||||
},
|
||||
Default: "id DESC",
|
||||
Table: "beatmaps",
|
||||
})
|
||||
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
|
||||
where := common.
|
||||
Where("song_name = ?", md.Query("song_name")).
|
||||
Where("ranked_status_freezed = ?", md.Query("ranked_status_frozen"), "0", "1").
|
||||
In("beatmap_id", pm("bb")...).
|
||||
In("beatmapset_id", pm("s")...).
|
||||
In("beatmap_md5", pm("md5")...)
|
||||
|
||||
rows, err := md.DB.Query(baseBeatmapSelect+
|
||||
where.Clause+" "+sort+" "+
|
||||
common.Paginate(md.Query("p"), md.Query("l"), 50), where.Params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapSetResponse
|
||||
for rows.Next() {
|
||||
var b beatmap
|
||||
err = rows.Scan(
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
|
||||
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
|
||||
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
|
||||
&b.LatestUpdate,
|
||||
)
|
||||
b.Difficulty = b.Diff2.STD
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Beatmaps = append(r.Beatmaps, b)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getBeatmapSingle(md common.MethodData, beatmapID int) common.CodeMessager {
|
||||
var b beatmap
|
||||
err := md.DB.QueryRow(baseBeatmapSelect+"WHERE beatmap_id = ? LIMIT 1", beatmapID).Scan(
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
|
||||
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
|
||||
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
|
||||
&b.LatestUpdate,
|
||||
)
|
||||
b.Difficulty = b.Diff2.STD
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That beatmap could not be found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapResponse
|
||||
r.Code = 200
|
||||
r.beatmap = b
|
||||
return r
|
||||
}
|
||||
|
||||
type beatmapReduced struct {
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
Ranked int `json:"ranked"`
|
||||
RankedStatusFrozen int `json:"ranked_status_frozen"`
|
||||
}
|
||||
|
||||
type beatmapRankedFrozenFullResponse struct {
|
||||
common.ResponseBase
|
||||
Beatmaps []beatmapReduced `json:"beatmaps"`
|
||||
}
|
||||
|
||||
// BeatmapRankedFrozenFullGET retrieves all beatmaps with a certain
|
||||
// ranked_status_freezed
|
||||
func BeatmapRankedFrozenFullGET(md common.MethodData) common.CodeMessager {
|
||||
rows, err := md.DB.Query(`
|
||||
SELECT beatmap_id, beatmapset_id, beatmap_md5, ranked, ranked_status_freezed
|
||||
FROM beatmaps
|
||||
WHERE ranked_status_freezed = '1'
|
||||
`)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapRankedFrozenFullResponse
|
||||
for rows.Next() {
|
||||
var b beatmapReduced
|
||||
err = rows.Scan(&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5, &b.Ranked, &b.RankedStatusFrozen)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Beatmaps = append(r.Beatmaps, b)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
159
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap_requests.go
vendored
Normal file
159
vendor/zxq.co/ripple/rippleapi/app/v1/beatmap_requests.go
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/limit"
|
||||
)
|
||||
|
||||
type rankRequestsStatusResponse struct {
|
||||
common.ResponseBase
|
||||
QueueSize int `json:"queue_size"`
|
||||
MaxPerUser int `json:"max_per_user"`
|
||||
Submitted int `json:"submitted"`
|
||||
SubmittedByUser *int `json:"submitted_by_user,omitempty"`
|
||||
CanSubmit *bool `json:"can_submit,omitempty"`
|
||||
NextExpiration *time.Time `json:"next_expiration"`
|
||||
}
|
||||
|
||||
// BeatmapRankRequestsStatusGET gets the current status for beatmap ranking requests.
|
||||
func BeatmapRankRequestsStatusGET(md common.MethodData) common.CodeMessager {
|
||||
c := common.GetConf()
|
||||
rows, err := md.DB.Query("SELECT userid, time FROM rank_requests WHERE time > ? ORDER BY id ASC LIMIT "+strconv.Itoa(c.RankQueueSize), time.Now().Add(-time.Hour*24).Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r rankRequestsStatusResponse
|
||||
// if it's not auth-free access and we have got ReadConfidential, we can
|
||||
// know if this user can submit beatmaps or not.
|
||||
hasConfid := md.ID() != 0 && md.User.TokenPrivileges&common.PrivilegeReadConfidential > 0
|
||||
if hasConfid {
|
||||
r.SubmittedByUser = new(int)
|
||||
}
|
||||
isFirst := true
|
||||
for rows.Next() {
|
||||
var (
|
||||
user int
|
||||
timestamp common.UnixTimestamp
|
||||
)
|
||||
err := rows.Scan(&user, ×tamp)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
// if the user submitted this rank request, increase the number of
|
||||
// rank requests submitted by this user
|
||||
if user == md.ID() && r.SubmittedByUser != nil {
|
||||
(*r.SubmittedByUser)++
|
||||
}
|
||||
// also, if this is the first result, it means it will be the next to
|
||||
// expire.
|
||||
if isFirst {
|
||||
x := time.Time(timestamp)
|
||||
r.NextExpiration = &x
|
||||
isFirst = false
|
||||
}
|
||||
r.Submitted++
|
||||
}
|
||||
r.QueueSize = c.RankQueueSize
|
||||
r.MaxPerUser = c.BeatmapRequestsPerUser
|
||||
if hasConfid {
|
||||
x := r.Submitted < r.QueueSize && *r.SubmittedByUser < r.MaxPerUser
|
||||
r.CanSubmit = &x
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type submitRequestData struct {
|
||||
ID int `json:"id"`
|
||||
SetID int `json:"set_id"`
|
||||
}
|
||||
|
||||
// BeatmapRankRequestsSubmitPOST submits a new beatmap for ranking approval.
|
||||
func BeatmapRankRequestsSubmitPOST(md common.MethodData) common.CodeMessager {
|
||||
var d submitRequestData
|
||||
err := md.Unmarshal(&d)
|
||||
if err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
// check json data is present
|
||||
if d.ID == 0 && d.SetID == 0 {
|
||||
return ErrMissingField("id|set_id")
|
||||
}
|
||||
|
||||
// you've been rate limited
|
||||
if !limit.NonBlockingRequest("rankrequest:u:"+strconv.Itoa(md.ID()), 5) {
|
||||
return common.SimpleResponse(429, "You may only try to request 5 beatmaps per minute.")
|
||||
}
|
||||
|
||||
// find out from BeatmapRankRequestsStatusGET if we can submit beatmaps.
|
||||
statusRaw := BeatmapRankRequestsStatusGET(md)
|
||||
status, ok := statusRaw.(rankRequestsStatusResponse)
|
||||
if !ok {
|
||||
// if it's not a rankRequestsStatusResponse, it means it's an error
|
||||
return statusRaw
|
||||
}
|
||||
if !*status.CanSubmit {
|
||||
return common.SimpleResponse(403, "It's not possible to do a rank request at this time.")
|
||||
}
|
||||
|
||||
w := common.
|
||||
Where("beatmap_id = ?", strconv.Itoa(d.ID)).Or().
|
||||
Where("beatmapset_id = ?", strconv.Itoa(d.SetID))
|
||||
|
||||
var ranked int
|
||||
err = md.DB.QueryRow("SELECT ranked FROM beatmaps "+w.Clause+" LIMIT 1", w.Params...).Scan(&ranked)
|
||||
if ranked >= 2 {
|
||||
return common.SimpleResponse(406, "That beatmap is already ranked.")
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
// move on
|
||||
case sql.ErrNoRows:
|
||||
data, _ := json.Marshal(d)
|
||||
md.R.Publish("lets:beatmap_updates", string(data))
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// type and value of beatmap rank request
|
||||
t := "b"
|
||||
v := d.ID
|
||||
if d.SetID != 0 {
|
||||
t = "s"
|
||||
v = d.SetID
|
||||
}
|
||||
err = md.DB.QueryRow("SELECT 1 FROM rank_requests WHERE bid = ? AND type = ? AND time > ?",
|
||||
v, t, time.Now().Add(-time.Hour*24).Unix()).Scan(new(int))
|
||||
|
||||
// error handling
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
break
|
||||
case nil:
|
||||
// we're returning a success because if the request was already sent in the past 24
|
||||
// hours, it's as if the user submitted it.
|
||||
return BeatmapRankRequestsStatusGET(md)
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
_, err = md.DB.Exec(
|
||||
"INSERT INTO rank_requests (userid, bid, type, time, blacklisted) VALUES (?, ?, ?, ?, 0)",
|
||||
md.ID(), v, t, time.Now().Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
return BeatmapRankRequestsStatusGET(md)
|
||||
}
|
184
vendor/zxq.co/ripple/rippleapi/app/v1/blog.go
vendored
Normal file
184
vendor/zxq.co/ripple/rippleapi/app/v1/blog.go
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// This basically proxies requests from Medium's API and is used on Ripple's
|
||||
// home page to display the latest blog posts.
|
||||
|
||||
type mediumResp struct {
|
||||
Success bool `json:"success"`
|
||||
Payload struct {
|
||||
Posts []mediumPost `json:"posts"`
|
||||
References struct {
|
||||
User map[string]mediumUser
|
||||
} `json:"references"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
type mediumPost struct {
|
||||
ID string `json:"id"`
|
||||
CreatorID string `json:"creatorId"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Virtuals mediumPostVirtuals `json:"virtuals"`
|
||||
ImportedURL string `json:"importedUrl"`
|
||||
UniqueSlug string `json:"uniqueSlug"`
|
||||
}
|
||||
|
||||
type mediumUser struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type mediumPostVirtuals struct {
|
||||
Subtitle string `json:"subtitle"`
|
||||
WordCount int `json:"wordCount"`
|
||||
ReadingTime float64 `json:"readingTime"`
|
||||
}
|
||||
|
||||
// there's gotta be a better way
|
||||
|
||||
type blogPost struct {
|
||||
ID string `json:"id"`
|
||||
Creator blogUser `json:"creator"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ImportedURL string `json:"imported_url"`
|
||||
UniqueSlug string `json:"unique_slug"`
|
||||
|
||||
Snippet string `json:"snippet"`
|
||||
WordCount int `json:"word_count"`
|
||||
ReadingTime float64 `json:"reading_time"`
|
||||
}
|
||||
|
||||
type blogUser struct {
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type blogPostsResponse struct {
|
||||
common.ResponseBase
|
||||
Posts []blogPost `json:"posts"`
|
||||
}
|
||||
|
||||
// consts for the medium API
|
||||
const (
|
||||
mediumAPIResponsePrefix = `])}while(1);</x>`
|
||||
mediumAPIAllPosts = `https://blog.ripple.moe/latest?format=json`
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register([]blogPost{})
|
||||
}
|
||||
|
||||
// BlogPostsGET retrieves the latest blog posts on the Ripple blog.
|
||||
func BlogPostsGET(md common.MethodData) common.CodeMessager {
|
||||
// check if posts are cached in redis
|
||||
res := md.R.Get("api:blog_posts").Val()
|
||||
if res != "" {
|
||||
// decode values
|
||||
posts := make([]blogPost, 0, 20)
|
||||
err := gob.NewDecoder(strings.NewReader(res)).Decode(&posts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// create response and return
|
||||
var r blogPostsResponse
|
||||
r.Code = 200
|
||||
r.Posts = blogLimit(posts, md.Query("l"))
|
||||
return r
|
||||
}
|
||||
|
||||
// get data from medium api
|
||||
resp, err := http.Get(mediumAPIAllPosts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// read body and trim the prefix
|
||||
all, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
all = bytes.TrimPrefix(all, []byte(mediumAPIResponsePrefix))
|
||||
|
||||
// unmarshal into response struct
|
||||
var mResp mediumResp
|
||||
err = json.Unmarshal(all, &mResp)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
if !mResp.Success {
|
||||
md.Err(errors.New("medium api call is not successful"))
|
||||
return Err500
|
||||
}
|
||||
|
||||
// create posts slice and fill it up with converted posts from the medium
|
||||
// API
|
||||
posts := make([]blogPost, len(mResp.Payload.Posts))
|
||||
for idx, mp := range mResp.Payload.Posts {
|
||||
var p blogPost
|
||||
|
||||
// convert structs
|
||||
p.ID = mp.ID
|
||||
p.Title = mp.Title
|
||||
p.CreatedAt = time.Unix(0, mp.CreatedAt*1000000)
|
||||
p.UpdatedAt = time.Unix(0, mp.UpdatedAt*1000000)
|
||||
p.ImportedURL = mp.ImportedURL
|
||||
p.UniqueSlug = mp.UniqueSlug
|
||||
|
||||
cr := mResp.Payload.References.User[mp.CreatorID]
|
||||
p.Creator.UserID = cr.UserID
|
||||
p.Creator.Name = cr.Name
|
||||
p.Creator.Username = cr.Username
|
||||
|
||||
p.Snippet = mp.Virtuals.Subtitle
|
||||
p.WordCount = mp.Virtuals.WordCount
|
||||
p.ReadingTime = mp.Virtuals.ReadingTime
|
||||
|
||||
posts[idx] = p
|
||||
}
|
||||
|
||||
// save in redis
|
||||
bb := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(bb).Encode(posts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
md.R.Set("api:blog_posts", bb.Bytes(), time.Minute*5)
|
||||
|
||||
var r blogPostsResponse
|
||||
r.Code = 200
|
||||
r.Posts = blogLimit(posts, md.Query("l"))
|
||||
return r
|
||||
}
|
||||
|
||||
func blogLimit(posts []blogPost, s string) []blogPost {
|
||||
i := common.Int(s)
|
||||
if i >= len(posts) || i < 1 {
|
||||
return posts
|
||||
}
|
||||
return posts[:i]
|
||||
}
|
21
vendor/zxq.co/ripple/rippleapi/app/v1/errors.go
vendored
Normal file
21
vendor/zxq.co/ripple/rippleapi/app/v1/errors.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Boilerplate errors
|
||||
var (
|
||||
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.CodeMessager {
|
||||
return common.ResponseBase{
|
||||
Code: 422, // http://stackoverflow.com/a/10323055/5328069
|
||||
Message: "Missing parameters: " + strings.Join(missingFields, ", ") + ".",
|
||||
}
|
||||
}
|
202
vendor/zxq.co/ripple/rippleapi/app/v1/friend.go
vendored
Normal file
202
vendor/zxq.co/ripple/rippleapi/app/v1/friend.go
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type friendData struct {
|
||||
userData
|
||||
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) 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)
|
||||
return Err500
|
||||
}
|
||||
defer myFriendersRaw.Close()
|
||||
for myFriendersRaw.Next() {
|
||||
var i int
|
||||
err := myFriendersRaw.Scan(&i)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
myFrienders = append(myFrienders, i)
|
||||
}
|
||||
if err := myFriendersRaw.Err(); err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
myFriendsQuery := `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users_relationships
|
||||
LEFT JOIN users
|
||||
ON users_relationships.user2 = users.id
|
||||
LEFT JOIN users_stats
|
||||
ON users_relationships.user2=users_stats.id
|
||||
WHERE users_relationships.user1=?
|
||||
`
|
||||
|
||||
myFriendsQuery += common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"id",
|
||||
"username",
|
||||
"latest_activity",
|
||||
},
|
||||
Default: "users.id asc",
|
||||
Table: "users",
|
||||
}) + "\n"
|
||||
|
||||
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.Query("p"), md.Query("l"), 100), md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var myFriends []friendData
|
||||
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
newFriend := friendPuts(md, results)
|
||||
for _, uid := range myFrienders {
|
||||
if uid == newFriend.ID {
|
||||
newFriend.IsMutual = true
|
||||
break
|
||||
}
|
||||
}
|
||||
myFriends = append(myFriends, newFriend)
|
||||
}
|
||||
if err := results.Err(); err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
r := friendsGETResponse{}
|
||||
r.Code = 200
|
||||
r.Friends = myFriends
|
||||
return r
|
||||
}
|
||||
|
||||
func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) {
|
||||
var err error
|
||||
|
||||
err = row.Scan(&user.ID, &user.Username, &user.RegisteredOn, &user.Privileges, &user.LatestActivity, &user.UsernameAKA, &user.Country)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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) common.CodeMessager {
|
||||
var r friendsWithResponse
|
||||
r.Code = 200
|
||||
uid := common.Int(md.Query("id"))
|
||||
if uid == 0 {
|
||||
return r
|
||||
}
|
||||
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)
|
||||
return Err500
|
||||
}
|
||||
if !r.Friends {
|
||||
r.Mutual = false
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// FriendsAddPOST adds an user to the friends.
|
||||
func FriendsAddPOST(md common.MethodData) common.CodeMessager {
|
||||
var u struct {
|
||||
User int `json:"user"`
|
||||
}
|
||||
md.Unmarshal(&u)
|
||||
return addFriend(md, u.User)
|
||||
}
|
||||
|
||||
func addFriend(md common.MethodData, u int) common.CodeMessager {
|
||||
if md.ID() == u {
|
||||
return common.SimpleResponse(406, "Just so you know: you can't add yourself to your friends.")
|
||||
}
|
||||
if !userExists(md, u) {
|
||||
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
|
||||
isMutual bool
|
||||
)
|
||||
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)
|
||||
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)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
var r friendsWithResponse
|
||||
r.Code = 200
|
||||
r.Friends = true
|
||||
r.Mutual = isMutual
|
||||
return r
|
||||
}
|
||||
|
||||
// userExists makes sure an user exists.
|
||||
func userExists(md common.MethodData, u int) (r bool) {
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND "+
|
||||
md.User.OnlyUserPublic(true)+")", u).Scan(&r)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
md.Err(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FriendsDelPOST deletes an user's friend.
|
||||
func FriendsDelPOST(md common.MethodData) common.CodeMessager {
|
||||
var u struct {
|
||||
User int `json:"user"`
|
||||
}
|
||||
md.Unmarshal(&u)
|
||||
return delFriend(md, u.User)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
r := friendsWithResponse{
|
||||
Friends: false,
|
||||
Mutual: false,
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
121
vendor/zxq.co/ripple/rippleapi/app/v1/leaderboard.go
vendored
Normal file
121
vendor/zxq.co/ripple/rippleapi/app/v1/leaderboard.go
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
redis "gopkg.in/redis.v5"
|
||||
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type leaderboardUser struct {
|
||||
userData
|
||||
ChosenMode modeData `json:"chosen_mode"`
|
||||
PlayStyle int `json:"play_style"`
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
}
|
||||
|
||||
type leaderboardResponse struct {
|
||||
common.ResponseBase
|
||||
Users []leaderboardUser `json:"users"`
|
||||
}
|
||||
|
||||
const lbUserQuery = `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka, users_stats.country,
|
||||
users_stats.play_style, users_stats.favourite_mode,
|
||||
|
||||
users_stats.ranked_score_%[1]s, users_stats.total_score_%[1]s, users_stats.playcount_%[1]s,
|
||||
users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s,
|
||||
users_stats.avg_accuracy_%[1]s, users_stats.pp_%[1]s
|
||||
FROM users
|
||||
INNER JOIN users_stats ON users_stats.id = users.id
|
||||
WHERE users.id IN (?)
|
||||
`
|
||||
|
||||
// LeaderboardGET gets the leaderboard.
|
||||
func LeaderboardGET(md common.MethodData) common.CodeMessager {
|
||||
m := getMode(md.Query("mode"))
|
||||
|
||||
// md.Query.Country
|
||||
p := common.Int(md.Query("p")) - 1
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
l := common.InString(1, md.Query("l"), 500, 50)
|
||||
|
||||
key := "ripple:leaderboard:" + m
|
||||
if md.Query("country") != "" {
|
||||
key += ":" + md.Query("country")
|
||||
}
|
||||
|
||||
results, err := md.R.ZRevRange(key, int64(p*l), int64(p*l+l-1)).Result()
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var resp leaderboardResponse
|
||||
resp.Code = 200
|
||||
|
||||
if len(results) == 0 {
|
||||
return resp
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(lbUserQuery+` ORDER BY users_stats.pp_%[1]s DESC, users_stats.ranked_score_%[1]s DESC`, m)
|
||||
query, params, _ := sqlx.In(query, results)
|
||||
rows, err := md.DB.Query(query, params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
for rows.Next() {
|
||||
var u leaderboardUser
|
||||
err := rows.Scan(
|
||||
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges, &u.LatestActivity,
|
||||
|
||||
&u.UsernameAKA, &u.Country, &u.PlayStyle, &u.FavouriteMode,
|
||||
|
||||
&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
|
||||
&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
|
||||
&u.ChosenMode.Accuracy, &u.ChosenMode.PP,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
u.ChosenMode.Level = ocl.GetLevelPrecise(int64(u.ChosenMode.TotalScore))
|
||||
if i := leaderboardPosition(md.R, m, u.ID); i != nil {
|
||||
u.ChosenMode.GlobalLeaderboardRank = i
|
||||
}
|
||||
if i := countryPosition(md.R, m, u.ID, u.Country); i != nil {
|
||||
u.ChosenMode.CountryLeaderboardRank = i
|
||||
}
|
||||
resp.Users = append(resp.Users, u)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func leaderboardPosition(r *redis.Client, mode string, user int) *int {
|
||||
return _position(r, "ripple:leaderboard:"+mode, user)
|
||||
}
|
||||
|
||||
func countryPosition(r *redis.Client, mode string, user int, country string) *int {
|
||||
return _position(r, "ripple:leaderboard:"+mode+":"+strings.ToLower(country), user)
|
||||
}
|
||||
|
||||
func _position(r *redis.Client, key string, user int) *int {
|
||||
res := r.ZRevRank(key, strconv.Itoa(user))
|
||||
if res.Err() == redis.Nil {
|
||||
return nil
|
||||
}
|
||||
x := int(res.Val()) + 1
|
||||
return &x
|
||||
}
|
192
vendor/zxq.co/ripple/rippleapi/app/v1/manage_user.go
vendored
Normal file
192
vendor/zxq.co/ripple/rippleapi/app/v1/manage_user.go
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type setAllowedData struct {
|
||||
UserID int `json:"user_id"`
|
||||
Allowed int `json:"allowed"`
|
||||
}
|
||||
|
||||
// UserManageSetAllowedPOST allows to set the allowed status of an user.
|
||||
func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager {
|
||||
var data setAllowedData
|
||||
if err := md.Unmarshal(&data); err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
if data.Allowed < 0 || data.Allowed > 2 {
|
||||
return common.SimpleResponse(400, "Allowed status must be between 0 and 2")
|
||||
}
|
||||
var banDatetime int64
|
||||
var privsSet string
|
||||
if data.Allowed == 0 {
|
||||
banDatetime = time.Now().Unix()
|
||||
privsSet = "privileges = (privileges & ~3)"
|
||||
} else {
|
||||
banDatetime = 0
|
||||
privsSet = "privileges = (privileges | 3)"
|
||||
}
|
||||
_, err := md.DB.Exec("UPDATE users SET "+privsSet+", ban_datetime = ? WHERE id = ?", banDatetime, data.UserID)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
rapLog(md, fmt.Sprintf("changed UserID:%d's allowed to %d. This was done using the API's terrible ManageSetAllowed.", data.UserID, data.Allowed))
|
||||
go fixPrivileges(data.UserID, md.DB)
|
||||
query := `
|
||||
SELECT users.id, users.username, register_datetime, privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
WHERE users.id=?
|
||||
LIMIT 1`
|
||||
return userPutsSingle(md, md.DB.QueryRowx(query, data.UserID))
|
||||
}
|
||||
|
||||
type userEditData struct {
|
||||
ID int `json:"id"`
|
||||
Username *string `json:"username"`
|
||||
UsernameAKA *string `json:"username_aka"`
|
||||
//Privileges *uint64 `json:"privileges"`
|
||||
Country *string `json:"country"`
|
||||
SilenceInfo *silenceInfo `json:"silence_info"`
|
||||
ResetUserpage bool `json:"reset_userpage"`
|
||||
//ResetAvatar bool `json:"reset_avatar"`
|
||||
}
|
||||
|
||||
// UserEditPOST allows to edit an user's information.
|
||||
func UserEditPOST(md common.MethodData) common.CodeMessager {
|
||||
var data userEditData
|
||||
if err := md.Unmarshal(&data); err != nil {
|
||||
fmt.Println(err)
|
||||
return ErrBadJSON
|
||||
}
|
||||
|
||||
if data.ID == 0 {
|
||||
return common.SimpleResponse(404, "That user could not be found")
|
||||
}
|
||||
|
||||
var prevUser struct {
|
||||
Username string
|
||||
Privileges uint64
|
||||
}
|
||||
err := md.DB.Get(&prevUser, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", data.ID)
|
||||
|
||||
switch err {
|
||||
case nil: // carry on
|
||||
case sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That user could not be found")
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
const initQuery = "UPDATE users SET\n"
|
||||
q := initQuery
|
||||
var args []interface{}
|
||||
|
||||
// totally did not realise I had to update some fields in users_stats as well
|
||||
// and just copy pasting the above code by prefixing "stats" to every
|
||||
// variable
|
||||
const statsInitQuery = "UPDATE users_stats SET\n"
|
||||
statsQ := statsInitQuery
|
||||
var statsArgs []interface{}
|
||||
|
||||
if common.UserPrivileges(prevUser.Privileges)&common.AdminPrivilegeManageUsers != 0 &&
|
||||
data.ID != md.User.UserID {
|
||||
return common.SimpleResponse(403, "Can't edit that user")
|
||||
}
|
||||
|
||||
if data.Username != nil {
|
||||
if strings.Contains(*data.Username, " ") && strings.Contains(*data.Username, "_") {
|
||||
return common.SimpleResponse(400, "Mixed spaces and underscores")
|
||||
}
|
||||
if usernameAvailable(md, *data.Username, data.ID) {
|
||||
return common.SimpleResponse(409, "User with that username exists")
|
||||
}
|
||||
jsonData, _ := json.Marshal(struct {
|
||||
UserID int `json:"userID"`
|
||||
NewUsername string `json:"newUsername"`
|
||||
}{data.ID, *data.Username})
|
||||
md.R.Publish("peppy:change_username", string(jsonData))
|
||||
appendToUserNotes(md, "Username change: "+prevUser.Username+" -> "+*data.Username, data.ID)
|
||||
}
|
||||
if data.UsernameAKA != nil {
|
||||
statsQ += "username_aka = ?,\n"
|
||||
statsArgs = append(statsArgs, *data.UsernameAKA)
|
||||
}
|
||||
/*if data.Privileges != nil {
|
||||
q += "privileges = ?,\n"
|
||||
args = append(args, *data.Privileges)
|
||||
// UserNormal or UserPublic changed
|
||||
if *data.Privileges & 3 != 3 && *data.Privileges & 3 != prevUser.Privileges & 3 {
|
||||
q += "ban_datetime = ?"
|
||||
args = append(args, meme)
|
||||
}
|
||||
// https://zxq.co/ripple/old-frontend/src/master/inc/Do.php#L355 ?
|
||||
// should also check for AdminManagePrivileges
|
||||
// should also check out the code for CM restring/banning
|
||||
}*/
|
||||
if data.Country != nil {
|
||||
statsQ += "country = ?,\n"
|
||||
statsArgs = append(statsArgs, *data.Country)
|
||||
rapLog(md, fmt.Sprintf("has changed %s country to %s", prevUser.Username, *data.Country))
|
||||
appendToUserNotes(md, "country changed to "+*data.Country, data.ID)
|
||||
}
|
||||
if data.SilenceInfo != nil && md.User.UserPrivileges&common.AdminPrivilegeSilenceUsers != 0 {
|
||||
q += "silence_end = ?, silence_reason = ?,\n"
|
||||
args = append(args, time.Time(data.SilenceInfo.End).Unix(), data.SilenceInfo.Reason)
|
||||
}
|
||||
if data.ResetUserpage {
|
||||
statsQ += "userpage_content = '',\n"
|
||||
}
|
||||
|
||||
if q != initQuery {
|
||||
q = q[:len(q)-2] + " WHERE id = ? LIMIT 1"
|
||||
args = append(args, data.ID)
|
||||
_, err = md.DB.Exec(q, args...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
if statsQ != statsInitQuery {
|
||||
statsQ = statsQ[:len(statsQ)-2] + " WHERE id = ? LIMIT 1"
|
||||
statsArgs = append(statsArgs, data.ID)
|
||||
_, err = md.DB.Exec(statsQ, statsArgs...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
|
||||
rapLog(md, fmt.Sprintf("has updated user %s", prevUser.Username))
|
||||
|
||||
return userPutsSingle(md, md.DB.QueryRowx(userFields+" WHERE users.id = ? LIMIT 1", data.ID))
|
||||
}
|
||||
|
||||
func appendToUserNotes(md common.MethodData, message string, user int) {
|
||||
message = "\n[" + time.Now().Format("2006-01-02 15:04:05") + "] API: " + message
|
||||
_, err := md.DB.Exec("UPDATE users SET notes = CONCAT(COALESCE(notes, ''), ?) WHERE id = ?",
|
||||
message, user)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
func usernameAvailable(md common.MethodData, u string, userID int) (r bool) {
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE username_safe = ? AND id != ?)", common.SafeUsername(u), userID).Scan(&r)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
md.Err(err)
|
||||
}
|
||||
return
|
||||
}
|
108
vendor/zxq.co/ripple/rippleapi/app/v1/meta_linux.go
vendored
Normal file
108
vendor/zxq.co/ripple/rippleapi/app/v1/meta_linux.go
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
// +build !windows
|
||||
|
||||
// TODO: Make all these methods POST
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// MetaRestartGET restarts the API with Zero Downtime™.
|
||||
func MetaRestartGET(md common.MethodData) common.CodeMessager {
|
||||
proc, err := os.FindProcess(syscall.Getpid())
|
||||
if err != nil {
|
||||
return common.SimpleResponse(500, "couldn't find process. what the fuck?")
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
proc.Signal(syscall.SIGUSR2)
|
||||
}()
|
||||
return common.SimpleResponse(200, "brb")
|
||||
}
|
||||
|
||||
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.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.CodeMessager {
|
||||
if f, err := os.Stat(".git"); err == os.ErrNotExist || !f.IsDir() {
|
||||
return common.SimpleResponse(500, "instance is not using git")
|
||||
}
|
||||
go func() {
|
||||
if !execCommand("git", "pull", "origin", "master") {
|
||||
return
|
||||
}
|
||||
// go get
|
||||
// -u: update all dependencies
|
||||
// -d: stop after downloading deps
|
||||
if !execCommand("go", "get", "-v", "-u", "-d") {
|
||||
return
|
||||
}
|
||||
if !execCommand("bash", "-c", "go build -v -ldflags \"-X main.Version=`git rev-parse HEAD`\"") {
|
||||
return
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(syscall.Getpid())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
proc.Signal(syscall.SIGUSR2)
|
||||
}()
|
||||
return common.SimpleResponse(200, "Started updating! "+surpriseMe())
|
||||
}
|
||||
|
||||
func execCommand(command string, args ...string) bool {
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Env = os.Environ()
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
data, err := ioutil.ReadAll(stderr)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
// Bob. We got a problem.
|
||||
if len(data) != 0 {
|
||||
log.Println(string(data))
|
||||
}
|
||||
io.Copy(os.Stdout, stdout)
|
||||
cmd.Wait()
|
||||
stdout.Close()
|
||||
return true
|
||||
}
|
42
vendor/zxq.co/ripple/rippleapi/app/v1/meta_windows.go
vendored
Normal file
42
vendor/zxq.co/ripple/rippleapi/app/v1/meta_windows.go
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// +build windows
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// MetaRestartGET restarts the API with Zero Downtime™.
|
||||
func MetaRestartGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "brb in your dreams")
|
||||
}
|
||||
|
||||
// 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) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "haha")
|
||||
}
|
||||
|
||||
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.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.CodeMessager {
|
||||
return common.SimpleResponse(200, "lol u wish")
|
||||
}
|
139
vendor/zxq.co/ripple/rippleapi/app/v1/ping.go
vendored
Normal file
139
vendor/zxq.co/ripple/rippleapi/app/v1/ping.go
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/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!",
|
||||
"Superman dies",
|
||||
"PP when?",
|
||||
"RWC hype",
|
||||
"I'd just like to interject for a moment.",
|
||||
"Running on an apple pie!",
|
||||
":thinking:",
|
||||
"The total entropy of an isolated system can only increase over time",
|
||||
"Where are my testicles, Summer?",
|
||||
"Why don't you ask the smartest people in the universe? Oh yeah, you can't. They blew up.",
|
||||
}
|
||||
|
||||
func surpriseMe() string {
|
||||
n := int(time.Now().UnixNano())
|
||||
return randomSentences[n%len(randomSentences)] + " " + kaomojis[n%len(kaomojis)]
|
||||
}
|
||||
|
||||
type pingResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"user_id"`
|
||||
Privileges common.Privileges `json:"privileges"`
|
||||
UserPrivileges common.UserPrivileges `json:"user_privileges"`
|
||||
PrivilegesS string `json:"privileges_string"`
|
||||
UserPrivilegesS string `json:"user_privileges_string"`
|
||||
}
|
||||
|
||||
// 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) 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 POST /tokens " + kaomojis[rn.Intn(len(kaomojis))]
|
||||
} else {
|
||||
r.Message = surpriseMe()
|
||||
}
|
||||
|
||||
r.ID = md.ID()
|
||||
r.Privileges = md.User.TokenPrivileges
|
||||
r.UserPrivileges = md.User.UserPrivileges
|
||||
r.PrivilegesS = md.User.TokenPrivileges.String()
|
||||
r.UserPrivilegesS = md.User.UserPrivileges.String()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type surpriseMeResponse struct {
|
||||
common.ResponseBase
|
||||
Cats [100]string `json:"cats"`
|
||||
}
|
||||
|
||||
// SurpriseMeGET generates cute cats.
|
||||
//
|
||||
// ... Yes.
|
||||
func SurpriseMeGET(md common.MethodData) common.CodeMessager {
|
||||
var r surpriseMeResponse
|
||||
r.Code = 200
|
||||
for i := 0; i < 100; i++ {
|
||||
r.Cats[i] = surpriseMe()
|
||||
}
|
||||
return r
|
||||
}
|
87
vendor/zxq.co/ripple/rippleapi/app/v1/rap.go
vendored
Normal file
87
vendor/zxq.co/ripple/rippleapi/app/v1/rap.go
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type rapLogData struct {
|
||||
Through string `json:"through"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type rapLogMessage struct {
|
||||
rapLogData
|
||||
Author int `json:"author"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type rapLogResponse struct {
|
||||
common.ResponseBase
|
||||
rapLogMessage
|
||||
}
|
||||
|
||||
// RAPLogPOST creates a new entry in the RAP logs
|
||||
func RAPLogPOST(md common.MethodData) common.CodeMessager {
|
||||
if md.User.UserPrivileges&common.AdminPrivilegeAccessRAP == 0 {
|
||||
return common.SimpleResponse(403, "Got lost, kiddo?")
|
||||
}
|
||||
|
||||
var d rapLogData
|
||||
if err := md.Unmarshal(&d); err != nil {
|
||||
fmt.Println(err)
|
||||
return ErrBadJSON
|
||||
}
|
||||
|
||||
if d.Text == "" {
|
||||
return ErrMissingField("text")
|
||||
}
|
||||
if d.Through == "" {
|
||||
ua := string(md.Ctx.UserAgent())
|
||||
if len(ua) > 20 {
|
||||
ua = ua[:20] + "…"
|
||||
}
|
||||
d.Through = "API"
|
||||
if ua != "" {
|
||||
d.Through += " (" + ua + ")"
|
||||
}
|
||||
}
|
||||
if len(d.Through) > 30 {
|
||||
d.Through = d.Through[:30]
|
||||
}
|
||||
|
||||
created := time.Now()
|
||||
_, err := md.DB.Exec("INSERT INTO rap_logs(userid, text, datetime, through) VALUES (?, ?, ?, ?)",
|
||||
md.User.UserID, d.Text, created.Unix(), d.Through)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var resp rapLogResponse
|
||||
resp.rapLogData = d
|
||||
resp.Author = md.User.UserID
|
||||
resp.CreatedAt = created.Truncate(time.Second)
|
||||
resp.Code = 200
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func rapLog(md common.MethodData, message string) {
|
||||
ua := string(md.Ctx.UserAgent())
|
||||
if len(ua) > 20 {
|
||||
ua = ua[:20] + "…"
|
||||
}
|
||||
through := "API"
|
||||
if ua != "" {
|
||||
through += " (" + ua + ")"
|
||||
}
|
||||
|
||||
_, err := md.DB.Exec("INSERT INTO rap_logs(userid, text, datetime, through) VALUES (?, ?, ?, ?)",
|
||||
md.User.UserID, message, time.Now().Unix(), through)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
}
|
156
vendor/zxq.co/ripple/rippleapi/app/v1/score.go
vendored
Normal file
156
vendor/zxq.co/ripple/rippleapi/app/v1/score.go
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// Score is a score done on Ripple.
|
||||
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.UnixTimestamp `json:"time"`
|
||||
PlayMode int `json:"play_mode"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP float32 `json:"pp"`
|
||||
Rank string `json:"rank"`
|
||||
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", "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' AND `+md.User.OnlyUserPublic(false)+
|
||||
` `+genModeClause(md)+`
|
||||
`+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.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
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(s.PlayMode),
|
||||
osuapi.Mods(s.Mods),
|
||||
s.Accuracy,
|
||||
s.Count300,
|
||||
s.Count100,
|
||||
s.Count50,
|
||||
s.CountMiss,
|
||||
))
|
||||
r.Scores = append(r.Scores, s)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getMode(m string) string {
|
||||
switch m {
|
||||
case "1":
|
||||
return "taiko"
|
||||
case "2":
|
||||
return "ctb"
|
||||
case "3":
|
||||
return "mania"
|
||||
default:
|
||||
return "std"
|
||||
}
|
||||
}
|
||||
|
||||
func genModeClause(md common.MethodData) string {
|
||||
var modeClause string
|
||||
if md.Query("mode") != "" {
|
||||
m, err := strconv.Atoi(md.Query("mode"))
|
||||
if err == nil && m >= 0 && m <= 3 {
|
||||
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
|
||||
}
|
||||
}
|
||||
return modeClause
|
||||
}
|
167
vendor/zxq.co/ripple/rippleapi/app/v1/self.go
vendored
Normal file
167
vendor/zxq.co/ripple/rippleapi/app/v1/self.go
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/semantic-icons-ugc"
|
||||
)
|
||||
|
||||
type donorInfoResponse struct {
|
||||
common.ResponseBase
|
||||
HasDonor bool `json:"has_donor"`
|
||||
Expiration common.UnixTimestamp `json:"expiration"`
|
||||
}
|
||||
|
||||
// UsersSelfDonorInfoGET returns information about the users' donor status
|
||||
func UsersSelfDonorInfoGET(md common.MethodData) common.CodeMessager {
|
||||
var r donorInfoResponse
|
||||
var privileges uint64
|
||||
err := md.DB.QueryRow("SELECT privileges, donor_expire FROM users WHERE id = ?", md.ID()).
|
||||
Scan(&privileges, &r.Expiration)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
r.HasDonor = common.UserPrivileges(privileges)&common.UserPrivilegeDonor > 0
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type favouriteModeResponse struct {
|
||||
common.ResponseBase
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
}
|
||||
|
||||
// UsersSelfFavouriteModeGET gets the current user's favourite mode
|
||||
func UsersSelfFavouriteModeGET(md common.MethodData) common.CodeMessager {
|
||||
var f favouriteModeResponse
|
||||
f.Code = 200
|
||||
if md.ID() == 0 {
|
||||
return f
|
||||
}
|
||||
err := md.DB.QueryRow("SELECT users_stats.favourite_mode FROM users_stats WHERE id = ?", md.ID()).
|
||||
Scan(&f.FavouriteMode)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type userSettingsData struct {
|
||||
UsernameAKA *string `json:"username_aka"`
|
||||
FavouriteMode *int `json:"favourite_mode"`
|
||||
CustomBadge struct {
|
||||
singleBadge
|
||||
Show *bool `json:"show"`
|
||||
} `json:"custom_badge"`
|
||||
PlayStyle *int `json:"play_style"`
|
||||
}
|
||||
|
||||
// UsersSelfSettingsPOST allows to modify information about the current user.
|
||||
func UsersSelfSettingsPOST(md common.MethodData) common.CodeMessager {
|
||||
var d userSettingsData
|
||||
md.Unmarshal(&d)
|
||||
|
||||
// input sanitisation
|
||||
*d.UsernameAKA = common.SanitiseString(*d.UsernameAKA)
|
||||
if md.User.UserPrivileges&common.UserPrivilegeDonor > 0 {
|
||||
d.CustomBadge.Name = common.SanitiseString(d.CustomBadge.Name)
|
||||
d.CustomBadge.Icon = sanitiseIconName(d.CustomBadge.Icon)
|
||||
} else {
|
||||
d.CustomBadge.singleBadge = singleBadge{}
|
||||
d.CustomBadge.Show = nil
|
||||
}
|
||||
d.FavouriteMode = intPtrIn(0, d.FavouriteMode, 3)
|
||||
|
||||
q := new(common.UpdateQuery).
|
||||
Add("s.username_aka", d.UsernameAKA).
|
||||
Add("s.favourite_mode", d.FavouriteMode).
|
||||
Add("s.custom_badge_name", d.CustomBadge.Name).
|
||||
Add("s.custom_badge_icon", d.CustomBadge.Icon).
|
||||
Add("s.show_custom_badge", d.CustomBadge.Show).
|
||||
Add("s.play_style", d.PlayStyle)
|
||||
_, err := md.DB.Exec("UPDATE users u, users_stats s SET "+q.Fields()+" WHERE s.id = u.id AND u.id = ?", append(q.Parameters, md.ID())...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return UsersSelfSettingsGET(md)
|
||||
}
|
||||
|
||||
func sanitiseIconName(s string) string {
|
||||
classes := strings.Split(s, " ")
|
||||
n := make([]string, 0, len(classes))
|
||||
for _, c := range classes {
|
||||
if !in(c, n) && in(c, semanticiconsugc.SaneIcons) {
|
||||
n = append(n, c)
|
||||
}
|
||||
}
|
||||
return strings.Join(n, " ")
|
||||
}
|
||||
|
||||
func in(a string, b []string) bool {
|
||||
for _, x := range b {
|
||||
if x == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type userSettingsResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Flags uint `json:"flags"`
|
||||
userSettingsData
|
||||
}
|
||||
|
||||
// UsersSelfSettingsGET allows to get "sensitive" information about the current user.
|
||||
func UsersSelfSettingsGET(md common.MethodData) common.CodeMessager {
|
||||
var r userSettingsResponse
|
||||
var ccb bool
|
||||
r.Code = 200
|
||||
err := md.DB.QueryRow(`
|
||||
SELECT
|
||||
u.id, u.username,
|
||||
u.email, s.username_aka, s.favourite_mode,
|
||||
s.show_custom_badge, s.custom_badge_icon,
|
||||
s.custom_badge_name, s.can_custom_badge,
|
||||
s.play_style, u.flags
|
||||
FROM users u
|
||||
LEFT JOIN users_stats s ON u.id = s.id
|
||||
WHERE u.id = ?`, md.ID()).Scan(
|
||||
&r.ID, &r.Username,
|
||||
&r.Email, &r.UsernameAKA, &r.FavouriteMode,
|
||||
&r.CustomBadge.Show, &r.CustomBadge.Icon,
|
||||
&r.CustomBadge.Name, &ccb,
|
||||
&r.PlayStyle, &r.Flags,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if !ccb {
|
||||
r.CustomBadge = struct {
|
||||
singleBadge
|
||||
Show *bool `json:"show"`
|
||||
}{}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func intPtrIn(x int, y *int, z int) *int {
|
||||
if y == nil {
|
||||
return nil
|
||||
}
|
||||
if *y > z {
|
||||
return nil
|
||||
}
|
||||
if *y < x {
|
||||
return nil
|
||||
}
|
||||
return y
|
||||
}
|
219
vendor/zxq.co/ripple/rippleapi/app/v1/token.go
vendored
Normal file
219
vendor/zxq.co/ripple/rippleapi/app/v1/token.go
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
)
|
||||
|
||||
// TokenSelfDeletePOST deletes the token the user is connecting with.
|
||||
func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager {
|
||||
if md.ID() == 0 {
|
||||
return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!")
|
||||
}
|
||||
var err error
|
||||
if md.IsBearer() {
|
||||
_, err = md.DB.Exec("DELETE FROM osin_access WHERE access_token = ? LIMIT 1",
|
||||
fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value))))
|
||||
} else {
|
||||
_, err = md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
|
||||
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value))))
|
||||
}
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return common.SimpleResponse(200, "Bye!")
|
||||
}
|
||||
|
||||
type token struct {
|
||||
ID int `json:"id"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
Description string `json:"description"`
|
||||
LastUpdated common.UnixTimestamp `json:"last_updated"`
|
||||
}
|
||||
type tokenResponse struct {
|
||||
common.ResponseBase
|
||||
Tokens []token `json:"tokens"`
|
||||
}
|
||||
|
||||
// TokenGET retrieves a list listing all the user's public tokens.
|
||||
func TokenGET(md common.MethodData) common.CodeMessager {
|
||||
wc := common.Where("user = ? AND private = 0", strconv.Itoa(md.ID()))
|
||||
if md.Query("id") != "" {
|
||||
wc.Where("id = ?", md.Query("id"))
|
||||
}
|
||||
rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens "+
|
||||
wc.Clause+common.Paginate(md.Query("p"), md.Query("l"), 50), wc.Params...)
|
||||
|
||||
if err != nil {
|
||||
return Err500
|
||||
}
|
||||
var r tokenResponse
|
||||
for rows.Next() {
|
||||
var t token
|
||||
err = rows.Scan(&t.ID, &t.Privileges, &t.Description, &t.LastUpdated)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Tokens = append(r.Tokens, t)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type oauthClient struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID int `json:"owner_id"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// Scan scans the extra in the mysql table into Name, OwnerID and Avatar.
|
||||
func (o *oauthClient) Scan(src interface{}) error {
|
||||
var s []byte
|
||||
switch x := src.(type) {
|
||||
case string:
|
||||
s = []byte(x)
|
||||
case []byte:
|
||||
s = x
|
||||
default:
|
||||
return errors.New("Can't scan non-string")
|
||||
}
|
||||
|
||||
var vals [3]string
|
||||
err := json.Unmarshal(s, &vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Name = vals[0]
|
||||
o.OwnerID, _ = strconv.Atoi(vals[1])
|
||||
o.Avatar = vals[2]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bearerToken struct {
|
||||
Client oauthClient `json:"client"`
|
||||
Scope string `json:"scope"`
|
||||
Privileges common.Privileges `json:"privileges"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
type tokenSingleResponse struct {
|
||||
common.ResponseBase
|
||||
token
|
||||
}
|
||||
|
||||
type bearerTokenSingleResponse struct {
|
||||
common.ResponseBase
|
||||
bearerToken
|
||||
}
|
||||
|
||||
// TokenSelfGET retrieves information about the token the user is connecting with.
|
||||
func TokenSelfGET(md common.MethodData) common.CodeMessager {
|
||||
if md.ID() == 0 {
|
||||
return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!")
|
||||
}
|
||||
if md.IsBearer() {
|
||||
return getBearerToken(md)
|
||||
}
|
||||
var r tokenSingleResponse
|
||||
// md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck
|
||||
err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan(
|
||||
&r.ID, &r.Privileges, &r.Description, &r.LastUpdated,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getBearerToken(md common.MethodData) common.CodeMessager {
|
||||
var b bearerTokenSingleResponse
|
||||
err := md.DB.
|
||||
QueryRow(`
|
||||
SELECT t.scope, t.created_at, c.id, c.extra
|
||||
FROM osin_access t INNER JOIN osin_client c
|
||||
WHERE t.access_token = ?
|
||||
`, fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value)))).Scan(
|
||||
&b.Scope, &b.Created, &b.Client.ID, &b.Client,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
b.Code = 200
|
||||
b.Privileges = md.User.TokenPrivileges
|
||||
return b
|
||||
}
|
||||
|
||||
// TokenFixPrivilegesPOST fixes the privileges on the token of the given user,
|
||||
// or of all the users if no user is given.
|
||||
func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager {
|
||||
id := common.Int(md.Query("id"))
|
||||
if md.Query("id") == "self" {
|
||||
id = md.ID()
|
||||
}
|
||||
go fixPrivileges(id, md.DB)
|
||||
return common.SimpleResponse(200, "Privilege fixing started!")
|
||||
}
|
||||
|
||||
func fixPrivileges(user int, db *sqlx.DB) {
|
||||
var wc string
|
||||
var params = make([]interface{}, 0, 1)
|
||||
if user != 0 {
|
||||
// dirty, but who gives a shit
|
||||
wc = "WHERE user = ?"
|
||||
params = append(params, user)
|
||||
}
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
tokens.id, tokens.privileges, users.privileges
|
||||
FROM tokens
|
||||
LEFT JOIN users ON users.id = tokens.user
|
||||
`+wc, params...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
privsRaw uint64
|
||||
privs common.Privileges
|
||||
newPrivs common.Privileges
|
||||
privilegesRaw uint64
|
||||
)
|
||||
err := rows.Scan(&id, &privsRaw, &privilegesRaw)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
privileges := common.UserPrivileges(privilegesRaw)
|
||||
privs = common.Privileges(privsRaw)
|
||||
newPrivs = privs.CanOnly(privileges)
|
||||
if newPrivs != privs {
|
||||
_, err := db.Exec("UPDATE tokens SET privileges = ? WHERE id = ? LIMIT 1", uint64(newPrivs), id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
447
vendor/zxq.co/ripple/rippleapi/app/v1/user.go
vendored
Normal file
447
vendor/zxq.co/ripple/rippleapi/app/v1/user.go
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
// Package v1 implements the first version of the Ripple API.
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type userData struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
UsernameAKA string `json:"username_aka"`
|
||||
RegisteredOn common.UnixTimestamp `json:"registered_on"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
LatestActivity common.UnixTimestamp `json:"latest_activity"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
const userFields = `SELECT users.id, users.username, register_datetime, users.privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users
|
||||
INNER JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
`
|
||||
|
||||
// UsersGET is the API handler for GET /users
|
||||
func UsersGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return userPutsMulti(md)
|
||||
}
|
||||
|
||||
query := userFields + `
|
||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
|
||||
LIMIT 1`
|
||||
return userPutsSingle(md, md.DB.QueryRowx(query, param))
|
||||
}
|
||||
|
||||
type userPutsSingleUserData struct {
|
||||
common.ResponseBase
|
||||
userData
|
||||
}
|
||||
|
||||
func userPutsSingle(md common.MethodData, row *sqlx.Row) common.CodeMessager {
|
||||
var err error
|
||||
var user userPutsSingleUserData
|
||||
|
||||
err = row.StructScan(&user.userData)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user was found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
user.Code = 200
|
||||
return user
|
||||
}
|
||||
|
||||
type userPutsMultiUserData struct {
|
||||
common.ResponseBase
|
||||
Users []userData `json:"users"`
|
||||
}
|
||||
|
||||
func userPutsMulti(md common.MethodData) common.CodeMessager {
|
||||
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
|
||||
// query composition
|
||||
wh := common.
|
||||
Where("users.username_safe = ?", common.SafeUsername(md.Query("nname"))).
|
||||
Where("users.id = ?", md.Query("iid")).
|
||||
Where("users.privileges = ?", md.Query("privileges")).
|
||||
Where("users.privileges & ? > 0", md.Query("has_privileges")).
|
||||
Where("users.privileges & ? = 0", md.Query("has_not_privileges")).
|
||||
Where("users_stats.country = ?", md.Query("country")).
|
||||
Where("users_stats.username_aka = ?", md.Query("name_aka")).
|
||||
Where("privileges_groups.name = ?", md.Query("privilege_group")).
|
||||
In("users.id", pm("ids")...).
|
||||
In("users.username_safe", safeUsernameBulk(pm("names"))...).
|
||||
In("users_stats.username_aka", pm("names_aka")...).
|
||||
In("users_stats.country", pm("countries")...)
|
||||
|
||||
var extraJoin string
|
||||
if md.Query("privilege_group") != "" {
|
||||
extraJoin = " LEFT JOIN privileges_groups ON users.privileges & privileges_groups.privileges = privileges_groups.privileges "
|
||||
}
|
||||
|
||||
query := userFields + extraJoin + wh.ClauseSafe() + " AND " + md.User.OnlyUserPublic(true) +
|
||||
" " + common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"id",
|
||||
"username",
|
||||
"privileges",
|
||||
"donor_expire",
|
||||
"latest_activity",
|
||||
"silence_end",
|
||||
},
|
||||
Default: "id ASC",
|
||||
Table: "users",
|
||||
}) +
|
||||
" " + common.Paginate(md.Query("p"), md.Query("l"), 100)
|
||||
|
||||
// query execution
|
||||
rows, err := md.DB.Queryx(query, wh.Params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r userPutsMultiUserData
|
||||
for rows.Next() {
|
||||
var u userData
|
||||
err := rows.StructScan(&u)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Users = append(r.Users, u)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
// UserSelfGET is a shortcut for /users/id/self. (/users/self)
|
||||
func UserSelfGET(md common.MethodData) common.CodeMessager {
|
||||
md.Ctx.Request.URI().SetQueryString("id=self")
|
||||
return UsersGET(md)
|
||||
}
|
||||
|
||||
func safeUsernameBulk(us [][]byte) [][]byte {
|
||||
for _, u := range us {
|
||||
for idx, v := range u {
|
||||
if v == ' ' {
|
||||
u[idx] = '_'
|
||||
continue
|
||||
}
|
||||
u[idx] = byte(unicode.ToLower(rune(v)))
|
||||
}
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
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.CodeMessager {
|
||||
var (
|
||||
r whatIDResponse
|
||||
privileges uint64
|
||||
)
|
||||
err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username_safe = ? LIMIT 1", common.SafeUsername(md.Query("name"))).Scan(&r.ID, &privileges)
|
||||
if err != nil || ((privileges&uint64(common.UserPrivilegePublic)) == 0 &&
|
||||
(md.User.UserPrivileges&common.AdminPrivilegeManageUsers == 0)) {
|
||||
return common.SimpleResponse(404, "That user could not be found!")
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
var modesToReadable = [...]string{
|
||||
"std",
|
||||
"taiko",
|
||||
"ctb",
|
||||
"mania",
|
||||
}
|
||||
|
||||
type modeData struct {
|
||||
RankedScore uint64 `json:"ranked_score"`
|
||||
TotalScore uint64 `json:"total_score"`
|
||||
PlayCount int `json:"playcount"`
|
||||
ReplaysWatched int `json:"replays_watched"`
|
||||
TotalHits int `json:"total_hits"`
|
||||
Level float64 `json:"level"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP int `json:"pp"`
|
||||
GlobalLeaderboardRank *int `json:"global_leaderboard_rank"`
|
||||
CountryLeaderboardRank *int `json:"country_leaderboard_rank"`
|
||||
}
|
||||
type userFullResponse struct {
|
||||
common.ResponseBase
|
||||
userData
|
||||
STD modeData `json:"std"`
|
||||
Taiko modeData `json:"taiko"`
|
||||
CTB modeData `json:"ctb"`
|
||||
Mania modeData `json:"mania"`
|
||||
PlayStyle int `json:"play_style"`
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
Badges []singleBadge `json:"badges"`
|
||||
CustomBadge *singleBadge `json:"custom_badge"`
|
||||
SilenceInfo silenceInfo `json:"silence_info"`
|
||||
CMNotes *string `json:"cm_notes,omitempty"`
|
||||
BanDate *common.UnixTimestamp `json:"ban_date,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
type silenceInfo struct {
|
||||
Reason string `json:"reason"`
|
||||
End common.UnixTimestamp `json:"end"`
|
||||
}
|
||||
|
||||
// UserFullGET gets all of an user's information, with one exception: their userpage.
|
||||
func UserFullGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
}
|
||||
|
||||
// Hellest query I've ever done.
|
||||
query := `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka, users_stats.country, users_stats.play_style, users_stats.favourite_mode,
|
||||
|
||||
users_stats.custom_badge_icon, users_stats.custom_badge_name, users_stats.can_custom_badge,
|
||||
users_stats.show_custom_badge,
|
||||
|
||||
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.playcount_std,
|
||||
users_stats.replays_watched_std, users_stats.total_hits_std,
|
||||
users_stats.avg_accuracy_std, users_stats.pp_std,
|
||||
|
||||
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.playcount_taiko,
|
||||
users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
|
||||
users_stats.avg_accuracy_taiko, users_stats.pp_taiko,
|
||||
|
||||
users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.playcount_ctb,
|
||||
users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
|
||||
users_stats.avg_accuracy_ctb, users_stats.pp_ctb,
|
||||
|
||||
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania,
|
||||
users_stats.replays_watched_mania, users_stats.total_hits_mania,
|
||||
users_stats.avg_accuracy_mania, users_stats.pp_mania,
|
||||
|
||||
users.silence_reason, users.silence_end,
|
||||
users.notes, users.ban_datetime, users.email
|
||||
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
|
||||
LIMIT 1
|
||||
`
|
||||
// Fuck.
|
||||
r := userFullResponse{}
|
||||
var (
|
||||
b singleBadge
|
||||
can bool
|
||||
show bool
|
||||
)
|
||||
err := md.DB.QueryRow(query, param).Scan(
|
||||
&r.ID, &r.Username, &r.RegisteredOn, &r.Privileges, &r.LatestActivity,
|
||||
|
||||
&r.UsernameAKA, &r.Country,
|
||||
&r.PlayStyle, &r.FavouriteMode,
|
||||
|
||||
&b.Icon, &b.Name, &can, &show,
|
||||
|
||||
&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
|
||||
&r.STD.ReplaysWatched, &r.STD.TotalHits,
|
||||
&r.STD.Accuracy, &r.STD.PP,
|
||||
|
||||
&r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
|
||||
&r.Taiko.ReplaysWatched, &r.Taiko.TotalHits,
|
||||
&r.Taiko.Accuracy, &r.Taiko.PP,
|
||||
|
||||
&r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
|
||||
&r.CTB.ReplaysWatched, &r.CTB.TotalHits,
|
||||
&r.CTB.Accuracy, &r.CTB.PP,
|
||||
|
||||
&r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
|
||||
&r.Mania.ReplaysWatched, &r.Mania.TotalHits,
|
||||
&r.Mania.Accuracy, &r.Mania.PP,
|
||||
|
||||
&r.SilenceInfo.Reason, &r.SilenceInfo.End,
|
||||
&r.CMNotes, &r.BanDate, &r.Email,
|
||||
)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That user could not be found!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
can = can && show && common.UserPrivileges(r.Privileges)&common.UserPrivilegeDonor > 0
|
||||
if can && (b.Name != "" || b.Icon != "") {
|
||||
r.CustomBadge = &b
|
||||
}
|
||||
|
||||
for modeID, m := range [...]*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
|
||||
m.Level = ocl.GetLevelPrecise(int64(m.TotalScore))
|
||||
|
||||
if i := leaderboardPosition(md.R, modesToReadable[modeID], r.ID); i != nil {
|
||||
m.GlobalLeaderboardRank = i
|
||||
}
|
||||
if i := countryPosition(md.R, modesToReadable[modeID], r.ID, r.Country); i != nil {
|
||||
m.CountryLeaderboardRank = i
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
|
||||
"LEFT JOIN badges b ON ub.badge = b.id WHERE user = ?", r.ID)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var badge singleBadge
|
||||
err := rows.Scan(&badge.ID, &badge.Name, &badge.Icon)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Badges = append(r.Badges, badge)
|
||||
}
|
||||
|
||||
if md.User.TokenPrivileges&common.PrivilegeManageUser == 0 {
|
||||
r.CMNotes = nil
|
||||
r.BanDate = nil
|
||||
r.Email = ""
|
||||
}
|
||||
|
||||
r.Code = 200
|
||||
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) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users_stats")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
}
|
||||
var r userpageResponse
|
||||
err := md.DB.QueryRow("SELECT userpage_content FROM users_stats WHERE "+whereClause+" LIMIT 1", param).Scan(&r.Userpage)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if r.Userpage == nil {
|
||||
r.Userpage = new(string)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
// UserSelfUserpagePOST allows to change the current user's userpage.
|
||||
func UserSelfUserpagePOST(md common.MethodData) common.CodeMessager {
|
||||
var d struct {
|
||||
Data *string `json:"data"`
|
||||
}
|
||||
md.Unmarshal(&d)
|
||||
if d.Data == nil {
|
||||
return ErrMissingField("data")
|
||||
}
|
||||
cont := common.SanitiseString(*d.Data)
|
||||
_, err := md.DB.Exec("UPDATE users_stats SET userpage_content = ? WHERE id = ? LIMIT 1", cont, md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
md.Ctx.URI().SetQueryString("id=self")
|
||||
return UserUserpageGET(md)
|
||||
}
|
||||
|
||||
func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) {
|
||||
switch {
|
||||
case md.Query("id") == "self":
|
||||
return nil, tableName + ".id = ?", md.ID()
|
||||
case md.Query("id") != "":
|
||||
id, err := strconv.Atoi(md.Query("id"))
|
||||
if err != nil {
|
||||
a := common.SimpleResponse(400, "please pass a valid user ID")
|
||||
return &a, "", nil
|
||||
}
|
||||
return nil, tableName + ".id = ?", id
|
||||
case md.Query("name") != "":
|
||||
return nil, tableName + ".username_safe = ?", common.SafeUsername(md.Query("name"))
|
||||
}
|
||||
a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id")
|
||||
return &a, "", nil
|
||||
}
|
||||
|
||||
type userLookupResponse struct {
|
||||
common.ResponseBase
|
||||
Users []lookupUser `json:"users"`
|
||||
}
|
||||
type lookupUser struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// UserLookupGET does a quick lookup of users beginning with the passed
|
||||
// querystring value name.
|
||||
func UserLookupGET(md common.MethodData) common.CodeMessager {
|
||||
name := common.SafeUsername(md.Query("name"))
|
||||
name = strings.NewReplacer(
|
||||
"%", "\\%",
|
||||
"_", "\\_",
|
||||
"\\", "\\\\",
|
||||
).Replace(name)
|
||||
if name == "" {
|
||||
return common.SimpleResponse(400, "please provide an username to start searching")
|
||||
}
|
||||
name = "%" + name + "%"
|
||||
|
||||
var email string
|
||||
if md.User.TokenPrivileges&common.PrivilegeManageUser != 0 &&
|
||||
strings.Contains(md.Query("name"), "@") {
|
||||
email = md.Query("name")
|
||||
}
|
||||
|
||||
rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE "+
|
||||
"(username_safe LIKE ? OR email = ?) AND "+
|
||||
md.User.OnlyUserPublic(true)+" LIMIT 25", name, email)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var r userLookupResponse
|
||||
for rows.Next() {
|
||||
var l lookupUser
|
||||
err := rows.Scan(&l.ID, &l.Username)
|
||||
if err != nil {
|
||||
continue // can't be bothered to handle properly
|
||||
}
|
||||
r.Users = append(r.Users, l)
|
||||
}
|
||||
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
84
vendor/zxq.co/ripple/rippleapi/app/v1/user_achievements.go
vendored
Normal file
84
vendor/zxq.co/ripple/rippleapi/app/v1/user_achievements.go
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Achievement represents an achievement in the database.
|
||||
type Achievement struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// LoadAchievementsEvery reloads the achievements in the database every given
|
||||
// amount of time.
|
||||
func LoadAchievementsEvery(db *sqlx.DB, d time.Duration) {
|
||||
for {
|
||||
achievs = nil
|
||||
err := db.Select(&achievs,
|
||||
"SELECT id, name, description, icon FROM achievements ORDER BY id ASC")
|
||||
if err != nil {
|
||||
fmt.Println("LoadAchievements error", err)
|
||||
common.GenericError(err)
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
}
|
||||
|
||||
var achievs []Achievement
|
||||
|
||||
type userAchievement struct {
|
||||
Achievement
|
||||
Achieved bool `json:"achieved"`
|
||||
}
|
||||
|
||||
type userAchievementsResponse struct {
|
||||
common.ResponseBase
|
||||
Achievements []userAchievement `json:"achievements"`
|
||||
}
|
||||
|
||||
// UserAchievementsGET handles requests for retrieving the achievements of a
|
||||
// given user.
|
||||
func UserAchievementsGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
}
|
||||
var ids []int
|
||||
err := md.DB.Select(&ids, `SELECT ua.achievement_id FROM users_achievements ua
|
||||
INNER JOIN users ON users.id = ua.user_id
|
||||
WHERE `+whereClause+` ORDER BY ua.achievement_id ASC`, param)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user!")
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
all := md.HasQuery("all")
|
||||
resp := userAchievementsResponse{Achievements: make([]userAchievement, 0, len(achievs))}
|
||||
for _, ach := range achievs {
|
||||
achieved := inInt(ach.ID, ids)
|
||||
if all || achieved {
|
||||
resp.Achievements = append(resp.Achievements, userAchievement{ach, achieved})
|
||||
}
|
||||
}
|
||||
resp.Code = 200
|
||||
return resp
|
||||
}
|
||||
|
||||
func inInt(i int, js []int) bool {
|
||||
for _, j := range js {
|
||||
if i == j {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
127
vendor/zxq.co/ripple/rippleapi/app/v1/user_scores.go
vendored
Normal file
127
vendor/zxq.co/ripple/rippleapi/app/v1/user_scores.go
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
type userScore struct {
|
||||
Score
|
||||
Beatmap beatmap `json:"beatmap"`
|
||||
}
|
||||
|
||||
type userScoresResponse struct {
|
||||
common.ResponseBase
|
||||
Scores []userScore `json:"scores"`
|
||||
}
|
||||
|
||||
const userScoreSelectBase = `
|
||||
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,
|
||||
|
||||
beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5,
|
||||
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty_std,
|
||||
beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania,
|
||||
beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked,
|
||||
beatmaps.ranked_status_freezed, beatmaps.latest_update
|
||||
FROM scores
|
||||
INNER JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
`
|
||||
|
||||
// UserScoresBestGET retrieves the best scores of an user, sorted by PP if
|
||||
// mode is standard and sorted by ranked score otherwise.
|
||||
func UserScoresBestGET(md common.MethodData) common.CodeMessager {
|
||||
cm, wc, param := whereClauseUser(md, "users")
|
||||
if cm != nil {
|
||||
return *cm
|
||||
}
|
||||
mc := genModeClause(md)
|
||||
// For all modes that have PP, we leave out 0 PP scores.
|
||||
if getMode(md.Query("mode")) != "ctb" {
|
||||
mc += " AND scores.pp > 0"
|
||||
}
|
||||
return scoresPuts(md, fmt.Sprintf(
|
||||
`WHERE
|
||||
scores.completed = '3'
|
||||
AND %s
|
||||
%s
|
||||
AND `+md.User.OnlyUserPublic(true)+`
|
||||
ORDER BY scores.pp DESC, scores.score DESC %s`,
|
||||
wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
|
||||
), param)
|
||||
}
|
||||
|
||||
// UserScoresRecentGET retrieves an user's latest scores.
|
||||
func UserScoresRecentGET(md common.MethodData) common.CodeMessager {
|
||||
cm, wc, param := whereClauseUser(md, "users")
|
||||
if cm != nil {
|
||||
return *cm
|
||||
}
|
||||
return scoresPuts(md, fmt.Sprintf(
|
||||
`WHERE
|
||||
%s
|
||||
%s
|
||||
AND `+md.User.OnlyUserPublic(true)+`
|
||||
ORDER BY scores.id DESC %s`,
|
||||
wc, genModeClause(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
|
||||
), param)
|
||||
}
|
||||
|
||||
func scoresPuts(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager {
|
||||
rows, err := md.DB.Query(userScoreSelectBase+whereClause, params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var scores []userScore
|
||||
for rows.Next() {
|
||||
var (
|
||||
us userScore
|
||||
b beatmap
|
||||
)
|
||||
err = rows.Scan(
|
||||
&us.ID, &us.BeatmapMD5, &us.Score.Score,
|
||||
&us.MaxCombo, &us.FullCombo, &us.Mods,
|
||||
&us.Count300, &us.Count100, &us.Count50,
|
||||
&us.CountGeki, &us.CountKatu, &us.CountMiss,
|
||||
&us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
|
||||
&us.Completed,
|
||||
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD,
|
||||
&b.Diff2.Taiko, &b.Diff2.CTB, &b.Diff2.Mania,
|
||||
&b.MaxCombo, &b.HitLength, &b.Ranked,
|
||||
&b.RankedStatusFrozen, &b.LatestUpdate,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
b.Difficulty = b.Diff2.STD
|
||||
us.Beatmap = b
|
||||
us.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(us.PlayMode),
|
||||
osuapi.Mods(us.Mods),
|
||||
us.Accuracy,
|
||||
us.Count300,
|
||||
us.Count100,
|
||||
us.Count50,
|
||||
us.CountMiss,
|
||||
))
|
||||
scores = append(scores, us)
|
||||
}
|
||||
r := userScoresResponse{}
|
||||
r.Code = 200
|
||||
r.Scores = scores
|
||||
return r
|
||||
}
|
Reference in New Issue
Block a user