diff --git a/README.md b/README.md
index 996ca95..6869e84 100644
--- a/README.md
+++ b/README.md
@@ -137,8 +137,9 @@ You can specify configuration options either via a config file (default: `config
| YAML key / Env. variable | Default | Description |
|------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `env` /
`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
-| `app.aggregation_time` /
`WAKAPI_AGGREGATION_TIME` | `02:15` | Time of day at which to periodically run summary generation for all users |
-| `app.report_time_weekly` /
`WAKAPI_REPORT_TIME_WEEKLY` | `fri,18:00` | Week day and time at which to send e-mail reports |
+| `app.aggregation_time` /
`WAKAPI_AGGREGATION_TIME` | `0 15 2 * * *` | Time of day at which to periodically run summary generation for all users |
+| `app.report_time_weekly` /
`WAKAPI_REPORT_TIME_WEEKLY` | `0 0 18 * * 5` | Week day and time at which to send e-mail reports |
+| `app.leaderboard_generation_time` /
`WAKAPI_LEADERBOARD_GENERATION_TIME` | `0 0 6 * * *,0 0 18 * * *` | One or multiple times of day at which to re-calculate the leaderboard |
| `app.import_batch_size` /
`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services |
| `app.inactive_days` /
`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) |
| `app.heartbeat_max_age /`
`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) |
diff --git a/config.default.yml b/config.default.yml
index 7a7c9c4..b3b48fe 100644
--- a/config.default.yml
+++ b/config.default.yml
@@ -12,12 +12,12 @@ server:
public_url: http://localhost:3000 # required for links (e.g. password reset) in e-mail
app:
- aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
- leaderboard_generation_time: '06:00;18:00' # time at which to run daily aggregation batch jobs
- report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: ',')
- inactive_days: 7 # time of previous days within a user must have logged in to be considered active
- import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
- heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
+ aggregation_time: '0 15 2 * * *' # time at which to run daily aggregation batch jobs
+ leaderboard_generation_time: '0 0 6 * * *,0 0 18 * * *' # times at which to re-calculate the leaderboard
+ report_time_weekly: '0 0 18 * * 5' # time at which to fan out weekly reports (extended cron)
+ inactive_days: 7 # time of previous days within a user must have logged in to be considered active
+ import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
+ heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
custom_languages:
vue: Vue
jsx: JSX
diff --git a/config/config.go b/config/config.go
index 9754e1d..4d18c64 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,10 +4,13 @@ import (
"encoding/json"
"flag"
"fmt"
+ "github.com/muety/wakapi/utils"
+ "github.com/robfig/cron/v3"
"io/ioutil"
"net/http"
"os"
"regexp"
+ "strconv"
"strings"
"time"
@@ -227,28 +230,96 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
}
func (c *appConfig) GetCustomLanguages() map[string]string {
- return cloneStringMap(c.CustomLanguages, false)
+ return utils.CloneStringMap(c.CustomLanguages, false)
}
func (c *appConfig) GetLanguageColors() map[string]string {
- return cloneStringMap(c.Colors["languages"], true)
+ return utils.CloneStringMap(c.Colors["languages"], true)
}
func (c *appConfig) GetEditorColors() map[string]string {
- return cloneStringMap(c.Colors["editors"], true)
+ return utils.CloneStringMap(c.Colors["editors"], true)
}
func (c *appConfig) GetOSColors() map[string]string {
- return cloneStringMap(c.Colors["operating_systems"], true)
+ return utils.CloneStringMap(c.Colors["operating_systems"], true)
}
-func (c *appConfig) GetWeeklyReportDay() time.Weekday {
- s := strings.Split(c.ReportTimeWeekly, ",")[0]
- return parseWeekday(s)
+func (c *appConfig) GetAggregationTimeCron() string {
+ if strings.Contains(c.AggregationTime, ":") {
+ // old gocron format, e.g. "15:04"
+ timeParts := strings.Split(c.AggregationTime, ":")
+ h, err := strconv.Atoi(timeParts[0])
+ if err != nil {
+ logbuch.Fatal(err.Error())
+ }
+
+ m, err := strconv.Atoi(timeParts[1])
+ if err != nil {
+ logbuch.Fatal(err.Error())
+ }
+
+ return fmt.Sprintf("0 %d %d * * *", m, h)
+ }
+
+ return utils.CronPadToSecondly(c.AggregationTime)
}
-func (c *appConfig) GetWeeklyReportTime() string {
- return strings.Split(c.ReportTimeWeekly, ",")[1]
+func (c *appConfig) GetWeeklyReportCron() string {
+ if strings.Contains(c.ReportTimeWeekly, ",") {
+ // old gocron format, e.g. "fri,18:00"
+ split := strings.Split(c.ReportTimeWeekly, ",")
+ weekday := utils.ParseWeekday(split[0])
+ timeParts := strings.Split(split[1], ":")
+
+ h, err := strconv.Atoi(timeParts[0])
+ if err != nil {
+ logbuch.Fatal(err.Error())
+ }
+
+ m, err := strconv.Atoi(timeParts[1])
+ if err != nil {
+ logbuch.Fatal(err.Error())
+ }
+
+ return fmt.Sprintf("0 %d %d * * %d", m, h, weekday)
+ }
+
+ return utils.CronPadToSecondly(c.ReportTimeWeekly)
+}
+
+func (c *appConfig) GetLeaderboardGenerationTimeCron() []string {
+ crons := []string{}
+
+ var parse func(string) string
+
+ if strings.Contains(c.LeaderboardGenerationTime, ":") {
+ // old gocron format, e.g. "15:04"
+ parse = func(s string) string {
+ timeParts := strings.Split(s, ":")
+ h, err := strconv.Atoi(timeParts[0])
+ if err != nil {
+ logbuch.Fatal(err.Error())
+ }
+
+ m, err := strconv.Atoi(timeParts[1])
+ if err != nil {
+ logbuch.Fatal(err.Error())
+ }
+
+ return fmt.Sprintf("0 %d %d * * *", m, h)
+ }
+ } else {
+ parse = func(s string) string {
+ return utils.CronPadToSecondly(s)
+ }
+ }
+
+ for _, s := range utils.SplitMulti(c.LeaderboardGenerationTime, ",", ";") {
+ crons = append(crons, parse(strings.TrimSpace(s)))
+ }
+
+ return crons
}
func (c *appConfig) HeartbeatsMaxAge() time.Duration {
@@ -323,35 +394,6 @@ func resolveDbDialect(dbType string) string {
return dbType
}
-func findString(needle string, haystack []string, defaultVal string) string {
- for _, s := range haystack {
- if s == needle {
- return s
- }
- }
- return defaultVal
-}
-
-func parseWeekday(s string) time.Weekday {
- switch strings.ToLower(s) {
- case "mon", strings.ToLower(time.Monday.String()):
- return time.Monday
- case "tue", strings.ToLower(time.Tuesday.String()):
- return time.Tuesday
- case "wed", strings.ToLower(time.Wednesday.String()):
- return time.Wednesday
- case "thu", strings.ToLower(time.Thursday.String()):
- return time.Thursday
- case "fri", strings.ToLower(time.Friday.String()):
- return time.Friday
- case "sat", strings.ToLower(time.Saturday.String()):
- return time.Saturday
- case "sun", strings.ToLower(time.Sunday.String()):
- return time.Sunday
- }
- return time.Monday
-}
-
func Set(config *Config) {
cfg = config
}
@@ -411,19 +453,38 @@ func Load(version string) *Config {
logbuch.Warn("with sqlite, only a single connection is supported") // otherwise 'PRAGMA foreign_keys=ON' would somehow have to be set for every connection in the pool
config.Db.MaxConn = 1
}
- if config.Mail.Provider != "" && findString(config.Mail.Provider, emailProviders, "") == "" {
+ if config.Mail.Provider != "" && utils.FindString(config.Mail.Provider, emailProviders, "") == "" {
logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider)
}
- if _, err := time.Parse("15:04", config.App.GetWeeklyReportTime()); err != nil {
- logbuch.Fatal("invalid interval set for report_time_weekly")
- }
- if _, err := time.Parse("15:04", config.App.AggregationTime); err != nil {
- logbuch.Fatal("invalid interval set for aggregation_time")
- }
if _, err := time.ParseDuration(config.App.HeartbeatMaxAge); err != nil {
logbuch.Fatal("invalid duration set for heartbeat_max_age")
}
+ cronParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
+
+ if _, err := cronParser.Parse(config.App.GetWeeklyReportCron()); err != nil {
+ logbuch.Fatal("invalid cron expression for report_time_weekly")
+ }
+ if _, err := cronParser.Parse(config.App.GetAggregationTimeCron()); err != nil {
+ logbuch.Fatal("invalid cron expression for aggregation_time")
+ }
+ for _, c := range config.App.GetLeaderboardGenerationTimeCron() {
+ if _, err := cronParser.Parse(c); err != nil {
+ logbuch.Fatal("invalid cron expression for leaderboard_generation_time")
+ }
+ }
+
+ // deprecation notices
+ if strings.Contains(config.App.AggregationTime, ":") {
+ logbuch.Warn("you're using deprecated syntax for 'aggregation_time', please change it to a valid cron expression")
+ }
+ if strings.Contains(config.App.ReportTimeWeekly, ":") {
+ logbuch.Warn("you're using deprecated syntax for 'report_time_weekly', please change it to a valid cron expression")
+ }
+ if strings.Contains(config.App.LeaderboardGenerationTime, ":") {
+ logbuch.Warn("you're using deprecated syntax for 'leaderboard_generation_time', please change it to a semicolon-separated list if valid cron expressions")
+ }
+
Set(config)
return Get()
}
diff --git a/config/jobqueue.go b/config/jobqueue.go
new file mode 100644
index 0000000..d6c2f94
--- /dev/null
+++ b/config/jobqueue.go
@@ -0,0 +1,73 @@
+package config
+
+import (
+ "fmt"
+ "github.com/emvi/logbuch"
+ "github.com/muety/artifex/v2"
+ "math"
+ "runtime"
+)
+
+var jobQueues map[string]*artifex.Dispatcher
+var jobCounts map[string]int
+
+const (
+ QueueDefault = "wakapi.default"
+ QueueProcessing = "wakapi.processing"
+ QueueReports = "wakapi.reports"
+ QueueImports = "wakapi.imports"
+)
+
+type JobQueueMetrics struct {
+ Queue string
+ EnqueuedJobs int
+ FinishedJobs int
+}
+
+func init() {
+ jobQueues = make(map[string]*artifex.Dispatcher)
+
+ InitQueue(QueueDefault, 1)
+ InitQueue(QueueProcessing, int(math.Ceil(float64(runtime.NumCPU())/2.0)))
+ InitQueue(QueueReports, 1)
+ InitQueue(QueueImports, 1)
+}
+
+func InitQueue(name string, workers int) error {
+ if _, ok := jobQueues[name]; ok {
+ return fmt.Errorf("queue '%s' already existing", name)
+ }
+ logbuch.Info("creating job queue '%s' (%d workers)", name, workers)
+ jobQueues[name] = artifex.NewDispatcher(workers, 4096)
+ jobQueues[name].Start()
+ return nil
+}
+
+func GetDefaultQueue() *artifex.Dispatcher {
+ return GetQueue(QueueDefault)
+}
+
+func GetQueue(name string) *artifex.Dispatcher {
+ if _, ok := jobQueues[name]; !ok {
+ InitQueue(name, 1)
+ }
+ return jobQueues[name]
+}
+
+func GetQueueMetrics() []*JobQueueMetrics {
+ metrics := make([]*JobQueueMetrics, 0, len(jobQueues))
+ for name, queue := range jobQueues {
+ metrics = append(metrics, &JobQueueMetrics{
+ Queue: name,
+ EnqueuedJobs: queue.CountEnqueued(),
+ FinishedJobs: queue.CountDispatched(),
+ })
+ }
+ return metrics
+}
+
+func CloseQueues() {
+ for _, q := range jobQueues {
+ q.Stop()
+ }
+}
diff --git a/config/utils.go b/config/utils.go
deleted file mode 100644
index 942c66d..0000000
--- a/config/utils.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package config
-
-import "strings"
-
-func cloneStringMap(m map[string]string, keysToLower bool) map[string]string {
- m2 := make(map[string]string)
- for k, v := range m {
- if keysToLower {
- k = strings.ToLower(k)
- }
- m2[k] = v
- }
- return m2
-}
diff --git a/coverage/coverage.out b/coverage/coverage.out
index e8313cd..640cf7d 100644
--- a/coverage/coverage.out
+++ b/coverage/coverage.out
@@ -1,64 +1,78 @@
mode: set
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:25.112,32.2 1 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:34.59,38.2 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:50.68,52.16 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:57.2,58.16 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:64.2,65.50 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:70.2,78.16 3 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:84.2,86.44 3 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:52.16,55.3 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:58.16,62.3 3 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:65.50,68.3 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:78.16,82.3 3 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:89.144,91.16 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:95.2,102.29 3 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:106.2,114.16 2 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:118.2,118.36 1 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:91.16,93.3 1 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:102.29,104.3 1 0
-github.com/muety/wakapi/routes/compat/shields/v1/badge.go:114.16,116.3 1 0
-github.com/muety/wakapi/utils/auth.go:16.79,18.54 2 0
-github.com/muety/wakapi/utils/auth.go:22.2,24.16 3 0
-github.com/muety/wakapi/utils/auth.go:28.2,30.45 3 0
-github.com/muety/wakapi/utils/auth.go:33.2,34.32 2 0
-github.com/muety/wakapi/utils/auth.go:18.54,20.3 1 0
-github.com/muety/wakapi/utils/auth.go:24.16,26.3 1 0
-github.com/muety/wakapi/utils/auth.go:30.45,32.3 1 0
-github.com/muety/wakapi/utils/auth.go:37.65,39.85 2 0
-github.com/muety/wakapi/utils/auth.go:43.2,44.30 2 0
-github.com/muety/wakapi/utils/auth.go:39.85,41.3 1 0
-github.com/muety/wakapi/utils/auth.go:47.94,49.16 2 0
-github.com/muety/wakapi/utils/auth.go:53.2,53.107 1 0
-github.com/muety/wakapi/utils/auth.go:57.2,57.22 1 0
-github.com/muety/wakapi/utils/auth.go:49.16,51.3 1 0
-github.com/muety/wakapi/utils/auth.go:53.107,55.3 1 0
-github.com/muety/wakapi/utils/auth.go:60.56,64.2 3 0
-github.com/muety/wakapi/utils/auth.go:66.55,69.16 3 0
-github.com/muety/wakapi/utils/auth.go:72.2,72.16 1 0
-github.com/muety/wakapi/utils/auth.go:69.16,71.3 1 0
+github.com/muety/wakapi/utils/collection.go:5.52,6.26 1 0
+github.com/muety/wakapi/utils/collection.go:9.2,9.28 1 0
+github.com/muety/wakapi/utils/collection.go:6.26,8.3 1 0
+github.com/muety/wakapi/utils/collection.go:12.78,14.22 2 0
+github.com/muety/wakapi/utils/collection.go:20.2,20.11 1 0
+github.com/muety/wakapi/utils/collection.go:14.22,15.18 1 0
+github.com/muety/wakapi/utils/collection.go:18.3,18.12 1 0
+github.com/muety/wakapi/utils/collection.go:15.18,17.4 1 0
github.com/muety/wakapi/utils/color.go:8.90,10.32 2 0
github.com/muety/wakapi/utils/color.go:15.2,15.15 1 0
github.com/muety/wakapi/utils/color.go:10.32,11.50 1 0
github.com/muety/wakapi/utils/color.go:11.50,13.4 1 0
-github.com/muety/wakapi/utils/http.go:22.13,24.2 1 1
-github.com/muety/wakapi/utils/http.go:26.90,29.58 3 0
-github.com/muety/wakapi/utils/http.go:29.58,31.3 1 0
-github.com/muety/wakapi/utils/http.go:34.62,36.48 2 0
-github.com/muety/wakapi/utils/http.go:39.2,39.93 1 0
-github.com/muety/wakapi/utils/http.go:44.2,44.14 1 0
-github.com/muety/wakapi/utils/http.go:36.48,38.3 1 0
-github.com/muety/wakapi/utils/http.go:39.93,40.89 1 0
-github.com/muety/wakapi/utils/http.go:40.89,42.4 1 0
-github.com/muety/wakapi/utils/http.go:47.58,51.46 4 0
-github.com/muety/wakapi/utils/http.go:54.2,54.73 1 0
-github.com/muety/wakapi/utils/http.go:57.2,57.19 1 0
-github.com/muety/wakapi/utils/http.go:51.46,53.3 1 0
-github.com/muety/wakapi/utils/http.go:54.73,56.3 1 0
-github.com/muety/wakapi/utils/http.go:60.85,62.26 2 0
-github.com/muety/wakapi/utils/http.go:65.2,65.30 1 0
-github.com/muety/wakapi/utils/http.go:68.2,68.19 1 0
-github.com/muety/wakapi/utils/http.go:62.26,64.3 1 0
-github.com/muety/wakapi/utils/http.go:65.30,67.3 1 0
+github.com/muety/wakapi/utils/cron.go:5.44,7.21 2 0
+github.com/muety/wakapi/utils/cron.go:10.2,10.20 1 0
+github.com/muety/wakapi/utils/cron.go:7.21,9.3 1 0
+github.com/muety/wakapi/utils/date.go:9.48,11.2 1 0
+github.com/muety/wakapi/utils/date.go:13.51,15.2 1 0
+github.com/muety/wakapi/utils/date.go:17.52,19.2 1 0
+github.com/muety/wakapi/utils/date.go:21.51,23.2 1 0
+github.com/muety/wakapi/utils/date.go:26.41,28.21 2 0
+github.com/muety/wakapi/utils/date.go:31.2,31.33 1 0
+github.com/muety/wakapi/utils/date.go:28.21,30.3 1 0
+github.com/muety/wakapi/utils/date.go:35.67,38.33 2 1
+github.com/muety/wakapi/utils/date.go:47.2,47.18 1 1
+github.com/muety/wakapi/utils/date.go:38.33,40.19 2 1
+github.com/muety/wakapi/utils/date.go:43.3,44.10 2 1
+github.com/muety/wakapi/utils/date.go:40.19,42.4 1 1
+github.com/muety/wakapi/utils/date.go:51.36,54.2 2 0
+github.com/muety/wakapi/utils/date.go:56.42,57.28 1 0
+github.com/muety/wakapi/utils/date.go:73.2,73.20 1 0
+github.com/muety/wakapi/utils/date.go:58.52,59.21 1 0
+github.com/muety/wakapi/utils/date.go:60.53,61.22 1 0
+github.com/muety/wakapi/utils/date.go:62.55,63.24 1 0
+github.com/muety/wakapi/utils/date.go:64.54,65.23 1 0
+github.com/muety/wakapi/utils/date.go:66.52,67.21 1 0
+github.com/muety/wakapi/utils/date.go:68.54,69.23 1 0
+github.com/muety/wakapi/utils/date.go:70.52,71.21 1 0
+github.com/muety/wakapi/utils/db.go:10.34,11.37 1 0
+github.com/muety/wakapi/utils/db.go:19.2,20.14 2 0
+github.com/muety/wakapi/utils/db.go:11.37,13.110 2 0
+github.com/muety/wakapi/utils/db.go:17.3,17.20 1 0
+github.com/muety/wakapi/utils/db.go:13.110,16.4 2 0
+github.com/muety/wakapi/utils/db.go:23.39,24.37 1 0
+github.com/muety/wakapi/utils/db.go:32.2,33.14 2 0
+github.com/muety/wakapi/utils/db.go:24.37,26.119 2 0
+github.com/muety/wakapi/utils/db.go:30.3,30.20 1 0
+github.com/muety/wakapi/utils/db.go:26.119,29.4 2 0
+github.com/muety/wakapi/utils/db.go:36.66,37.48 1 0
+github.com/muety/wakapi/utils/db.go:40.2,40.53 1 0
+github.com/muety/wakapi/utils/db.go:37.48,39.3 1 0
+github.com/muety/wakapi/utils/db.go:43.59,44.16 1 0
+github.com/muety/wakapi/utils/db.go:47.2,47.15 1 0
+github.com/muety/wakapi/utils/db.go:50.2,50.14 1 0
+github.com/muety/wakapi/utils/db.go:44.16,46.3 1 0
+github.com/muety/wakapi/utils/db.go:47.15,49.3 1 0
+github.com/muety/wakapi/utils/sync.go:11.66,13.12 2 0
+github.com/muety/wakapi/utils/sync.go:17.2,17.9 1 0
+github.com/muety/wakapi/utils/sync.go:13.12,16.3 2 0
+github.com/muety/wakapi/utils/sync.go:18.11,19.15 1 0
+github.com/muety/wakapi/utils/sync.go:20.29,21.14 1 0
+github.com/muety/wakapi/utils/auth.go:14.79,16.54 2 0
+github.com/muety/wakapi/utils/auth.go:20.2,22.16 3 0
+github.com/muety/wakapi/utils/auth.go:26.2,28.45 3 0
+github.com/muety/wakapi/utils/auth.go:31.2,32.32 2 0
+github.com/muety/wakapi/utils/auth.go:16.54,18.3 1 0
+github.com/muety/wakapi/utils/auth.go:22.16,24.3 1 0
+github.com/muety/wakapi/utils/auth.go:28.45,30.3 1 0
+github.com/muety/wakapi/utils/auth.go:35.65,37.85 2 0
+github.com/muety/wakapi/utils/auth.go:41.2,42.30 2 0
+github.com/muety/wakapi/utils/auth.go:37.85,39.3 1 0
+github.com/muety/wakapi/utils/auth.go:45.56,49.2 3 0
+github.com/muety/wakapi/utils/auth.go:51.55,54.16 3 0
+github.com/muety/wakapi/utils/auth.go:57.2,57.16 1 0
+github.com/muety/wakapi/utils/auth.go:54.16,56.3 1 0
github.com/muety/wakapi/utils/json.go:12.75,18.62 2 0
github.com/muety/wakapi/utils/json.go:22.2,22.29 1 0
github.com/muety/wakapi/utils/json.go:26.2,26.72 1 0
@@ -68,58 +82,16 @@ github.com/muety/wakapi/utils/json.go:18.62,20.3 1 0
github.com/muety/wakapi/utils/json.go:22.29,24.3 1 0
github.com/muety/wakapi/utils/json.go:26.72,28.3 1 0
github.com/muety/wakapi/utils/json.go:29.70,31.3 1 0
-github.com/muety/wakapi/utils/summary.go:10.66,11.40 1 0
-github.com/muety/wakapi/utils/summary.go:16.2,16.48 1 0
-github.com/muety/wakapi/utils/summary.go:11.40,12.27 1 0
-github.com/muety/wakapi/utils/summary.go:12.27,14.4 1 0
-github.com/muety/wakapi/utils/summary.go:19.88,22.2 2 0
-github.com/muety/wakapi/utils/summary.go:24.95,26.16 2 0
-github.com/muety/wakapi/utils/summary.go:29.2,29.38 1 0
-github.com/muety/wakapi/utils/summary.go:26.16,28.3 1 0
-github.com/muety/wakapi/utils/summary.go:32.105,36.18 3 0
-github.com/muety/wakapi/utils/summary.go:73.2,73.22 1 0
-github.com/muety/wakapi/utils/summary.go:37.28,38.26 1 0
-github.com/muety/wakapi/utils/summary.go:39.32,41.24 2 0
-github.com/muety/wakapi/utils/summary.go:42.31,43.29 1 0
-github.com/muety/wakapi/utils/summary.go:44.31,46.27 2 0
-github.com/muety/wakapi/utils/summary.go:47.32,48.30 1 0
-github.com/muety/wakapi/utils/summary.go:49.32,51.28 2 0
-github.com/muety/wakapi/utils/summary.go:52.31,53.29 1 0
-github.com/muety/wakapi/utils/summary.go:54.32,55.31 1 0
-github.com/muety/wakapi/utils/summary.go:56.41,58.42 2 0
-github.com/muety/wakapi/utils/summary.go:59.33,60.32 1 0
-github.com/muety/wakapi/utils/summary.go:61.33,62.32 1 0
-github.com/muety/wakapi/utils/summary.go:63.34,64.31 1 0
-github.com/muety/wakapi/utils/summary.go:65.35,66.32 1 0
-github.com/muety/wakapi/utils/summary.go:67.26,68.21 1 0
-github.com/muety/wakapi/utils/summary.go:69.10,70.39 1 0
-github.com/muety/wakapi/utils/summary.go:76.73,83.56 5 0
-github.com/muety/wakapi/utils/summary.go:99.2,109.8 3 0
-github.com/muety/wakapi/utils/summary.go:83.56,85.3 1 0
-github.com/muety/wakapi/utils/summary.go:85.8,85.54 1 0
-github.com/muety/wakapi/utils/summary.go:85.54,87.3 1 0
-github.com/muety/wakapi/utils/summary.go:87.8,89.17 2 0
-github.com/muety/wakapi/utils/summary.go:93.3,94.17 2 0
-github.com/muety/wakapi/utils/summary.go:89.17,91.4 1 0
-github.com/muety/wakapi/utils/summary.go:94.17,96.4 1 0
-github.com/muety/wakapi/utils/summary.go:112.59,114.48 2 0
-github.com/muety/wakapi/utils/summary.go:117.2,117.49 1 0
-github.com/muety/wakapi/utils/summary.go:120.2,120.47 1 0
-github.com/muety/wakapi/utils/summary.go:123.2,123.48 1 0
-github.com/muety/wakapi/utils/summary.go:126.2,126.57 1 0
-github.com/muety/wakapi/utils/summary.go:129.2,129.46 1 0
-github.com/muety/wakapi/utils/summary.go:132.2,132.47 1 0
-github.com/muety/wakapi/utils/summary.go:135.2,135.16 1 0
-github.com/muety/wakapi/utils/summary.go:114.48,116.3 1 0
-github.com/muety/wakapi/utils/summary.go:117.49,119.3 1 0
-github.com/muety/wakapi/utils/summary.go:120.47,122.3 1 0
-github.com/muety/wakapi/utils/summary.go:123.48,125.3 1 0
-github.com/muety/wakapi/utils/summary.go:126.57,128.3 1 0
-github.com/muety/wakapi/utils/summary.go:129.46,131.3 1 0
-github.com/muety/wakapi/utils/summary.go:132.47,134.3 1 0
-github.com/muety/wakapi/utils/summary.go:138.48,142.51 2 0
-github.com/muety/wakapi/utils/summary.go:145.2,145.12 1 0
-github.com/muety/wakapi/utils/summary.go:142.51,144.3 1 0
+github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
+github.com/muety/wakapi/utils/strings.go:12.58,13.49 1 0
+github.com/muety/wakapi/utils/strings.go:13.49,14.32 1 0
+github.com/muety/wakapi/utils/strings.go:19.3,19.15 1 0
+github.com/muety/wakapi/utils/strings.go:14.32,15.22 1 0
+github.com/muety/wakapi/utils/strings.go:15.22,17.5 1 0
+github.com/muety/wakapi/utils/strings.go:23.77,24.29 1 0
+github.com/muety/wakapi/utils/strings.go:29.2,29.19 1 0
+github.com/muety/wakapi/utils/strings.go:24.29,25.18 1 0
+github.com/muety/wakapi/utils/strings.go:25.18,27.4 1 0
github.com/muety/wakapi/utils/template.go:13.41,15.16 2 0
github.com/muety/wakapi/utils/template.go:18.2,18.23 1 0
github.com/muety/wakapi/utils/template.go:15.16,17.3 1 0
@@ -139,55 +111,107 @@ github.com/muety/wakapi/utils/template.go:39.51,40.12 1 0
github.com/muety/wakapi/utils/template.go:44.17,46.4 1 0
github.com/muety/wakapi/utils/template.go:48.17,50.4 1 0
github.com/muety/wakapi/utils/template.go:55.17,57.4 1 0
-github.com/muety/wakapi/utils/common.go:18.73,19.58 1 0
-github.com/muety/wakapi/utils/common.go:22.2,22.87 1 0
-github.com/muety/wakapi/utils/common.go:25.2,25.64 1 0
-github.com/muety/wakapi/utils/common.go:19.58,21.3 1 0
-github.com/muety/wakapi/utils/common.go:22.87,24.3 1 0
-github.com/muety/wakapi/utils/common.go:28.40,30.2 1 0
-github.com/muety/wakapi/utils/common.go:32.44,34.2 1 0
-github.com/muety/wakapi/utils/common.go:36.49,38.2 1 0
-github.com/muety/wakapi/utils/common.go:40.45,42.2 1 0
-github.com/muety/wakapi/utils/common.go:44.24,46.2 1 0
-github.com/muety/wakapi/utils/common.go:48.56,51.45 3 1
-github.com/muety/wakapi/utils/common.go:54.2,54.40 1 1
-github.com/muety/wakapi/utils/common.go:51.45,53.3 1 1
-github.com/muety/wakapi/utils/common.go:57.52,58.26 1 0
-github.com/muety/wakapi/utils/common.go:61.2,61.28 1 0
-github.com/muety/wakapi/utils/common.go:58.26,60.3 1 0
-github.com/muety/wakapi/utils/date.go:9.48,11.2 1 0
-github.com/muety/wakapi/utils/date.go:13.51,15.2 1 0
-github.com/muety/wakapi/utils/date.go:17.52,19.2 1 0
-github.com/muety/wakapi/utils/date.go:21.51,23.2 1 0
-github.com/muety/wakapi/utils/date.go:26.41,28.21 2 0
-github.com/muety/wakapi/utils/date.go:31.2,31.33 1 0
-github.com/muety/wakapi/utils/date.go:28.21,30.3 1 0
-github.com/muety/wakapi/utils/date.go:35.67,38.33 2 1
-github.com/muety/wakapi/utils/date.go:47.2,47.18 1 1
-github.com/muety/wakapi/utils/date.go:38.33,40.19 2 1
-github.com/muety/wakapi/utils/date.go:43.3,44.10 2 1
-github.com/muety/wakapi/utils/date.go:40.19,42.4 1 1
-github.com/muety/wakapi/utils/date.go:50.50,56.2 5 0
-github.com/muety/wakapi/utils/date.go:59.36,62.2 2 0
-github.com/muety/wakapi/utils/db.go:10.34,11.37 1 0
-github.com/muety/wakapi/utils/db.go:19.2,20.14 2 0
-github.com/muety/wakapi/utils/db.go:11.37,13.110 2 0
-github.com/muety/wakapi/utils/db.go:17.3,17.20 1 0
-github.com/muety/wakapi/utils/db.go:13.110,16.4 2 0
-github.com/muety/wakapi/utils/db.go:23.39,24.37 1 0
-github.com/muety/wakapi/utils/db.go:32.2,33.14 2 0
-github.com/muety/wakapi/utils/db.go:24.37,26.119 2 0
-github.com/muety/wakapi/utils/db.go:30.3,30.20 1 0
-github.com/muety/wakapi/utils/db.go:26.119,29.4 2 0
-github.com/muety/wakapi/utils/db.go:36.66,37.48 1 0
-github.com/muety/wakapi/utils/db.go:40.2,40.53 1 0
-github.com/muety/wakapi/utils/db.go:37.48,39.3 1 0
-github.com/muety/wakapi/utils/db.go:43.59,44.16 1 0
-github.com/muety/wakapi/utils/db.go:47.2,47.15 1 0
-github.com/muety/wakapi/utils/db.go:50.2,50.14 1 0
-github.com/muety/wakapi/utils/db.go:44.16,46.3 1 0
-github.com/muety/wakapi/utils/db.go:47.15,49.3 1 0
-github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
+github.com/muety/wakapi/utils/http.go:21.13,23.2 1 1
+github.com/muety/wakapi/utils/http.go:25.62,27.48 2 0
+github.com/muety/wakapi/utils/http.go:30.2,30.93 1 0
+github.com/muety/wakapi/utils/http.go:35.2,35.14 1 0
+github.com/muety/wakapi/utils/http.go:27.48,29.3 1 0
+github.com/muety/wakapi/utils/http.go:30.93,31.89 1 0
+github.com/muety/wakapi/utils/http.go:31.89,33.4 1 0
+github.com/muety/wakapi/utils/http.go:38.58,42.46 4 0
+github.com/muety/wakapi/utils/http.go:45.2,45.73 1 0
+github.com/muety/wakapi/utils/http.go:48.2,48.19 1 0
+github.com/muety/wakapi/utils/http.go:42.46,44.3 1 0
+github.com/muety/wakapi/utils/http.go:45.73,47.3 1 0
+github.com/muety/wakapi/utils/http.go:51.85,53.26 2 0
+github.com/muety/wakapi/utils/http.go:56.2,56.30 1 0
+github.com/muety/wakapi/utils/http.go:59.2,59.19 1 0
+github.com/muety/wakapi/utils/http.go:53.26,55.3 1 0
+github.com/muety/wakapi/utils/http.go:56.30,58.3 1 0
+github.com/muety/wakapi/utils/http.go:62.56,65.45 3 1
+github.com/muety/wakapi/utils/http.go:68.2,68.40 1 1
+github.com/muety/wakapi/utils/http.go:65.45,67.3 1 1
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:25.112,32.2 1 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:34.59,38.2 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:50.68,52.16 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:57.2,58.16 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:64.2,65.50 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:70.2,78.16 3 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:84.2,86.46 3 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:52.16,55.3 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:58.16,62.3 3 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:65.50,68.3 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:78.16,82.3 3 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:89.144,91.16 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:95.2,102.29 3 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:106.2,114.16 2 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:118.2,118.36 1 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:91.16,93.3 1 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:102.29,104.3 1 0
+github.com/muety/wakapi/routes/compat/shields/v1/badge.go:114.16,116.3 1 0
+github.com/muety/wakapi/middlewares/principal.go:15.62,17.2 1 0
+github.com/muety/wakapi/middlewares/principal.go:19.58,21.2 1 0
+github.com/muety/wakapi/middlewares/principal.go:42.71,43.43 1 0
+github.com/muety/wakapi/middlewares/principal.go:43.43,45.3 1 0
+github.com/muety/wakapi/middlewares/principal.go:48.81,51.2 2 0
+github.com/muety/wakapi/middlewares/principal.go:53.55,54.52 1 0
+github.com/muety/wakapi/middlewares/principal.go:54.52,56.3 1 0
+github.com/muety/wakapi/middlewares/principal.go:59.49,60.52 1 0
+github.com/muety/wakapi/middlewares/principal.go:63.2,63.12 1 0
+github.com/muety/wakapi/middlewares/principal.go:60.52,62.3 1 0
+github.com/muety/wakapi/middlewares/security.go:19.62,20.43 1 0
+github.com/muety/wakapi/middlewares/security.go:20.43,22.3 1 0
+github.com/muety/wakapi/middlewares/security.go:25.80,26.36 1 0
+github.com/muety/wakapi/middlewares/security.go:31.2,31.27 1 0
+github.com/muety/wakapi/middlewares/security.go:26.36,27.30 1 0
+github.com/muety/wakapi/middlewares/security.go:27.30,29.4 1 0
+github.com/muety/wakapi/middlewares/sentry.go:15.60,16.43 1 0
+github.com/muety/wakapi/middlewares/sentry.go:16.43,20.3 1 0
+github.com/muety/wakapi/middlewares/sentry.go:23.78,26.54 3 0
+github.com/muety/wakapi/middlewares/sentry.go:26.54,27.43 1 0
+github.com/muety/wakapi/middlewares/sentry.go:27.43,29.4 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:31.91,37.2 1 1
+github.com/muety/wakapi/middlewares/authenticate.go:39.90,42.2 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:44.90,47.2 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:49.71,50.71 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:50.71,52.3 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:55.107,59.16 3 0
+github.com/muety/wakapi/middlewares/authenticate.go:62.2,62.16 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:66.2,66.31 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:82.2,83.12 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:59.16,61.3 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:62.16,64.3 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:66.31,67.31 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:72.3,72.29 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:79.3,79.9 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:67.31,70.4 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:72.29,75.4 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:75.9,78.4 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:86.70,87.39 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:92.2,92.14 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:87.39,88.60 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:88.60,90.4 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:95.98,97.16 2 1
+github.com/muety/wakapi/middlewares/authenticate.go:101.2,104.16 4 1
+github.com/muety/wakapi/middlewares/authenticate.go:107.2,107.18 1 1
+github.com/muety/wakapi/middlewares/authenticate.go:97.16,99.3 1 1
+github.com/muety/wakapi/middlewares/authenticate.go:104.16,106.3 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:110.97,114.19 4 1
+github.com/muety/wakapi/middlewares/authenticate.go:117.2,118.16 2 1
+github.com/muety/wakapi/middlewares/authenticate.go:121.2,121.18 1 1
+github.com/muety/wakapi/middlewares/authenticate.go:114.19,116.3 1 1
+github.com/muety/wakapi/middlewares/authenticate.go:118.16,120.3 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:124.92,126.16 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:130.2,131.16 2 0
+github.com/muety/wakapi/middlewares/authenticate.go:138.2,138.18 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:126.16,128.3 1 0
+github.com/muety/wakapi/middlewares/authenticate.go:131.16,133.3 1 0
+github.com/muety/wakapi/middlewares/filetype.go:13.83,14.43 1 0
+github.com/muety/wakapi/middlewares/filetype.go:14.43,19.3 1 0
+github.com/muety/wakapi/middlewares/filetype.go:22.84,24.34 2 0
+github.com/muety/wakapi/middlewares/filetype.go:31.2,31.27 1 0
+github.com/muety/wakapi/middlewares/filetype.go:24.34,25.50 1 0
+github.com/muety/wakapi/middlewares/filetype.go:25.50,29.4 3 0
github.com/muety/wakapi/middlewares/logging.go:20.102,21.43 1 0
github.com/muety/wakapi/middlewares/logging.go:21.43,27.3 1 0
github.com/muety/wakapi/middlewares/logging.go:30.80,39.44 7 0
@@ -215,72 +239,153 @@ github.com/muety/wakapi/middlewares/logging.go:142.36,144.2 1 0
github.com/muety/wakapi/middlewares/logging.go:145.42,147.2 1 0
github.com/muety/wakapi/middlewares/logging.go:148.40,150.2 1 0
github.com/muety/wakapi/middlewares/logging.go:151.52,153.2 1 0
-github.com/muety/wakapi/middlewares/principal.go:15.62,17.2 1 0
-github.com/muety/wakapi/middlewares/principal.go:19.58,21.2 1 0
-github.com/muety/wakapi/middlewares/principal.go:42.71,43.43 1 0
-github.com/muety/wakapi/middlewares/principal.go:43.43,45.3 1 0
-github.com/muety/wakapi/middlewares/principal.go:48.81,51.2 2 0
-github.com/muety/wakapi/middlewares/principal.go:53.55,54.52 1 0
-github.com/muety/wakapi/middlewares/principal.go:54.52,56.3 1 0
-github.com/muety/wakapi/middlewares/principal.go:59.49,60.52 1 0
-github.com/muety/wakapi/middlewares/principal.go:63.2,63.12 1 0
-github.com/muety/wakapi/middlewares/principal.go:60.52,62.3 1 0
-github.com/muety/wakapi/middlewares/security.go:19.62,20.43 1 0
-github.com/muety/wakapi/middlewares/security.go:20.43,22.3 1 0
-github.com/muety/wakapi/middlewares/security.go:25.80,26.36 1 0
-github.com/muety/wakapi/middlewares/security.go:31.2,31.27 1 0
-github.com/muety/wakapi/middlewares/security.go:26.36,27.30 1 0
-github.com/muety/wakapi/middlewares/security.go:27.30,29.4 1 0
-github.com/muety/wakapi/middlewares/sentry.go:15.60,16.43 1 0
-github.com/muety/wakapi/middlewares/sentry.go:16.43,20.3 1 0
-github.com/muety/wakapi/middlewares/sentry.go:23.78,26.54 3 0
-github.com/muety/wakapi/middlewares/sentry.go:26.54,27.43 1 0
-github.com/muety/wakapi/middlewares/sentry.go:27.43,29.4 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:30.91,36.2 1 1
-github.com/muety/wakapi/middlewares/authenticate.go:38.90,41.2 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:43.90,46.2 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:48.71,49.71 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:49.71,51.3 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:54.107,58.16 3 0
-github.com/muety/wakapi/middlewares/authenticate.go:61.2,61.16 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:65.2,65.31 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:81.2,82.12 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:58.16,60.3 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:61.16,63.3 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:65.31,66.31 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:71.3,71.29 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:78.3,78.9 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:66.31,69.4 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:71.29,74.4 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:74.9,77.4 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:85.70,86.39 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:91.2,91.14 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:86.39,87.60 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:87.60,89.4 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:94.98,96.16 2 1
-github.com/muety/wakapi/middlewares/authenticate.go:100.2,103.16 4 1
-github.com/muety/wakapi/middlewares/authenticate.go:106.2,106.18 1 1
-github.com/muety/wakapi/middlewares/authenticate.go:96.16,98.3 1 1
-github.com/muety/wakapi/middlewares/authenticate.go:103.16,105.3 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:109.97,113.19 4 1
-github.com/muety/wakapi/middlewares/authenticate.go:116.2,117.16 2 1
-github.com/muety/wakapi/middlewares/authenticate.go:120.2,120.18 1 1
-github.com/muety/wakapi/middlewares/authenticate.go:113.19,115.3 1 1
-github.com/muety/wakapi/middlewares/authenticate.go:117.16,119.3 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:123.92,125.16 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:129.2,130.16 2 0
-github.com/muety/wakapi/middlewares/authenticate.go:137.2,137.18 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:125.16,127.3 1 0
-github.com/muety/wakapi/middlewares/authenticate.go:130.16,132.3 1 0
-github.com/muety/wakapi/middlewares/filetype.go:13.83,14.43 1 0
-github.com/muety/wakapi/middlewares/filetype.go:14.43,19.3 1 0
-github.com/muety/wakapi/middlewares/filetype.go:22.84,24.34 2 0
-github.com/muety/wakapi/middlewares/filetype.go:31.2,31.27 1 0
-github.com/muety/wakapi/middlewares/filetype.go:24.34,25.50 1 0
-github.com/muety/wakapi/middlewares/filetype.go:25.50,29.4 3 0
+github.com/muety/wakapi/config/config.go:165.64,167.2 1 0
+github.com/muety/wakapi/config/config.go:169.59,171.2 1 0
+github.com/muety/wakapi/config/config.go:173.82,183.2 1 0
+github.com/muety/wakapi/config/config.go:185.31,187.2 1 0
+github.com/muety/wakapi/config/config.go:189.32,191.2 1 0
+github.com/muety/wakapi/config/config.go:193.74,194.19 1 0
+github.com/muety/wakapi/config/config.go:195.10,196.34 1 0
+github.com/muety/wakapi/config/config.go:196.34,197.90 1 0
+github.com/muety/wakapi/config/config.go:200.4,200.100 1 0
+github.com/muety/wakapi/config/config.go:203.4,203.91 1 0
+github.com/muety/wakapi/config/config.go:206.4,206.95 1 0
+github.com/muety/wakapi/config/config.go:209.4,209.93 1 0
+github.com/muety/wakapi/config/config.go:212.4,212.97 1 0
+github.com/muety/wakapi/config/config.go:215.4,215.101 1 0
+github.com/muety/wakapi/config/config.go:218.4,218.98 1 0
+github.com/muety/wakapi/config/config.go:221.4,221.97 1 0
+github.com/muety/wakapi/config/config.go:224.4,224.101 1 0
+github.com/muety/wakapi/config/config.go:227.4,227.14 1 0
+github.com/muety/wakapi/config/config.go:197.90,199.5 1 0
+github.com/muety/wakapi/config/config.go:200.100,202.5 1 0
+github.com/muety/wakapi/config/config.go:203.91,205.5 1 0
+github.com/muety/wakapi/config/config.go:206.95,208.5 1 0
+github.com/muety/wakapi/config/config.go:209.93,211.5 1 0
+github.com/muety/wakapi/config/config.go:212.97,214.5 1 0
+github.com/muety/wakapi/config/config.go:215.101,217.5 1 0
+github.com/muety/wakapi/config/config.go:218.98,220.5 1 0
+github.com/muety/wakapi/config/config.go:221.97,223.5 1 0
+github.com/muety/wakapi/config/config.go:224.101,226.5 1 0
+github.com/muety/wakapi/config/config.go:232.60,234.2 1 0
+github.com/muety/wakapi/config/config.go:236.59,238.2 1 0
+github.com/muety/wakapi/config/config.go:240.57,242.2 1 0
+github.com/muety/wakapi/config/config.go:244.53,246.2 1 0
+github.com/muety/wakapi/config/config.go:248.53,249.46 1 0
+github.com/muety/wakapi/config/config.go:265.2,265.51 1 0
+github.com/muety/wakapi/config/config.go:249.46,253.17 3 0
+github.com/muety/wakapi/config/config.go:257.3,258.17 2 0
+github.com/muety/wakapi/config/config.go:262.3,262.44 1 0
+github.com/muety/wakapi/config/config.go:253.17,255.4 1 0
+github.com/muety/wakapi/config/config.go:258.17,260.4 1 0
+github.com/muety/wakapi/config/config.go:268.50,269.47 1 0
+github.com/muety/wakapi/config/config.go:288.2,288.52 1 0
+github.com/muety/wakapi/config/config.go:269.47,276.17 5 0
+github.com/muety/wakapi/config/config.go:280.3,281.17 2 0
+github.com/muety/wakapi/config/config.go:285.3,285.54 1 0
+github.com/muety/wakapi/config/config.go:276.17,278.4 1 0
+github.com/muety/wakapi/config/config.go:281.17,283.4 1 0
+github.com/muety/wakapi/config/config.go:291.65,296.56 3 0
+github.com/muety/wakapi/config/config.go:318.2,318.76 1 0
+github.com/muety/wakapi/config/config.go:322.2,322.14 1 0
+github.com/muety/wakapi/config/config.go:296.56,298.33 1 0
+github.com/muety/wakapi/config/config.go:298.33,301.18 3 0
+github.com/muety/wakapi/config/config.go:305.4,306.18 2 0
+github.com/muety/wakapi/config/config.go:310.4,310.45 1 0
+github.com/muety/wakapi/config/config.go:301.18,303.5 1 0
+github.com/muety/wakapi/config/config.go:306.18,308.5 1 0
+github.com/muety/wakapi/config/config.go:312.8,313.33 1 0
+github.com/muety/wakapi/config/config.go:313.33,315.4 1 0
+github.com/muety/wakapi/config/config.go:318.76,320.3 1 0
+github.com/muety/wakapi/config/config.go:325.54,328.2 2 0
+github.com/muety/wakapi/config/config.go:330.36,332.2 1 0
+github.com/muety/wakapi/config/config.go:334.35,336.2 1 0
+github.com/muety/wakapi/config/config.go:338.38,340.2 1 0
+github.com/muety/wakapi/config/config.go:342.46,344.2 1 0
+github.com/muety/wakapi/config/config.go:346.43,348.2 1 0
+github.com/muety/wakapi/config/config.go:350.29,352.2 1 1
+github.com/muety/wakapi/config/config.go:354.48,365.16 2 0
+github.com/muety/wakapi/config/config.go:369.2,370.53 2 0
+github.com/muety/wakapi/config/config.go:374.2,374.15 1 0
+github.com/muety/wakapi/config/config.go:365.16,367.3 1 0
+github.com/muety/wakapi/config/config.go:370.53,372.3 1 0
+github.com/muety/wakapi/config/config.go:377.38,378.43 1 0
+github.com/muety/wakapi/config/config.go:381.2,381.15 1 0
+github.com/muety/wakapi/config/config.go:378.43,380.3 1 0
+github.com/muety/wakapi/config/config.go:384.45,385.27 1 0
+github.com/muety/wakapi/config/config.go:388.2,388.24 1 0
+github.com/muety/wakapi/config/config.go:391.2,391.25 1 0
+github.com/muety/wakapi/config/config.go:394.2,394.15 1 0
+github.com/muety/wakapi/config/config.go:385.27,387.3 1 0
+github.com/muety/wakapi/config/config.go:388.24,390.3 1 0
+github.com/muety/wakapi/config/config.go:391.25,393.3 1 0
+github.com/muety/wakapi/config/config.go:397.26,399.2 1 0
+github.com/muety/wakapi/config/config.go:401.20,403.2 1 0
+github.com/muety/wakapi/config/config.go:405.35,410.96 3 0
+github.com/muety/wakapi/config/config.go:414.2,418.21 4 0
+github.com/muety/wakapi/config/config.go:422.2,430.52 5 0
+github.com/muety/wakapi/config/config.go:434.2,434.47 1 0
+github.com/muety/wakapi/config/config.go:440.2,440.29 1 0
+github.com/muety/wakapi/config/config.go:446.2,446.106 1 0
+github.com/muety/wakapi/config/config.go:449.2,449.28 1 0
+github.com/muety/wakapi/config/config.go:452.2,452.51 1 0
+github.com/muety/wakapi/config/config.go:456.2,456.100 1 0
+github.com/muety/wakapi/config/config.go:459.2,459.74 1 0
+github.com/muety/wakapi/config/config.go:463.2,465.78 2 0
+github.com/muety/wakapi/config/config.go:468.2,468.81 1 0
+github.com/muety/wakapi/config/config.go:471.2,471.66 1 0
+github.com/muety/wakapi/config/config.go:478.2,478.55 1 0
+github.com/muety/wakapi/config/config.go:481.2,481.56 1 0
+github.com/muety/wakapi/config/config.go:484.2,484.65 1 0
+github.com/muety/wakapi/config/config.go:488.2,489.14 2 0
+github.com/muety/wakapi/config/config.go:410.96,412.3 1 0
+github.com/muety/wakapi/config/config.go:418.21,420.3 1 0
+github.com/muety/wakapi/config/config.go:430.52,432.3 1 0
+github.com/muety/wakapi/config/config.go:434.47,435.14 1 0
+github.com/muety/wakapi/config/config.go:435.14,437.4 1 0
+github.com/muety/wakapi/config/config.go:440.29,443.3 2 0
+github.com/muety/wakapi/config/config.go:446.106,448.3 1 0
+github.com/muety/wakapi/config/config.go:449.28,451.3 1 0
+github.com/muety/wakapi/config/config.go:452.51,455.3 2 0
+github.com/muety/wakapi/config/config.go:456.100,458.3 1 0
+github.com/muety/wakapi/config/config.go:459.74,461.3 1 0
+github.com/muety/wakapi/config/config.go:465.78,467.3 1 0
+github.com/muety/wakapi/config/config.go:468.81,470.3 1 0
+github.com/muety/wakapi/config/config.go:471.66,472.48 1 0
+github.com/muety/wakapi/config/config.go:472.48,474.4 1 0
+github.com/muety/wakapi/config/config.go:478.55,480.3 1 0
+github.com/muety/wakapi/config/config.go:481.56,483.3 1 0
+github.com/muety/wakapi/config/config.go:484.65,486.3 1 0
+github.com/muety/wakapi/config/db.go:39.50,40.19 1 0
+github.com/muety/wakapi/config/db.go:53.2,53.12 1 0
+github.com/muety/wakapi/config/db.go:41.23,45.5 1 0
+github.com/muety/wakapi/config/db.go:46.26,49.5 1 0
+github.com/muety/wakapi/config/db.go:50.24,51.48 1 0
+github.com/muety/wakapi/config/db.go:56.53,66.2 1 1
+github.com/muety/wakapi/config/db.go:68.56,69.25 1 1
+github.com/muety/wakapi/config/db.go:73.2,74.16 2 1
+github.com/muety/wakapi/config/db.go:78.2,85.3 1 1
+github.com/muety/wakapi/config/db.go:69.25,71.3 1 0
+github.com/muety/wakapi/config/db.go:74.16,76.3 1 0
+github.com/muety/wakapi/config/db.go:88.54,90.2 1 1
+github.com/muety/wakapi/config/eventbus.go:26.13,28.2 1 1
+github.com/muety/wakapi/config/eventbus.go:30.26,32.2 1 0
github.com/muety/wakapi/config/fs.go:9.56,10.19 1 0
github.com/muety/wakapi/config/fs.go:13.2,13.19 1 0
github.com/muety/wakapi/config/fs.go:10.19,12.3 1 0
+github.com/muety/wakapi/config/jobqueue.go:27.13,34.2 5 1
+github.com/muety/wakapi/config/jobqueue.go:36.48,37.34 1 1
+github.com/muety/wakapi/config/jobqueue.go:40.2,43.12 4 1
+github.com/muety/wakapi/config/jobqueue.go:37.34,39.3 1 0
+github.com/muety/wakapi/config/jobqueue.go:46.44,48.2 1 0
+github.com/muety/wakapi/config/jobqueue.go:50.48,51.35 1 0
+github.com/muety/wakapi/config/jobqueue.go:54.2,54.24 1 0
+github.com/muety/wakapi/config/jobqueue.go:51.35,53.3 1 0
+github.com/muety/wakapi/config/jobqueue.go:57.43,59.37 2 0
+github.com/muety/wakapi/config/jobqueue.go:66.2,66.16 1 0
+github.com/muety/wakapi/config/jobqueue.go:59.37,65.3 1 0
+github.com/muety/wakapi/config/jobqueue.go:69.20,70.30 1 0
+github.com/muety/wakapi/config/jobqueue.go:70.30,72.3 1 0
github.com/muety/wakapi/config/sentry.go:22.35,24.2 1 0
github.com/muety/wakapi/config/sentry.go:26.62,29.2 2 0
github.com/muety/wakapi/config/sentry.go:39.33,46.2 2 0
@@ -314,260 +419,6 @@ github.com/muety/wakapi/config/sentry.go:143.17,145.3 1 0
github.com/muety/wakapi/config/sentry.go:148.49,152.51 2 0
github.com/muety/wakapi/config/sentry.go:155.2,155.12 1 0
github.com/muety/wakapi/config/sentry.go:152.51,154.3 1 0
-github.com/muety/wakapi/config/utils.go:5.78,7.22 2 0
-github.com/muety/wakapi/config/utils.go:13.2,13.11 1 0
-github.com/muety/wakapi/config/utils.go:7.22,8.18 1 0
-github.com/muety/wakapi/config/utils.go:11.3,11.12 1 0
-github.com/muety/wakapi/config/utils.go:8.18,10.4 1 0
-github.com/muety/wakapi/config/config.go:162.64,164.2 1 0
-github.com/muety/wakapi/config/config.go:166.59,168.2 1 0
-github.com/muety/wakapi/config/config.go:170.82,180.2 1 0
-github.com/muety/wakapi/config/config.go:182.31,184.2 1 0
-github.com/muety/wakapi/config/config.go:186.32,188.2 1 0
-github.com/muety/wakapi/config/config.go:190.74,191.19 1 0
-github.com/muety/wakapi/config/config.go:192.10,193.34 1 0
-github.com/muety/wakapi/config/config.go:193.34,194.90 1 0
-github.com/muety/wakapi/config/config.go:197.4,197.100 1 0
-github.com/muety/wakapi/config/config.go:200.4,200.91 1 0
-github.com/muety/wakapi/config/config.go:203.4,203.95 1 0
-github.com/muety/wakapi/config/config.go:206.4,206.93 1 0
-github.com/muety/wakapi/config/config.go:209.4,209.97 1 0
-github.com/muety/wakapi/config/config.go:212.4,212.101 1 0
-github.com/muety/wakapi/config/config.go:215.4,215.98 1 0
-github.com/muety/wakapi/config/config.go:218.4,218.97 1 0
-github.com/muety/wakapi/config/config.go:221.4,221.101 1 0
-github.com/muety/wakapi/config/config.go:224.4,224.14 1 0
-github.com/muety/wakapi/config/config.go:194.90,196.5 1 0
-github.com/muety/wakapi/config/config.go:197.100,199.5 1 0
-github.com/muety/wakapi/config/config.go:200.91,202.5 1 0
-github.com/muety/wakapi/config/config.go:203.95,205.5 1 0
-github.com/muety/wakapi/config/config.go:206.93,208.5 1 0
-github.com/muety/wakapi/config/config.go:209.97,211.5 1 0
-github.com/muety/wakapi/config/config.go:212.101,214.5 1 0
-github.com/muety/wakapi/config/config.go:215.98,217.5 1 0
-github.com/muety/wakapi/config/config.go:218.97,220.5 1 0
-github.com/muety/wakapi/config/config.go:221.101,223.5 1 0
-github.com/muety/wakapi/config/config.go:229.60,231.2 1 0
-github.com/muety/wakapi/config/config.go:233.59,235.2 1 0
-github.com/muety/wakapi/config/config.go:237.57,239.2 1 0
-github.com/muety/wakapi/config/config.go:241.53,243.2 1 0
-github.com/muety/wakapi/config/config.go:245.55,248.2 2 0
-github.com/muety/wakapi/config/config.go:250.50,252.2 1 0
-github.com/muety/wakapi/config/config.go:254.54,257.2 2 0
-github.com/muety/wakapi/config/config.go:259.36,261.2 1 0
-github.com/muety/wakapi/config/config.go:263.35,265.2 1 0
-github.com/muety/wakapi/config/config.go:267.38,269.2 1 0
-github.com/muety/wakapi/config/config.go:271.46,273.2 1 0
-github.com/muety/wakapi/config/config.go:275.43,277.2 1 0
-github.com/muety/wakapi/config/config.go:279.29,281.2 1 1
-github.com/muety/wakapi/config/config.go:283.48,294.16 2 0
-github.com/muety/wakapi/config/config.go:298.2,299.53 2 0
-github.com/muety/wakapi/config/config.go:303.2,303.15 1 0
-github.com/muety/wakapi/config/config.go:294.16,296.3 1 0
-github.com/muety/wakapi/config/config.go:299.53,301.3 1 0
-github.com/muety/wakapi/config/config.go:306.38,307.43 1 0
-github.com/muety/wakapi/config/config.go:310.2,310.15 1 0
-github.com/muety/wakapi/config/config.go:307.43,309.3 1 0
-github.com/muety/wakapi/config/config.go:313.45,314.27 1 0
-github.com/muety/wakapi/config/config.go:317.2,317.24 1 0
-github.com/muety/wakapi/config/config.go:320.2,320.25 1 0
-github.com/muety/wakapi/config/config.go:323.2,323.15 1 0
-github.com/muety/wakapi/config/config.go:314.27,316.3 1 0
-github.com/muety/wakapi/config/config.go:317.24,319.3 1 0
-github.com/muety/wakapi/config/config.go:320.25,322.3 1 0
-github.com/muety/wakapi/config/config.go:326.77,327.29 1 0
-github.com/muety/wakapi/config/config.go:332.2,332.19 1 0
-github.com/muety/wakapi/config/config.go:327.29,328.18 1 0
-github.com/muety/wakapi/config/config.go:328.18,330.4 1 0
-github.com/muety/wakapi/config/config.go:335.42,336.28 1 0
-github.com/muety/wakapi/config/config.go:352.2,352.20 1 0
-github.com/muety/wakapi/config/config.go:337.52,338.21 1 0
-github.com/muety/wakapi/config/config.go:339.53,340.22 1 0
-github.com/muety/wakapi/config/config.go:341.55,342.24 1 0
-github.com/muety/wakapi/config/config.go:343.54,344.23 1 0
-github.com/muety/wakapi/config/config.go:345.52,346.21 1 0
-github.com/muety/wakapi/config/config.go:347.54,348.23 1 0
-github.com/muety/wakapi/config/config.go:349.52,350.21 1 0
-github.com/muety/wakapi/config/config.go:355.26,357.2 1 0
-github.com/muety/wakapi/config/config.go:359.20,361.2 1 0
-github.com/muety/wakapi/config/config.go:363.35,368.96 3 0
-github.com/muety/wakapi/config/config.go:372.2,376.21 4 0
-github.com/muety/wakapi/config/config.go:380.2,388.52 5 0
-github.com/muety/wakapi/config/config.go:392.2,392.47 1 0
-github.com/muety/wakapi/config/config.go:398.2,398.29 1 0
-github.com/muety/wakapi/config/config.go:404.2,404.106 1 0
-github.com/muety/wakapi/config/config.go:407.2,407.28 1 0
-github.com/muety/wakapi/config/config.go:410.2,410.51 1 0
-github.com/muety/wakapi/config/config.go:414.2,414.94 1 0
-github.com/muety/wakapi/config/config.go:417.2,417.81 1 0
-github.com/muety/wakapi/config/config.go:420.2,420.75 1 0
-github.com/muety/wakapi/config/config.go:423.2,423.74 1 0
-github.com/muety/wakapi/config/config.go:427.2,428.14 2 0
-github.com/muety/wakapi/config/config.go:368.96,370.3 1 0
-github.com/muety/wakapi/config/config.go:376.21,378.3 1 0
-github.com/muety/wakapi/config/config.go:388.52,390.3 1 0
-github.com/muety/wakapi/config/config.go:392.47,393.14 1 0
-github.com/muety/wakapi/config/config.go:393.14,395.4 1 0
-github.com/muety/wakapi/config/config.go:398.29,401.3 2 0
-github.com/muety/wakapi/config/config.go:404.106,406.3 1 0
-github.com/muety/wakapi/config/config.go:407.28,409.3 1 0
-github.com/muety/wakapi/config/config.go:410.51,413.3 2 0
-github.com/muety/wakapi/config/config.go:414.94,416.3 1 0
-github.com/muety/wakapi/config/config.go:417.81,419.3 1 0
-github.com/muety/wakapi/config/config.go:420.75,422.3 1 0
-github.com/muety/wakapi/config/config.go:423.74,425.3 1 0
-github.com/muety/wakapi/config/db.go:39.50,40.19 1 0
-github.com/muety/wakapi/config/db.go:53.2,53.12 1 0
-github.com/muety/wakapi/config/db.go:41.23,45.5 1 0
-github.com/muety/wakapi/config/db.go:46.26,49.5 1 0
-github.com/muety/wakapi/config/db.go:50.24,51.48 1 0
-github.com/muety/wakapi/config/db.go:56.53,66.2 1 1
-github.com/muety/wakapi/config/db.go:68.56,69.25 1 1
-github.com/muety/wakapi/config/db.go:73.2,74.16 2 1
-github.com/muety/wakapi/config/db.go:78.2,85.3 1 1
-github.com/muety/wakapi/config/db.go:69.25,71.3 1 0
-github.com/muety/wakapi/config/db.go:74.16,76.3 1 0
-github.com/muety/wakapi/config/db.go:88.54,90.2 1 1
-github.com/muety/wakapi/config/eventbus.go:26.13,28.2 1 1
-github.com/muety/wakapi/config/eventbus.go:30.26,32.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:22.112,28.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:30.59,41.2 6 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:61.68,66.56 4 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:70.2,71.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:77.2,78.22 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:82.2,83.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:89.2,91.69 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:97.2,98.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:104.2,107.33 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:110.2,110.35 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:113.2,113.34 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:116.2,116.29 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:119.2,119.34 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:123.2,123.47 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:66.56,68.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:71.16,75.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:78.22,80.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:83.16,87.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:91.69,95.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:98.16,102.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:107.33,109.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:110.35,112.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:113.34,115.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:116.29,118.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:119.34,121.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:126.136,135.16 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:139.2,139.36 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:135.16,137.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:28.120,34.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:36.63,45.2 5 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:56.72,58.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:61.2,64.22 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:68.2,69.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:75.2,76.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:81.2,85.4 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:58.16,60.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:64.22,66.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:69.16,73.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:76.16,80.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:88.115,97.29 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:101.2,102.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:106.2,106.36 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:97.29,99.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:102.16,104.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:26.120,32.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:34.63,40.2 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:65.72,67.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:71.2,72.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:78.2,79.44 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:67.16,69.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:72.16,76.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:82.95,88.19 5 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:94.2,95.22 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:124.2,138.37 6 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:149.2,149.38 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:88.19,89.56 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:89.56,91.4 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:95.22,97.96 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:97.96,99.4 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:99.9,101.4 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:102.8,102.128 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:102.128,105.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:105.8,110.17 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:114.3,115.17 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:110.17,112.4 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:115.17,117.4 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:138.37,140.17 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:144.3,146.25 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:140.17,142.4 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:21.116,27.2 1 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:29.59,35.2 3 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:46.68,48.16 2 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:52.2,53.72 2 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:59.2,59.70 1 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:48.16,50.3 1 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:53.72,55.3 1 1
-github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:55.8,57.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:22.116,28.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:30.61,36.2 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:47.70,49.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:53.2,54.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:60.2,61.44 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:49.16,51.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:54.16,58.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:64.116,73.29 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:77.2,85.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:89.2,89.36 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:73.29,75.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:85.16,87.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:29.124,34.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:36.63,42.2 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:53.72,55.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:59.2,62.16 4 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:68.2,72.16 4 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:79.2,85.45 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:55.16,57.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:62.16,66.3 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:72.16,77.3 4 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:23.123,29.2 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:31.62,37.2 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:49.71,51.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:55.2,56.16 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:63.2,66.28 3 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:72.2,73.44 2 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:51.16,53.3 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:56.16,61.3 4 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:66.28,67.30 1 0
-github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:67.30,69.4 1 0
-github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
-github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
-github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
-github.com/muety/wakapi/models/heartbeats.go:19.43,22.2 2 0
-github.com/muety/wakapi/models/heartbeats.go:24.41,26.18 1 0
-github.com/muety/wakapi/models/heartbeats.go:29.2,29.16 1 0
-github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
-github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
-github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
-github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
-github.com/muety/wakapi/models/project_label.go:14.39,16.2 1 0
-github.com/muety/wakapi/models/alias.go:18.32,20.2 1 0
-github.com/muety/wakapi/models/alias.go:22.37,23.35 1 0
-github.com/muety/wakapi/models/alias.go:28.2,28.14 1 0
-github.com/muety/wakapi/models/alias.go:23.35,24.18 1 0
-github.com/muety/wakapi/models/alias.go:24.18,26.4 1 0
-github.com/muety/wakapi/models/duration.go:24.55,38.2 2 0
-github.com/muety/wakapi/models/duration.go:40.39,42.16 2 0
-github.com/muety/wakapi/models/duration.go:45.2,46.10 2 0
-github.com/muety/wakapi/models/duration.go:42.16,44.3 1 0
-github.com/muety/wakapi/models/duration.go:49.49,50.11 1 0
-github.com/muety/wakapi/models/duration.go:65.2,65.15 1 0
-github.com/muety/wakapi/models/duration.go:69.2,69.12 1 0
-github.com/muety/wakapi/models/duration.go:51.22,52.18 1 0
-github.com/muety/wakapi/models/duration.go:53.21,54.17 1 0
-github.com/muety/wakapi/models/duration.go:55.23,56.19 1 0
-github.com/muety/wakapi/models/duration.go:57.17,58.26 1 0
-github.com/muety/wakapi/models/duration.go:59.22,60.18 1 0
-github.com/muety/wakapi/models/duration.go:61.21,62.17 1 0
-github.com/muety/wakapi/models/duration.go:65.15,67.3 1 0
github.com/muety/wakapi/models/heartbeat.go:33.34,35.2 1 1
github.com/muety/wakapi/models/heartbeat.go:37.55,40.2 2 0
github.com/muety/wakapi/models/heartbeat.go:42.65,44.46 2 1
@@ -588,6 +439,111 @@ github.com/muety/wakapi/models/heartbeat.go:99.41,101.16 2 0
github.com/muety/wakapi/models/heartbeat.go:104.2,105.10 2 0
github.com/muety/wakapi/models/heartbeat.go:101.16,103.3 1 0
github.com/muety/wakapi/models/heartbeat.go:108.38,118.2 1 0
+github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
+github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
+github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
+github.com/muety/wakapi/models/leaderboard.go:28.73,30.2 1 0
+github.com/muety/wakapi/models/leaderboard.go:34.56,35.103 1 0
+github.com/muety/wakapi/models/leaderboard.go:35.103,37.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:37.13,39.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:42.63,43.29 1 0
+github.com/muety/wakapi/models/leaderboard.go:43.29,45.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:48.41,49.123 1 0
+github.com/muety/wakapi/models/leaderboard.go:49.123,51.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:54.50,56.2 1 0
+github.com/muety/wakapi/models/leaderboard.go:58.65,59.95 1 0
+github.com/muety/wakapi/models/leaderboard.go:59.95,61.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:64.49,72.25 3 0
+github.com/muety/wakapi/models/leaderboard.go:83.2,83.129 1 0
+github.com/muety/wakapi/models/leaderboard.go:86.2,86.67 1 0
+github.com/muety/wakapi/models/leaderboard.go:90.2,90.79 1 0
+github.com/muety/wakapi/models/leaderboard.go:72.25,73.58 1 0
+github.com/muety/wakapi/models/leaderboard.go:76.3,77.38 2 0
+github.com/muety/wakapi/models/leaderboard.go:80.3,80.40 1 0
+github.com/muety/wakapi/models/leaderboard.go:73.58,74.12 1 0
+github.com/muety/wakapi/models/leaderboard.go:77.38,79.4 1 0
+github.com/muety/wakapi/models/leaderboard.go:83.129,85.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:86.67,88.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:90.79,92.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:95.70,96.107 1 0
+github.com/muety/wakapi/models/leaderboard.go:96.107,98.3 1 0
+github.com/muety/wakapi/models/leaderboard.go:101.45,103.25 2 0
+github.com/muety/wakapi/models/leaderboard.go:108.2,108.19 1 0
+github.com/muety/wakapi/models/leaderboard.go:103.25,104.43 1 0
+github.com/muety/wakapi/models/leaderboard.go:104.43,106.4 1 0
+github.com/muety/wakapi/models/project_label.go:14.39,16.2 1 0
+github.com/muety/wakapi/models/alias.go:18.32,20.2 1 0
+github.com/muety/wakapi/models/alias.go:22.37,23.35 1 0
+github.com/muety/wakapi/models/alias.go:28.2,28.14 1 0
+github.com/muety/wakapi/models/alias.go:23.35,24.18 1 0
+github.com/muety/wakapi/models/alias.go:24.18,26.4 1 0
+github.com/muety/wakapi/models/interval.go:41.47,42.23 1 0
+github.com/muety/wakapi/models/interval.go:47.2,47.14 1 0
+github.com/muety/wakapi/models/interval.go:42.23,43.13 1 0
+github.com/muety/wakapi/models/interval.go:43.13,45.4 1 0
+github.com/muety/wakapi/models/user.go:11.13,13.2 1 1
+github.com/muety/wakapi/models/user.go:85.36,86.22 1 1
+github.com/muety/wakapi/models/user.go:89.2,90.16 2 1
+github.com/muety/wakapi/models/user.go:93.2,93.11 1 1
+github.com/muety/wakapi/models/user.go:86.22,88.3 1 1
+github.com/muety/wakapi/models/user.go:90.16,92.3 1 0
+github.com/muety/wakapi/models/user.go:98.41,101.2 2 1
+github.com/muety/wakapi/models/user.go:103.53,106.54 3 0
+github.com/muety/wakapi/models/user.go:109.2,109.51 1 0
+github.com/muety/wakapi/models/user.go:112.2,112.20 1 0
+github.com/muety/wakapi/models/user.go:106.54,108.3 1 0
+github.com/muety/wakapi/models/user.go:109.51,111.3 1 0
+github.com/muety/wakapi/models/user.go:116.52,117.28 1 0
+github.com/muety/wakapi/models/user.go:120.2,120.17 1 0
+github.com/muety/wakapi/models/user.go:117.28,119.3 1 0
+github.com/muety/wakapi/models/user.go:123.43,126.2 1 0
+github.com/muety/wakapi/models/user.go:128.45,131.2 1 0
+github.com/muety/wakapi/models/user.go:133.33,138.2 1 0
+github.com/muety/wakapi/models/user.go:140.41,142.2 1 0
+github.com/muety/wakapi/models/user.go:144.45,146.2 1 0
+github.com/muety/wakapi/models/user.go:148.45,150.2 1 0
+github.com/muety/wakapi/models/user.go:152.39,154.2 1 0
+github.com/muety/wakapi/models/user.go:156.39,159.2 2 0
+github.com/muety/wakapi/models/mail_address.go:15.13,18.2 2 1
+github.com/muety/wakapi/models/mail_address.go:24.38,26.2 1 0
+github.com/muety/wakapi/models/mail_address.go:28.35,30.21 2 1
+github.com/muety/wakapi/models/mail_address.go:36.2,36.11 1 1
+github.com/muety/wakapi/models/mail_address.go:30.21,31.21 1 1
+github.com/muety/wakapi/models/mail_address.go:34.3,34.18 1 1
+github.com/muety/wakapi/models/mail_address.go:31.21,33.4 1 1
+github.com/muety/wakapi/models/mail_address.go:39.35,41.2 1 1
+github.com/muety/wakapi/models/mail_address.go:43.43,45.22 2 0
+github.com/muety/wakapi/models/mail_address.go:48.2,48.12 1 0
+github.com/muety/wakapi/models/mail_address.go:45.22,47.3 1 0
+github.com/muety/wakapi/models/mail_address.go:51.46,53.22 2 1
+github.com/muety/wakapi/models/mail_address.go:56.2,56.12 1 1
+github.com/muety/wakapi/models/mail_address.go:53.22,55.3 1 1
+github.com/muety/wakapi/models/mail_address.go:59.40,60.22 1 1
+github.com/muety/wakapi/models/mail_address.go:65.2,65.13 1 1
+github.com/muety/wakapi/models/mail_address.go:60.22,61.17 1 1
+github.com/muety/wakapi/models/mail_address.go:61.17,63.4 1 1
+github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
+github.com/muety/wakapi/models/shared.go:45.52,47.2 1 0
+github.com/muety/wakapi/models/shared.go:49.52,52.16 3 0
+github.com/muety/wakapi/models/shared.go:55.2,57.12 3 0
+github.com/muety/wakapi/models/shared.go:52.16,54.3 1 0
+github.com/muety/wakapi/models/shared.go:60.52,66.22 2 0
+github.com/muety/wakapi/models/shared.go:81.2,84.12 3 0
+github.com/muety/wakapi/models/shared.go:67.14,71.17 2 0
+github.com/muety/wakapi/models/shared.go:74.17,76.8 2 0
+github.com/muety/wakapi/models/shared.go:77.10,78.64 1 0
+github.com/muety/wakapi/models/shared.go:71.17,73.4 1 0
+github.com/muety/wakapi/models/shared.go:87.51,90.2 2 0
+github.com/muety/wakapi/models/shared.go:92.45,94.2 1 0
+github.com/muety/wakapi/models/shared.go:96.37,98.2 1 0
+github.com/muety/wakapi/models/shared.go:100.35,102.2 1 0
+github.com/muety/wakapi/models/shared.go:104.34,106.2 1 0
+github.com/muety/wakapi/models/shared.go:108.34,109.20 1 0
+github.com/muety/wakapi/models/shared.go:112.2,112.19 1 0
+github.com/muety/wakapi/models/shared.go:109.20,111.3 1 0
+github.com/muety/wakapi/models/shared.go:115.35,116.21 1 0
+github.com/muety/wakapi/models/shared.go:119.2,119.34 1 0
+github.com/muety/wakapi/models/shared.go:116.21,118.3 1 0
github.com/muety/wakapi/models/summary.go:64.29,66.2 1 1
github.com/muety/wakapi/models/summary.go:68.35,70.2 1 0
github.com/muety/wakapi/models/summary.go:72.38,74.2 1 0
@@ -686,6 +642,20 @@ github.com/muety/wakapi/models/summary.go:361.50,365.2 1 1
github.com/muety/wakapi/models/summary.go:367.33,369.2 1 1
github.com/muety/wakapi/models/summary.go:371.43,373.2 1 1
github.com/muety/wakapi/models/summary.go:375.38,377.2 1 1
+github.com/muety/wakapi/models/duration.go:24.55,38.2 2 0
+github.com/muety/wakapi/models/duration.go:40.39,42.16 2 0
+github.com/muety/wakapi/models/duration.go:45.2,46.10 2 0
+github.com/muety/wakapi/models/duration.go:42.16,44.3 1 0
+github.com/muety/wakapi/models/duration.go:49.49,50.11 1 0
+github.com/muety/wakapi/models/duration.go:65.2,65.15 1 0
+github.com/muety/wakapi/models/duration.go:69.2,69.12 1 0
+github.com/muety/wakapi/models/duration.go:51.22,52.18 1 0
+github.com/muety/wakapi/models/duration.go:53.21,54.17 1 0
+github.com/muety/wakapi/models/duration.go:55.23,56.19 1 0
+github.com/muety/wakapi/models/duration.go:57.17,58.26 1 0
+github.com/muety/wakapi/models/duration.go:59.22,60.18 1 0
+github.com/muety/wakapi/models/duration.go:61.21,62.17 1 0
+github.com/muety/wakapi/models/duration.go:65.15,67.3 1 0
github.com/muety/wakapi/models/durations.go:7.30,9.2 1 0
github.com/muety/wakapi/models/durations.go:11.40,13.2 1 0
github.com/muety/wakapi/models/durations.go:15.35,17.2 1 0
@@ -786,164 +756,180 @@ github.com/muety/wakapi/models/filters.go:219.2,219.28 1 1
github.com/muety/wakapi/models/filters.go:222.2,222.10 1 1
github.com/muety/wakapi/models/filters.go:216.41,218.3 1 0
github.com/muety/wakapi/models/filters.go:219.28,221.3 1 1
-github.com/muety/wakapi/models/interval.go:41.47,42.23 1 0
-github.com/muety/wakapi/models/interval.go:47.2,47.14 1 0
-github.com/muety/wakapi/models/interval.go:42.23,43.13 1 0
-github.com/muety/wakapi/models/interval.go:43.13,45.4 1 0
-github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
-github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
-github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
+github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
+github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
+github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
+github.com/muety/wakapi/models/heartbeats.go:19.43,22.2 2 0
+github.com/muety/wakapi/models/heartbeats.go:24.41,26.18 1 0
+github.com/muety/wakapi/models/heartbeats.go:29.2,29.16 1 0
+github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
+github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
+github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
+github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
github.com/muety/wakapi/models/mail.go:19.44,23.2 3 0
github.com/muety/wakapi/models/mail.go:25.44,29.2 3 0
github.com/muety/wakapi/models/mail.go:31.32,44.2 1 0
github.com/muety/wakapi/models/mail.go:46.41,48.2 1 0
-github.com/muety/wakapi/models/mail_address.go:15.13,18.2 2 1
-github.com/muety/wakapi/models/mail_address.go:24.38,26.2 1 0
-github.com/muety/wakapi/models/mail_address.go:28.35,30.21 2 1
-github.com/muety/wakapi/models/mail_address.go:36.2,36.11 1 1
-github.com/muety/wakapi/models/mail_address.go:30.21,31.21 1 1
-github.com/muety/wakapi/models/mail_address.go:34.3,34.18 1 1
-github.com/muety/wakapi/models/mail_address.go:31.21,33.4 1 1
-github.com/muety/wakapi/models/mail_address.go:39.35,41.2 1 1
-github.com/muety/wakapi/models/mail_address.go:43.43,45.22 2 0
-github.com/muety/wakapi/models/mail_address.go:48.2,48.12 1 0
-github.com/muety/wakapi/models/mail_address.go:45.22,47.3 1 0
-github.com/muety/wakapi/models/mail_address.go:51.46,53.22 2 1
-github.com/muety/wakapi/models/mail_address.go:56.2,56.12 1 1
-github.com/muety/wakapi/models/mail_address.go:53.22,55.3 1 1
-github.com/muety/wakapi/models/mail_address.go:59.40,60.22 1 1
-github.com/muety/wakapi/models/mail_address.go:65.2,65.13 1 1
-github.com/muety/wakapi/models/mail_address.go:60.22,61.17 1 1
-github.com/muety/wakapi/models/mail_address.go:61.17,63.4 1 1
-github.com/muety/wakapi/models/leaderboard.go:28.73,30.2 1 0
-github.com/muety/wakapi/models/leaderboard.go:34.56,35.103 1 0
-github.com/muety/wakapi/models/leaderboard.go:35.103,37.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:37.13,39.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:42.63,43.29 1 0
-github.com/muety/wakapi/models/leaderboard.go:43.29,45.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:48.41,49.123 1 0
-github.com/muety/wakapi/models/leaderboard.go:49.123,51.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:54.50,56.2 1 0
-github.com/muety/wakapi/models/leaderboard.go:58.65,59.95 1 0
-github.com/muety/wakapi/models/leaderboard.go:59.95,61.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:64.49,72.25 3 0
-github.com/muety/wakapi/models/leaderboard.go:83.2,83.129 1 0
-github.com/muety/wakapi/models/leaderboard.go:86.2,86.67 1 0
-github.com/muety/wakapi/models/leaderboard.go:90.2,90.79 1 0
-github.com/muety/wakapi/models/leaderboard.go:72.25,73.58 1 0
-github.com/muety/wakapi/models/leaderboard.go:76.3,77.38 2 0
-github.com/muety/wakapi/models/leaderboard.go:80.3,80.40 1 0
-github.com/muety/wakapi/models/leaderboard.go:73.58,74.12 1 0
-github.com/muety/wakapi/models/leaderboard.go:77.38,79.4 1 0
-github.com/muety/wakapi/models/leaderboard.go:83.129,85.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:86.67,88.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:90.79,92.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:95.70,96.107 1 0
-github.com/muety/wakapi/models/leaderboard.go:96.107,98.3 1 0
-github.com/muety/wakapi/models/leaderboard.go:101.45,103.25 2 0
-github.com/muety/wakapi/models/leaderboard.go:108.2,108.19 1 0
-github.com/muety/wakapi/models/leaderboard.go:103.25,104.43 1 0
-github.com/muety/wakapi/models/leaderboard.go:104.43,106.4 1 0
-github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
-github.com/muety/wakapi/models/shared.go:45.52,47.2 1 0
-github.com/muety/wakapi/models/shared.go:49.52,52.16 3 0
-github.com/muety/wakapi/models/shared.go:55.2,57.12 3 0
-github.com/muety/wakapi/models/shared.go:52.16,54.3 1 0
-github.com/muety/wakapi/models/shared.go:60.52,66.22 2 0
-github.com/muety/wakapi/models/shared.go:81.2,84.12 3 0
-github.com/muety/wakapi/models/shared.go:67.14,71.17 2 0
-github.com/muety/wakapi/models/shared.go:74.17,76.8 2 0
-github.com/muety/wakapi/models/shared.go:77.10,78.64 1 0
-github.com/muety/wakapi/models/shared.go:71.17,73.4 1 0
-github.com/muety/wakapi/models/shared.go:87.51,90.2 2 0
-github.com/muety/wakapi/models/shared.go:92.45,94.2 1 0
-github.com/muety/wakapi/models/shared.go:96.37,98.2 1 0
-github.com/muety/wakapi/models/shared.go:100.35,102.2 1 0
-github.com/muety/wakapi/models/shared.go:104.34,106.2 1 0
-github.com/muety/wakapi/models/shared.go:108.34,109.20 1 0
-github.com/muety/wakapi/models/shared.go:112.2,112.19 1 0
-github.com/muety/wakapi/models/shared.go:109.20,111.3 1 0
-github.com/muety/wakapi/models/shared.go:115.35,116.21 1 0
-github.com/muety/wakapi/models/shared.go:119.2,119.34 1 0
-github.com/muety/wakapi/models/shared.go:116.21,118.3 1 0
-github.com/muety/wakapi/models/user.go:11.13,13.2 1 1
-github.com/muety/wakapi/models/user.go:85.36,86.22 1 1
-github.com/muety/wakapi/models/user.go:89.2,90.16 2 1
-github.com/muety/wakapi/models/user.go:93.2,93.11 1 1
-github.com/muety/wakapi/models/user.go:86.22,88.3 1 1
-github.com/muety/wakapi/models/user.go:90.16,92.3 1 0
-github.com/muety/wakapi/models/user.go:98.41,101.2 2 1
-github.com/muety/wakapi/models/user.go:103.53,106.54 3 0
-github.com/muety/wakapi/models/user.go:109.2,109.51 1 0
-github.com/muety/wakapi/models/user.go:112.2,112.20 1 0
-github.com/muety/wakapi/models/user.go:106.54,108.3 1 0
-github.com/muety/wakapi/models/user.go:109.51,111.3 1 0
-github.com/muety/wakapi/models/user.go:116.52,117.28 1 0
-github.com/muety/wakapi/models/user.go:120.2,120.17 1 0
-github.com/muety/wakapi/models/user.go:117.28,119.3 1 0
-github.com/muety/wakapi/models/user.go:123.43,126.2 1 0
-github.com/muety/wakapi/models/user.go:128.45,131.2 1 0
-github.com/muety/wakapi/models/user.go:133.33,138.2 1 0
-github.com/muety/wakapi/models/user.go:140.41,142.2 1 0
-github.com/muety/wakapi/models/user.go:144.45,146.2 1 0
-github.com/muety/wakapi/models/user.go:148.45,150.2 1 0
-github.com/muety/wakapi/models/user.go:152.39,154.2 1 0
-github.com/muety/wakapi/models/user.go:156.39,159.2 2 0
-github.com/muety/wakapi/services/duration.go:18.78,24.2 2 1
-github.com/muety/wakapi/services/duration.go:26.123,29.42 2 1
-github.com/muety/wakapi/services/duration.go:35.2,36.16 2 1
-github.com/muety/wakapi/services/duration.go:43.2,48.31 4 1
-github.com/muety/wakapi/services/duration.go:96.2,98.31 2 1
-github.com/muety/wakapi/services/duration.go:111.2,111.49 1 1
-github.com/muety/wakapi/services/duration.go:115.2,115.32 1 1
-github.com/muety/wakapi/services/duration.go:29.42,30.90 1 1
-github.com/muety/wakapi/services/duration.go:30.90,32.4 1 1
-github.com/muety/wakapi/services/duration.go:36.16,38.3 1 0
-github.com/muety/wakapi/services/duration.go:48.31,49.42 1 1
-github.com/muety/wakapi/services/duration.go:53.3,55.62 2 1
-github.com/muety/wakapi/services/duration.go:59.3,59.20 1 1
-github.com/muety/wakapi/services/duration.go:64.3,74.15 3 1
-github.com/muety/wakapi/services/duration.go:77.3,83.84 2 1
-github.com/muety/wakapi/services/duration.go:93.3,93.10 1 1
-github.com/muety/wakapi/services/duration.go:49.42,50.12 1 1
-github.com/muety/wakapi/services/duration.go:55.62,57.4 1 1
-github.com/muety/wakapi/services/duration.go:59.20,61.12 2 1
-github.com/muety/wakapi/services/duration.go:74.15,76.4 1 0
-github.com/muety/wakapi/services/duration.go:83.84,85.41 2 1
-github.com/muety/wakapi/services/duration.go:88.4,88.15 1 1
-github.com/muety/wakapi/services/duration.go:85.41,87.5 1 1
-github.com/muety/wakapi/services/duration.go:89.9,91.4 1 1
-github.com/muety/wakapi/services/duration.go:98.31,99.26 1 1
-github.com/muety/wakapi/services/duration.go:99.26,104.23 1 1
-github.com/muety/wakapi/services/duration.go:107.4,107.36 1 1
-github.com/muety/wakapi/services/duration.go:104.23,106.5 1 1
-github.com/muety/wakapi/services/duration.go:111.49,113.3 1 1
-github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
-github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
-github.com/muety/wakapi/services/key_value.go:25.78,27.16 2 0
-github.com/muety/wakapi/services/key_value.go:33.2,33.11 1 0
-github.com/muety/wakapi/services/key_value.go:27.16,32.3 1 0
-github.com/muety/wakapi/services/key_value.go:36.72,38.2 1 0
-github.com/muety/wakapi/services/key_value.go:40.60,42.2 1 0
-github.com/muety/wakapi/services/misc.go:21.126,28.2 1 0
-github.com/muety/wakapi/services/misc.go:40.50,44.2 3 0
-github.com/muety/wakapi/services/misc.go:46.51,48.16 2 0
-github.com/muety/wakapi/services/misc.go:52.2,55.26 3 0
-github.com/muety/wakapi/services/misc.go:61.2,63.40 2 0
-github.com/muety/wakapi/services/misc.go:68.2,70.33 3 0
-github.com/muety/wakapi/services/misc.go:74.2,79.17 2 0
-github.com/muety/wakapi/services/misc.go:83.2,86.17 1 0
-github.com/muety/wakapi/services/misc.go:90.2,90.12 1 0
-github.com/muety/wakapi/services/misc.go:48.16,50.3 1 0
-github.com/muety/wakapi/services/misc.go:55.26,60.3 1 0
-github.com/muety/wakapi/services/misc.go:63.40,65.3 1 0
-github.com/muety/wakapi/services/misc.go:70.33,73.3 2 0
-github.com/muety/wakapi/services/misc.go:79.17,81.3 1 0
-github.com/muety/wakapi/services/misc.go:86.17,88.3 1 0
-github.com/muety/wakapi/services/misc.go:93.116,94.24 1 0
-github.com/muety/wakapi/services/misc.go:94.24,95.156 1 0
-github.com/muety/wakapi/services/misc.go:95.156,97.4 1 0
-github.com/muety/wakapi/services/misc.go:97.9,102.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:28.120,34.2 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:36.63,45.2 5 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:56.72,58.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:61.2,64.22 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:68.2,69.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:75.2,76.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:81.2,85.4 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:58.16,60.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:64.22,66.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:69.16,73.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:76.16,80.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:88.115,97.29 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:101.2,102.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:106.2,106.36 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:97.29,99.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/statusbar.go:102.16,104.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:27.120,33.2 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:35.63,41.2 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:66.72,68.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:72.2,73.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:79.2,80.46 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:68.16,70.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:73.16,77.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:83.95,89.19 5 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:95.2,96.22 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:125.2,139.37 6 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:150.2,150.38 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:89.19,90.56 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:90.56,92.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:96.22,98.98 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:98.98,100.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:100.9,102.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:103.8,103.130 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:103.130,106.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:106.8,111.17 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:115.3,116.17 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:111.17,113.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:116.17,118.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:139.37,141.17 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:145.3,147.25 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/summaries.go:141.17,143.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:21.116,27.2 1 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:29.59,35.2 3 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:46.68,48.16 2 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:52.2,53.72 2 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:59.2,59.72 1 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:48.16,50.3 1 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:53.72,55.3 1 1
+github.com/muety/wakapi/routes/compat/wakatime/v1/users.go:55.8,57.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:22.116,28.2 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:30.61,36.2 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:47.70,49.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:53.2,54.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:60.2,61.46 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:49.16,51.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:54.16,58.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:64.116,73.29 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:77.2,85.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:89.2,89.36 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:73.29,75.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/all_time.go:85.16,87.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:29.124,34.2 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:36.63,42.2 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:53.72,55.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:59.2,62.16 4 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:68.2,72.16 4 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:79.2,85.47 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:55.16,57.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:62.16,66.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/heartbeat.go:72.16,77.3 4 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:23.123,29.2 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:31.62,37.2 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:49.71,51.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:55.2,56.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:63.2,66.28 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:72.2,73.46 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:51.16,53.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:56.16,61.3 4 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:66.28,67.30 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/projects.go:67.30,69.4 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:22.112,28.2 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:30.59,41.2 6 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:61.68,66.56 4 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:70.2,71.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:77.2,78.22 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:82.2,83.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:89.2,91.69 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:97.2,98.16 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:104.2,107.33 2 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:110.2,110.35 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:113.2,113.34 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:116.2,116.29 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:119.2,119.34 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:123.2,123.49 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:66.56,68.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:71.16,75.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:78.22,80.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:83.16,87.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:91.69,95.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:98.16,102.3 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:107.33,109.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:110.35,112.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:113.34,115.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:116.29,118.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:119.34,121.3 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:126.136,135.16 3 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:139.2,139.36 1 0
+github.com/muety/wakapi/routes/compat/wakatime/v1/stats.go:135.16,137.3 1 0
+github.com/muety/wakapi/services/diagnostics.go:14.101,19.2 1 0
+github.com/muety/wakapi/services/diagnostics.go:21.101,24.2 2 0
+github.com/muety/wakapi/services/project_label.go:21.111,28.2 1 0
+github.com/muety/wakapi/services/project_label.go:30.80,32.2 1 0
+github.com/muety/wakapi/services/project_label.go:34.90,35.51 1 0
+github.com/muety/wakapi/services/project_label.go:39.2,40.16 2 0
+github.com/muety/wakapi/services/project_label.go:43.2,44.20 2 0
+github.com/muety/wakapi/services/project_label.go:35.51,37.3 1 0
+github.com/muety/wakapi/services/project_label.go:40.16,42.3 1 0
+github.com/muety/wakapi/services/project_label.go:48.108,50.16 2 0
+github.com/muety/wakapi/services/project_label.go:53.2,53.112 1 0
+github.com/muety/wakapi/services/project_label.go:56.2,56.26 1 0
+github.com/muety/wakapi/services/project_label.go:50.16,52.3 1 0
+github.com/muety/wakapi/services/project_label.go:53.112,55.3 1 0
+github.com/muety/wakapi/services/project_label.go:60.116,62.16 2 0
+github.com/muety/wakapi/services/project_label.go:65.2,65.112 1 0
+github.com/muety/wakapi/services/project_label.go:68.2,68.26 1 0
+github.com/muety/wakapi/services/project_label.go:62.16,64.3 1 0
+github.com/muety/wakapi/services/project_label.go:65.112,67.3 1 0
+github.com/muety/wakapi/services/project_label.go:71.98,73.16 2 0
+github.com/muety/wakapi/services/project_label.go:77.2,79.20 3 0
+github.com/muety/wakapi/services/project_label.go:73.16,75.3 1 0
+github.com/muety/wakapi/services/project_label.go:82.74,83.24 1 0
+github.com/muety/wakapi/services/project_label.go:86.2,89.12 4 0
+github.com/muety/wakapi/services/project_label.go:83.24,85.3 1 0
+github.com/muety/wakapi/services/project_label.go:92.89,94.14 2 0
+github.com/muety/wakapi/services/project_label.go:97.2,100.4 1 0
+github.com/muety/wakapi/services/project_label.go:94.14,96.3 1 0
+github.com/muety/wakapi/services/report.go:31.122,44.2 2 0
+github.com/muety/wakapi/services/report.go:46.38,49.45 2 0
+github.com/muety/wakapi/services/report.go:67.2,67.49 1 0
+github.com/muety/wakapi/services/report.go:87.2,87.16 1 0
+github.com/muety/wakapi/services/report.go:49.45,50.46 1 0
+github.com/muety/wakapi/services/report.go:50.46,53.57 2 0
+github.com/muety/wakapi/services/report.go:58.4,58.58 1 0
+github.com/muety/wakapi/services/report.go:53.57,55.5 1 0
+github.com/muety/wakapi/services/report.go:58.58,61.5 2 0
+github.com/muety/wakapi/services/report.go:62.18,64.4 1 0
+github.com/muety/wakapi/services/report.go:67.49,70.17 2 0
+github.com/muety/wakapi/services/report.go:76.3,76.78 1 0
+github.com/muety/wakapi/services/report.go:81.3,82.27 2 0
+github.com/muety/wakapi/services/report.go:70.17,73.4 2 0
+github.com/muety/wakapi/services/report.go:76.78,78.4 1 0
+github.com/muety/wakapi/services/report.go:82.27,84.4 1 0
+github.com/muety/wakapi/services/report.go:87.16,89.3 1 0
+github.com/muety/wakapi/services/report.go:92.87,93.22 1 0
+github.com/muety/wakapi/services/report.go:98.2,104.16 5 0
+github.com/muety/wakapi/services/report.go:109.2,116.65 2 0
+github.com/muety/wakapi/services/report.go:121.2,122.12 2 0
+github.com/muety/wakapi/services/report.go:93.22,96.3 2 0
+github.com/muety/wakapi/services/report.go:104.16,107.3 2 0
+github.com/muety/wakapi/services/report.go:116.65,119.3 2 0
github.com/muety/wakapi/services/summary.go:29.189,41.33 3 1
github.com/muety/wakapi/services/summary.go:47.2,47.12 1 1
github.com/muety/wakapi/services/summary.go:41.33,42.31 1 1
@@ -1072,8 +1058,113 @@ github.com/muety/wakapi/services/summary.go:469.3,469.24 1 1
github.com/muety/wakapi/services/summary.go:460.17,461.33 1 1
github.com/muety/wakapi/services/summary.go:461.33,463.5 1 1
github.com/muety/wakapi/services/summary.go:466.28,468.4 1 1
-github.com/muety/wakapi/services/diagnostics.go:14.101,19.2 1 0
-github.com/muety/wakapi/services/diagnostics.go:21.101,24.2 2 0
+github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
+github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
+github.com/muety/wakapi/services/key_value.go:25.78,27.16 2 0
+github.com/muety/wakapi/services/key_value.go:33.2,33.11 1 0
+github.com/muety/wakapi/services/key_value.go:27.16,32.3 1 0
+github.com/muety/wakapi/services/key_value.go:36.72,38.2 1 0
+github.com/muety/wakapi/services/key_value.go:40.60,42.2 1 0
+github.com/muety/wakapi/services/aggregation.go:31.142,41.2 1 0
+github.com/muety/wakapi/services/aggregation.go:50.43,53.52 2 0
+github.com/muety/wakapi/services/aggregation.go:53.52,54.80 1 0
+github.com/muety/wakapi/services/aggregation.go:54.80,56.4 1 0
+github.com/muety/wakapi/services/aggregation.go:57.58,59.3 1 0
+github.com/muety/wakapi/services/aggregation.go:62.92,63.47 1 0
+github.com/muety/wakapi/services/aggregation.go:66.2,72.16 4 0
+github.com/muety/wakapi/services/aggregation.go:78.2,79.16 2 0
+github.com/muety/wakapi/services/aggregation.go:85.2,86.44 2 0
+github.com/muety/wakapi/services/aggregation.go:91.2,93.12 3 0
+github.com/muety/wakapi/services/aggregation.go:104.2,104.41 1 0
+github.com/muety/wakapi/services/aggregation.go:122.2,122.12 1 0
+github.com/muety/wakapi/services/aggregation.go:63.47,65.3 1 0
+github.com/muety/wakapi/services/aggregation.go:72.16,75.3 2 0
+github.com/muety/wakapi/services/aggregation.go:79.16,82.3 2 0
+github.com/muety/wakapi/services/aggregation.go:86.44,88.3 1 0
+github.com/muety/wakapi/services/aggregation.go:93.12,94.25 1 0
+github.com/muety/wakapi/services/aggregation.go:94.25,95.47 1 0
+github.com/muety/wakapi/services/aggregation.go:95.47,97.5 1 0
+github.com/muety/wakapi/services/aggregation.go:97.19,99.5 1 0
+github.com/muety/wakapi/services/aggregation.go:104.41,105.71 1 0
+github.com/muety/wakapi/services/aggregation.go:109.3,109.21 1 0
+github.com/muety/wakapi/services/aggregation.go:105.71,106.12 1 0
+github.com/muety/wakapi/services/aggregation.go:109.21,113.4 1 0
+github.com/muety/wakapi/services/aggregation.go:113.9,113.62 1 0
+github.com/muety/wakapi/services/aggregation.go:113.62,117.4 1 0
+github.com/muety/wakapi/services/aggregation.go:125.61,126.115 1 0
+github.com/muety/wakapi/services/aggregation.go:126.115,128.3 1 0
+github.com/muety/wakapi/services/aggregation.go:128.8,130.60 2 0
+github.com/muety/wakapi/services/aggregation.go:130.60,132.4 1 0
+github.com/muety/wakapi/services/aggregation.go:136.83,151.41 5 0
+github.com/muety/wakapi/services/aggregation.go:151.41,161.3 3 0
+github.com/muety/wakapi/services/aggregation.go:164.83,167.27 3 0
+github.com/muety/wakapi/services/aggregation.go:172.2,173.12 2 0
+github.com/muety/wakapi/services/aggregation.go:167.27,168.34 1 0
+github.com/muety/wakapi/services/aggregation.go:168.34,170.4 1 0
+github.com/muety/wakapi/services/aggregation.go:176.79,179.27 3 0
+github.com/muety/wakapi/services/aggregation.go:179.27,181.3 1 0
+github.com/muety/wakapi/services/aggregation.go:184.34,187.2 2 0
+github.com/muety/wakapi/services/alias.go:19.77,24.2 1 1
+github.com/muety/wakapi/services/alias.go:28.60,29.43 1 1
+github.com/muety/wakapi/services/alias.go:32.2,32.14 1 1
+github.com/muety/wakapi/services/alias.go:29.43,31.3 1 1
+github.com/muety/wakapi/services/alias.go:35.62,37.16 2 1
+github.com/muety/wakapi/services/alias.go:40.2,40.12 1 1
+github.com/muety/wakapi/services/alias.go:37.16,39.3 1 1
+github.com/muety/wakapi/services/alias.go:43.59,44.51 1 1
+github.com/muety/wakapi/services/alias.go:44.51,46.3 1 0
+github.com/muety/wakapi/services/alias.go:49.76,50.32 1 0
+github.com/muety/wakapi/services/alias.go:53.2,53.49 1 0
+github.com/muety/wakapi/services/alias.go:50.32,52.3 1 0
+github.com/muety/wakapi/services/alias.go:53.49,55.3 1 0
+github.com/muety/wakapi/services/alias.go:55.8,57.3 1 0
+github.com/muety/wakapi/services/alias.go:60.102,61.38 1 0
+github.com/muety/wakapi/services/alias.go:64.2,64.39 1 0
+github.com/muety/wakapi/services/alias.go:61.38,63.3 1 0
+github.com/muety/wakapi/services/alias.go:67.113,68.38 1 0
+github.com/muety/wakapi/services/alias.go:71.2,71.39 1 0
+github.com/muety/wakapi/services/alias.go:68.38,70.3 1 0
+github.com/muety/wakapi/services/alias.go:74.108,75.32 1 1
+github.com/muety/wakapi/services/alias.go:79.2,79.49 1 1
+github.com/muety/wakapi/services/alias.go:87.2,87.19 1 1
+github.com/muety/wakapi/services/alias.go:75.32,77.3 1 1
+github.com/muety/wakapi/services/alias.go:79.49,80.47 1 1
+github.com/muety/wakapi/services/alias.go:80.47,81.49 1 1
+github.com/muety/wakapi/services/alias.go:81.49,83.5 1 1
+github.com/muety/wakapi/services/alias.go:90.77,92.16 2 0
+github.com/muety/wakapi/services/alias.go:96.2,100.20 3 0
+github.com/muety/wakapi/services/alias.go:92.16,94.3 1 0
+github.com/muety/wakapi/services/alias.go:103.60,104.24 1 0
+github.com/muety/wakapi/services/alias.go:107.2,110.16 2 0
+github.com/muety/wakapi/services/alias.go:114.2,116.12 2 0
+github.com/muety/wakapi/services/alias.go:104.24,106.3 1 0
+github.com/muety/wakapi/services/alias.go:110.16,112.3 1 0
+github.com/muety/wakapi/services/alias.go:119.69,123.28 3 0
+github.com/muety/wakapi/services/alias.go:131.2,134.16 2 0
+github.com/muety/wakapi/services/alias.go:140.2,140.31 1 0
+github.com/muety/wakapi/services/alias.go:144.2,144.12 1 0
+github.com/muety/wakapi/services/alias.go:123.28,124.21 1 0
+github.com/muety/wakapi/services/alias.go:127.3,128.16 2 0
+github.com/muety/wakapi/services/alias.go:124.21,126.4 1 0
+github.com/muety/wakapi/services/alias.go:134.16,135.29 1 0
+github.com/muety/wakapi/services/alias.go:135.29,137.4 1 0
+github.com/muety/wakapi/services/alias.go:140.31,142.3 1 0
+github.com/muety/wakapi/services/alias.go:147.74,148.14 1 0
+github.com/muety/wakapi/services/alias.go:148.14,149.57 1 0
+github.com/muety/wakapi/services/alias.go:149.57,153.4 3 0
+github.com/muety/wakapi/services/alias.go:154.8,155.57 1 0
+github.com/muety/wakapi/services/alias.go:155.57,157.48 2 0
+github.com/muety/wakapi/services/alias.go:162.4,162.52 1 0
+github.com/muety/wakapi/services/alias.go:157.48,158.26 1 0
+github.com/muety/wakapi/services/alias.go:158.26,160.6 1 0
+github.com/muety/wakapi/services/alias.go:167.116,168.32 1 0
+github.com/muety/wakapi/services/alias.go:171.2,171.49 1 0
+github.com/muety/wakapi/services/alias.go:168.32,170.3 1 0
+github.com/muety/wakapi/services/alias.go:171.49,173.47 2 0
+github.com/muety/wakapi/services/alias.go:178.3,178.30 1 0
+github.com/muety/wakapi/services/alias.go:173.47,174.16 1 0
+github.com/muety/wakapi/services/alias.go:174.16,176.5 1 0
+github.com/muety/wakapi/services/alias.go:179.8,181.3 1 0
github.com/muety/wakapi/services/language_mapping.go:18.118,24.2 1 0
github.com/muety/wakapi/services/language_mapping.go:26.86,28.2 1 0
github.com/muety/wakapi/services/language_mapping.go:30.96,31.53 1 0
@@ -1093,109 +1184,92 @@ github.com/muety/wakapi/services/language_mapping.go:66.82,67.26 1 0
github.com/muety/wakapi/services/language_mapping.go:70.2,72.12 3 0
github.com/muety/wakapi/services/language_mapping.go:67.26,69.3 1 0
github.com/muety/wakapi/services/language_mapping.go:75.74,78.2 1 0
-github.com/muety/wakapi/services/leaderboard.go:27.159,38.33 3 0
-github.com/muety/wakapi/services/leaderboard.go:62.2,62.12 1 0
-github.com/muety/wakapi/services/leaderboard.go:38.33,39.31 1 0
-github.com/muety/wakapi/services/leaderboard.go:39.31,45.18 3 0
-github.com/muety/wakapi/services/leaderboard.go:49.4,49.41 1 0
-github.com/muety/wakapi/services/leaderboard.go:45.18,47.5 1 0
-github.com/muety/wakapi/services/leaderboard.go:49.41,52.5 2 0
-github.com/muety/wakapi/services/leaderboard.go:52.10,52.48 1 0
-github.com/muety/wakapi/services/leaderboard.go:52.48,54.64 2 0
-github.com/muety/wakapi/services/leaderboard.go:57.5,57.22 1 0
-github.com/muety/wakapi/services/leaderboard.go:54.64,56.6 1 0
-github.com/muety/wakapi/services/leaderboard.go:65.50,66.64 1 0
-github.com/muety/wakapi/services/leaderboard.go:76.2,78.19 3 0
-github.com/muety/wakapi/services/leaderboard.go:66.64,68.17 2 0
-github.com/muety/wakapi/services/leaderboard.go:73.3,73.31 1 0
-github.com/muety/wakapi/services/leaderboard.go:68.17,71.4 2 0
-github.com/muety/wakapi/services/leaderboard.go:81.106,84.29 2 0
-github.com/muety/wakapi/services/leaderboard.go:119.2,121.12 3 0
-github.com/muety/wakapi/services/leaderboard.go:84.29,85.83 1 0
-github.com/muety/wakapi/services/leaderboard.go:90.3,91.17 2 0
-github.com/muety/wakapi/services/leaderboard.go:96.3,96.85 1 0
-github.com/muety/wakapi/services/leaderboard.go:101.3,101.25 1 0
-github.com/muety/wakapi/services/leaderboard.go:85.83,87.12 2 0
-github.com/muety/wakapi/services/leaderboard.go:91.17,93.12 2 0
-github.com/muety/wakapi/services/leaderboard.go:96.85,98.12 2 0
-github.com/muety/wakapi/services/leaderboard.go:101.25,103.18 2 0
-github.com/muety/wakapi/services/leaderboard.go:108.4,108.23 1 0
-github.com/muety/wakapi/services/leaderboard.go:112.4,112.60 1 0
-github.com/muety/wakapi/services/leaderboard.go:103.18,105.13 2 0
-github.com/muety/wakapi/services/leaderboard.go:108.23,109.13 1 0
-github.com/muety/wakapi/services/leaderboard.go:112.60,114.13 2 0
-github.com/muety/wakapi/services/leaderboard.go:124.77,127.2 2 0
-github.com/muety/wakapi/services/leaderboard.go:129.60,132.52 2 0
-github.com/muety/wakapi/services/leaderboard.go:136.2,137.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:140.2,140.19 1 0
-github.com/muety/wakapi/services/leaderboard.go:132.52,134.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:137.16,139.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:143.154,145.2 1 0
-github.com/muety/wakapi/services/leaderboard.go:147.144,149.2 1 0
-github.com/muety/wakapi/services/leaderboard.go:151.175,154.52 2 0
-github.com/muety/wakapi/services/leaderboard.go:158.2,159.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:163.2,163.18 1 0
-github.com/muety/wakapi/services/leaderboard.go:176.2,177.19 2 0
-github.com/muety/wakapi/services/leaderboard.go:154.52,156.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:159.16,161.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:163.18,165.17 2 0
-github.com/muety/wakapi/services/leaderboard.go:165.17,167.4 1 0
-github.com/muety/wakapi/services/leaderboard.go:167.9,168.31 1 0
-github.com/muety/wakapi/services/leaderboard.go:168.31,169.40 1 0
-github.com/muety/wakapi/services/leaderboard.go:169.40,171.6 1 0
-github.com/muety/wakapi/services/leaderboard.go:180.165,183.52 2 0
-github.com/muety/wakapi/services/leaderboard.go:187.2,188.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:192.2,192.17 1 0
-github.com/muety/wakapi/services/leaderboard.go:203.2,204.19 2 0
-github.com/muety/wakapi/services/leaderboard.go:183.52,185.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:188.16,190.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:192.17,194.17 2 0
-github.com/muety/wakapi/services/leaderboard.go:194.17,196.4 1 0
-github.com/muety/wakapi/services/leaderboard.go:196.9,197.31 1 0
-github.com/muety/wakapi/services/leaderboard.go:197.31,199.5 1 0
-github.com/muety/wakapi/services/leaderboard.go:207.129,209.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:213.2,214.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:218.2,223.8 1 0
-github.com/muety/wakapi/services/leaderboard.go:209.16,211.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:214.16,216.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:226.151,228.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:232.2,233.16 2 0
-github.com/muety/wakapi/services/leaderboard.go:237.2,240.42 3 0
-github.com/muety/wakapi/services/leaderboard.go:252.2,252.19 1 0
-github.com/muety/wakapi/services/leaderboard.go:228.16,230.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:233.16,235.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:240.42,250.3 2 0
-github.com/muety/wakapi/services/leaderboard.go:255.132,257.47 2 0
-github.com/muety/wakapi/services/leaderboard.go:260.2,260.23 1 0
-github.com/muety/wakapi/services/leaderboard.go:263.2,263.10 1 0
-github.com/muety/wakapi/services/leaderboard.go:257.47,259.3 1 0
-github.com/muety/wakapi/services/leaderboard.go:260.23,262.3 1 0
-github.com/muety/wakapi/services/project_label.go:21.111,28.2 1 0
-github.com/muety/wakapi/services/project_label.go:30.80,32.2 1 0
-github.com/muety/wakapi/services/project_label.go:34.90,35.51 1 0
-github.com/muety/wakapi/services/project_label.go:39.2,40.16 2 0
-github.com/muety/wakapi/services/project_label.go:43.2,44.20 2 0
-github.com/muety/wakapi/services/project_label.go:35.51,37.3 1 0
-github.com/muety/wakapi/services/project_label.go:40.16,42.3 1 0
-github.com/muety/wakapi/services/project_label.go:48.108,50.16 2 0
-github.com/muety/wakapi/services/project_label.go:53.2,53.112 1 0
-github.com/muety/wakapi/services/project_label.go:56.2,56.26 1 0
-github.com/muety/wakapi/services/project_label.go:50.16,52.3 1 0
-github.com/muety/wakapi/services/project_label.go:53.112,55.3 1 0
-github.com/muety/wakapi/services/project_label.go:60.116,62.16 2 0
-github.com/muety/wakapi/services/project_label.go:65.2,65.112 1 0
-github.com/muety/wakapi/services/project_label.go:68.2,68.26 1 0
-github.com/muety/wakapi/services/project_label.go:62.16,64.3 1 0
-github.com/muety/wakapi/services/project_label.go:65.112,67.3 1 0
-github.com/muety/wakapi/services/project_label.go:71.98,73.16 2 0
-github.com/muety/wakapi/services/project_label.go:77.2,79.20 3 0
-github.com/muety/wakapi/services/project_label.go:73.16,75.3 1 0
-github.com/muety/wakapi/services/project_label.go:82.74,83.24 1 0
-github.com/muety/wakapi/services/project_label.go:86.2,89.12 4 0
-github.com/muety/wakapi/services/project_label.go:83.24,85.3 1 0
-github.com/muety/wakapi/services/project_label.go:92.89,94.14 2 0
-github.com/muety/wakapi/services/project_label.go:97.2,100.4 1 0
-github.com/muety/wakapi/services/project_label.go:94.14,96.3 1 0
+github.com/muety/wakapi/services/user.go:26.99,36.33 3 0
+github.com/muety/wakapi/services/user.go:57.2,57.12 1 0
+github.com/muety/wakapi/services/user.go:36.33,37.31 1 0
+github.com/muety/wakapi/services/user.go:37.31,43.73 4 0
+github.com/muety/wakapi/services/user.go:47.4,47.24 1 0
+github.com/muety/wakapi/services/user.go:43.73,45.5 1 0
+github.com/muety/wakapi/services/user.go:47.24,48.80 1 0
+github.com/muety/wakapi/services/user.go:48.80,50.6 1 0
+github.com/muety/wakapi/services/user.go:50.11,52.6 1 0
+github.com/muety/wakapi/services/user.go:60.74,61.40 1 0
+github.com/muety/wakapi/services/user.go:65.2,66.16 2 0
+github.com/muety/wakapi/services/user.go:70.2,71.15 2 0
+github.com/muety/wakapi/services/user.go:61.40,63.3 1 0
+github.com/muety/wakapi/services/user.go:66.16,68.3 1 0
+github.com/muety/wakapi/services/user.go:74.72,75.37 1 0
+github.com/muety/wakapi/services/user.go:79.2,80.16 2 0
+github.com/muety/wakapi/services/user.go:84.2,85.15 2 0
+github.com/muety/wakapi/services/user.go:75.37,77.3 1 0
+github.com/muety/wakapi/services/user.go:80.16,82.3 1 0
+github.com/muety/wakapi/services/user.go:88.76,90.2 1 0
+github.com/muety/wakapi/services/user.go:92.86,94.2 1 0
+github.com/muety/wakapi/services/user.go:96.58,98.2 1 0
+github.com/muety/wakapi/services/user.go:100.71,102.2 1 0
+github.com/muety/wakapi/services/user.go:104.86,106.16 2 0
+github.com/muety/wakapi/services/user.go:109.2,109.112 1 0
+github.com/muety/wakapi/services/user.go:106.16,108.3 1 0
+github.com/muety/wakapi/services/user.go:109.112,111.3 1 0
+github.com/muety/wakapi/services/user.go:114.86,116.2 1 0
+github.com/muety/wakapi/services/user.go:118.94,120.2 1 0
+github.com/muety/wakapi/services/user.go:122.71,124.12 2 0
+github.com/muety/wakapi/services/user.go:128.2,129.42 2 0
+github.com/muety/wakapi/services/user.go:133.2,134.16 2 0
+github.com/muety/wakapi/services/user.go:138.2,139.21 2 0
+github.com/muety/wakapi/services/user.go:124.12,126.3 1 0
+github.com/muety/wakapi/services/user.go:129.42,131.3 1 0
+github.com/muety/wakapi/services/user.go:134.16,136.3 1 0
+github.com/muety/wakapi/services/user.go:142.48,144.2 1 0
+github.com/muety/wakapi/services/user.go:146.102,156.93 2 0
+github.com/muety/wakapi/services/user.go:162.2,162.38 1 0
+github.com/muety/wakapi/services/user.go:156.93,158.3 1 0
+github.com/muety/wakapi/services/user.go:158.8,160.3 1 0
+github.com/muety/wakapi/services/user.go:165.73,169.2 3 0
+github.com/muety/wakapi/services/user.go:171.78,175.2 3 0
+github.com/muety/wakapi/services/user.go:177.122,180.35 2 0
+github.com/muety/wakapi/services/user.go:186.2,186.35 1 0
+github.com/muety/wakapi/services/user.go:190.2,190.18 1 0
+github.com/muety/wakapi/services/user.go:180.35,181.89 1 0
+github.com/muety/wakapi/services/user.go:181.89,183.4 1 0
+github.com/muety/wakapi/services/user.go:186.35,188.3 1 0
+github.com/muety/wakapi/services/user.go:193.106,196.96 3 0
+github.com/muety/wakapi/services/user.go:201.2,201.68 1 0
+github.com/muety/wakapi/services/user.go:196.96,198.3 1 0
+github.com/muety/wakapi/services/user.go:198.8,200.3 1 0
+github.com/muety/wakapi/services/user.go:204.85,206.2 1 0
+github.com/muety/wakapi/services/user.go:208.57,215.2 4 0
+github.com/muety/wakapi/services/user.go:217.38,219.2 1 0
+github.com/muety/wakapi/services/user.go:221.57,226.2 1 0
+github.com/muety/wakapi/services/duration.go:18.78,24.2 2 1
+github.com/muety/wakapi/services/duration.go:26.123,29.42 2 1
+github.com/muety/wakapi/services/duration.go:35.2,36.16 2 1
+github.com/muety/wakapi/services/duration.go:43.2,48.31 4 1
+github.com/muety/wakapi/services/duration.go:96.2,98.31 2 1
+github.com/muety/wakapi/services/duration.go:111.2,111.49 1 1
+github.com/muety/wakapi/services/duration.go:115.2,115.32 1 1
+github.com/muety/wakapi/services/duration.go:29.42,30.90 1 1
+github.com/muety/wakapi/services/duration.go:30.90,32.4 1 1
+github.com/muety/wakapi/services/duration.go:36.16,38.3 1 0
+github.com/muety/wakapi/services/duration.go:48.31,49.42 1 1
+github.com/muety/wakapi/services/duration.go:53.3,55.62 2 1
+github.com/muety/wakapi/services/duration.go:59.3,59.20 1 1
+github.com/muety/wakapi/services/duration.go:64.3,74.15 3 1
+github.com/muety/wakapi/services/duration.go:77.3,83.84 2 1
+github.com/muety/wakapi/services/duration.go:93.3,93.10 1 1
+github.com/muety/wakapi/services/duration.go:49.42,50.12 1 1
+github.com/muety/wakapi/services/duration.go:55.62,57.4 1 1
+github.com/muety/wakapi/services/duration.go:59.20,61.12 2 1
+github.com/muety/wakapi/services/duration.go:74.15,76.4 1 0
+github.com/muety/wakapi/services/duration.go:83.84,85.41 2 1
+github.com/muety/wakapi/services/duration.go:88.4,88.15 1 1
+github.com/muety/wakapi/services/duration.go:85.41,87.5 1 1
+github.com/muety/wakapi/services/duration.go:89.9,91.4 1 1
+github.com/muety/wakapi/services/duration.go:98.31,99.26 1 1
+github.com/muety/wakapi/services/duration.go:99.26,104.23 1 1
+github.com/muety/wakapi/services/duration.go:107.4,107.36 1 1
+github.com/muety/wakapi/services/duration.go:104.23,106.5 1 1
+github.com/muety/wakapi/services/duration.go:111.49,113.3 1 1
github.com/muety/wakapi/services/heartbeat.go:26.141,40.33 3 0
github.com/muety/wakapi/services/heartbeat.go:48.2,48.12 1 0
github.com/muety/wakapi/services/heartbeat.go:40.33,41.31 1 0
@@ -1267,191 +1341,101 @@ github.com/muety/wakapi/services/heartbeat.go:259.94,261.48 2 0
github.com/muety/wakapi/services/heartbeat.go:267.2,267.18 1 0
github.com/muety/wakapi/services/heartbeat.go:261.48,263.18 2 0
github.com/muety/wakapi/services/heartbeat.go:263.18,265.4 1 0
-github.com/muety/wakapi/services/aggregation.go:30.142,38.2 1 0
-github.com/muety/wakapi/services/aggregation.go:47.43,51.2 3 0
-github.com/muety/wakapi/services/aggregation.go:53.77,54.47 1 0
-github.com/muety/wakapi/services/aggregation.go:57.2,62.40 4 0
-github.com/muety/wakapi/services/aggregation.go:66.2,66.50 1 0
-github.com/muety/wakapi/services/aggregation.go:71.2,71.60 1 0
-github.com/muety/wakapi/services/aggregation.go:77.2,77.35 1 0
-github.com/muety/wakapi/services/aggregation.go:54.47,56.3 1 0
-github.com/muety/wakapi/services/aggregation.go:62.40,64.3 1 0
-github.com/muety/wakapi/services/aggregation.go:66.50,68.3 1 0
-github.com/muety/wakapi/services/aggregation.go:71.60,75.3 3 0
-github.com/muety/wakapi/services/aggregation.go:80.109,81.24 1 0
-github.com/muety/wakapi/services/aggregation.go:81.24,82.116 1 0
-github.com/muety/wakapi/services/aggregation.go:82.116,84.4 1 0
-github.com/muety/wakapi/services/aggregation.go:84.9,87.4 2 0
-github.com/muety/wakapi/services/aggregation.go:91.80,92.33 1 0
-github.com/muety/wakapi/services/aggregation.go:92.33,93.60 1 0
-github.com/muety/wakapi/services/aggregation.go:93.60,95.4 1 0
-github.com/muety/wakapi/services/aggregation.go:99.110,104.16 3 0
-github.com/muety/wakapi/services/aggregation.go:110.2,111.16 2 0
-github.com/muety/wakapi/services/aggregation.go:117.2,118.44 2 0
-github.com/muety/wakapi/services/aggregation.go:123.2,123.41 1 0
-github.com/muety/wakapi/services/aggregation.go:141.2,141.12 1 0
-github.com/muety/wakapi/services/aggregation.go:104.16,107.3 2 0
-github.com/muety/wakapi/services/aggregation.go:111.16,114.3 2 0
-github.com/muety/wakapi/services/aggregation.go:118.44,120.3 1 0
-github.com/muety/wakapi/services/aggregation.go:123.41,124.71 1 0
-github.com/muety/wakapi/services/aggregation.go:128.3,128.21 1 0
-github.com/muety/wakapi/services/aggregation.go:124.71,125.12 1 0
-github.com/muety/wakapi/services/aggregation.go:128.21,132.4 1 0
-github.com/muety/wakapi/services/aggregation.go:132.9,132.62 1 0
-github.com/muety/wakapi/services/aggregation.go:132.62,136.4 1 0
-github.com/muety/wakapi/services/aggregation.go:144.83,147.27 3 0
-github.com/muety/wakapi/services/aggregation.go:152.2,153.12 2 0
-github.com/muety/wakapi/services/aggregation.go:147.27,148.34 1 0
-github.com/muety/wakapi/services/aggregation.go:148.34,150.4 1 0
-github.com/muety/wakapi/services/aggregation.go:156.79,159.27 3 0
-github.com/muety/wakapi/services/aggregation.go:159.27,161.3 1 0
-github.com/muety/wakapi/services/aggregation.go:164.83,179.41 5 0
-github.com/muety/wakapi/services/aggregation.go:179.41,189.3 3 0
-github.com/muety/wakapi/services/aggregation.go:192.34,195.2 2 0
-github.com/muety/wakapi/services/alias.go:19.77,24.2 1 1
-github.com/muety/wakapi/services/alias.go:28.60,29.43 1 1
-github.com/muety/wakapi/services/alias.go:32.2,32.14 1 1
-github.com/muety/wakapi/services/alias.go:29.43,31.3 1 1
-github.com/muety/wakapi/services/alias.go:35.62,37.16 2 1
-github.com/muety/wakapi/services/alias.go:40.2,40.12 1 1
-github.com/muety/wakapi/services/alias.go:37.16,39.3 1 1
-github.com/muety/wakapi/services/alias.go:43.59,44.51 1 1
-github.com/muety/wakapi/services/alias.go:44.51,46.3 1 0
-github.com/muety/wakapi/services/alias.go:49.76,50.32 1 0
-github.com/muety/wakapi/services/alias.go:53.2,53.49 1 0
-github.com/muety/wakapi/services/alias.go:50.32,52.3 1 0
-github.com/muety/wakapi/services/alias.go:53.49,55.3 1 0
-github.com/muety/wakapi/services/alias.go:55.8,57.3 1 0
-github.com/muety/wakapi/services/alias.go:60.102,61.38 1 0
-github.com/muety/wakapi/services/alias.go:64.2,64.39 1 0
-github.com/muety/wakapi/services/alias.go:61.38,63.3 1 0
-github.com/muety/wakapi/services/alias.go:67.113,68.38 1 0
-github.com/muety/wakapi/services/alias.go:71.2,71.39 1 0
-github.com/muety/wakapi/services/alias.go:68.38,70.3 1 0
-github.com/muety/wakapi/services/alias.go:74.108,75.32 1 1
-github.com/muety/wakapi/services/alias.go:79.2,79.49 1 1
-github.com/muety/wakapi/services/alias.go:87.2,87.19 1 1
-github.com/muety/wakapi/services/alias.go:75.32,77.3 1 1
-github.com/muety/wakapi/services/alias.go:79.49,80.47 1 1
-github.com/muety/wakapi/services/alias.go:80.47,81.49 1 1
-github.com/muety/wakapi/services/alias.go:81.49,83.5 1 1
-github.com/muety/wakapi/services/alias.go:90.77,92.16 2 0
-github.com/muety/wakapi/services/alias.go:96.2,100.20 3 0
-github.com/muety/wakapi/services/alias.go:92.16,94.3 1 0
-github.com/muety/wakapi/services/alias.go:103.60,104.24 1 0
-github.com/muety/wakapi/services/alias.go:107.2,110.16 2 0
-github.com/muety/wakapi/services/alias.go:114.2,116.12 2 0
-github.com/muety/wakapi/services/alias.go:104.24,106.3 1 0
-github.com/muety/wakapi/services/alias.go:110.16,112.3 1 0
-github.com/muety/wakapi/services/alias.go:119.69,123.28 3 0
-github.com/muety/wakapi/services/alias.go:131.2,134.16 2 0
-github.com/muety/wakapi/services/alias.go:140.2,140.31 1 0
-github.com/muety/wakapi/services/alias.go:144.2,144.12 1 0
-github.com/muety/wakapi/services/alias.go:123.28,124.21 1 0
-github.com/muety/wakapi/services/alias.go:127.3,128.16 2 0
-github.com/muety/wakapi/services/alias.go:124.21,126.4 1 0
-github.com/muety/wakapi/services/alias.go:134.16,135.29 1 0
-github.com/muety/wakapi/services/alias.go:135.29,137.4 1 0
-github.com/muety/wakapi/services/alias.go:140.31,142.3 1 0
-github.com/muety/wakapi/services/alias.go:147.74,148.14 1 0
-github.com/muety/wakapi/services/alias.go:148.14,149.57 1 0
-github.com/muety/wakapi/services/alias.go:149.57,153.4 3 0
-github.com/muety/wakapi/services/alias.go:154.8,155.57 1 0
-github.com/muety/wakapi/services/alias.go:155.57,157.48 2 0
-github.com/muety/wakapi/services/alias.go:162.4,162.52 1 0
-github.com/muety/wakapi/services/alias.go:157.48,158.26 1 0
-github.com/muety/wakapi/services/alias.go:158.26,160.6 1 0
-github.com/muety/wakapi/services/alias.go:167.116,168.32 1 0
-github.com/muety/wakapi/services/alias.go:171.2,171.49 1 0
-github.com/muety/wakapi/services/alias.go:168.32,170.3 1 0
-github.com/muety/wakapi/services/alias.go:171.49,173.47 2 0
-github.com/muety/wakapi/services/alias.go:178.3,178.30 1 0
-github.com/muety/wakapi/services/alias.go:173.47,174.16 1 0
-github.com/muety/wakapi/services/alias.go:174.16,176.5 1 0
-github.com/muety/wakapi/services/alias.go:179.8,181.3 1 0
-github.com/muety/wakapi/services/report.go:30.122,44.33 4 0
-github.com/muety/wakapi/services/report.go:50.2,50.12 1 0
-github.com/muety/wakapi/services/report.go:44.33,45.31 1 0
-github.com/muety/wakapi/services/report.go:45.31,47.4 1 0
-github.com/muety/wakapi/services/report.go:53.38,57.16 3 0
-github.com/muety/wakapi/services/report.go:61.2,62.26 2 0
-github.com/muety/wakapi/services/report.go:57.16,59.3 1 0
-github.com/muety/wakapi/services/report.go:62.26,64.3 1 0
-github.com/muety/wakapi/services/report.go:69.61,74.22 3 0
-github.com/muety/wakapi/services/report.go:81.2,81.65 1 0
-github.com/muety/wakapi/services/report.go:98.2,98.24 1 0
-github.com/muety/wakapi/services/report.go:74.22,78.3 3 0
-github.com/muety/wakapi/services/report.go:81.65,91.47 3 0
-github.com/muety/wakapi/services/report.go:91.47,93.4 1 0
-github.com/muety/wakapi/services/report.go:93.9,95.4 1 0
-github.com/muety/wakapi/services/report.go:101.80,102.22 1 0
-github.com/muety/wakapi/services/report.go:107.2,107.29 1 0
-github.com/muety/wakapi/services/report.go:112.2,116.16 4 0
-github.com/muety/wakapi/services/report.go:121.2,128.65 2 0
-github.com/muety/wakapi/services/report.go:133.2,134.12 2 0
-github.com/muety/wakapi/services/report.go:102.22,105.3 2 0
-github.com/muety/wakapi/services/report.go:107.29,110.3 2 0
-github.com/muety/wakapi/services/report.go:116.16,119.3 2 0
-github.com/muety/wakapi/services/report.go:128.65,131.3 2 0
-github.com/muety/wakapi/services/report.go:137.63,138.41 1 0
-github.com/muety/wakapi/services/report.go:145.2,145.12 1 0
-github.com/muety/wakapi/services/report.go:138.41,139.30 1 0
-github.com/muety/wakapi/services/report.go:139.30,140.16 1 0
-github.com/muety/wakapi/services/report.go:140.16,142.5 1 0
-github.com/muety/wakapi/services/user.go:26.99,36.33 3 0
-github.com/muety/wakapi/services/user.go:57.2,57.12 1 0
-github.com/muety/wakapi/services/user.go:36.33,37.31 1 0
-github.com/muety/wakapi/services/user.go:37.31,43.73 4 0
-github.com/muety/wakapi/services/user.go:47.4,47.24 1 0
-github.com/muety/wakapi/services/user.go:43.73,45.5 1 0
-github.com/muety/wakapi/services/user.go:47.24,48.80 1 0
-github.com/muety/wakapi/services/user.go:48.80,50.6 1 0
-github.com/muety/wakapi/services/user.go:50.11,52.6 1 0
-github.com/muety/wakapi/services/user.go:60.74,61.40 1 0
-github.com/muety/wakapi/services/user.go:65.2,66.16 2 0
-github.com/muety/wakapi/services/user.go:70.2,71.15 2 0
-github.com/muety/wakapi/services/user.go:61.40,63.3 1 0
-github.com/muety/wakapi/services/user.go:66.16,68.3 1 0
-github.com/muety/wakapi/services/user.go:74.72,75.37 1 0
-github.com/muety/wakapi/services/user.go:79.2,80.16 2 0
-github.com/muety/wakapi/services/user.go:84.2,85.15 2 0
-github.com/muety/wakapi/services/user.go:75.37,77.3 1 0
-github.com/muety/wakapi/services/user.go:80.16,82.3 1 0
-github.com/muety/wakapi/services/user.go:88.76,90.2 1 0
-github.com/muety/wakapi/services/user.go:92.86,94.2 1 0
-github.com/muety/wakapi/services/user.go:96.58,98.2 1 0
-github.com/muety/wakapi/services/user.go:100.71,102.2 1 0
-github.com/muety/wakapi/services/user.go:104.86,106.16 2 0
-github.com/muety/wakapi/services/user.go:109.2,109.112 1 0
-github.com/muety/wakapi/services/user.go:106.16,108.3 1 0
-github.com/muety/wakapi/services/user.go:109.112,111.3 1 0
-github.com/muety/wakapi/services/user.go:114.86,116.2 1 0
-github.com/muety/wakapi/services/user.go:118.94,120.2 1 0
-github.com/muety/wakapi/services/user.go:122.71,124.12 2 0
-github.com/muety/wakapi/services/user.go:128.2,129.42 2 0
-github.com/muety/wakapi/services/user.go:133.2,134.16 2 0
-github.com/muety/wakapi/services/user.go:138.2,139.21 2 0
-github.com/muety/wakapi/services/user.go:124.12,126.3 1 0
-github.com/muety/wakapi/services/user.go:129.42,131.3 1 0
-github.com/muety/wakapi/services/user.go:134.16,136.3 1 0
-github.com/muety/wakapi/services/user.go:142.48,144.2 1 0
-github.com/muety/wakapi/services/user.go:146.102,156.93 2 0
-github.com/muety/wakapi/services/user.go:162.2,162.38 1 0
-github.com/muety/wakapi/services/user.go:156.93,158.3 1 0
-github.com/muety/wakapi/services/user.go:158.8,160.3 1 0
-github.com/muety/wakapi/services/user.go:165.73,169.2 3 0
-github.com/muety/wakapi/services/user.go:171.78,175.2 3 0
-github.com/muety/wakapi/services/user.go:177.122,180.35 2 0
-github.com/muety/wakapi/services/user.go:186.2,186.35 1 0
-github.com/muety/wakapi/services/user.go:190.2,190.18 1 0
-github.com/muety/wakapi/services/user.go:180.35,181.89 1 0
-github.com/muety/wakapi/services/user.go:181.89,183.4 1 0
-github.com/muety/wakapi/services/user.go:186.35,188.3 1 0
-github.com/muety/wakapi/services/user.go:193.106,196.96 3 0
-github.com/muety/wakapi/services/user.go:201.2,201.68 1 0
-github.com/muety/wakapi/services/user.go:196.96,198.3 1 0
-github.com/muety/wakapi/services/user.go:198.8,200.3 1 0
-github.com/muety/wakapi/services/user.go:204.85,206.2 1 0
-github.com/muety/wakapi/services/user.go:208.57,215.2 4 0
-github.com/muety/wakapi/services/user.go:217.38,219.2 1 0
-github.com/muety/wakapi/services/user.go:221.57,226.2 1 0
+github.com/muety/wakapi/services/leaderboard.go:29.159,42.33 3 0
+github.com/muety/wakapi/services/leaderboard.go:66.2,66.12 1 0
+github.com/muety/wakapi/services/leaderboard.go:42.33,43.31 1 0
+github.com/muety/wakapi/services/leaderboard.go:43.31,49.18 3 0
+github.com/muety/wakapi/services/leaderboard.go:53.4,53.41 1 0
+github.com/muety/wakapi/services/leaderboard.go:49.18,51.5 1 0
+github.com/muety/wakapi/services/leaderboard.go:53.41,56.5 2 0
+github.com/muety/wakapi/services/leaderboard.go:56.10,56.48 1 0
+github.com/muety/wakapi/services/leaderboard.go:56.48,58.64 2 0
+github.com/muety/wakapi/services/leaderboard.go:61.5,61.22 1 0
+github.com/muety/wakapi/services/leaderboard.go:58.64,60.6 1 0
+github.com/muety/wakapi/services/leaderboard.go:69.43,72.21 2 0
+github.com/muety/wakapi/services/leaderboard.go:81.2,81.76 1 0
+github.com/muety/wakapi/services/leaderboard.go:72.21,74.17 2 0
+github.com/muety/wakapi/services/leaderboard.go:78.3,78.91 1 0
+github.com/muety/wakapi/services/leaderboard.go:74.17,77.4 2 0
+github.com/muety/wakapi/services/leaderboard.go:81.76,82.77 1 0
+github.com/muety/wakapi/services/leaderboard.go:82.77,84.4 1 0
+github.com/muety/wakapi/services/leaderboard.go:88.121,91.29 2 0
+github.com/muety/wakapi/services/leaderboard.go:126.2,128.12 3 0
+github.com/muety/wakapi/services/leaderboard.go:91.29,92.83 1 0
+github.com/muety/wakapi/services/leaderboard.go:97.3,98.17 2 0
+github.com/muety/wakapi/services/leaderboard.go:103.3,103.85 1 0
+github.com/muety/wakapi/services/leaderboard.go:108.3,108.25 1 0
+github.com/muety/wakapi/services/leaderboard.go:92.83,94.12 2 0
+github.com/muety/wakapi/services/leaderboard.go:98.17,100.12 2 0
+github.com/muety/wakapi/services/leaderboard.go:103.85,105.12 2 0
+github.com/muety/wakapi/services/leaderboard.go:108.25,110.18 2 0
+github.com/muety/wakapi/services/leaderboard.go:115.4,115.23 1 0
+github.com/muety/wakapi/services/leaderboard.go:119.4,119.60 1 0
+github.com/muety/wakapi/services/leaderboard.go:110.18,112.13 2 0
+github.com/muety/wakapi/services/leaderboard.go:115.23,116.13 1 0
+github.com/muety/wakapi/services/leaderboard.go:119.60,121.13 2 0
+github.com/muety/wakapi/services/leaderboard.go:131.77,134.2 2 0
+github.com/muety/wakapi/services/leaderboard.go:136.60,139.52 2 0
+github.com/muety/wakapi/services/leaderboard.go:143.2,144.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:147.2,147.19 1 0
+github.com/muety/wakapi/services/leaderboard.go:139.52,141.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:144.16,146.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:150.154,152.2 1 0
+github.com/muety/wakapi/services/leaderboard.go:154.144,156.2 1 0
+github.com/muety/wakapi/services/leaderboard.go:158.175,161.52 2 0
+github.com/muety/wakapi/services/leaderboard.go:165.2,166.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:170.2,170.18 1 0
+github.com/muety/wakapi/services/leaderboard.go:183.2,184.19 2 0
+github.com/muety/wakapi/services/leaderboard.go:161.52,163.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:166.16,168.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:170.18,172.17 2 0
+github.com/muety/wakapi/services/leaderboard.go:172.17,174.4 1 0
+github.com/muety/wakapi/services/leaderboard.go:174.9,175.31 1 0
+github.com/muety/wakapi/services/leaderboard.go:175.31,176.40 1 0
+github.com/muety/wakapi/services/leaderboard.go:176.40,178.6 1 0
+github.com/muety/wakapi/services/leaderboard.go:187.165,190.52 2 0
+github.com/muety/wakapi/services/leaderboard.go:194.2,195.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:199.2,199.17 1 0
+github.com/muety/wakapi/services/leaderboard.go:210.2,211.19 2 0
+github.com/muety/wakapi/services/leaderboard.go:190.52,192.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:195.16,197.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:199.17,201.17 2 0
+github.com/muety/wakapi/services/leaderboard.go:201.17,203.4 1 0
+github.com/muety/wakapi/services/leaderboard.go:203.9,204.31 1 0
+github.com/muety/wakapi/services/leaderboard.go:204.31,206.5 1 0
+github.com/muety/wakapi/services/leaderboard.go:214.129,216.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:220.2,221.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:225.2,230.8 1 0
+github.com/muety/wakapi/services/leaderboard.go:216.16,218.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:221.16,223.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:233.151,235.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:239.2,240.16 2 0
+github.com/muety/wakapi/services/leaderboard.go:244.2,247.42 3 0
+github.com/muety/wakapi/services/leaderboard.go:259.2,259.19 1 0
+github.com/muety/wakapi/services/leaderboard.go:235.16,237.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:240.16,242.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:247.42,257.3 2 0
+github.com/muety/wakapi/services/leaderboard.go:262.132,264.47 2 0
+github.com/muety/wakapi/services/leaderboard.go:267.2,267.23 1 0
+github.com/muety/wakapi/services/leaderboard.go:270.2,270.10 1 0
+github.com/muety/wakapi/services/leaderboard.go:264.47,266.3 1 0
+github.com/muety/wakapi/services/leaderboard.go:267.23,269.3 1 0
+github.com/muety/wakapi/services/misc.go:24.126,33.2 1 0
+github.com/muety/wakapi/services/misc.go:35.50,37.91 2 0
+github.com/muety/wakapi/services/misc.go:37.91,39.3 1 0
+github.com/muety/wakapi/services/misc.go:42.42,44.16 2 0
+github.com/muety/wakapi/services/misc.go:48.2,52.26 4 0
+github.com/muety/wakapi/services/misc.go:63.2,63.30 1 0
+github.com/muety/wakapi/services/misc.go:44.16,46.3 1 0
+github.com/muety/wakapi/services/misc.go:52.26,53.46 1 0
+github.com/muety/wakapi/services/misc.go:53.46,56.4 2 0
+github.com/muety/wakapi/services/misc.go:56.18,59.4 2 0
+github.com/muety/wakapi/services/misc.go:63.30,64.54 1 0
+github.com/muety/wakapi/services/misc.go:64.54,68.19 1 0
+github.com/muety/wakapi/services/misc.go:72.4,75.19 1 0
+github.com/muety/wakapi/services/misc.go:68.19,70.5 1 0
+github.com/muety/wakapi/services/misc.go:75.19,77.5 1 0
+github.com/muety/wakapi/services/misc.go:78.9,80.4 1 0
+github.com/muety/wakapi/services/misc.go:84.73,86.16 2 0
+github.com/muety/wakapi/services/misc.go:90.2,90.27 1 0
+github.com/muety/wakapi/services/misc.go:86.16,89.3 2 0
diff --git a/go.mod b/go.mod
index 0a118cf..808d77e 100644
--- a/go.mod
+++ b/go.mod
@@ -4,13 +4,12 @@ go 1.18
require (
codeberg.org/Codeberg/avatars v1.0.0
- github.com/duke-git/lancet/v2 v2.1.6
+ github.com/duke-git/lancet/v2 v2.1.10
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.15.0
github.com/emvi/logbuch v1.2.0
- github.com/getsentry/sentry-go v0.14.0
+ github.com/getsentry/sentry-go v0.15.0
github.com/glebarez/sqlite v1.5.0
- github.com/go-co-op/gocron v1.17.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.2.0
@@ -20,27 +19,29 @@ require (
github.com/leandro-lugaresi/hub v1.1.1
github.com/lpar/gzipped/v2 v2.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
+ github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e
github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/robfig/cron/v3 v3.0.1
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/http-swagger v1.3.3
- github.com/swaggo/swag v1.8.6
+ github.com/swaggo/swag v1.8.8
go.uber.org/atomic v1.10.0
- golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
- golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
- gorm.io/driver/mysql v1.4.1
- gorm.io/driver/postgres v1.4.4
- gorm.io/driver/sqlite v1.4.2
- gorm.io/gorm v1.24.0
+ golang.org/x/crypto v0.3.0
+ golang.org/x/sync v0.1.0
+ gorm.io/driver/mysql v1.4.4
+ gorm.io/driver/postgres v1.4.5
+ gorm.io/driver/sqlite v1.4.3
+ gorm.io/gorm v1.24.2
)
require (
- github.com/BurntSushi/toml v1.2.0 // indirect
+ github.com/BurntSushi/toml v1.2.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
- github.com/glebarez/go-sqlite v1.19.1 // indirect
+ github.com/glebarez/go-sqlite v1.19.5 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
@@ -65,18 +66,17 @@ require (
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
- github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
- golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
- golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
- golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
- golang.org/x/text v0.3.7 // indirect
- golang.org/x/tools v0.1.12 // indirect
+ golang.org/x/image v0.1.0 // indirect
+ golang.org/x/net v0.2.0 // indirect
+ golang.org/x/sys v0.2.0 // indirect
+ golang.org/x/text v0.4.0 // indirect
+ golang.org/x/tools v0.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- modernc.org/libc v1.20.3 // indirect
+ modernc.org/libc v1.21.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
- modernc.org/sqlite v1.19.1 // indirect
+ modernc.org/sqlite v1.20.0 // indirect
)
diff --git a/go.sum b/go.sum
index 937577b..c0fe5f2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,15 @@
codeberg.org/Codeberg/avatars v1.0.0 h1:MRx5QxuT/oVCcPvC5rXwgwWKD7hc6J0GnZ0Kl67lYEM=
codeberg.org/Codeberg/avatars v1.0.0/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
-github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
+github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
+github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -16,8 +19,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/duke-git/lancet/v2 v2.1.6 h1:zRWZkK3IAoGnzEonbrkmUP2NyHqtH9qIlW0AaSQrzmY=
-github.com/duke-git/lancet/v2 v2.1.6/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
+github.com/duke-git/lancet/v2 v2.1.10 h1:q6YKhbYg6KChBS+T41e/IhK+sTDPVk2wRhWLTevCeuY=
+github.com/duke-git/lancet/v2 v2.1.10/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
@@ -29,21 +32,13 @@ github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrb
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
-github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
-github.com/getsentry/sentry-go v0.14.0 h1:rlOBkuFZRKKdUnKO+0U3JclRDQKlRu5vVQtkWSQvC70=
-github.com/getsentry/sentry-go v0.14.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
-github.com/glebarez/go-sqlite v1.18.2 h1:ck3PQVaEzzzapP0g7pfhzbB3Jw4rNk+IldLMy/lgdeQ=
-github.com/glebarez/go-sqlite v1.18.2/go.mod h1:/kOdnnt5T0ztYXqBPdjRVM8JwMpFtyAQp1mtRoNxziM=
-github.com/glebarez/go-sqlite v1.19.1 h1:o2XhjyR8CQ2m84+bVz10G0cabmG0tY4sIMiCbrcUTrY=
+github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
+github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M=
-github.com/glebarez/sqlite v1.4.7 h1:tIBxEWLJOPkekuQcwfenNfh13itj9GoVJYxp7GidJAo=
-github.com/glebarez/sqlite v1.4.7/go.mod h1:UY1smw9rBTSGnJE0He8pVRPvlxCP1C8hlB8Z24K8fG4=
+github.com/glebarez/go-sqlite v1.19.5 h1:krEVjICcImFNi+X81GmEkSe/brhzLL3Csbkb/ihi8sI=
+github.com/glebarez/go-sqlite v1.19.5/go.mod h1:IjVxx3ezfL9clKLLSzVgv2sGZe28yIa116YyLTIvp84=
github.com/glebarez/sqlite v1.5.0 h1:+8LAEpmywqresSoGlqjjT+I9m4PseIM3NcerIJ/V7mk=
github.com/glebarez/sqlite v1.5.0/go.mod h1:0wzXzTvfVJIN2GqRhCdMbnYd+m+aH5/QV7B30rM6NgY=
-github.com/go-co-op/gocron v1.17.0 h1:IixLXsti+Qo0wMvmn6Kmjp2csk2ykpkcL+EmHmST18w=
-github.com/go-co-op/gocron v1.17.0/go.mod h1:IpDBSaJOVfFw7hXZuTag3SCSkqazXBBUkbQ1m1aesBs=
-github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@@ -66,8 +61,9 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -81,7 +77,7 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
+github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@@ -102,7 +98,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
@@ -173,20 +168,21 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
+github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf h1:zd7IU9rxVMl2FBwSwiWCUh6s0TkPKgOU6GyVBciNdlo=
+github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf/go.mod h1:eElbcdMwTDc7Wzl7A46IopgkC6a9nV7jOB6Mw8r0waE=
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e h1:bR8DQ4ZfItytLJwRlrLOPUHd5z18V6tECwYQFy8W+8g=
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -223,9 +219,10 @@ github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowN
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
-github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg=
-github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
+github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
+github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -249,18 +246,18 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
-golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
-golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
-golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
+golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
+golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -269,14 +266,14 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
-golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
-golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
-golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -292,20 +289,23 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
-golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
-golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -316,14 +316,14 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
+golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -338,73 +338,58 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
-gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
-gorm.io/driver/mysql v1.4.1 h1:4InA6SOaYtt4yYpV1NF9B2kvUKe9TbvUd1iWrvxnjic=
-gorm.io/driver/mysql v1.4.1/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
-gorm.io/driver/postgres v1.3.10 h1:Fsd+pQpFMGlGxxVMUPJhNo8gG8B1lKtk8QQ4/VZZAJw=
-gorm.io/driver/postgres v1.3.10/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
-gorm.io/driver/postgres v1.4.4 h1:zt1fxJ+C+ajparn0SteEnkoPg0BQ6wOWXEQ99bteAmw=
-gorm.io/driver/postgres v1.4.4/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
-gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
-gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
-gorm.io/driver/sqlite v1.4.2 h1:F6vYJcmR4Cnh0ErLyoY8JSfabBGyR0epIGuhgHJuNws=
-gorm.io/driver/sqlite v1.4.2/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
-gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
-gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
+gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
+gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
+gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
+gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
+gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
-gorm.io/gorm v1.23.10 h1:4Ne9ZbzID9GUxRkllxN4WjJKpsHx8YbKvekVdgyWh24=
-gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
-gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
+gorm.io/gorm v1.24.2 h1:9wR6CFD+G8nOusLdvkZelOEhpJVwwHzpQOUM+REd6U0=
+gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
-modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
-modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
-modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
+modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
-modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
-modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
-modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
+modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
+modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
-modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
-modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
-modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
-modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
-modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
-modernc.org/libc v1.20.0 h1:MEbCfCKpuDC/LRb3HOCM9fZOqnPx8le3kzTJVmUGDbU=
-modernc.org/libc v1.20.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
-modernc.org/libc v1.20.3 h1:BodaDPuUse7taQchAClMmbE/yZp3T2ZBiwCDFyBLEXw=
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
+modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
+modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
+modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0=
-modernc.org/sqlite v1.19.1 h1:8xmS5oLnZtAK//vnd4aTVj8VOeTAccEFOtUnIzfSw+4=
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
+modernc.org/sqlite v1.19.5/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
+modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
+modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
-modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0=
modernc.org/tcl v1.14.0/go.mod h1:gQ7c1YPMvryCHCcmf8acB6VPabE59QBeuRQLL7cTUlM=
+modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
+modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
diff --git a/utils/common.go b/helpers/date.go
similarity index 71%
rename from utils/common.go
rename to helpers/date.go
index 69f5b20..c099d40 100644
--- a/utils/common.go
+++ b/helpers/date.go
@@ -1,9 +1,8 @@
-package utils
+package helpers
import (
- "errors"
+ "fmt"
"github.com/muety/wakapi/config"
- "regexp"
"time"
)
@@ -41,22 +40,10 @@ func FormatDateHuman(date time.Time) string {
return date.Format("Mon, 02 Jan 2006")
}
-func Add(i, j int) int {
- return i + j
-}
-
-func ParseUserAgent(ua string) (string, string, error) {
- re := regexp.MustCompile(`(?iU)^wakatime\/(?:v?[\d+.]+|unset)\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
- groups := re.FindAllStringSubmatch(ua, -1)
- if len(groups) == 0 || len(groups[0]) != 3 {
- return "", "", errors.New("failed to parse user agent string")
- }
- return groups[0][1], groups[0][2], nil
-}
-
-func SubSlice[T any](slice []T, from, to uint) []T {
- if int(to) > len(slice) {
- to = uint(len(slice))
- }
- return slice[from:int(to)]
+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)
}
diff --git a/helpers/helpers.go b/helpers/helpers.go
new file mode 100644
index 0000000..f6c2dbf
--- /dev/null
+++ b/helpers/helpers.go
@@ -0,0 +1,4 @@
+package helpers
+
+// helpers are different from utils in that they contain wakapi-specific utility functions
+// also, helpers may depend on the config package, while utils must be entirely static
diff --git a/helpers/http.go b/helpers/http.go
new file mode 100644
index 0000000..dee40d4
--- /dev/null
+++ b/helpers/http.go
@@ -0,0 +1,30 @@
+package helpers
+
+import (
+ "encoding/json"
+ "errors"
+ "github.com/muety/wakapi/config"
+ "github.com/muety/wakapi/models"
+ "net/http"
+)
+
+func ExtractCookieAuth(r *http.Request, config *config.Config) (username *string, err error) {
+ cookie, err := r.Cookie(models.AuthCookieKey)
+ if err != nil {
+ return nil, errors.New("missing authentication")
+ }
+
+ if err := config.Security.SecureCookie.Decode(models.AuthCookieKey, cookie.Value, &username); err != nil {
+ return nil, errors.New("cookie is invalid")
+ }
+
+ return username, nil
+}
+
+func RespondJSON(w http.ResponseWriter, r *http.Request, status int, object interface{}) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ if err := json.NewEncoder(w).Encode(object); err != nil {
+ config.Log().Request(r).Error("error while writing json response: %v", err)
+ }
+}
diff --git a/utils/summary.go b/helpers/summary.go
similarity index 86%
rename from utils/summary.go
rename to helpers/summary.go
index e2f62e9..842c6d2 100644
--- a/utils/summary.go
+++ b/helpers/summary.go
@@ -1,78 +1,13 @@
-package utils
+package helpers
import (
"errors"
"github.com/muety/wakapi/models"
+ "github.com/muety/wakapi/utils"
"net/http"
"time"
)
-func ParseInterval(interval string) (*models.IntervalKey, error) {
- for _, i := range models.AllIntervals {
- if i.HasAlias(interval) {
- return i, nil
- }
- }
- return nil, errors.New("not a valid interval")
-}
-
-func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
- _, from, to = ResolveIntervalRawTZ(interval, tz)
- return from, to
-}
-
-func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
- parsed, err := ParseInterval(interval)
- if err != nil {
- return err, time.Time{}, time.Time{}
- }
- return ResolveIntervalTZ(parsed, tz)
-}
-
-func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
- now := time.Now().In(tz)
- to = now
-
- switch interval {
- case models.IntervalToday:
- from = BeginOfToday(tz)
- case models.IntervalYesterday:
- from = BeginOfToday(tz).Add(-24 * time.Hour)
- to = BeginOfToday(tz)
- case models.IntervalThisWeek:
- from = BeginOfThisWeek(tz)
- case models.IntervalLastWeek:
- from = BeginOfThisWeek(tz).AddDate(0, 0, -7)
- to = BeginOfThisWeek(tz)
- case models.IntervalThisMonth:
- from = BeginOfThisMonth(tz)
- case models.IntervalLastMonth:
- from = BeginOfThisMonth(tz).AddDate(0, -1, 0)
- to = BeginOfThisMonth(tz)
- case models.IntervalThisYear:
- from = BeginOfThisYear(tz)
- case models.IntervalPast7Days:
- from = now.AddDate(0, 0, -7)
- case models.IntervalPast7DaysYesterday:
- from = BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
- to = BeginOfToday(tz).AddDate(0, 0, -1)
- case models.IntervalPast14Days:
- from = now.AddDate(0, 0, -14)
- case models.IntervalPast30Days:
- from = now.AddDate(0, 0, -30)
- case models.IntervalPast6Months:
- from = now.AddDate(0, -6, 0)
- case models.IntervalPast12Months:
- from = now.AddDate(0, -12, 0)
- case models.IntervalAny:
- from = time.Time{}
- default:
- err = errors.New("invalid interval")
- }
-
- return err, from, to
-}
-
func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
user := extractUser(r)
params := r.URL.Query()
@@ -144,3 +79,69 @@ func extractUser(r *http.Request) *models.User {
}
return nil
}
+
+func ParseInterval(interval string) (*models.IntervalKey, error) {
+ for _, i := range models.AllIntervals {
+ if i.HasAlias(interval) {
+ return i, nil
+ }
+ }
+ return nil, errors.New("not a valid interval")
+}
+
+func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
+ _, from, to = ResolveIntervalRawTZ(interval, tz)
+ return from, to
+}
+
+func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
+ parsed, err := ParseInterval(interval)
+ if err != nil {
+ return err, time.Time{}, time.Time{}
+ }
+ return ResolveIntervalTZ(parsed, tz)
+}
+
+func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
+ now := time.Now().In(tz)
+ to = now
+
+ switch interval {
+ case models.IntervalToday:
+ from = utils.BeginOfToday(tz)
+ case models.IntervalYesterday:
+ from = utils.BeginOfToday(tz).Add(-24 * time.Hour)
+ to = utils.BeginOfToday(tz)
+ case models.IntervalThisWeek:
+ from = utils.BeginOfThisWeek(tz)
+ case models.IntervalLastWeek:
+ from = utils.BeginOfThisWeek(tz).AddDate(0, 0, -7)
+ to = utils.BeginOfThisWeek(tz)
+ case models.IntervalThisMonth:
+ from = utils.BeginOfThisMonth(tz)
+ case models.IntervalLastMonth:
+ from = utils.BeginOfThisMonth(tz).AddDate(0, -1, 0)
+ to = utils.BeginOfThisMonth(tz)
+ case models.IntervalThisYear:
+ from = utils.BeginOfThisYear(tz)
+ case models.IntervalPast7Days:
+ from = now.AddDate(0, 0, -7)
+ case models.IntervalPast7DaysYesterday:
+ from = utils.BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
+ to = utils.BeginOfToday(tz).AddDate(0, 0, -1)
+ case models.IntervalPast14Days:
+ from = now.AddDate(0, 0, -14)
+ case models.IntervalPast30Days:
+ from = now.AddDate(0, 0, -30)
+ case models.IntervalPast6Months:
+ from = now.AddDate(0, -6, 0)
+ case models.IntervalPast12Months:
+ from = now.AddDate(0, -12, 0)
+ case models.IntervalAny:
+ from = time.Time{}
+ default:
+ err = errors.New("invalid interval")
+ }
+
+ return err, from, to
+}
diff --git a/main.go b/main.go
index 51fc4bd..237a135 100644
--- a/main.go
+++ b/main.go
@@ -186,9 +186,9 @@ func main() {
// Schedule background tasks
go aggregationService.Schedule()
- go leaderboardService.ScheduleDefault()
- go miscService.ScheduleCountTotalTime()
+ go leaderboardService.Schedule()
go reportService.Schedule()
+ go miscService.ScheduleCountTotalTime()
routes.Init()
diff --git a/middlewares/authenticate.go b/middlewares/authenticate.go
index 82417b4..06bc41b 100644
--- a/middlewares/authenticate.go
+++ b/middlewares/authenticate.go
@@ -2,6 +2,7 @@ package middlewares
import (
"fmt"
+ "github.com/muety/wakapi/helpers"
"net/http"
"strings"
@@ -121,7 +122,7 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKeyQuery(r *http.Request) (*mode
}
func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.User, error) {
- username, err := utils.ExtractCookieAuth(r, m.config)
+ username, err := helpers.ExtractCookieAuth(r, m.config)
if err != nil {
return nil, err
}
diff --git a/models/compat/shields/v1/badge.go b/models/compat/shields/v1/badge.go
index 39f6dc1..6ef754d 100644
--- a/models/compat/shields/v1/badge.go
+++ b/models/compat/shields/v1/badge.go
@@ -1,8 +1,8 @@
package v1
import (
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
- "github.com/muety/wakapi/utils"
)
// https://shields.io/endpoint
@@ -23,7 +23,7 @@ func NewBadgeDataFrom(summary *models.Summary) *BadgeData {
return &BadgeData{
SchemaVersion: 1,
Label: defaultLabel,
- Message: utils.FmtWakatimeDuration(summary.TotalTime()),
+ Message: helpers.FmtWakatimeDuration(summary.TotalTime()),
Color: defaultColor,
}
}
diff --git a/models/compat/wakatime/v1/all_time.go b/models/compat/wakatime/v1/all_time.go
index 8d200d4..55dc68f 100644
--- a/models/compat/wakatime/v1/all_time.go
+++ b/models/compat/wakatime/v1/all_time.go
@@ -1,8 +1,8 @@
package v1
import (
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
- "github.com/muety/wakapi/utils"
"time"
)
@@ -33,13 +33,13 @@ func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
return &AllTimeViewModel{
Data: &AllTimeData{
TotalSeconds: float32(total.Seconds()),
- Text: utils.FmtWakatimeDuration(total),
+ Text: helpers.FmtWakatimeDuration(total),
IsUpToDate: true,
Range: &AllTimeRange{
End: summary.ToTime.T().Format(time.RFC3339),
- EndDate: utils.FormatDate(summary.ToTime.T()),
+ EndDate: helpers.FormatDate(summary.ToTime.T()),
Start: summary.FromTime.T().Format(time.RFC3339),
- StartDate: utils.FormatDate(summary.FromTime.T()),
+ StartDate: helpers.FormatDate(summary.FromTime.T()),
Timezone: tzName,
},
},
diff --git a/models/compat/wakatime/v1/summaries.go b/models/compat/wakatime/v1/summaries.go
index 7716e3a..cbbffa4 100644
--- a/models/compat/wakatime/v1/summaries.go
+++ b/models/compat/wakatime/v1/summaries.go
@@ -2,8 +2,8 @@ package v1
import (
"fmt"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
- "github.com/muety/wakapi/utils"
"math"
"sync"
"time"
@@ -96,7 +96,7 @@ func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
Decimal: fmt.Sprintf("%.2f", totalHrs),
Digital: fmt.Sprintf("%d:%d", int(totalHrs), int(totalMins)),
Seconds: totalSecs,
- Text: utils.FmtWakatimeDuration(totalTime),
+ Text: helpers.FmtWakatimeDuration(totalTime),
},
}
}
@@ -119,7 +119,7 @@ func newDataFrom(s *models.Summary) *SummariesData {
Digital: fmt.Sprintf("%d:%d", totalHrs, totalMins),
Hours: totalHrs,
Minutes: totalMins,
- Text: utils.FmtWakatimeDuration(total),
+ Text: helpers.FmtWakatimeDuration(total),
TotalSeconds: total.Seconds(),
},
Range: &SummariesRange{
@@ -201,7 +201,7 @@ func convertEntry(e *models.SummaryItem, entityTotal time.Duration) *SummariesEn
Name: e.Key,
Percent: percentage,
Seconds: secs,
- Text: utils.FmtWakatimeDuration(total),
+ Text: helpers.FmtWakatimeDuration(total),
TotalSeconds: total.Seconds(),
}
}
diff --git a/models/metrics/gauge_metric.go b/models/metrics/gauge_metric.go
new file mode 100644
index 0000000..651281b
--- /dev/null
+++ b/models/metrics/gauge_metric.go
@@ -0,0 +1,22 @@
+package metrics
+
+import "fmt"
+
+type GaugeMetric struct {
+ Name string
+ Value int64
+ Desc string
+ Labels Labels
+}
+
+func (c GaugeMetric) Key() string {
+ return c.Name
+}
+
+func (c GaugeMetric) Print() string {
+ return fmt.Sprintf("%s%s %d", c.Name, c.Labels.Print(), c.Value)
+}
+
+func (c GaugeMetric) Header() string {
+ return fmt.Sprintf("# HELP %s %s\n# TYPE %s gauge", c.Name, c.Desc, c.Name)
+}
diff --git a/routes/api/diagnostics.go b/routes/api/diagnostics.go
index 219204d..5b3b8de 100644
--- a/routes/api/diagnostics.go
+++ b/routes/api/diagnostics.go
@@ -2,14 +2,13 @@ package api
import (
"encoding/json"
+ "github.com/muety/wakapi/helpers"
"net/http"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
- "github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
-
"github.com/muety/wakapi/models"
+ "github.com/muety/wakapi/services"
)
type DiagnosticsApiHandler struct {
@@ -55,5 +54,5 @@ func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
return
}
- utils.RespondJSON(w, r, http.StatusCreated, struct{}{})
+ helpers.RespondJSON(w, r, http.StatusCreated, struct{}{})
}
diff --git a/routes/api/heartbeat.go b/routes/api/heartbeat.go
index 58574d8..c923d2c 100644
--- a/routes/api/heartbeat.go
+++ b/routes/api/heartbeat.go
@@ -1,6 +1,7 @@
package api
import (
+ "github.com/muety/wakapi/helpers"
"net/http"
"github.com/gorilla/mux"
@@ -120,7 +121,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
defer func() {}()
- utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
+ helpers.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
}
// construct weird response format (see https://github.com/wakatime/wakatime/blob/2e636d389bf5da4e998e05d5285a96ce2c181e3d/wakatime/api.py#L288)
diff --git a/routes/api/metrics.go b/routes/api/metrics.go
index 7fe98bc..404c1bd 100644
--- a/routes/api/metrics.go
+++ b/routes/api/metrics.go
@@ -5,13 +5,13 @@ import (
"github.com/emvi/logbuch"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
mm "github.com/muety/wakapi/models/metrics"
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
"net/http"
"runtime"
"sort"
@@ -37,6 +37,9 @@ const (
DescAdminTotalUsers = "Total number of registered users."
DescAdminActiveUsers = "Number of active users."
+ DescJobQueueEnqueued = "Number of jobs currently enqueued"
+ DescJobQueueTotalFinished = "Total number of processed jobs"
+
DescMemAllocTotal = "Total number of bytes allocated for heap"
DescMemSysTotal = "Total number of bytes obtained from the OS"
DescGoroutines = "Total number of running goroutines"
@@ -126,7 +129,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
return nil, err
}
- from, to := utils.MustResolveIntervalRawTZ("today", user.TZ())
+ from, to := helpers.MustResolveIntervalRawTZ("today", user.TZ())
summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve, nil, false)
if err != nil {
@@ -142,21 +145,21 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
// User Metrics
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_cumulative_seconds_total",
Desc: DescAllTime,
Value: int64(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_seconds_total",
Desc: DescTotal,
Value: int64(summaryToday.TotalTime().Seconds()),
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_heartbeats_total",
Desc: DescHeartbeats,
Value: int64(heartbeatCount),
@@ -164,7 +167,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
})
for _, p := range summaryToday.Projects {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_project_seconds_total",
Desc: DescProjects,
Value: int64(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
@@ -173,7 +176,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
}
for _, l := range summaryToday.Languages {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_language_seconds_total",
Desc: DescLanguages,
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
@@ -182,7 +185,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
}
for _, e := range summaryToday.Editors {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_editor_seconds_total",
Desc: DescEditors,
Value: int64(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
@@ -191,7 +194,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
}
for _, o := range summaryToday.OperatingSystems {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_operating_system_seconds_total",
Desc: DescOperatingSystems,
Value: int64(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
@@ -200,7 +203,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
}
for _, m := range summaryToday.Machines {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_machine_seconds_total",
Desc: DescMachines,
Value: int64(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
@@ -209,7 +212,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
}
for _, m := range summaryToday.Labels {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_label_seconds_total",
Desc: DescLabels,
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLabel, m.Key).Seconds()),
@@ -221,21 +224,21 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_goroutines_total",
Desc: DescGoroutines,
Value: int64(runtime.NumGoroutine()),
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_mem_alloc_total",
Desc: DescMemAllocTotal,
Value: int64(memStats.Alloc),
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_mem_sys_total",
Desc: DescMemSysTotal,
Value: int64(memStats.Sys),
@@ -248,13 +251,30 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
logbuch.Warn("failed to get database size (%v)", err)
}
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_db_total_bytes",
Desc: DescDatabaseSize,
Value: dbSize,
Labels: []mm.Label{},
})
+ // Miscellaneous
+ for _, qm := range conf.GetQueueMetrics() {
+ metrics = append(metrics, &mm.GaugeMetric{
+ Name: MetricsPrefix + "_queue_jobs_enqueued",
+ Value: int64(qm.EnqueuedJobs),
+ Desc: DescJobQueueEnqueued,
+ Labels: []mm.Label{{Key: "queue", Value: qm.Queue}},
+ })
+
+ metrics = append(metrics, &mm.CounterMetric{
+ Name: MetricsPrefix + "_queue_jobs_total_finished",
+ Value: int64(qm.FinishedJobs),
+ Desc: DescJobQueueTotalFinished,
+ Labels: []mm.Label{{Key: "queue", Value: qm.Queue}},
+ })
+ }
+
return &metrics, nil
}
@@ -281,28 +301,28 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
return nil, err
}
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_admin_seconds_total",
Desc: DescAdminTotalTime,
Value: int64(totalSeconds),
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_admin_heartbeats_total",
Desc: DescAdminTotalHeartbeats,
Value: totalHeartbeats,
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_admin_users_total",
Desc: DescAdminTotalUsers,
Value: totalUsers,
Labels: []mm.Label{},
})
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_admin_users_active_total",
Desc: DescAdminActiveUsers,
Value: int64(len(activeUsers)),
@@ -318,7 +338,7 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
}
for _, uc := range userCounts {
- metrics = append(metrics, &mm.CounterMetric{
+ metrics = append(metrics, &mm.GaugeMetric{
Name: MetricsPrefix + "_admin_user_heartbeats_total",
Desc: DescAdminUserHeartbeats,
Value: uc.Count,
diff --git a/routes/api/summary.go b/routes/api/summary.go
index ac013e8..a5926e8 100644
--- a/routes/api/summary.go
+++ b/routes/api/summary.go
@@ -1,6 +1,7 @@
package api
import (
+ "github.com/muety/wakapi/helpers"
routeutils "github.com/muety/wakapi/routes/utils"
"net/http"
@@ -8,7 +9,6 @@ import (
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
)
type SummaryApiHandler struct {
@@ -58,5 +58,5 @@ func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) {
return
}
- utils.RespondJSON(w, r, http.StatusOK, summary)
+ helpers.RespondJSON(w, r, http.StatusOK, summary)
}
diff --git a/routes/compat/shields/v1/badge.go b/routes/compat/shields/v1/badge.go
index 854d9d2..600cb4b 100644
--- a/routes/compat/shields/v1/badge.go
+++ b/routes/compat/shields/v1/badge.go
@@ -2,6 +2,7 @@ package v1
import (
"fmt"
+ "github.com/muety/wakapi/helpers"
routeutils "github.com/muety/wakapi/routes/utils"
"net/http"
"time"
@@ -11,7 +12,6 @@ import (
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/shields/v1"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache"
)
@@ -63,7 +63,7 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
if cacheResult, ok := h.cache.Get(cacheKey); ok {
- utils.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
+ helpers.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
return
}
@@ -83,11 +83,11 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
vm := v1.NewBadgeDataFrom(summary)
h.cache.SetDefault(cacheKey, vm)
- utils.RespondJSON(w, r, http.StatusOK, vm)
+ helpers.RespondJSON(w, r, http.StatusOK, vm)
}
func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.IntervalKey, filters *models.Filters) (*models.Summary, error, int) {
- err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
+ err, from, to := helpers.ResolveIntervalTZ(interval, user.TZ())
if err != nil {
return nil, err, http.StatusBadRequest
}
diff --git a/routes/compat/wakatime/v1/all_time.go b/routes/compat/wakatime/v1/all_time.go
index 8da804e..431eaec 100644
--- a/routes/compat/wakatime/v1/all_time.go
+++ b/routes/compat/wakatime/v1/all_time.go
@@ -3,12 +3,12 @@ package v1
import (
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
"net/http"
"time"
)
@@ -50,7 +50,7 @@ func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
return // response was already sent by util function
}
- summary, err, status := h.loadUserSummary(user, utils.ParseSummaryFilters(r))
+ summary, err, status := h.loadUserSummary(user, helpers.ParseSummaryFilters(r))
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))
@@ -58,7 +58,7 @@ func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
}
vm := v1.NewAllTimeFrom(summary)
- utils.RespondJSON(w, r, http.StatusOK, vm)
+ helpers.RespondJSON(w, r, http.StatusOK, vm)
}
func (h *AllTimeHandler) loadUserSummary(user *models.User, filters *models.Filters) (*models.Summary, error, int) {
diff --git a/routes/compat/wakatime/v1/heartbeat.go b/routes/compat/wakatime/v1/heartbeat.go
index c21f797..1a30bad 100644
--- a/routes/compat/wakatime/v1/heartbeat.go
+++ b/routes/compat/wakatime/v1/heartbeat.go
@@ -2,6 +2,7 @@ package v1
import (
"github.com/duke-git/lancet/v2/datetime"
+ "github.com/muety/wakapi/helpers"
"net/http"
"time"
@@ -11,7 +12,6 @@ import (
wakatime "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
)
type HeartbeatsResult struct {
@@ -82,5 +82,5 @@ func (h *HeartbeatHandler) Get(w http.ResponseWriter, r *http.Request) {
End: rangeTo.UTC().Format(time.RFC3339),
Timezone: timezone.String(),
}
- utils.RespondJSON(w, r, http.StatusOK, res)
+ helpers.RespondJSON(w, r, http.StatusOK, res)
}
diff --git a/routes/compat/wakatime/v1/projects.go b/routes/compat/wakatime/v1/projects.go
index 5e2ebb6..acdc12e 100644
--- a/routes/compat/wakatime/v1/projects.go
+++ b/routes/compat/wakatime/v1/projects.go
@@ -1,6 +1,7 @@
package v1
import (
+ "github.com/muety/wakapi/helpers"
"net/http"
"strings"
@@ -11,7 +12,6 @@ import (
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
)
type ProjectsHandler struct {
@@ -70,5 +70,5 @@ func (h *ProjectsHandler) Get(w http.ResponseWriter, r *http.Request) {
}
vm := &v1.ProjectsViewModel{Data: projects}
- utils.RespondJSON(w, r, http.StatusOK, vm)
+ helpers.RespondJSON(w, r, http.StatusOK, vm)
}
diff --git a/routes/compat/wakatime/v1/stats.go b/routes/compat/wakatime/v1/stats.go
index b625eaa..fa03c1e 100644
--- a/routes/compat/wakatime/v1/stats.go
+++ b/routes/compat/wakatime/v1/stats.go
@@ -1,6 +1,7 @@
package v1
import (
+ "github.com/muety/wakapi/helpers"
"net/http"
"time"
@@ -10,7 +11,6 @@ import (
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
)
type StatsHandler struct {
@@ -79,7 +79,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
rangeParam = (*models.IntervalPast7Days)[0]
}
- err, rangeFrom, rangeTo := utils.ResolveIntervalRawTZ(rangeParam, requestedUser.TZ())
+ err, rangeFrom, rangeTo := helpers.ResolveIntervalRawTZ(rangeParam, requestedUser.TZ())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("invalid range"))
@@ -94,7 +94,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
return
}
- summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, utils.ParseSummaryFilters(r))
+ summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, helpers.ParseSummaryFilters(r))
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))
@@ -120,7 +120,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
stats.Data.Machines = nil
}
- utils.RespondJSON(w, r, http.StatusOK, stats)
+ helpers.RespondJSON(w, r, http.StatusOK, stats)
}
func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time, filters *models.Filters) (*models.Summary, error, int) {
diff --git a/routes/compat/wakatime/v1/statusbar.go b/routes/compat/wakatime/v1/statusbar.go
index 9fade55..a71a791 100644
--- a/routes/compat/wakatime/v1/statusbar.go
+++ b/routes/compat/wakatime/v1/statusbar.go
@@ -1,6 +1,7 @@
package v1
import (
+ "github.com/muety/wakapi/helpers"
"net/http"
"time"
@@ -11,7 +12,6 @@ import (
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
)
type StatusBarViewModel struct {
@@ -65,7 +65,7 @@ func (h *StatusBarHandler) Get(w http.ResponseWriter, r *http.Request) {
rangeParam = (*models.IntervalToday)[0]
}
- err, rangeFrom, rangeTo := utils.ResolveIntervalRawTZ(rangeParam, user.TZ())
+ err, rangeFrom, rangeTo := helpers.ResolveIntervalRawTZ(rangeParam, user.TZ())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("invalid range"))
@@ -79,7 +79,7 @@ func (h *StatusBarHandler) Get(w http.ResponseWriter, r *http.Request) {
return
}
summariesView := v1.NewSummariesFrom([]*models.Summary{summary})
- utils.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
+ helpers.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
CachedAt: time.Now(),
Data: *summariesView.Data[0],
})
diff --git a/routes/compat/wakatime/v1/summaries.go b/routes/compat/wakatime/v1/summaries.go
index 62be34c..5a45b7b 100644
--- a/routes/compat/wakatime/v1/summaries.go
+++ b/routes/compat/wakatime/v1/summaries.go
@@ -3,6 +3,7 @@ package v1
import (
"errors"
"github.com/duke-git/lancet/v2/datetime"
+ "github.com/muety/wakapi/helpers"
"net/http"
"strings"
"time"
@@ -76,7 +77,7 @@ func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
}
vm := v1.NewSummariesFrom(summaries)
- utils.RespondJSON(w, r, http.StatusOK, vm)
+ helpers.RespondJSON(w, r, http.StatusOK, vm)
}
func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary, error, int) {
@@ -94,24 +95,24 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
var start, end time.Time
if rangeParam != "" {
// range param takes precedence
- if err, parsedFrom, parsedTo := utils.ResolveIntervalRawTZ(rangeParam, timezone); err == nil {
+ if err, parsedFrom, parsedTo := helpers.ResolveIntervalRawTZ(rangeParam, timezone); err == nil {
start, end = parsedFrom, parsedTo
} else {
return nil, errors.New("invalid 'range' parameter"), http.StatusBadRequest
}
- } else if err, parsedFrom, parsedTo := utils.ResolveIntervalRawTZ(startParam, timezone); err == nil && startParam == endParam {
+ } else if err, parsedFrom, parsedTo := helpers.ResolveIntervalRawTZ(startParam, timezone); err == nil && startParam == endParam {
// also accept start param to be a range param
start, end = parsedFrom, parsedTo
} else {
// eventually, consider start and end params a date
var err error
- start, err = utils.ParseDateTimeTZ(strings.Replace(startParam, " ", "+", 1), timezone)
+ start, err = helpers.ParseDateTimeTZ(strings.Replace(startParam, " ", "+", 1), timezone)
if err != nil {
return nil, errors.New("missing required 'start' parameter"), http.StatusBadRequest
}
- end, err = utils.ParseDateTimeTZ(strings.Replace(endParam, " ", "+", 1), timezone)
+ end, err = helpers.ParseDateTimeTZ(strings.Replace(endParam, " ", "+", 1), timezone)
if err != nil {
return nil, errors.New("missing required 'end' parameter"), http.StatusBadRequest
}
@@ -133,7 +134,7 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
summaries := make([]*models.Summary, len(intervals))
// filtering
- filters := utils.ParseSummaryFilters(r)
+ filters := helpers.ParseSummaryFilters(r)
for i, interval := range intervals {
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, filters, end.After(time.Now()))
diff --git a/routes/compat/wakatime/v1/users.go b/routes/compat/wakatime/v1/users.go
index 72601bc..b1ae7ab 100644
--- a/routes/compat/wakatime/v1/users.go
+++ b/routes/compat/wakatime/v1/users.go
@@ -1,6 +1,7 @@
package v1
import (
+ "github.com/muety/wakapi/helpers"
"net/http"
"github.com/gorilla/mux"
@@ -9,7 +10,6 @@ import (
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
)
type UsersHandler struct {
@@ -56,5 +56,5 @@ func (h *UsersHandler) Get(w http.ResponseWriter, r *http.Request) {
conf.Log().Request(r).Error("%v", err)
}
- utils.RespondJSON(w, r, http.StatusOK, v1.UserViewModel{Data: user})
+ helpers.RespondJSON(w, r, http.StatusOK, v1.UserViewModel{Data: user})
}
diff --git a/routes/routes.go b/routes/routes.go
index 1a8d508..7b281b0 100644
--- a/routes/routes.go
+++ b/routes/routes.go
@@ -2,6 +2,7 @@ package routes
import (
"fmt"
+ "github.com/muety/wakapi/helpers"
"html/template"
"net/http"
"strings"
@@ -24,16 +25,16 @@ func Init() {
func DefaultTemplateFuncs() template.FuncMap {
return template.FuncMap{
"json": utils.Json,
- "date": utils.FormatDateHuman,
- "datetime": utils.FormatDateTimeHuman,
- "simpledate": utils.FormatDate,
- "simpledatetime": utils.FormatDateTime,
- "duration": utils.FmtWakatimeDuration,
+ "date": helpers.FormatDateHuman,
+ "datetime": helpers.FormatDateTimeHuman,
+ "simpledate": helpers.FormatDate,
+ "simpledatetime": helpers.FormatDateTime,
+ "duration": helpers.FmtWakatimeDuration,
"floordate": datetime.BeginOfDay,
"ceildate": utils.CeilDate,
"title": strings.Title,
"join": strings.Join,
- "add": utils.Add,
+ "add": add,
"capitalize": utils.Capitalize,
"lower": strings.ToLower,
"toRunes": utils.ToRunes,
@@ -106,3 +107,7 @@ func loadTemplates() {
func defaultErrorRedirectTarget() string {
return fmt.Sprintf("%s/?error=unauthorized", config.Get().Server.BasePath)
}
+
+func add(i, j int) int {
+ return i + j
+}
diff --git a/routes/settings.go b/routes/settings.go
index bf2066f..5e8ad9c 100644
--- a/routes/settings.go
+++ b/routes/settings.go
@@ -661,7 +661,7 @@ func (h *SettingsHandler) regenerateSummaries(user *models.User) error {
return err
}
- if err := h.aggregationSrvc.Run(datastructure.NewSet(user.ID)); err != nil {
+ if err := h.aggregationSrvc.AggregateSummaries(datastructure.NewSet(user.ID)); err != nil {
logbuch.Error("failed to regenerate summaries: %v", err)
return err
}
diff --git a/routes/summary.go b/routes/summary.go
index 268da1d..709c4c7 100644
--- a/routes/summary.go
+++ b/routes/summary.go
@@ -3,6 +3,7 @@ package routes
import (
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models/view"
su "github.com/muety/wakapi/routes/utils"
@@ -47,7 +48,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
r.URL.RawQuery = q.Encode()
}
- summaryParams, _ := utils.ParseSummaryParams(r)
+ summaryParams, _ := helpers.ParseSummaryParams(r)
summary, err, status := su.LoadUserSummary(h.summarySrvc, r)
if err != nil {
w.WriteHeader(status)
diff --git a/routes/utils/badge_utils.go b/routes/utils/badge_utils.go
index 327296b..5b012b6 100644
--- a/routes/utils/badge_utils.go
+++ b/routes/utils/badge_utils.go
@@ -2,8 +2,8 @@ package utils
import (
"errors"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
- "github.com/muety/wakapi/utils"
"net/http"
"regexp"
)
@@ -31,12 +31,12 @@ func GetBadgeParams(r *http.Request, requestedUser *models.User) (*models.KeyedI
var intervalKey = models.IntervalPast30Days
if groups := intervalReg.FindStringSubmatch(r.URL.Path); len(groups) > 1 {
- if i, err := utils.ParseInterval(groups[1]); err == nil {
+ if i, err := helpers.ParseInterval(groups[1]); err == nil {
intervalKey = i
}
}
- _, rangeFrom, rangeTo := utils.ResolveIntervalTZ(intervalKey, requestedUser.TZ())
+ _, rangeFrom, rangeTo := helpers.ResolveIntervalTZ(intervalKey, requestedUser.TZ())
interval := &models.KeyedInterval{
Interval: models.Interval{Start: rangeFrom, End: rangeTo},
Key: intervalKey,
diff --git a/routes/utils/summary_utils.go b/routes/utils/summary_utils.go
index d07304c..dfee8bf 100644
--- a/routes/utils/summary_utils.go
+++ b/routes/utils/summary_utils.go
@@ -1,14 +1,14 @@
package utils
import (
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
- "github.com/muety/wakapi/utils"
"net/http"
)
func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summary, error, int) {
- summaryParams, err := utils.ParseSummaryParams(r)
+ summaryParams, err := helpers.ParseSummaryParams(r)
if err != nil {
return nil, err, http.StatusBadRequest
}
diff --git a/services/aggregation.go b/services/aggregation.go
index f11653a..0fec34b 100644
--- a/services/aggregation.go
+++ b/services/aggregation.go
@@ -4,12 +4,11 @@ import (
"errors"
datastructure "github.com/duke-git/lancet/v2/datastructure/set"
"github.com/emvi/logbuch"
+ "github.com/muety/artifex/v2"
"github.com/muety/wakapi/config"
- "runtime"
"sync"
"time"
- "github.com/go-co-op/gocron"
"github.com/muety/wakapi/models"
)
@@ -25,6 +24,8 @@ type AggregationService struct {
summaryService ISummaryService
heartbeatService IHeartbeatService
inProgress datastructure.Set[string]
+ queueDefault *artifex.Dispatcher
+ queueWorkers *artifex.Dispatcher
}
func NewAggregationService(userService IUserService, summaryService ISummaryService, heartbeatService IHeartbeatService) *AggregationService {
@@ -34,6 +35,8 @@ func NewAggregationService(userService IUserService, summaryService ISummaryServ
summaryService: summaryService,
heartbeatService: heartbeatService,
inProgress: datastructure.NewSet[string](),
+ queueDefault: config.GetDefaultQueue(),
+ queueWorkers: config.GetQueue(config.QueueProcessing),
}
}
@@ -45,58 +48,23 @@ type AggregationJob struct {
// Schedule a job to (re-)generate summaries every day shortly after midnight
func (srv *AggregationService) Schedule() {
- s := gocron.NewScheduler(time.Local)
- s.Every(1).Day().At(srv.config.App.AggregationTime).WaitForSchedule().Do(srv.Run, datastructure.NewSet[string]())
- s.StartBlocking()
+ logbuch.Info("scheduling summary aggregation")
+
+ if _, err := srv.queueDefault.DispatchCron(func() {
+ if err := srv.AggregateSummaries(datastructure.NewSet[string]()); err != nil {
+ config.Log().Error("failed to generate summaries, %v", err)
+ }
+ }, srv.config.App.GetAggregationTimeCron()); err != nil {
+ config.Log().Error("failed to schedule summary generation, %v", err)
+ }
}
-func (srv *AggregationService) Run(userIds datastructure.Set[string]) error {
+func (srv *AggregationService) AggregateSummaries(userIds datastructure.Set[string]) error {
if err := srv.lockUsers(userIds); err != nil {
return err
}
defer srv.unlockUsers(userIds)
- jobs := make(chan *AggregationJob)
- summaries := make(chan *models.Summary)
-
- for i := 0; i < runtime.NumCPU(); i++ {
- go srv.summaryWorker(jobs, summaries)
- }
-
- for i := 0; i < int(srv.config.Db.MaxConn); i++ {
- go srv.persistWorker(summaries)
- }
-
- // don't leak open channels
- go func(c1 chan *AggregationJob, c2 chan *models.Summary) {
- defer close(c1)
- defer close(c2)
- time.Sleep(1 * time.Hour)
- }(jobs, summaries)
-
- return srv.trigger(jobs, userIds)
-}
-
-func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
- for job := range jobs {
- if summary, err := srv.summaryService.Summarize(job.From, job.To, &models.User{ID: job.UserID}, nil); err != nil {
- config.Log().Error("failed to generate summary (%v, %v, %s) - %v", job.From, job.To, job.UserID, err)
- } else {
- logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID)
- summaries <- summary
- }
- }
-}
-
-func (srv *AggregationService) persistWorker(summaries <-chan *models.Summary) {
- for summary := range summaries {
- if err := srv.summaryService.Insert(summary); err != nil {
- config.Log().Error("failed to save summary (%v, %v, %s) - %v", summary.UserID, summary.FromTime, summary.ToTime, err)
- }
- }
-}
-
-func (srv *AggregationService) trigger(jobs chan<- *AggregationJob, userIds datastructure.Set[string]) error {
logbuch.Info("generating summaries")
// Get a map from user ids to the time of their latest summary or nil if none exists yet
@@ -119,6 +87,20 @@ func (srv *AggregationService) trigger(jobs chan<- *AggregationJob, userIds data
firstUserHeartbeatLookup[e.User] = e.Time
}
+ // Dispatch summary generation jobs
+ jobs := make(chan *AggregationJob)
+ defer close(jobs)
+ go func() {
+ for jobRef := range jobs {
+ job := *jobRef
+ if err := srv.queueWorkers.Dispatch(func() {
+ srv.process(job)
+ }); err != nil {
+ config.Log().Error("failed to dispatch summary generation job for user '%s'", job.UserID)
+ }
+ }
+ }()
+
// Generate summary aggregation jobs
for _, e := range lastUserSummaryTimes {
if userIds != nil && !userIds.IsEmpty() && !userIds.Contain(e.User) {
@@ -141,24 +123,15 @@ func (srv *AggregationService) trigger(jobs chan<- *AggregationJob, userIds data
return nil
}
-func (srv *AggregationService) lockUsers(userIds datastructure.Set[string]) error {
- aggregationLock.Lock()
- defer aggregationLock.Unlock()
- for uid := range userIds {
- if srv.inProgress.Contain(uid) {
- return errors.New("aggregation already in progress for at least of the request users")
+func (srv *AggregationService) process(job AggregationJob) {
+ if summary, err := srv.summaryService.Summarize(job.From, job.To, &models.User{ID: job.UserID}, nil); err != nil {
+ config.Log().Error("failed to generate summary (%v, %v, %s) - %v", job.From, job.To, job.UserID, err)
+ } else {
+ logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID)
+ if err := srv.summaryService.Insert(summary); err != nil {
+ config.Log().Error("failed to save summary (%v, %v, %s) - %v", summary.UserID, summary.FromTime, summary.ToTime, err)
}
}
- srv.inProgress = srv.inProgress.Union(userIds)
- return nil
-}
-
-func (srv *AggregationService) unlockUsers(userIds datastructure.Set[string]) {
- aggregationLock.Lock()
- defer aggregationLock.Unlock()
- for uid := range userIds {
- srv.inProgress.Delete(uid)
- }
}
func generateUserJobs(userId string, from time.Time, jobs chan<- *AggregationJob) {
@@ -189,6 +162,26 @@ func generateUserJobs(userId string, from time.Time, jobs chan<- *AggregationJob
}
}
+func (srv *AggregationService) lockUsers(userIds datastructure.Set[string]) error {
+ aggregationLock.Lock()
+ defer aggregationLock.Unlock()
+ for uid := range userIds {
+ if srv.inProgress.Contain(uid) {
+ return errors.New("aggregation already in progress for at least of the request users")
+ }
+ }
+ srv.inProgress = srv.inProgress.Union(userIds)
+ return nil
+}
+
+func (srv *AggregationService) unlockUsers(userIds datastructure.Set[string]) {
+ aggregationLock.Lock()
+ defer aggregationLock.Unlock()
+ for uid := range userIds {
+ srv.inProgress.Delete(uid)
+ }
+}
+
func getStartOfToday() time.Time {
now := time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 1, now.Location())
diff --git a/services/imports/wakatime.go b/services/imports/wakatime.go
index 94dd354..626452f 100644
--- a/services/imports/wakatime.go
+++ b/services/imports/wakatime.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"github.com/duke-git/lancet/v2/datetime"
+ "github.com/muety/artifex/v2"
"github.com/muety/wakapi/utils"
"net/http"
"strings"
@@ -32,19 +33,23 @@ const (
type WakatimeHeartbeatImporter struct {
ApiKey string
httpClient *http.Client
+ queue *artifex.Dispatcher
}
func NewWakatimeHeartbeatImporter(apiKey string) *WakatimeHeartbeatImporter {
return &WakatimeHeartbeatImporter{
ApiKey: apiKey,
httpClient: &http.Client{Timeout: 10 * time.Second},
+ queue: config.GetQueue(config.QueueImports),
}
}
func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time, maxTo time.Time) <-chan *models.Heartbeat {
out := make(chan *models.Heartbeat)
- go func(user *models.User, out chan *models.Heartbeat) {
+ process := func(user *models.User, minFrom time.Time, maxTo time.Time, out chan *models.Heartbeat) {
+ logbuch.Info("running wakatime import for user '%s'", user.ID)
+
baseUrl := user.WakaTimeURL(config.WakatimeApiUrl)
startDate, endDate, err := w.fetchRange(baseUrl)
@@ -109,7 +114,14 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time,
}
}(d)
}
- }(user, out)
+ }
+
+ logbuch.Info("scheduling wakatime import for user '%s'", user.ID)
+ if err := w.queue.Dispatch(func() {
+ process(user, minFrom, maxTo, out)
+ }); err != nil {
+ config.Log().Error("failed to dispatch wakatime import job for user '%s', %v", user.ID, err)
+ }
return out
}
diff --git a/services/leaderboard.go b/services/leaderboard.go
index 0dd2cb1..ec74c58 100644
--- a/services/leaderboard.go
+++ b/services/leaderboard.go
@@ -2,12 +2,12 @@ package services
import (
"github.com/emvi/logbuch"
- "github.com/go-co-op/gocron"
"github.com/leandro-lugaresi/hub"
+ "github.com/muety/artifex/v2"
"github.com/muety/wakapi/config"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/repositories"
- "github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache"
"reflect"
"strconv"
@@ -22,6 +22,8 @@ type LeaderboardService struct {
repository repositories.ILeaderboardRepository
summaryService ISummaryService
userService IUserService
+ queueDefault *artifex.Dispatcher
+ queueWorkers *artifex.Dispatcher
}
func NewLeaderboardService(leaderboardRepo repositories.ILeaderboardRepository, summaryService ISummaryService, userService IUserService) *LeaderboardService {
@@ -32,6 +34,8 @@ func NewLeaderboardService(leaderboardRepo repositories.ILeaderboardRepository,
repository: leaderboardRepo,
summaryService: summaryService,
userService: userService,
+ queueDefault: config.GetDefaultQueue(),
+ queueWorkers: config.GetQueue(config.QueueProcessing),
}
onUserUpdate := srv.eventBus.Subscribe(0, config.EventUserUpdate)
@@ -48,7 +52,7 @@ func NewLeaderboardService(leaderboardRepo repositories.ILeaderboardRepository,
if user.PublicLeaderboard && !exists {
logbuch.Info("generating leaderboard for '%s' after settings update", user.ID)
- srv.Run([]*models.User{user}, models.IntervalPast7Days, []uint8{models.SummaryLanguage})
+ srv.ComputeLeaderboard([]*models.User{user}, models.IntervalPast7Days, []uint8{models.SummaryLanguage})
} else if !user.PublicLeaderboard && exists {
logbuch.Info("clearing leaderboard for '%s' after settings update", user.ID)
if err := srv.repository.DeleteByUser(user.ID); err != nil {
@@ -62,23 +66,26 @@ func NewLeaderboardService(leaderboardRepo repositories.ILeaderboardRepository,
return srv
}
-func (srv *LeaderboardService) ScheduleDefault() {
- runAllUsers := func(interval *models.IntervalKey, by []uint8) {
+func (srv *LeaderboardService) Schedule() {
+ logbuch.Info("scheduling leaderboard generation")
+
+ generate := func() {
users, err := srv.userService.GetAllByLeaderboard(true)
if err != nil {
config.Log().Error("failed to get users for leaderboard generation - %v", err)
return
}
-
- srv.Run(users, interval, by)
+ srv.ComputeLeaderboard(users, models.IntervalPast7Days, []uint8{models.SummaryLanguage})
}
- s := gocron.NewScheduler(time.Local)
- s.Every(1).Day().At(srv.config.App.LeaderboardGenerationTime).Do(runAllUsers, models.IntervalPast7Days, []uint8{models.SummaryLanguage})
- s.StartBlocking()
+ for _, cronExp := range srv.config.App.GetLeaderboardGenerationTimeCron() {
+ if _, err := srv.queueDefault.DispatchCron(generate, cronExp); err != nil {
+ config.Log().Error("failed to schedule leaderboard generation (%s), %v", cronExp, err)
+ }
+ }
}
-func (srv *LeaderboardService) Run(users []*models.User, interval *models.IntervalKey, by []uint8) error {
+func (srv *LeaderboardService) ComputeLeaderboard(users []*models.User, interval *models.IntervalKey, by []uint8) error {
logbuch.Info("generating leaderboard (%s) for %d users (%d aggregations)", (*interval)[0], len(users), len(by))
for _, user := range users {
@@ -205,7 +212,7 @@ func (srv *LeaderboardService) GetAggregatedByIntervalAndUser(interval *models.I
}
func (srv *LeaderboardService) GenerateByUser(user *models.User, interval *models.IntervalKey) (*models.LeaderboardItem, error) {
- err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
+ err, from, to := helpers.ResolveIntervalTZ(interval, user.TZ())
if err != nil {
return nil, err
}
@@ -224,7 +231,7 @@ func (srv *LeaderboardService) GenerateByUser(user *models.User, interval *model
}
func (srv *LeaderboardService) GenerateAggregatedByUser(user *models.User, interval *models.IntervalKey, by uint8) ([]*models.LeaderboardItem, error) {
- err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
+ err, from, to := helpers.ResolveIntervalTZ(interval, user.TZ())
if err != nil {
return nil, err
}
diff --git a/services/mail/mail.go b/services/mail/mail.go
index 6f1c638..33c917c 100644
--- a/services/mail/mail.go
+++ b/services/mail/mail.go
@@ -3,6 +3,7 @@ package mail
import (
"bytes"
"fmt"
+ "github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/routes"
"github.com/muety/wakapi/services"
@@ -115,7 +116,7 @@ func (m *MailService) SendReport(recipient *models.User, report *models.Report)
mail := &models.Mail{
From: models.MailAddress(m.config.Mail.Sender),
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
- Subject: fmt.Sprintf(subjectReport, utils.FormatDateHuman(time.Now().In(recipient.TZ()))),
+ Subject: fmt.Sprintf(subjectReport, helpers.FormatDateHuman(time.Now().In(recipient.TZ()))),
}
mail.WithHTML(tpl.String())
return m.sendingService.Send(mail)
diff --git a/services/misc.go b/services/misc.go
index f493209..e148a11 100644
--- a/services/misc.go
+++ b/services/misc.go
@@ -2,20 +2,30 @@ package services
import (
"github.com/emvi/logbuch"
+ "github.com/muety/artifex/v2"
"github.com/muety/wakapi/config"
- "runtime"
+ "github.com/muety/wakapi/utils"
+ "go.uber.org/atomic"
"strconv"
+ "sync"
"time"
- "github.com/go-co-op/gocron"
"github.com/muety/wakapi/models"
)
+const (
+ countUsersEvery = 1 * time.Hour
+)
+
+var countLock = sync.Mutex{}
+
type MiscService struct {
config *config.Config
userService IUserService
summaryService ISummaryService
keyValueService IKeyValueService
+ queueDefault *artifex.Dispatcher
+ queueWorkers *artifex.Dispatcher
}
func NewMiscService(userService IUserService, summaryService ISummaryService, keyValueService IKeyValueService) *MiscService {
@@ -24,81 +34,72 @@ func NewMiscService(userService IUserService, summaryService ISummaryService, ke
userService: userService,
summaryService: summaryService,
keyValueService: keyValueService,
+ queueDefault: config.GetDefaultQueue(),
+ queueWorkers: config.GetQueue(config.QueueProcessing),
}
}
-type CountTotalTimeJob struct {
- UserID string
- NumJobs int
-}
-
-type CountTotalTimeResult struct {
- UserId string
- Total time.Duration
-}
-
func (srv *MiscService) ScheduleCountTotalTime() {
- s := gocron.NewScheduler(time.Local)
- s.Every(1).Hour().WaitForSchedule().Do(srv.runCountTotalTime)
- s.StartBlocking()
+ logbuch.Info("scheduling total time counting")
+ if _, err := srv.queueDefault.DispatchEvery(srv.CountTotalTime, countUsersEvery); err != nil {
+ config.Log().Error("failed to schedule user counting jobs, %v", err)
+ }
}
-func (srv *MiscService) runCountTotalTime() error {
+func (srv *MiscService) CountTotalTime() {
+ logbuch.Info("counting users total time")
+ if ok := countLock.TryLock(); !ok {
+ config.Log().Warn("couldn't acquire lock for counting users total time, job is still pending")
+ }
+ defer countLock.Unlock()
+
users, err := srv.userService.GetAll()
if err != nil {
- return err
+ config.Log().Error("failed to fetch users for time counting, %v", err)
}
- jobs := make(chan *CountTotalTimeJob, len(users))
- results := make(chan *CountTotalTimeResult, len(users))
+ var totalTime = atomic.NewDuration(0)
+ var pendingJobs sync.WaitGroup
+ pendingJobs.Add(len(users))
for _, u := range users {
- jobs <- &CountTotalTimeJob{
- UserID: u.ID,
- NumJobs: len(users),
+ user := *u
+ if err := srv.queueWorkers.Dispatch(func() {
+ defer pendingJobs.Done()
+ totalTime.Add(srv.countUserTotalTime(user.ID))
+ }); err != nil {
+ config.Log().Error("failed to enqueue counting job for user '%s'", user.ID)
+ pendingJobs.Done()
}
}
- close(jobs)
-
- for i := 0; i < runtime.NumCPU(); i++ {
- go srv.countTotalTimeWorker(jobs, results)
- }
// persist
- var i int
- var total time.Duration
- for i = 0; i < len(users); i++ {
- result := <-results
- total += result.Total
- }
- close(results)
-
- if err := srv.keyValueService.PutString(&models.KeyStringValue{
- Key: config.KeyLatestTotalTime,
- Value: total.String(),
- }); err != nil {
- logbuch.Error("failed to save total time count: %v", err)
- }
-
- if err := srv.keyValueService.PutString(&models.KeyStringValue{
- Key: config.KeyLatestTotalUsers,
- Value: strconv.Itoa(i),
- }); err != nil {
- logbuch.Error("failed to save total users count: %v", err)
- }
-
- return nil
-}
-
-func (srv *MiscService) countTotalTimeWorker(jobs <-chan *CountTotalTimeJob, results chan<- *CountTotalTimeResult) {
- for job := range jobs {
- if result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: job.UserID}, srv.summaryService.Retrieve, nil, false); err != nil {
- config.Log().Error("failed to count total for user %s: %v", job.UserID, err)
- } else {
- results <- &CountTotalTimeResult{
- UserId: job.UserID,
- Total: result.TotalTime(),
+ go func(wg *sync.WaitGroup) {
+ if !utils.WaitTimeout(&pendingJobs, 2*countUsersEvery) {
+ if err := srv.keyValueService.PutString(&models.KeyStringValue{
+ Key: config.KeyLatestTotalTime,
+ Value: totalTime.Load().String(),
+ }); err != nil {
+ config.Log().Error("failed to save total time count: %v", err)
}
+
+ if err := srv.keyValueService.PutString(&models.KeyStringValue{
+ Key: config.KeyLatestTotalUsers,
+ Value: strconv.Itoa(len(users)),
+ }); err != nil {
+ config.Log().Error("failed to save total users count: %v", err)
+ }
+ } else {
+ config.Log().Error("waiting for user counting jobs timed out")
}
- }
+ }(&pendingJobs)
+}
+
+func (srv *MiscService) countUserTotalTime(userId string) time.Duration {
+ result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: userId}, srv.summaryService.Retrieve, nil, false)
+ if err != nil {
+ config.Log().Error("failed to count total for user %s: %v", userId, err)
+ return 0
+ }
+ return result.TotalTime()
}
diff --git a/services/report.go b/services/report.go
index c332c29..3702b77 100644
--- a/services/report.go
+++ b/services/report.go
@@ -1,21 +1,21 @@
package services
import (
+ "github.com/duke-git/lancet/v2/slice"
"github.com/emvi/logbuch"
- "github.com/go-co-op/gocron"
"github.com/leandro-lugaresi/hub"
+ "github.com/muety/artifex/v2"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"math/rand"
- "sync"
"time"
)
-var reportLock = sync.Mutex{}
+// delay between evey report generation task (to throttle email sending frequency)
+const reportDelay = 10 * time.Second
-// range for random offset to add / subtract when scheduling a new job
-// to avoid all mails being sent at once, but distributed over 2*offsetIntervalMin minutes
-const offsetIntervalMin = 15
+// past time range to cover in the report
+const reportRange = 7 * 24 * time.Hour
type ReportService struct {
config *config.Config
@@ -23,8 +23,9 @@ type ReportService struct {
summaryService ISummaryService
userService IUserService
mailService IMailService
- scheduler *gocron.Scheduler
rand *rand.Rand
+ queueDefault *artifex.Dispatcher
+ queueWorkers *artifex.Dispatcher
}
func NewReportService(summaryService ISummaryService, userService IUserService, mailService IMailService) *ReportService {
@@ -34,80 +35,67 @@ func NewReportService(summaryService ISummaryService, userService IUserService,
summaryService: summaryService,
userService: userService,
mailService: mailService,
- scheduler: gocron.NewScheduler(time.Local),
rand: rand.New(rand.NewSource(time.Now().Unix())),
+ queueDefault: config.GetDefaultQueue(),
+ queueWorkers: config.GetQueue(config.QueueReports),
}
- srv.scheduler.StartAsync()
-
- sub := srv.eventBus.Subscribe(0, config.EventUserUpdate)
- go func(sub *hub.Subscription) {
- for m := range sub.Receiver {
- srv.SyncSchedule(m.Fields[config.FieldPayload].(*models.User))
- }
- }(&sub)
-
return srv
}
func (srv *ReportService) Schedule() {
- logbuch.Info("initializing report service")
+ logbuch.Info("scheduling report generation")
- users, err := srv.userService.GetAllByReports(true)
- if err != nil {
- config.Log().Fatal("%v", err)
- }
+ scheduleUserReport := func(u *models.User) {
+ if err := srv.queueWorkers.Dispatch(func() {
+ t0 := time.Now()
- logbuch.Info("scheduling reports for %d users", len(users))
- for _, u := range users {
- srv.SyncSchedule(u)
- }
-}
+ if err := srv.SendReport(u, reportRange); err != nil {
+ config.Log().Error("failed to generate report for '%s', %v", u.ID, err)
+ }
-// SyncSchedule syncs the currently active schedulers with the user's wish about whether or not to receive reports.
-// Returns whether a scheduler is active after this operation has run.
-func (srv *ReportService) SyncSchedule(u *models.User) bool {
- reportLock.Lock()
- defer reportLock.Unlock()
-
- // unschedule
- if !u.ReportsWeekly {
- _ = srv.scheduler.RemoveByTag(u.ID)
- logbuch.Info("disabled scheduled reports for user %s", u.ID)
- return false
- }
-
- // schedule
- if job := srv.getJobByTag(u.ID); job == nil && u.ReportsWeekly {
- t, _ := time.ParseInLocation("15:04", srv.config.App.GetWeeklyReportTime(), u.TZ())
- t = t.Add(time.Duration(srv.rand.Intn(offsetIntervalMin*60)) * time.Second)
- if job, err := srv.scheduler.
- SingletonMode().
- Every(1).
- Week().
- Weekday(srv.config.App.GetWeeklyReportDay()).
- At(t).
- Tag(u.ID).
- Do(srv.Run, u, 7*24*time.Hour); err != nil {
- config.Log().Error("failed to schedule report job for user '%s' - %v", u.ID, err)
- } else {
- logbuch.Info("next report for user %s is scheduled for %v", u.ID, job.NextRun())
+ // make the job take at least reportDelay seconds
+ if diff := reportDelay - time.Now().Sub(t0); diff > 0 {
+ logbuch.Debug("waiting for %v before sending next report", diff)
+ time.Sleep(diff)
+ }
+ }); err != nil {
+ config.Log().Error("failed to dispatch report generation job for user '%s', %v", u.ID, err)
}
}
- return u.ReportsWeekly
+ _, err := srv.queueDefault.DispatchCron(func() {
+ // fetch all users with reports enabled
+ users, err := srv.userService.GetAllByReports(true)
+ if err != nil {
+ config.Log().Error("failed to get users for report generation, %v", err)
+ return
+ }
+
+ // filter users who have their email set
+ users = slice.Filter[*models.User](users, func(i int, u *models.User) bool {
+ return u.Email != ""
+ })
+
+ // schedule jobs, throttled by one job per x seconds
+ logbuch.Info("scheduling report generation for %d users", len(users))
+ for _, u := range users {
+ scheduleUserReport(u)
+ }
+ }, srv.config.App.GetWeeklyReportCron())
+
+ if err != nil {
+ config.Log().Error("failed to dispatch report generation jobs, %v", err)
+ }
}
-func (srv *ReportService) Run(user *models.User, duration time.Duration) error {
+func (srv *ReportService) SendReport(user *models.User, duration time.Duration) error {
if user.Email == "" {
logbuch.Warn("not generating report for '%s' as no e-mail address is set")
return nil
}
- if !srv.SyncSchedule(user) {
- logbuch.Info("reports for user '%s' were turned off in the meanwhile since last report job ran")
- return nil
- }
+ logbuch.Info("generating report for '%s'", user.ID)
end := time.Now().In(user.TZ())
start := time.Now().Add(-1 * duration)
@@ -126,21 +114,10 @@ func (srv *ReportService) Run(user *models.User, duration time.Duration) error {
}
if err := srv.mailService.SendReport(user, report); err != nil {
- config.Log().Error("failed to send report for '%s' - %v", user.ID, err)
+ config.Log().Error("failed to send report for '%s', %v", user.ID, err)
return err
}
logbuch.Info("sent report to user '%s'", user.ID)
return nil
}
-
-func (srv *ReportService) getJobByTag(tag string) *gocron.Job {
- for _, j := range srv.scheduler.Jobs() {
- for _, t := range j.Tags() {
- if t == tag {
- return j
- }
- }
- }
- return nil
-}
diff --git a/services/services.go b/services/services.go
index 867e7bf..d3db4ed 100644
--- a/services/services.go
+++ b/services/services.go
@@ -8,11 +8,12 @@ import (
type IAggregationService interface {
Schedule()
- Run(set datastructure.Set[string]) error
+ AggregateSummaries(set datastructure.Set[string]) error
}
type IMiscService interface {
ScheduleCountTotalTime()
+ CountTotalTime()
}
type IAliasService interface {
@@ -93,13 +94,12 @@ type ISummaryService interface {
type IReportService interface {
Schedule()
- SyncSchedule(user *models.User) bool
- Run(*models.User, time.Duration) error
+ SendReport(*models.User, time.Duration) error
}
type ILeaderboardService interface {
- ScheduleDefault()
- Run([]*models.User, *models.IntervalKey, []uint8) error
+ Schedule()
+ ComputeLeaderboard([]*models.User, *models.IntervalKey, []uint8) error
ExistsAnyByUser(string) (bool, error)
CountUsers() (int64, error)
GetByInterval(*models.IntervalKey, *models.PageParams, bool) (models.Leaderboard, error)
diff --git a/utils/auth.go b/utils/auth.go
index ab96242..1c68041 100644
--- a/utils/auth.go
+++ b/utils/auth.go
@@ -3,8 +3,6 @@ package utils
import (
"encoding/base64"
"errors"
- "github.com/muety/wakapi/config"
- "github.com/muety/wakapi/models"
"golang.org/x/crypto/bcrypt"
"net/http"
"regexp"
@@ -44,19 +42,6 @@ func ExtractBearerAuth(r *http.Request) (key string, err error) {
return string(keyBytes), err
}
-func ExtractCookieAuth(r *http.Request, config *config.Config) (username *string, err error) {
- cookie, err := r.Cookie(models.AuthCookieKey)
- if err != nil {
- return nil, errors.New("missing authentication")
- }
-
- if err := config.Security.SecureCookie.Decode(models.AuthCookieKey, cookie.Value, &username); err != nil {
- return nil, errors.New("cookie is invalid")
- }
-
- return username, nil
-}
-
func CompareBcrypt(wanted, actual, pepper string) bool {
plainPassword := []byte(strings.TrimSpace(actual) + pepper)
err := bcrypt.CompareHashAndPassword([]byte(wanted), plainPassword)
diff --git a/utils/collection.go b/utils/collection.go
new file mode 100644
index 0000000..b079e4a
--- /dev/null
+++ b/utils/collection.go
@@ -0,0 +1,21 @@
+package utils
+
+import "strings"
+
+func SubSlice[T any](slice []T, from, to uint) []T {
+ if int(to) > len(slice) {
+ to = uint(len(slice))
+ }
+ return slice[from:int(to)]
+}
+
+func CloneStringMap(m map[string]string, keysToLower bool) map[string]string {
+ m2 := make(map[string]string)
+ for k, v := range m {
+ if keysToLower {
+ k = strings.ToLower(k)
+ }
+ m2[k] = v
+ }
+ return m2
+}
diff --git a/utils/common_test.go b/utils/collection_test.go
similarity index 100%
rename from utils/common_test.go
rename to utils/collection_test.go
diff --git a/utils/cron.go b/utils/cron.go
new file mode 100644
index 0000000..d2444f3
--- /dev/null
+++ b/utils/cron.go
@@ -0,0 +1,11 @@
+package utils
+
+import "strings"
+
+func CronPadToSecondly(expr string) string {
+ parts := strings.Split(expr, " ")
+ if len(parts) == 6 {
+ return expr
+ }
+ return "0 " + expr
+}
diff --git a/utils/date.go b/utils/date.go
index 4a0a64e..2fc1717 100644
--- a/utils/date.go
+++ b/utils/date.go
@@ -1,8 +1,8 @@
package utils
import (
- "fmt"
"github.com/duke-git/lancet/v2/datetime"
+ "strings"
"time"
)
@@ -47,16 +47,28 @@ func SplitRangeByDays(from time.Time, to time.Time) [][]time.Time {
return intervals
}
-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)
-}
-
// LocalTZOffset returns the time difference between server local time and UTC
func LocalTZOffset() time.Duration {
_, offset := time.Now().Zone()
return time.Duration(offset * int(time.Second))
}
+
+func ParseWeekday(s string) time.Weekday {
+ switch strings.ToLower(s) {
+ case "mon", strings.ToLower(time.Monday.String()):
+ return time.Monday
+ case "tue", strings.ToLower(time.Tuesday.String()):
+ return time.Tuesday
+ case "wed", strings.ToLower(time.Wednesday.String()):
+ return time.Wednesday
+ case "thu", strings.ToLower(time.Thursday.String()):
+ return time.Thursday
+ case "fri", strings.ToLower(time.Friday.String()):
+ return time.Friday
+ case "sat", strings.ToLower(time.Saturday.String()):
+ return time.Saturday
+ case "sun", strings.ToLower(time.Sunday.String()):
+ return time.Sunday
+ }
+ return time.Monday
+}
diff --git a/utils/date_test.go b/utils/date_test.go
index 02d85db..a5d676c 100644
--- a/utils/date_test.go
+++ b/utils/date_test.go
@@ -2,7 +2,6 @@ package utils
import (
"github.com/duke-git/lancet/v2/datetime"
- "github.com/muety/wakapi/config"
"github.com/stretchr/testify/assert"
"testing"
"time"
@@ -23,8 +22,8 @@ func init() {
}
func TestDate_SplitRangeByDays(t *testing.T) {
- df1, _ := time.Parse(config.SimpleDateTimeFormat, "2021-04-25 20:25:00")
- dt1, _ := time.Parse(config.SimpleDateTimeFormat, "2021-04-28 06:45:00")
+ df1, _ := time.Parse("2006-01-02 15:04:05", "2021-04-25 20:25:00")
+ dt1, _ := time.Parse("2006-01-02 15:04:05", "2021-04-28 06:45:00")
df2 := df1
dt2 := datetime.EndOfDay(df1)
df3 := df1
diff --git a/utils/http.go b/utils/http.go
index faebb7e..5ee9b29 100644
--- a/utils/http.go
+++ b/utils/http.go
@@ -1,8 +1,7 @@
package utils
import (
- "encoding/json"
- "github.com/muety/wakapi/config"
+ "errors"
"github.com/muety/wakapi/models"
"net/http"
"regexp"
@@ -23,14 +22,6 @@ func init() {
cacheMaxAgeRe = regexp.MustCompile(cacheMaxAgePattern)
}
-func RespondJSON(w http.ResponseWriter, r *http.Request, status int, object interface{}) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- if err := json.NewEncoder(w).Encode(object); err != nil {
- config.Log().Request(r).Error("error while writing json response: %v", err)
- }
-}
-
func IsNoCache(r *http.Request, cacheTtl time.Duration) bool {
cacheControl := r.Header.Get("cache-control")
if strings.Contains(cacheControl, "no-cache") {
@@ -67,3 +58,12 @@ func ParsePageParamsWithDefault(r *http.Request, page, size int) *models.PagePar
}
return pageParams
}
+
+func ParseUserAgent(ua string) (string, string, error) {
+ re := regexp.MustCompile(`(?iU)^wakatime\/(?:v?[\d+.]+|unset)\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
+ groups := re.FindAllStringSubmatch(ua, -1)
+ if len(groups) == 0 || len(groups[0]) != 3 {
+ return "", "", errors.New("failed to parse user agent string")
+ }
+ return groups[0][1], groups[0][2], nil
+}
diff --git a/utils/strings.go b/utils/strings.go
index 3175d79..ef78ffc 100644
--- a/utils/strings.go
+++ b/utils/strings.go
@@ -8,3 +8,23 @@ import (
func Capitalize(s string) string {
return fmt.Sprintf("%s%s", strings.ToUpper(s[:1]), s[1:])
}
+
+func SplitMulti(s string, delimiters ...string) []string {
+ return strings.FieldsFunc(s, func(r rune) bool {
+ for _, d := range delimiters {
+ if string(r) == d {
+ return true
+ }
+ }
+ return false
+ })
+}
+
+func FindString(needle string, haystack []string, defaultVal string) string {
+ for _, s := range haystack {
+ if s == needle {
+ return s
+ }
+ }
+ return defaultVal
+}
diff --git a/utils/sync.go b/utils/sync.go
new file mode 100644
index 0000000..0536970
--- /dev/null
+++ b/utils/sync.go
@@ -0,0 +1,23 @@
+package utils
+
+import (
+ "sync"
+ "time"
+)
+
+// WaitTimeout waits for the waitgroup for the specified max timeout.
+// Returns true if waiting timed out.
+// See // https://stackoverflow.com/a/32843750/3112139.
+func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
+ c := make(chan struct{})
+ go func() {
+ defer close(c)
+ wg.Wait()
+ }()
+ select {
+ case <-c:
+ return false // completed normally
+ case <-time.After(timeout):
+ return true // timed out
+ }
+}