mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat: add wakatime-compatible alltime endpoint
This commit is contained in:
parent
97cb29ee4d
commit
587ac6a330
8
main.go
8
main.go
@ -15,6 +15,7 @@ import (
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/routes"
|
||||
v1Routes "github.com/muety/wakapi/routes/compat/v1"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
@ -86,12 +87,15 @@ func main() {
|
||||
go heartbeatService.ScheduleCleanUp()
|
||||
}
|
||||
|
||||
// TODO: move endpoint registration to the respective routes files
|
||||
|
||||
// Handlers
|
||||
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService)
|
||||
summaryHandler := routes.NewSummaryHandler(summaryService)
|
||||
healthHandler := routes.NewHealthHandler(db)
|
||||
settingsHandler := routes.NewSettingsHandler(userService)
|
||||
publicHandler := routes.NewIndexHandler(userService, keyValueService)
|
||||
compatV1AllHandler := v1Routes.NewCompatV1AllHandler(summaryService)
|
||||
|
||||
// Setup Routers
|
||||
router := mux.NewRouter()
|
||||
@ -99,6 +103,7 @@ func main() {
|
||||
settingsRouter := publicRouter.PathPrefix("/settings").Subrouter()
|
||||
summaryRouter := publicRouter.PathPrefix("/summary").Subrouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
compatV1Router := apiRouter.PathPrefix("/compat/v1").Subrouter()
|
||||
|
||||
// Middlewares
|
||||
recoveryMiddleware := handlers.RecoveryHandler()
|
||||
@ -136,6 +141,9 @@ func main() {
|
||||
apiRouter.Path("/summary").Methods(http.MethodGet).HandlerFunc(summaryHandler.ApiGet)
|
||||
apiRouter.Path("/health").Methods(http.MethodGet).HandlerFunc(healthHandler.ApiGet)
|
||||
|
||||
// Compat V1 API Routes
|
||||
compatV1Router.Path("/users/{user}/all_time_since_today").Methods(http.MethodGet).HandlerFunc(compatV1AllHandler.ApiGet)
|
||||
|
||||
// Static Routes
|
||||
router.PathPrefix("/assets").Handler(http.FileServer(http.Dir("./static")))
|
||||
|
||||
|
9
models/compat/v1/all_time.go
Normal file
9
models/compat/v1/all_time.go
Normal file
@ -0,0 +1,9 @@
|
||||
package v1
|
||||
|
||||
// https://wakatime.com/developers#all_time_since_today
|
||||
|
||||
type AllTimeVieModel struct {
|
||||
Seconds float32 `json:"seconds"` // total number of seconds logged since account created
|
||||
Text string `json:"text"` // total time logged since account created as human readable string>
|
||||
IsUpToDate bool `json:"is_up_to_date"` // true if the stats are up to date; when false, a 202 response code is returned and stats will be refreshed soon>
|
||||
}
|
@ -13,6 +13,15 @@ const (
|
||||
SummaryMachine uint8 = 4
|
||||
)
|
||||
|
||||
const (
|
||||
IntervalToday string = "today"
|
||||
IntervalLastDay string = "day"
|
||||
IntervalLastWeek string = "week"
|
||||
IntervalLastMonth string = "month"
|
||||
IntervalLastYear string = "year"
|
||||
IntervalAny string = "any"
|
||||
)
|
||||
|
||||
const UnknownSummaryKey = "unknown"
|
||||
|
||||
type Summary struct {
|
||||
@ -48,6 +57,31 @@ type SummaryViewModel struct {
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type SummaryParams struct {
|
||||
From time.Time
|
||||
To time.Time
|
||||
User *User
|
||||
Recompute bool
|
||||
}
|
||||
|
||||
func SummaryTypes() []uint8 {
|
||||
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine}
|
||||
}
|
||||
|
||||
func (s *Summary) Types() []uint8 {
|
||||
return SummaryTypes()
|
||||
}
|
||||
|
||||
func (s *Summary) MappedItems() map[uint8]*[]*SummaryItem {
|
||||
return map[uint8]*[]*SummaryItem{
|
||||
SummaryProject: &s.Projects,
|
||||
SummaryLanguage: &s.Languages,
|
||||
SummaryEditor: &s.Editors,
|
||||
SummaryOS: &s.OperatingSystems,
|
||||
SummaryMachine: &s.Machines,
|
||||
}
|
||||
}
|
||||
|
||||
/* Augments the summary in a way that at least one item is present for every type.
|
||||
If a summary has zero items for a given type, but one or more for any of the other types,
|
||||
the total summary duration can be derived from those and inserted as a dummy-item with key "unknown"
|
||||
@ -60,22 +94,13 @@ To avoid having to modify persisted data retrospectively, i.e. inserting a dummy
|
||||
such is generated dynamically here, considering the "machine" for all old heartbeats "unknown".
|
||||
*/
|
||||
func (s *Summary) FillUnknown() {
|
||||
types := []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine}
|
||||
types := s.Types()
|
||||
typeItems := s.MappedItems()
|
||||
missingTypes := make([]uint8, 0)
|
||||
typeItems := map[uint8]*[]*SummaryItem{
|
||||
SummaryProject: &s.Projects,
|
||||
SummaryLanguage: &s.Languages,
|
||||
SummaryEditor: &s.Editors,
|
||||
SummaryOS: &s.OperatingSystems,
|
||||
SummaryMachine: &s.Machines,
|
||||
}
|
||||
var somePresentType uint8
|
||||
|
||||
for _, t := range types {
|
||||
if len(*typeItems[t]) == 0 {
|
||||
missingTypes = append(missingTypes, t)
|
||||
} else {
|
||||
somePresentType = t
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,11 +109,7 @@ func (s *Summary) FillUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// calculate total duration from any of the present sets of items
|
||||
var timeSum time.Duration
|
||||
for _, item := range *typeItems[somePresentType] {
|
||||
timeSum += item.Total
|
||||
}
|
||||
timeSum := s.TotalTime()
|
||||
|
||||
// construct dummy item for all missing types
|
||||
for _, t := range missingTypes {
|
||||
@ -99,3 +120,21 @@ func (s *Summary) FillUnknown() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Summary) TotalTime() time.Duration {
|
||||
var timeSum time.Duration
|
||||
|
||||
mappedItems := s.MappedItems()
|
||||
|
||||
// calculate total duration from any of the present sets of items
|
||||
for _, t := range s.Types() {
|
||||
if items := mappedItems[t]; len(*items) > 0 {
|
||||
for _, item := range *items {
|
||||
timeSum += item.Total
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return timeSum
|
||||
}
|
||||
|
71
routes/compat/v1/all_time.go
Normal file
71
routes/compat/v1/all_time.go
Normal file
@ -0,0 +1,71 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/v1"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CompatV1AllHandler struct {
|
||||
summarySrvc *services.SummaryService
|
||||
config *models.Config
|
||||
}
|
||||
|
||||
func NewCompatV1AllHandler(summaryService *services.SummaryService) *CompatV1AllHandler {
|
||||
return &CompatV1AllHandler{
|
||||
summarySrvc: summaryService,
|
||||
config: models.GetConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CompatV1AllHandler) ApiGet(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
requestedUser := vars["user"]
|
||||
authorizedUser := r.Context().Value(models.UserKey).(*models.User)
|
||||
|
||||
if requestedUser != authorizedUser.ID && requestedUser != "current" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
values, _ := url.ParseQuery(r.URL.RawQuery)
|
||||
values.Set("interval", models.IntervalAny)
|
||||
r.URL.RawQuery = values.Encode()
|
||||
|
||||
summary, err, status := h.loadUserSummary(authorizedUser)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
total := summary.TotalTime()
|
||||
vm := &v1.AllTimeVieModel{
|
||||
Seconds: float32(total),
|
||||
Text: utils.FmtWakatimeDuration(total * time.Second),
|
||||
IsUpToDate: true,
|
||||
}
|
||||
|
||||
utils.RespondJSON(w, http.StatusOK, vm)
|
||||
}
|
||||
|
||||
func (h *CompatV1AllHandler) loadUserSummary(user *models.User) (*models.Summary, error, int) {
|
||||
summaryParams := &models.SummaryParams{
|
||||
From: time.Time{},
|
||||
To: time.Now(),
|
||||
User: user,
|
||||
Recompute: false,
|
||||
}
|
||||
|
||||
summary, err := h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute) // 'to' is always constant
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return summary, nil, http.StatusOK
|
||||
}
|
@ -1,22 +1,10 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
IntervalToday string = "today"
|
||||
IntervalLastDay string = "day"
|
||||
IntervalLastWeek string = "week"
|
||||
IntervalLastMonth string = "month"
|
||||
IntervalLastYear string = "year"
|
||||
IntervalAny string = "any"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type SummaryHandler struct {
|
||||
@ -32,7 +20,7 @@ func NewSummaryHandler(summaryService *services.SummaryService) *SummaryHandler
|
||||
}
|
||||
|
||||
func (h *SummaryHandler) ApiGet(w http.ResponseWriter, r *http.Request) {
|
||||
summary, err, status := loadUserSummary(r, h.summarySrvc)
|
||||
summary, err, status := h.loadUserSummary(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(err.Error()))
|
||||
@ -53,7 +41,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
summary, err, status := loadUserSummary(r, h.summarySrvc)
|
||||
summary, err, status := h.loadUserSummary(r)
|
||||
if err != nil {
|
||||
respondAlert(w, err.Error(), "", "summary.tpl.html", status)
|
||||
return
|
||||
@ -74,39 +62,13 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
templates["summary.tpl.html"].Execute(w, vm)
|
||||
}
|
||||
|
||||
func loadUserSummary(r *http.Request, summaryService *services.SummaryService) (*models.Summary, error, int) {
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
params := r.URL.Query()
|
||||
interval := params.Get("interval")
|
||||
from, err := utils.ParseDate(params.Get("from"))
|
||||
func (h *SummaryHandler) loadUserSummary(r *http.Request) (*models.Summary, error, int) {
|
||||
summaryParams, err := utils.ParseSummaryParams(r)
|
||||
if err != nil {
|
||||
switch interval {
|
||||
case IntervalToday:
|
||||
from = utils.StartOfDay()
|
||||
case IntervalLastDay:
|
||||
from = utils.StartOfDay().Add(-24 * time.Hour)
|
||||
case IntervalLastWeek:
|
||||
from = utils.StartOfWeek()
|
||||
case IntervalLastMonth:
|
||||
from = utils.StartOfMonth()
|
||||
case IntervalLastYear:
|
||||
from = utils.StartOfYear()
|
||||
case IntervalAny:
|
||||
from = time.Time{}
|
||||
default:
|
||||
return nil, errors.New("missing 'from' parameter"), http.StatusBadRequest
|
||||
}
|
||||
return nil, err, http.StatusBadRequest
|
||||
}
|
||||
|
||||
live := (params.Get("live") != "" && params.Get("live") != "false") || interval == IntervalToday
|
||||
recompute := params.Get("recompute") != "" && params.Get("recompute") != "false"
|
||||
to := utils.StartOfDay()
|
||||
if live {
|
||||
to = time.Now()
|
||||
}
|
||||
|
||||
var summary *models.Summary
|
||||
summary, err = summaryService.Construct(from, to, user, recompute) // 'to' is always constant
|
||||
summary, err := h.summarySrvc.Construct(summaryParams.From, summaryParams.To, summaryParams.User, summaryParams.Recompute) // 'to' is always constant
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco
|
||||
heartbeats = append(heartbeats, hb...)
|
||||
}
|
||||
|
||||
types := []uint8{models.SummaryProject, models.SummaryLanguage, models.SummaryEditor, models.SummaryOS, models.SummaryMachine}
|
||||
types := models.SummaryTypes()
|
||||
|
||||
var projectItems []*models.SummaryItem
|
||||
var languageItems []*models.SummaryItem
|
||||
|
@ -1,6 +1,9 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func StartOfDay() time.Time {
|
||||
ref := time.Now()
|
||||
@ -23,6 +26,14 @@ func StartOfYear() time.Time {
|
||||
return time.Date(ref.Year(), time.January, 1, 0, 0, 0, 0, ref.Location())
|
||||
}
|
||||
|
||||
func FmtWakatimeDuration(d time.Duration) string {
|
||||
d = d.Round(time.Minute)
|
||||
h := d / time.Hour
|
||||
d -= h * time.Hour
|
||||
m := d / time.Minute
|
||||
return fmt.Sprintf("%d hrs %d mins", h, m)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/18632496
|
||||
func firstDayOfISOWeek(year int, week int, timezone *time.Location) time.Time {
|
||||
date := time.Date(year, 0, 0, 0, 0, 0, 0, timezone)
|
||||
|
52
utils/summary.go
Normal file
52
utils/summary.go
Normal file
@ -0,0 +1,52 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/muety/wakapi/models"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
|
||||
params := r.URL.Query()
|
||||
|
||||
interval := params.Get("interval")
|
||||
|
||||
from, err := ParseDate(params.Get("from"))
|
||||
if err != nil {
|
||||
switch interval {
|
||||
case models.IntervalToday:
|
||||
from = StartOfDay()
|
||||
case models.IntervalLastDay:
|
||||
from = StartOfDay().Add(-24 * time.Hour)
|
||||
case models.IntervalLastWeek:
|
||||
from = StartOfWeek()
|
||||
case models.IntervalLastMonth:
|
||||
from = StartOfMonth()
|
||||
case models.IntervalLastYear:
|
||||
from = StartOfYear()
|
||||
case models.IntervalAny:
|
||||
from = time.Time{}
|
||||
default:
|
||||
return nil, errors.New("missing 'from' parameter")
|
||||
}
|
||||
}
|
||||
|
||||
live := (params.Get("live") != "" && params.Get("live") != "false") || interval == models.IntervalToday
|
||||
|
||||
recompute := params.Get("recompute") != "" && params.Get("recompute") != "false"
|
||||
|
||||
to := StartOfDay()
|
||||
if live {
|
||||
to = time.Now()
|
||||
}
|
||||
|
||||
return &models.SummaryParams{
|
||||
From: from,
|
||||
To: to,
|
||||
User: user,
|
||||
Recompute: recompute,
|
||||
}, nil
|
||||
}
|
@ -1 +1 @@
|
||||
1.8.4
|
||||
1.9.0
|
Loading…
Reference in New Issue
Block a user