Implement country leaderboards
This commit is contained in:
		@@ -4,14 +4,21 @@ package peppy
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/thehowl/go-osuapi"
 | 
			
		||||
	"github.com/valyala/fasthttp"
 | 
			
		||||
	"gopkg.in/redis.v5"
 | 
			
		||||
	"zxq.co/ripple/ocl"
 | 
			
		||||
	"zxq.co/ripple/rippleapi/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// R is a redis client.
 | 
			
		||||
var R *redis.Client
 | 
			
		||||
 | 
			
		||||
// GetUser retrieves general user information.
 | 
			
		||||
func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
 | 
			
		||||
	if query(c, "u") == "" {
 | 
			
		||||
@@ -24,23 +31,21 @@ func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
 | 
			
		||||
 | 
			
		||||
	mode := genmode(query(c, "m"))
 | 
			
		||||
 | 
			
		||||
	var lbpos *int
 | 
			
		||||
	err := db.QueryRow(fmt.Sprintf(
 | 
			
		||||
		`SELECT
 | 
			
		||||
			users.id, users.username,
 | 
			
		||||
			users_stats.playcount_%s, users_stats.ranked_score_%s, users_stats.total_score_%s,
 | 
			
		||||
			leaderboard_%s.position, users_stats.pp_%s, users_stats.avg_accuracy_%s,
 | 
			
		||||
			users_stats.pp_%s, users_stats.avg_accuracy_%s,
 | 
			
		||||
			users_stats.country
 | 
			
		||||
		FROM users
 | 
			
		||||
		LEFT JOIN users_stats ON users_stats.id = users.id
 | 
			
		||||
		INNER JOIN leaderboard_%s ON leaderboard_%s.user = users.id
 | 
			
		||||
		%s
 | 
			
		||||
		LIMIT 1`,
 | 
			
		||||
		mode, mode, mode, mode, mode, mode, mode, mode, whereClause,
 | 
			
		||||
		mode, mode, mode, mode, mode, whereClause,
 | 
			
		||||
	), p).Scan(
 | 
			
		||||
		&user.UserID, &user.Username,
 | 
			
		||||
		&user.Playcount, &user.RankedScore, &user.TotalScore,
 | 
			
		||||
		&lbpos, &user.PP, &user.Accuracy,
 | 
			
		||||
		&user.PP, &user.Accuracy,
 | 
			
		||||
		&user.Country,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -50,9 +55,9 @@ func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if lbpos != nil {
 | 
			
		||||
		user.Rank = *lbpos
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user.Rank = int(R.ZRevRank("ripple:leaderboard:"+mode, strconv.Itoa(user.UserID)).Val()) + 1
 | 
			
		||||
	user.CountryRank = int(R.ZRevRank("ripple:leaderboard:"+mode+":"+strings.ToLower(user.Country), strconv.Itoa(user.UserID)).Val()) + 1
 | 
			
		||||
	user.Level = ocl.GetLevelPrecise(user.TotalScore)
 | 
			
		||||
 | 
			
		||||
	json(c, 200, []osuapi.User{user})
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@ func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
 | 
			
		||||
		Password: conf.RedisPassword,
 | 
			
		||||
		DB:       conf.RedisDB,
 | 
			
		||||
	})
 | 
			
		||||
	peppy.R = red
 | 
			
		||||
 | 
			
		||||
	// token updater
 | 
			
		||||
	go tokenUpdater(db)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,12 @@ package v1
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
 | 
			
		||||
	redis "gopkg.in/redis.v5"
 | 
			
		||||
 | 
			
		||||
	"zxq.co/ripple/ocl"
 | 
			
		||||
	"zxq.co/ripple/rippleapi/common"
 | 
			
		||||
@@ -28,29 +34,48 @@ SELECT
 | 
			
		||||
 | 
			
		||||
	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, leaderboard_%[1]s.position as %[1]s_position
 | 
			
		||||
FROM leaderboard_%[1]s
 | 
			
		||||
INNER JOIN users ON users.id = leaderboard_%[1]s.user
 | 
			
		||||
INNER JOIN users_stats ON users_stats.id = leaderboard_%[1]s.user
 | 
			
		||||
%[2]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"))
 | 
			
		||||
	w := &common.WhereClause{
 | 
			
		||||
		Clause: "WHERE " + md.User.OnlyUserPublic(md.HasQuery("see_everything")),
 | 
			
		||||
 | 
			
		||||
	// md.Query.Country
 | 
			
		||||
	p := common.Int(md.Query("p")) - 1
 | 
			
		||||
	if p < 0 {
 | 
			
		||||
		p = 0
 | 
			
		||||
	}
 | 
			
		||||
	w.Where("users_stats.country = ?", md.Query("country"))
 | 
			
		||||
	// Admins may not want to see banned users on the leaderboard.
 | 
			
		||||
	// This is the default setting. In case they do, they have to activate see_everything.
 | 
			
		||||
	query := fmt.Sprintf(lbUserQuery, m, w.Clause+
 | 
			
		||||
		` ORDER BY leaderboard_`+m+`.position `+common.Paginate(md.Query("p"), md.Query("l"), 500))
 | 
			
		||||
	rows, err := md.DB.Query(query, w.Params...)
 | 
			
		||||
	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(
 | 
			
		||||
@@ -60,15 +85,28 @@ func LeaderboardGET(md common.MethodData) common.CodeMessager {
 | 
			
		||||
 | 
			
		||||
			&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
 | 
			
		||||
			&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
 | 
			
		||||
			&u.ChosenMode.Accuracy, &u.ChosenMode.PP, &u.ChosenMode.GlobalLeaderboardRank,
 | 
			
		||||
			&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 != 0 {
 | 
			
		||||
			u.ChosenMode.GlobalLeaderboardRank = &i
 | 
			
		||||
		}
 | 
			
		||||
		if i := countryPosition(md.R, m, u.ID, u.Country); i != 0 {
 | 
			
		||||
			u.ChosenMode.CountryLeaderboardRank = &i
 | 
			
		||||
		}
 | 
			
		||||
		resp.Users = append(resp.Users, u)
 | 
			
		||||
	}
 | 
			
		||||
	resp.Code = 200
 | 
			
		||||
	return resp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func leaderboardPosition(r *redis.Client, mode string, user int) int {
 | 
			
		||||
	return int(r.ZRevRank("ripple:leaderboard:"+mode, strconv.Itoa(user)).Val()) + 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func countryPosition(r *redis.Client, mode string, user int, country string) int {
 | 
			
		||||
	return int(r.ZRevRank("ripple:leaderboard:"+mode+":"+strings.ToLower(country), strconv.Itoa(user)).Val()) + 1
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -166,16 +166,24 @@ func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager {
 | 
			
		||||
	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"`
 | 
			
		||||
	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
 | 
			
		||||
@@ -214,33 +222,25 @@ SELECT
 | 
			
		||||
 | 
			
		||||
	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, leaderboard_std.position as std_position,
 | 
			
		||||
	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, leaderboard_taiko.position as taiko_position,
 | 
			
		||||
	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, leaderboard_ctb.position as ctb_position,
 | 
			
		||||
	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, leaderboard_mania.position as mania_position,
 | 
			
		||||
	users_stats.avg_accuracy_mania, users_stats.pp_mania,
 | 
			
		||||
 | 
			
		||||
	users.silence_reason, users.silence_end
 | 
			
		||||
 | 
			
		||||
FROM users
 | 
			
		||||
LEFT JOIN users_stats
 | 
			
		||||
ON users.id=users_stats.id
 | 
			
		||||
LEFT JOIN leaderboard_std
 | 
			
		||||
ON users.id=leaderboard_std.user
 | 
			
		||||
LEFT JOIN leaderboard_taiko
 | 
			
		||||
ON users.id=leaderboard_taiko.user
 | 
			
		||||
LEFT JOIN leaderboard_ctb
 | 
			
		||||
ON users.id=leaderboard_ctb.user
 | 
			
		||||
LEFT JOIN leaderboard_mania
 | 
			
		||||
ON users.id=leaderboard_mania.user
 | 
			
		||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
 | 
			
		||||
LIMIT 1
 | 
			
		||||
`
 | 
			
		||||
@@ -261,19 +261,19 @@ LIMIT 1
 | 
			
		||||
 | 
			
		||||
		&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
 | 
			
		||||
		&r.STD.ReplaysWatched, &r.STD.TotalHits,
 | 
			
		||||
		&r.STD.Accuracy, &r.STD.PP, &r.STD.GlobalLeaderboardRank,
 | 
			
		||||
		&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.Taiko.GlobalLeaderboardRank,
 | 
			
		||||
		&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.CTB.GlobalLeaderboardRank,
 | 
			
		||||
		&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.Mania.GlobalLeaderboardRank,
 | 
			
		||||
		&r.Mania.Accuracy, &r.Mania.PP,
 | 
			
		||||
 | 
			
		||||
		&r.SilenceInfo.Reason, &r.SilenceInfo.End,
 | 
			
		||||
	)
 | 
			
		||||
@@ -290,8 +290,15 @@ LIMIT 1
 | 
			
		||||
		r.CustomBadge = &b
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, m := range []*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
 | 
			
		||||
	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 != 0 {
 | 
			
		||||
			m.GlobalLeaderboardRank = &i
 | 
			
		||||
		}
 | 
			
		||||
		if i := countryPosition(md.R, modesToReadable[modeID], r.ID, r.Country); i != 0 {
 | 
			
		||||
			m.CountryLeaderboardRank = &i
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user