diff --git a/main.go b/main.go index 0d5ce1b..effd6b5 100644 --- a/main.go +++ b/main.go @@ -132,6 +132,7 @@ func main() { // Compat Handlers wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService) wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService) + wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(summaryService) shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService) // MVC Handlers @@ -172,6 +173,7 @@ func main() { // Compat route registrations wakatimeV1AllHandler.RegisterRoutes(compatApiRouter) wakatimeV1SummariesHandler.RegisterRoutes(compatApiRouter) + wakatimeV1StatsHandler.RegisterRoutes(compatApiRouter) shieldV1BadgeHandler.RegisterRoutes(compatApiRouter) // Static Routes diff --git a/models/compat/wakatime/v1/heartbeat.go b/models/compat/wakatime/v1/heartbeat.go index 8461db4..b192456 100644 --- a/models/compat/wakatime/v1/heartbeat.go +++ b/models/compat/wakatime/v1/heartbeat.go @@ -10,16 +10,16 @@ type HeartbeatsViewModel struct { // that is actually required for the import type HeartbeatEntry struct { - Id string - Branch string - Category string - Entity string - IsWrite bool `json:"is_write"` - Language string - Project string - Time models.CustomTime - Type string - UserId string `json:"user_id"` - MachineNameId string `json:"machine_name_id"` - UserAgentId string `json:"user_agent_id"` + Id string `json:"id"` + Branch string `json:"branch"` + Category string `json:"category"` + Entity string `json:"entity"` + IsWrite bool `json:"is_write"` + Language string `json:"language"` + Project string `json:"project"` + Time models.CustomTime `json:"time"` + Type string `json:"type"` + UserId string `json:"user_id"` + MachineNameId string `json:"machine_name_id"` + UserAgentId string `json:"user_agent_id"` } diff --git a/models/compat/wakatime/v1/stats.go b/models/compat/wakatime/v1/stats.go new file mode 100644 index 0000000..3690316 --- /dev/null +++ b/models/compat/wakatime/v1/stats.go @@ -0,0 +1,78 @@ +package v1 + +import ( + "github.com/muety/wakapi/models" + "time" +) + +// https://wakatime.com/api/v1/users/current/stats/last_7_days +// https://pastr.de/p/f2fxg6ragj7z5e7fhsow9rb6 + +type StatsViewModel struct { + Data *StatsData `json:"data"` +} + +type StatsData struct { + Username string `json:"username"` + UserId string `json:"user_id"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + TotalSeconds float64 `json:"total_seconds"` + DailyAverage float64 `json:"daily_average"` + DaysIncludingHolidays int `json:"days_including_holidays"` + Editors []*SummariesEntry `json:"editors"` + Languages []*SummariesEntry `json:"languages"` + Machines []*SummariesEntry `json:"machines"` + Projects []*SummariesEntry `json:"projects"` + OperatingSystems []*SummariesEntry `json:"operating_systems"` +} + +func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewModel { + totalTime := summary.TotalTime() + numDays := int(summary.ToTime.T().Sub(summary.FromTime.T()).Hours() / 24) + + data := &StatsData{ + Username: summary.UserID, + UserId: summary.UserID, + Start: summary.FromTime.T(), + End: summary.ToTime.T(), + TotalSeconds: totalTime.Seconds(), + DailyAverage: totalTime.Seconds() / float64(numDays), + DaysIncludingHolidays: numDays, + } + + editors := make([]*SummariesEntry, len(summary.Editors)) + for i, e := range summary.Editors { + editors[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryEditor)) + } + + languages := make([]*SummariesEntry, len(summary.Languages)) + for i, e := range summary.Languages { + languages[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryLanguage)) + } + + machines := make([]*SummariesEntry, len(summary.Machines)) + for i, e := range summary.Machines { + machines[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryMachine)) + } + + projects := make([]*SummariesEntry, len(summary.Projects)) + for i, e := range summary.Projects { + projects[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryProject)) + } + + oss := make([]*SummariesEntry, len(summary.OperatingSystems)) + for i, e := range summary.OperatingSystems { + oss[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryOS)) + } + + data.Editors = editors + data.Languages = languages + data.Machines = machines + data.Projects = projects + data.OperatingSystems = oss + + return &StatsViewModel{ + Data: data, + } +} diff --git a/models/compat/wakatime/v1/user_agent.go b/models/compat/wakatime/v1/user_agent.go index a73a755..af427bd 100644 --- a/models/compat/wakatime/v1/user_agent.go +++ b/models/compat/wakatime/v1/user_agent.go @@ -5,8 +5,8 @@ type UserAgentsViewModel struct { } type UserAgentEntry struct { - Id string - Editor string - Os string - Value string + Id string `json:"id"` + Editor string `json:"editor"` + Os string `json:"os"` + Value string `json:"value"` } diff --git a/models/summary.go b/models/summary.go index 03b7323..2d8a723 100644 --- a/models/summary.go +++ b/models/summary.go @@ -16,13 +16,13 @@ const ( const ( IntervalToday string = "today" - IntervalYesterday string = "day" + IntervalYesterday string = "yesterday" IntervalThisWeek string = "week" IntervalThisMonth string = "month" IntervalThisYear string = "year" - IntervalPast7Days string = "7_days" - IntervalPast30Days string = "30_days" - IntervalPast12Months string = "12_months" + IntervalPast7Days string = "last_7_days" + IntervalPast30Days string = "last_30_days" + IntervalPast12Months string = "last_12_months" IntervalAny string = "any" // https://wakatime.com/developers/#summaries diff --git a/routes/compat/wakatime/v1/stats.go b/routes/compat/wakatime/v1/stats.go new file mode 100644 index 0000000..c8d931f --- /dev/null +++ b/routes/compat/wakatime/v1/stats.go @@ -0,0 +1,83 @@ +package v1 + +import ( + "errors" + "github.com/gorilla/mux" + conf "github.com/muety/wakapi/config" + "github.com/muety/wakapi/models" + v1 "github.com/muety/wakapi/models/compat/wakatime/v1" + "github.com/muety/wakapi/services" + "github.com/muety/wakapi/utils" + "net/http" + "time" +) + +type StatsHandler struct { + config *conf.Config + summarySrvc services.ISummaryService +} + +func NewStatsHandler(summaryService services.ISummaryService) *StatsHandler { + return &StatsHandler{ + summarySrvc: summaryService, + config: conf.Get(), + } +} + +func (h *StatsHandler) RegisterRoutes(router *mux.Router) { + router.Path("/wakatime/v1/users/{user}/stats/{range}").Methods(http.MethodGet).HandlerFunc(h.Get) +} + +// TODO: support filtering (requires https://github.com/muety/wakapi/issues/108) + +func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + requestedUser := vars["user"] + requestedRange := vars["range"] + + user := r.Context().Value(models.UserKey).(*models.User) + + if requestedUser != user.ID && requestedUser != "current" { + w.WriteHeader(http.StatusForbidden) + return + } + + summary, err, status := h.loadUserSummary(user, requestedRange) + if err != nil { + w.WriteHeader(status) + w.Write([]byte(err.Error())) + return + } + + filters := &models.Filters{} + if projectQuery := r.URL.Query().Get("project"); projectQuery != "" { + filters.Project = projectQuery + } + + vm := v1.NewStatsFrom(summary, filters) + utils.RespondJSON(w, http.StatusOK, vm) +} + +func (h *StatsHandler) loadUserSummary(user *models.User, rangeKey string) (*models.Summary, error, int) { + var start, end time.Time + + if err, parsedFrom, parsedTo := utils.ResolveInterval(rangeKey); err == nil { + start, end = parsedFrom, parsedTo + } else { + return nil, errors.New("invalid 'range' parameter"), http.StatusBadRequest + } + + overallParams := &models.SummaryParams{ + From: start, + To: end, + User: user, + Recompute: false, + } + + summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve) + if err != nil { + return nil, err, http.StatusInternalServerError + } + + return summary, nil, http.StatusOK +} diff --git a/views/index.tpl.html b/views/index.tpl.html index ffb1a71..f4073e5 100644 --- a/views/index.tpl.html +++ b/views/index.tpl.html @@ -22,7 +22,7 @@ class="text-green-700">Your Coding Time 🕓

Wakapi is an open-source tool that helps you keep track of the time you have spent coding on different projects in different programming languages and more. Ideal for - statistics freaks any anyone else.

+ statistics freaks and anyone else.

💡 The system has tracked a total of diff --git a/views/summary.tpl.html b/views/summary.tpl.html index b724a3a..313847a 100644 --- a/views/summary.tpl.html +++ b/views/summary.tpl.html @@ -41,13 +41,13 @@

Today - Yesterday + Yesterday This Week This Month This Year - Past 7 Days - Past 30 Days - Past 12 Months + Past 7 Days + Past 30 Days + Past 12 Months All Time