2021-02-06 18:05:34 +03:00
package v1
import (
2023-03-03 22:40:50 +03:00
"github.com/go-chi/chi/v5"
2022-12-01 12:57:07 +03:00
"github.com/muety/wakapi/helpers"
2022-01-02 03:22:58 +03:00
"net/http"
"time"
2021-02-06 18:05:34 +03:00
conf "github.com/muety/wakapi/config"
2021-02-06 22:09:08 +03:00
"github.com/muety/wakapi/middlewares"
2021-02-06 18:05:34 +03:00
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
"github.com/muety/wakapi/services"
)
type StatsHandler struct {
config * conf . Config
2021-02-06 22:09:08 +03:00
userSrvc services . IUserService
2021-02-06 18:05:34 +03:00
summarySrvc services . ISummaryService
}
2021-02-06 22:09:08 +03:00
func NewStatsHandler ( userService services . IUserService , summaryService services . ISummaryService ) * StatsHandler {
2021-02-06 18:05:34 +03:00
return & StatsHandler {
2021-02-06 22:09:08 +03:00
userSrvc : userService ,
2021-02-06 18:05:34 +03:00
summarySrvc : summaryService ,
config : conf . Get ( ) ,
}
}
2023-03-03 22:40:50 +03:00
func ( h * StatsHandler ) RegisterRoutes ( router chi . Router ) {
router . Group ( func ( r chi . Router ) {
r . Use (
middlewares . NewAuthenticateMiddleware ( h . userSrvc ) . WithOptionalFor ( [ ] string { "/" } ) . Handler ,
)
r . Get ( "/v1/users/{user}/stats/{range}" , h . Get )
r . Get ( "/compat/wakatime/v1/users/{user}/stats/{range}" , h . Get )
2021-02-12 12:29:01 +03:00
2023-03-03 22:40:50 +03:00
// Also works without range, see https://github.com/anuraghazra/github-readme-stats/issues/865#issuecomment-776186592
r . Get ( "/v1/users/{user}/stats" , h . Get )
r . Get ( "/compat/wakatime/v1/users/{user}/stats" , h . Get )
} )
2021-02-06 18:05:34 +03:00
}
// TODO: support filtering (requires https://github.com/muety/wakapi/issues/108)
2021-04-30 11:12:28 +03:00
// @Summary Retrieve statistics for a given user
// @Description Mimics https://wakatime.com/developers#stats
// @ID get-wakatimes-tats
// @Tags wakatime
// @Produce json
// @Param user path string true "User ID to fetch data for (or 'current')"
2022-08-19 18:14:00 +03:00
// @Param range path string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 6_months, last_6_months, 12_months, last_12_months, last_year, any, all_time)
2021-12-26 21:29:17 +03:00
// @Param project query string false "Project to filter by"
// @Param language query string false "Language to filter by"
// @Param editor query string false "Editor to filter by"
// @Param operating_system query string false "OS to filter by"
// @Param machine query string false "Machine to filter by"
// @Param label query string false "Project label to filter by"
2021-04-30 11:12:28 +03:00
// @Security ApiKeyAuth
// @Success 200 {object} v1.StatsViewModel
2022-01-02 03:22:58 +03:00
// @Router /compat/wakatime/v1/users/{user}/stats/{range} [get]
2021-02-06 18:05:34 +03:00
func ( h * StatsHandler ) Get ( w http . ResponseWriter , r * http . Request ) {
2023-03-03 22:40:50 +03:00
userParam := chi . URLParam ( r , "user" )
rangeParam := chi . URLParam ( r , "range" )
2021-02-07 00:32:03 +03:00
var authorizedUser , requestedUser * models . User
2021-02-06 18:05:34 +03:00
2021-03-26 15:10:10 +03:00
authorizedUser = middlewares . GetPrincipal ( r )
2023-03-03 22:40:50 +03:00
if authorizedUser != nil && userParam == "current" {
userParam = authorizedUser . ID
2021-02-07 00:32:03 +03:00
}
2023-03-03 22:40:50 +03:00
requestedUser , err := h . userSrvc . GetUserById ( userParam )
2021-02-07 00:32:03 +03:00
if err != nil {
w . WriteHeader ( http . StatusNotFound )
w . Write ( [ ] byte ( "user not found" ) )
return
}
2021-02-06 18:05:34 +03:00
2023-07-28 13:08:47 +03:00
// if no range was requested, get the maximum allowed range given the users max shared days, otherwise default to past 7 days (which will fail in the next step, because user didn't allow any sharing)
// this "floors" the user's maximum shared date to the supported range buckets (e.g. if user opted to share 12 days, we'll still fallback to "last_7_days") for consistency with wakatime
2021-02-12 12:29:01 +03:00
if rangeParam == "" {
2023-07-28 13:08:47 +03:00
if _ , userRange := helpers . ResolveMaximumRange ( requestedUser . ShareDataMaxDays ) ; userRange != nil {
rangeParam = ( * userRange ) [ 1 ]
} else {
rangeParam = ( * models . IntervalPast7Days ) [ 1 ]
}
2021-02-12 12:29:01 +03:00
}
2022-12-01 12:57:07 +03:00
err , rangeFrom , rangeTo := helpers . ResolveIntervalRawTZ ( rangeParam , requestedUser . TZ ( ) )
2021-02-07 00:32:03 +03:00
if err != nil {
w . WriteHeader ( http . StatusBadRequest )
w . Write ( [ ] byte ( "invalid range" ) )
return
}
2022-11-01 01:24:25 +03:00
minStart := rangeTo . AddDate ( 0 , 0 , - requestedUser . ShareDataMaxDays )
2021-02-07 00:32:03 +03:00
if ( authorizedUser == nil || requestedUser . ID != authorizedUser . ID ) &&
2021-02-07 01:23:26 +03:00
rangeFrom . Before ( minStart ) && requestedUser . ShareDataMaxDays >= 0 {
2021-02-06 18:05:34 +03:00
w . WriteHeader ( http . StatusForbidden )
2021-02-07 00:32:03 +03:00
w . Write ( [ ] byte ( "requested time range too broad" ) )
2021-02-06 18:05:34 +03:00
return
}
2022-12-01 12:57:07 +03:00
summary , err , status := h . loadUserSummary ( requestedUser , rangeFrom , rangeTo , helpers . ParseSummaryFilters ( r ) )
2021-02-06 18:05:34 +03:00
if err != nil {
w . WriteHeader ( status )
w . Write ( [ ] byte ( err . Error ( ) ) )
return
}
2021-02-07 00:32:03 +03:00
stats := v1 . NewStatsFrom ( summary , & models . Filters { } )
2023-07-28 13:08:47 +03:00
stats . Data . Range = rangeParam
stats . Data . HumanReadableRange = helpers . MustParseInterval ( rangeParam ) . GetHumanReadable ( )
stats . Data . IsCodingActivityVisible = requestedUser . ShareDataMaxDays != 0
stats . Data . IsOtherUsageVisible = requestedUser . AnyDataShared ( )
2021-02-07 00:32:03 +03:00
// post filter stats according to user's given sharing permissions
if ! requestedUser . ShareEditors {
stats . Data . Editors = nil
}
if ! requestedUser . ShareLanguages {
stats . Data . Languages = nil
}
if ! requestedUser . ShareProjects {
stats . Data . Projects = nil
}
if ! requestedUser . ShareOSs {
stats . Data . OperatingSystems = nil
}
if ! requestedUser . ShareMachines {
stats . Data . Machines = nil
2021-02-06 18:05:34 +03:00
}
2022-12-01 12:57:07 +03:00
helpers . RespondJSON ( w , r , http . StatusOK , stats )
2021-02-06 18:05:34 +03:00
}
2021-12-26 19:02:14 +03:00
func ( h * StatsHandler ) loadUserSummary ( user * models . User , start , end time . Time , filters * models . Filters ) ( * models . Summary , error , int ) {
2021-02-06 18:05:34 +03:00
overallParams := & models . SummaryParams {
From : start ,
To : end ,
User : user ,
Recompute : false ,
}
2021-12-26 19:02:14 +03:00
summary , err := h . summarySrvc . Aliased ( overallParams . From , overallParams . To , user , h . summarySrvc . Retrieve , filters , false )
2021-02-06 18:05:34 +03:00
if err != nil {
return nil , err , http . StatusInternalServerError
}
return summary , nil , http . StatusOK
}