1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

Compare commits

..

8 Commits

34 changed files with 2061 additions and 439 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ build
*.db
config*.yml
!config.default.yml
!testing/config.testing.yml
pkged.go
package.json
yarn.lock

View File

@ -24,7 +24,7 @@
<span> | </span>
<a href="#-features">Features</a>
<span> | </span>
<a href="#-how-to-use">How to use</a>
<a href="#%EF%B8%8F-how-to-use">How to use</a>
<span> | </span>
<a href="https://github.com/muety/wakapi/issues">Issues</a>
<span> | </span>
@ -45,6 +45,7 @@
* [API Endpoints](#-api-endpoints)
* [Integrations](#-integrations)
* [Best Practices](#-best-practices)
* [Tests](#-tests)
* [Developer Notes](#-developer-notes)
* [Support](#-support)
* [FAQs](#-faqs)
@ -59,6 +60,7 @@ I'd love to get some community feedback from active Wakapi users. If you want, p
* ✅ Built by developers for developers
* ✅ Statistics for projects, languages, editors, hosts and operating systems
* ✅ Badges
* ✅ Weekly E-Mail Reports
* ✅ REST API
* ✅ Partially compatible with WakaTime
* ✅ WakaTime integration
@ -131,8 +133,8 @@ Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime
```ini
[settings]
# Your Wakapi server URL or 'https://wakapi.dev/api/heartbeat' when using the cloud server
api_url = http://localhost:3000/api/heartbeat
# Your Wakapi server URL or 'https://wakapi.dev/api' when using the cloud server
api_url = http://localhost:3000/api
# Your Wakapi API key (get it from the web interface after having created an account)
api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b
@ -282,12 +284,40 @@ Preview:
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or _nginx_ to enable **TLS encryption** (HTTPS).
However, if you want to expose your wakapi instance to the public anyway, you need to set `server.listen_ipv4` to `0.0.0.0` in `config.yml`
## 🤓 Developer Notes
### Running tests
## 🧪 Tests
### Unit Tests
Unit tests are supposed to test business logic on a fine-grained level. They are implemented as part of the application, using Go's [testing](https://pkg.go.dev/testing?utm_source=godoc) package alongside [stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify).
#### How to run
```bash
CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
$ CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
```
### API Tests
API tests are implemented as black box tests, which interact with a fully-fledged, standalone Wakapi through HTTP requests. They are supposed to check Wakapi's web stack and endpoints, including response codes, headers and data on a syntactical level, rather than checking the actual content that is returned.
Our API (or end-to-end, in some way) tests are implemented as a [Postman](https://www.postman.com/) collection and can be run either from inside Postman, or using [newman](https://www.npmjs.com/package/newman) as a command-line runner.
To get a predictable environment, tests are run against a fresh and clean Wakapi instance with a SQLite database that is populated with nothing but some seed data (see [data.sql](testing/data.sql)). It is usually recommended for software tests to be [safe](https://www.restapitutorial.com/lessons/idempotency.html), stateless and without side effects. In contrary to that paradigm, our API tests strictly require a fixed execution order (which Postman assures) and their assertions may rely on specific previous tests having succeeded.
#### Prerequisites (Linux only)
```bash
# 1. sqlite (cli)
$ sudo apt install sqlite # Fedora: sudo dnf install sqlite
# 2. screen
$ sudo apt install screen # Fedora: sudo dnf install screen
# 3. newman
$ npm install -g newman
```
#### How to run (Linux only)
```bash
$ ./testing/run_api_tests.sh
```
## 🤓 Developer Notes
### Building web assets
To keep things minimal, Wakapi does not contain a `package.json`, `node_modules` or any sort of frontend build step. Instead, all JS and CSS assets are included as static files and checked in to Git. This way we can avoid requiring NodeJS to build Wakapi. However, for [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) it makes sense to run it through a "build" step to benefit from purging and significantly reduce it in size. To only require this at the time of development, the compiled asset is checked in to Git as well. Similarly, [Iconify](https://iconify.design/docs/icon-bundles/) bundles are also created at development time and checked in to the repo.

View File

@ -13,6 +13,7 @@ app:
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
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
custom_languages:
vue: Vue
jsx: JSX

View File

@ -64,7 +64,7 @@ type appConfig struct {
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
ImportBatchSize int `yaml:"import_batch_size" default:"100" env:"WAKAPI_IMPORT_BATCH_SIZE"`
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
CustomLanguages map[string]string `yaml:"custom_languages"`
Colors map[string]map[string]string `yaml:"-"`
@ -341,22 +341,27 @@ func Load(version string) *Config {
}
}
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" {
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 must be set")
}
if config.Db.MaxConn <= 0 {
logbuch.Fatal("you must allow at least one database connection")
}
if config.Sentry.Dsn != "" {
logbuch.Info("enabling sentry integration")
initSentry(config.Sentry, config.IsDev())
}
// some validation checks
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" {
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 must be set")
}
if config.Db.MaxConn <= 0 {
logbuch.Fatal("you must allow at least one database connection")
}
if config.Mail.Provider != "" && 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")
}
Set(config)
return Get()

View File

@ -1,22 +1,4 @@
mode: set
github.com/muety/wakapi/models/filters.go:16.56,17.16 1 0
github.com/muety/wakapi/models/filters.go:29.2,29.19 1 0
github.com/muety/wakapi/models/filters.go:18.22,19.32 1 0
github.com/muety/wakapi/models/filters.go:20.17,21.27 1 0
github.com/muety/wakapi/models/filters.go:22.23,23.33 1 0
github.com/muety/wakapi/models/filters.go:24.21,25.31 1 0
github.com/muety/wakapi/models/filters.go:26.22,27.32 1 0
github.com/muety/wakapi/models/filters.go:32.47,33.21 1 1
github.com/muety/wakapi/models/filters.go:44.2,44.21 1 1
github.com/muety/wakapi/models/filters.go:33.21,35.3 1 1
github.com/muety/wakapi/models/filters.go:35.8,35.23 1 1
github.com/muety/wakapi/models/filters.go:35.23,37.3 1 0
github.com/muety/wakapi/models/filters.go:37.8,37.29 1 1
github.com/muety/wakapi/models/filters.go:37.29,39.3 1 1
github.com/muety/wakapi/models/filters.go:39.8,39.27 1 1
github.com/muety/wakapi/models/filters.go:39.27,41.3 1 0
github.com/muety/wakapi/models/filters.go:41.8,41.28 1 1
github.com/muety/wakapi/models/filters.go:41.28,43.3 1 0
github.com/muety/wakapi/models/heartbeat.go:32.34,34.2 1 1
github.com/muety/wakapi/models/heartbeat.go:36.65,38.46 2 1
github.com/muety/wakapi/models/heartbeat.go:38.46,39.108 1 1
@ -34,6 +16,17 @@ github.com/muety/wakapi/models/heartbeat.go:67.37,83.2 1 0
github.com/muety/wakapi/models/heartbeat.go:91.41,93.16 2 0
github.com/muety/wakapi/models/heartbeat.go:96.2,97.10 2 0
github.com/muety/wakapi/models/heartbeat.go:93.16,95.3 1 0
github.com/muety/wakapi/models/interval.go:39.47,40.23 1 0
github.com/muety/wakapi/models/interval.go:45.2,45.14 1 0
github.com/muety/wakapi/models/interval.go:40.23,41.13 1 0
github.com/muety/wakapi/models/interval.go:41.13,43.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/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
@ -53,6 +46,54 @@ 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:35.52,37.2 1 0
github.com/muety/wakapi/models/shared.go:39.52,42.16 3 0
github.com/muety/wakapi/models/shared.go:45.2,47.12 3 0
github.com/muety/wakapi/models/shared.go:42.16,44.3 1 0
github.com/muety/wakapi/models/shared.go:50.52,56.22 2 0
github.com/muety/wakapi/models/shared.go:71.2,74.12 3 0
github.com/muety/wakapi/models/shared.go:57.14,61.17 2 0
github.com/muety/wakapi/models/shared.go:64.17,66.8 2 0
github.com/muety/wakapi/models/shared.go:67.10,68.64 1 0
github.com/muety/wakapi/models/shared.go:61.17,63.4 1 0
github.com/muety/wakapi/models/shared.go:77.51,80.2 2 0
github.com/muety/wakapi/models/shared.go:82.45,84.2 1 0
github.com/muety/wakapi/models/shared.go:86.37,88.2 1 0
github.com/muety/wakapi/models/shared.go:90.35,92.2 1 0
github.com/muety/wakapi/models/shared.go:94.34,96.2 1 0
github.com/muety/wakapi/models/alias.go:12.32,14.2 1 0
github.com/muety/wakapi/models/alias.go:16.37,17.35 1 0
github.com/muety/wakapi/models/alias.go:22.2,22.14 1 0
github.com/muety/wakapi/models/alias.go:17.35,18.18 1 0
github.com/muety/wakapi/models/alias.go:18.18,20.4 1 0
github.com/muety/wakapi/models/filters.go:16.56,17.16 1 0
github.com/muety/wakapi/models/filters.go:29.2,29.19 1 0
github.com/muety/wakapi/models/filters.go:18.22,19.32 1 0
github.com/muety/wakapi/models/filters.go:20.17,21.27 1 0
github.com/muety/wakapi/models/filters.go:22.23,23.33 1 0
github.com/muety/wakapi/models/filters.go:24.21,25.31 1 0
github.com/muety/wakapi/models/filters.go:26.22,27.32 1 0
github.com/muety/wakapi/models/filters.go:32.47,33.21 1 1
github.com/muety/wakapi/models/filters.go:44.2,44.21 1 1
github.com/muety/wakapi/models/filters.go:33.21,35.3 1 1
github.com/muety/wakapi/models/filters.go:35.8,35.23 1 1
github.com/muety/wakapi/models/filters.go:35.23,37.3 1 0
github.com/muety/wakapi/models/filters.go:37.8,37.29 1 1
github.com/muety/wakapi/models/filters.go:37.29,39.3 1 1
github.com/muety/wakapi/models/filters.go:39.8,39.27 1 1
github.com/muety/wakapi/models/filters.go:39.27,41.3 1 0
github.com/muety/wakapi/models/filters.go:41.8,41.28 1 1
github.com/muety/wakapi/models/filters.go:41.28,43.3 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/summary.go:70.29,72.2 1 1
github.com/muety/wakapi/models/summary.go:74.37,81.2 6 1
github.com/muety/wakapi/models/summary.go:83.35,85.2 1 1
@ -119,219 +160,6 @@ github.com/muety/wakapi/models/user.go:115.45,117.2 1 0
github.com/muety/wakapi/models/user.go:119.45,121.2 1 0
github.com/muety/wakapi/models/user.go:123.39,125.2 1 0
github.com/muety/wakapi/models/user.go:127.39,130.2 2 0
github.com/muety/wakapi/models/alias.go:12.32,14.2 1 0
github.com/muety/wakapi/models/alias.go:16.37,17.35 1 0
github.com/muety/wakapi/models/alias.go:22.2,22.14 1 0
github.com/muety/wakapi/models/alias.go:17.35,18.18 1 0
github.com/muety/wakapi/models/alias.go:18.18,20.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/interval.go:39.47,40.23 1 0
github.com/muety/wakapi/models/interval.go:45.2,45.14 1 0
github.com/muety/wakapi/models/interval.go:40.23,41.13 1 0
github.com/muety/wakapi/models/interval.go:41.13,43.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/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/shared.go:35.52,37.2 1 0
github.com/muety/wakapi/models/shared.go:39.52,42.16 3 0
github.com/muety/wakapi/models/shared.go:45.2,47.12 3 0
github.com/muety/wakapi/models/shared.go:42.16,44.3 1 0
github.com/muety/wakapi/models/shared.go:50.52,56.22 2 0
github.com/muety/wakapi/models/shared.go:71.2,74.12 3 0
github.com/muety/wakapi/models/shared.go:57.14,61.17 2 0
github.com/muety/wakapi/models/shared.go:64.17,66.8 2 0
github.com/muety/wakapi/models/shared.go:67.10,68.64 1 0
github.com/muety/wakapi/models/shared.go:61.17,63.4 1 0
github.com/muety/wakapi/models/shared.go:77.51,80.2 2 0
github.com/muety/wakapi/models/shared.go:82.45,84.2 1 0
github.com/muety/wakapi/models/shared.go:86.37,88.2 1 0
github.com/muety/wakapi/models/shared.go:90.35,92.2 1 0
github.com/muety/wakapi/models/shared.go:94.34,96.2 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/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/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/filesystem.go:14.68,16.16 2 0
github.com/muety/wakapi/utils/filesystem.go:20.2,21.15 2 0
github.com/muety/wakapi/utils/filesystem.go:33.2,33.15 1 0
github.com/muety/wakapi/utils/filesystem.go:16.16,18.3 1 0
github.com/muety/wakapi/utils/filesystem.go:21.15,23.47 2 0
github.com/muety/wakapi/utils/filesystem.go:23.47,25.23 2 0
github.com/muety/wakapi/utils/filesystem.go:29.4,29.19 1 0
github.com/muety/wakapi/utils/filesystem.go:25.23,27.5 1 0
github.com/muety/wakapi/utils/http.go:9.90,12.58 3 0
github.com/muety/wakapi/utils/http.go:12.58,14.3 1 0
github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
github.com/muety/wakapi/utils/template.go:16.37,17.30 1 0
github.com/muety/wakapi/utils/template.go:20.2,20.10 1 0
github.com/muety/wakapi/utils/template.go:17.30,19.3 1 0
github.com/muety/wakapi/utils/date.go:8.43,10.2 1 1
github.com/muety/wakapi/utils/date.go:12.48,14.2 1 0
github.com/muety/wakapi/utils/date.go:16.41,18.21 2 1
github.com/muety/wakapi/utils/date.go:21.2,21.23 1 1
github.com/muety/wakapi/utils/date.go:18.21,20.3 1 0
github.com/muety/wakapi/utils/date.go:24.46,26.2 1 0
github.com/muety/wakapi/utils/date.go:28.51,30.2 1 0
github.com/muety/wakapi/utils/date.go:32.44,35.2 2 1
github.com/muety/wakapi/utils/date.go:37.52,39.2 1 0
github.com/muety/wakapi/utils/date.go:41.45,43.2 1 0
github.com/muety/wakapi/utils/date.go:45.51,47.2 1 0
github.com/muety/wakapi/utils/date.go:49.44,51.2 1 0
github.com/muety/wakapi/utils/date.go:54.42,56.2 1 1
github.com/muety/wakapi/utils/date.go:59.41,61.21 2 1
github.com/muety/wakapi/utils/date.go:64.2,64.36 1 1
github.com/muety/wakapi/utils/date.go:61.21,63.3 1 1
github.com/muety/wakapi/utils/date.go:68.63,70.2 1 0
github.com/muety/wakapi/utils/date.go:73.62,79.2 5 0
github.com/muety/wakapi/utils/date.go:82.67,85.33 2 1
github.com/muety/wakapi/utils/date.go:94.2,94.18 1 1
github.com/muety/wakapi/utils/date.go:85.33,87.19 2 1
github.com/muety/wakapi/utils/date.go:90.3,91.10 2 1
github.com/muety/wakapi/utils/date.go:87.19,89.4 1 1
github.com/muety/wakapi/utils/date.go:97.50,103.2 5 0
github.com/muety/wakapi/utils/date.go:106.79,109.36 3 1
github.com/muety/wakapi/utils/date.go:113.2,113.21 1 1
github.com/muety/wakapi/utils/date.go:117.2,117.21 1 1
github.com/muety/wakapi/utils/date.go:121.2,121.13 1 1
github.com/muety/wakapi/utils/date.go:109.36,112.3 2 0
github.com/muety/wakapi/utils/date.go:113.21,116.3 2 1
github.com/muety/wakapi/utils/date.go:117.21,120.3 2 1
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,35.18 2 0
github.com/muety/wakapi/utils/summary.go:70.2,70.22 1 0
github.com/muety/wakapi/utils/summary.go:36.28,37.26 1 0
github.com/muety/wakapi/utils/summary.go:38.32,40.24 2 0
github.com/muety/wakapi/utils/summary.go:41.31,42.29 1 0
github.com/muety/wakapi/utils/summary.go:43.31,45.27 2 0
github.com/muety/wakapi/utils/summary.go:46.32,47.30 1 0
github.com/muety/wakapi/utils/summary.go:48.32,50.28 2 0
github.com/muety/wakapi/utils/summary.go:51.31,52.29 1 0
github.com/muety/wakapi/utils/summary.go:53.32,54.44 1 0
github.com/muety/wakapi/utils/summary.go:55.41,57.42 2 0
github.com/muety/wakapi/utils/summary.go:58.33,59.45 1 0
github.com/muety/wakapi/utils/summary.go:60.33,61.45 1 0
github.com/muety/wakapi/utils/summary.go:62.35,63.45 1 0
github.com/muety/wakapi/utils/summary.go:64.26,65.21 1 0
github.com/muety/wakapi/utils/summary.go:66.10,67.39 1 0
github.com/muety/wakapi/utils/summary.go:73.73,80.56 5 0
github.com/muety/wakapi/utils/summary.go:96.2,103.8 2 0
github.com/muety/wakapi/utils/summary.go:80.56,82.3 1 0
github.com/muety/wakapi/utils/summary.go:82.8,82.54 1 0
github.com/muety/wakapi/utils/summary.go:82.54,84.3 1 0
github.com/muety/wakapi/utils/summary.go:84.8,86.17 2 0
github.com/muety/wakapi/utils/summary.go:90.3,91.17 2 0
github.com/muety/wakapi/utils/summary.go:86.17,88.4 1 0
github.com/muety/wakapi/utils/summary.go:91.17,93.4 1 0
github.com/muety/wakapi/utils/summary.go:106.48,110.51 2 0
github.com/muety/wakapi/utils/summary.go:113.2,113.12 1 0
github.com/muety/wakapi/utils/summary.go:110.51,112.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,70.16 2 1
github.com/muety/wakapi/config/db.go:74.2,81.3 1 1
github.com/muety/wakapi/config/db.go:70.16,72.3 1 0
github.com/muety/wakapi/config/db.go:84.54,86.2 1 1
github.com/muety/wakapi/config/eventbus.go:18.13,20.2 1 1
github.com/muety/wakapi/config/eventbus.go:22.26,24.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/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
github.com/muety/wakapi/config/sentry.go:48.79,51.2 2 0
github.com/muety/wakapi/config/sentry.go:53.72,57.2 3 0
github.com/muety/wakapi/config/sentry.go:59.71,63.2 3 0
github.com/muety/wakapi/config/sentry.go:65.71,69.2 3 0
github.com/muety/wakapi/config/sentry.go:71.72,75.2 3 0
github.com/muety/wakapi/config/sentry.go:77.72,81.2 3 0
github.com/muety/wakapi/config/sentry.go:83.67,88.18 4 0
github.com/muety/wakapi/config/sentry.go:100.2,100.28 1 0
github.com/muety/wakapi/config/sentry.go:88.18,89.65 1 0
github.com/muety/wakapi/config/sentry.go:89.65,92.42 3 0
github.com/muety/wakapi/config/sentry.go:95.4,96.10 2 0
github.com/muety/wakapi/config/sentry.go:92.42,94.5 1 0
github.com/muety/wakapi/config/sentry.go:110.50,114.91 1 0
github.com/muety/wakapi/config/sentry.go:114.91,115.29 1 0
github.com/muety/wakapi/config/sentry.go:119.4,122.38 3 0
github.com/muety/wakapi/config/sentry.go:127.4,127.39 1 0
github.com/muety/wakapi/config/sentry.go:130.4,130.69 1 0
github.com/muety/wakapi/config/sentry.go:115.29,117.5 1 0
github.com/muety/wakapi/config/sentry.go:122.38,123.38 1 0
github.com/muety/wakapi/config/sentry.go:123.38,125.6 1 0
github.com/muety/wakapi/config/sentry.go:127.39,129.5 1 0
github.com/muety/wakapi/config/sentry.go:132.79,133.27 1 0
github.com/muety/wakapi/config/sentry.go:140.4,140.16 1 0
github.com/muety/wakapi/config/sentry.go:133.27,134.84 1 0
github.com/muety/wakapi/config/sentry.go:134.84,135.42 1 0
github.com/muety/wakapi/config/sentry.go:135.42,137.7 1 0
github.com/muety/wakapi/config/sentry.go:142.17,144.3 1 0
github.com/muety/wakapi/config/sentry.go:147.49,151.51 2 0
github.com/muety/wakapi/config/sentry.go:154.2,154.12 1 0
github.com/muety/wakapi/config/sentry.go:151.51,153.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
@ -397,19 +225,71 @@ github.com/muety/wakapi/config/config.go:312.20,314.2 1 0
github.com/muety/wakapi/config/config.go:316.35,321.96 3 0
github.com/muety/wakapi/config/config.go:325.2,334.52 6 0
github.com/muety/wakapi/config/config.go:338.2,338.47 1 0
github.com/muety/wakapi/config/config.go:344.2,344.70 1 0
github.com/muety/wakapi/config/config.go:348.2,348.28 1 0
github.com/muety/wakapi/config/config.go:352.2,352.29 1 0
github.com/muety/wakapi/config/config.go:357.2,357.94 1 0
github.com/muety/wakapi/config/config.go:361.2,362.14 2 0
github.com/muety/wakapi/config/config.go:344.2,344.29 1 0
github.com/muety/wakapi/config/config.go:350.2,350.70 1 0
github.com/muety/wakapi/config/config.go:353.2,353.28 1 0
github.com/muety/wakapi/config/config.go:356.2,356.94 1 0
github.com/muety/wakapi/config/config.go:359.2,359.81 1 0
github.com/muety/wakapi/config/config.go:362.2,362.75 1 0
github.com/muety/wakapi/config/config.go:366.2,367.14 2 0
github.com/muety/wakapi/config/config.go:321.96,323.3 1 0
github.com/muety/wakapi/config/config.go:334.52,336.3 1 0
github.com/muety/wakapi/config/config.go:338.47,339.14 1 0
github.com/muety/wakapi/config/config.go:339.14,341.4 1 0
github.com/muety/wakapi/config/config.go:344.70,346.3 1 0
github.com/muety/wakapi/config/config.go:348.28,350.3 1 0
github.com/muety/wakapi/config/config.go:352.29,355.3 2 0
github.com/muety/wakapi/config/config.go:357.94,359.3 1 0
github.com/muety/wakapi/config/config.go:344.29,347.3 2 0
github.com/muety/wakapi/config/config.go:350.70,352.3 1 0
github.com/muety/wakapi/config/config.go:353.28,355.3 1 0
github.com/muety/wakapi/config/config.go:356.94,358.3 1 0
github.com/muety/wakapi/config/config.go:359.81,361.3 1 0
github.com/muety/wakapi/config/config.go:362.75,364.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,70.16 2 1
github.com/muety/wakapi/config/db.go:74.2,81.3 1 1
github.com/muety/wakapi/config/db.go:70.16,72.3 1 0
github.com/muety/wakapi/config/db.go:84.54,86.2 1 1
github.com/muety/wakapi/config/eventbus.go:18.13,20.2 1 1
github.com/muety/wakapi/config/eventbus.go:22.26,24.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/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
github.com/muety/wakapi/config/sentry.go:48.79,51.2 2 0
github.com/muety/wakapi/config/sentry.go:53.72,57.2 3 0
github.com/muety/wakapi/config/sentry.go:59.71,63.2 3 0
github.com/muety/wakapi/config/sentry.go:65.71,69.2 3 0
github.com/muety/wakapi/config/sentry.go:71.72,75.2 3 0
github.com/muety/wakapi/config/sentry.go:77.72,81.2 3 0
github.com/muety/wakapi/config/sentry.go:83.67,88.18 4 0
github.com/muety/wakapi/config/sentry.go:100.2,100.28 1 0
github.com/muety/wakapi/config/sentry.go:88.18,89.65 1 0
github.com/muety/wakapi/config/sentry.go:89.65,92.42 3 0
github.com/muety/wakapi/config/sentry.go:95.4,96.10 2 0
github.com/muety/wakapi/config/sentry.go:92.42,94.5 1 0
github.com/muety/wakapi/config/sentry.go:110.50,114.91 1 0
github.com/muety/wakapi/config/sentry.go:114.91,115.29 1 0
github.com/muety/wakapi/config/sentry.go:119.4,122.38 3 0
github.com/muety/wakapi/config/sentry.go:127.4,127.39 1 0
github.com/muety/wakapi/config/sentry.go:130.4,130.69 1 0
github.com/muety/wakapi/config/sentry.go:115.29,117.5 1 0
github.com/muety/wakapi/config/sentry.go:122.38,123.38 1 0
github.com/muety/wakapi/config/sentry.go:123.38,125.6 1 0
github.com/muety/wakapi/config/sentry.go:127.39,129.5 1 0
github.com/muety/wakapi/config/sentry.go:132.79,133.27 1 0
github.com/muety/wakapi/config/sentry.go:140.4,140.16 1 0
github.com/muety/wakapi/config/sentry.go:133.27,134.84 1 0
github.com/muety/wakapi/config/sentry.go:134.84,135.42 1 0
github.com/muety/wakapi/config/sentry.go:135.42,137.7 1 0
github.com/muety/wakapi/config/sentry.go:142.17,144.3 1 0
github.com/muety/wakapi/config/sentry.go:147.49,151.51 2 0
github.com/muety/wakapi/config/sentry.go:154.2,154.12 1 0
github.com/muety/wakapi/config/sentry.go:151.51,153.3 1 0
github.com/muety/wakapi/middlewares/authenticate.go:19.91,25.2 1 1
github.com/muety/wakapi/middlewares/authenticate.go:27.90,30.2 2 0
github.com/muety/wakapi/middlewares/authenticate.go:32.90,35.2 2 0
@ -493,80 +373,136 @@ 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/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
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
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/misc.go:23.126,30.2 1 0
github.com/muety/wakapi/services/misc.go:42.50,44.48 1 0
github.com/muety/wakapi/services/misc.go:48.2,50.19 3 0
github.com/muety/wakapi/services/misc.go:44.48,46.3 1 0
github.com/muety/wakapi/services/misc.go:53.51,59.40 4 0
github.com/muety/wakapi/services/misc.go:63.2,66.56 2 0
github.com/muety/wakapi/services/misc.go:77.2,77.12 1 0
github.com/muety/wakapi/services/misc.go:59.40,61.3 1 0
github.com/muety/wakapi/services/misc.go:66.56,67.27 1 0
github.com/muety/wakapi/services/misc.go:67.27,72.4 1 0
github.com/muety/wakapi/services/misc.go:73.8,75.3 1 0
github.com/muety/wakapi/services/misc.go:80.116,81.24 1 0
github.com/muety/wakapi/services/misc.go:81.24,82.151 1 0
github.com/muety/wakapi/services/misc.go:91.3,91.48 1 0
github.com/muety/wakapi/services/misc.go:82.151,84.4 1 0
github.com/muety/wakapi/services/misc.go:84.9,90.4 2 0
github.com/muety/wakapi/services/misc.go:91.48,94.4 2 0
github.com/muety/wakapi/services/misc.go:98.86,101.30 3 0
github.com/muety/wakapi/services/misc.go:106.2,109.17 1 0
github.com/muety/wakapi/services/misc.go:113.2,116.17 1 0
github.com/muety/wakapi/services/misc.go:101.30,104.3 2 0
github.com/muety/wakapi/services/misc.go:109.17,111.3 1 0
github.com/muety/wakapi/services/misc.go:116.17,118.3 1 0
github.com/muety/wakapi/services/user.go:21.73,28.2 1 0
github.com/muety/wakapi/services/user.go:30.74,31.40 1 0
github.com/muety/wakapi/services/user.go:35.2,36.16 2 0
github.com/muety/wakapi/services/user.go:40.2,41.15 2 0
github.com/muety/wakapi/services/user.go:31.40,33.3 1 0
github.com/muety/wakapi/services/user.go:36.16,38.3 1 0
github.com/muety/wakapi/services/user.go:44.72,45.37 1 0
github.com/muety/wakapi/services/user.go:49.2,50.16 2 0
github.com/muety/wakapi/services/user.go:54.2,55.15 2 0
github.com/muety/wakapi/services/user.go:45.37,47.3 1 0
github.com/muety/wakapi/services/user.go:50.16,52.3 1 0
github.com/muety/wakapi/services/user.go:58.76,60.2 1 0
github.com/muety/wakapi/services/user.go:62.86,64.2 1 0
github.com/muety/wakapi/services/user.go:66.58,68.2 1 0
github.com/muety/wakapi/services/user.go:70.86,72.2 1 0
github.com/muety/wakapi/services/user.go:74.61,77.2 2 0
github.com/muety/wakapi/services/user.go:79.48,81.2 1 0
github.com/muety/wakapi/services/user.go:83.102,93.93 2 0
github.com/muety/wakapi/services/user.go:99.2,99.38 1 0
github.com/muety/wakapi/services/user.go:93.93,95.3 1 0
github.com/muety/wakapi/services/user.go:95.8,97.3 1 0
github.com/muety/wakapi/services/user.go:102.73,106.2 3 0
github.com/muety/wakapi/services/user.go:108.78,112.2 3 0
github.com/muety/wakapi/services/user.go:114.99,117.2 2 0
github.com/muety/wakapi/services/user.go:119.106,122.96 3 0
github.com/muety/wakapi/services/user.go:127.2,127.68 1 0
github.com/muety/wakapi/services/user.go:122.96,124.3 1 0
github.com/muety/wakapi/services/user.go:124.8,126.3 1 0
github.com/muety/wakapi/services/user.go:130.85,132.2 1 0
github.com/muety/wakapi/services/user.go:134.57,141.2 4 0
github.com/muety/wakapi/services/user.go:143.38,145.2 1 0
github.com/muety/wakapi/services/user.go:147.57,152.2 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/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/date.go:8.43,10.2 1 1
github.com/muety/wakapi/utils/date.go:12.48,14.2 1 0
github.com/muety/wakapi/utils/date.go:16.41,18.21 2 1
github.com/muety/wakapi/utils/date.go:21.2,21.23 1 1
github.com/muety/wakapi/utils/date.go:18.21,20.3 1 0
github.com/muety/wakapi/utils/date.go:24.46,26.2 1 0
github.com/muety/wakapi/utils/date.go:28.51,30.2 1 0
github.com/muety/wakapi/utils/date.go:32.44,35.2 2 1
github.com/muety/wakapi/utils/date.go:37.52,39.2 1 0
github.com/muety/wakapi/utils/date.go:41.45,43.2 1 0
github.com/muety/wakapi/utils/date.go:45.51,47.2 1 0
github.com/muety/wakapi/utils/date.go:49.44,51.2 1 0
github.com/muety/wakapi/utils/date.go:54.42,56.2 1 1
github.com/muety/wakapi/utils/date.go:59.41,61.21 2 1
github.com/muety/wakapi/utils/date.go:64.2,64.36 1 1
github.com/muety/wakapi/utils/date.go:61.21,63.3 1 1
github.com/muety/wakapi/utils/date.go:68.63,70.2 1 0
github.com/muety/wakapi/utils/date.go:73.62,79.2 5 0
github.com/muety/wakapi/utils/date.go:82.67,85.33 2 1
github.com/muety/wakapi/utils/date.go:94.2,94.18 1 1
github.com/muety/wakapi/utils/date.go:85.33,87.19 2 1
github.com/muety/wakapi/utils/date.go:90.3,91.10 2 1
github.com/muety/wakapi/utils/date.go:87.19,89.4 1 1
github.com/muety/wakapi/utils/date.go:97.50,103.2 5 0
github.com/muety/wakapi/utils/date.go:106.79,109.36 3 1
github.com/muety/wakapi/utils/date.go:113.2,113.21 1 1
github.com/muety/wakapi/utils/date.go:117.2,117.21 1 1
github.com/muety/wakapi/utils/date.go:121.2,121.13 1 1
github.com/muety/wakapi/utils/date.go:109.36,112.3 2 0
github.com/muety/wakapi/utils/date.go:113.21,116.3 2 1
github.com/muety/wakapi/utils/date.go:117.21,120.3 2 1
github.com/muety/wakapi/utils/http.go:9.90,12.58 3 0
github.com/muety/wakapi/utils/http.go:12.58,14.3 1 0
github.com/muety/wakapi/utils/set.go:3.51,5.26 2 0
github.com/muety/wakapi/utils/set.go:8.2,8.12 1 0
github.com/muety/wakapi/utils/set.go:5.26,7.3 1 0
github.com/muety/wakapi/utils/set.go:11.49,13.21 2 0
github.com/muety/wakapi/utils/set.go:16.2,16.14 1 0
github.com/muety/wakapi/utils/set.go:13.21,15.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,35.18 2 0
github.com/muety/wakapi/utils/summary.go:70.2,70.22 1 0
github.com/muety/wakapi/utils/summary.go:36.28,37.26 1 0
github.com/muety/wakapi/utils/summary.go:38.32,40.24 2 0
github.com/muety/wakapi/utils/summary.go:41.31,42.29 1 0
github.com/muety/wakapi/utils/summary.go:43.31,45.27 2 0
github.com/muety/wakapi/utils/summary.go:46.32,47.30 1 0
github.com/muety/wakapi/utils/summary.go:48.32,50.28 2 0
github.com/muety/wakapi/utils/summary.go:51.31,52.29 1 0
github.com/muety/wakapi/utils/summary.go:53.32,54.44 1 0
github.com/muety/wakapi/utils/summary.go:55.41,57.42 2 0
github.com/muety/wakapi/utils/summary.go:58.33,59.45 1 0
github.com/muety/wakapi/utils/summary.go:60.33,61.45 1 0
github.com/muety/wakapi/utils/summary.go:62.35,63.45 1 0
github.com/muety/wakapi/utils/summary.go:64.26,65.21 1 0
github.com/muety/wakapi/utils/summary.go:66.10,67.39 1 0
github.com/muety/wakapi/utils/summary.go:73.73,80.56 5 0
github.com/muety/wakapi/utils/summary.go:96.2,103.8 2 0
github.com/muety/wakapi/utils/summary.go:80.56,82.3 1 0
github.com/muety/wakapi/utils/summary.go:82.8,82.54 1 0
github.com/muety/wakapi/utils/summary.go:82.54,84.3 1 0
github.com/muety/wakapi/utils/summary.go:84.8,86.17 2 0
github.com/muety/wakapi/utils/summary.go:90.3,91.17 2 0
github.com/muety/wakapi/utils/summary.go:86.17,88.4 1 0
github.com/muety/wakapi/utils/summary.go:91.17,93.4 1 0
github.com/muety/wakapi/utils/summary.go:106.48,110.51 2 0
github.com/muety/wakapi/utils/summary.go:113.2,113.12 1 0
github.com/muety/wakapi/utils/summary.go:110.51,112.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/filesystem.go:14.68,16.16 2 0
github.com/muety/wakapi/utils/filesystem.go:20.2,21.15 2 0
github.com/muety/wakapi/utils/filesystem.go:33.2,33.15 1 0
github.com/muety/wakapi/utils/filesystem.go:16.16,18.3 1 0
github.com/muety/wakapi/utils/filesystem.go:21.15,23.47 2 0
github.com/muety/wakapi/utils/filesystem.go:23.47,25.23 2 0
github.com/muety/wakapi/utils/filesystem.go:29.4,29.19 1 0
github.com/muety/wakapi/utils/filesystem.go:25.23,27.5 1 0
github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
github.com/muety/wakapi/utils/template.go:16.37,17.30 1 0
github.com/muety/wakapi/utils/template.go:20.2,20.10 1 0
github.com/muety/wakapi/utils/template.go:17.30,19.3 1 0
github.com/muety/wakapi/services/alias.go:17.77,22.2 1 1
github.com/muety/wakapi/services/alias.go:26.60,27.43 1 1
github.com/muety/wakapi/services/alias.go:30.2,30.14 1 1
@ -602,27 +538,6 @@ github.com/muety/wakapi/services/alias.go:95.21,97.4 1 0
github.com/muety/wakapi/services/alias.go:104.31,106.3 1 0
github.com/muety/wakapi/services/alias.go:111.52,112.51 1 0
github.com/muety/wakapi/services/alias.go:112.51,114.3 1 0
github.com/muety/wakapi/services/heartbeat.go:17.141,23.2 1 0
github.com/muety/wakapi/services/heartbeat.go:25.72,27.2 1 0
github.com/muety/wakapi/services/heartbeat.go:29.80,34.32 3 0
github.com/muety/wakapi/services/heartbeat.go:41.2,41.55 1 0
github.com/muety/wakapi/services/heartbeat.go:34.32,35.36 1 0
github.com/muety/wakapi/services/heartbeat.go:35.36,38.4 2 0
github.com/muety/wakapi/services/heartbeat.go:44.53,46.2 1 0
github.com/muety/wakapi/services/heartbeat.go:48.76,50.2 1 0
github.com/muety/wakapi/services/heartbeat.go:52.96,54.2 1 0
github.com/muety/wakapi/services/heartbeat.go:56.111,58.16 2 0
github.com/muety/wakapi/services/heartbeat.go:61.2,61.43 1 0
github.com/muety/wakapi/services/heartbeat.go:58.16,60.3 1 0
github.com/muety/wakapi/services/heartbeat.go:64.92,66.2 1 0
github.com/muety/wakapi/services/heartbeat.go:68.116,70.2 1 0
github.com/muety/wakapi/services/heartbeat.go:72.78,74.2 1 0
github.com/muety/wakapi/services/heartbeat.go:76.62,78.2 1 0
github.com/muety/wakapi/services/heartbeat.go:80.116,82.16 2 0
github.com/muety/wakapi/services/heartbeat.go:86.2,86.28 1 0
github.com/muety/wakapi/services/heartbeat.go:90.2,90.24 1 0
github.com/muety/wakapi/services/heartbeat.go:82.16,84.3 1 0
github.com/muety/wakapi/services/heartbeat.go:86.28,88.3 1 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
@ -630,6 +545,66 @@ 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/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:80.2,80.61 1 0
github.com/muety/wakapi/services/report.go:94.2,94.24 1 0
github.com/muety/wakapi/services/report.go:74.22,77.3 2 0
github.com/muety/wakapi/services/report.go:80.61,89.47 3 0
github.com/muety/wakapi/services/report.go:89.47,91.4 1 0
github.com/muety/wakapi/services/report.go:97.80,98.22 1 0
github.com/muety/wakapi/services/report.go:102.2,102.29 1 0
github.com/muety/wakapi/services/report.go:107.2,111.16 4 0
github.com/muety/wakapi/services/report.go:116.2,123.65 2 0
github.com/muety/wakapi/services/report.go:128.2,129.12 2 0
github.com/muety/wakapi/services/report.go:98.22,100.3 1 0
github.com/muety/wakapi/services/report.go:102.29,105.3 2 0
github.com/muety/wakapi/services/report.go:111.16,114.3 2 0
github.com/muety/wakapi/services/report.go:123.65,126.3 2 0
github.com/muety/wakapi/services/report.go:132.63,133.41 1 0
github.com/muety/wakapi/services/report.go:140.2,140.12 1 0
github.com/muety/wakapi/services/report.go:133.41,134.30 1 0
github.com/muety/wakapi/services/report.go:134.30,135.16 1 0
github.com/muety/wakapi/services/report.go:135.16,137.5 1 0
github.com/muety/wakapi/services/user.go:21.73,28.2 1 0
github.com/muety/wakapi/services/user.go:30.74,31.40 1 0
github.com/muety/wakapi/services/user.go:35.2,36.16 2 0
github.com/muety/wakapi/services/user.go:40.2,41.15 2 0
github.com/muety/wakapi/services/user.go:31.40,33.3 1 0
github.com/muety/wakapi/services/user.go:36.16,38.3 1 0
github.com/muety/wakapi/services/user.go:44.72,45.37 1 0
github.com/muety/wakapi/services/user.go:49.2,50.16 2 0
github.com/muety/wakapi/services/user.go:54.2,55.15 2 0
github.com/muety/wakapi/services/user.go:45.37,47.3 1 0
github.com/muety/wakapi/services/user.go:50.16,52.3 1 0
github.com/muety/wakapi/services/user.go:58.76,60.2 1 0
github.com/muety/wakapi/services/user.go:62.86,64.2 1 0
github.com/muety/wakapi/services/user.go:66.58,68.2 1 0
github.com/muety/wakapi/services/user.go:70.86,72.2 1 0
github.com/muety/wakapi/services/user.go:74.61,77.2 2 0
github.com/muety/wakapi/services/user.go:79.48,81.2 1 0
github.com/muety/wakapi/services/user.go:83.102,93.93 2 0
github.com/muety/wakapi/services/user.go:99.2,99.38 1 0
github.com/muety/wakapi/services/user.go:93.93,95.3 1 0
github.com/muety/wakapi/services/user.go:95.8,97.3 1 0
github.com/muety/wakapi/services/user.go:102.73,106.2 3 0
github.com/muety/wakapi/services/user.go:108.78,112.2 3 0
github.com/muety/wakapi/services/user.go:114.99,117.2 2 0
github.com/muety/wakapi/services/user.go:119.106,122.96 3 0
github.com/muety/wakapi/services/user.go:127.2,127.68 1 0
github.com/muety/wakapi/services/user.go:122.96,124.3 1 0
github.com/muety/wakapi/services/user.go:124.8,126.3 1 0
github.com/muety/wakapi/services/user.go:130.85,132.2 1 0
github.com/muety/wakapi/services/user.go:134.57,141.2 4 0
github.com/muety/wakapi/services/user.go:143.38,145.2 1 0
github.com/muety/wakapi/services/user.go:147.57,152.2 1 0
github.com/muety/wakapi/services/aggregation.go:29.142,37.2 1 0
github.com/muety/wakapi/services/aggregation.go:46.43,48.37 1 0
github.com/muety/wakapi/services/aggregation.go:52.2,54.19 3 0
@ -680,28 +655,78 @@ github.com/muety/wakapi/services/aggregation.go:176.27,178.3 1 0
github.com/muety/wakapi/services/aggregation.go:181.83,196.41 5 0
github.com/muety/wakapi/services/aggregation.go:196.41,206.3 3 0
github.com/muety/wakapi/services/aggregation.go:209.34,212.2 2 0
github.com/muety/wakapi/services/report.go:24.122,35.33 3 0
github.com/muety/wakapi/services/report.go:41.2,41.12 1 0
github.com/muety/wakapi/services/report.go:35.33,36.31 1 0
github.com/muety/wakapi/services/report.go:36.31,38.4 1 0
github.com/muety/wakapi/services/report.go:44.38,48.16 3 0
github.com/muety/wakapi/services/report.go:52.2,53.26 2 0
github.com/muety/wakapi/services/report.go:48.16,50.3 1 0
github.com/muety/wakapi/services/report.go:53.26,55.3 1 0
github.com/muety/wakapi/services/report.go:60.61,65.65 3 0
github.com/muety/wakapi/services/report.go:73.2,73.65 1 0
github.com/muety/wakapi/services/report.go:85.2,85.24 1 0
github.com/muety/wakapi/services/report.go:65.65,70.3 4 0
github.com/muety/wakapi/services/report.go:73.65,83.3 4 0
github.com/muety/wakapi/services/report.go:88.80,89.22 1 0
github.com/muety/wakapi/services/report.go:93.2,93.29 1 0
github.com/muety/wakapi/services/report.go:98.2,102.16 4 0
github.com/muety/wakapi/services/report.go:107.2,114.65 2 0
github.com/muety/wakapi/services/report.go:119.2,120.12 2 0
github.com/muety/wakapi/services/report.go:89.22,91.3 1 0
github.com/muety/wakapi/services/report.go:93.29,96.3 2 0
github.com/muety/wakapi/services/report.go:102.16,105.3 2 0
github.com/muety/wakapi/services/report.go:114.65,117.3 2 0
github.com/muety/wakapi/services/heartbeat.go:21.141,28.2 1 0
github.com/muety/wakapi/services/heartbeat.go:30.72,33.2 2 0
github.com/muety/wakapi/services/heartbeat.go:35.80,40.32 3 0
github.com/muety/wakapi/services/heartbeat.go:48.2,48.55 1 0
github.com/muety/wakapi/services/heartbeat.go:40.32,41.36 1 0
github.com/muety/wakapi/services/heartbeat.go:45.3,45.43 1 0
github.com/muety/wakapi/services/heartbeat.go:41.36,44.4 2 0
github.com/muety/wakapi/services/heartbeat.go:51.53,53.2 1 0
github.com/muety/wakapi/services/heartbeat.go:55.76,57.2 1 0
github.com/muety/wakapi/services/heartbeat.go:59.96,61.2 1 0
github.com/muety/wakapi/services/heartbeat.go:63.111,65.16 2 0
github.com/muety/wakapi/services/heartbeat.go:68.2,68.43 1 0
github.com/muety/wakapi/services/heartbeat.go:65.16,67.3 1 0
github.com/muety/wakapi/services/heartbeat.go:71.92,73.2 1 0
github.com/muety/wakapi/services/heartbeat.go:75.116,77.2 1 0
github.com/muety/wakapi/services/heartbeat.go:79.78,81.2 1 0
github.com/muety/wakapi/services/heartbeat.go:83.104,85.54 2 0
github.com/muety/wakapi/services/heartbeat.go:89.2,90.16 2 0
github.com/muety/wakapi/services/heartbeat.go:93.2,94.21 2 0
github.com/muety/wakapi/services/heartbeat.go:85.54,87.3 1 0
github.com/muety/wakapi/services/heartbeat.go:90.16,92.3 1 0
github.com/muety/wakapi/services/heartbeat.go:97.62,99.2 1 0
github.com/muety/wakapi/services/heartbeat.go:101.116,103.16 2 0
github.com/muety/wakapi/services/heartbeat.go:107.2,107.28 1 0
github.com/muety/wakapi/services/heartbeat.go:111.2,111.24 1 0
github.com/muety/wakapi/services/heartbeat.go:103.16,105.3 1 0
github.com/muety/wakapi/services/heartbeat.go:107.28,109.3 1 0
github.com/muety/wakapi/services/heartbeat.go:114.96,116.2 1 0
github.com/muety/wakapi/services/heartbeat.go:118.107,120.55 2 0
github.com/muety/wakapi/services/heartbeat.go:120.55,121.58 1 0
github.com/muety/wakapi/services/heartbeat.go:121.58,125.4 1 0
github.com/muety/wakapi/services/heartbeat.go:129.85,135.2 5 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
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
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/misc.go:21.126,28.2 1 0
github.com/muety/wakapi/services/misc.go:40.50,42.48 1 0
github.com/muety/wakapi/services/misc.go:46.2,48.19 3 0
github.com/muety/wakapi/services/misc.go:42.48,44.3 1 0
github.com/muety/wakapi/services/misc.go:51.51,53.16 2 0
github.com/muety/wakapi/services/misc.go:57.2,60.26 3 0
github.com/muety/wakapi/services/misc.go:66.2,68.40 2 0
github.com/muety/wakapi/services/misc.go:73.2,75.33 3 0
github.com/muety/wakapi/services/misc.go:79.2,84.17 2 0
github.com/muety/wakapi/services/misc.go:88.2,91.17 1 0
github.com/muety/wakapi/services/misc.go:95.2,95.12 1 0
github.com/muety/wakapi/services/misc.go:53.16,55.3 1 0
github.com/muety/wakapi/services/misc.go:60.26,65.3 1 0
github.com/muety/wakapi/services/misc.go:68.40,70.3 1 0
github.com/muety/wakapi/services/misc.go:75.33,78.3 2 0
github.com/muety/wakapi/services/misc.go:84.17,86.3 1 0
github.com/muety/wakapi/services/misc.go:91.17,93.3 1 0
github.com/muety/wakapi/services/misc.go:98.116,99.24 1 0
github.com/muety/wakapi/services/misc.go:99.24,100.151 1 0
github.com/muety/wakapi/services/misc.go:100.151,102.4 1 0
github.com/muety/wakapi/services/misc.go:102.9,107.4 1 0
github.com/muety/wakapi/services/summary.go:28.149,36.2 1 1
github.com/muety/wakapi/services/summary.go:40.136,43.66 2 1
github.com/muety/wakapi/services/summary.go:48.2,48.44 1 1

View File

@ -168,6 +168,7 @@ func main() {
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(userService, summaryService)
wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(userService, summaryService)
wakatimeV1UsersHandler := wtV1Routes.NewUsersHandler(userService, heartbeatService)
wakatimeV1ProjectsHandler := wtV1Routes.NewProjectsHandler(userService, heartbeatService)
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
// MVC Handlers
@ -207,6 +208,7 @@ func main() {
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
wakatimeV1StatsHandler.RegisterRoutes(apiRouter)
wakatimeV1UsersHandler.RegisterRoutes(apiRouter)
wakatimeV1ProjectsHandler.RegisterRoutes(apiRouter)
shieldV1BadgeHandler.RegisterRoutes(apiRouter)
// Static Routes

View File

@ -55,6 +55,11 @@ func (m *HeartbeatServiceMock) GetLatestByOriginAndUser(s string, user *models.U
return args.Get(0).(*models.Heartbeat), args.Error(1)
}
func (m *HeartbeatServiceMock) GetEntitySetByUser(u uint8, user *models.User) ([]string, error) {
args := m.Called(u, user)
return args.Get(0).([]string), args.Error(1)
}
func (m *HeartbeatServiceMock) DeleteBefore(time time.Time) error {
args := m.Called(time)
return args.Error(0)

View File

@ -0,0 +1,11 @@
package v1
type ProjectsViewModel struct {
Data []*Project `json:"data"`
}
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Repository string `json:"repository"`
}

View File

@ -31,9 +31,6 @@ type StatsData struct {
func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewModel {
totalTime := summary.TotalTime()
numDays := int(summary.ToTime.T().Sub(summary.FromTime.T()).Hours() / 24)
if math.IsInf(float64(numDays), 0) {
numDays = 0
}
data := &StatsData{
Username: summary.UserID,
@ -45,6 +42,10 @@ func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewMo
DaysIncludingHolidays: numDays,
}
if math.IsInf(data.DailyAverage, 0) || math.IsNaN(data.DailyAverage) {
data.DailyAverage = 0
}
editors := make([]*SummariesEntry, len(summary.Editors))
for i, e := range summary.Editors {
editors[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryEditor))

View File

@ -1,6 +1,7 @@
package repositories
import (
"errors"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -63,6 +64,7 @@ func (r *HeartbeatRepository) GetLatestByOriginAndUser(origin string, user *mode
}
func (r *HeartbeatRepository) GetAllWithin(from, to time.Time, user *models.User) ([]*models.Heartbeat, error) {
// https://stackoverflow.com/a/20765152/3112139
var heartbeats []*models.Heartbeat
if err := r.db.
Where(&models.Heartbeat{UserID: user.ID}).
@ -125,9 +127,8 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
}
if err := r.db.
Model(&models.User{}).
Select("users.id as user, count(heartbeats.id) as count").
Joins("left join heartbeats on users.id = heartbeats.user_id").
Model(&models.Heartbeat{}).
Select("user_id as user, count(id) as count").
Where("user_id in ?", userIds).
Group("user").
Find(&counts).Error; err != nil {
@ -136,6 +137,24 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
return counts, nil
}
func (r HeartbeatRepository) GetEntitySetByUser(entityType uint8, user *models.User) ([]string, error) {
columns := []string{"project", "language", "editor", "operating_system", "machine"}
if int(entityType) >= len(columns) {
// invalid entity type
return nil, errors.New("invalid entity type")
}
var results []string
if err := r.db.
Model(&models.Heartbeat{}).
Distinct(columns[entityType]).
Where(&models.Heartbeat{UserID: user.ID}).
Find(&results).Error; err != nil {
return nil, err
}
return results, nil
}
func (r *HeartbeatRepository) DeleteBefore(t time.Time) error {
if err := r.db.
Where("time <= ?", t.Local()).

View File

@ -27,6 +27,7 @@ type IHeartbeatRepository interface {
Count() (int64, error)
CountByUser(*models.User) (int64, error)
CountByUsers([]*models.User) ([]*models.CountByUser, error)
GetEntitySetByUser(uint8, *models.User) ([]string, error)
DeleteBefore(time.Time) error
}

View File

@ -6,6 +6,7 @@ import (
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
customMiddleware "github.com/muety/wakapi/middlewares/custom"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
@ -34,12 +35,14 @@ type heartbeatResponseVm struct {
}
func (h *HeartbeatApiHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/heartbeat").Subrouter()
r := router.PathPrefix("").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
customMiddleware.NewWakatimeRelayMiddleware().Handler,
)
r.Path("").Methods(http.MethodPost).HandlerFunc(h.Post)
r.PathPrefix("/heartbeat").Methods(http.MethodPost).HandlerFunc(h.Post)
r.PathPrefix("/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
r.PathPrefix("/compat/wakatime/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
}
// @Summary Push a new heartbeat
@ -51,8 +54,12 @@ func (h *HeartbeatApiHandler) RegisterRoutes(router *mux.Router) {
// @Success 201
// @Router /heartbeat [post]
func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
var heartbeats []*models.Heartbeat
user := middlewares.GetPrincipal(r)
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
machineName := r.Header.Get("X-Machine-Name")

View File

@ -6,6 +6,7 @@ import (
"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"
@ -45,18 +46,14 @@ func (h *AllTimeHandler) RegisterRoutes(router *mux.Router) {
// @Success 200 {object} v1.AllTimeViewModel
// @Router /compat/wakatime/v1/users/{user}/all_time_since_today [get]
func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
values, _ := url.ParseQuery(r.URL.RawQuery)
requestedUser := vars["user"]
authorizedUser := middlewares.GetPrincipal(r)
if requestedUser != authorizedUser.ID && requestedUser != "current" {
w.WriteHeader(http.StatusForbidden)
return
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
summary, err, status := h.loadUserSummary(authorizedUser)
summary, err, status := h.loadUserSummary(user)
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))

View File

@ -0,0 +1,73 @@
package v1
import (
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"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"
"strings"
)
type ProjectsHandler struct {
config *conf.Config
userSrvc services.IUserService
heartbeatSrvc services.IHeartbeatService
}
func NewProjectsHandler(userService services.IUserService, heartbeatsService services.IHeartbeatService) *ProjectsHandler {
return &ProjectsHandler{
userSrvc: userService,
heartbeatSrvc: heartbeatsService,
config: conf.Get(),
}
}
func (h *ProjectsHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/compat/wakatime/v1/users/{user}/projects").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
)
r.Path("").Methods(http.MethodGet).HandlerFunc(h.Get)
}
// @Summary Retrieve and fitler the user's projects
// @Description Mimics https://wakatime.com/developers#projects
// @ID get-wakatime-projects
// @Tags wakatime
// @Produce json
// @Param user path string true "User ID to fetch data for (or 'current')"
// @Param q query string true "Query to filter projects by"
// @Security ApiKeyAuth
// @Success 200 {object} v1.ProjectsViewModel
// @Router /compat/wakatime/v1/users/{user}/projects [get]
func (h *ProjectsHandler) Get(w http.ResponseWriter, r *http.Request) {
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
results, err := h.heartbeatSrvc.GetEntitySetByUser(models.SummaryProject, user)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("something went wrong"))
conf.Log().Request(r).Error(err.Error())
return
}
q := r.URL.Query().Get("q")
projects := make([]*v1.Project, 0, len(results))
for _, p := range results {
if strings.HasPrefix(p, q) {
projects = append(projects, &v1.Project{ID: p, Name: p})
}
}
vm := &v1.ProjectsViewModel{Data: projects}
utils.RespondJSON(w, r, http.StatusOK, vm)
}

View File

@ -7,6 +7,7 @@ import (
"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"
@ -36,7 +37,7 @@ func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {
r.Path("").Methods(http.MethodGet).HandlerFunc(h.Get)
}
// TODO: Support parameters: project, branches, timeout, writes_only, timezone
// TODO: Support parameters: project, branches, timeout, writes_only
// See https://wakatime.com/developers#summaries.
// Timezone can be specified via an offset suffix (e.g. +02:00) in date strings.
// Requires https://github.com/muety/wakapi/issues/108.
@ -54,13 +55,9 @@ func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {
// @Success 200 {object} v1.SummariesViewModel
// @Router /compat/wakatime/v1/users/{user}/summaries [get]
func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
requestedUser := vars["user"]
authorizedUser := middlewares.GetPrincipal(r)
if requestedUser != authorizedUser.ID && requestedUser != "current" {
w.WriteHeader(http.StatusForbidden)
return
_, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
summaries, err, status := h.loadUserSummaries(r)
@ -82,35 +79,42 @@ func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary, error, int) {
user := middlewares.GetPrincipal(r)
params := r.URL.Query()
rangeParam, startParam, endParam := params.Get("range"), params.Get("start"), params.Get("end")
rangeParam, startParam, endParam, tzParam := params.Get("range"), params.Get("start"), params.Get("end"), params.Get("timezone")
timezone := user.TZ()
if tzParam != "" {
if tz, err := time.LoadLocation(tzParam); err == nil {
timezone = tz
}
}
var start, end time.Time
if rangeParam != "" {
// range param takes precedence
if err, parsedFrom, parsedTo := utils.ResolveIntervalRawTZ(rangeParam, user.TZ()); err == nil {
if err, parsedFrom, parsedTo := utils.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, user.TZ()); err == nil && startParam == endParam {
} else if err, parsedFrom, parsedTo := utils.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), user.TZ())
start, err = utils.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), user.TZ())
end, err = utils.ParseDateTimeTZ(strings.Replace(endParam, " ", "+", 1), timezone)
if err != nil {
return nil, errors.New("missing required 'end' parameter"), http.StatusBadRequest
}
}
// wakatime iterprets end date as "inclusive", wakapi usually as "exclusive"
// wakatime interprets end date as "inclusive", wakapi usually as "exclusive"
// i.e. for wakatime, an interval 2021-04-29 - 2021-04-29 is actually 2021-04-29 - 2021-04-30,
// while for wakapi it would be empty
// see https://github.com/muety/wakapi/issues/192

View File

@ -5,6 +5,7 @@ import (
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
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"
@ -42,18 +43,13 @@ func (h *UsersHandler) RegisterRoutes(router *mux.Router) {
// @Success 200 {object} v1.UserViewModel
// @Router /compat/wakatime/v1/users/{user} [get]
func (h *UsersHandler) Get(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
requestedUser := vars["user"]
authorizedUser := middlewares.GetPrincipal(r)
if requestedUser != authorizedUser.ID && requestedUser != "current" {
w.WriteHeader(http.StatusForbidden)
return
wakapiUser, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
user := v1.NewFromUser(authorizedUser)
if hb, err := h.heartbeatSrvc.GetLatestByUser(authorizedUser); err == nil {
user := v1.NewFromUser(wakapiUser)
if hb, err := h.heartbeatSrvc.GetLatestByUser(wakapiUser); err == nil {
user = user.WithLatestHeartbeat(hb)
} else {
conf.Log().Request(r).Error("%v", err)

View File

@ -450,6 +450,13 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
h.regenerateSummaries(user)
if !user.HasData {
user.HasData = true
if _, err := h.userSrvc.Update(user); err != nil {
conf.Log().Request(r).Error("failed to set 'has_data' flag for user %s %v", user.ID, err)
}
}
if user.Email != "" {
if err := h.mailSrvc.SendImportNotification(user, time.Now().Sub(start), int(countAfter-countBefore)); err != nil {
conf.Log().Request(r).Error("failed to send import notification mail to %s %v", user.ID, err)

View File

@ -0,0 +1,46 @@
package utils
import (
"errors"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
"net/http"
)
// CheckEffectiveUser extracts the requested user from a URL (like '/users/{user}'), compares it with the currently authorized user and writes an HTTP error if they differ.
// Fallback can be used to manually set a value for '{user}' if none is present.
func CheckEffectiveUser(w http.ResponseWriter, r *http.Request, userService services.IUserService, fallback string) (*models.User, error) {
var vars = mux.Vars(r)
var authorizedUser, requestedUser *models.User
if vars["user"] == "" {
vars["user"] = fallback
}
authorizedUser = middlewares.GetPrincipal(r)
if authorizedUser != nil {
if vars["user"] == "current" {
vars["user"] = authorizedUser.ID
}
}
requestedUser, err := userService.GetUserById(vars["user"])
if err != nil {
err := errors.New("user not found")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(err.Error()))
return nil, err
}
if authorizedUser == nil || authorizedUser.ID != requestedUser.ID {
err := errors.New(conf.ErrUnauthorized)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(err.Error()))
return nil, err
}
return authorizedUser, nil
}

View File

@ -1,3 +1,3 @@
#!/bin/bash
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secretpassword -e MYSQL_DATABASE=wakapi_local -e MYSQL_USER=wakapi_user -e MYSQL_PASSWORD=wakapi --name wakapi-mysql mysql:5
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secretpassword -e MYSQL_DATABASE=wakapi_local -e MYSQL_USER=wakapi_user -e MYSQL_PASSWORD=wakapi --name wakapi-mysql mysql:8

View File

@ -1,8 +1,11 @@
package services
import (
"fmt"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache"
"time"
"github.com/muety/wakapi/models"
@ -10,6 +13,7 @@ import (
type HeartbeatService struct {
config *config.Config
cache *cache.Cache
repository repositories.IHeartbeatRepository
languageMappingSrvc ILanguageMappingService
}
@ -17,12 +21,14 @@ type HeartbeatService struct {
func NewHeartbeatService(heartbeatRepo repositories.IHeartbeatRepository, languageMappingService ILanguageMappingService) *HeartbeatService {
return &HeartbeatService{
config: config.Get(),
cache: cache.New(24*time.Hour, 24*time.Hour),
repository: heartbeatRepo,
languageMappingSrvc: languageMappingService,
}
}
func (srv *HeartbeatService) Insert(heartbeat *models.Heartbeat) error {
srv.updateEntityUserCacheByHeartbeat(heartbeat)
return srv.repository.InsertBatch([]*models.Heartbeat{heartbeat})
}
@ -36,6 +42,7 @@ func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat) error {
filteredHeartbeats = append(filteredHeartbeats, hb)
hashes[hb.Hash] = true
}
srv.updateEntityUserCacheByHeartbeat(hb)
}
return srv.repository.InsertBatch(filteredHeartbeats)
@ -73,6 +80,20 @@ func (srv *HeartbeatService) GetFirstByUsers() ([]*models.TimeByUser, error) {
return srv.repository.GetFirstByUsers()
}
func (srv *HeartbeatService) GetEntitySetByUser(entityType uint8, user *models.User) ([]string, error) {
cacheKey := srv.getEntityUserCacheKey(entityType, user)
if results, found := srv.cache.Get(cacheKey); found {
return utils.SetToStrings(results.(map[string]bool)), nil
}
results, err := srv.repository.GetEntitySetByUser(entityType, user)
if err != nil {
return nil, err
}
srv.cache.Set(cacheKey, utils.StringsToSet(results), cache.DefaultExpiration)
return results, nil
}
func (srv *HeartbeatService) DeleteBefore(t time.Time) error {
return srv.repository.DeleteBefore(t)
}
@ -89,3 +110,26 @@ func (srv *HeartbeatService) augmented(heartbeats []*models.Heartbeat, userId st
return heartbeats, nil
}
func (srv *HeartbeatService) getEntityUserCacheKey(entityType uint8, user *models.User) string {
return fmt.Sprintf("entity_set_%d_%s", entityType, user.ID)
}
func (srv *HeartbeatService) updateEntityUserCache(entityType uint8, entityKey string, user *models.User) {
cacheKey := srv.getEntityUserCacheKey(entityType, user)
if entities, found := srv.cache.Get(cacheKey); found {
if _, ok := entities.(map[string]bool)[entityKey]; !ok {
// new project / language / ..., which is not yet present in cache, arrived as part of a heartbeats
// -> invalidate cache
srv.cache.Delete(cacheKey)
}
}
}
func (srv *HeartbeatService) updateEntityUserCacheByHeartbeat(hb *models.Heartbeat) {
srv.updateEntityUserCache(models.SummaryProject, hb.Project, hb.User)
srv.updateEntityUserCache(models.SummaryLanguage, hb.Language, hb.User)
srv.updateEntityUserCache(models.SummaryEditor, hb.Editor, hb.User)
srv.updateEntityUserCache(models.SummaryOS, hb.OperatingSystem, hb.User)
srv.updateEntityUserCache(models.SummaryMachine, hb.Machine, hb.User)
}

View File

@ -6,31 +6,40 @@ import (
"github.com/leandro-lugaresi/hub"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"math/rand"
"sync"
"time"
)
var reportLock = sync.Mutex{}
// 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
type ReportService struct {
config *config.Config
eventBus *hub.Hub
summaryService ISummaryService
userService IUserService
mailService IMailService
schedulersWeekly map[string]*gocron.Scheduler // user id -> scheduler
config *config.Config
eventBus *hub.Hub
summaryService ISummaryService
userService IUserService
mailService IMailService
scheduler *gocron.Scheduler
rand *rand.Rand
}
func NewReportService(summaryService ISummaryService, userService IUserService, mailService IMailService) *ReportService {
srv := &ReportService{
config: config.Get(),
eventBus: config.EventBus(),
summaryService: summaryService,
userService: userService,
mailService: mailService,
schedulersWeekly: map[string]*gocron.Scheduler{},
config: config.Get(),
eventBus: config.EventBus(),
summaryService: summaryService,
userService: userService,
mailService: mailService,
scheduler: gocron.NewScheduler(time.Local),
rand: rand.New(rand.NewSource(time.Now().Unix())),
}
srv.scheduler.StartAsync()
sub := srv.eventBus.Subscribe(0, config.EventUserUpdate)
go func(sub *hub.Subscription) {
for m := range sub.Receiver {
@ -62,24 +71,24 @@ func (srv *ReportService) SyncSchedule(u *models.User) bool {
defer reportLock.Unlock()
// unschedule
if s, ok := srv.schedulersWeekly[u.ID]; ok && !u.ReportsWeekly {
s.Stop()
s.Clear()
delete(srv.schedulersWeekly, u.ID)
if !u.ReportsWeekly {
_ = srv.scheduler.RemoveByTag(u.ID)
return false
}
// schedule
if _, ok := srv.schedulersWeekly[u.ID]; !ok && u.ReportsWeekly {
s := gocron.NewScheduler(u.TZ())
s.
if j := srv.getJobByTag(u.ID); j == nil && u.ReportsWeekly {
t, _ := time.ParseInLocation("15:04", srv.config.App.GetWeeklyReportTime(), u.TZ())
t = t.Add(time.Duration(srv.rand.Intn(offsetIntervalMin)*srv.rand.Intn(2)) * time.Minute)
if _, err := srv.scheduler.
Every(1).
Week().
Weekday(srv.config.App.GetWeeklyReportDay()).
At(srv.config.App.GetWeeklyReportTime()).
Do(srv.Run, u, 7*24*time.Hour)
s.StartAsync()
srv.schedulersWeekly[u.ID] = s
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)
}
}
return u.ReportsWeekly
@ -119,3 +128,14 @@ func (srv *ReportService) Run(user *models.User, duration time.Duration) error {
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
}

View File

@ -35,6 +35,7 @@ type IHeartbeatService interface {
GetFirstByUsers() ([]*models.TimeByUser, error)
GetLatestByUser(*models.User) (*models.Heartbeat, error)
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
GetEntitySetByUser(uint8, *models.User) ([]string, error)
DeleteBefore(time.Time) error
}

View File

@ -160,6 +160,48 @@ var doc = `{
}
}
},
"/compat/wakatime/v1/users/{user}/projects": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Mimics https://wakatime.com/developers#projects",
"produces": [
"application/json"
],
"tags": [
"wakatime"
],
"summary": "Retrieve and fitler the user's projects",
"operationId": "get-wakatime-projects",
"parameters": [
{
"type": "string",
"description": "User ID to fetch data for (or 'current')",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Query to filter projects by",
"name": "q",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ProjectsViewModel"
}
}
}
}
},
"/compat/wakatime/v1/users/{user}/stats/{range}": {
"get": {
"security": [
@ -574,6 +616,31 @@ var doc = `{
}
}
},
"v1.Project": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"repository": {
"type": "string"
}
}
},
"v1.ProjectsViewModel": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/v1.Project"
}
}
}
},
"v1.StatsData": {
"type": "object",
"properties": {

View File

@ -144,6 +144,48 @@
}
}
},
"/compat/wakatime/v1/users/{user}/projects": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Mimics https://wakatime.com/developers#projects",
"produces": [
"application/json"
],
"tags": [
"wakatime"
],
"summary": "Retrieve and fitler the user's projects",
"operationId": "get-wakatime-projects",
"parameters": [
{
"type": "string",
"description": "User ID to fetch data for (or 'current')",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Query to filter projects by",
"name": "q",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ProjectsViewModel"
}
}
}
}
},
"/compat/wakatime/v1/users/{user}/stats/{range}": {
"get": {
"security": [
@ -558,6 +600,31 @@
}
}
},
"v1.Project": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"repository": {
"type": "string"
}
}
},
"v1.ProjectsViewModel": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/v1.Project"
}
}
}
},
"v1.StatsData": {
"type": "object",
"properties": {

View File

@ -117,6 +117,22 @@ definitions:
schemaVersion:
type: integer
type: object
v1.Project:
properties:
id:
type: string
name:
type: string
repository:
type: string
type: object
v1.ProjectsViewModel:
properties:
data:
items:
$ref: '#/definitions/v1.Project'
type: array
type: object
v1.StatsData:
properties:
daily_average:
@ -392,6 +408,33 @@ paths:
summary: Retrieve summary for all time
tags:
- wakatime
/compat/wakatime/v1/users/{user}/projects:
get:
description: Mimics https://wakatime.com/developers#projects
operationId: get-wakatime-projects
parameters:
- description: User ID to fetch data for (or 'current')
in: path
name: user
required: true
type: string
- description: Query to filter projects by
in: query
name: q
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ProjectsViewModel'
security:
- ApiKeyAuth: []
summary: Retrieve and fitler the user's projects
tags:
- wakatime
/compat/wakatime/v1/users/{user}/stats/{range}:
get:
description: Mimics https://wakatime.com/developers#stats

View File

@ -0,0 +1,879 @@
{
"info": {
"_postman_id": "472dcea5-a8b1-4507-8480-61644295c35b",
"name": "Wakapi API Tests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Auth",
"item": [
{
"name": "Sign up user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Body matches string\", function () {",
" pm.expect(pm.response.text()).to.include(\"Account created successfully\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "location",
"value": "{{TZ}}",
"type": "text"
},
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "email",
"value": "testuser@wakapi.dev",
"type": "text"
},
{
"key": "password",
"value": "testpassword",
"type": "text"
},
{
"key": "password_repeat",
"value": "testpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/signup",
"host": [
"{{BASE_URL}}"
],
"path": [
"signup"
]
}
},
"response": []
},
{
"name": "Sign up existing user (conflict)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 409\", function () {",
" pm.response.to.have.status(409);",
"});",
"",
"pm.test(\"Body matches string\", function () {",
" pm.expect(pm.response.text()).to.include(\"User already existing\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "location",
"value": "{{TZ}}",
"type": "text"
},
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "email",
"value": "testuser@wakapi.dev",
"type": "text"
},
{
"key": "password",
"value": "testpassword",
"type": "text"
},
{
"key": "password_repeat",
"value": "testpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/signup",
"host": [
"{{BASE_URL}}"
],
"path": [
"signup"
]
}
},
"response": []
},
{
"name": "Login",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 302\", function () {",
" pm.response.to.have.status(302);",
"});",
"",
"pm.test(\"Redirect to summary\", function () {",
" pm.expect(pm.response.headers.get(\"Location\")).to.eql(\"/summary\");",
"});",
"",
"pm.test(\"Sets cookie\", function () {",
" pm.expect(pm.response.headers.get(\"Set-Cookie\")).to.include(\"wakapi_auth=\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true,
"followRedirects": false
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "password",
"value": "testpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/login",
"host": [
"{{BASE_URL}}"
],
"path": [
"login"
]
}
},
"response": []
},
{
"name": "Login (wrong password)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 401\", function () {",
" pm.response.to.have.status(401);",
"});",
"",
"pm.test(\"No redirect\", function () {",
" pm.response.to.not.have.header(\"Location\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true,
"followRedirects": false
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "password",
"value": "wrongpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/login",
"host": [
"{{BASE_URL}}"
],
"path": [
"login"
]
}
},
"response": []
}
]
},
{
"name": "Heartbeats",
"item": [
{
"name": "Create heartbeats",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Response body is correct\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.responses.length).to.eql(2);",
" pm.expect(jsonData.responses[0].length).to.eql(2);",
" pm.expect(jsonData.responses[1].length).to.eql(2);",
" pm.expect(jsonData.responses[0][1]).to.eql(201);",
" pm.expect(jsonData.responses[1][1]).to.eql(201);",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "[{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": {{tsNowMinus1Min}}\n},\n{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": {{tsNowMinus2Min}}\n}]",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{BASE_URL}}/api/heartbeat",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"heartbeat"
]
}
},
"response": []
},
{
"name": "Create heartbeats (unauthorized)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 401\", function () {",
" pm.response.to.have.status(401);",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "[{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": {{tsNowMinus1Min}}\n}]",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{BASE_URL}}/api/heartbeat",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"heartbeat"
]
}
},
"response": []
}
]
},
{
"name": "Summary",
"item": [
{
"name": "Get summary (today)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
"",
"/*",
"// This is something the unit tests are supposed to check",
"pm.test(\"Correct summary range\", function () {",
" const jsonData = pm.response.json();",
" const from = moment(jsonData.from)",
" const to = moment(jsonData.to)",
"",
" pm.expect(moment.duration(moment().diff(from.add(2, 'm'))).asSeconds()).to.lt(10); // first heartbeat is now minus 1 min minus some latency",
" pm.expect(moment.duration(moment().diff(to.add(1, 'm'))).asSeconds()).to.lt(10); // first heartbeat is now minus 1 min minus some latency",
"});",
"*/"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?interval=today",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "interval",
"value": "today"
}
]
}
},
"response": []
},
{
"name": "Get summary (last 7 days)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?interval=last_7_days",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "interval",
"value": "last_7_days"
}
]
}
},
"response": []
},
{
"name": "Get summary (week)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?start=week",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "start",
"value": "week"
}
]
}
},
"response": []
},
{
"name": "Get summary (range)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
"",
"pm.test(\"Correct dates\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(moment(jsonData.from).unix()).to.gt(moment(pm.variables.get('tsStartOfDayDate')).unix())",
" pm.expect(moment(jsonData.to).unix()).to.gt(moment(pm.variables.get('tsEndOfDayDate')).unix())",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?from={{tsStartOfDayDate}}&to={{tsEndOfTomorrowDate}}",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "from",
"value": "{{tsStartOfDayDate}}"
},
{
"key": "to",
"value": "{{tsEndOfTomorrowDate}}"
}
]
}
},
"response": []
},
{
"name": "Get summary (default tz)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct time zone\", function () {",
" const jsonData = pm.response.json();",
" const targetDateTz = moment(`2021-05-28T00:00:00${pm.variables.get('TZ_OFFSET')}`)",
" pm.expect(moment(jsonData.from).isSame(targetDateTz)).to.eql(true)",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true,
"disableUrlEncoding": false
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?from=2021-05-28&to=2021-05-28",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "from",
"value": "2021-05-28"
},
{
"key": "to",
"value": "2021-05-28"
}
]
}
},
"response": []
},
{
"name": "Get summary (parse tz)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct time zone\", function () {",
" const jsonData = pm.response.json();",
" // when it was midnight in UTC+3, it was still 11 pm in Germany",
" const targetDateTz = moment(`2021-05-28T00:00:00${pm.variables.get('TZ_OFFSET')}`).add(-1, 'h')",
" pm.expect(moment(jsonData.from).isSame(targetDateTz)).to.eql(true)",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?from=2021-05-28T00:00:00%2B03:00&to=2021-05-28T00:00:00%2B03:00",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "from",
"value": "2021-05-28T00:00:00%2B03:00"
},
{
"key": "to",
"value": "2021-05-28T00:00:00%2B03:00"
}
]
}
},
"response": []
}
]
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const moment = require('moment')",
"",
"const now = moment()",
"const startOfDay = moment().startOf('day')",
"const endOfDay = moment().endOf('day')",
"const endOfTomorrow = moment().add(1, 'd').endOf('day')",
"",
"console.log(`Current timestamp is: ${now.format('x') / 1000}`)",
"",
"",
"// Auth stuff",
"const readApiKey = pm.variables.get('READUSER_API_KEY')",
"const writeApiKey = pm.variables.get('WRITEUSER_API_KEY')",
"",
"if (!readApiKey || !writeApiKey) {",
" throw new Error('no api key given')",
"}",
"",
"pm.variables.set('READUSER_TOKEN', base64encode(readApiKey))",
"pm.variables.set('WRITEUSER_TOKEN', base64encode(writeApiKey))",
"",
"function base64encode(str) {",
" return Buffer.from(str, 'utf-8').toString('base64')",
"}",
"",
"// Heartbeat stuff",
"pm.variables.set('tsNow', now.format('x') / 1000)",
"pm.variables.set('tsNowMinus1Min', now.add(-1, 'm').format('x') / 1000)",
"pm.variables.set('tsNowMinus2Min', now.add(-2, 'm').format('x') / 1000)",
"pm.variables.set('tsNowMinus3Min', now.add(-3, 'm').format('x') / 1000)",
"pm.variables.set('tsStartOfDay', startOfDay.format('x') / 1000)",
"pm.variables.set('tsEndOfDay', endOfDay.format('x') / 1000)",
"pm.variables.set('tsEndOfTomorrow', endOfTomorrow.format('x') / 1000)",
"pm.variables.set('tsStartOfDayIso', startOfDay.toISOString())",
"pm.variables.set('tsEndOfDayIso', endOfDay.toISOString())",
"pm.variables.set('tsEndOfTomorrowIso', endOfTomorrow.toISOString())",
"pm.variables.set('tsStartOfDayDate', startOfDay.format('YYYY-MM-DD'))",
"pm.variables.set('tsEndOfDayDate', endOfDay.format('YYYY-MM-DD'))",
"pm.variables.set('tsEndOfTomorrowDate', endOfTomorrow.format('YYYY-MM-DD'))"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "BASE_URL",
"value": "http://localhost:3000"
},
{
"key": "READUSER_API_KEY",
"value": "33e7f538-0dce-4eba-8ffe-53db6814ed42"
},
{
"key": "WRITEUSER_API_KEY",
"value": "f7aa255c-8647-4d0b-b90f-621c58fd580f"
},
{
"key": "TZ",
"value": "Europe/Berlin"
},
{
"key": "TZ_OFFSET",
"value": "+02:00"
}
]
}

View File

@ -0,0 +1,61 @@
env: production
server:
listen_ipv4: 127.0.0.1
listen_ipv6:
tls_cert_path:
tls_key_path:
port: 3000
base_path: /
public_url: http://localhost:3000
app:
aggregation_time: '02:15'
report_time_weekly: 'fri,18:00'
inactive_days: 7
custom_languages:
vue: Vue
jsx: JSX
svelte: Svelte
db:
host:
port:
user:
password:
name: wakapi_testing.db
dialect: sqlite3
charset:
max_conn: 2
ssl: false
automgirate_fail_silently: false
security:
password_salt:
insecure_cookies: true
cookie_max_age: 172800
allow_signup: true
expose_metrics: false
sentry:
dsn:
enable_tracing: false
sample_rate:
sample_rate_heartbeats:
mail:
enabled: false
provider: smtp
sender: Wakapi <noreply@wakapi.dev>
smtp:
host:
port:
username:
password:
tls:
mailwhale:
url:
client_id:
client_secret:

4
testing/data.sql Normal file
View File

@ -0,0 +1,4 @@
BEGIN TRANSACTION;
INSERT INTO "users" ("id","api_key","email","location","password","created_at","last_logged_in_at","share_data_max_days","share_editors","share_languages","share_projects","share_oss","share_machines","is_admin","has_data","wakatime_api_key","reset_token","reports_weekly") VALUES ('readuser','33e7f538-0dce-4eba-8ffe-53db6814ed42','','Europe/Berlin','$2a$10$RCyfAFdlZdFJVWbxKz4f2uJ/MospiE1EFAIjvRizC4Nop9GfjgKzW','2021-05-28 12:34:25','2021-05-28 14:34:34.178+02:00',0,0,0,0,0,0,0,0,'','',0);
INSERT INTO "users" ("id","api_key","email","location","password","created_at","last_logged_in_at","share_data_max_days","share_editors","share_languages","share_projects","share_oss","share_machines","is_admin","has_data","wakatime_api_key","reset_token","reports_weekly") VALUES ('writeuser','f7aa255c-8647-4d0b-b90f-621c58fd580f','','Europe/Berlin','$2a$10$vsksPpiXZE9/xG9pRrZP.eKkbe/bGWW4wpPoXqvjiImZqMbN5c4Km','2021-05-28 12:34:56','2021-05-28 14:35:05.118+02:00',0,0,0,0,0,0,0,1,'','',0);
COMMIT;

40
testing/run_api_tests.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
if [ ! -f "wakapi" ]; then
echo "Wakapi executable not found. Run 'go build' first."
exit 1
fi
if ! command -v newman &> /dev/null
then
echo "Newman could not be found. Run 'npm install -g newman' first."
exit 1
fi
cd "$(dirname "$0")"
echo "Creating database and schema ..."
sqlite3 wakapi_testing.db < schema.sql
echo "Importing seed data ..."
sqlite3 wakapi_testing.db < data.sql
echo "Running Wakapi testing instance in background ..."
screen -S wakapi_testing -dm bash -c "../wakapi -config config.testing.yml"
echo "Waiting for Wakapi to come up ..."
until $(curl --output /dev/null --silent --get --fail http://localhost:3000/api/health); do
printf '.'
sleep 1
done
echo ""
echo "Running test collection ..."
newman run "Wakapi API Tests.postman_collection.json"
echo "Shutting down Wakapi ..."
screen -S wakapi_testing -X quit
echo "Deleting database ..."
rm wakapi_testing.db

147
testing/schema.sql Normal file
View File

@ -0,0 +1,147 @@
BEGIN TRANSACTION;
DROP TABLE IF EXISTS "users";
CREATE TABLE IF NOT EXISTS "users" (
"id" text,
"api_key" text UNIQUE,
"email" text,
"password" text,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"last_logged_in_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"share_data_max_days" integer DEFAULT 0,
"share_editors" numeric DEFAULT false,
"share_languages" numeric DEFAULT false,
"share_projects" numeric DEFAULT false,
"share_oss" numeric DEFAULT false,
"share_machines" numeric DEFAULT false,
"is_admin" numeric DEFAULT false,
"has_data" numeric DEFAULT false,
"wakatime_api_key" text,
"reset_token" text,
"location" text,
"reports_weekly" numeric DEFAULT false,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "key_string_values";
CREATE TABLE IF NOT EXISTS "key_string_values" (
"key" text,
"value" text,
PRIMARY KEY("key")
);
DROP TABLE IF EXISTS "summary_items";
CREATE TABLE IF NOT EXISTS "summary_items" (
"id" integer,
"summary_id" integer,
"type" integer,
"key" text,
"total" integer,
CONSTRAINT "fk_summaries_languages" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summary_items_summary" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_machines" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_projects" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_operating_systems" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_editors" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "aliases";
CREATE TABLE IF NOT EXISTS "aliases" (
"id" integer,
"type" integer NOT NULL,
"user_id" text NOT NULL,
"key" text NOT NULL,
"value" text NOT NULL,
CONSTRAINT "fk_aliases_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "heartbeats";
CREATE TABLE IF NOT EXISTS "heartbeats" (
"id" integer,
"user_id" text NOT NULL,
"entity" text NOT NULL,
"type" text,
"category" text,
"project" text,
"branch" text,
"language" text,
"is_write" numeric,
"editor" text,
"operating_system" text,
"machine" text,
"time" timestamp,
"hash" varchar(17),
"origin" text,
"origin_id" text,
"created_at" timestamp,
CONSTRAINT "fk_heartbeats_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "summaries";
CREATE TABLE IF NOT EXISTS "summaries" (
"id" integer,
"user_id" text NOT NULL,
"from_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"to_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "fk_summaries_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "language_mappings";
CREATE TABLE IF NOT EXISTS "language_mappings" (
"id" integer,
"user_id" text NOT NULL,
"extension" varchar(16),
"language" varchar(64),
CONSTRAINT "fk_language_mappings_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP INDEX IF EXISTS "idx_user_email";
CREATE INDEX IF NOT EXISTS "idx_user_email" ON "users" (
"email"
);
DROP INDEX IF EXISTS "idx_type";
CREATE INDEX IF NOT EXISTS "idx_type" ON "summary_items" (
"type"
);
DROP INDEX IF EXISTS "idx_alias_type_key";
CREATE INDEX IF NOT EXISTS "idx_alias_type_key" ON "aliases" (
"type",
"key"
);
DROP INDEX IF EXISTS "idx_alias_user";
CREATE INDEX IF NOT EXISTS "idx_alias_user" ON "aliases" (
"user_id"
);
DROP INDEX IF EXISTS "idx_time";
CREATE INDEX IF NOT EXISTS "idx_time" ON "heartbeats" (
"time"
);
DROP INDEX IF EXISTS "idx_heartbeats_hash";
CREATE UNIQUE INDEX IF NOT EXISTS "idx_heartbeats_hash" ON "heartbeats" (
"hash"
);
DROP INDEX IF EXISTS "idx_time_user";
CREATE INDEX IF NOT EXISTS "idx_time_user" ON "heartbeats" (
"user_id"
);
DROP INDEX IF EXISTS "idx_entity";
CREATE INDEX IF NOT EXISTS "idx_entity" ON "heartbeats" (
"entity"
);
DROP INDEX IF EXISTS "idx_language";
CREATE INDEX IF NOT EXISTS "idx_language" ON "heartbeats" (
"language"
);
DROP INDEX IF EXISTS "idx_time_summary_user";
CREATE INDEX IF NOT EXISTS "idx_time_summary_user" ON "summaries" (
"user_id",
"from_time",
"to_time"
);
DROP INDEX IF EXISTS "idx_language_mapping_composite";
CREATE UNIQUE INDEX IF NOT EXISTS "idx_language_mapping_composite" ON "language_mappings" (
"user_id",
"extension"
);
DROP INDEX IF EXISTS "idx_language_mapping_user";
CREATE INDEX IF NOT EXISTS "idx_language_mapping_user" ON "language_mappings" (
"user_id"
);
COMMIT;

17
utils/set.go Normal file
View File

@ -0,0 +1,17 @@
package utils
func StringsToSet(slice []string) map[string]bool {
set := make(map[string]bool, len(slice))
for _, e := range slice {
set[e] = true
}
return set
}
func SetToStrings(set map[string]bool) []string {
slice := make([]string, 0, len(set))
for k := range set {
slice = append(slice, k)
}
return slice
}

View File

@ -1 +1 @@
1.27.0
1.27.3

View File

@ -69,6 +69,7 @@
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; Built by developers for developers</li>
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; Fancy statistics and plots</li>
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; Cool badges for readmes</li>
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; Weekly e-mail reports</li>
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; Intuitive REST API</li>
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; Compatible with <a href="https://wakatime.com" target="_blank" rel="noopener noreferrer" class="underline">Wakatime</a></li>
<li><span class="iconify inline text-green-700" data-icon="ant-design:check-square-filled"></span> &nbsp; <a href="https://prometheus.io" target="_blank" rel="noopener noreferrer" class="underline">Prometheus</a> metrics</li>

View File

@ -192,7 +192,7 @@
# <strong>Step 2:</strong> Adapt your config<br>
$ vi ~/.wakatime.cfg<br>
# Set <em>api_url = <span class="with-url-inner">%s/api/heartbeat</span></em><br>
# Set <em>api_url = <span class="with-url-inner">%s/api</span></em><br>
# Set <em>api_key = <span id="api-key-instruction"></span></em><br><br>
# <strong>Step 3:</strong> Start coding and then check back here!