mirror of
https://github.com/muety/wakapi.git
synced 2025-12-05 22:20:24 -08:00
172 lines
5.8 KiB
Go
172 lines
5.8 KiB
Go
package v1
|
|
|
|
import (
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/duke-git/lancet/v2/slice"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/muety/wakapi/helpers"
|
|
"github.com/muety/wakapi/middlewares"
|
|
"github.com/muety/wakapi/models"
|
|
"github.com/muety/wakapi/utils"
|
|
|
|
conf "github.com/muety/wakapi/config"
|
|
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
|
"github.com/muety/wakapi/services"
|
|
)
|
|
|
|
type LeadersHandler struct {
|
|
config *conf.Config
|
|
userSrvc services.IUserService
|
|
leaderboardSrvc services.ILeaderboardService
|
|
}
|
|
|
|
func NewLeadersHandler(userService services.IUserService, leaderboardService services.ILeaderboardService) *LeadersHandler {
|
|
return &LeadersHandler{
|
|
userSrvc: userService,
|
|
leaderboardSrvc: leaderboardService,
|
|
config: conf.Get(),
|
|
}
|
|
}
|
|
|
|
func (h *LeadersHandler) RegisterRoutes(router chi.Router) {
|
|
router.Group(func(r chi.Router) {
|
|
r.Use(middlewares.NewAuthenticateMiddleware(h.userSrvc).WithOptionalFor("/").Handler)
|
|
r.Get("/compat/wakatime/v1/leaders", h.Get)
|
|
})
|
|
}
|
|
|
|
// @Summary List of users ranked by coding activity in descending order.
|
|
// @Description Mimics https://wakatime.com/developers#leaders
|
|
// @ID get-wakatime-leaders
|
|
// @Tags wakatime
|
|
// @Produce json
|
|
// @Security ApiKeyAuth
|
|
// @Success 200 {object} v1.LeadersViewModel
|
|
// @Router /compat/wakatime/v1/leaders [get]
|
|
func (h *LeadersHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
user := middlewares.GetPrincipal(r)
|
|
languageParam := strings.ToLower(r.URL.Query().Get("language"))
|
|
pageParams := utils.ParsePageParamsWithDefault(r, 1, 100)
|
|
by := models.SummaryLanguage
|
|
|
|
loadPrimaryLeaderboard := func() (models.Leaderboard, error) {
|
|
if languageParam == "" {
|
|
return h.leaderboardSrvc.GetByInterval(h.leaderboardSrvc.GetDefaultScope(), pageParams, true)
|
|
} else {
|
|
l, err := h.leaderboardSrvc.GetAggregatedByInterval(h.leaderboardSrvc.GetDefaultScope(), &by, pageParams, true)
|
|
if err == nil {
|
|
return l.TopByKey(by, languageParam), err
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
loadPrimaryUserLeaderboard := func() (models.Leaderboard, error) {
|
|
if user == nil {
|
|
return []*models.LeaderboardItemRanked{}, nil
|
|
}
|
|
if languageParam == "" {
|
|
return h.leaderboardSrvc.GetByIntervalAndUser(h.leaderboardSrvc.GetDefaultScope(), user.ID, true)
|
|
} else {
|
|
l, err := h.leaderboardSrvc.GetAggregatedByIntervalAndUser(h.leaderboardSrvc.GetDefaultScope(), user.ID, &by, true)
|
|
if err == nil {
|
|
return l.TopByKey(by, languageParam), err
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
primaryLeaderboard, err := loadPrimaryLeaderboard()
|
|
if err != nil {
|
|
conf.Log().Request(r).Error("error while fetching general leaderboard items", "error", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("something went wrong"))
|
|
return
|
|
}
|
|
primaryLeaderboard.FilterEmpty()
|
|
|
|
languageLeaderboard, err := h.leaderboardSrvc.GetAggregatedByInterval(h.leaderboardSrvc.GetDefaultScope(), &by, &utils.PageParams{Page: 1, PageSize: math.MaxUint16}, true)
|
|
if err != nil {
|
|
conf.Log().Request(r).Error("error while fetching language-specific leaderboard items", "error", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("something went wrong"))
|
|
return
|
|
}
|
|
|
|
// regardless of page, always show own rank
|
|
if user != nil && !primaryLeaderboard.HasUser(user.ID) {
|
|
if l, err := loadPrimaryUserLeaderboard(); err == nil {
|
|
primaryLeaderboard.AddMany(l)
|
|
} else {
|
|
conf.Log().Request(r).Error("error while fetching own general user leaderboard", "userID", user.ID, "error", err)
|
|
}
|
|
// no need to fetch language-leaderboard for user, because not using pagination above
|
|
}
|
|
|
|
vm := h.buildViewModel(primaryLeaderboard, languageLeaderboard, user, h.leaderboardSrvc.GetDefaultScope(), pageParams)
|
|
vm.Language = languageParam
|
|
helpers.RespondJSON(w, r, http.StatusOK, vm)
|
|
}
|
|
|
|
func (h *LeadersHandler) buildViewModel(globalLeaderboard, languageLeaderboard models.Leaderboard, user *models.User, interval *models.IntervalKey, pageParams *utils.PageParams) *v1.LeadersViewModel {
|
|
var currentUserGlobal []*models.LeaderboardItemRanked
|
|
if user != nil {
|
|
currentUserGlobal = *globalLeaderboard.GetByUser(user.ID)
|
|
}
|
|
|
|
totalUsers, _ := h.leaderboardSrvc.CountUsers(true)
|
|
totalPages := int(totalUsers/int64(pageParams.PageSize) + 1)
|
|
|
|
_, from, to := helpers.ResolveIntervalTZ(interval, time.UTC, time.Monday)
|
|
numDays := len(utils.SplitRangeByDays(from, to))
|
|
|
|
vm := &v1.LeadersViewModel{
|
|
Data: make([]*v1.LeadersEntry, 0, len(languageLeaderboard.UserIDs())),
|
|
Page: pageParams.Page,
|
|
TotalPages: totalPages,
|
|
Range: &v1.LeadersRange{
|
|
EndText: helpers.FormatDateHuman(to),
|
|
EndDate: to.Format(time.RFC3339),
|
|
StartText: helpers.FormatDateHuman(from),
|
|
StartDate: from.Format(time.RFC3339),
|
|
Name: (*interval)[0],
|
|
Text: interval.GetHumanReadable(),
|
|
},
|
|
}
|
|
|
|
if len(currentUserGlobal) > 0 {
|
|
vm.CurrentUser = &v1.LeadersCurrentUser{
|
|
Rank: int(currentUserGlobal[0].Rank),
|
|
Page: 1,
|
|
User: v1.NewFromUser(currentUserGlobal[0].User),
|
|
}
|
|
}
|
|
|
|
for _, entry := range globalLeaderboard {
|
|
dailyAverage := entry.Total / time.Duration(numDays)
|
|
|
|
vm.Data = append(vm.Data, &v1.LeadersEntry{
|
|
Rank: int(entry.Rank),
|
|
RunningTotal: &v1.LeadersRunningTotal{
|
|
TotalSeconds: float64(entry.Total / time.Second),
|
|
HumanReadableTotal: helpers.FmtWakatimeDuration(entry.Total),
|
|
DailyAverage: float64(dailyAverage / time.Second),
|
|
HumanReadableDailyAverage: helpers.FmtWakatimeDuration(dailyAverage),
|
|
Languages: slice.Map[models.LeaderboardKeyTotal, *v1.LeadersLanguage](languageLeaderboard.TopKeysTotalsByUser(models.SummaryLanguage, entry.UserID), func(i int, item models.LeaderboardKeyTotal) *v1.LeadersLanguage {
|
|
return &v1.LeadersLanguage{
|
|
Name: item.Key,
|
|
TotalSeconds: float64(item.Total / time.Second),
|
|
}
|
|
}),
|
|
},
|
|
User: v1.NewFromUser(entry.User),
|
|
})
|
|
}
|
|
|
|
return vm
|
|
}
|