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

Compare commits

...

510 Commits

Author SHA1 Message Date
0f3b41c2dd fix(ci): adapt docker and gha build to use go 1.19 2022-12-03 00:32:30 +01:00
5ae7527b7b feat: implement data retention mechanism 2022-12-01 20:26:03 +01:00
2db065d47a Merge branch 'muety/427-job-processing' 2022-12-01 15:55:40 +01:00
0e5c5a56d2 chore: dependency upgrades 2022-12-01 15:31:19 +01:00
a4b89d3a69 fix: concurrency bugs with summary aggregation and user counting 2022-12-01 14:13:52 +01:00
aab9e98ebd fix: error handling for user counting
fix: make user counting thread-safe
2022-12-01 13:46:06 +01:00
d4945c982f fix: tests 2022-12-01 11:11:45 +01:00
964405f349 chore: refine report scheduling 2022-12-01 10:57:51 +01:00
21f6809f05 refactor: split utility functions into utils and helpers 2022-12-01 10:57:07 +01:00
c5fda02900 docs: update default cron expressions 2022-12-01 10:10:39 +01:00
10e432c185 Merge pull request #431 from rummik/patch-1
Increase avatar entropy
2022-11-29 23:07:28 +01:00
f121112d09 Increase avatar entropy 2022-11-26 16:26:03 -05:00
c13fc96a16 refactor: use job queue for data imports 2022-11-20 11:09:51 +01:00
61f13fce20 fix: prometheus metrics types 2022-11-20 10:59:06 +01:00
99e50b1062 chore: logging 2022-11-20 10:12:34 +01:00
4ce75c2acb chore: clean up dependencies 2022-11-20 10:11:23 +01:00
fcca881cfc refactor: move more background jobs to using job queue 2022-11-20 10:10:24 +01:00
e2ef54152d refactor(wip): introduce job processing system
refactor: adapt report generation scheduling
2022-11-19 22:21:51 +01:00
b1a12a5759 Merge pull request #430 from xiecang/bugfix/send_smtp_mail
fix: ignore error "starttls command has been already sent"
2022-11-17 08:31:24 +01:00
ae407fffca fix: ignore error "starttls command has been already sent" 2022-11-16 12:29:20 +08:00
94e0d06e5d fix: user agents and machine names in wakatime import 2022-11-15 23:53:30 +01:00
088bd17803 chore: update iconify 2022-11-13 20:20:41 +01:00
2976203ecc fix: missing icons 2022-11-13 20:11:53 +01:00
e75bd94531 fix: include cumulative total key in wakatime summary compat endpoint (resolve #426) 2022-11-13 19:52:53 +01:00
4cc8c21f67 fix: importing data from wakapi instance (resolve #428) 2022-11-13 19:27:44 +01:00
f182b804bb chore: add additional language icons
fix: support ipynb, cjs, tsx file endings
2022-11-11 16:13:41 +01:00
9586dbf781 fix: make intervals robust to daylight saving time shift 2022-10-31 23:24:54 +01:00
c8ea1a503f Merge pull request #424 from f0x52/postgres-dsn
Add postgres DSN config option
2022-10-31 19:22:03 +01:00
f0x
ebbc21f0b1 add postgres DSN config option 2022-10-31 18:07:16 +01:00
6e5bc38e5e fix: index migration for sqlite 2022-10-28 10:32:47 +02:00
9424c49760 fix: composite index on heartbeats table 2022-10-28 09:54:11 +02:00
efd6ba36e3 fix: errors during leaderboard generation 2022-10-20 08:33:12 +02:00
b1d7f87095 chore: add maximum default leaderboard length 2022-10-19 18:28:30 +02:00
ffbcfc7467 fix: cache key 2022-10-19 17:23:40 +02:00
41f6db8f34 feat(wip): leaderboard pagination (resolve #417) [ci-skip] 2022-10-16 19:38:43 +02:00
8a21be4306 fix: ignore rank column in migrations 2022-10-16 18:59:00 +02:00
31ca4a1e02 chore: logging 2022-10-16 17:42:32 +02:00
7cab2b0be7 chore: add clarification on relaying to other wakapi instance (resolve #420) [skip-ci] 2022-10-15 11:08:44 +02:00
777997c883 fix: swagger ui (resolve #421) 2022-10-14 12:00:56 +02:00
060a33263a chore: update dependencies 2022-10-09 10:16:27 +02:00
33d259592c chore: improve summary id fixing migration (see #416) 2022-10-09 10:16:18 +02:00
fbae5f8757 Tailwind 3 & Footer alignment (#419)
* ui: footer alignment
* chore: upgrade tailwind to v3
* fix: tailwind 3 class renames
* ui(fix): alias green to emerald for tailwind 3
2022-10-09 10:53:52 +11:00
bc99dc990a fix: case sensitivity with leaderboard languages (resolve #418) 2022-10-07 08:58:51 +02:00
1e9d3f9e80 Merge branch '182-leaderboards' 2022-10-06 20:43:55 +02:00
2ce720c20f fix: leaderboard responsiveness 2022-10-06 20:42:05 +02:00
ef87445e43 chore: display leaderboard update time 2022-10-06 15:30:32 +02:00
dec5849661 fix: replace mysql backticks 2022-10-06 15:23:59 +02:00
5609c0ada3 chore: empty leaderboard placeholder 2022-10-06 15:17:37 +02:00
1632cea949 fix: clear leaderboard after user opted out 2022-10-06 14:52:06 +02:00
23759d526a feat: settings option to opt in to leaderboards 2022-10-06 14:47:22 +02:00
82a565738f test: adapt mocks 2022-10-06 14:34:46 +02:00
1989a69926 feat: show users top languages
feat: language icons
2022-10-05 23:36:57 +02:00
7a07c9d4fc feat: top languages by user 2022-10-05 21:52:10 +02:00
a27fe04919 feat: leaderboard aggregation functionality
feat: leaderboard ui design
2022-10-03 23:53:47 +02:00
1d7ff4bc2a refactor: use query param for leaderboard controls 2022-10-03 20:38:19 +02:00
b3fa032bde feat(wip): leaderboard ui 2022-10-03 10:53:27 +02:00
94377a8dea fix: summary items id type (see #416) 2022-10-02 11:31:32 +02:00
dba4da8641 chore: caching for leaderboard 2022-10-02 10:31:01 +02:00
4a22a19cb0 chore: generate leaderboard when enabled in user settings 2022-10-02 10:13:39 +02:00
13a3d9f03a feat: leaderboard generation and querying 2022-10-02 00:01:39 +02:00
beffe71ea6 feat: add leaderboard data model 2022-09-30 17:19:32 +02:00
0ab7faf7b6 Merge remote-tracking branch 'origin/master' 2022-09-30 15:28:15 +02:00
a2ac049578 fix: heartbeat entity character length (resolve #415) 2022-09-30 15:28:11 +02:00
b287c4ca36 Merge pull request #414 from muety/cgo
Remove gcc dependency in release
2022-09-30 14:49:58 +02:00
018cc50fb8 chore(release): reinclude ci conditions 2022-09-30 22:44:25 +10:00
1d4156bdfe chore(build): remove gcc dependency 2022-09-30 22:35:03 +10:00
147c79db60 fix: label on settings page 2022-09-30 14:12:07 +02:00
f204ca888d fix: swagger docs path [skip ci] 2022-09-30 11:02:32 +02:00
e28070b288 fix(ci): build env vars on windows 2022-09-30 00:02:30 +02:00
4d217a83c1 chore: update readme and dockerfile 2022-09-30 00:02:04 +02:00
9e0581b311 chore: update dependencies after sqlite pure go migration 2022-09-29 23:41:57 +02:00
ffb529f4cf Merge branch 'sqlite-go-replacement'
# Conflicts:
#	go.mod
#	go.sum
2022-09-29 23:37:40 +02:00
c9aac2a273 fix: swagger docs base path (resolve #412) 2022-09-29 23:33:49 +02:00
dd8658e33e refactor: replace sqlite with pure go implementation 2022-09-08 21:13:13 +02:00
e399af1f1f fix(build): enable cgo, downgrade release to ubuntu-18.04, add -w -s (#404)
* fix(build): releases with cgo
* build: downgrade to ubuntu-18.04
* build: add -w -s flags
2022-09-08 17:51:26 +10:00
4c1f4ed39b docs: add eget option [ci skip] 2022-09-07 10:50:26 +02:00
7e5c00d0ae chore: update quick run script (resolve #400) 2022-09-06 22:28:49 +02:00
cec2a84e2d Merge pull request #402 from muety/release-arch
Add more architectures to release binaries (resolve #401)
2022-09-06 22:16:35 +02:00
ffb0b84d78 build: re-enable release steps 2022-09-06 20:51:05 +10:00
8a7333b899 build: test release architectures 2022-09-06 20:23:47 +10:00
dd3b9c9b9c Merge pull request #398 from marvinscham/patch-1
Add last_6_months interval identifier
2022-08-21 10:24:03 +02:00
d2b62e21c5 Fix indentation 2022-08-20 07:07:44 +02:00
9505773165 Add last_6_months interval identifier 2022-08-19 17:14:00 +02:00
4bfc8a9e9f Merge pull request #397 from muety/docker-simplify
Simplify Dockerfile
2022-08-19 13:59:33 +02:00
df5fe6e623 fix: initialise data/ dir in Docker 2022-08-19 21:55:58 +10:00
037ad7b9b1 fix: check database open 2022-08-19 21:48:03 +10:00
ec10cc922c perf: strip binaries 2022-08-19 19:53:02 +10:00
acb76e1ab1 ref: simplify Dockerfile stages 2022-08-19 19:37:05 +10:00
252a304ba8 Merge pull request #396 from HellsCrimson/master
add last_year and all_time interval identifier like in wakatime docs
2022-08-17 18:50:07 +10:00
c863cf6dc5 add last_year and all_time interval identifier like in wakatime docs 2022-08-16 23:28:48 +02:00
373d969734 feat: introduce newsbox for front page (resolve #393) 2022-08-13 10:28:36 +02:00
99a3e8f5da Merge pull request #394 from muety/nil-fix
fix: 500 on POST /api/heartbeats
2022-08-12 14:37:33 +02:00
4302cfcbd6 fix: 500 on POST /api/heartbeats 2022-08-12 17:25:43 +10:00
7cca0055fe Merge pull request #388 from muety/automated-version
feat: automatic version.txt updates
2022-07-14 14:31:06 +02:00
20993a1182 feat: automatic version.txt updates
Resolves #387
2022-07-09 13:58:16 +10:00
d5a85639b1 fix: broken summary aggregation (resolve #385) 2022-07-03 20:39:16 +02:00
b6a8185957 Merge branch 'Enthys_master' 2022-07-02 00:30:15 +02:00
c5da5e4622 fix: bug in same day comparison 2022-07-02 00:28:56 +02:00
a0f69a371f fix: api tests time zone bug (resolve #355) 2022-07-01 23:31:51 +02:00
2f0cb112dd test: user api retrieving user information 2022-06-30 10:36:13 +03:00
2173954b84 docs: instructions for go install [ci skip] 2022-06-29 23:39:35 +02:00
991e64b961 fix: returning current user instead of requested one 2022-06-29 10:46:46 +03:00
affff0c386 fix: admin users can't fetch other user data 2022-06-28 13:01:35 +03:00
099cdaddbc chore: add example systemd service unit file [ci skip] 2022-06-22 00:18:50 +02:00
409405117e Merge pull request #377 from NChechulin/master
elaborate on cloud server API URL [ci skip]
2022-05-29 15:06:42 +02:00
af89ecc9c1 update client configuration URLs for the new version 2022-05-29 15:43:59 +03:00
be354fa790 elaborate on cloud server API URL
Personally, for me, it was slightly unclear which URL has to be pasted.
2022-05-28 11:05:12 +03:00
a1c4c5da6b Merge pull request #376 from Daste745/avatar_url_template_env
Add an env configuration option for AvatarURLTemplate
2022-05-22 23:06:25 +02:00
33509beaf7 Enable env configuration for AvatarURLTemplate
Added an `env:"WAKAPI_AVATAR_URL_TEMPLATE"` option for the Avatar URL Template configuration setting.

I wanted to configure this on my instance, but the only way now is through the yaml config file.
2022-05-22 02:04:25 +02:00
ab6ccbdfbe README: Mention the WAKAPI_AVATAR_URL_TEMPLATE configuration variable 2022-05-22 02:03:04 +02:00
77e6cd9faa Merge pull request #375 from daief/master
Github Actions add a release for macOS
2022-05-20 09:40:26 +02:00
34bc38cecf chore: add descriptive names for workflows 2022-05-20 09:34:54 +02:00
69d3e0494b chore: add a new endline 2022-05-20 12:17:35 +08:00
a3136ebb13 chore: fix upload relese 2022-05-20 12:01:34 +08:00
4a4d0dad4b chore: merge mapi, test, build to ci.yml & keep release in release.yml 2022-05-20 11:46:29 +08:00
3b87511f48 chore: try use include 2022-05-19 14:12:00 +08:00
f5fba04097 chore: try keep same name 2022-05-19 13:50:43 +08:00
ad566993ad chore: fix release.yml 2022-05-19 13:10:23 +08:00
5f1e498454 chore: fix release.yml 2022-05-19 13:09:50 +08:00
2e0f79df3b chore: try merge release action 2022-05-19 13:06:11 +08:00
4a4e19fcbd fix: error when querying with label filter (resolve #374) 2022-05-16 23:23:18 +02:00
45d4ba89f5 fix: update swaggo dependency 2022-05-13 16:19:35 +02:00
29b3e619ca chore: update mapi org 2022-05-13 16:13:55 +02:00
1a85ebc0f7 Merge branch 'mapi' of https://github.com/mayhemheroes/wakapi into mayhemheroes-mapi 2022-05-13 16:12:45 +02:00
4bd58789f4 fix: server error when passing empty heartbeats slice
fix: do not allow to set id for diagnostics inputs
chore: remove authentication for diagnostics endpoint from swagger docs
2022-05-13 16:12:18 +02:00
09d1124794 fix: work around invalid all_time_since_today data schema to fix failing import (resolve #370) 2022-05-12 00:59:42 +02:00
41584bdd82 add Mayhem for API as a github workflow 2022-05-11 10:27:37 -07:00
1b7baf6fc9 fix: explicitly set default value for unique columns (fix #367) 2022-05-07 23:17:15 +02:00
a76db3e95f fix: clear summary cache on new project label (resolve #369) 2022-05-07 09:37:10 +02:00
74a5226e73 Merge pull request #364 from bdeshi/fork
chore: remove hard-coded volume from Dockerfile
2022-04-24 20:28:22 +02:00
d245b1e5d0 chore: remove hard-coded volume from Dockerfile 2022-04-24 17:33:42 +06:00
d5eff46651 fix: summary page layout 2022-04-24 09:33:04 +02:00
30a65b4de9 chore: minor changes to vibrant colors toggling 2022-04-24 09:26:04 +02:00
9048a8eb7a Merge branch 'master' into fork 2022-04-24 03:56:18 +06:00
1f19c5e93c feat: make vibrantColors a localStorage setting 2022-04-24 03:39:08 +06:00
4b0a3cf0d6 fix: index error during summary generation (resolve #361)
chore(sentry): include stacktrace with panics
2022-04-20 21:36:39 +02:00
d778612242 fix: remove authentication requirement from diagnostics endpoint 2022-04-18 21:32:30 +02:00
ff7d595a86 chore: do not run expensive jobs initially but only scheduled 2022-04-18 21:16:27 +02:00
9d7688957f chore: explicit width and height for front page images [ci skip] 2022-04-18 19:28:30 +02:00
179042f81b refactor: use cross join instead of subquery for populating summary items (see #350) 2022-04-18 17:15:09 +02:00
e6441f124c chore: adapt tests 2022-04-18 16:14:58 +02:00
15c391d1d4 chore: downgrade postgres driver 2022-04-18 16:07:52 +02:00
91c765202c fix: prevent large difference between aggregated and recomputed summaries (resolve #354) 2022-04-18 16:06:32 +02:00
5276f68918 fix: double counting when using precise missing intervals 2022-04-18 15:18:01 +02:00
e774039831 chore: fall back to today badge on project page 2022-04-18 11:49:06 +02:00
40067d252e fix: non-ascii project badges (resolve #357)
chore: locally generated badges (resolve #348)
2022-04-18 11:39:26 +02:00
1a47243f70 chore: make summary items subquery unique by summary id 2022-04-13 00:05:18 +02:00
a1f6c2884b Merge remote-tracking branch 'origin/master' 2022-04-03 18:03:25 +02:00
977420c68d fix: failing heartbeats index auto-migration on sqlite (resolve #346) 2022-04-03 18:03:09 +02:00
3ae66a3898 docs: add public url parameter to readme (resolve #349) [ci skip] 2022-04-02 09:26:14 +02:00
5c5c462035 docs: update readme
- adds `app.vibrant_color` config description
- adds missing `server.public_url` config description
- removes trailing spaces
- makes letter cases consistent in headings
- misc. typo fix and adjustments
2022-03-28 03:12:54 +06:00
f6cc489425 feat: allow toggling vibrant color for all charts
- supports new config key `app.vibrant_color` or env `WAKAPI_VIBRANT_COLOR`
- updates and extends `data/colors.json` with editor and os colors
- fixes #343
2022-03-28 01:56:13 +06:00
5aae18e241 refactor: replace most custom date util functions by lancet ones
refactor: add precision mode to missing intervals function
2022-03-25 12:51:40 +01:00
8a731a252a chore: ditch lo module again 2022-03-25 11:34:16 +01:00
8fc0d78f64 refactor: include generics based utility lib and refactor some parts accordingly [ci-skip] 2022-03-20 16:40:14 +01:00
a675417ab9 chore: upgrade dependencies
chore: update dockerfile to build with go 1.18
2022-03-20 16:40:14 +01:00
408d9086e7 fix(ci): use go 1.18 in ci build 2022-03-20 16:39:38 +01:00
8f933d8648 chore: upgrade dependencies
chore: update dockerfile to build with go 1.18
2022-03-20 16:29:13 +01:00
bbc85de34b chore: metrics performance improvements 2022-03-19 10:30:32 +01:00
ec70d024fa fix: remove user property of diagnostics as sent without auth 2022-03-19 09:27:13 +01:00
eae45baf38 chore: allow heartbeats from one hour into the future to compensate for clock inaccuracies (see #342) 2022-03-19 09:02:15 +01:00
4cea50b5c8 chore: add user project index on heartbeats table 2022-03-19 08:57:33 +01:00
e4814431e0 feat: add database size metric 2022-03-18 18:20:13 +01:00
91b4cb2c13 fix: explicit milliseconds precision of timestamp columns 2022-03-18 13:48:28 +01:00
a3acdc7041 fix: duration aggregation for heartbeats with identical timestamps (resolve #340) 2022-03-18 12:29:43 +01:00
e7e5254673 feat: ability to clear all user data (resolve #339) 2022-03-17 11:55:13 +01:00
8e558d8dee chore: introduce heartbeat max age 2022-03-17 11:35:20 +01:00
b763c4acc6 fix(perf): speed up summary retrieval of all time interval (resolve #336) 2022-03-17 11:08:40 +01:00
d1bd7b96b8 fix: hotfix for #337 (resolve #33) 2022-03-16 18:29:19 +01:00
8c65da9031 chore: remove entity index again
chore: add migration note
2022-03-13 09:42:51 +01:00
647bf1781d chore: apply filters in database query (see #335) 2022-03-13 08:49:03 +01:00
85515d6cb5 Merge branch 'patch-1' 2022-03-06 12:00:29 +01:00
1258ec0438 docs: add smtp and mailwhale config details to readme 2022-03-06 12:00:19 +01:00
965d8e22b3 chore: fix typo in error message 2022-03-06 11:52:03 +01:00
ed6e51b4df add error when no authentication is configured 2022-03-04 17:03:04 +01:00
af879f8d57 fix: example for mail sender 2022-03-04 16:58:52 +01:00
f15efcd6f2 chore: bump version 2022-03-02 17:58:53 +01:00
22e91ad362 Merge pull request #327 from daief/patch-2
fix: wrong key
2022-03-02 17:56:53 +01:00
932ba111cc fix: wrong key 2022-03-02 22:29:09 +08:00
23d00d574b chore: easier setup instructions (resolve #325) 2022-03-02 08:55:58 +01:00
d4b15e7959 fix: href 2022-03-02 08:51:27 +01:00
42808fa38a fix: href to a 404 when service on a subpath
click project detail will redirect to a not exist page, when the service runs with a base path.

For example, the base path is `wakatime`,  and the dashboard uri will be `/wakatime/summary`. When click project detail, page will be redirect to `/wakatime/wakatime/summary?project=demo` but the correct detail page is `/wakatime/summary?project=demo`.

And i think `pushing a history stack` is better than `replace`, so that can back to dashboard by backwards.
2022-03-02 11:35:40 +08:00
52269c780f add missing expose to Dockerfile 2022-03-01 18:19:47 +11:00
302eb33b1b fix: branches chart (resolve #322) 2022-02-22 08:19:51 +01:00
784adec3c1 docs: update readme 2022-02-21 19:49:43 +01:00
d2cdd35fff Merge pull request #320 from muety/gitattributes
ref: add gitattributes file, remove unnecessary unicode characters
2022-02-20 21:27:02 +01:00
33d65fb33a ref: add .gitattributes file for line normalisation 2022-02-18 19:53:04 +11:00
6d762f5fd6 ref: remove unnecessary unicode characters 2022-02-18 19:52:55 +11:00
222024dabb chore: cache avatars in memory 2022-02-17 10:34:33 +01:00
660a09475e chore: include avatar rendering into wakapi itself 2022-02-17 09:53:37 +01:00
5cc932177f chore: update precompressed assets 2022-02-16 08:57:00 +01:00
ac9d96c563 Remove "Create Account" button when AllowSignup is set to false (#319)
Merge pull request #319
2022-02-16 08:56:27 +01:00
3758eecc96 docs: extend postman collection by get heartbeats compat endpoint
(cherry picked from commit e6c6d0eb0d8b4ac6acf68c17002e069b6fe66626)
2022-02-13 11:04:34 +01:00
e21788b8b5 chore: minor fixes 2022-02-13 11:03:10 +01:00
e7f3432113 feat: GET /heartbeat endpoint (resolves #241) 2022-02-13 11:03:10 +01:00
7159df30c2 feat: allow to configure custom api url for relay and import (resolve #105) 2022-01-21 12:35:05 +01:00
fce3a3ea20 docs: update docker run command to ghcr [ci skip] 2022-01-19 15:21:26 +01:00
bd2a8c5a7f fix: make cookie path respect server.base_path (resolve #310) 2022-01-17 08:25:50 +01:00
632a3d4a91 docs: update readme [ci skip] 2022-01-16 06:39:45 +01:00
8a344ce4a2 Merge pull request #308 from muety/docker-version
ci: major and major.minor tags for Docker publish
2022-01-16 06:32:14 +01:00
cbbb592143 ci: major and major.minor tags for Docker publish
Resolves #307
2022-01-16 14:53:55 +11:00
67f0d19a65 fix: allow to create labels for aliased projects (resolve #231) 2022-01-13 17:10:24 +01:00
03b104a390 chore: fix user agent parsing for unset wakatime version (resolve #306) [ci skip] 2022-01-12 21:23:36 +01:00
4a3fe48cce chore: indentation [ci skip] 2022-01-08 13:35:11 +01:00
1033343702 Merge remote-tracking branch 'origin/master' 2022-01-08 13:33:55 +01:00
31c462c275 chore: add caddyfile [ci skip] 2022-01-08 13:33:24 +01:00
0a7ebc4dc7 fix: allow to display more than ten entities and nine legend items (fix #303) 2022-01-07 16:10:27 +01:00
91768cf927 chore: version 2022-01-07 11:20:59 +01:00
e967a74e36 fix: allow project names with dots for badges (resolve #301) 2022-01-06 14:45:26 +01:00
8e6719f0b7 fix: missing project labels form [ci skip] 2022-01-03 17:48:59 +01:00
bcbd6236df fix: adapt badge filter entity regex to cover labels [ci skip] 2022-01-03 17:30:40 +01:00
d1cbabf662 ci: remove armv6 again 2022-01-03 17:06:35 +01:00
ad4d251154 feat: build linux/arm/v7,linux/arm/v6 2022-01-03 12:06:36 +11:00
2bb3b886c2 chore: update Docker to use golang:1.17-alpine 2022-01-03 12:05:09 +11:00
4f183ed637 fix: exec to replace environment.sh 2022-01-03 12:02:21 +11:00
b66f9b5cf5 perf: separate download stage in Dockerfile 2022-01-03 12:01:33 +11:00
bf7f93fcd4 perf: use --no-cache in Dockerfile 2022-01-03 11:16:17 +11:00
36c96dafca chore: update screenshot [ci skip] 2022-01-02 21:57:24 +01:00
8f87c4e283 fix: omit null branches property of wakatime summary 2022-01-02 21:29:16 +01:00
247aef5ef3 chore: fix api url in setup instructions
chore: bump version
2022-01-02 21:02:20 +01:00
c0dada7e7a chore: add precompressed app css 2022-01-02 20:28:55 +01:00
8b8c5675af fix: wrongly displayed timezone offset 2022-01-02 20:25:07 +01:00
f69dce39d8 fix: settings forms 2022-01-02 20:13:38 +01:00
c2d3426bcd feat: project details page with branch statistics (resolve #242) 2022-01-02 20:04:29 +01:00
bb0d0569fd chore: make time picker a standalone petite-vue component 2022-01-02 18:30:22 +01:00
c4c62f31e4 chore: move time picker to separate component 2022-01-02 14:38:04 +01:00
2bc53e6f11 feat: basic implementation of branch statistics 2022-01-02 13:39:20 +01:00
fd6c36832e fix: critical infinite loop at the date of switch to daylight saving time 2022-01-02 13:17:30 +01:00
6f9015d3d8 fix: neutered file system
docs: add filter params to api docs
2022-01-02 12:03:20 +01:00
cbcdd938eb chore: add api tests for filtering 2022-01-02 12:03:20 +01:00
bf82935849 chore: add more filtering unit tests 2022-01-02 12:03:20 +01:00
fe3ba79d54 chore: filter model tests 2022-01-02 12:03:20 +01:00
d80c1a4c4b feat: ability to filter by project labels 2022-01-02 12:03:20 +01:00
a279548c89 feat: comprehensive summary-level filtering (resolve #262) 2022-01-02 12:03:19 +01:00
8a3e6f0179 chore: more verbose logging with regard to reports 2022-01-02 12:02:17 +01:00
a72af7d57e chore: add title attribute for kpi texts 2022-01-02 12:02:16 +01:00
ec236909c9 chore: add migration for heartbeats count 2022-01-02 12:02:12 +01:00
92f6d44606 feat: total heartbeats per summary (resolve #283) 2022-01-02 12:02:12 +01:00
e14f8c1463 chore: minor performance improvements 2022-01-02 12:02:12 +01:00
80252ff701 docs: add readme link 2022-01-02 12:02:12 +01:00
374e578a7c feat: brotli precompressed assets (resolve #284) 2022-01-02 12:02:12 +01:00
aebfdc535d refactor: redefine tailwind build
chore: encapsulate commonly used css classes
chore: add npm scripts for building tailwind and iconify assets
2022-01-02 12:02:12 +01:00
c217f8e664 chore: move vue components to separate js files 2022-01-02 12:02:12 +01:00
ba54e7bb96 fix: responsiveness 2022-01-02 12:02:12 +01:00
1e505b91f3 chore: imprint and password reset pages 2022-01-02 12:02:12 +01:00
26825b07de refactor: migrate most non-chart-related js logic to petite-vue (resolve #282) 2022-01-02 12:02:12 +01:00
6a5f08dc95 fix: popups and dropdowns 2022-01-02 12:02:12 +01:00
62e3decf0f fix: url template vars 2022-01-02 12:02:12 +01:00
0557a5000f refactor(wip): finish settings page 2022-01-02 12:02:12 +01:00
7b7fa8bdf3 refactor(wip): redesign settings page 2022-01-02 12:02:12 +01:00
4e7322c985 fix: api tests 2022-01-02 12:02:12 +01:00
af0d2e84e1 refactor: replace roboto by source sans 3 font
chore: minor front page styling
2022-01-02 12:02:12 +01:00
44a2e609fb refactor: redesign login page
refactor: redesign signup page
refactor: redesign summary page
2022-01-02 12:02:12 +01:00
ee501ca3c5 fix: mocks 2022-01-02 12:02:12 +01:00
148f581906 fix: properly sort durations to prevent heartbeats from being counted twice 2022-01-02 12:02:12 +01:00
acf16421a6 chore: add quick start option 2022-01-02 12:02:12 +01:00
0039f67a2f fix: duration test 2022-01-02 12:02:12 +01:00
c8a07cee36 refactor: introduce concept of durations (resolve #261) 2022-01-02 12:02:11 +01:00
15c8838fea chore: bump version 2022-01-02 11:09:56 +01:00
f363135261 chore: minor code changes 2022-01-02 11:06:00 +01:00
d561ce1766 Merge branch 'master' of https://github.com/jabra98/wakapi into jabra98-master 2022-01-02 10:52:25 +01:00
6712f0a390 Merge pull request #291 from muety/swagger-api-patch
fix: swagger /api/api duplication
2022-01-02 10:49:32 +01:00
9950da3e7e fix: swagger /api/api duplication
Resolves #289
2022-01-02 11:22:58 +11:00
c7e12ba3b5 fix: consider all Machine/UserAgent entries 2022-01-01 20:33:58 +01:00
aaa907a7b2 Merge pull request #287 from RafDevX/patch-1 [ci skip]
Fix typos
2021-12-30 09:46:24 +01:00
0ee52662d3 fix misc. typos 2021-12-30 02:09:04 +00:00
e1daf1406e docs: mention blog post 2021-12-27 08:43:24 +01:00
7dd0967451 chore: drop debian based docker image and use alpine by default 2021-12-15 15:37:14 +01:00
d6aa2c4405 chore: bump version 2021-12-15 15:16:31 +01:00
821ae94c1e fix: auto increment in bigint migration 2021-12-15 13:17:07 +01:00
adcd7b35ae fix: adapt tests 2021-12-15 12:52:24 +01:00
b0bd26f0ec chore: upgrade dependencies (fix #280) 2021-12-15 12:51:44 +01:00
259f711f2d fix: migrate id column type to bigint (resolve #281) 2021-12-15 10:50:16 +01:00
1c0477f861 Merge pull request #279 from muety/docker
fix: anticipated docker push issue
2021-12-14 15:31:06 +01:00
28a3418ad5 fix: limit sqlite connection pool to one 2021-12-14 02:17:59 +01:00
c5db2c235f chore: enable foreign key constraints for new sqlite databases 2021-12-14 00:47:04 +01:00
9cbddaeedf fix: anticipated docker push issue 2021-12-02 23:06:19 +11:00
485dfe2888 fix: user time zone test (fix #275) [ci skip] 2021-11-28 12:40:46 +01:00
Kid
78a26dbf3c Another typo fix 2021-11-26 22:55:43 +11:00
b2c72c6420 Merge pull request #272 from kidonng/patch-1 [ci skip]
Fix a typo
2021-11-25 15:31:05 +01:00
Kid
6852494d36 Fix a typo 2021-11-25 22:28:20 +08:00
305166ce68 Remove Table of Contents from README [ci skip]
Github has menu, and the links don't seem to work on Docker Hub
2021-11-25 20:31:41 +11:00
400f25c23e fix: remove dead client-side proxy link in README [ci skip]
This link is present in Client Setup section
2021-11-25 20:28:43 +11:00
3aacd3461d Merge pull request #265 from muety/ghcr
ci: add ghcr for Docker deploy, change cache
2021-10-21 13:42:29 +02:00
7e2460e1f0 ci: add ghcr for Docker deploy, change cache 2021-10-21 20:54:13 +11:00
57175ae7f8 docs: update readme [ci skip] 2021-10-14 13:09:26 +02:00
5df0f48303 feat: user avatars 2021-10-14 12:04:21 +02:00
76a7cf7e80 chore: include runtime metrics 2021-10-14 10:35:01 +02:00
7cae3c43d0 chore: enhanced caching for user entity sets (resolve #264) 2021-10-14 10:22:59 +02:00
5fc87dd143 fix: tests 2021-10-13 17:51:54 +02:00
7329f6a34e chore: version 2021-10-13 17:47:33 +02:00
3b96bd3723 docs: include relay endpoint in swagger docs 2021-10-13 17:47:18 +02:00
2c7977cf63 chore: invert visualization of project labels (resolve #263) 2021-10-13 17:12:55 +02:00
782da0b49e chore: update landing page 2021-10-13 11:27:04 +02:00
ed9a7ccd5a fix: tests 2021-10-11 11:38:55 +02:00
9451848ad4 chore: bump version 2021-10-11 11:30:18 +02:00
6c0145b149 Merge branch 'gaocegege-auth' 2021-10-11 11:30:08 +02:00
a94092e31c test: add api tests for query auth 2021-10-11 11:29:38 +02:00
52744dbcd0 Merge branch 'auth' of https://github.com/gaocegege/wakapi into gaocegege-auth 2021-10-11 11:22:01 +02:00
cc11226eab fix: add missing non-zero field checks (fix #259) 2021-10-11 11:07:04 +02:00
8d073aaef2 feat: implement relay endpoint (see #237) 2021-10-11 11:00:50 +02:00
d2f078443e fix: Remove hard coded string 2021-10-11 16:00:48 +08:00
c6e1651d9e fix: Fix the empty key error 2021-10-11 15:58:29 +08:00
630090e38a feat: Support query parameter token 2021-10-11 15:10:30 +08:00
5394349c73 update readme [ci skip] 2021-09-29 21:04:00 +02:00
5cd3bf83a6 Merge pull request #253 from muety/ci-test
feat: add test steps to Linux workflow
2021-09-18 14:25:15 +02:00
13cf911edf fix: make test script fail if tests fail [ci skip] 2021-09-18 14:24:16 +02:00
fe0f41cecb feat: add test steps to Linux workflow 2021-09-17 21:24:12 +10:00
265080453a Merge pull request #252 from kondr1/gitpod
Config for working with repo use gitpod [ci skip]
2021-09-09 12:22:47 +02:00
f9fb7c7a8a chore: bump version 2021-09-07 23:23:06 +02:00
90477dbb01 chore: fix typo in test case 2021-09-07 23:22:22 +02:00
35926a19e2 Merge branch 'master' of https://github.com/kondr1/wakapi into kondr1-master 2021-09-07 23:19:15 +02:00
84dc594548 add statusbar endpoint test 2021-09-07 19:33:07 +03:00
2f9b8fbcfe .gitpod.yml 2021-09-07 13:29:36 +00:00
9235c1ca78 add statusbar endpoint to postman collection 2021-09-07 13:26:26 +03:00
a869897f80 statusbar endpoint fix 2021-09-07 10:12:24 +00:00
2f9cafc88c Merge pull request #250 from muety/swagger-fix
fix: relative swagger-ui definition
2021-09-07 08:56:08 +02:00
816d0c8cdc fix: relative swagger-ui definition
Relevant for those hosting wakapi on subdirectory

Note: /swagger-ui redirects to /swagger-ui/
2021-09-07 14:42:52 +10:00
1ab29b22e1 add statusbar endpoint 2021-09-06 17:01:49 +00:00
cafe4133e4 Merge pull request #247 from muety/dist-perf
perf(dist): load external swagger-ui files
2021-09-06 14:02:10 +02:00
5a0a3c40ca perf(dist): load external swagger-ui files 2021-09-06 21:19:24 +10:00
9b5f00ea5d Merge remote-tracking branch 'origin/master' 2021-09-05 20:26:05 +02:00
7a418aa519 fix: continue chartjs migration 2021-09-05 20:25:24 +02:00
d96a48d5dc Merge pull request #245 from hiroyaonoe/fix-wrong-parameter-openAPI
fix(docs): wrong parameter in OpenAPI-Docs [ci skip]
2021-09-05 09:54:53 +02:00
fa4512f79b fix: tooltip and legend 2021-09-05 14:28:00 +10:00
398b4c16d6 fix(docs): wrong parameter in OpenAPI-Docs
The parameter `range` of `/compat/wakatime/v1/users/{user}/stats/{range} [get]` is path.
However, it was described as query in OpenAPI-Docs.
So, I fixed it and generated files in `static/docs`.

Closes: #239

Signed-off-by: Hiroya Onoe <hiroyanoe.io@gmail.com>
2021-09-05 03:02:01 +09:00
d1577fc6be Reverted test-chings 2021-08-29 12:00:51 +02:00
23f8a5cf7f Started fixing things 2021-08-29 11:55:58 +02:00
81835a3d88 chore: bump version 2021-08-29 10:54:26 +02:00
30de96950b chore: persist raw user agent value 2021-08-29 10:54:00 +02:00
11291b0d6c fix: properly format x axis for durations (see #232) 2021-08-29 10:32:23 +02:00
f0ac0f6153 fix: ui errors from conditional HasData on summary 2021-08-29 11:10:54 +10:00
6aad1633e1 chore: update issue templates [ci skip] 2021-08-21 09:35:45 +02:00
c07a4d71a0 fix: include tzdata package in alpine docker image [ci-skip] 2021-08-21 09:16:45 +02:00
dff0b742fc Merge branch 'sdvcrx-master' 2021-08-19 09:01:27 +02:00
4f65f94766 chore: bump version 2021-08-19 09:01:19 +02:00
825663acde fix: compatible with new wakatime-cli 2021-08-19 14:48:26 +08:00
f399fd4ea7 docs: readme [ci-skip] 2021-08-12 09:25:19 +02:00
87fadf46f7 chore: use partial includes in mail templates to avoid code duplication 2021-08-08 12:33:40 +02:00
69f5d510dc chore: exclude health endpoint from logging 2021-08-07 10:31:26 +02:00
0542813ed6 docs: update backwards-compatible api url in readme 2021-08-07 10:23:27 +02:00
c962a3891d chore: update postman collection 2021-08-07 10:18:33 +02:00
2088987a0c chore: implement diagnostics endpoint (resolve #225) 2021-08-07 10:16:50 +02:00
9e3203ac41 fix: tests 2021-08-07 00:12:45 +02:00
58719182c4 chore: notify users about failing wakatime connection 2021-08-06 23:28:03 +02:00
a8df25be08 chore: more verbose logging 2021-08-06 22:38:57 +02:00
391cc1e5b4 chore: fix syntax for postgres 2021-08-06 17:17:06 +02:00
3bb22e5e84 Merge remote-tracking branch 'origin/master' 2021-08-06 17:08:28 +02:00
93bdb48d95 fix: resolve project labels before resolving aliases (resolve #222) 2021-08-06 17:08:11 +02:00
533b5d62fc fix: speed up settings page (resolve #226) 2021-08-06 16:37:01 +02:00
0af5fab75f refactor: resolve project labels at runtime (resolve #227) 2021-08-06 16:36:56 +02:00
fecc8b3b5f fix: remove unix socket if exists (#220) 2021-08-06 16:36:44 +02:00
24b8ff6381 feat: build/push arm64 Docker image 2021-08-06 16:36:44 +02:00
180e75a5eb fix: README link to 'config.default.yml' 2021-08-06 16:36:44 +02:00
f48b49d26e chore: upgrade dependencies 2021-08-06 14:26:03 +02:00
47b9cacb26 fix: remove unix socket if exists (#220) 2021-07-10 09:10:55 +00:00
23fc1b62cc Merge pull request #219 from muety/docker-arm
Build and push arm64 Docker image
2021-07-09 09:47:31 +02:00
74f6a255a8 feat: build/push arm64 Docker image 2021-07-09 16:17:50 +10:00
7a5dce29bd Merge pull request #218 from donPabloNow/master
fix: README link to 'config.default.yml'
2021-07-07 11:36:26 +02:00
0e1596fe70 fix: README link to 'config.default.yml' 2021-07-06 23:44:08 +02:00
48513b660d chore: configurable count cache ttl 2021-06-27 12:08:11 +02:00
69f73fc0ea chore: dependency upgrades 2021-06-27 11:46:08 +02:00
0e788b0777 chore: bump version 2021-06-27 11:37:54 +02:00
181aefa2f9 chore: further optimizations and caching to speed up metrics endpoint (resolve #215) 2021-06-27 11:33:14 +02:00
407925ec53 feat: add alpine image 2021-06-27 18:01:43 +10:00
5e96e2a601 chore: cache active users with hourly precision 2021-06-26 12:42:51 +02:00
4d2a160ccb chore: configurable request timeout 2021-06-24 21:56:47 +02:00
c3957ec0c8 chore: log unmatched requests 2021-06-24 21:40:51 +02:00
312dfb36d8 chore: add default config param 2021-06-23 18:45:58 +02:00
c66605d463 chore: bump version 2021-06-23 18:43:54 +02:00
3c12df52d9 feat: 🎸 add support for using a UNIX domain socket 2021-06-23 11:44:00 -04:00
dd6a040171 chore: add api tests for all alternative heartbeat endpoints 2021-06-22 00:27:46 +02:00
9f1266957b fix: single heartbeat endpoint (resolve #212)
docs: swagger docs for all available heartbeat endpoints
2021-06-21 21:53:47 +02:00
466f2e1786 fix: summary caching (resolve #211) 2021-06-19 12:47:35 +02:00
82b8951437 fix: attempt to fix failing sqlite migrations (resolve #210) 2021-06-13 11:43:24 +02:00
25464e9519 chore: code smells 2021-06-13 10:14:15 +02:00
650fffa344 fix: exclude zero entries again 2021-06-12 12:06:24 +02:00
69627fbe11 fix: exclude zero entries 2021-06-12 12:04:38 +02:00
561198b203 chore: minor ui improvements 2021-06-12 12:01:20 +02:00
7c4a2024b6 chore: link to labels settings 2021-06-12 11:40:13 +02:00
7bcd6890d1 chore: adapt tests and bump version 2021-06-12 11:26:15 +02:00
1e4e530c21 chore: adapt tests 2021-06-12 11:09:24 +02:00
490cca05eb feat: ui for managing project labels 2021-06-12 10:44:19 +02:00
3780ae4255 fix: invalidate user summary cache (fix #209) 2021-06-12 10:43:56 +02:00
628ea0b9dd fix: nil pointer dereference
chore: allow to share labels publicly on settings page
2021-06-12 09:12:28 +02:00
0d64858721 feat: implement project labels (resolve #204) 2021-06-11 20:59:34 +02:00
c1c78d8d5b test: add more api tests 2021-06-11 17:47:33 +02:00
538b9d2463 fix: permissions for stats endpoint 2021-06-11 17:41:45 +02:00
f4612fd542 fix: badge endpoint permission fixes (resolve #205)
fix: reference past x days intervals from now instead of start of day
2021-06-11 16:02:28 +02:00
fb643571d2 Merge remote-tracking branch 'origin/master' 2021-06-10 23:22:58 +02:00
101fdfb957 chore: adapt default insert batch size (fix #206)
fix: set user data flag after import (fix #207)
2021-06-10 23:22:04 +02:00
a4d47fb566 test: more api tests [ci skip] 2021-05-29 09:52:26 +02:00
1a808f9197 feat: basic integration / api tests (wip) (resolve #9) 2021-05-28 17:14:16 +02:00
ee31212cdd fix: hotfix for invalid api base url prefix (#203) 2021-05-19 10:18:18 +02:00
712949afc7 chore: minor optimization to heartbeats by multi-user query 2021-05-14 09:38:31 +02:00
9dbc2039fc chore: add random time offset to scheduled reports jobs 2021-05-04 21:04:11 +02:00
f3b738b250 fix: empty projects (resolve #197)
fix: potential division by zero (see #199)
2021-05-03 21:32:26 +02:00
cf3d293688 feat: implement wakatime projects endpoint (resolve #196) 2021-05-01 13:52:03 +02:00
0fbb554fc3 fix: respect timezone parameter for wakatime summary endpoint (resolve #195) 2021-05-01 12:46:53 +02:00
11b224fc24 fix: exact path matching for api endpoints (resolve #194) 2021-04-30 18:08:53 +02:00
0673c26043 fix: attempt to fix race condition when counting 2021-04-30 17:19:17 +02:00
8dc69c58cb chore: upgrade dependencies 2021-04-30 16:33:48 +02:00
99d8349277 fix: rebuild tailwind assets 2021-04-30 16:23:27 +02:00
cf14fc46ef chore: less verbose logging 2021-04-30 16:22:28 +02:00
ef9303e61e feat: settings dialog for mail reports 2021-04-30 16:20:24 +02:00
a4e7158db2 refactor: mail service abstraction layer 2021-04-30 15:17:07 +02:00
29c04c3ac5 feat: email reports (resolve #124) 2021-04-30 14:07:14 +02:00
1beca82875 feat: implement wakatime users endpoint (resolve #193) 2021-04-30 10:13:32 +02:00
b16f777cc7 docs: minor typos [ci skip] 2021-04-29 21:46:22 +02:00
cead20a505 Merge branch 'master' of https://github.com/MeerBiene/wakapi into MeerBiene-master 2021-04-29 21:44:53 +02:00
5a8287a06b chore: exclude static endpoints from sentry tracing
chore: include user info to sentry tracing again
2021-04-29 21:19:43 +02:00
37d4d58b57 fix: make wakatime summary endpoint date range inclusive (resolve #192) 2021-04-29 21:08:47 +02:00
7d03a9b12d add code to stats example, add metrics example 2021-04-28 23:35:30 +02:00
331ace3c1e chore: remove config script [ci ckip] 2021-04-28 22:31:36 +02:00
4dd77ded26 docs: quick run script in readme 2021-04-28 22:26:44 +02:00
0bccbffd80 chore: quick run script
fix: run in production mode by default
2021-04-28 22:20:25 +02:00
2b45b064eb fix: permit simple date time format in wakatime summaries endpoint (resolve #190) 2021-04-28 22:19:44 +02:00
5d8fc99b93 docs: clarify time zone comments [ci skip] 2021-04-27 08:50:39 +02:00
8231d76200 Merge branch '184-fix-time-zone'
# Conflicts:
#	views/settings.tpl.html
2021-04-26 21:28:39 +02:00
c6fd43a964 chore: log requests from json response util method 2021-04-26 21:26:59 +02:00
4ab657ebd5 fix: fix divide by zero (resolve #189) 2021-04-26 21:26:56 +02:00
0a07ac1dd4 docs: document thoughts about time zones 2021-04-25 21:41:41 +02:00
a64201c93b fix: timezone selector 2021-04-25 21:12:36 +02:00
b105b0fe1c chore: version 2021-04-25 21:05:58 +02:00
649c658923 chore: add same date tests 2021-04-25 21:05:05 +02:00
bc9191a514 chore: fix api key on instructions page 2021-04-25 21:05:05 +02:00
04690d287d chore: guess user timezone on signup 2021-04-25 21:05:05 +02:00
c142b525a4 refactor: time zone sensitivity (resolve #184) 2021-04-25 21:05:04 +02:00
304fa3b03f chore: add same date tests 2021-04-25 20:53:17 +02:00
e01e6575db chore: fix api key on instructions page 2021-04-25 20:07:15 +02:00
75e61c0dc3 chore: guess user timezone on signup 2021-04-25 20:02:45 +02:00
6973743f41 refactor: time zone sensitivity (resolve #184) 2021-04-25 14:15:18 +02:00
26ef93c1af chore: minor refactorings to custom time parsing logic 2021-04-25 09:21:21 +02:00
0556efd39a chore: minor tweaks to migration script 2021-04-23 15:50:00 +02:00
030181fb2f sqlitemigrate patches for larger datasets 2021-04-22 23:37:20 +02:00
8b9a9a1a42 fix: merge summaries by unique from date only 2021-04-19 21:14:35 +02:00
6576837396 chore: batch mode for sample data script 2021-04-19 21:01:09 +02:00
1a10a4fb21 fix: prevent duplicate summaries from being counted twice (resolve #179) 2021-04-19 20:48:07 +02:00
0e3ce1e9e4 fix: lock aggregation jobs to one at a time on a per-user basis (resolve #180) 2021-04-19 20:36:37 +02:00
50a54bde22 chore: usage instructions for sqlite migration script [ci-skip] 2021-04-18 11:08:28 +02:00
53f3a9d685 chore: make back button on settings page a relative link 2021-04-18 11:05:59 +02:00
c37278e660 chore: add option to silently fail in case of schema migration errors 2021-04-18 11:03:54 +02:00
e2deadfd44 chore: add experimental sqlite to mysql migration script 2021-04-18 10:59:13 +02:00
ed35e7b82d chore: increment version 2021-04-16 19:17:30 +02:00
b672859021 fix: rebuild tailwind 2021-04-16 17:09:23 +02:00
d3713017e3 fix: include icon library to fix missing emojis on some platforms (resolve #119) 2021-04-16 17:07:11 +02:00
dca736752e refactor: logging (resolve #169) 2021-04-16 16:02:55 +02:00
337b39481b chore: set basic security headers (resolve #174) 2021-04-16 12:35:49 +02:00
b9ea6530f9 fix: serve static file from local fs when on dev (fix #176) 2021-04-16 12:24:19 +02:00
a9739a6db0 fix: make range picker show actual range with ceiled to date (fix #175) 2021-04-16 11:53:37 +02:00
a22836a644 fix: remove uniqueness constraint for email 2021-04-14 00:17:02 +02:00
c8e7fb461a Merge remote-tracking branch 'origin/master' 2021-04-13 23:50:10 +02:00
c2b099378a chore: add contribute.json (resolve #170) 2021-04-13 23:49:54 +02:00
20dd4cf0ab fix: precedence in case of multiple matching language mappings (fix #172) 2021-04-13 23:39:31 +02:00
f8e1453754 fix: failing auto migration of users table (resolve #171) 2021-04-13 23:23:57 +02:00
fbd90d2cc1 ci: adapt go version in github build action 2021-04-13 10:34:35 +02:00
129e208169 fix: very basic sentry error logging 2021-04-13 00:02:55 +02:00
9fd9ffbb3d fix: missing summary aggregation after days without heartbeats (see #168) 2021-04-12 23:36:22 +02:00
0884f620f1 chore: increment version 2021-04-12 22:58:52 +02:00
7ab9c45f4f fix: table drop in migration 2021-04-12 22:58:40 +02:00
915436822b fix: make mail provider configs non-nullable 2021-04-12 22:57:52 +02:00
0f1d1bce4d fix: summary missing interval calculation (fix #168) 2021-04-12 22:57:15 +02:00
6256c8e10a ref: embed files, bump to go 1.16 (#167)
* ref: embed portion of files
* fix: readd pkger
* ref: embed version.txt
* fix: wrong mail template import path
* refactor: get rid of sql-migrate
refactor: get rid of pkger in favor of go embed (resolve #164)
* chore: remove unused var [ci-skip]

Co-authored-by: Ferdinand Mütsch <ferdinand@muetsch.io>
2021-04-11 10:42:43 +00:00
2a9fbfdfd7 chore: send notification on successful import 2021-04-10 10:48:06 +02:00
56247b4e1e fix: throttle wakatime api requests (attempt to fix #152) 2021-04-10 10:18:09 +02:00
9d7afde6a9 chore: version 2021-04-10 00:34:37 +02:00
0df0168584 Merge branch '133-password-resets' 2021-04-10 00:34:20 +02:00
a6fe15d69b chore: add support button 2021-04-10 00:27:01 +02:00
ae363c1c82 Merge remote-tracking branch 'origin/master' 2021-04-10 00:17:13 +02:00
127a614190 Merge pull request #163 from notarock/master
Add Open Graph meta tags
2021-04-10 00:16:07 +02:00
b8cefeb595 chore: add html lang 2021-04-10 00:15:20 +02:00
ae97095688 chore: exclude health endpoint from sentry tracing 2021-04-10 00:10:16 +02:00
4706809170 feat: smtp mail provider implementation 2021-04-10 00:07:13 +02:00
ddc29f0414 chore: log mailwhale error 2021-04-09 22:51:03 +02:00
f4af787ecf Add Open Graph meta tags 2021-04-09 00:37:14 -04:00
da6a00fec5 fix: adapt tests 2021-04-05 23:00:21 +02:00
6ad33e3c3b feat: password resets (resolve #133) 2021-04-05 22:57:57 +02:00
e6e134678a wip: password resets 2021-04-05 16:25:13 +02:00
1783858854 fix: minor fixes (resolve #151) (resolve #154) 2021-04-04 10:42:27 +02:00
e1d040bd55 docs: mention wiki in docs 2021-04-04 10:23:35 +02:00
7f3a654b26 fix: import bug with small number of heartbeats (fix #160) 2021-04-04 09:45:32 +02:00
2b57da224c chore: write authenticated user to logs and sentry 2021-03-26 13:10:10 +01:00
01d51b78b1 chore: include postman collection (resolve #148) [ci-skip] 2021-03-25 15:50:47 +01:00
6b83600acc fix: responsiveness in firefox mobile (resolve #137) 2021-03-25 10:51:36 +01:00
65bbd744b5 chore: exclude static assets from sonarqube [ci skip] 2021-03-24 23:38:38 +01:00
81ca703501 fix: disable recompute caching (resolve #149) 2021-03-24 23:31:04 +01:00
2d1010e9d9 chore: add caching to badge endpoint
chore: add type index for summary items
2021-03-24 21:49:42 +01:00
5ca9a6a8be test: add further tests for heartbeat augmentation (resolve #144) 2021-03-24 20:43:49 +01:00
caf87de887 fix: allow custom mappings with dot (resolve #144) 2021-03-24 19:25:36 +01:00
9fc3c65efe feat: sentry integration (resolve #142) 2021-03-23 22:12:15 +01:00
f73285160d fix: delete language mapping (resolve #143) 2021-03-22 21:20:23 +01:00
1f557d562f chore: remove integrity hashes from assets files since served locally anyway (resolve #141) 2021-03-20 09:55:21 +01:00
3685f3a156 chore: add svelte language mapping 2021-03-13 21:01:24 +01:00
b3afe9bfa2 feat: gui for sample data generator script 2021-03-11 23:54:35 +01:00
9de2c20885 fix: enable strict slash mode for api router 2021-03-09 23:56:38 +01:00
2846748b26 hotfix: remove user agent check for shields.io 2021-03-09 23:15:35 +01:00
f2f6fe1483 Merge pull request #140 from notarock/master
Prevent invalid dates ranges in summary page
2021-03-06 09:05:11 +01:00
17ddd7ca76 Prevent invalid dates ranges in summary page
It is currently possible to enter a "End" date that is before the start
date, or a "start" date that is after the end date. This commit prevent the
user from directly entering an invalid date by setting a "min" and "max"
value on those two date picker.

There are no validation or the server's side, but that shouldn't be a
problem since the invalid date will not create an error, it will simply
not return any data.
2021-03-06 02:38:28 -05:00
292ae41c58 fix: batch insert conflict bug (resolve #139) 2021-03-05 21:39:21 +01:00
4f86f67716 chore: fix success messages 2021-02-21 13:12:29 +01:00
017530ac4a chore: introduce user email addresses (resolve #132) 2021-02-21 13:02:11 +01:00
81d3251856 fix: use compatible tailwind version 2021-02-21 13:02:01 +01:00
16af17fc37 chore: save heartbeats created date 2021-02-21 12:02:19 +01:00
701ed0a3e1 chore: purge tailwind classes (resolve #22) 2021-02-21 11:14:55 +01:00
218c93e975 Merge pull request #136 from notarock/master
Fixed scale label to show seconds instead of minutes
2021-02-21 10:38:52 +01:00
44de057022 Fixed scale label to show seconds instead of minutes 2021-02-21 04:27:45 -05:00
e55adf6287 fix: mysql character encoding (resolve #131) 2021-02-17 21:04:22 +01:00
56800be8e8 chore: change info message in migration (resolve #128) 2021-02-16 17:18:56 +01:00
c149766ecc fix: drop_badges_column migration for sqlite 2021-02-16 22:07:41 +11:00
759e8e4dfd chore: change logging middleware to use different output 2021-02-14 16:41:02 +01:00
293 changed files with 23784 additions and 4905 deletions

6
.gitattributes vendored Normal file
View File

@ -0,0 +1,6 @@
* text=auto
*.db -text
*.png -text
*.br -text
*.ico -text
*.woff2 -text

19
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,19 @@
---
name: Bug
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is. Please briefly describe how to reproduce the bug as well as _expected_ vs. _actual_ behavior. Optionally include screenshots and server logs, if helpful.
**System information**
Please provide information on:
* Wakapi version
* Operating system
* If Linux: which distro?
* If Docker: which image and tag?
* Database (SQLite, MySQL, ... ?)

10
.github/ISSUE_TEMPLATE/other.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Other (feature request, question, ...)
about: Anything else
title: ''
labels: ''
assignees: ''
---

103
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,103 @@
name: ci
on:
push:
pull_request:
jobs:
test:
name: 'Unit- & API tests'
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.19
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get
- name: Unit Tests
run: go test ./... -run ./...
- name: API Tests
run: |
npm -g install newman
./testing/run_api_tests.sh
mapi:
name: 'Automated pen-tests with Mayhem for API'
runs-on: ubuntu-latest
env:
CGO_ENABLED: 0
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.19
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get
- name: Build
run: go build -v .
- name: start wakapi
run: ./wakapi --config config.default.yml &
- name: create a trivial testing user
run: sqlite3 wakapi_db.db "insert into users (id, api_key) values ('mapi', 'test-api-key')"
- name: Run Mayhem for API
uses: ForAllSecure/mapi-action@v1
continue-on-error: true
with:
mapi-token: ${{ secrets.MAPI_TOKEN }}
api-url: http://localhost:3000/api/
api-spec: static/docs/swagger.yaml
target: muety/wakapi
duration: 1min
sarif-report: mapi.sarif
run-args: |
--header-auth
Authorization: Basic dGVzdC1hcGkta2V5
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: mapi.sarif
build:
name: 'Build (Win, Linux, Mac)'
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
env:
CGO_ENABLED: 0
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.19
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get
- name: Build
run: go build -v .

View File

@ -8,35 +8,58 @@ on:
jobs:
docker-publish:
name: 'Build and publish Docker image'
runs-on: ubuntu-latest
steps:
# https://stackoverflow.com/questions/58177786
- name: Get version
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Set version
run: |
(git describe --tags --exact-match \
|| git symbolic-ref -q --short HEAD \
|| git rev-parse --short HEAD) > version.txt 2> /dev/null
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to Docker Hub
- name: Log in to the Container registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Metadata
id: meta
uses: docker/metadata-action@v3
with:
images: |
ghcr.io/${{ github.repository }}
n1try/wakapi
tags: |
latest
alpine
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
push: true
tags: |
n1try/wakapi:${{ env.GIT_TAG }}
n1try/wakapi:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=n1try/wakapi:buildcache-alpine
cache-to: type=registry,ref=n1try/wakapi:buildcache-alpine,mode=max

View File

@ -1,50 +0,0 @@
name: Build Wakapi on Linux
on:
push:
branches:
pull_request:
release:
types:
- published
jobs:
build-and-release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get github.com/markbates/pkger/cmd/pkger
go get
go generate
- name: Build
run: GO111MODULE=on go build -v .
- name: Zip executable and sample config
if: github.event_name == 'release'
run: |
cp config.default.yml config.yml
zip -9 release.zip wakapi config.yml
- name: Upload built executable to Release
if: github.event_name == 'release'
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: release.zip
asset_name: wakapi_linux_amd64.zip
asset_content_type: application/gzip

77
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,77 @@
name: Release
on:
release:
types:
- published
jobs:
release:
name: 'Build, package and release to GitHub'
strategy:
fail-fast: false
matrix:
include:
- platform: ubuntu-18.04
GOOS: linux
GOARCH: amd64
- platform: ubuntu-18.04
GOOS: linux
GOARCH: arm64
- platform: windows-latest
GOOS: windows
GOARCH: amd64
- platform: macos-latest
GOOS: darwin
GOARCH: amd64
- platform: macos-latest
GOOS: darwin
GOARCH: arm64
runs-on: ${{ matrix.platform }}
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.19
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Set version
shell: bash
run: |
(git describe --tags --exact-match \
|| git symbolic-ref -q --short HEAD \
|| git rev-parse --short HEAD) > version.txt 2> /dev/null
- name: Prepare
run: |
mkdir -p dist/ && cd dist/
cp ../config.default.yml config.yml
- name: Build
working-directory: ./dist
shell: bash
run: |
GOOS=${{ matrix.GOOS }} GOARCH=${{ matrix.GOARCH }} CGO_ENABLED=0 \
go build -v -ldflags '-w -s' ../
- name: Compress working folder (Windows PowerShell)
working-directory: ./dist
if: "${{ matrix.GOOS == 'windows' }}"
run: |
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip
- name: Compress working folder
working-directory: ./dist
if: "${{ matrix.GOOS != 'windows' }}"
run: |
zip -9 wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip *
- name: Upload built executable to Release
uses: softprops/action-gh-release@v1
with:
files: ./dist/*.zip

View File

@ -1,53 +0,0 @@
name: Build Wakapi on Windows
on:
push:
branches:
pull_request:
release:
types:
- published
jobs:
build-and-release:
name: Build
runs-on: windows-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get github.com/markbates/pkger/cmd/pkger
go get
go generate
- name: Enable Go 1.11 modules
run: cmd /c "set GO111MODULE=on"
- name: Build
run: go build -v .
- name: Compress working folder
if: github.event_name == 'release'
run: |
cp .\config.default.yml .\config.yml
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath release.zip
- name: Upload built executable to Release
if: github.event_name == 'release'
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: release.zip
asset_name: wakapi_win_amd64.zip
asset_content_type: application/gzip

3
.gitignore vendored
View File

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

6
.gitpod.yml Normal file
View File

@ -0,0 +1,6 @@
# List the start up tasks. Learn more https://www.gitpod.io/docs/config-start-tasks/
tasks:
- before: printf "\n[settings]\napi_key = $WAKA_TIME_API_KEY\napi_url = $WAKA_TIME_API_URL\n" > ~/.wakatime.cfg
ports:
- port: 3000
visibility: public

View File

@ -1,23 +1,22 @@
# Build Stage
FROM golang:1.15 AS build-env
FROM golang:1.19-alpine AS build-env
WORKDIR /src
ADD ./go.mod .
RUN go mod download && go get github.com/markbates/pkger/cmd/pkger
RUN curl "https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh" -o wait-for-it.sh && \
RUN wget "https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh" -O wait-for-it.sh && \
chmod +x wait-for-it.sh
ADD ./go.mod ./go.sum ./
RUN go mod download
ADD . .
RUN go generate && go build -o wakapi
WORKDIR /app
RUN cp /src/wakapi . && \
cp /src/config.default.yml config.yml && \
sed -i 's/listen_ipv6: ::1/listen_ipv6: /g' config.yml && \
cp /src/wait-for-it.sh . && \
cp /src/entrypoint.sh .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o wakapi main.go
WORKDIR /staging
RUN mkdir ./data ./app && \
cp /src/wakapi app/ && \
cp /src/config.default.yml app/config.yml && \
sed -i 's/listen_ipv6: ::1/listen_ipv6: /g' app/config.yml && \
cp /src/wait-for-it.sh app/ && \
cp /src/entrypoint.sh app/
# Run Stage
@ -25,25 +24,25 @@ RUN cp /src/wakapi . && \
# to override config values using `-e` syntax.
# Available options can be found in [README.md#-configuration](README.md#-configuration)
FROM debian
FROM alpine:3
WORKDIR /app
RUN apt update && \
apt install -y ca-certificates
RUN apk add --no-cache bash ca-certificates tzdata
ENV ENVIRONMENT prod
ENV WAKAPI_DB_TYPE sqlite3
ENV WAKAPI_DB_USER ''
ENV WAKAPI_DB_PASSWORD ''
ENV WAKAPI_DB_HOST ''
ENV WAKAPI_DB_NAME=/data/wakapi.db
ENV WAKAPI_PASSWORD_SALT ''
ENV WAKAPI_LISTEN_IPV4 '0.0.0.0'
ENV WAKAPI_INSECURE_COOKIES 'true'
ENV WAKAPI_ALLOW_SIGNUP 'true'
# See README.md and config.default.yml for all config options
ENV ENVIRONMENT=prod \
WAKAPI_DB_TYPE=sqlite3 \
WAKAPI_DB_USER='' \
WAKAPI_DB_PASSWORD='' \
WAKAPI_DB_HOST='' \
WAKAPI_DB_NAME=/data/wakapi.db \
WAKAPI_PASSWORD_SALT='' \
WAKAPI_LISTEN_IPV4='0.0.0.0' \
WAKAPI_INSECURE_COOKIES='true' \
WAKAPI_ALLOW_SIGNUP='true'
COPY --from=build-env /app .
COPY --from=build-env /staging /
VOLUME /data
EXPOSE 3000
ENTRYPOINT ./entrypoint.sh
ENTRYPOINT /app/entrypoint.sh

384
README.md
View File

@ -4,15 +4,11 @@
<p align="center">
<img src="https://badges.fw-web.space/github/license/muety/wakapi">
<a href="https://saythanks.io/to/n1try"><img src="https://badges.fw-web.space/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg"></a>
<a href="#-treeware"><img src="https://badges.fw-web.space:/treeware/trees/muety/wakapi?color=%234EC820&label=%F0%9F%8C%B3%20trees"></a>
<a href="https://liberapay.com/muety/"><img src="https://badges.fw-web.space/liberapay/receives/muety.svg?logo=liberapay"></a>
<img src="https://badges.fw-web.space/endpoint?url=https://wakapi.dev/api/compat/shields/v1/n1try/interval:any/project:wakapi&color=blue&label=wakapi">
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/muety/wakapi"><img src="https://goreportcard.com/badge/github.com/muety/wakapi"></a>
<img src="https://wakapi.dev/api/badge/n1try/interval:any/project:wakapi?label=wakapi">
<img src="https://badges.fw-web.space/github/languages/code-size/muety/wakapi">
<a href="https://sonarcloud.io/dashboard?id=muety_wakapi"><img src="https://sonarcloud.io/api/project_badges/measure?project=muety_wakapi&metric=sqale_index"></a>
<a href="https://goreportcard.com/report/github.com/muety/wakapi"><img src="https://goreportcard.com/badge/github.com/muety/wakapi"></a>
<a href="https://sonarcloud.io/dashboard?id=muety_wakapi"><img src="https://sonarcloud.io/api/project_badges/measure?project=muety_wakapi&metric=ncloc"></a>
</p>
@ -24,7 +20,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>
@ -33,30 +29,18 @@
</div>
<p align="center">
<img src="static/assets/images/screenshot.png" width="500px">
<img src="static/assets/images/screenshot.webp" width="500px">
</p>
## Table of Contents
* [User Survey](#-user-survey)
* [Features](#-features)
* [Roadmap](#-roadmap)
* [How to use](#-how-to-use)
* [Configuration Options](#-configuration-options)
* [API Endpoints](#-api-endpoints)
* [Integrations](#-integrations)
* [Best Practices](#-best-practices)
* [Developer Notes](#-developer-notes)
* [Support](#-support)
* [FAQs](#-faqs)
## 📬 **User Survey**
I'd love to get some community feedback from active Wakapi users. If you want, please participate in the recent [user survey](https://github.com/muety/wakapi/issues/82). Thanks a lot!
Installation instructions can be found below and in the [Wiki](https://github.com/muety/wakapi/wiki).
## 🚀 Features
* ✅ 100 % free and open-source
* ✅ 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
@ -65,139 +49,168 @@ I'd love to get some community feedback from active Wakapi users. If you want, p
* ✅ Self-hosted
## 🚧 Roadmap
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#80](https://github.com/muety/wakapi/issues/80), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
## ⌨️ How to use?
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
### ☁️ Option 1: Use [wakapi.dev](https://wakapi.dev)
If you want to you out free, hosted cloud service, all you need to do is create an account and the set up your client-side tooling (see below).
However, we do not guarantee data persistence, so you might potentially lose your data if the service is taken down some day ❕
If you want to try out a free, hosted cloud service, all you need to do is create an account and then set up your client-side tooling (see below).
### 📦 Option 2: Quick-run a release
```bash
$ curl -L https://wakapi.dev/get | bash
```
**Alternatively** using [eget](https://github.com/zyedidia/eget):
```bash
$ eget muety/wakapi
```
### 🐳 Option 3: Use Docker
### 🐳 Option 2: Use Docker
```bash
# Create a persistent volume
$ docker volume create wakapi-data
$ SALT="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)"
# Run the container
$ docker run -d \
-p 3000:3000 \
-e "WAKAPI_PASSWORD_SALT=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)" \
-e "WAKAPI_PASSWORD_SALT=$SALT" \
-v wakapi-data:/data \
--name wakapi n1try/wakapi
--name wakapi \
ghcr.io/muety/wakapi:latest
```
**Note:** By default, SQLite is used as a database. To run Wakapi in Docker with MySQL or Postgres, see [Dockerfile](https://github.com/muety/wakapi/blob/master/Dockerfile) and [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for further options.
### 📦 Option 3: Run a release
```bash
# Download the release and unpack it
$ wget https://github.com/muety/wakapi/releases/download/1.20.2/wakapi_linux_amd64.zip
$ unzip wakapi_linux_amd64.zip
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) for quick and easy deployment.
# Optionally adapt config to your needs
$ vi config.yml
### 🧑‍💻 Option 4: Compile and run from source
```bash
# Build and install
# Alternatively: go build -o wakapi
$ go install github.com/muety/wakapi@latest
# Get default config and customize
$ curl -o wakapi.yml https://raw.githubusercontent.com/muety/wakapi/master/config.default.yml
$ vi wakapi.yml
# Run it
$ ./wakapi
$ ./wakapi -config wakapi.yml
```
### 🧑‍💻 Option 4: Run from source
#### Prerequisites
* Go >= 1.13 (with `$GOPATH` properly set)
* gcc (to compile [go-sqlite3](https://github.com/mattn/go-sqlite3))
* Fedora / RHEL: `dnf install @development-tools`
* Ubuntu / Debian: `apt install build-essential`
* Windows: See [here](https://github.com/mattn/go-sqlite3/issues/214#issuecomment-253216476)
**Note:** Check the comments in `config.yml` for best practices regarding security configuration and more.
#### Compile & Run
```bash
# Adapt config to your needs
$ cp config.default.yml config.yml
$ vi config.yml
💡 When running Wakapi standalone (without Docker), it is recommended to run it as a [SystemD service](etc/wakapi.service).
# Install packaging tool
$ export GO111MODULE=on
$ go get github.com/markbates/pkger/cmd/pkger
### 💻 Client setup
# Build the executable
$ go generate
$ go build -o wakapi
# Run it
$ ./wakapi
```
**Note:** By default, the application is running in dev mode. However, it is recommended to set `ENV=production` for enhanced performance and security. To still be able to log in when using production mode, you either have to run Wakapi behind a reverse proxy, that enables for HTTPS encryption (see [best practices](#best-practices)) or set `security.insecure_cookies = true` in `config.yml`.
### 💻 Client Setup
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime) client tools. In order to collect statistics to Wakapi, you need to set them up.
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime) client tools. In order to collect statistics for Wakapi, you need to set them up.
1. **Set up WakaTime** for your specific IDE or editor. Please refer to the respective [plugin guide](https://wakatime.com/plugins)
2. **Editing your local `~/.wakatime.cfg`** file as follows
2. **Edit your local `~/.wakatime.cfg`** file as follows.
```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
```
Optionally, you can set up a [client-side proxy](docs/advanced_setup.md) in addition.
Optionally, you can set up a [client-side proxy](https://github.com/muety/wakapi/wiki/Advanced-Setup:-Client-side-proxy) in addition.
## 🔧 Configuration Options
You can specify configuration options either via a config file (default: `config.yml`, customziable through the `-c` argument) or via environment variables. Here is an overview of all options.
## 🔧 Configuration options
| YAML Key | Environment Variable | Default | Description |
|---------------------------|---------------------------|--------------|---------------------------------------------------------------------|
| `env` | `ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.custom_languages` | - | - | Map from file endings to language names |
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
| `server.tls_cert_path` | `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
| `server.tls_key_path` | `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
| `server.base_path` | `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
| `security.password_salt` | `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
| `security.insecure_cookies` | `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
| `security.cookie_max_age` | `WAKAPI_COOKIE_MAX_AGE` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
| `security.allow_signup` | `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
| `security.expose_metrics` | `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
| `db.host` | `WAKAPI_DB_HOST` | - | Database host |
| `db.port` | `WAKAPI_DB_PORT` | - | Database port |
| `db.user` | `WAKAPI_DB_USER` | - | Database user |
| `db.password` | `WAKAPI_DB_PASSWORD` | - | Database password |
| `db.name` | `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
You can specify configuration options either via a config file (default: `config.yml`, customizable through the `-c` argument) or via environment variables. Here is an overview of all options.
| YAML key / Env. variable | Default | Description |
|------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `env` /<br>`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.aggregation_time` /<br>`WAKAPI_AGGREGATION_TIME` | `0 15 2 * * *` | Time of day at which to periodically run summary generation for all users |
| `app.report_time_weekly` /<br>`WAKAPI_REPORT_TIME_WEEKLY` | `0 0 18 * * 5` | Week day and time at which to send e-mail reports |
| `app.leaderboard_generation_time` /<br>`WAKAPI_LEADERBOARD_GENERATION_TIME` | `0 0 6 * * *,0 0 18 * * *` | One or multiple times of day at which to re-calculate the leaderboard |
| `app.import_batch_size` /<br>`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services |
| `app.inactive_days` /<br>`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) |
| `app.heartbeat_max_age /`<br>`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) |
| `app.custom_languages` | - | Map from file endings to language names |
| `app.avatar_url_template` /<br>`WAKAPI_AVATAR_URL_TEMPLATE` | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
| `server.port` /<br> `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv4` /<br> `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
| `server.listen_ipv6` /<br> `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
| `server.listen_socket` /<br> `WAKAPI_LISTEN_SOCKET` | - | UNIX socket to listen on (leave blank to disable UNIX socket) |
| `server.timeout_sec` /<br> `WAKAPI_TIMEOUT_SEC` | `30` | Request timeout in seconds |
| `server.tls_cert_path` /<br> `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
| `server.tls_key_path` /<br> `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
| `server.base_path` /<br> `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
| `server.public_url` /<br> `WAKAPI_PUBLIC_URL` | `http://localhost:3000` | URL at which your Wakapi instance can be found publicly |
| `security.password_salt` /<br> `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
| `security.insecure_cookies` /<br> `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
| `security.cookie_max_age` /<br> `WAKAPI_COOKIE_MAX_AGE` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
| `security.allow_signup` /<br> `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
| `security.expose_metrics` /<br> `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
| `db.host` /<br> `WAKAPI_DB_HOST` | - | Database host |
| `db.port` /<br> `WAKAPI_DB_PORT` | - | Database port |
| `db.user` /<br> `WAKAPI_DB_USER` | - | Database user |
| `db.password` /<br> `WAKAPI_DB_PASSWORD` | - | Database password |
| `db.name` /<br> `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
| `db.dialect` /<br> `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
| `db.charset` /<br> `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
| `db.max_conn` /<br> `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
| `db.ssl` /<br> `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
| `db.automgirate_fail_silently` /<br> `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
| `mail.enabled` /<br> `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
| `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `Wakapi <noreply@wakapi.dev>` | Default sender address for outgoing mails (ignored for MailWhale) |
| `mail.provider` /<br> `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
| `mail.smtp.host` /<br> `WAKAPI_MAIL_SMTP_HOST` | - | SMTP server address for sending mail (if using `smtp` mail provider) |
| `mail.smtp.port` /<br> `WAKAPI_MAIL_SMTP_PORT` | - | SMTP server port (usually 465) |
| `mail.smtp.username` /<br> `WAKAPI_MAIL_SMTP_USER` | - | SMTP server authentication username |
| `mail.smtp.password` /<br> `WAKAPI_MAIL_SMTP_PASS` | - | SMTP server authentication password |
| `mail.smtp.tls` /<br> `WAKAPI_MAIL_SMTP_TLS` | `false` | Whether the SMTP server requires TLS encryption (`false` for STARTTLS or no encryption) |
| `mail.mailwhale.url` /<br> `WAKAPI_MAIL_MAILWHALE_URL` | - | URL of [MailWhale](https://mailwhale.dev) instance (e.g. `https://mailwhale.dev`) (if using `mailwhale` mail provider) |
| `mail.mailwhale.client_id` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_ID` | - | MailWhale API client ID |
| `mail.mailwhale.client_secret` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET` | - | MailWhale API client secret |
| `sentry.dsn` /<br> `WAKAPI_SENTRY_DSN` | | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
| `sentry.enable_tracing` /<br> `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
| `sentry.sample_rate` /<br> `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
| `sentry.sample_rate_heartbeats` /<br> `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeat request in Sentry |
| `quick_start` /<br> `WAKAPI_QUICK_START` | `false` | Whether to skip initial boot tasks. Use only for development purposes! |
### Supported databases
Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of different relational databases is supported.
* [SQLite](https://sqlite.org/) (_default, easy setup_)
* [MySQL](https://hub.docker.com/_/mysql) (_recommended, because most extensively tested_)
* [MariaDB](https://hub.docker.com/_/mariadb) (_open-source MySQL alternative_)
* [Postgres](https://hub.docker.com/_/postgres) (_open-source as well_)
* [CockroachDB](https://www.cockroachlabs.com/docs/stable/install-cockroachdb-linux.html) (_cloud-native, distributed, Postgres-compatible API_)
### Client-side proxy (`optional`)
See the [advanced setup instructions](docs/advanced_setup.md).
## 🔧 API endpoints
## 🔧 API Endpoints
See our [Swagger API Documentation](https://wakapi.dev/swagger-ui).
### Generating Swagger docs
```bash
$ go get -u github.com/swaggo/swag/cmd/swag
$ go install github.com/swaggo/swag/cmd/swag@latest
$ swag init -o static/docs
```
## 🤝 Integrations
### Prometheus Export
### Prometheus export
You can export your Wakapi statistics to Prometheus to view them in a Grafana dashboard or so. Here is how.
```bash
@ -212,6 +225,7 @@ $ echo "<YOUR_API_KEY>" | base64
```
#### Scrape config example
```yml
# prometheus.yml
# (assuming your Wakapi instance listens at localhost, port 3000)
@ -225,62 +239,166 @@ scrape_configs:
- targets: ['localhost:3000']
```
#### Grafana
#### Grafana
There is also a [nice Grafana dashboard](https://grafana.com/grafana/dashboards/12790), provided by the author of [wakatime_exporter](https://github.com/MacroPower/wakatime_exporter).
![](https://grafana.com/api/dashboards/12790/images/8741/image)
### WakaTime Integration
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
### WakaTime integration
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
### GitHub Readme Stats integrations
### GitHub Readme Stats Integrations
Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats) to generate fancy cards for you. Here is an example.
![](https://github-readme-stats.vercel.app/api/wakatime?username=n1try&api_domain=wakapi.dev&bg_color=2D3748&title_color=2F855A&icon_color=2F855A&text_color=ffffff&custom_title=Wakapi%20Week%20Stats&layout=compact)
## 👍 Best Practices
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`
<details>
<summary>Click to view code</summary>
## 🤓 Developer Notes
### Running tests
```bash
CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
```markdown
![](https://github-readme-stats.vercel.app/api/wakatime?username={yourusername}&api_domain=wakapi.dev&bg_color=2D3748&title_color=2F855A&icon_color=2F855A&text_color=ffffff&custom_title=Wakapi%20Week%20Stats&layout=compact)
```
## 🙏 Support
If you like this project, please consider supporting it 🙂. You can donate either through [buying me a coffee](https://buymeacoff.ee/n1try) or becoming a GitHub sponsor. Every little donation is highly appreciated and boosts the developers' motivation to keep improving Wakapi!
</details>
<br>
## ❔ FAQs
Since Wakapi heavily relies on the concepts provided by WakaTime, [their FAQs](https://wakatime.com/faq) apply to Wakapi for large parts as well. You might find answers there.
### Github Readme Metrics integration
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [Metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi.
Preview:
![](https://raw.githubusercontent.com/lowlighter/metrics/examples/metrics.plugin.wakatime.svg)
<details>
<summary><b>What data is sent to Wakapi?</b></summary>
<summary>Click to view code</summary>
```yml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_wakatime: yes
plugin_wakatime_token: ${{ secrets.WAKATIME_TOKEN }} # Required
plugin_wakatime_days: 7 # Display last week stats
plugin_wakatime_sections: time, projects, projects-graphs # Display time and projects sections, along with projects graphs
plugin_wakatime_limit: 4 # Show 4 entries per graph
plugin_wakatime_url: http://wakapi.dev # Wakatime url endpoint
plugin_wakatime_user: .user.login # User
```
</details>
<br>
## 👍 Best practices
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or [nginx](https://www.nginx.com/), 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`.
## 🧪 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_ENABLED=0 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. 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, all JS and CSS assets are included as static files and checked in to Git. [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) and [Iconify](https://iconify.design/docs/icon-bundles/) require an additional build step. To only require this at the time of development, the compiled assets are checked in to Git as well.
```bash
$ yarn
$ yarn build # or: yarn watch
```
New icons can be added by editing the `icons` array in [scripts/bundle_icons.js](scripts/bundle_icons.js).
#### Precompression
As explained in [#284](https://github.com/muety/wakapi/issues/284), precompressed (using Brotli) versions of some of the assets are delivered to save additional bandwidth. This was inspired by Caddy's [`precompressed`](https://caddyserver.com/docs/caddyfile/directives/file_server) directive. [`gzipped.FileServer`](https://github.com/muety/wakapi/blob/07a367ce0a97c7738ba8e255e9c72df273fd43a3/main.go#L249) checks for every static file's `.br` or `.gz` equivalents and, if present, delivers those instead of the actual file, alongside `Content-Encoding: br`. Currently, compressed assets are simply checked in to Git. Later we might want to have this be part of a new build step.
To pre-compress files, run this:
```bash
# Install brotli first
$ sudo apt install brotli # or: sudo dnf install brotli
# Watch, build and compress
$ yarn watch:compress
# Alternatively: build and compress only
$ yarn build:all:compress
# Alternatively: compress only
$ yarn compress
```
## ❔ FAQs
Since Wakapi heavily relies on the concepts provided by WakaTime, [their FAQs](https://wakatime.com/faq) largely apply to Wakapi as well. You might find answers there.
<details>
<summary><b>What data are sent to Wakapi?</b></summary>
<ul>
<li>File names</li>
<li>Project names</li>
<li>Editor names</li>
<li>You computer's host name</li>
<li>Your computer's host name</li>
<li>Timestamps for every action you take in your editor</li>
<li>...</li>
</ul>
See the related [WakaTime FAQ section](https://wakatime.com/faq#data-collected) for details.
If you host Wakapi yourself, you have control over all your data. However, if you use our webservice and are concerned about privacy, you can also [exclude or obfuscate](https://wakatime.com/faq#exclude-paths) certain file- or project names.
If you host Wakapi yourself, you have control over all your data. However, if you use our webservice and are concerned about privacy, you can also [exclude or obfuscate](https://wakatime.com/faq#exclude-paths) certain file- or project names.
</details>
<details>
<summary><b>What happens if I'm offline?</b></summary>
All data is cached locally on your machine and sent in batches once you're online again.
All data are cached locally on your machine and sent in batches once you're online again.
</details>
<details>
<summary><b>How did Wakapi come about?</b></summary>
Wakapi was started when I was a student, who wanted to track detailed statistics about my coding time. Although I'm a big fan of WakaTime I didn't want to pay <a href="https://wakatime.com/pricing)">9 $ a month</a> back then. Luckily, most parts of WakaTime are open source!
Wakapi was started when I was a student, who wanted to track detailed statistics about my coding time. Although I'm a big fan of WakaTime I didn't want to pay <a href="https://wakatime.com/pricing">$9 a month</a> back then. Luckily, most parts of WakaTime are open source!
</details>
<details>
@ -293,27 +411,27 @@ Wakapi is a small subset of WakaTime and has a lot less features. Cool WakaTime
<li><a href="https://wakatime.com/share/embed">Embeddable Charts</a></li>
<li>Personal Goals</li>
<li>Team- / Organization Support</li>
<li>Integrations (with GitLab, etc.)</li>
<li>Additional Integrations (with GitLab, etc.)</li>
<li>Richer API</li>
</ul>
WakaTime is worth the price. However, if you only want basic statistics and keep sovereignty over your data, you might want to go with Wakapi.
WakaTime is worth the price. However, if you only need basic statistics and like to keep sovereignty over your data, you might want to go with Wakapi.
</details>
<details>
<summary><b>How are durations calculated?</b></summary>
Inferring a measure for your coding time from heartbeats works a bit different than in WakaTime. While WakaTime has <a href="https://wakatime.com/faq#timeout">timeout intervals</a>, Wakapi essentially just pads every heartbeat, that occurs after a longer pause, with 2 extra minutes.
Inferring a measure for your coding time from heartbeats works a bit differently than in WakaTime. While WakaTime has <a href="https://wakatime.com/faq#timeout">timeout intervals</a>, Wakapi essentially just pads every heartbeat that occurs after a longer pause with 2 extra minutes.
Here is an example (circles are heartbeats):
```
```text
|---o---o--------------o---o---|
| |10s| 3m |10s| |
```
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break or were just no heartbeats being sent, e.g. because the developer was starring at the screen find a solution, but not actually typing code.
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break, or were just no heartbeats being sent, e.g. because the developer was staring at the screen trying to find a solution, but not actually typing code?
<ul>
<li><b>WakaTime</b> (with 5 min timeout): 3 min 20 sec
@ -324,8 +442,22 @@ It is unclear how to handle the three minutes in between. Did the developer do a
Wakapi adds a "padding" of two minutes before the third heartbeat. This is why total times will slightly vary between Wakapi and WakaTime.
</details>
## 🌳 Treeware
This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/muety/wakapi) to thank us for our work. By contributing to the Treeware forest youll be creating employment for local families and restoring wildlife habitats.
## 👏 Support
Coding in open source is my passion and I would love to do it on a full-time basis and make a living from it one day. So if you like this project, please consider supporting it 🙂. You can donate either through [buying me a coffee](https://buymeacoff.ee/n1try) or becoming a GitHub sponsor. Every little donation is highly appreciated and boosts my motivation to keep improving Wakapi!
## 🙏 Thanks
I highly appreciate the efforts of [@alanhamlett](https://github.com/alanhamlett) and the WakaTime team and am thankful for their software being open source.
I highly appreciate the efforts of **[@alanhamlett](https://github.com/alanhamlett)** and the WakaTime team and am thankful for their software being open source.
Moreover, thanks to **[JetBrains](https://jb.gg/OpenSource)** for supporting this project as part of their open-source program.
![](static/assets/images/jetbrains-logo.png)
## 📓 License
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)

View File

@ -1,33 +1,82 @@
env: development
env: production
server:
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
listen_ipv6: ::1 # leave blank to disable ipv6
tls_cert_path: # leave blank to not use https
tls_key_path: # leave blank to not use https
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
listen_ipv6: ::1 # leave blank to disable ipv6
listen_socket: # leave blank to disable unix sockets
timeout_sec: 30 # request timeout
tls_cert_path: # leave blank to not use https
tls_key_path: # leave blank to not use https
port: 3000
base_path: /
public_url: http://localhost:3000 # required for links (e.g. password reset) in e-mail
app:
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
aggregation_time: '0 15 2 * * *' # time at which to run daily aggregation batch jobs
leaderboard_generation_time: '0 0 6 * * *,0 0 18 * * *' # times at which to re-calculate the leaderboard
report_time_weekly: '0 0 18 * * 5' # time at which to fan out weekly reports (extended cron)
data_cleanup_time: '0 0 6 * * 7' # time at which to run old data cleanup (if enabled through data_retention_months)
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
data_retention_months: -1 # maximum retention period on months for user data (heartbeats) (-1 for infinity)
custom_languages:
vue: Vue
jsx: JSX
tsx: TSX
cjs: JavaScript
ipynb: Python
svelte: Svelte
# url template for user avatar images (to be used with services like gravatar or dicebear)
# available variable placeholders are: username, username_hash, email, email_hash
# defaults to wakapi's internal avatar rendering powered by https://codeberg.org/Codeberg/avatars
avatar_url_template: api/avatar/{username_hash}.svg
db:
host: # leave blank when using sqlite3
port: # leave blank when using sqlite3
user: # leave blank when using sqlite3
password: # leave blank when using sqlite3
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
dialect: sqlite3 # mysql, postgres, sqlite3
max_conn: 2 # maximum number of concurrent connections to maintain
ssl: false # whether to use tls for db connection (must be true for cockroachdb) (ignored for mysql and sqlite)
host: # leave blank when using sqlite3
port: # leave blank when using sqlite3
user: # leave blank when using sqlite3
password: # leave blank when using sqlite3
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
dialect: sqlite3 # mysql, postgres, sqlite3
charset: utf8mb4 # only used for mysql connections
max_conn: 2 # maximum number of concurrent connections to maintain
ssl: false # whether to use tls for db connection (must be true for cockroachdb) (ignored for mysql and sqlite)
automgirate_fail_silently: false # whether to ignore schema auto-migration failures when starting up
security:
password_salt: # CHANGE !
insecure_cookies: false # You need to set this to 'true' when on localhost
password_salt: # change this
insecure_cookies: true # should be set to 'false', except when not running with HTTPS (e.g. on localhost)
cookie_max_age: 172800
allow_signup: true
expose_metrics: false
expose_metrics: false
enable_proxy: false # only intended for production instance at wakapi.dev
sentry:
dsn: # leave blank to disable sentry integration
enable_tracing: true # whether to use performance monitoring
sample_rate: 0.75 # probability of tracing a request
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
mail:
enabled: true # whether to enable mails (used for password resets, reports, etc.)
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
sender: Wakapi <noreply@wakapi.dev> # ignored for mailwhale
# smtp settings when sending mails via smtp
smtp:
host:
port:
username:
password:
tls:
# mailwhale.dev settings when using mailwhale as sending service
mailwhale:
url:
client_id:
client_secret:
quick_start: false # whether to skip initial tasks on application startup, like summary generation
skip_migrations: false # whether to intentionally not run database migrations, only use for dev purposes

View File

@ -4,20 +4,23 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/emvi/logbuch"
"github.com/gorilla/securecookie"
"github.com/jinzhu/configor"
"github.com/markbates/pkger"
"github.com/muety/wakapi/models"
migrate "github.com/rubenv/sql-migrate"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/muety/wakapi/utils"
"github.com/robfig/cron/v3"
"io/ioutil"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/emvi/logbuch"
"github.com/gorilla/securecookie"
"github.com/jinzhu/configor"
"github.com/muety/wakapi/data"
"github.com/muety/wakapi/models"
uuid "github.com/satori/go.uuid"
"gorm.io/gorm"
)
const (
@ -30,11 +33,13 @@ const (
KeyLatestTotalTime = "latest_total_time"
KeyLatestTotalUsers = "latest_total_users"
KeyLastImportImport = "last_import"
KeyNewsbox = "newsbox"
SimpleDateFormat = "2006-01-02"
SimpleDateTimeFormat = "2006-01-02 15:04:05"
ErrUnauthorized = "401 unauthorized"
ErrBadRequest = "400 bad request"
ErrInternalServerError = "500 internal server error"
)
@ -48,21 +53,40 @@ const (
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
)
const (
MailProviderSmtp = "smtp"
MailProviderMailWhale = "mailwhale"
)
var emailProviders = []string{
MailProviderSmtp,
MailProviderMailWhale,
}
var cfg *Config
var cFlag = flag.String("config", defaultConfigPath, "config file location")
var env string
type appConfig struct {
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
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"`
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:"-"`
AggregationTime string `yaml:"aggregation_time" default:"0 15 2 * * *" env:"WAKAPI_AGGREGATION_TIME"`
LeaderboardGenerationTime string `yaml:"leaderboard_generation_time" default:"0 0 6 * * *,0 0 18 * * *" env:"WAKAPI_LEADERBOARD_GENERATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"0 0 18 * * 5" env:"WAKAPI_REPORT_TIME_WEEKLY"`
DataCleanupTime string `yaml:"data_cleanup_time" default:"0 0 6 * * 7" env:"WAKAPI_DATA_CLEANUP_TIME"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
DataRetentionMonths int `yaml:"data_retention_months" default:"-1" env:"WAKAPI_DATA_RETENTION_MONTHS"`
AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg" env:"WAKAPI_AVATAR_URL_TEMPLATE"`
CustomLanguages map[string]string `yaml:"custom_languages"`
Colors map[string]map[string]string `yaml:"-"`
}
type securityConfig struct {
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
EnableProxy bool `yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY"` // only intended for production instance at wakapi.dev
// this is actually a pepper (https://en.wikipedia.org/wiki/Pepper_(cryptography))
PasswordSalt string `yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT"`
InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"`
@ -71,41 +95,81 @@ type securityConfig struct {
}
type dbConfig struct {
Host string `env:"WAKAPI_DB_HOST"`
Port uint `env:"WAKAPI_DB_PORT"`
User string `env:"WAKAPI_DB_USER"`
Password string `env:"WAKAPI_DB_PASSWORD"`
Name string `default:"wakapi_db.db" env:"WAKAPI_DB_NAME"`
Dialect string `yaml:"-"`
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
Host string `env:"WAKAPI_DB_HOST"`
Port uint `env:"WAKAPI_DB_PORT"`
User string `env:"WAKAPI_DB_USER"`
Password string `env:"WAKAPI_DB_PASSWORD"`
Name string `default:"wakapi_db.db" env:"WAKAPI_DB_NAME"`
Dialect string `yaml:"-"`
Charset string `default:"utf8mb4" env:"WAKAPI_DB_CHARSET"`
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
DSN string `yaml:"DSN" default:"" env:"WAKAPI_DB_DSN"`
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
AutoMigrateFailSilently bool `yaml:"automigrate_fail_silently" default:"false" env:"WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY"`
}
type serverConfig struct {
Port int `default:"3000" env:"WAKAPI_PORT"`
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
Port int `default:"3000" env:"WAKAPI_PORT"`
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
ListenSocket string `yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET"`
TimeoutSec int `yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC"`
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"`
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
}
type sentryConfig struct {
Dsn string `env:"WAKAPI_SENTRY_DSN"`
EnableTracing bool `yaml:"enable_tracing" env:"WAKAPI_SENTRY_TRACING"`
SampleRate float32 `yaml:"sample_rate" default:"0.75" env:"WAKAPI_SENTRY_SAMPLE_RATE"`
SampleRateHeartbeats float32 `yaml:"sample_rate_heartbeats" default:"0.1" env:"WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS"`
}
type mailConfig struct {
Enabled bool `env:"WAKAPI_MAIL_ENABLED" default:"true"`
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
MailWhale MailwhaleMailConfig `yaml:"mailwhale"`
Smtp SMTPMailConfig `yaml:"smtp"`
Sender string `env:"WAKAPI_MAIL_SENDER" yaml:"sender"`
}
type MailwhaleMailConfig struct {
Url string `env:"WAKAPI_MAIL_MAILWHALE_URL"`
ClientId string `yaml:"client_id" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_ID"`
ClientSecret string `yaml:"client_secret" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET"`
}
type SMTPMailConfig struct {
Host string `env:"WAKAPI_MAIL_SMTP_HOST"`
Port uint `env:"WAKAPI_MAIL_SMTP_PORT"`
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
}
type Config struct {
Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"`
App appConfig
Security securityConfig
Db dbConfig
Server serverConfig
Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"`
QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
SkipMigrations bool `yaml:"skip_migrations" env:"WAKAPI_SKIP_MIGRATIONS"`
InstanceId string `yaml:"-"` // only temporary, changes between runs
App appConfig
Security securityConfig
Db dbConfig
Server serverConfig
Sentry sentryConfig
Mail mailConfig
}
func (c *Config) CreateCookie(name, value, path string) *http.Cookie {
return c.createCookie(name, value, path, c.Security.CookieMaxAgeSec)
func (c *Config) CreateCookie(name, value string) *http.Cookie {
return c.createCookie(name, value, c.Server.BasePath, c.Security.CookieMaxAgeSec)
}
func (c *Config) GetClearCookie(name, path string) *http.Cookie {
return c.createCookie(name, "", path, -1)
func (c *Config) GetClearCookie(name string) *http.Cookie {
return c.createCookie(name, "", c.Server.BasePath, -1)
}
func (c *Config) createCookie(name, value, path string, maxAge int) *http.Cookie {
@ -132,142 +196,180 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
switch dbDialect {
default:
return func(db *gorm.DB) error {
db.AutoMigrate(&models.User{})
db.AutoMigrate(&models.KeyStringValue{})
db.AutoMigrate(&models.Alias{})
db.AutoMigrate(&models.Heartbeat{})
db.AutoMigrate(&models.Summary{})
db.AutoMigrate(&models.SummaryItem{})
db.AutoMigrate(&models.LanguageMapping{})
if err := db.AutoMigrate(&models.User{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.KeyStringValue{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Alias{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Heartbeat{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Summary{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.SummaryItem{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.LanguageMapping{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.LeaderboardItem{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
return nil
}
}
}
func (c *Config) GetFixturesFunc(dbDialect string) models.MigrationFunc {
return func(db *gorm.DB) error {
migrations := &migrate.HttpFileSystemMigrationSource{
FileSystem: pkger.Dir("/migrations"),
}
migrate.SetIgnoreUnknown(true)
sqlDb, _ := db.DB()
n, err := migrate.Exec(sqlDb, dbDialect, migrations, migrate.Up)
if err != nil {
return err
}
logbuch.Info("applied %d fixtures", n)
return nil
}
}
func (c *dbConfig) GetDialector() gorm.Dialector {
switch c.Dialect {
case SQLDialectMysql:
return mysql.New(mysql.Config{
DriverName: c.Dialect,
DSN: mysqlConnectionString(c),
})
case SQLDialectPostgres:
return postgres.New(postgres.Config{
DSN: postgresConnectionString(c),
})
case SQLDialectSqlite:
return sqlite.Open(sqliteConnectionString(c))
}
return nil
}
func mysqlConnectionString(config *dbConfig) string {
//location, _ := time.LoadLocation("Local")
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
config.User,
config.Password,
config.Host,
config.Port,
config.Name,
"Local",
)
}
func postgresConnectionString(config *dbConfig) string {
sslmode := "disable"
if config.Ssl {
sslmode = "require"
}
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s",
config.Host,
config.Port,
config.User,
config.Name,
config.Password,
sslmode,
)
}
func sqliteConnectionString(config *dbConfig) string {
return config.Name
}
func (c *appConfig) GetCustomLanguages() map[string]string {
return cloneStringMap(c.CustomLanguages, false)
return utils.CloneStringMap(c.CustomLanguages, false)
}
func (c *appConfig) GetLanguageColors() map[string]string {
return cloneStringMap(c.Colors["languages"], true)
return utils.CloneStringMap(c.Colors["languages"], true)
}
func (c *appConfig) GetEditorColors() map[string]string {
return cloneStringMap(c.Colors["editors"], true)
return utils.CloneStringMap(c.Colors["editors"], true)
}
func (c *appConfig) GetOSColors() map[string]string {
return cloneStringMap(c.Colors["operating_systems"], true)
return utils.CloneStringMap(c.Colors["operating_systems"], true)
}
func (c *appConfig) GetAggregationTimeCron() string {
if strings.Contains(c.AggregationTime, ":") {
// old gocron format, e.g. "15:04"
timeParts := strings.Split(c.AggregationTime, ":")
h, err := strconv.Atoi(timeParts[0])
if err != nil {
logbuch.Fatal(err.Error())
}
m, err := strconv.Atoi(timeParts[1])
if err != nil {
logbuch.Fatal(err.Error())
}
return fmt.Sprintf("0 %d %d * * *", m, h)
}
return utils.CronPadToSecondly(c.AggregationTime)
}
func (c *appConfig) GetWeeklyReportCron() string {
if strings.Contains(c.ReportTimeWeekly, ",") {
// old gocron format, e.g. "fri,18:00"
split := strings.Split(c.ReportTimeWeekly, ",")
weekday := utils.ParseWeekday(split[0])
timeParts := strings.Split(split[1], ":")
h, err := strconv.Atoi(timeParts[0])
if err != nil {
logbuch.Fatal(err.Error())
}
m, err := strconv.Atoi(timeParts[1])
if err != nil {
logbuch.Fatal(err.Error())
}
return fmt.Sprintf("0 %d %d * * %d", m, h, weekday)
}
return utils.CronPadToSecondly(c.ReportTimeWeekly)
}
func (c *appConfig) GetLeaderboardGenerationTimeCron() []string {
crons := []string{}
var parse func(string) string
if strings.Contains(c.LeaderboardGenerationTime, ":") {
// old gocron format, e.g. "15:04"
parse = func(s string) string {
timeParts := strings.Split(s, ":")
h, err := strconv.Atoi(timeParts[0])
if err != nil {
logbuch.Fatal(err.Error())
}
m, err := strconv.Atoi(timeParts[1])
if err != nil {
logbuch.Fatal(err.Error())
}
return fmt.Sprintf("0 %d %d * * *", m, h)
}
} else {
parse = func(s string) string {
return utils.CronPadToSecondly(s)
}
}
for _, s := range utils.SplitMulti(c.LeaderboardGenerationTime, ",", ";") {
crons = append(crons, parse(strings.TrimSpace(s)))
}
return crons
}
func (c *appConfig) HeartbeatsMaxAge() time.Duration {
d, _ := time.ParseDuration(c.HeartbeatMaxAge)
return d
}
func (c *dbConfig) IsSQLite() bool {
return c.Dialect == "sqlite3"
}
func (c *dbConfig) IsMySQL() bool {
return c.Dialect == "mysql"
}
func (c *dbConfig) IsPostgres() bool {
return c.Dialect == "postgres"
}
func (c *serverConfig) GetPublicUrl() string {
return strings.TrimSuffix(c.PublicUrl, "/")
}
func (c *SMTPMailConfig) ConnStr() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
func IsDev(env string) bool {
return env == "dev" || env == "development"
}
func readVersion() string {
file, err := pkger.Open("/version.txt")
if err != nil {
logbuch.Fatal(err.Error())
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
logbuch.Fatal(err.Error())
}
return strings.TrimSpace(string(bytes))
}
func readColors() map[string]map[string]string {
// Read language colors
// Source:
// https://raw.githubusercontent.com/ozh/github-colors/master/colors.json
// https://wakatime.com/colors/operating_systems
// - https://raw.githubusercontent.com/ozh/github-colors/master/colors.json
// - https://wakatime.com/colors/operating_systems
// - https://wakatime.com/colors/editors
// Extracted from Wakatime website with XPath (see below) and did a bit of regex magic after.
// $x('//span[@class="editor-icon tip"]/@data-original-title').map(e => e.nodeValue)
// $x('//span[@class="editor-icon tip"]/div[1]/text()').map(e => e.nodeValue)
// - $x('//span[@class="editor-icon tip"]/@data-original-title').map(e => e.nodeValue)
// - $x('//span[@class="editor-icon tip"]/div[1]/text()').map(e => e.nodeValue)
raw := data.ColorsFile
if IsDev(env) {
raw, _ = ioutil.ReadFile("data/colors.json")
}
var colors = make(map[string]map[string]string)
file, err := pkger.Open("/data/colors.json")
if err != nil {
logbuch.Fatal(err.Error())
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
logbuch.Fatal(err.Error())
}
if err := json.Unmarshal(bytes, &colors); err != nil {
if err := json.Unmarshal(raw, &colors); err != nil {
logbuch.Fatal(err.Error())
}
@ -285,6 +387,12 @@ func resolveDbDialect(dbType string) string {
if dbType == "cockroach" {
return "postgres"
}
if dbType == "sqlite" {
return "sqlite3"
}
if dbType == "mariadb" {
return "mysql"
}
return dbType
}
@ -296,7 +404,7 @@ func Get() *Config {
return cfg
}
func Load() *Config {
func Load(version string) *Config {
config := &Config{}
flag.Parse()
@ -305,7 +413,15 @@ func Load() *Config {
logbuch.Fatal("failed to read config: %v", err)
}
config.Version = readVersion()
env = config.Env
config.Version = strings.TrimSpace(version)
tagVersionMatch, _ := regexp.MatchString(`\d+\.\d+\.\d+`, version)
if tagVersionMatch {
config.Version = "v" + config.Version
}
config.InstanceId = uuid.NewV4().String()
config.App.Colors = readColors()
config.Db.Dialect = resolveDbDialect(config.Db.Type)
config.Security.SecureCookie = securecookie.New(
@ -323,13 +439,59 @@ func Load() *Config {
}
}
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" {
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 must be set")
if config.Sentry.Dsn != "" {
logbuch.Info("enabling sentry integration")
initSentry(config.Sentry, config.IsDev())
}
if config.App.DataRetentionMonths <= 0 {
logbuch.Info("disabling data retention policy, keeping data forever")
} else {
logbuch.Info("data retention policy set to keep data for %d months at max", config.App.DataRetentionMonths)
}
// some validation checks
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" && config.Server.ListenSocket == "" {
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 or listen_socket must be set")
}
if config.Db.MaxConn <= 0 {
logbuch.Fatal("you must allow at least one database connection")
}
if config.Db.MaxConn > 1 && config.Db.IsSQLite() {
logbuch.Warn("with sqlite, only a single connection is supported") // otherwise 'PRAGMA foreign_keys=ON' would somehow have to be set for every connection in the pool
config.Db.MaxConn = 1
}
if config.Mail.Provider != "" && utils.FindString(config.Mail.Provider, emailProviders, "") == "" {
logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider)
}
if _, err := time.ParseDuration(config.App.HeartbeatMaxAge); err != nil {
logbuch.Fatal("invalid duration set for heartbeat_max_age")
}
cronParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
if _, err := cronParser.Parse(config.App.GetWeeklyReportCron()); err != nil {
logbuch.Fatal("invalid cron expression for report_time_weekly")
}
if _, err := cronParser.Parse(config.App.GetAggregationTimeCron()); err != nil {
logbuch.Fatal("invalid cron expression for aggregation_time")
}
for _, c := range config.App.GetLeaderboardGenerationTimeCron() {
if _, err := cronParser.Parse(c); err != nil {
logbuch.Fatal("invalid cron expression for leaderboard_generation_time")
}
}
// deprecation notices
if strings.Contains(config.App.AggregationTime, ":") {
logbuch.Warn("you're using deprecated syntax for 'aggregation_time', please change it to a valid cron expression")
}
if strings.Contains(config.App.ReportTimeWeekly, ":") {
logbuch.Warn("you're using deprecated syntax for 'report_time_weekly', please change it to a valid cron expression")
}
if strings.Contains(config.App.LeaderboardGenerationTime, ":") {
logbuch.Warn("you're using deprecated syntax for 'leaderboard_generation_time', please change it to a semicolon-separated list if valid cron expressions")
}
Set(config)
return Get()

View File

@ -22,11 +22,12 @@ func Test_mysqlConnectionString(t *testing.T) {
Password: "test_password",
Name: "test_name",
Dialect: "mysql",
Charset: "utf8mb4",
MaxConn: 10,
}
assert.Equal(t, fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
c.User,
c.Password,
c.Host,

90
config/db.go Normal file
View File

@ -0,0 +1,90 @@
package config
import (
"fmt"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
/*
A quick note to myself including some clarifications about time zones.
- There are basically four time zones (at least in case of MySQL): (1) User, (2) Wakapi (host system), (3) MySQL server, (4) MySQL session
- From my understanding, MySQL server tz is only a fallback and can be ignored as long as a connection tz is specified
- All times are currently stored inside TIMESTAMP columns (alternatives would be DATETIME and BIGINT (plain Unix timestamps))
- TIMESTAMP columns, to my understanding, do not keep any time zone information, but only the very time they store
- Setting a `loc` parameter specifies what location parsed time.Time objects will be in, however, does not affect the session time zone setting (https://github.com/go-sql-driver/mysql#loc)
- I.e., when not setting `time_zone` in addition, the session time zone will probably default to the server time zone (UTC in case of Docker)
- Session time zone will result in conversions of inserted times from that time zone to UTC
- From my understanding, TIMESTAMP only stores a plain time value without tz information and then converts it only for retrieval to whatever tz is set for the session
- E.g., when inserting '2021-04-27 08:26:07' with session tz set to Europe/Berlin and then viewing the database table with UTC tz will return '2021-04-27 06:26:07' instead
- Currently, no session tz is set (only loc), so the database server will assume it receives UTC. However, as no tz is set when retrieving the values either, they are also going to be returned just as is and as long as `loc=Local` is set properly, they are parsed in Go code with the correct time zone
- As long as the Wakapi server always runs in the same time zone, it will always parse these dates the same way (i.e. as time.Local, Europe/Berlin in case of Wakapi.dev)
- Using TIMESTAMP columns would only become problematic when either data needs to be migrated to a Wakapi instance in a different tz or if two consumers in different tzs were reading and writing to the same table
- It is important to have same `time_zone` and `loc` parameters set when sending and receiving, no matter what it is (writing / reading in 'UTC' will yield same results as writing / reading in 'Europe/Berlin')
- "The session time zone setting affects display and storage of time values that are zone-sensitive. This includes the values displayed by functions such as NOW() or CURTIME(), and values stored in and retrieved from TIMESTAMP columns. Values for TIMESTAMP columns are converted from the session time zone to UTC for storage, and from UTC to the session time zone for retrieval." (https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html)
- Wakapi always uses time.Local for everything, i.e. all times in the database have to be interpreted with that tz
- New heartbeats are sent with Python-like Unix timestamps, i.e. are absolute points in time as therefore not subject to any kind of tz issues
- E.g. with Wakapi running in Europe/Berlin, 1619379014.7335322 (2021-04-25T19:30:14.733Z (UTC)) will be inserted as 2021-04-25T21:30:14.733+0200 (CEST), but obviously represents the exact same point in time no matter where it originated from
- The reason why we need to explicitly care about tzs in the first place is the fact that user's can request their data within intervals and the results should correspond to their tz
- Users from California wouldn't have to care about their heartbeats being stored in German time zone
- However, they DO care when requesting their summaries
- A request with `?from=2021-04-25` from California (PST / UTC-7) would ideally have to be translated into a database query like `from >= 2021-04-25T00:00:00+0900)`, assuming that Wakapi runs at CEST (UTC+2)
- This translation comes from either the user explicitly requesting with a specified tz (i.e. sending `from` as ISO8601 / RFC3999) or them having specified a tz in their profile
- Implicit intervals are tricky, too, as they are generated on the server, but still have to respect the user's tz, as `today` is different for a user in Cali and one in Karlsruhe
*/
func (c *dbConfig) GetDialector() gorm.Dialector {
switch c.Dialect {
case SQLDialectMysql:
return mysql.New(mysql.Config{
DriverName: c.Dialect,
DSN: mysqlConnectionString(c),
})
case SQLDialectPostgres:
return postgres.New(postgres.Config{
DSN: postgresConnectionString(c),
})
case SQLDialectSqlite:
return sqlite.Open(sqliteConnectionString(c))
}
return nil
}
func mysqlConnectionString(config *dbConfig) string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
config.User,
config.Password,
config.Host,
config.Port,
config.Name,
config.Charset,
"Local",
)
}
func postgresConnectionString(config *dbConfig) string {
if len(config.DSN) > 0 {
return config.DSN
}
sslmode := "disable"
if config.Ssl {
sslmode = "require"
}
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s",
config.Host,
config.Port,
config.User,
config.Name,
config.Password,
sslmode,
)
}
func sqliteConnectionString(config *dbConfig) string {
return config.Name
}

32
config/eventbus.go Normal file
View File

@ -0,0 +1,32 @@
package config
import "github.com/leandro-lugaresi/hub"
type ApplicationEvent struct {
Type string
Payload interface{}
}
const (
TopicUser = "user.*"
TopicHeartbeat = "heartbeat.*"
TopicProjectLabel = "project_label.*"
EventUserUpdate = "user.update"
EventHeartbeatCreate = "heartbeat.create"
EventProjectLabelCreate = "project_label.create"
EventProjectLabelDelete = "project_label.delete"
EventWakatimeFailure = "wakatime.failure"
FieldPayload = "payload"
FieldUser = "user"
FieldUserId = "user.id"
)
var eventHub *hub.Hub
func init() {
eventHub = hub.New()
}
func EventBus() *hub.Hub {
return eventHub
}

14
config/fs.go Normal file
View File

@ -0,0 +1,14 @@
package config
import (
"io/fs"
"os"
)
// ChooseFS returns a local (DirFS) file system when on 'dev' environment and the given go-embed file system otherwise
func ChooseFS(localDir string, embeddedFS fs.FS) fs.FS {
if Get().IsDev() {
return os.DirFS(localDir)
}
return embeddedFS
}

83
config/jobqueue.go Normal file
View File

@ -0,0 +1,83 @@
package config
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/muety/artifex/v2"
"math"
"runtime"
)
var jobQueues map[string]*artifex.Dispatcher
var jobCounts map[string]int
const (
QueueDefault = "wakapi.default"
QueueProcessing = "wakapi.processing"
QueueReports = "wakapi.reports"
QueueImports = "wakapi.imports"
QueueHousekeeping = "wakapi.housekeeping"
)
type JobQueueMetrics struct {
Queue string
EnqueuedJobs int
FinishedJobs int
}
func init() {
jobQueues = make(map[string]*artifex.Dispatcher)
InitQueue(QueueDefault, 1)
InitQueue(QueueProcessing, halfCPUs())
InitQueue(QueueReports, 1)
InitQueue(QueueImports, 1)
InitQueue(QueueHousekeeping, halfCPUs())
}
func InitQueue(name string, workers int) error {
if _, ok := jobQueues[name]; ok {
return fmt.Errorf("queue '%s' already existing", name)
}
logbuch.Info("creating job queue '%s' (%d workers)", name, workers)
jobQueues[name] = artifex.NewDispatcher(workers, 4096)
jobQueues[name].Start()
return nil
}
func GetDefaultQueue() *artifex.Dispatcher {
return GetQueue(QueueDefault)
}
func GetQueue(name string) *artifex.Dispatcher {
if _, ok := jobQueues[name]; !ok {
InitQueue(name, 1)
}
return jobQueues[name]
}
func GetQueueMetrics() []*JobQueueMetrics {
metrics := make([]*JobQueueMetrics, 0, len(jobQueues))
for name, queue := range jobQueues {
metrics = append(metrics, &JobQueueMetrics{
Queue: name,
EnqueuedJobs: queue.CountEnqueued(),
FinishedJobs: queue.CountDispatched(),
})
}
return metrics
}
func CloseQueues() {
for _, q := range jobQueues {
q.Stop()
}
}
func allCPUs() int {
return runtime.NumCPU()
}
func halfCPUs() int {
return int(math.Ceil(float64(runtime.NumCPU()) / 2.0))
}

156
config/sentry.go Normal file
View File

@ -0,0 +1,156 @@
package config
import (
"github.com/emvi/logbuch"
"github.com/getsentry/sentry-go"
"github.com/muety/wakapi/models"
"io"
"net/http"
"os"
"strings"
)
// How to: Logging
// Use logbuch.[Debug|Info|Warn|Error|Fatal]() by default
// Use config.Log().[Debug|Info|Warn|Error|Fatal]() when wanting the log to appear in Sentry as well
type capturingWriter struct {
Writer io.Writer
Message string
}
func (c *capturingWriter) Clear() {
c.Message = ""
}
func (c *capturingWriter) Write(p []byte) (n int, err error) {
c.Message = string(p)
return c.Writer.Write(p)
}
// SentryWrapperLogger is a wrapper around a logbuch.Logger that forwards events to Sentry in addition and optionally allows to attach a request context
type SentryWrapperLogger struct {
*logbuch.Logger
req *http.Request
outWriter *capturingWriter
errWriter *capturingWriter
}
func Log() *SentryWrapperLogger {
ow, ew := &capturingWriter{Writer: os.Stdout}, &capturingWriter{Writer: os.Stderr}
return &SentryWrapperLogger{
Logger: logbuch.NewLogger(ow, ew),
outWriter: ow,
errWriter: ew,
}
}
func (l *SentryWrapperLogger) Request(req *http.Request) *SentryWrapperLogger {
l.req = req
return l
}
func (l *SentryWrapperLogger) Debug(msg string, params ...interface{}) {
l.outWriter.Clear()
l.Logger.Debug(msg, params...)
l.log(l.errWriter.Message, sentry.LevelDebug)
}
func (l *SentryWrapperLogger) Info(msg string, params ...interface{}) {
l.outWriter.Clear()
l.Logger.Info(msg, params...)
l.log(l.errWriter.Message, sentry.LevelInfo)
}
func (l *SentryWrapperLogger) Warn(msg string, params ...interface{}) {
l.outWriter.Clear()
l.Logger.Warn(msg, params...)
l.log(l.errWriter.Message, sentry.LevelWarning)
}
func (l *SentryWrapperLogger) Error(msg string, params ...interface{}) {
l.errWriter.Clear()
l.Logger.Error(msg, params...)
l.log(l.errWriter.Message, sentry.LevelError)
}
func (l *SentryWrapperLogger) Fatal(msg string, params ...interface{}) {
l.errWriter.Clear()
l.Logger.Fatal(msg, params...)
l.log(l.errWriter.Message, sentry.LevelFatal)
}
func (l *SentryWrapperLogger) log(msg string, level sentry.Level) {
event := sentry.NewEvent()
event.Level = level
event.Message = msg
if l.req != nil {
if h := l.req.Context().Value(sentry.HubContextKey); h != nil {
hub := h.(*sentry.Hub)
hub.Scope().SetRequest(l.req)
if u := getPrincipal(l.req); u != nil {
hub.Scope().SetUser(sentry.User{ID: u.ID})
}
hub.CaptureEvent(event)
return
}
}
sentry.CaptureEvent(event)
}
var excludedRoutes = []string{
"GET /assets",
"GET /api/health",
"GET /swagger-ui",
"GET /docs",
}
func initSentry(config sentryConfig, debug bool) {
if err := sentry.Init(sentry.ClientOptions{
Dsn: config.Dsn,
Debug: debug,
AttachStacktrace: true,
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
if !config.EnableTracing {
return sentry.SampledFalse
}
hub := sentry.GetHubFromContext(ctx.Span.Context())
txName := hub.Scope().Transaction()
for _, ex := range excludedRoutes {
if strings.HasPrefix(txName, ex) {
return sentry.SampledFalse
}
}
if txName == "POST /api/heartbeat" {
return sentry.UniformTracesSampler(config.SampleRateHeartbeats).Sample(ctx)
}
return sentry.UniformTracesSampler(config.SampleRate).Sample(ctx)
}),
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
if hint.Context != nil {
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok {
if u := getPrincipal(req); u != nil {
event.User.ID = u.ID
}
}
}
return event
},
}); err != nil {
logbuch.Fatal("failed to initialized sentry - %v", err)
}
}
func getPrincipal(r *http.Request) *models.User {
type principalGetter interface {
GetPrincipal() *models.User
}
if p := r.Context().Value("principal"); p != nil {
return p.(principalGetter).GetPrincipal()
}
return nil
}

View File

@ -1,10 +1,13 @@
package config
const (
IndexTemplate = "index.tpl.html"
LoginTemplate = "login.tpl.html"
ImprintTemplate = "imprint.tpl.html"
SignupTemplate = "signup.tpl.html"
SettingsTemplate = "settings.tpl.html"
SummaryTemplate = "summary.tpl.html"
IndexTemplate = "index.tpl.html"
LoginTemplate = "login.tpl.html"
ImprintTemplate = "imprint.tpl.html"
SignupTemplate = "signup.tpl.html"
SetPasswordTemplate = "set-password.tpl.html"
ResetPasswordTemplate = "reset-password.tpl.html"
SettingsTemplate = "settings.tpl.html"
SummaryTemplate = "summary.tpl.html"
LeaderboardTemplate = "leaderboard.tpl.html"
)

View File

@ -1,14 +0,0 @@
package config
import "strings"
func cloneStringMap(m map[string]string, keysToLower bool) map[string]string {
m2 := make(map[string]string)
for k, v := range m {
if keysToLower {
k = strings.ToLower(k)
}
m2[k] = v
}
return m2
}

File diff suppressed because it is too large Load Diff

View File

@ -2,290 +2,396 @@
"languages": {
"1C Enterprise": "#814CCC",
"ABAP": "#E8274B",
"ActionScript": "#882B0F",
"Ada": "#02f88c",
"Agda": "#315665",
"AGS Script": "#B9D9FF",
"Alloy": "#64C800",
"AL": "#3AA2B5",
"AMPL": "#E6EFBB",
"AngelScript": "#C7D7DC",
"ANTLR": "#9DC3FF",
"API Blueprint": "#2ACCA8",
"APL": "#5A8164",
"AppleScript": "#101F1F",
"Arc": "#aa2afe",
"ASP": "#6a40fd",
"AspectJ": "#a957b0",
"Assembly": "#6E4C13",
"Asymptote": "#4a0c0c",
"APL": "#8a0707",
"ASP.NET": "#9400ff",
"ATS": "#1ac620",
"ActionScript": "#e3491a",
"Ada": "#02f88c",
"Agda": "#467C91",
"Alloy": "#cc5c24",
"AngelScript": "#C7D7DC",
"Apex": "#1797c0",
"Apollo Guidance Computer": "#0B3D91",
"AppleScript": "#101F1F",
"Arc": "#ca2afe",
"Arduino": "#bd79d1",
"AspectJ": "#1957b0",
"Assembly": "#6E4C13",
"Asymptote": "#ff0000",
"Augeas": "#62331f",
"AutoHotkey": "#6594b9",
"AutoIt": "#1C3552",
"AutoIt": "#36699B",
"Ballerina": "#FF5000",
"Batchfile": "#C1F12E",
"Beef": "#a52f4e",
"Bison": "#6A463F",
"Blade": "#f7523f",
"BlitzMax": "#cd6400",
"Boo": "#d4bec1",
"Boogie": "#c80fa0",
"Brainfuck": "#2F2530",
"Browserslist": "#ffd539",
"C": "#555555",
"C#": "#178600",
"C Sharp": "#178600",
"C#": "#5a25a2",
"C++": "#f34b7d",
"CSON": "#244776",
"CSS": "#563d7c",
"Ceylon": "#dfa535",
"Chapel": "#8dc63f",
"Cirru": "#ccccff",
"Cirru": "#aaaaff",
"Clarion": "#db901e",
"Clean": "#3F85AF",
"Classic ASP": "#6a40fd",
"Clean": "#3a81ad",
"Click": "#E4E6F3",
"Clojure": "#db5855",
"Closure Templates": "#0d948f",
"CoffeeScript": "#244776",
"ColdFusion": "#ed2cd6",
"ColdFusion CFC": "#ed2cd6",
"Common Lisp": "#3fb68b",
"Common Workflow Language": "#B5314C",
"Component Pascal": "#B0CE4E",
"Component Pascal": "#b0ce4e",
"Crystal": "#000100",
"CSS": "#563d7c",
"Cuda": "#3A4E3A",
"D": "#ba595e",
"Dart": "#00B4AB",
"D": "#fcd46d",
"DM": "#075ff1",
"Dafny": "#FFEC25",
"Dart": "#98BAD6",
"DataWeave": "#003a52",
"DM": "#447265",
"Denizen": "#faf094",
"Dhall": "#dfafff",
"Dockerfile": "#384d54",
"Docker": "#384d54",
"Dogescript": "#cca760",
"Dylan": "#6c616e",
"Dylan": "#3ebc27",
"E": "#ccce35",
"eC": "#913960",
"ECL": "#8a1267",
"EJS": "#a91e50",
"EQ": "#a78649",
"Eagle": "#3994bc",
"Eiffel": "#946d57",
"Elixir": "#6e4a7e",
"Elm": "#60B5CC",
"Emacs Lisp": "#c065db",
"EmberScript": "#FFF4F3",
"EQ": "#a78649",
"Erlang": "#B83998",
"EmberScript": "#f64e3e",
"Erlang": "#0faf8d",
"F#": "#b845fc",
"F*": "#572e30",
"FLUX": "#33CCFF",
"FORTRAN": "#4d41b1",
"Factor": "#636746",
"Fancy": "#7b9db4",
"Fantom": "#14253c",
"FLUX": "#88ccff",
"Fantom": "#dbded5",
"Faust": "#c37240",
"Forth": "#341708",
"Fortran": "#4d41b1",
"FreeMarker": "#0050b2",
"Frege": "#00cafe",
"Game Maker Language": "#71b417",
"Futhark": "#5f021f",
"G-code": "#D08CF2",
"GAML": "#FFC766",
"GDScript": "#355570",
"Game Maker Language": "#8ad353",
"Genie": "#fb855d",
"Gherkin": "#5B2063",
"Glyph": "#c1ac7f",
"Glyph": "#e4cc98",
"Gnuplot": "#f0a9f0",
"Go": "#00ADD8",
"Golo": "#88562A",
"Go": "#375eab",
"Golo": "#f6a51f",
"Gosu": "#82937f",
"Grammatical Framework": "#79aa7a",
"Grammatical Framework": "#ff0000",
"GraphQL": "#e10098",
"Groovy": "#e69f56",
"HTML": "#e44b23",
"Hack": "#878787",
"Haml": "#ece2a9",
"Handlebars": "#f7931e",
"Harbour": "#0e60e3",
"Haskell": "#5e5086",
"Haxe": "#df7900",
"Haskell": "#29b544",
"Haxe": "#f7941e",
"HiveQL": "#dce200",
"HTML": "#e34c26",
"Hy": "#7790B2",
"IDL": "#a3522f",
"HolyC": "#ffefaf",
"Hy": "#7891b1",
"IDL": "#e3592c",
"IGOR Pro": "#0000cc",
"Idris": "#b30000",
"ImageJ Macro": "#99AAFF",
"Io": "#a9188d",
"Ioke": "#078193",
"Isabelle": "#FEFE00",
"Isabelle": "#fdcd00",
"J": "#9EEDFF",
"JFlex": "#DBCA00",
"JSONiq": "#40d47e",
"Java": "#b07219",
"JavaScript": "#f1e05a",
"Jolie": "#843179",
"JSONiq": "#40d47e",
"Jsonnet": "#0064bd",
"Julia": "#a270ba",
"Jupyter Notebook": "#DA5B0B",
"KRL": "#f5c800",
"Kaitai Struct": "#773b37",
"Kotlin": "#F18E33",
"KRL": "#28430A",
"Lasso": "#999999",
"Lex": "#DBCA00",
"LFE": "#4C3023",
"LiveScript": "#499886",
"LFE": "#004200",
"LLVM": "#185619",
"LOLCODE": "#cc9900",
"LookML": "#652B81",
"LSL": "#3d9970",
"Lua": "#000080",
"Makefile": "#427819",
"Mask": "#f97732",
"Lark": "#0b130f",
"Lasso": "#2584c3",
"Latte": "#A8FF97",
"Less": "#1d365d",
"Lex": "#DBCA00",
"Liquid": "#67b8de",
"LiveScript": "#499886",
"LookML": "#652B81",
"Lua": "#fa1fa1",
"MATLAB": "#e16737",
"Max": "#c4a79c",
"MAXScript": "#00a6a6",
"mcfunction": "#E22837",
"Mercury": "#ff2b2b",
"MLIR": "#5EC8DB",
"MQL4": "#62A8D6",
"MQL5": "#4A76B8",
"MTML": "#0095d9",
"Macaulay2": "#d8ffff",
"Makefile": "#427819",
"Markdown": "#083fa1",
"Marko": "#42bff2",
"Mask": "#f97732",
"Matlab": "#bb92ac",
"Max": "#ce279c",
"Mercury": "#abcdef",
"Meson": "#007800",
"Metal": "#8f14e9",
"Mirah": "#c7a938",
"Modula-3": "#223388",
"MQL4": "#62A8D6",
"MQL5": "#4A76B8",
"MTML": "#b7e1f4",
"Mustache": "#724b3b",
"NCL": "#28431f",
"NWScript": "#111522",
"Nearley": "#990000",
"Nemerle": "#3d3c6e",
"nesC": "#94B0C7",
"Nemerle": "#0d3c6e",
"NetLinx": "#0aa0ff",
"NetLinx+ERB": "#747faa",
"NetLogo": "#ff6375",
"NewLisp": "#87AED7",
"NetLogo": "#ff2b2b",
"NewLisp": "#eedd66",
"Nextflow": "#3ac486",
"Nim": "#37775b",
"Nit": "#009917",
"Nix": "#7e7eff",
"Nim": "#ffc200",
"Nimrod": "#37775b",
"Nit": "#0d8921",
"Nix": "#7070ff",
"Nu": "#c9df40",
"Objective-C": "#438eff",
"Objective-C++": "#6866fb",
"Objective-J": "#ff0c5a",
"NumPy": "#9C8AF9",
"Nunjucks": "#3d8137",
"OCaml": "#3be133",
"ObjectScript": "#424893",
"Objective-C": "#438eff",
"Objective-C++": "#4886FC",
"Objective-J": "#ff0c5a",
"Odin": "#60AFFE",
"Omgrofl": "#cabbff",
"ooc": "#b0b77e",
"Opal": "#f7ede0",
"Oxygene": "#cdd0e3",
"Oz": "#fab738",
"OpenQASM": "#AA70FF",
"Org": "#77aa99",
"Oxygene": "#5a63a3",
"Oz": "#fcaf3e",
"P4": "#7055b5",
"PAWN": "#dbb284",
"PHP": "#4F5D95",
"PLSQL": "#dad8d8",
"Pan": "#cc0000",
"Papyrus": "#6600cc",
"Parrot": "#f3ca0a",
"Pascal": "#E3F171",
"Pascal": "#b0ce4e",
"Pawn": "#dbb284",
"Pep8": "#C76F5B",
"Perl": "#0298c3",
"Perl 6": "#0000fb",
"PHP": "#4F5D95",
"Perl6": "#0298c3",
"PigLatin": "#fcd7de",
"Pike": "#005390",
"PLSQL": "#dad8d8",
"Pike": "#066ab2",
"PogoScript": "#d80074",
"PostScript": "#da291c",
"PowerBuilder": "#8f0f8d",
"PowerShell": "#012456",
"Processing": "#0096D8",
"Prisma": "#0c344b",
"Processing": "#2779ab",
"Prolog": "#74283c",
"Propeller Spin": "#7fa2a7",
"Puppet": "#302B6D",
"Propeller Spin": "#2b446d",
"Pug": "#a86454",
"Puppet": "#cc5555",
"Pure Data": "#91de79",
"PureBasic": "#5a6986",
"PureScript": "#1D222D",
"Python": "#3572A5",
"q": "#0040cd",
"PureScript": "#bcdc53",
"Python": "#3581ba",
"Q#": "#fed659",
"QML": "#44a51c",
"Qt Script": "#00b841",
"Quake": "#882233",
"R": "#198CE7",
"Racket": "#3c5caa",
"Ragel": "#9d5200",
"R": "#198ce7",
"RAML": "#77d9fb",
"RUNOFF": "#665a4e",
"Racket": "#ae17ff",
"Ragel": "#9d5200",
"Ragel in Ruby Host": "#ff9c2e",
"Raku": "#0000fb",
"Rascal": "#fffaa0",
"ReScript": "#ed5051",
"Reason": "#ff5847",
"Rebol": "#358a5b",
"Red": "#f50000",
"Record Jar": "#0673ba",
"Red": "#ee0000",
"Ren'Py": "#ff7f7f",
"Ring": "#2D54CB",
"Riot": "#A71E49",
"Roff": "#ecdebe",
"Rouge": "#cc0088",
"Ruby": "#701516",
"RUNOFF": "#665a4e",
"Rust": "#dea584",
"SAS": "#1E90FF",
"SCSS": "#c6538c",
"SQF": "#FFCB1F",
"SRecode Template": "#348a34",
"SVG": "#ff9900",
"SaltStack": "#646464",
"SAS": "#B34936",
"Scala": "#c22d40",
"Sass": "#a53b70",
"Scala": "#7dd3b0",
"Scaml": "#bd181a",
"Scheme": "#1e4aec",
"sed": "#64b970",
"Self": "#0579aa",
"Shell": "#89e051",
"Shell": "#5861ce",
"Shen": "#120F14",
"Slash": "#007eff",
"Slice": "#003fa2",
"Slim": "#ff8877",
"SmPL": "#c94949",
"Smalltalk": "#596706",
"Solidity": "#AA6746",
"SourcePawn": "#5c7611",
"SQF": "#3F3F3F",
"SourcePawn": "#f69e1d",
"Squirrel": "#800000",
"SRecode Template": "#348a34",
"Stan": "#b2011d",
"Standard ML": "#dc566d",
"Starlark": "#76d275",
"Stylus": "#ff6347",
"SuperCollider": "#46390b",
"Svelte": "#ff3e00",
"Swift": "#ffac45",
"SystemVerilog": "#DAE1C2",
"Tcl": "#e4cc98",
"Terra": "#00004c",
"TeX": "#3D6117",
"SystemVerilog": "#343761",
"TI Program": "#A0AA87",
"Turing": "#cf142b",
"TypeScript": "#2b7489",
"Tcl": "#e4cc98",
"TeX": "#3D6117",
"Terra": "#00004c",
"Turing": "#45f715",
"Twig": "#c1d026",
"TypeScript": "#31859c",
"Unified Parallel C": "#755223",
"Uno": "#9933cc",
"UnrealScript": "#a54c4d",
"Vala": "#fbe5cd",
"VCL": "#148AA8",
"Verilog": "#b2b7f8",
"VHDL": "#adb2cb",
"V": "#4f87c4",
"VBA": "#867db1",
"VBScript": "#15dcdc",
"VCL": "#0298c3",
"VHDL": "#543978",
"Vala": "#ee7d06",
"Verilog": "#848bf3",
"Vim script": "#199f4b",
"VimL": "#199c4b",
"Visual Basic": "#945db7",
"Volt": "#1F1F1F",
"Visual Basic .NET": "#945db7",
"Volt": "#0098db",
"Vue": "#2c3e50",
"wdl": "#42f1f4",
"Web Ontology Language": "#3994bc",
"WebAssembly": "#04133b",
"wisp": "#7582D1",
"Wollok": "#a23738",
"X10": "#4B6BEF",
"xBase": "#403a40",
"XC": "#99DA07",
"XQuery": "#5232e7",
"XQuery": "#2700e2",
"XSLT": "#EB8CEB",
"Yacc": "#4B6C4B",
"YAML": "#cb171e",
"YARA": "#220000",
"YASnippet": "#32AB90",
"Yacc": "#4B6C4B",
"ZAP": "#0d665e",
"ZIL": "#dc75e5",
"ZenScript": "#00BCD1",
"Zephir": "#118f9e",
"Zig": "#ec915c",
"ZIL": "#dc75e5"
"cpp": "#f34b7d",
"eC": "#913960",
"edn": "#db5855",
"mIRC Script": "#3d57c3",
"mcfunction": "#E22837",
"nesC": "#ffce3b",
"ooc": "#b0b77e",
"q": "#0040cd",
"sed": "#64b970",
"wdl": "#42f1f4",
"wisp": "#7582D1",
"xBase": "#3a4040",
"Other": "#1f9aef"
},
"editors": {
"Adobe XD": "#fd27bc",
"Android Studio": "#99cd00",
"AppCode": "#04dbde",
"Aptana": "#ec8623",
"Atom": "#49b77e",
"Azure Data Studio": "#0271c6",
"Blender": "#fb8007",
"BlueJ": "#5d89af",
"Brackets": "#067dc3",
"Chrome": "#fdd308",
"CLion": "#14c9a5",
"Cloud9": "#25a6d9",
"Coda": "#3e8e1c",
"Code: :Blocks": "#d0ce71",
"Code::Blocks": "#d0ce71",
"CodeLite": "#1892e5",
"CodeTasty": "#7368a8",
"DataGrip": "#907cf2",
"DBeaver": "#897363",
"Eclipse": "#443582",
"Emacs": "#8c76c3",
"Embarcadero Delphi": "#d9242a",
"EmEditor": "#ed3103",
"Eric": "#423f13",
"Excel": "#0f753c",
"Figma": "#c7b9ff",
"Firefox": "#d96527",
"Flash Builder": "#aca3a4",
"Geany": "#fbec75",
"Gedit": "#872114",
"GoLand": "#bd4ffc",
"HBuilder X": "#1ba334",
"IntelliJ IDEA": "#237ce2",
"IntelliJ": "#237ce2",
"IntelliJ IDEA": "#2876e1",
"IntelliJ": "#2876e1",
"Kakoune": "#dd5f4a",
"Kate": "#3f4040",
"KDevelop": "#22a273",
"Komodo": "#fcb414",
"Light Table": "#007ac1",
"MacRabbit Espresso": "#e6593f",
"Micro": "#2c3494",
"MonoDevelop": "#6185b3",
"MySQL Workbench": "#245279",
"Neovim": "#068304",
"NetBeans": "#f1f6e2",
"Notepad++": "#9ecf54",
"Nova": "#ff054a",
"Onivim": "#ee848e",
"Photoshop": "#0a0054",
"PhpStorm": "#d93ac1",
"PowerPoint": "#c6421f",
"Processing": "#6a7152",
"PyCharm": "#d2ee5c",
"Pymakr": "#323d4f",
"QtCreator": "#7fc342",
"Rider": "#f7a415",
"RStudio": "#2369c7",
"RubyMine": "#ff6336",
"Sketch": "#fdad00",
"SlickEdit": "#57ca57",
"Spyder": "#ee181e",
"SQL Server Management Studio": "#ffb901",
"Sublime Text": "#ff9800",
"Terminal": "#133f1c",
@ -296,29 +402,17 @@
"Visual Studio": "#9460cd",
"VS Code": "#027acd",
"VSCode": "#027acd",
"WebMatrix": "#aeaeae",
"WebStorm": "#00c6d7",
"Wing": "#b3b3b3",
"Word": "#0f4091",
"WPS Office": "#fc6143",
"Xamarin": "#3598db",
"Xcode": "#3fa7e4",
"Adobe XD": "#fd27bc",
"Code::Blocks": "#d0ce71",
"Embarcadero Delphi": "#d9242a",
"EmEditor": "#ed3103",
"Figma": "#c7b9ff",
"Firefox": "#d96527",
"Geany": "#fbec75",
"Light Table": "#007ac1",
"MacRabbit Espresso": "#e6593f",
"MySQL Workbench": "#245279",
"Photoshop": "#0a0054",
"QtCreator": "#7fc342",
"RStudio": "#2369c7",
"WebMatrix": "#aeaeae"
"Xcode": "#3fa7e4"
},
"operating_systems": {
"Linux": "#f0b912",
"Windows": "#00b7ee",
"Mac": "#4d66cb"
}
}
}

6
data/data.go Normal file
View File

@ -0,0 +1,6 @@
package data
import _ "embed"
//go:embed colors.json
var ColorsFile []byte

View File

@ -7,10 +7,11 @@ services:
- 3000:3000
restart: always
environment:
# See README.md and config.default.yml for all config options
WAKAPI_DB_TYPE: "postgres"
WAKAPI_DB_NAME: "wakapi"
WAKAPI_DB_USER: "wakapi"
WAKAPI_DB_PASSWORD: "CHANGE_ME!!!"
WAKAPI_DB_PASSWORD: "choose-a-password"
WAKAPI_DB_HOST: "db"
WAKAPI_DB_PORT: "5432"
ENVIRONMENT: "prod"
@ -19,5 +20,10 @@ services:
image: postgres:12.3
environment:
POSTGRES_USER: "wakapi"
POSTGRES_PASSWORD: "CHANGE_ME!!!"
POSTGRES_PASSWORD: "choose-a-password"
POSTGRES_DB: "wakapi"
volumes:
- wakapi-db-data:/var/lib/postgresql/data
volumes:
wakapi-db-data: {}

View File

@ -1,47 +0,0 @@
# Advanced Setup
This page contains instructions for additional setup options, none of which are mandatory.
## Optional: Client-side proxy
Most Wakatime plugins work in a way that, for every heartbeat to send, the plugin calls your local [wakatime-cli](https://github.com/wakatime/wakatime) (a small Python program that is automatically installed when installing a Wakatime plugin) with a few command-line arguments, which is then run as a new process. Inside that process, a heartbeat request is forged and sent to the backend API Wakapi in this case.
While this is convenient for plugin developers, as they do not have to deal with sending HTTP requests, etc., it comes with a minor drawback. Because the CLI process shuts down after each request, its TCP connection is closed as well. Accordingly, **TCP connections cannot be re-used** and every single heartbeat request is inevitably preceded by the `SYN` + `SYN ACK` + `ACK` sequence for establishing a new TCP connection as well as a handshake for establishing a new TLS session.
While this certainly does not hurt, it is still a bit of overhead. You can avoid that by setting up a local reverse proxy on your machine, that keeps running as a daemon and can therefore keep a continuous connection.
### Option 1: [tinyproxy](https://tinyproxy.github.io) forward proxy (`Linux`, `Mac` only)
In this example we use _tinyproxy_ as a small, easy-to-install proxy server, written in C, that runs on your local machine.
1. Install [tinyproxy](https://tinyproxy.github.io)
* Fedora / RHEL: `dnf install tinyproxy`
* Debian / Ubuntu: `apt install tinyproxy`
* MacOS: Install from [MacPorts](https://ports.macports.org/port/tinyproxy/summary)
1. Enable and start it
* Linux: `sudo systemctl start tinyproxy && sudo systemctl enable tinyproxy`
* Mac: Not sure, sorry ¯\_(ツ)_/¯
1. Update `~/.wakatime.cfg`
* Set `proxy = http://localhost:8888`
1. Done
* All Wakapi requests are passed through tinyproxy now, which keeps a TCP connection with the server open for some time
### Option 2: [Caddy](https://caddyserver.com) reverse proxy (`Win`, `Linux`, `Mac`)
In this example, we misuse Caddy, which is a web server and reverse proxy, to fulfil the above scenario.
1. [Install Caddy](https://caddyserver.com/)
* When installing manually, don't forget to set up a systemd service to start Caddy on system startup
1. Create a Caddyfile
```
# /etc/caddy/Caddyfile
http://localhost:8070 {
reverse_proxy * {
to https://wakapi.dev # <-- substitute your own Wakapi host here
header_up Host {http.reverse_proxy.upstream.host}
header_down -Server
}
}
```
1. Restart Caddy
1. Verify that you can access [`http://localhost:8070/api/health`](http://localhost:8070/api/health)
1. Update `~/.wakatime.cfg`
* Set `api_url = http://localhost:8070/api/heartbeat`
1. Done
* All Wakapi requests are passed through Caddy now, which keeps a TCP connection with the server open for some time

View File

@ -1,8 +1,8 @@
#!/bin/bash
if [ "$WAKAPI_DB_TYPE" == "sqlite3" ] || [ "$WAKAPI_DB_TYPE" == "" ]; then
./wakapi
exec ./wakapi
else
echo "Waiting for database to come up"
./wait-for-it.sh "$WAKAPI_DB_HOST:$WAKAPI_DB_PORT" -s -t 60 -- ./wakapi
fi
exec ./wait-for-it.sh "$WAKAPI_DB_HOST:$WAKAPI_DB_PORT" -s -t 60 -- ./wakapi
fi

26
etc/Caddyfile Normal file
View File

@ -0,0 +1,26 @@
wakapi.yourdomain.tld {
encode zstd gzip
header {
Strict-Transport-Security "max-age=2592000; includeSubDomains"
}
log {
output file /var/log/caddy/wakapi.dev.access.log
format single_field common_log
}
reverse_proxy http://[::1]:3000
@api path_regexp "^/api.*"
@notapi not path_regexp "^/api.*"
push @notapi /assets/vendor/source-sans-3.css
push @notapi /assets/css/app.dist.css
push @notapi /assets/vendor/petite-vue.min.js
push @notapi /assets/vendor/chart.min.js
push @notapi /assets/vendor/iconify.basic.min.js
push @notapi /assets/js/icons.dist.js
push @notapi /assets/js/base.js
push @notapi /assets/images/logo.svg
}

53
etc/wakapi.service Normal file
View File

@ -0,0 +1,53 @@
[Unit]
Description=Wakapi
StartLimitIntervalSec=400
StartLimitBurst=3
# Optional, in case you're running MySQL / Postgres with Systemd, too
Requires=mysql.service
After=mysql.service
[Service]
Type=simple
# Assuming Wakapi executable is under /opt/wakapi and config file at /etc
# Feel free to change this
WorkingDirectory=/opt/wakapi
ExecStart=/opt/wakapi/wakapi -config /etc/wakapi.yml
# Environment variables, see README for more
Environment=WAKAPI_DB_HOST=localhost
Environment=WAKAPI_DB_USER=wakapi
Environment=WAKAPI_DB_NAME=wakapi
Environment=WAKAPI_DB_PASSWORD=secretpassword
Environment=WAKAPI_PASSWORD_SALT=somerandomstring
# TODO: Use Systemd's credentials management (https://systemd.io/CREDENTIALS/) introduced in v247 (%d syntax in v250) once more established
# sudo groupadd wakapi
# sudo useradd -g wakapi wakapi
User=wakapi
Group=wakapi
Restart=on-failure
RestartSec=90
# Security hardening
PrivateTmp=true
PrivateUsers=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
PrivateDevices=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ProtectClock=true
RestrictSUIDSGID=true
ProtectHostname=true
ProtectProc=invisible
[Install]
WantedBy=multi-user.target

99
go.mod
View File

@ -1,33 +1,82 @@
module github.com/muety/wakapi
go 1.13
go 1.19
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/emvi/logbuch v1.1.1
github.com/go-co-op/gocron v0.3.3
github.com/go-openapi/spec v0.20.2 // indirect
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.3
github.com/gorilla/schema v1.1.0
codeberg.org/Codeberg/avatars v1.0.0
github.com/duke-git/lancet/v2 v2.1.10
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.15.0
github.com/emvi/logbuch v1.2.0
github.com/getsentry/sentry-go v0.15.0
github.com/glebarez/sqlite v1.5.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.2.0
github.com/gorilla/securecookie v1.1.1
github.com/jinzhu/configor v1.2.0
github.com/mailru/easyjson v0.7.7 // indirect
github.com/markbates/pkger v0.17.1
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/hashstructure/v2 v2.0.1
github.com/hashicorp/golang-lru v0.5.4
github.com/jinzhu/configor v1.2.1
github.com/leandro-lugaresi/hub v1.1.1
github.com/lpar/gzipped/v2 v2.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/rubenv/sql-migrate v0.0.0-20200402132117-435005d389bc
github.com/robfig/cron/v3 v3.0.1
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.6.1
github.com/swaggo/swag v1.7.0
go.uber.org/atomic v1.6.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/tools v0.1.0 // indirect
gorm.io/driver/mysql v1.0.3
gorm.io/driver/postgres v1.0.5
gorm.io/driver/sqlite v1.1.3
gorm.io/gorm v1.20.11
github.com/stretchr/testify v1.8.0
github.com/swaggo/http-swagger v1.3.3
github.com/swaggo/swag v1.8.8
go.uber.org/atomic v1.10.0
golang.org/x/crypto v0.3.0
golang.org/x/sync v0.1.0
gorm.io/driver/mysql v1.4.4
gorm.io/driver/postgres v1.4.5
gorm.io/driver/sqlite v1.4.3
gorm.io/gorm v1.24.2
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/glebarez/go-sqlite v1.19.5 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/tools v0.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.21.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/sqlite v1.20.0 // indirect
)

716
go.sum
View File

@ -1,190 +1,83 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
codeberg.org/Codeberg/avatars v1.0.0 h1:MRx5QxuT/oVCcPvC5rXwgwWKD7hc6J0GnZ0Kl67lYEM=
codeberg.org/Codeberg/avatars v1.0.0/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-co-op/gocron v0.3.3 h1:QnarcMZWWKrEP25uCbtDiLsnnGw+PhCjL3wNITdWJOs=
github.com/go-co-op/gocron v0.3.3/go.mod h1:Y9PWlYqDChf2Nbgg7kfS+ZsXHDTZbMZYPEQ0MILqH+M=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/duke-git/lancet/v2 v2.1.10 h1:q6YKhbYg6KChBS+T41e/IhK+sTDPVk2wRhWLTevCeuY=
github.com/duke-git/lancet/v2 v2.1.10/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emvi/logbuch v1.2.0 h1:Bw0jQH1Dbs+oIygZBNx/2Ub1igXRFtKQrIMRrZdVFJM=
github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrbQHuc=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M=
github.com/glebarez/go-sqlite v1.19.5 h1:krEVjICcImFNi+X81GmEkSe/brhzLL3Csbkb/ihi8sI=
github.com/glebarez/go-sqlite v1.19.5/go.mod h1:IjVxx3ezfL9clKLLSzVgv2sGZe28yIa116YyLTIvp84=
github.com/glebarez/sqlite v1.5.0 h1:+8LAEpmywqresSoGlqjjT+I9m4PseIM3NcerIJ/V7mk=
github.com/glebarez/sqlite v1.5.0/go.mod h1:0wzXzTvfVJIN2GqRhCdMbnYd+m+aH5/QV7B30rM6NgY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/spec v0.19.14 h1:r4fbYFo6N4ZelmSX8G6p+cv/hZRXzcuqQIADGT1iNKM=
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
github.com/go-openapi/spec v0.20.2 h1:pFPUZsiIbZ20kLUcuCGeuQWG735fPMxW7wHF9BWlnQU=
github.com/go-openapi/spec v0.20.2/go.mod h1:RW6Xcbs6LOyWLU/mXGdzn2Qc+3aj+ASfI7rvSZh1Vls=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc=
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog=
github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -192,274 +85,151 @@ github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgO
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.7.0 h1:pwjzcYyfmz/HQOQlENvG1OcDqauTGaqlVahq934F0/U=
github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.5 h1:NUbEWPmCQZbMmYlTjVoNPhc0CfnYyz2bfUAh6A5ZVJM=
github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.5.0 h1:jzBqRk2HFG2CV4AIwgCI2PwTgm6UUoCAK2ofHHRirtc=
github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.9.0 h1:6STjDqppM2ROy5p1wNDcsC7zJTjSHeuCsguZmXyzx7c=
github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/configor v1.2.0 h1:u78Jsrxw2+3sGbGMgpY64ObKU4xWCNmNRJIjGVqxYQA=
github.com/jinzhu/configor v1.2.0/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeKl/aw8=
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lpar/gzipped/v2 v2.1.0 h1:87/ug239roEqXLVOnXZg6NjDfFvMwmkGTKnFWJPUA9U=
github.com/lpar/gzipped/v2 v2.1.0/go.mod h1:G3UlFoFYzjCx6NV4zDmD1BIWMNBaJuKoUvxrEWJuZ3Y=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/hashstructure/v2 v2.0.1 h1:L60q1+q7cXE4JeEJJKMnh2brFIe3rZxCihYAB61ypAY=
github.com/mitchellh/hashstructure/v2 v2.0.1/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf h1:zd7IU9rxVMl2FBwSwiWCUh6s0TkPKgOU6GyVBciNdlo=
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf/go.mod h1:eElbcdMwTDc7Wzl7A46IopgkC6a9nV7jOB6Mw8r0waE=
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e h1:bR8DQ4ZfItytLJwRlrLOPUHd5z18V6tECwYQFy8W+8g=
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY=
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rubenv/sql-migrate v0.0.0-20200402132117-435005d389bc h1:+2DdDcxVYlarHjYcZTt8dZ4Ec8cXZirzL5ko0mkKPjU=
github.com/rubenv/sql-migrate v0.0.0-20200402132117-435005d389bc/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -467,197 +237,159 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0=
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
gorm.io/driver/postgres v1.0.5 h1:raX6ezL/ciUmaYTvOq48jq1GE95aMC0CmxQYbxQ4Ufw=
gorm.io/driver/postgres v1.0.5/go.mod h1:qrD92UurYzNctBMVCJ8C3VQEjffEuphycXtxOudXNCA=
gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.11 h1:jYHQ0LLUViV85V8dM1TP9VBBkfzKTnuTXDjYObkI6yc=
gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.2 h1:9wR6CFD+G8nOusLdvkZelOEhpJVwwHzpQOUM+REd6U0=
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
modernc.org/sqlite v1.19.5/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.14.0/go.mod h1:gQ7c1YPMvryCHCcmf8acB6VPabE59QBeuRQLL7cTUlM=
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=

49
helpers/date.go Normal file
View File

@ -0,0 +1,49 @@
package helpers
import (
"fmt"
"github.com/muety/wakapi/config"
"time"
)
// ParseDateTimeTZ attempts to parse the given date string from multiple formats.
// First, a time-zoned date-time string (e.g. 2006-01-02T15:04:05+02:00) is tried
// Second, a non-time-zoned date-time string (e.g. 2006-01-02 15:04:05) is tried at the given zone
// Third, a non-time-zoned date string (e.g. 2006-01-02) is tried at the given zone
// Example:
// - Server runs in CEST (UTC+2), requesting user lives in PDT (UTC-7).
// - 2021-04-25T10:30:00Z, 2021-04-25T3:30:00-0100 and 2021-04-25T12:30:00+0200 are equivalent, they represent the same point in time
// - When user requests non-time-zoned range (e.g. 2021-04-25T00:00:00), but has their time zone properly configured, this will resolve to 2021-04-25T09:00:00
func ParseDateTimeTZ(date string, tz *time.Location) (time.Time, error) {
if t, err := time.Parse(time.RFC3339, date); err == nil {
return t, nil
}
if t, err := time.ParseInLocation(config.SimpleDateTimeFormat, date, tz); err == nil {
return t, nil
}
return time.ParseInLocation(config.SimpleDateFormat, date, tz)
}
func FormatDate(date time.Time) string {
return date.Format(config.SimpleDateFormat)
}
func FormatDateTime(date time.Time) string {
return date.Format(config.SimpleDateTimeFormat)
}
func FormatDateTimeHuman(date time.Time) string {
return date.Format("Mon, 02 Jan 2006 15:04")
}
func FormatDateHuman(date time.Time) string {
return date.Format("Mon, 02 Jan 2006")
}
func FmtWakatimeDuration(d time.Duration) string {
d = d.Round(time.Minute)
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
return fmt.Sprintf("%d hrs %d mins", h, m)
}

4
helpers/helpers.go Normal file
View File

@ -0,0 +1,4 @@
package helpers
// helpers are different from utils in that they contain wakapi-specific utility functions
// also, helpers may depend on the config package, while utils must be entirely static

30
helpers/http.go Normal file
View File

@ -0,0 +1,30 @@
package helpers
import (
"encoding/json"
"errors"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"net/http"
)
func ExtractCookieAuth(r *http.Request, config *config.Config) (username *string, err error) {
cookie, err := r.Cookie(models.AuthCookieKey)
if err != nil {
return nil, errors.New("missing authentication")
}
if err := config.Security.SecureCookie.Decode(models.AuthCookieKey, cookie.Value, &username); err != nil {
return nil, errors.New("cookie is invalid")
}
return username, nil
}
func RespondJSON(w http.ResponseWriter, r *http.Request, status int, object interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(object); err != nil {
config.Log().Request(r).Error("error while writing json response: %v", err)
}
}

147
helpers/summary.go Normal file
View File

@ -0,0 +1,147 @@
package helpers
import (
"errors"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"net/http"
"time"
)
func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
user := extractUser(r)
params := r.URL.Query()
var err error
var from, to time.Time
if interval := params.Get("interval"); interval != "" {
err, from, to = ResolveIntervalRawTZ(interval, user.TZ())
} else if start := params.Get("start"); start != "" {
err, from, to = ResolveIntervalRawTZ(start, user.TZ())
} else {
from, err = ParseDateTimeTZ(params.Get("from"), user.TZ())
if err != nil {
return nil, errors.New("missing or invalid 'from' parameter")
}
to, err = ParseDateTimeTZ(params.Get("to"), user.TZ())
if err != nil {
return nil, errors.New("missing or invalid 'to' parameter")
}
}
recompute := params.Get("recompute") != "" && params.Get("recompute") != "false"
filters := ParseSummaryFilters(r)
return &models.SummaryParams{
From: from,
To: to,
User: user,
Recompute: recompute,
Filters: filters,
}, nil
}
func ParseSummaryFilters(r *http.Request) *models.Filters {
filters := &models.Filters{}
if q := r.URL.Query().Get("project"); q != "" {
filters.With(models.SummaryProject, q)
}
if q := r.URL.Query().Get("language"); q != "" {
filters.With(models.SummaryLanguage, q)
}
if q := r.URL.Query().Get("editor"); q != "" {
filters.With(models.SummaryEditor, q)
}
if q := r.URL.Query().Get("machine"); q != "" {
filters.With(models.SummaryMachine, q)
}
if q := r.URL.Query().Get("operating_system"); q != "" {
filters.With(models.SummaryOS, q)
}
if q := r.URL.Query().Get("label"); q != "" {
filters.With(models.SummaryLabel, q)
}
if q := r.URL.Query().Get("branch"); q != "" {
filters.With(models.SummaryBranch, q)
}
return filters
}
func extractUser(r *http.Request) *models.User {
type principalGetter interface {
GetPrincipal() *models.User
}
if p := r.Context().Value("principal"); p != nil {
return p.(principalGetter).GetPrincipal()
}
return nil
}
func ParseInterval(interval string) (*models.IntervalKey, error) {
for _, i := range models.AllIntervals {
if i.HasAlias(interval) {
return i, nil
}
}
return nil, errors.New("not a valid interval")
}
func MustResolveIntervalRawTZ(interval string, tz *time.Location) (from, to time.Time) {
_, from, to = ResolveIntervalRawTZ(interval, tz)
return from, to
}
func ResolveIntervalRawTZ(interval string, tz *time.Location) (err error, from, to time.Time) {
parsed, err := ParseInterval(interval)
if err != nil {
return err, time.Time{}, time.Time{}
}
return ResolveIntervalTZ(parsed, tz)
}
func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location) (err error, from, to time.Time) {
now := time.Now().In(tz)
to = now
switch interval {
case models.IntervalToday:
from = utils.BeginOfToday(tz)
case models.IntervalYesterday:
from = utils.BeginOfToday(tz).Add(-24 * time.Hour)
to = utils.BeginOfToday(tz)
case models.IntervalThisWeek:
from = utils.BeginOfThisWeek(tz)
case models.IntervalLastWeek:
from = utils.BeginOfThisWeek(tz).AddDate(0, 0, -7)
to = utils.BeginOfThisWeek(tz)
case models.IntervalThisMonth:
from = utils.BeginOfThisMonth(tz)
case models.IntervalLastMonth:
from = utils.BeginOfThisMonth(tz).AddDate(0, -1, 0)
to = utils.BeginOfThisMonth(tz)
case models.IntervalThisYear:
from = utils.BeginOfThisYear(tz)
case models.IntervalPast7Days:
from = now.AddDate(0, 0, -7)
case models.IntervalPast7DaysYesterday:
from = utils.BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
to = utils.BeginOfToday(tz).AddDate(0, 0, -1)
case models.IntervalPast14Days:
from = now.AddDate(0, 0, -14)
case models.IntervalPast30Days:
from = now.AddDate(0, 0, -30)
case models.IntervalPast6Months:
from = now.AddDate(0, -6, 0)
case models.IntervalPast12Months:
from = now.AddDate(0, -12, 0)
case models.IntervalAny:
from = time.Time{}
default:
err = errors.New("invalid interval")
}
return err, from, to
}

220
main.go
View File

@ -1,35 +1,54 @@
package main
//go:generate $GOPATH/bin/pkger
import (
"github.com/emvi/logbuch"
"github.com/gorilla/handlers"
"github.com/markbates/pkger"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/migrations"
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/routes/api"
"github.com/muety/wakapi/utils"
"gorm.io/gorm/logger"
"embed"
"github.com/muety/wakapi/static/docs"
"io/fs"
"log"
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/emvi/logbuch"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/lpar/gzipped/v2"
"github.com/swaggo/http-swagger"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/migrations"
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/routes"
"github.com/muety/wakapi/routes/api"
shieldsV1Routes "github.com/muety/wakapi/routes/compat/shields/v1"
wtV1Routes "github.com/muety/wakapi/routes/compat/wakatime/v1"
"github.com/muety/wakapi/routes/relay"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/services/mail"
fsutils "github.com/muety/wakapi/utils/fs"
_ "gorm.io/driver/mysql"
_ "gorm.io/driver/postgres"
_ "gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
_ "github.com/muety/wakapi/static/docs"
)
// Embed version.txt
//
//go:embed version.txt
var version string
// Embed static files
//
//go:embed static
var staticFiles embed.FS
var (
db *gorm.DB
config *conf.Config
@ -40,8 +59,12 @@ var (
heartbeatRepository repositories.IHeartbeatRepository
userRepository repositories.IUserRepository
languageMappingRepository repositories.ILanguageMappingRepository
projectLabelRepository repositories.IProjectLabelRepository
summaryRepository repositories.ISummaryRepository
leaderboardRepository *repositories.LeaderboardRepository
keyValueRepository repositories.IKeyValueRepository
diagnosticsRepository repositories.IDiagnosticsRepository
metricsRepository *repositories.MetricsRepository
)
var (
@ -49,9 +72,16 @@ var (
heartbeatService services.IHeartbeatService
userService services.IUserService
languageMappingService services.ILanguageMappingService
projectLabelService services.IProjectLabelService
durationService services.IDurationService
summaryService services.ISummaryService
leaderboardService services.ILeaderboardService
aggregationService services.IAggregationService
mailService services.IMailService
keyValueService services.IKeyValueService
reportService services.IReportService
diagnosticsService services.IDiagnosticsService
housekeepingService services.IHousekeepingService
miscService services.IMiscService
)
@ -76,9 +106,11 @@ var (
// @in header
// @name Authorization
// @BasePath /api
func main() {
config = conf.Load()
config = conf.Load(version)
// Configure Swagger docs
docs.SwaggerInfo.BasePath = config.Server.BasePath + "/api"
// Set log level
if config.IsDev() {
@ -100,14 +132,18 @@ func main() {
// Connect to database
var err error
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger})
if config.Db.Dialect == "sqlite3" {
db.Raw("PRAGMA foreign_keys = ON;")
if err != nil {
logbuch.Error(err.Error())
logbuch.Fatal("could not open database")
}
if config.Db.IsSQLite() {
db.Exec("PRAGMA foreign_keys = ON;")
}
if config.IsDev() {
db = db.Debug()
}
sqlDb, _ := db.DB()
sqlDb, err := db.DB()
sqlDb.SetMaxIdleConns(int(config.Db.MaxConn))
sqlDb.SetMaxOpenConns(int(config.Db.MaxConn))
if err != nil {
@ -117,30 +153,44 @@ func main() {
defer sqlDb.Close()
// Migrate database schema
migrations.RunPreMigrations(db, config)
runDatabaseMigrations()
migrations.RunCustomPostMigrations(db, config)
if !config.SkipMigrations {
migrations.Run(db, config)
}
// Repositories
aliasRepository = repositories.NewAliasRepository(db)
heartbeatRepository = repositories.NewHeartbeatRepository(db)
userRepository = repositories.NewUserRepository(db)
languageMappingRepository = repositories.NewLanguageMappingRepository(db)
projectLabelRepository = repositories.NewProjectLabelRepository(db)
summaryRepository = repositories.NewSummaryRepository(db)
leaderboardRepository = repositories.NewLeaderboardRepository(db)
keyValueRepository = repositories.NewKeyValueRepository(db)
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
metricsRepository = repositories.NewMetricsRepository(db)
// Services
mailService = mail.NewMailService()
aliasService = services.NewAliasService(aliasRepository)
userService = services.NewUserService(userRepository)
userService = services.NewUserService(mailService, userRepository)
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
durationService = services.NewDurationService(heartbeatService)
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
leaderboardService = services.NewLeaderboardService(leaderboardRepository, summaryService, userService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
keyValueService = services.NewKeyValueService(keyValueRepository)
reportService = services.NewReportService(summaryService, userService, mailService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService)
miscService = services.NewMiscService(userService, summaryService, keyValueService)
// Schedule background tasks
go aggregationService.Schedule()
go leaderboardService.Schedule()
go reportService.Schedule()
go housekeepingService.Schedule()
go miscService.ScheduleCountTotalTime()
routes.Init()
@ -149,67 +199,103 @@ func main() {
healthApiHandler := api.NewHealthApiHandler(db)
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService)
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService, metricsRepository)
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
avatarHandler := api.NewAvatarHandler()
badgeHandler := api.NewBadgeHandler(userService, summaryService)
// Compat Handlers
wakatimeV1StatusBarHandler := wtV1Routes.NewStatusBarHandler(userService, summaryService)
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(userService, summaryService)
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(userService, summaryService)
wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(userService, summaryService)
wakatimeV1UsersHandler := wtV1Routes.NewUsersHandler(userService, heartbeatService)
wakatimeV1ProjectsHandler := wtV1Routes.NewProjectsHandler(userService, heartbeatService)
wakatimeV1HeartbeatsHandler := wtV1Routes.NewHeartbeatHandler(userService, heartbeatService)
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
// MVC Handlers
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
leaderboardHandler := routes.NewLeaderboardHandler(userService, leaderboardService)
homeHandler := routes.NewHomeHandler(keyValueService)
loginHandler := routes.NewLoginHandler(userService)
loginHandler := routes.NewLoginHandler(userService, mailService)
imprintHandler := routes.NewImprintHandler(keyValueService)
// Other Handlers
relayHandler := relay.NewRelayHandler()
// Setup Routers
router := mux.NewRouter()
rootRouter := router.PathPrefix("/").Subrouter()
apiRouter := router.PathPrefix("/api").Subrouter()
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true)
// https://github.com/gorilla/mux/issues/416
router.NotFoundHandler = router.NewRoute().BuildOnly().HandlerFunc(http.NotFound).GetHandler()
router.NotFoundHandler = middlewares.NewLoggingMiddleware(logbuch.Info, []string{
"/assets",
"/favicon",
"/service-worker.js",
})(router.NotFoundHandler)
// Globally used middlewares
recoveryMiddleware := handlers.RecoveryHandler()
loggingMiddleware := middlewares.NewLoggingMiddleware(
log.New(os.Stdout, "", log.LstdFlags),
[]string{"/assets"},
)
// Router configs
router.Use(loggingMiddleware, recoveryMiddleware)
router.Use(middlewares.NewPrincipalMiddleware())
router.Use(middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets", "/api/health"}))
router.Use(handlers.RecoveryHandler())
if config.Sentry.Dsn != "" {
router.Use(middlewares.NewSentryMiddleware())
}
rootRouter.Use(middlewares.NewSecurityMiddleware())
// Route registrations
homeHandler.RegisterRoutes(rootRouter)
loginHandler.RegisterRoutes(rootRouter)
imprintHandler.RegisterRoutes(rootRouter)
summaryHandler.RegisterRoutes(rootRouter)
leaderboardHandler.RegisterRoutes(rootRouter)
settingsHandler.RegisterRoutes(rootRouter)
relayHandler.RegisterRoutes(rootRouter)
// API route registrations
summaryApiHandler.RegisterRoutes(apiRouter)
healthApiHandler.RegisterRoutes(apiRouter)
heartbeatApiHandler.RegisterRoutes(apiRouter)
metricsHandler.RegisterRoutes(apiRouter)
diagnosticsHandler.RegisterRoutes(apiRouter)
avatarHandler.RegisterRoutes(apiRouter)
badgeHandler.RegisterRoutes(apiRouter)
wakatimeV1StatusBarHandler.RegisterRoutes(apiRouter)
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
wakatimeV1StatsHandler.RegisterRoutes(apiRouter)
wakatimeV1UsersHandler.RegisterRoutes(apiRouter)
wakatimeV1ProjectsHandler.RegisterRoutes(apiRouter)
wakatimeV1HeartbeatsHandler.RegisterRoutes(apiRouter)
shieldV1BadgeHandler.RegisterRoutes(apiRouter)
// Static Routes
fileServer := http.FileServer(utils.NeuteredFileSystem{Fs: pkger.Dir("/static")})
router.PathPrefix("/assets").Handler(fileServer)
router.PathPrefix("/swagger-ui").Handler(fileServer)
router.PathPrefix("/docs").Handler(
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(fileServer),
)
// https://github.com/golang/go/issues/43431
embeddedStatic, _ := fs.Sub(staticFiles, "static")
static := conf.ChooseFS("static", embeddedStatic)
assetsFileServer := gzipped.FileServer(fsutils.NewExistsHttpFS(
fsutils.NewExistsFS(static).WithCache(!config.IsDev()),
))
staticFileServer := http.FileServer(http.FS(
fsutils.NeuteredFileSystem{FS: static},
))
router.PathPrefix("/contribute.json").Handler(staticFileServer)
router.PathPrefix("/assets").Handler(assetsFileServer)
router.Path("/swagger-ui").Handler(http.RedirectHandler("swagger-ui/", http.StatusMovedPermanently)) // https://github.com/swaggo/http-swagger/issues/44
router.PathPrefix("/swagger-ui").Handler(httpSwagger.WrapHandler)
// Listen HTTP
listen(router)
}
func listen(handler http.Handler) {
var s4, s6 *http.Server
var s4, s6, sSocket *http.Server
// IPv4
if config.Server.ListenIpV4 != "" {
@ -217,8 +303,8 @@ func listen(handler http.Handler) {
s4 = &http.Server{
Handler: handler,
Addr: bindString4,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
}
}
@ -228,8 +314,24 @@ func listen(handler http.Handler) {
s6 = &http.Server{
Handler: handler,
Addr: bindString6,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
}
}
// UNIX domain socket
if config.Server.ListenSocket != "" {
// Remove if exists
if _, err := os.Stat(config.Server.ListenSocket); err == nil {
logbuch.Info("--> Removing unix socket %s", config.Server.ListenSocket)
if err := os.Remove(config.Server.ListenSocket); err != nil {
logbuch.Fatal(err.Error())
}
}
sSocket = &http.Server{
Handler: handler,
ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
}
}
@ -250,6 +352,18 @@ func listen(handler http.Handler) {
}
}()
}
if sSocket != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", config.Server.ListenSocket)
go func() {
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
if err != nil {
logbuch.Fatal(err.Error())
}
if err := sSocket.ServeTLS(unixListener, config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
}
}()
}
} else {
if s4 != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", s4.Addr)
@ -267,13 +381,19 @@ func listen(handler http.Handler) {
}
}()
}
if sSocket != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", config.Server.ListenSocket)
go func() {
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
if err != nil {
logbuch.Fatal(err.Error())
}
if err := sSocket.Serve(unixListener); err != nil {
logbuch.Fatal(err.Error())
}
}()
}
}
<-make(chan interface{}, 1)
}
func runDatabaseMigrations() {
if err := config.GetMigrationFunc(config.Db.Dialect)(db); err != nil {
logbuch.Fatal(err.Error())
}
}

View File

@ -1,13 +1,24 @@
package middlewares
import (
"context"
"fmt"
"github.com/muety/wakapi/helpers"
"net/http"
"strings"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
"strings"
)
const (
// queryApiKey is the query parameter name for api key.
queryApiKey = "api_key"
)
var (
errEmptyKey = fmt.Errorf("the api_key is empty")
)
type AuthenticateMiddleware struct {
@ -46,7 +57,10 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
user, err := m.tryGetUserByCookie(r)
if err != nil {
user, err = m.tryGetUserByApiKey(r)
user, err = m.tryGetUserByApiKeyHeader(r)
}
if err != nil {
user, err = m.tryGetUserByApiKeyQuery(r)
}
if err != nil || user == nil {
@ -59,14 +73,14 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(conf.ErrUnauthorized))
} else {
http.SetCookie(w, m.config.GetClearCookie(models.AuthCookieKey, "/"))
http.SetCookie(w, m.config.GetClearCookie(models.AuthCookieKey))
http.Redirect(w, r, m.redirectTarget, http.StatusFound)
}
return
}
ctx := context.WithValue(r.Context(), models.UserKey, user)
next(w, r.WithContext(ctx))
SetPrincipal(r, user)
next(w, r)
}
func (m *AuthenticateMiddleware) isOptional(requestPath string) bool {
@ -78,7 +92,7 @@ func (m *AuthenticateMiddleware) isOptional(requestPath string) bool {
return false
}
func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.User, error) {
func (m *AuthenticateMiddleware) tryGetUserByApiKeyHeader(r *http.Request) (*models.User, error) {
key, err := utils.ExtractBearerAuth(r)
if err != nil {
return nil, err
@ -93,8 +107,22 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.Us
return user, nil
}
func (m *AuthenticateMiddleware) tryGetUserByApiKeyQuery(r *http.Request) (*models.User, error) {
key := r.URL.Query().Get(queryApiKey)
var user *models.User
userKey := strings.TrimSpace(key)
if userKey == "" {
return nil, errEmptyKey
}
user, err := m.userSrvc.GetUserByKey(userKey)
if err != nil {
return nil, err
}
return user, nil
}
func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.User, error) {
username, err := utils.ExtractCookieAuth(r, m.config)
username, err := helpers.ExtractCookieAuth(r, m.config)
if err != nil {
return nil, err
}

View File

@ -3,14 +3,16 @@ package middlewares
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/muety/wakapi/mocks"
"github.com/muety/wakapi/models"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
func TestAuthenticateMiddleware_tryGetUserByApiKey_Success(t *testing.T) {
func TestAuthenticateMiddleware_tryGetUserByApiKeyHeader_Success(t *testing.T) {
testApiKey := "z5uig69cn9ut93n"
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
testUser := &models.User{ApiKey: testApiKey}
@ -26,13 +28,13 @@ func TestAuthenticateMiddleware_tryGetUserByApiKey_Success(t *testing.T) {
sut := NewAuthenticateMiddleware(userServiceMock)
result, err := sut.tryGetUserByApiKey(mockRequest)
result, err := sut.tryGetUserByApiKeyHeader(mockRequest)
assert.Nil(t, err)
assert.Equal(t, testUser, result)
}
func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) {
func TestAuthenticateMiddleware_tryGetUserByApiKeyHeader_Invalid(t *testing.T) {
testApiKey := "z5uig69cn9ut93n"
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
@ -47,10 +49,55 @@ func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) {
sut := NewAuthenticateMiddleware(userServiceMock)
result, err := sut.tryGetUserByApiKey(mockRequest)
result, err := sut.tryGetUserByApiKeyHeader(mockRequest)
assert.Error(t, err)
assert.Nil(t, result)
}
func TestAuthenticateMiddleware_tryGetUserByApiKeyQuery_Success(t *testing.T) {
testApiKey := "z5uig69cn9ut93n"
testUser := &models.User{ApiKey: testApiKey}
params := url.Values{}
params.Add("api_key", testApiKey)
mockRequest := &http.Request{
URL: &url.URL{
RawQuery: params.Encode(),
},
}
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserByKey", testApiKey).Return(testUser, nil)
sut := NewAuthenticateMiddleware(userServiceMock)
result, err := sut.tryGetUserByApiKeyQuery(mockRequest)
assert.Nil(t, err)
assert.Equal(t, testUser, result)
}
func TestAuthenticateMiddleware_tryGetUserByApiKeyQuery_Invalid(t *testing.T) {
testApiKey := "z5uig69cn9ut93n"
params := url.Values{}
params.Add("token", testApiKey)
mockRequest := &http.Request{
URL: &url.URL{
RawQuery: params.Encode(),
},
}
userServiceMock := new(mocks.UserServiceMock)
sut := NewAuthenticateMiddleware(userServiceMock)
result, actualErr := sut.tryGetUserByApiKeyQuery(mockRequest)
assert.Error(t, actualErr)
assert.Equal(t, errEmptyKey, actualErr)
assert.Nil(t, result)
}
// TODO: somehow test cookie auth function

View File

@ -3,19 +3,30 @@ package relay
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/emvi/logbuch"
"github.com/leandro-lugaresi/hub"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/patrickmn/go-cache"
"io"
"io/ioutil"
"net/http"
"time"
)
/* Middleware to conditionally relay heartbeats to Wakatime */
const maxFailuresPerDay = 100
// WakatimeRelayMiddleware is a middleware to conditionally relay heartbeats to Wakatime (and other compatible services)
type WakatimeRelayMiddleware struct {
httpClient *http.Client
httpClient *http.Client
hashCache *cache.Cache
failureCache *cache.Cache
eventBus *hub.Hub
}
func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware {
@ -23,6 +34,9 @@ func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware {
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
hashCache: cache.New(10*time.Minute, 10*time.Minute),
failureCache: cache.New(24*time.Hour, 1*time.Hour),
eventBus: config.EventBus(),
}
}
@ -35,19 +49,34 @@ func (m *WakatimeRelayMiddleware) Handler(h http.Handler) http.Handler {
func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer next(w, r)
if r.Method != http.MethodPost {
ownInstanceId := config.Get().InstanceId
originInstanceId := r.Header.Get("X-Origin-Instance")
if r.Method != http.MethodPost || originInstanceId == ownInstanceId {
return
}
user := r.Context().Value(models.UserKey).(*models.User)
user := middlewares.GetPrincipal(r)
if user == nil || user.WakatimeApiKey == "" {
return
}
err := m.filterByCache(r)
if err != nil {
logbuch.Warn("%v", err)
return
}
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
// prevent cycles
downstreamInstanceId := ownInstanceId
if originInstanceId != "" {
downstreamInstanceId = originInstanceId
}
headers := http.Header{
"X-Machine-Name": r.Header.Values("X-Machine-Name"),
"Content-Type": r.Header.Values("Content-Type"),
@ -56,23 +85,27 @@ func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
"X-Origin": []string{
fmt.Sprintf("wakapi v%s", config.Get().Version),
},
"X-Origin-Instance": []string{downstreamInstanceId},
"Authorization": []string{
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user.WakatimeApiKey))),
},
}
url := user.WakaTimeURL(config.WakatimeApiUrl) + config.WakatimeApiHeartbeatsBulkUrl
go m.send(
http.MethodPost,
config.WakatimeApiUrl+config.WakatimeApiHeartbeatsBulkUrl,
url,
bytes.NewReader(body),
headers,
user,
)
}
func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, headers http.Header) {
func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, headers http.Header, forUser *models.User) {
request, err := http.NewRequest(method, url, body)
if err != nil {
logbuch.Warn("error constructing relayed request %v", err)
logbuch.Warn("error constructing relayed request - %v", err)
return
}
@ -84,11 +117,74 @@ func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, heade
response, err := m.httpClient.Do(request)
if err != nil {
logbuch.Warn("error executing relayed request %v", err)
logbuch.Warn("error executing relayed request - %v", err)
return
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
logbuch.Warn("failed to relay request, got status %d", response.StatusCode)
logbuch.Warn("failed to relay request for user %s, got status %d", forUser.ID, response.StatusCode)
// TODO: use leaky bucket instead of expiring cache?
if _, found := m.failureCache.Get(forUser.ID); !found {
m.failureCache.SetDefault(forUser.ID, 0)
}
if n, _ := m.failureCache.IncrementInt(forUser.ID, 1); n == maxFailuresPerDay {
m.eventBus.Publish(hub.Message{
Name: config.EventWakatimeFailure,
Fields: map[string]interface{}{config.FieldUser: forUser, config.FieldPayload: n},
})
} else if n%10 == 0 {
logbuch.Warn("%d / %d failed wakatime heartbeat relaying attempts for user %s within last 24 hours", n, maxFailuresPerDay, forUser.ID)
}
}
}
// filterByCache takes an HTTP request, tries to parse the body contents as heartbeats, checks against a local cache for whether a heartbeat has already been relayed before according to its hash and in-place filters these from the request's raw json body.
// This method operates on the raw body data (interface{}), because serialization of models.Heartbeat is not necessarily identical to what the CLI has actually sent.
// Purpose of this mechanism is mainly to prevent cyclic relays / loops.
// Caution: this method does in-place changes to the request.
func (m *WakatimeRelayMiddleware) filterByCache(r *http.Request) error {
heartbeats, err := routeutils.ParseHeartbeats(r)
if err != nil {
return err
}
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
var rawData interface{}
if err := json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body))).Decode(&rawData); err != nil {
return err
}
newData := make([]interface{}, 0, len(heartbeats))
for i, hb := range heartbeats {
hb = hb.Hashed()
// we didn't see this particular heartbeat before
if _, found := m.hashCache.Get(hb.Hash); !found {
m.hashCache.SetDefault(hb.Hash, true)
newData = append(newData, rawData.([]interface{})[i])
continue
}
}
if len(newData) == 0 {
return errors.New("no new heartbeats to relay")
}
if len(newData) != len(heartbeats) {
user := middlewares.GetPrincipal(r)
logbuch.Warn("only relaying %d of %d heartbeats for user %s", len(newData), len(heartbeats), user.ID)
}
buf := bytes.Buffer{}
if err := json.NewEncoder(&buf).Encode(newData); err != nil {
return err
}
r.Body = ioutil.NopCloser(&buf)
return nil
}

View File

@ -4,23 +4,24 @@ package middlewares
import (
"io"
"log"
"net/http"
"strings"
"time"
)
type logFunc func(string, ...interface{})
type LoggingMiddleware struct {
handler http.Handler
output *log.Logger
logFunc logFunc
excludePrefixes []string
}
func NewLoggingMiddleware(output *log.Logger, excludePrefixes []string) func(http.Handler) http.Handler {
func NewLoggingMiddleware(logFunc logFunc, excludePrefixes []string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &LoggingMiddleware{
handler: h,
output: output,
logFunc: logFunc,
excludePrefixes: excludePrefixes,
}
}
@ -41,15 +42,15 @@ func (lg *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
lg.output.Printf(
"%v status=%d, method=%s, uri=%s, duration=%v, bytes=%d, addr=%s\n",
time.Now().Format(time.RFC3339Nano),
lg.logFunc(
"[request] status=%d, method=%s, uri=%s, duration=%v, bytes=%d, addr=%s, user=%s",
ww.Status(),
r.Method,
r.URL.String(),
duration,
ww.BytesWritten(),
readUserIP(r),
readUserID(r),
)
}
@ -64,6 +65,13 @@ func readUserIP(r *http.Request) string {
return ip
}
func readUserID(r *http.Request) string {
if user := GetPrincipal(r); user != nil {
return user.ID
}
return "-"
}
// The below writer-wrapping code has been lifted from
// https://github.com/zenazn/goji/blob/master/web/middleware/logger.go - because
// it does exactly what is needed, and it's unlikely to change in any

64
middlewares/principal.go Normal file
View File

@ -0,0 +1,64 @@
package middlewares
import (
"context"
"github.com/muety/wakapi/models"
"net/http"
)
const keyPrincipal = "principal"
type PrincipalContainer struct {
principal *models.User
}
func (c *PrincipalContainer) SetPrincipal(user *models.User) {
c.principal = user
}
func (c *PrincipalContainer) GetPrincipal() *models.User {
return c.principal
}
// This middleware is a bit of a dirty workaround to the fact that a http.Request's context
// does not allow to pass values from an inner to an outer middleware. Calling WithContext() on a
// request shallow-copies the whole request itself and therefore, in a chain of handler1(handler2()),
// handler 1 will not have access to values handler 2 writes to its context. In addition, Context.WithValue
// returns a new context with the old context as a parent.
//
// As a concrete example, SentryMiddleware as well as LoggingMiddleware should be quite the outer layers,
// while AuthenticationMiddleware is on the very inside of the chain. However, we still want sentry or the
// logger to have access to the user object populated by the auth. middleware, if present.
//
// This middleware shall be included as the outermost layers and it injects a stateful container that does
// nothing but conditionally hold a reference to an authenticated user object.
//
// Other reference: https://stackoverflow.com/questions/55972869/send-errors-to-sentry-with-golang-and-mux
type PrincipalMiddleware struct {
handler http.Handler
}
func NewPrincipalMiddleware() func(handler http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &PrincipalMiddleware{handler: h}
}
}
func (p *PrincipalMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), keyPrincipal, &PrincipalContainer{})
p.handler.ServeHTTP(w, r.WithContext(ctx))
}
func SetPrincipal(r *http.Request, user *models.User) {
if p := r.Context().Value(keyPrincipal); p != nil {
p.(*PrincipalContainer).SetPrincipal(user)
}
}
func GetPrincipal(r *http.Request) *models.User {
if p := r.Context().Value(keyPrincipal); p != nil {
return p.(*PrincipalContainer).GetPrincipal()
}
return nil
}

32
middlewares/security.go Normal file
View File

@ -0,0 +1,32 @@
package middlewares
import (
"net/http"
)
var securityHeaders = map[string]string{
"Cross-Origin-Opener-Policy": "same-origin",
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:; form-action 'self'; block-all-mixed-content;",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
}
// SecurityMiddleware is a handler to add some basic security headers to responses
type SecurityMiddleware struct {
handler http.Handler
}
func NewSecurityMiddleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &SecurityMiddleware{h}
}
}
func (f *SecurityMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for k, v := range securityHeaders {
if w.Header().Get(k) == "" {
w.Header().Set(k, v)
}
}
f.handler.ServeHTTP(w, r)
}

31
middlewares/sentry.go Normal file
View File

@ -0,0 +1,31 @@
package middlewares
import (
"context"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"net/http"
)
// SentryMiddleware is a wrapper around sentryhttp to include user information to traces
type SentryMiddleware struct {
handler http.Handler
}
func NewSentryMiddleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return sentryhttp.New(sentryhttp.Options{
Repanic: true,
}).Handle(&SentryMiddleware{handler: h})
}
}
func (h *SentryMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "-", "-")
h.handler.ServeHTTP(w, r.WithContext(ctx))
if hub := sentry.GetHubFromContext(ctx); hub != nil {
if user := GetPrincipal(r); user != nil {
hub.Scope().SetUser(sentry.User{ID: user.ID})
}
}
}

View File

@ -1,17 +0,0 @@
package migrations
import (
"github.com/muety/wakapi/config"
"gorm.io/gorm"
)
func init() {
f := migrationFunc{
name: "000-apply_fixtures",
f: func(db *gorm.DB, cfg *config.Config) error {
return cfg.GetFixturesFunc(cfg.Db.Dialect)(db)
},
}
registerPostMigration(f)
}

View File

@ -31,13 +31,7 @@ func init() {
return nil
}
condition := "key = ?"
if cfg.Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?"
}
lookupResult := db.Where(condition, name).First(&models.KeyStringValue{})
if lookupResult.Error == nil && lookupResult.RowsAffected > 0 {
logbuch.Info("no need to migrate '%s'", name)
if hasRun(name, db) {
return nil
}
@ -64,13 +58,7 @@ func init() {
}
}
if err := db.Create(&models.KeyStringValue{
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}

View File

@ -26,13 +26,7 @@ func init() {
return nil
}
condition := "key = ?"
if cfg.Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?"
}
lookupResult := db.Where(condition, name).First(&models.KeyStringValue{})
if lookupResult.Error == nil && lookupResult.RowsAffected > 0 {
logbuch.Info("no need to migrate '%s'", name)
if hasRun(name, db) {
return nil
}
@ -43,13 +37,7 @@ func init() {
}
}
if err := db.Create(&models.KeyStringValue{
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}

View File

@ -37,11 +37,15 @@ func init() {
return err
}
if cfg.Db.Dialect == config.SQLDialectSqlite {
logbuch.Info("not attempting to drop column 'badges_enabled' on sqlite")
return nil
}
if err := migrator.DropColumn(&models.User{}, "badges_enabled"); err != nil {
return err
} else {
logbuch.Info("dropped column 'badges_enabled' after substituting it by sharing indicators")
}
logbuch.Info("dropped column 'badges_enabled' after substituting it by sharing indicators")
return nil
},

View File

@ -1,9 +1,7 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
@ -12,13 +10,7 @@ func init() {
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
condition := "key = ?"
if cfg.Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?"
}
lookupResult := db.Where(condition, name).First(&models.KeyStringValue{})
if lookupResult.Error == nil && lookupResult.RowsAffected > 0 {
logbuch.Info("no need to migrate '%s'", name)
if hasRun(name, db) {
return nil
}
@ -26,13 +18,7 @@ func init() {
return err
}
if err := db.Create(&models.KeyStringValue{
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}

View File

@ -0,0 +1,27 @@
package migrations
import (
"github.com/muety/wakapi/config"
"gorm.io/gorm"
)
func init() {
const name = "20210221-add_created_date_column"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
if err := db.Exec("UPDATE heartbeats SET created_at = time WHERE TRUE").Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,39 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func init() {
const name = "20210411-add_imprint_content"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
condition := "key = ?"
if cfg.Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?"
}
imprintKv := &models.KeyStringValue{Key: "imprint", Value: "no content here"}
if err := db.
Clauses(clause.OnConflict{UpdateAll: false, DoNothing: true}).
Where(condition, imprintKv.Key).
Assign(imprintKv).
Create(imprintKv).Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,22 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"gorm.io/gorm"
)
func init() {
const name = "20210411-drop_migrations_table"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if err := db.Migrator().DropTable("gorp_migrations"); err == nil {
logbuch.Info("dropped table 'gorp_migrations'")
}
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,37 @@
package migrations
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20210806-remove_persisted_project_labels"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
rawDb, err := db.DB()
if err != nil {
logbuch.Error("failed to retrieve raw sql db instance")
return err
}
if _, err := rawDb.Exec(fmt.Sprintf("delete from summary_items where type = %d", models.SummaryLabel)); err != nil {
logbuch.Error("failed to delete project label summary items")
return err
}
logbuch.Info("successfully deleted project label summary items")
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,52 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"gorm.io/gorm"
)
func init() {
const name = "20211215-migrate_id_to_bigint-add_has_data_field"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
logbuch.Info("this may take a while!")
if cfg.Db.IsMySQL() {
tx := db.Begin()
if err := tx.Exec("ALTER TABLE heartbeats MODIFY COLUMN id BIGINT UNSIGNED AUTO_INCREMENT").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE summary_items MODIFY COLUMN id BIGINT UNSIGNED AUTO_INCREMENT").Error; err != nil {
return err
}
tx.Commit()
} else if cfg.Db.IsPostgres() {
// postgres does not have unsigned data types
// https://www.postgresql.org/docs/10/datatype-numeric.html
tx := db.Begin()
if err := tx.Exec("ALTER TABLE heartbeats ALTER COLUMN id TYPE BIGINT").Error; err != nil {
return err
}
if err := tx.Exec("ALTER TABLE summary_items ALTER COLUMN id TYPE BIGINT").Error; err != nil {
return err
}
tx.Commit()
} else {
// sqlite doesn't allow for changing column type easily
// https://stackoverflow.com/a/2083562/3112139
logbuch.Warn("unable to migrate id columns to bigint on %s", cfg.Db.Dialect)
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,48 @@
package migrations
import (
"database/sql"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20212212-total_summary_heartbeats"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
logbuch.Info("this may take a while!")
// this turns out to actually be way faster than using joins and instead has the benefit of being cross-dialect compatible
var summaries []*models.Summary
if err := db.Model(&models.Summary{}).
Select("id, from_time, to_time, user_id").
Scan(&summaries).Error; err != nil {
return err
}
tx := db.Begin()
for _, s := range summaries {
query := "UPDATE summaries SET num_heartbeats = (SELECT count(id) AS num_heartbeats FROM heartbeats WHERE user_id = @user AND time BETWEEN @from AND @to) WHERE id = @id"
tx.Exec(query, sql.Named("from", s.FromTime), sql.Named("to", s.ToTime), sql.Named("id", s.ID), sql.Named("user", s.UserID))
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
logbuch.Error("failed to retroactively determine total summary heartbeats")
return err
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,56 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20220317-align_num_heartbeats"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
logbuch.Info("this may take a while!")
// find all summaries whose num_heartbeats is zero even though they have items
var faultyIds []uint
if err := db.Model(&models.Summary{}).
Distinct("summaries.id").
Joins("INNER JOIN summary_items ON summaries.num_heartbeats = 0 AND summaries.id = summary_items.summary_id").
Scan(&faultyIds).Error; err != nil {
return err
}
// update their heartbeats counter
result := db.
Table("summaries AS s1").
Where("s1.id IN ?", faultyIds).
Update(
"num_heartbeats",
db.
Model(&models.Heartbeat{}).
Select("COUNT(*)").
Where("user_id = ?", gorm.Expr("s1.user_id")).
Where("time BETWEEN ? AND ?", gorm.Expr("s1.from_time"), gorm.Expr("s1.to_time")),
)
if err := result.Error; err != nil {
return err
}
logbuch.Info("corrected heartbeats counter of %d summaries", result.RowsAffected)
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,41 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"gorm.io/gorm"
)
func init() {
const name = "20220318-mysql_timestamp_precision"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
if cfg.Db.IsMySQL() {
logbuch.Info("altering heartbeats table, this may take a while (up to hours)")
db.Exec("SET foreign_key_checks=0;")
db.Exec("SET unique_checks=0;")
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `time` TIMESTAMP(3) NOT NULL").Error; err != nil {
return err
}
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `created_at` TIMESTAMP(3) NOT NULL").Error; err != nil {
return err
}
db.Exec("SET foreign_key_checks=1;")
db.Exec("SET unique_checks=1;")
logbuch.Info("migrated timestamp columns to millisecond precision")
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,39 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "202203191-drop_diagnostics_user"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
migrator := db.Migrator()
if migrator.HasColumn(&models.Diagnostics{}, "user_id") {
logbuch.Info("running migration '%s'", name)
if err := migrator.DropConstraint(&models.Diagnostics{}, "fk_diagnostics_user"); err != nil {
logbuch.Warn("failed to drop 'fk_diagnostics_user' constraint (%v)", err)
}
if err := migrator.DropColumn(&models.Diagnostics{}, "user_id"); err != nil {
logbuch.Warn("failed to drop user_id column of diagnostics (%v)", err)
}
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,40 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
// migration to fix https://github.com/muety/wakapi/issues/346
// caused by https://github.com/muety/wakapi/blob/2.3.2/migrations/20220319_add_user_project_idx.go in combination with
// the wrongly defined index at https://github.com/muety/wakapi/blob/5aae18e2415d9e620f383f98cd8cbdf39cd99f27/models/heartbeat.go#L18
// and https://github.com/go-gorm/sqlite/issues/87
// -> drop index and let it be auto-created again with properly formatted ddl
func init() {
const name = "20220403-drop_user_project_idx"
const idxName = "idx_user_project"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if !db.Migrator().HasTable(&models.KeyStringValue{}) || hasRun(name, db) {
return nil
}
if cfg.Db.IsSQLite() && db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
logbuch.Info("running migration '%s'", name)
if err := db.Migrator().DropIndex(&models.Heartbeat{}, idxName); err != nil {
logbuch.Warn("failed to drop %s", idxName)
}
}
setHasRun(name, db)
return nil
},
}
registerPreMigration(f)
}

View File

@ -0,0 +1,24 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20220930-drop_heartbeats_entity_idx"
const idxName = "idx_entity"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if !db.Migrator().HasTable(&models.Heartbeat{}) || !db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
return nil
}
return db.Migrator().DropIndex(&models.Heartbeat{}, idxName)
},
}
registerPreMigration(f)
}

View File

@ -0,0 +1,88 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"strings"
)
// fix for https://github.com/muety/wakapi/issues/416
func init() {
const name = "20221002-fix_summary_id_types"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if cfg.Db.Dialect != config.SQLDialectMysql {
return nil
}
if !db.Migrator().HasTable(&models.Summary{}) || !db.Migrator().HasTable(&models.SummaryItem{}) {
return nil
}
var currentType string
if err := db.
Table("information_schema.columns").
Select("data_type").
Where("table_name = ?", "summary_items").
Where("column_name = ?", "summary_id").
Limit(1).
Row().Scan(&currentType); err != nil {
return err
}
if strings.ToLower(currentType) != "int" {
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_editors") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_editors"); err != nil {
return err
}
}
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_languages") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_languages"); err != nil {
return err
}
}
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_machines") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_machines"); err != nil {
return err
}
}
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_operating_systems") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_operating_systems"); err != nil {
return err
}
}
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_projects") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_projects"); err != nil {
return err
}
}
// https://github.com/muety/wakapi/issues/416#issuecomment-1271674792
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summary_items_summary") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summary_items_summary"); err != nil {
return err
}
}
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_labels") {
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_labels"); err != nil {
return err
}
}
if err := db.Migrator().AlterColumn(&models.Summary{}, "id"); err != nil {
return err
}
if err := db.Migrator().AlterColumn(&models.SummaryItem{}, "summary_id"); err != nil {
return err
}
}
return nil
},
}
registerPreMigration(f)
}

View File

@ -0,0 +1,35 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20221016-drop_rank_column"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
migrator := db.Migrator()
if migrator.HasTable(&models.LeaderboardItem{}) && migrator.HasColumn(&models.LeaderboardItem{}, "rank") {
logbuch.Info("running migration '%s'", name)
if err := migrator.DropColumn(&models.LeaderboardItem{}, "rank"); err != nil {
logbuch.Warn("failed to drop 'rank' column (%v)", err)
}
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@ -0,0 +1,71 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"regexp"
"strings"
)
// due to an error in the model definition, idx_time_user used to only cover 'user_id', but not time column
// if that's the case in the current state of the database, drop the index and let it be recreated by auto migration afterwards
func init() {
const name = "20221028-fix_heartbeats_time_user_idx"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
migrator := db.Migrator()
if !migrator.HasTable(&models.Heartbeat{}) {
return nil
}
var drop bool
if cfg.Db.IsSQLite() {
// sqlite migrator doesn't support GetIndexes() currently
var ddl string
if err := db.
Table("sqlite_schema").
Select("sql").
Where("type = 'index'").
Where("tbl_name = 'heartbeats'").
Where("name = 'idx_time_user'").
Scan(&ddl).Error; err != nil {
return err
}
matches := regexp.MustCompile("(?i)\\((.+)\\)$").FindStringSubmatch(ddl)
if len(matches) > 0 && !strings.Contains(matches[0], "`user_id`") || !strings.Contains(matches[0], "`time`") {
drop = true
}
} else {
indexes, err := migrator.GetIndexes(&models.Heartbeat{})
if err != nil {
return err
}
for _, idx := range indexes {
if idx.Table() == "heartbeats" && idx.Name() == "idx_time_user" && len(idx.Columns()) == 1 {
drop = true
break
}
}
}
if !drop {
return nil
}
if err := migrator.DropIndex(&models.Heartbeat{}, "idx_time_user"); err != nil {
return err
}
logbuch.Info("index 'idx_time_user' needs to be recreated, this may take a while")
return nil
},
}
registerPreMigration(f)
}

View File

@ -1,8 +0,0 @@
-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
insert into key_string_values ("key", "value") values ('imprint', 'no content here');
-- +migrate Down
-- SQL section 'Down' is executed when this migration is rolled back
SET SQL_MODE=ANSI_QUOTES;
delete from key_string_values where key = 'imprint';

View File

@ -28,9 +28,17 @@ func registerPostMigration(f migrationFunc) {
postMigrations = append(postMigrations, f)
}
// NOTE: Currently, migrations themselves keep track
// of whether they have run, yet or not, because some
// simply run on every start.
func Run(db *gorm.DB, cfg *config.Config) {
RunPreMigrations(db, cfg)
RunSchemaMigrations(db, cfg)
RunPostMigrations(db, cfg)
}
func RunSchemaMigrations(db *gorm.DB, cfg *config.Config) {
if err := cfg.GetMigrationFunc(cfg.Db.Dialect)(db); err != nil {
logbuch.Fatal(err.Error())
}
}
func RunPreMigrations(db *gorm.DB, cfg *config.Config) {
sort.Sort(preMigrations)
@ -38,18 +46,18 @@ func RunPreMigrations(db *gorm.DB, cfg *config.Config) {
for _, m := range preMigrations {
logbuch.Info("potentially running migration '%s'", m.name)
if err := m.f(db, cfg); err != nil {
logbuch.Fatal("migration '%s' failed %v", m.name, err)
logbuch.Fatal("migration '%s' failed - %v", m.name, err)
}
}
}
func RunCustomPostMigrations(db *gorm.DB, cfg *config.Config) {
func RunPostMigrations(db *gorm.DB, cfg *config.Config) {
sort.Sort(postMigrations)
for _, m := range postMigrations {
logbuch.Info("potentially running migration '%s'", m.name)
if err := m.f(db, cfg); err != nil {
logbuch.Fatal("migration '%s' failed %v", m.name, err)
logbuch.Fatal("migration '%s' failed - %v", m.name, err)
}
}
}

30
migrations/shared.go Normal file
View File

@ -0,0 +1,30 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func hasRun(name string, db *gorm.DB) bool {
condition := "key = ?"
if config.Get().Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?"
}
lookupResult := db.Where(condition, name).First(&models.KeyStringValue{})
if lookupResult.Error == nil && lookupResult.RowsAffected > 0 {
logbuch.Info("no need to migrate '%s'", name)
return true
}
return false
}
func setHasRun(name string, db *gorm.DB) {
if err := db.Create(&models.KeyStringValue{
Key: name,
Value: "done",
}).Error; err != nil {
logbuch.Error("failed to mark migration %s as run - %v", name, err)
}
}

View File

@ -9,6 +9,11 @@ type AliasRepositoryMock struct {
mock.Mock
}
func (m *AliasRepositoryMock) GetAll() ([]*models.Alias, error) {
args := m.Called()
return args.Get(0).([]*models.Alias), args.Error(1)
}
func (m *AliasRepositoryMock) GetByUser(s string) ([]*models.Alias, error) {
args := m.Called(s)
return args.Get(0).([]*models.Alias), args.Error(1)

View File

@ -29,6 +29,11 @@ func (m *AliasServiceMock) GetByUser(s string) ([]*models.Alias, error) {
return args.Get(0).([]*models.Alias), args.Error(1)
}
func (m *AliasServiceMock) GetByUserAndType(s string, u uint8) ([]*models.Alias, error) {
args := m.Called(s, u)
return args.Get(0).([]*models.Alias), args.Error(1)
}
func (m *AliasServiceMock) GetByUserAndKeyAndType(s string, s2 string, u uint8) ([]*models.Alias, error) {
args := m.Called(s, s2, u)
return args.Get(0).([]*models.Alias), args.Error(1)

16
mocks/duration_service.go Normal file
View File

@ -0,0 +1,16 @@
package mocks
import (
"github.com/muety/wakapi/models"
"github.com/stretchr/testify/mock"
"time"
)
type DurationServiceMock struct {
mock.Mock
}
func (m *DurationServiceMock) Get(time time.Time, time2 time.Time, user *models.User, f *models.Filters) (models.Durations, error) {
args := m.Called(time, time2, user, f)
return args.Get(0).(models.Durations), args.Error(1)
}

View File

@ -10,18 +10,18 @@ type HeartbeatServiceMock struct {
mock.Mock
}
func (m *HeartbeatServiceMock) Insert(heartbeat *models.Heartbeat) error {
args := m.Called(heartbeat)
func (m *HeartbeatServiceMock) Insert(h *models.Heartbeat) error {
args := m.Called(h)
return args.Error(0)
}
func (m *HeartbeatServiceMock) InsertBatch(heartbeats []*models.Heartbeat) error {
args := m.Called(heartbeats)
func (m *HeartbeatServiceMock) InsertBatch(h []*models.Heartbeat) error {
args := m.Called(h)
return args.Error(0)
}
func (m *HeartbeatServiceMock) Count() (int64, error) {
args := m.Called()
func (m *HeartbeatServiceMock) Count(a bool) (int64, error) {
args := m.Called(a)
return int64(args.Int(0)), args.Error(1)
}
@ -40,17 +40,42 @@ func (m *HeartbeatServiceMock) GetAllWithin(time time.Time, time2 time.Time, use
return args.Get(0).([]*models.Heartbeat), args.Error(1)
}
func (m *HeartbeatServiceMock) GetAllWithinByFilters(time time.Time, time2 time.Time, user *models.User, filters *models.Filters) ([]*models.Heartbeat, error) {
args := m.Called(time, time2, user, filters)
return args.Get(0).([]*models.Heartbeat), args.Error(1)
}
func (m *HeartbeatServiceMock) GetFirstByUsers() ([]*models.TimeByUser, error) {
args := m.Called()
return args.Get(0).([]*models.TimeByUser), args.Error(1)
}
func (m *HeartbeatServiceMock) GetLatestByUser(user *models.User) (*models.Heartbeat, error) {
args := m.Called(user)
return args.Get(0).(*models.Heartbeat), args.Error(1)
}
func (m *HeartbeatServiceMock) GetLatestByOriginAndUser(s string, user *models.User) (*models.Heartbeat, error) {
args := m.Called(s, user)
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)
}
func (m *HeartbeatServiceMock) DeleteByUser(u *models.User) error {
args := m.Called(u)
return args.Error(0)
}
func (m *HeartbeatServiceMock) DeleteByUserBefore(u *models.User, t time.Time) error {
args := m.Called(u, t)
return args.Error(0)
}

View File

@ -0,0 +1,40 @@
package mocks
import (
"github.com/muety/wakapi/models"
"github.com/stretchr/testify/mock"
)
type ProjectLabelServiceMock struct {
mock.Mock
}
func (p *ProjectLabelServiceMock) GetById(u uint) (*models.ProjectLabel, error) {
args := p.Called(u)
return args.Get(0).(*models.ProjectLabel), args.Error(1)
}
func (p *ProjectLabelServiceMock) GetByUser(s string) ([]*models.ProjectLabel, error) {
args := p.Called(s)
return args.Get(0).([]*models.ProjectLabel), args.Error(1)
}
func (p *ProjectLabelServiceMock) GetByUserGrouped(s string) (map[string][]*models.ProjectLabel, error) {
args := p.Called(s)
return args.Get(0).(map[string][]*models.ProjectLabel), args.Error(1)
}
func (p *ProjectLabelServiceMock) GetByUserGroupedInverted(s string) (map[string][]*models.ProjectLabel, error) {
args := p.Called(s)
return args.Get(0).(map[string][]*models.ProjectLabel), args.Error(1)
}
func (p *ProjectLabelServiceMock) Create(l *models.ProjectLabel) (*models.ProjectLabel, error) {
args := p.Called(l)
return args.Get(0).(*models.ProjectLabel), args.Error(1)
}
func (p *ProjectLabelServiceMock) Delete(l *models.ProjectLabel) error {
args := p.Called(l)
return args.Error(0)
}

View File

@ -10,13 +10,18 @@ type SummaryRepositoryMock struct {
mock.Mock
}
func (m *SummaryRepositoryMock) Insert(summary *models.Summary) error {
args := m.Called(summary)
func (m *SummaryRepositoryMock) Insert(s *models.Summary) error {
args := m.Called(s)
return args.Error(0)
}
func (m *SummaryRepositoryMock) GetByUserWithin(user *models.User, time time.Time, time2 time.Time) ([]*models.Summary, error) {
args := m.Called(user, time, time2)
func (m *SummaryRepositoryMock) GetAll() ([]*models.Summary, error) {
args := m.Called()
return args.Get(0).([]*models.Summary), args.Error(1)
}
func (m *SummaryRepositoryMock) GetByUserWithin(u *models.User, t1 time.Time, t2 time.Time) ([]*models.Summary, error) {
args := m.Called(u, t1, t2)
return args.Get(0).([]*models.Summary), args.Error(1)
}
@ -29,3 +34,8 @@ func (m *SummaryRepositoryMock) DeleteByUser(s string) error {
args := m.Called(s)
return args.Error(0)
}
func (m *SummaryRepositoryMock) DeleteByUserBefore(s string, t time.Time) error {
args := m.Called(s, t)
return args.Error(0)
}

View File

@ -19,13 +19,43 @@ func (m *UserServiceMock) GetUserByKey(s string) (*models.User, error) {
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetUserByEmail(s string) (*models.User, error) {
args := m.Called(s)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetUserByResetToken(s string) (*models.User, error) {
args := m.Called(s)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GetAll() ([]*models.User, error) {
args := m.Called()
return args.Get(0).([]*models.User), args.Error(1)
}
func (m *UserServiceMock) GetActive() ([]*models.User, error) {
func (m *UserServiceMock) GetMany(s []string) ([]*models.User, error) {
args := m.Called(s)
return args.Get(0).([]*models.User), args.Error(1)
}
func (m *UserServiceMock) GetManyMapped(s []string) (map[string]*models.User, error) {
args := m.Called()
return args.Get(0).(map[string]*models.User), args.Error(1)
}
func (m *UserServiceMock) GetAllByLeaderboard(b bool) ([]*models.User, error) {
//TODO implement me
panic("implement me")
}
func (m *UserServiceMock) GetAllByReports(b bool) ([]*models.User, error) {
args := m.Called(b)
return args.Get(0).([]*models.User), args.Error(1)
}
func (m *UserServiceMock) GetActive(b bool) ([]*models.User, error) {
args := m.Called(b)
return args.Get(0).([]*models.User), args.Error(1)
}
@ -59,8 +89,8 @@ func (m *UserServiceMock) ToggleBadges(user *models.User) (*models.User, error)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) SetWakatimeApiKey(user *models.User, s string) (*models.User, error) {
args := m.Called(user, s)
func (m *UserServiceMock) SetWakatimeApiCredentials(user *models.User, s1, s2 string) (*models.User, error) {
args := m.Called(user, s1, s2)
return args.Get(0).(*models.User), args.Error(1)
}
@ -69,6 +99,11 @@ func (m *UserServiceMock) MigrateMd5Password(user *models.User, login *models.Lo
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) GenerateResetToken(user *models.User) (*models.User, error) {
args := m.Called(user)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) FlushCache() {
m.Called()
}

View File

@ -1,5 +1,11 @@
package models
// AliasResolver returns the alias of an entity, given its original name. I.e., it returns Alias.Key, given an Alias.Value
type AliasResolver func(t uint8, k string) string
// AliasReverseResolver returns all original names, which have the given alias as mapping target. I.e., it returns a list of Alias.Value, given an Alias.Key
type AliasReverseResolver func(t uint8, k string) []string
type Alias struct {
ID uint `gorm:"primary_key"`
Type uint8 `gorm:"not null; index:idx_alias_type_key"`

View File

@ -1,16 +1,15 @@
package v1
import (
"github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"time"
)
// https://shields.io/endpoint
const (
defaultLabel = "coding time"
defaultColor = "#2D3748" // not working
defaultLabel = "wakapi.dev"
defaultColor = "2F855A"
)
type BadgeData struct {
@ -20,18 +19,11 @@ type BadgeData struct {
Color string `json:"color"`
}
func NewBadgeDataFrom(summary *models.Summary, filters *models.Filters) *BadgeData {
var total time.Duration
if hasFilter, _, _ := filters.One(); hasFilter {
total = summary.TotalTimeByFilters(filters)
} else {
total = summary.TotalTime()
}
func NewBadgeDataFrom(summary *models.Summary) *BadgeData {
return &BadgeData{
SchemaVersion: 1,
Label: defaultLabel,
Message: utils.FmtWakatimeDuration(total),
Message: helpers.FmtWakatimeDuration(summary.TotalTime()),
Color: defaultColor,
}
}

View File

@ -1,8 +1,8 @@
package v1
import (
"github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"time"
)
@ -27,19 +27,21 @@ type AllTimeRange struct {
Timezone string `json:"timezone"`
}
func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel {
var total time.Duration
if key := filters.Project; key != "" {
total = summary.TotalTimeByFilters(filters)
} else {
total = summary.TotalTime()
}
func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
total := summary.TotalTime()
tzName, _ := summary.FromTime.T().Zone()
return &AllTimeViewModel{
Data: &AllTimeData{
TotalSeconds: float32(total.Seconds()),
Text: utils.FmtWakatimeDuration(total),
Text: helpers.FmtWakatimeDuration(total),
IsUpToDate: true,
Range: &AllTimeRange{
End: summary.ToTime.T().Format(time.RFC3339),
EndDate: helpers.FormatDate(summary.ToTime.T()),
Start: summary.FromTime.T().Format(time.RFC3339),
StartDate: helpers.FormatDate(summary.FromTime.T()),
Timezone: tzName,
},
},
}
}

View File

@ -1,6 +1,11 @@
package v1
import "github.com/muety/wakapi/models"
import (
"strconv"
"time"
"github.com/muety/wakapi/models"
)
type HeartbeatsViewModel struct {
Data []*HeartbeatEntry `json:"data"`
@ -10,16 +15,40 @@ type HeartbeatsViewModel struct {
// that is actually required for the import
type HeartbeatEntry struct {
Id string `json:"id"`
Branch string `json:"branch"`
Category string `json:"category"`
Entity string `json:"entity"`
IsWrite bool `json:"is_write"`
Language string `json:"language"`
Project string `json:"project"`
Time models.CustomTime `json:"time"`
Type string `json:"type"`
UserId string `json:"user_id"`
MachineNameId string `json:"machine_name_id"`
UserAgentId string `json:"user_agent_id"`
Id string `json:"id"`
Branch string `json:"branch"`
Category string `json:"category"`
Entity string `json:"entity"`
IsWrite bool `json:"is_write"`
Language string `json:"language"`
Project string `json:"project"`
Time float64 `json:"time"`
Type string `json:"type"`
UserId string `json:"user_id"`
MachineNameId string `json:"machine_name_id"`
UserAgentId string `json:"user_agent_id"`
CreatedAt time.Time `json:"created_at"`
}
func HeartbeatsToCompat(entries []*models.Heartbeat) []*HeartbeatEntry {
out := make([]*HeartbeatEntry, len(entries))
for i := 0; i < len(entries); i++ {
entry := entries[i]
out[i] = &HeartbeatEntry{
Id: strconv.FormatUint(entry.ID, 10),
Branch: entry.Branch,
Category: entry.Category,
Entity: entry.Entity,
IsWrite: entry.IsWrite,
Language: entry.Language,
Project: entry.Project,
Time: float64(entry.Time.T().Unix()),
Type: entry.Type,
UserId: entry.UserID,
MachineNameId: entry.Machine,
UserAgentId: entry.UserAgent,
CreatedAt: entry.CreatedAt.T(),
}
}
return out
}

View File

@ -3,7 +3,8 @@ package v1
// https://wakatime.com/api/v1/users/current/machine_names
type MachineViewModel struct {
Data []*MachineEntry `json:"data"`
Data []*MachineEntry `json:"data"`
TotalPages int `json:"total_pages"`
}
type MachineEntry struct {

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

@ -2,6 +2,7 @@ package v1
import (
"github.com/muety/wakapi/models"
"math"
"time"
)
@ -25,6 +26,7 @@ type StatsData struct {
Machines []*SummariesEntry `json:"machines"`
Projects []*SummariesEntry `json:"projects"`
OperatingSystems []*SummariesEntry `json:"operating_systems"`
Branches []*SummariesEntry `json:"branches,omitempty"`
}
func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewModel {
@ -41,6 +43,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))
@ -66,11 +72,21 @@ func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewMo
oss[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryOS))
}
branches := make([]*SummariesEntry, len(summary.Branches))
for i, e := range summary.Branches {
branches[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryBranch))
}
data.Editors = editors
data.Languages = languages
data.Machines = machines
data.Projects = projects
data.OperatingSystems = oss
data.Branches = branches
if summary.Branches == nil {
data.Branches = nil
}
return &StatsViewModel{
Data: data,

View File

@ -2,8 +2,8 @@ package v1
import (
"fmt"
"github.com/muety/wakapi/helpers"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"math"
"sync"
"time"
@ -13,9 +13,17 @@ import (
// https://pastr.de/v/736450
type SummariesViewModel struct {
Data []*SummariesData `json:"data"`
End time.Time `json:"end"`
Start time.Time `json:"start"`
Data []*SummariesData `json:"data"`
End time.Time `json:"end"`
Start time.Time `json:"start"`
CumulativeTotal *SummariesCumulativeTotal `json:"cummulative_total"` // typo is intended
}
type SummariesCumulativeTotal struct {
Decimal string `json:"decimal"`
Digital string `json:"digital"`
Seconds float64 `json:"seconds"`
Text string `json:"text"`
}
type SummariesData struct {
@ -26,6 +34,7 @@ type SummariesData struct {
Machines []*SummariesEntry `json:"machines"`
OperatingSystems []*SummariesEntry `json:"operating_systems"`
Projects []*SummariesEntry `json:"projects"`
Branches []*SummariesEntry `json:"branches,omitempty"`
GrandTotal *SummariesGrandTotal `json:"grand_total"`
Range *SummariesRange `json:"range"`
}
@ -57,7 +66,7 @@ type SummariesRange struct {
Timezone string `json:"timezone"`
}
func NewSummariesFrom(summaries []*models.Summary, filters *models.Filters) *SummariesViewModel {
func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
data := make([]*SummariesData, len(summaries))
minDate, maxDate := time.Now().Add(1*time.Second), time.Time{}
@ -72,10 +81,23 @@ func NewSummariesFrom(summaries []*models.Summary, filters *models.Filters) *Sum
}
}
var totalTime time.Duration
for _, s := range summaries {
totalTime += s.TotalTime()
}
totalHrs, totalMins, totalSecs := totalTime.Hours(), (totalTime - time.Duration(totalTime.Hours())*time.Hour).Minutes(), totalTime.Seconds()
return &SummariesViewModel{
Data: data,
End: maxDate,
Start: minDate,
CumulativeTotal: &SummariesCumulativeTotal{
Decimal: fmt.Sprintf("%.2f", totalHrs),
Digital: fmt.Sprintf("%d:%d", int(totalHrs), int(totalMins)),
Seconds: totalSecs,
Text: helpers.FmtWakatimeDuration(totalTime),
},
}
}
@ -92,11 +114,12 @@ func newDataFrom(s *models.Summary) *SummariesData {
Machines: make([]*SummariesEntry, len(s.Machines)),
OperatingSystems: make([]*SummariesEntry, len(s.OperatingSystems)),
Projects: make([]*SummariesEntry, len(s.Projects)),
Branches: make([]*SummariesEntry, len(s.Branches)),
GrandTotal: &SummariesGrandTotal{
Digital: fmt.Sprintf("%d:%d", totalHrs, totalMins),
Hours: totalHrs,
Minutes: totalMins,
Text: utils.FmtWakatimeDuration(total),
Text: helpers.FmtWakatimeDuration(total),
TotalSeconds: total.Seconds(),
},
Range: &SummariesRange{
@ -109,7 +132,7 @@ func newDataFrom(s *models.Summary) *SummariesData {
}
var wg sync.WaitGroup
wg.Add(5)
wg.Add(6)
go func(data *SummariesData) {
defer wg.Done()
@ -129,7 +152,6 @@ func newDataFrom(s *models.Summary) *SummariesData {
defer wg.Done()
for i, e := range s.Languages {
data.Languages[i] = convertEntry(e, s.TotalTimeBy(models.SummaryLanguage))
}
}(data)
@ -147,14 +169,23 @@ func newDataFrom(s *models.Summary) *SummariesData {
}
}(data)
go func(data *SummariesData) {
defer wg.Done()
for i, e := range s.Branches {
data.Branches[i] = convertEntry(e, s.TotalTimeBy(models.SummaryBranch))
}
}(data)
if s.Branches == nil {
data.Branches = nil
}
wg.Wait()
return data
}
func convertEntry(e *models.SummaryItem, entityTotal time.Duration) *SummariesEntry {
// this is a workaround, since currently, the total time of a summary item is mistakenly represented in seconds
// TODO: fix some day, while migrating persisted summary items
total := e.Total * time.Second
total := e.TotalFixed()
hrs := int(total.Hours())
mins := int((total - time.Duration(hrs)*time.Hour).Minutes())
secs := int((total - time.Duration(hrs)*time.Hour - time.Duration(mins)*time.Minute).Seconds())
@ -170,7 +201,7 @@ func convertEntry(e *models.SummaryItem, entityTotal time.Duration) *SummariesEn
Name: e.Key,
Percent: percentage,
Seconds: secs,
Text: utils.FmtWakatimeDuration(total),
Text: helpers.FmtWakatimeDuration(total),
TotalSeconds: total.Seconds(),
}
}

View File

@ -0,0 +1,55 @@
package v1
import (
"github.com/muety/wakapi/models"
"time"
)
const DefaultWakaUserDisplayName = "Anonymous User"
// partially compatible with https://wakatime.com/developers#users
type UserViewModel struct {
Data *User `json:"data"`
}
type User struct {
ID string `json:"id"`
DisplayName string `json:"display_name"`
FullName string `json:"full_name"`
Email string `json:"email"`
IsEmailPublic bool `json:"is_email_public"`
IsEmailConfirmed bool `json:"is_email_confirmed"`
TimeZone string `json:"timezone"`
LastHeartbeatAt models.CustomTime `json:"last_heartbeat_at"`
LastProject string `json:"last_project"`
LastPluginName string `json:"last_plugin_name"`
Username string `json:"username"`
Website string `json:"website"`
CreatedAt models.CustomTime `json:"created_at"`
ModifiedAt models.CustomTime `json:"modified_at"`
}
func NewFromUser(user *models.User) *User {
tz, _ := time.Now().Zone()
if user.Location != "" {
tz = user.Location
}
return &User{
ID: user.ID,
DisplayName: DefaultWakaUserDisplayName,
Email: user.Email,
TimeZone: tz,
Username: user.ID,
CreatedAt: user.CreatedAt,
ModifiedAt: user.CreatedAt,
}
}
func (u *User) WithLatestHeartbeat(h *models.Heartbeat) *User {
u.LastHeartbeatAt = h.Time
u.LastProject = h.Project
u.LastPluginName = h.Editor
return u
}

View File

@ -1,7 +1,8 @@
package v1
type UserAgentsViewModel struct {
Data []*UserAgentEntry `json:"data"`
Data []*UserAgentEntry `json:"data"`
TotalPages int `json:"total_pages"`
}
type UserAgentEntry struct {

11
models/diagnostics.go Normal file
View File

@ -0,0 +1,11 @@
package models
type Diagnostics struct {
ID uint `gorm:"primary_key"`
Platform string `json:"platform"`
Architecture string `json:"architecture"`
Plugin string `json:"plugin"`
CliVersion string `json:"cli_version"`
Logs string `json:"logs" gorm:"type:text"`
StackTrace string `json:"stacktrace" gorm:"type:text"`
}

70
models/duration.go Normal file
View File

@ -0,0 +1,70 @@
package models
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/mitchellh/hashstructure/v2"
"time"
)
type Duration struct {
UserID string `json:"user_id"`
Time CustomTime `json:"time" hash:"ignore"`
Duration time.Duration `json:"duration" hash:"ignore"`
Project string `json:"project"`
Language string `json:"language"`
Editor string `json:"editor"`
OperatingSystem string `json:"operating_system"`
Machine string `json:"machine"`
Branch string `json:"branch"`
NumHeartbeats int `json:"-" hash:"ignore"`
GroupHash string `json:"-" hash:"ignore"`
}
func NewDurationFromHeartbeat(h *Heartbeat) *Duration {
d := &Duration{
UserID: h.UserID,
Time: h.Time,
Duration: 0,
Project: h.Project,
Language: h.Language,
Editor: h.Editor,
OperatingSystem: h.OperatingSystem,
Machine: h.Machine,
Branch: h.Branch,
NumHeartbeats: 1,
}
return d.Hashed()
}
func (d *Duration) Hashed() *Duration {
hash, err := hashstructure.Hash(d, hashstructure.FormatV2, nil)
if err != nil {
logbuch.Error("CRITICAL ERROR: failed to hash struct - %v", err)
}
d.GroupHash = fmt.Sprintf("%x", hash)
return d
}
func (d *Duration) GetKey(t uint8) (key string) {
switch t {
case SummaryProject:
key = d.Project
case SummaryEditor:
key = d.Editor
case SummaryLanguage:
key = d.Language
case SummaryOS:
key = d.OperatingSystem
case SummaryMachine:
key = d.Machine
case SummaryBranch:
key = d.Branch
}
if key == "" {
key = UnknownSummaryKey
}
return key
}

46
models/durations.go Normal file
View File

@ -0,0 +1,46 @@
package models
import "sort"
type Durations []*Duration
func (d Durations) Len() int {
return len(d)
}
func (d Durations) Less(i, j int) bool {
return d[i].Time.T().Before(d[j].Time.T())
}
func (d Durations) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}
func (d Durations) TotalNumHeartbeats() int {
var total int
for _, e := range d {
total += e.NumHeartbeats
}
return total
}
func (d Durations) Sorted() Durations {
sort.Sort(d)
return d
}
func (d *Durations) First() *Duration {
// assumes slice to be sorted
if d.Len() == 0 {
return nil
}
return (*d)[0]
}
func (d *Durations) Last() *Duration {
// assumes slice to be sorted
if d.Len() == 0 {
return nil
}
return (*d)[d.Len()-1]
}

View File

@ -1,45 +1,223 @@
package models
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/mitchellh/hashstructure/v2"
)
type Filters struct {
Project string
OS string
Language string
Editor string
Machine string
Project OrFilter
OS OrFilter
Language OrFilter
Editor OrFilter
Machine OrFilter
Label OrFilter
Branch OrFilter
}
type OrFilter []string
func (f OrFilter) Exists() bool {
return len(f) > 0 && f[0] != ""
}
func (f OrFilter) MatchAny(search string) bool {
for _, s := range f {
if s == search {
return true
}
}
return false
}
type FilterElement struct {
Type uint8
Key string
entity uint8
filter OrFilter
}
func NewFiltersWith(entity uint8, key string) *Filters {
switch entity {
case SummaryProject:
return &Filters{Project: key}
case SummaryOS:
return &Filters{OS: key}
case SummaryLanguage:
return &Filters{Language: key}
case SummaryEditor:
return &Filters{Editor: key}
case SummaryMachine:
return &Filters{Machine: key}
}
return &Filters{}
return NewFilterWithMultiple(entity, []string{key})
}
func (f *Filters) One() (bool, uint8, string) {
if f.Project != "" {
return true, SummaryProject, f.Project
} else if f.OS != "" {
return true, SummaryOS, f.OS
} else if f.Language != "" {
return true, SummaryLanguage, f.Language
} else if f.Editor != "" {
return true, SummaryEditor, f.Editor
} else if f.Machine != "" {
return true, SummaryMachine, f.Machine
}
return false, 0, ""
func NewFilterWithMultiple(entity uint8, keys []string) *Filters {
filters := &Filters{}
return filters.WithMultiple(entity, keys)
}
func (f *Filters) With(entity uint8, key string) *Filters {
return f.WithMultiple(entity, []string{key})
}
func (f *Filters) WithMultiple(entity uint8, keys []string) *Filters {
switch entity {
case SummaryProject:
f.Project = append(f.Project, keys...)
case SummaryOS:
f.OS = append(f.OS, keys...)
case SummaryLanguage:
f.Language = append(f.Language, keys...)
case SummaryEditor:
f.Editor = append(f.Editor, keys...)
case SummaryMachine:
f.Machine = append(f.Machine, keys...)
case SummaryLabel:
f.Label = append(f.Label, keys...)
case SummaryBranch:
f.Branch = append(f.Branch, keys...)
}
return f
}
func (f *Filters) One() (bool, uint8, OrFilter) {
if f.Project != nil && f.Project.Exists() {
return true, SummaryProject, f.Project
} else if f.OS != nil && f.OS.Exists() {
return true, SummaryOS, f.OS
} else if f.Language != nil && f.Language.Exists() {
return true, SummaryLanguage, f.Language
} else if f.Editor != nil && f.Editor.Exists() {
return true, SummaryEditor, f.Editor
} else if f.Machine != nil && f.Machine.Exists() {
return true, SummaryMachine, f.Machine
} else if f.Label != nil && f.Label.Exists() {
return true, SummaryLabel, f.Label
} else if f.Branch != nil && f.Branch.Exists() {
return true, SummaryBranch, f.Branch
}
return false, 0, OrFilter{}
}
func (f *Filters) OneOrEmpty() FilterElement {
if ok, t, of := f.One(); ok {
return FilterElement{entity: t, filter: of}
}
return FilterElement{entity: SummaryUnknown, filter: []string{}}
}
func (f *Filters) IsEmpty() bool {
nonEmpty, _, _ := f.One()
return !nonEmpty
}
func (f *Filters) Count() int {
var count int
for i := SummaryProject; i <= SummaryBranch; i++ {
count += f.CountByEntity(i)
}
return count
}
func (f *Filters) CountByEntity(entity uint8) int {
return len(*f.ResolveEntity(entity))
}
func (f *Filters) EntityCount() int {
var count int
for i := SummaryProject; i <= SummaryBranch; i++ {
if c := f.CountByEntity(i); c > 0 {
count++
}
}
return count
}
func (f *Filters) ResolveEntity(entityId uint8) *OrFilter {
switch entityId {
case SummaryProject:
return &f.Project
case SummaryLanguage:
return &f.Language
case SummaryEditor:
return &f.Editor
case SummaryOS:
return &f.OS
case SummaryMachine:
return &f.Machine
case SummaryLabel:
return &f.Label
case SummaryBranch:
return &f.Branch
default:
return &OrFilter{}
}
}
func (f *Filters) Hash() string {
hash, err := hashstructure.Hash(f, hashstructure.FormatV2, nil)
if err != nil {
logbuch.Error("CRITICAL ERROR: failed to hash struct - %v", err)
}
return fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
}
func (f *Filters) Match(h *Heartbeat) bool {
return (f.Project == nil || f.Project.MatchAny(h.Project)) &&
(f.OS == nil || f.OS.MatchAny(h.OperatingSystem)) &&
(f.Language == nil || f.Language.MatchAny(h.Language)) &&
(f.Editor == nil || f.Editor.MatchAny(h.Editor)) &&
(f.Machine == nil || f.Machine.MatchAny(h.Machine))
}
// WithAliases adds OR-conditions for every alias of a filter key as additional filter keys
func (f *Filters) WithAliases(resolve AliasReverseResolver) *Filters {
if f.Project != nil {
updated := OrFilter(make([]string, 0, len(f.Project)))
for _, e := range f.Project {
updated = append(updated, e)
updated = append(updated, resolve(SummaryProject, e)...)
}
f.Project = updated
}
if f.OS != nil {
updated := OrFilter(make([]string, 0, len(f.OS)))
for _, e := range f.OS {
updated = append(updated, e)
updated = append(updated, resolve(SummaryOS, e)...)
}
f.OS = updated
}
if f.Language != nil {
updated := OrFilter(make([]string, 0, len(f.Language)))
for _, e := range f.Language {
updated = append(updated, e)
updated = append(updated, resolve(SummaryLanguage, e)...)
}
f.Language = updated
}
if f.Editor != nil {
updated := OrFilter(make([]string, 0, len(f.Editor)))
for _, e := range f.Editor {
updated = append(updated, e)
updated = append(updated, resolve(SummaryEditor, e)...)
}
f.Editor = updated
}
if f.Machine != nil {
updated := OrFilter(make([]string, 0, len(f.Machine)))
for _, e := range f.Machine {
updated = append(updated, e)
updated = append(updated, resolve(SummaryMachine, e)...)
}
f.Machine = updated
}
if f.Branch != nil {
updated := OrFilter(make([]string, 0, len(f.Branch)))
for _, e := range f.Branch {
updated = append(updated, e)
updated = append(updated, resolve(SummaryBranch, e)...)
}
f.Branch = updated
}
return f
}
func (f *Filters) WithProjectLabels(resolve ProjectLabelReverseResolver) *Filters {
if f.Label == nil || !f.Label.Exists() {
return f
}
for _, l := range f.Label {
f.WithMultiple(SummaryProject, resolve(l))
}
return f
}

160
models/filters_test.go Normal file
View File

@ -0,0 +1,160 @@
package models
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"testing"
)
type FiltersTestSuite struct {
suite.Suite
TestAliases []*Alias
TestProjectLabels []*ProjectLabel
GetAliasReverseResolver func(indices []int) AliasReverseResolver
GetProjectLabelReverseResolver func(indices []int) ProjectLabelReverseResolver
}
func (suite *FiltersTestSuite) SetupSuite() {
suite.TestAliases = []*Alias{
{
Type: SummaryProject,
Key: "wakapi",
Value: "wakapi-mobile",
},
{
Type: SummaryProject,
Key: "wakapi",
Value: "wakapi-desktop",
},
{
Type: SummaryLanguage,
Key: "Python",
Value: "Python 3",
},
}
suite.TestProjectLabels = []*ProjectLabel{
{
ProjectKey: "wakapi",
Label: "oss",
},
{
ProjectKey: "anchr",
Label: "oss",
},
{
ProjectKey: "business-application",
Label: "work",
},
}
suite.GetAliasReverseResolver = func(indices []int) AliasReverseResolver {
return func(t uint8, k string) []string {
aliases := make([]string, 0, len(indices))
for _, j := range indices {
if a := suite.TestAliases[j]; a.Type == t && a.Key == k {
aliases = append(aliases, a.Value)
}
}
return aliases
}
}
suite.GetProjectLabelReverseResolver = func(indices []int) ProjectLabelReverseResolver {
return func(k string) []string {
labels := make([]string, 0, len(indices))
for _, j := range indices {
if l := suite.TestProjectLabels[j]; l.Label == k {
labels = append(labels, l.ProjectKey)
}
}
return labels
}
}
}
func TestFiltersTestSuite(t *testing.T) {
suite.Run(t, new(FiltersTestSuite))
}
func (suite *FiltersTestSuite) TestFilters_IsEmpty() {
assert.False(suite.T(), NewFiltersWith(SummaryProject, "wakapi").IsEmpty())
assert.True(suite.T(), (&Filters{}).IsEmpty())
}
func (suite *FiltersTestSuite) TestFilters_Match() {
heartbeats := []*Heartbeat{
{Project: "wakapi", Language: "Go"},
{Project: "anchr", Language: "Javascript"},
}
sut1 := NewFiltersWith(SummaryProject, "wakapi")
assert.True(suite.T(), sut1.Match(heartbeats[0]))
assert.False(suite.T(), sut1.Match(heartbeats[1]))
sut2 := NewFiltersWith(SummaryProject, "Go").With(SummaryLanguage, "JavaScript")
assert.False(suite.T(), sut2.Match(heartbeats[0]))
assert.False(suite.T(), sut2.Match(heartbeats[1]))
sut3 := NewFilterWithMultiple(SummaryProject, []string{"wakapi", "anchr"})
assert.True(suite.T(), sut3.Match(heartbeats[0]))
assert.True(suite.T(), sut3.Match(heartbeats[1]))
sut4 := &Filters{}
assert.True(suite.T(), sut4.Match(heartbeats[0]))
assert.True(suite.T(), sut4.Match(heartbeats[1]))
}
func (suite *FiltersTestSuite) TestFilters_One() {
sut1 := NewFiltersWith(SummaryLanguage, "Java")
ok1, type1, filters1 := sut1.One()
assert.True(suite.T(), ok1)
assert.Equal(suite.T(), SummaryLanguage, type1)
assert.Equal(suite.T(), "Java", filters1[0])
sut2 := &Filters{}
ok2, type2, filters2 := sut2.One()
assert.False(suite.T(), ok2)
assert.Zero(suite.T(), type2)
assert.Empty(suite.T(), filters2)
}
func (suite *FiltersTestSuite) TestFilters_WithAliases() {
sut1 := NewFiltersWith(SummaryProject, "wakapi")
sut1 = sut1.WithAliases(suite.GetAliasReverseResolver([]int{0, 1, 2}))
assert.Len(suite.T(), sut1.Project, 3)
assert.Len(suite.T(), sut1.Language, 0)
assert.Contains(suite.T(), sut1.Project, "wakapi")
assert.Contains(suite.T(), sut1.Project, "wakapi-desktop")
assert.Contains(suite.T(), sut1.Project, "wakapi-mobile")
sut2 := NewFiltersWith(SummaryProject, "wakapi").With(SummaryLanguage, "Python")
sut2 = sut2.WithAliases(suite.GetAliasReverseResolver([]int{0, 1, 2}))
assert.Len(suite.T(), sut2.Project, 3)
assert.Len(suite.T(), sut2.Language, 2)
assert.Contains(suite.T(), sut2.Language, "Python")
assert.Contains(suite.T(), sut2.Language, "Python 3")
sut3 := NewFiltersWith(SummaryProject, "foo")
sut3 = sut3.WithAliases(suite.GetAliasReverseResolver([]int{0, 1, 2}))
assert.Len(suite.T(), sut3.Project, 1)
assert.Len(suite.T(), sut3.Language, 0)
assert.Contains(suite.T(), sut3.Project, "foo")
}
func (suite *FiltersTestSuite) TestFilters_WithProjectLabels() {
sut1 := NewFiltersWith(SummaryProject, "mailwhale").With(SummaryLabel, "oss")
sut1 = sut1.WithProjectLabels(suite.GetProjectLabelReverseResolver([]int{0, 1, 2}))
assert.Len(suite.T(), sut1.Project, 3)
assert.Contains(suite.T(), sut1.Project, "wakapi")
assert.Contains(suite.T(), sut1.Project, "anchr")
assert.Contains(suite.T(), sut1.Project, "mailwhale")
assert.Contains(suite.T(), sut1.Label, "oss")
sut2 := NewFiltersWith(SummaryLabel, "oss")
sut2 = sut2.WithProjectLabels(suite.GetProjectLabelReverseResolver([]int{0, 1, 2}))
assert.Len(suite.T(), sut2.Project, 2)
assert.Contains(suite.T(), sut2.Project, "wakapi")
assert.Contains(suite.T(), sut2.Project, "anchr")
assert.Contains(suite.T(), sut2.Label, "oss")
}

View File

@ -4,48 +4,49 @@ import (
"fmt"
"github.com/emvi/logbuch"
"github.com/mitchellh/hashstructure/v2"
"regexp"
"strings"
"time"
)
type Heartbeat struct {
ID uint `gorm:"primary_key" hash:"ignore"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
Type string `json:"type"`
Category string `json:"category"`
Project string `json:"project"`
Branch string `json:"branch"`
Language string `json:"language" gorm:"index:idx_language"`
IsWrite bool `json:"is_write"`
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
Time CustomTime `json:"time" gorm:"type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
Origin string `json:"-" hash:"ignore"`
OriginId string `json:"-" hash:"ignore"`
languageRegex *regexp.Regexp `hash:"ignore"`
ID uint64 `gorm:"primary_key" hash:"ignore"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
UserID string `json:"-" gorm:"not null; index:idx_time_user; index:idx_user_project"` // idx_user_project is for quickly fetching a user's project list (settings page)
Entity string `json:"entity" gorm:"not null"`
Type string `json:"type" gorm:"size:255"`
Category string `json:"category" gorm:"size:255"`
Project string `json:"project" gorm:"index:idx_project; index:idx_user_project"`
Branch string `json:"branch" gorm:"index:idx_branch"`
Language string `json:"language" gorm:"index:idx_language"`
IsWrite bool `json:"is_write"`
Editor string `json:"editor" gorm:"index:idx_editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
OperatingSystem string `json:"operating_system" gorm:"index:idx_operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
Machine string `json:"machine" gorm:"index:idx_machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
UserAgent string `json:"user_agent" hash:"ignore" gorm:"type:varchar(255)"`
Time CustomTime `json:"time" gorm:"type:timestamp(3); index:idx_time; index:idx_time_user" swaggertype:"primitive,number"`
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
Origin string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
OriginId string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp(3)" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
}
func (h *Heartbeat) Valid() bool {
return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{})
}
func (h *Heartbeat) Timely(maxAge time.Duration) bool {
now := time.Now()
return now.Sub(h.Time.T()) <= maxAge && h.Time.T().Sub(now) < 1*time.Hour
}
func (h *Heartbeat) Augment(languageMappings map[string]string) {
if h.languageRegex == nil {
h.languageRegex = regexp.MustCompile(`^.+\.(.+)$`)
maxPrec := -1 // precision / mapping complexity -> more concrete ones shall take precedence
for ending, value := range languageMappings {
if ok, prec := strings.HasSuffix(h.Entity, "."+ending), strings.Count(ending, "."); ok && prec > maxPrec {
h.Language = value
maxPrec = prec
}
}
groups := h.languageRegex.FindAllStringSubmatch(h.Entity, -1)
if len(groups) == 0 || len(groups[0]) != 2 {
return
}
ending := groups[0][1]
if _, ok := languageMappings[ending]; !ok {
return
}
h.Language, _ = languageMappings[ending]
}
func (h *Heartbeat) GetKey(t uint8) (key string) {
@ -60,6 +61,8 @@ func (h *Heartbeat) GetKey(t uint8) (key string) {
key = h.OperatingSystem
case SummaryMachine:
key = h.Machine
case SummaryBranch:
key = h.Branch
}
if key == "" {
@ -96,8 +99,20 @@ func (h *Heartbeat) String() string {
func (h *Heartbeat) Hashed() *Heartbeat {
hash, err := hashstructure.Hash(h, hashstructure.FormatV2, nil)
if err != nil {
logbuch.Error("CRITICAL ERROR: failed to hash struct %v", err)
logbuch.Error("CRITICAL ERROR: failed to hash struct - %v", err)
}
h.Hash = fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
return h
}
func GetEntityColumn(t uint8) string {
return []string{
"project",
"language",
"editor",
"operating_system",
"machine",
"label",
"branch",
}[t]
}

View File

@ -26,17 +26,30 @@ func TestHeartbeat_Valid_MissingUser(t *testing.T) {
func TestHeartbeat_Augment(t *testing.T) {
testMappings := map[string]string{
"py": "Python3",
"py": "Python3",
"foo": "Foo Script",
"php": "PHP 8",
"blade.php": "Blade",
}
sut := &Heartbeat{
sut1, sut2, sut3 := &Heartbeat{
Entity: "~/dev/file.py",
Language: "Python",
}, &Heartbeat{
Entity: "~/dev/file.blade.php",
Language: "unknown",
}, &Heartbeat{
Entity: "~/dev/file.php",
Language: "PHP",
}
sut.Augment(testMappings)
sut1.Augment(testMappings)
sut2.Augment(testMappings)
sut3.Augment(testMappings)
assert.Equal(t, "Python3", sut.Language)
assert.Equal(t, "Python3", sut1.Language)
assert.Equal(t, "Blade", sut2.Language)
assert.Equal(t, "PHP 8", sut3.Language)
}
func TestHeartbeat_GetKey(t *testing.T) {

View File

@ -14,8 +14,9 @@ var (
IntervalPast7DaysYesterday = &IntervalKey{"Last 7 Days from Yesterday"}
IntervalPast14Days = &IntervalKey{"Last 14 Days"}
IntervalPast30Days = &IntervalKey{"30_days", "last_30_days", "Last 30 Days"}
IntervalPast12Months = &IntervalKey{"12_months", "last_12_months"}
IntervalAny = &IntervalKey{"any"}
IntervalPast6Months = &IntervalKey{"6_months", "last_6_months"}
IntervalPast12Months = &IntervalKey{"12_months", "last_12_months", "last_year"}
IntervalAny = &IntervalKey{"any", "all_time"}
)
var AllIntervals = []*IntervalKey{
@ -30,6 +31,7 @@ var AllIntervals = []*IntervalKey{
IntervalPast7DaysYesterday,
IntervalPast14Days,
IntervalPast30Days,
IntervalPast6Months,
IntervalPast12Months,
IntervalAny,
}

109
models/leaderboard.go Normal file
View File

@ -0,0 +1,109 @@
package models
import (
"github.com/duke-git/lancet/v2/maputil"
"github.com/duke-git/lancet/v2/slice"
"strings"
"time"
)
type LeaderboardItem struct {
ID uint `json:"-" gorm:"primary_key; size:32"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
UserID string `json:"user_id" gorm:"not null; index:idx_leaderboard_user"`
Interval string `json:"interval" gorm:"not null; size:32; index:idx_leaderboard_combined"`
By *uint8 `json:"aggregated_by" gorm:"index:idx_leaderboard_combined"` // pointer because nullable
Total time.Duration `json:"total" gorm:"not null" swaggertype:"primitive,integer"`
Key *string `json:"key" gorm:"size:255"` // pointer because nullable
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
}
// https://github.com/go-gorm/gorm/issues/5789
// https://github.com/go-gorm/gorm/issues/5284#issuecomment-1107775806
type LeaderboardItemRanked struct {
LeaderboardItem
Rank uint
}
func (l1 *LeaderboardItemRanked) Equals(l2 *LeaderboardItemRanked) bool {
return l1.ID == l2.ID
}
type Leaderboard []*LeaderboardItemRanked
func (l *Leaderboard) Add(item *LeaderboardItemRanked) {
if _, found := slice.Find[*LeaderboardItemRanked](*l, func(i int, item2 *LeaderboardItemRanked) bool {
return item.Equals(item2)
}); !found {
*l = append(*l, item)
}
}
func (l *Leaderboard) AddMany(items []*LeaderboardItemRanked) {
for _, item := range items {
l.Add(item)
}
}
func (l Leaderboard) UserIDs() []string {
return slice.Unique[string](slice.Map[*LeaderboardItemRanked, string](l, func(i int, item *LeaderboardItemRanked) string {
return item.UserID
}))
}
func (l Leaderboard) HasUser(userId string) bool {
return slice.Contain(l.UserIDs(), userId)
}
func (l Leaderboard) TopByKey(by uint8, key string) Leaderboard {
return slice.Filter[*LeaderboardItemRanked](l, func(i int, item *LeaderboardItemRanked) bool {
return item.By != nil && *item.By == by && item.Key != nil && strings.ToLower(*item.Key) == strings.ToLower(key)
})
}
func (l Leaderboard) TopKeys(by uint8) []string {
type keyTotal struct {
Key string
Total time.Duration
}
totalsMapped := make(map[string]*keyTotal, len(l))
for _, item := range l {
if item.Key == nil || item.By == nil || *item.By != by {
continue
}
key := strings.ToLower(*item.Key)
if _, ok := totalsMapped[key]; !ok {
totalsMapped[key] = &keyTotal{Key: *item.Key, Total: 0}
}
totalsMapped[key].Total += item.Total
}
totals := slice.Map[*keyTotal, keyTotal](maputil.Values[string, *keyTotal](totalsMapped), func(i int, item *keyTotal) keyTotal {
return *item
})
if err := slice.SortByField(totals, "Total", "desc"); err != nil {
return []string{} // TODO
}
return slice.Map[keyTotal, string](totals, func(i int, item keyTotal) string {
return item.Key
})
}
func (l Leaderboard) TopKeysByUser(by uint8, userId string) []string {
return Leaderboard(slice.Filter[*LeaderboardItemRanked](l, func(i int, item *LeaderboardItemRanked) bool {
return item.UserID == userId
})).TopKeys(by)
}
func (l Leaderboard) LastUpdate() time.Time {
lastUpdate := time.Time{}
for _, item := range l {
if item.CreatedAt.T().After(lastUpdate) {
lastUpdate = item.CreatedAt.T()
}
}
return lastUpdate
}

48
models/mail.go Normal file
View File

@ -0,0 +1,48 @@
package models
import (
"fmt"
"strings"
)
const HtmlType = "text/html; charset=UTF-8"
const PlainType = "text/html; charset=UTF-8"
type Mail struct {
From MailAddress
To MailAddresses
Subject string
Body string
Type string
}
func (m *Mail) WithText(text string) *Mail {
m.Body = text
m.Type = PlainType
return m
}
func (m *Mail) WithHTML(html string) *Mail {
m.Body = html
m.Type = HtmlType
return m
}
func (m *Mail) String() string {
return fmt.Sprintf("To: %s\r\n"+
"From: %s\r\n"+
"Subject: %s\r\n"+
"Content-Type: %s\r\n"+
"\r\n"+
"%s\r\n",
strings.Join(m.To.RawStrings(), ", "),
m.From.String(),
m.Subject,
m.Type,
m.Body,
)
}
func (m *Mail) Reader() *strings.Reader {
return strings.NewReader(m.String())
}

66
models/mail_address.go Normal file
View File

@ -0,0 +1,66 @@
package models
import "regexp"
const (
MailPattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
EmailAddrPattern = ".*\\s<(" + MailPattern + ")>|(" + MailPattern + ")"
)
var (
mailRegex *regexp.Regexp
emailAddrRegex *regexp.Regexp
)
func init() {
mailRegex = regexp.MustCompile(MailPattern)
emailAddrRegex = regexp.MustCompile(EmailAddrPattern)
}
type MailAddress string
type MailAddresses []MailAddress
func (m MailAddress) String() string {
return string(m)
}
func (m MailAddress) Raw() string {
match := emailAddrRegex.FindStringSubmatch(string(m))
if len(match) == 3 {
if match[2] != "" {
return match[2]
}
return match[1]
}
return ""
}
func (m MailAddress) Valid() bool {
return emailAddrRegex.Match([]byte(m))
}
func (m MailAddresses) Strings() []string {
out := make([]string, len(m))
for i, s := range m {
out[i] = s.String()
}
return out
}
func (m MailAddresses) RawStrings() []string {
out := make([]string, len(m))
for i, s := range m {
out[i] = s.Raw()
}
return out
}
func (m MailAddresses) AllValid() bool {
for _, a := range m {
if !a.Valid() {
return false
}
}
return true
}

View File

@ -0,0 +1,88 @@
package models
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestMailAddress_SingleRaw(t *testing.T) {
tests := []struct {
in string
out string
}{
{
"john.doe@example.org",
"john.doe@example.org",
},
{
"John Doe <john.doe@example.org>",
"john.doe@example.org",
},
{
"invalid",
"",
},
}
for _, test := range tests {
out := MailAddress(test.in).Raw()
assert.Equal(t, test.out, out)
}
}
func TestMailAddress_AllRaw(t *testing.T) {
tests := []struct {
in []string
out []string
}{
{
[]string{"john.doe@example.org", "foo@bar.com"},
[]string{"john.doe@example.org", "foo@bar.com"},
},
{
[]string{"John Doe <john.doe@example.org>", "foo@bar.com"},
[]string{"john.doe@example.org", "foo@bar.com"},
},
{
[]string{"john.doe@example.org", "invalid"},
[]string{"john.doe@example.org", ""},
},
}
for _, test := range tests {
out := castAddresses(test.in).RawStrings()
assert.EqualValues(t, test.out, out)
}
}
func TestMailAddress_AllValid(t *testing.T) {
tests := []struct {
in []string
out bool
}{
{
[]string{"john.doe@example.org", "foo@bar.com"},
true,
},
{
[]string{"John Doe <john.doe@example.org>", "ínvalid"},
false,
},
{
[]string{"", "invalid"},
false,
},
}
for _, test := range tests {
out := castAddresses(test.in).AllValid()
assert.EqualValues(t, test.out, out)
}
}
func castAddresses(addresses []string) (m MailAddresses) {
for _, a := range addresses {
m = append(m, MailAddress(a))
}
return m
}

View File

@ -4,7 +4,7 @@ import "fmt"
type CounterMetric struct {
Name string
Value int
Value int64
Desc string
Labels Labels
}

View File

@ -0,0 +1,22 @@
package metrics
import "fmt"
type GaugeMetric struct {
Name string
Value int64
Desc string
Labels Labels
}
func (c GaugeMetric) Key() string {
return c.Name
}
func (c GaugeMetric) Print() string {
return fmt.Sprintf("%s%s %d", c.Name, c.Labels.Print(), c.Value)
}
func (c GaugeMetric) Header() string {
return fmt.Sprintf("# HELP %s %s\n# TYPE %s gauge", c.Name, c.Desc, c.Name)
}

16
models/project_label.go Normal file
View File

@ -0,0 +1,16 @@
package models
// ProjectLabelReverseResolver returns all projects for a given label
type ProjectLabelReverseResolver func(l string) []string
type ProjectLabel struct {
ID uint `json:"id" gorm:"primary_key"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
UserID string `json:"-" gorm:"not null; index:idx_project_label_user"`
ProjectKey string `json:"project"`
Label string `json:"label" gorm:"type:varchar(64)"`
}
func (l *ProjectLabel) IsValid() bool {
return l.ProjectKey != "" && l.Label != ""
}

10
models/report.go Normal file
View File

@ -0,0 +1,10 @@
package models
import "time"
type Report struct {
From time.Time
To time.Time
User *User
Summary *Summary
}

View File

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"gorm.io/gorm"
"math"
"strconv"
"strings"
"time"
@ -30,24 +29,34 @@ type Interval struct {
End time.Time
}
type KeyedInterval struct {
Interval
Key *IntervalKey
}
type PageParams struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// CustomTime is a wrapper type around time.Time, mainly used for the purpose of transparently unmarshalling Python timestamps in the format <sec>.<nsec> (e.g. 1619335137.3324468)
type CustomTime time.Time
func (j *CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(j.String())
return json.Marshal(j.T())
}
func (j *CustomTime) UnmarshalJSON(b []byte) error {
s := strings.Replace(strings.Trim(string(b), "\""), ".", "", 1)
i, err := strconv.ParseInt(s, 10, 64)
s := strings.Trim(string(b), "\"")
ts, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
t := time.Unix(0, i*int64(math.Pow10(19-len(s))))
t := time.Unix(0, int64(ts*1e9)) // ms to ns
*j = CustomTime(t)
return nil
}
// heartbeat timestamps arrive as strings for sqlite and as time.Time for postgres
func (j *CustomTime) Scan(value interface{}) error {
var (
t time.Time
@ -56,13 +65,12 @@ func (j *CustomTime) Scan(value interface{}) error {
switch value.(type) {
case string:
// with sqlite, some queries (like GetLastByUser()) return dates as strings,
// however, most of the time they are returned as time.Time
t, err = time.Parse("2006-01-02 15:04:05-07:00", value.(string))
if err != nil {
return errors.New(fmt.Sprintf("unsupported date time format: %s", value))
}
case int64:
t = time.Unix(0, value.(int64))
break
case time.Time:
t = value.(time.Time)
break
@ -76,18 +84,17 @@ func (j *CustomTime) Scan(value interface{}) error {
return nil
}
func (j *CustomTime) Hash() (uint64, error) {
return uint64((j.T().UnixNano() / 1000) / 1000), nil
}
func (j CustomTime) Value() (driver.Value, error) {
t := time.Unix(0, j.T().UnixNano()/int64(time.Millisecond)*int64(time.Millisecond)) // round to millisecond precision
return t, nil
}
func (j *CustomTime) Hash() (uint64, error) {
return uint64((j.T().UnixNano() / 1000) / 1000), nil
}
func (j CustomTime) String() string {
t := time.Time(j)
return t.Format("2006-01-02 15:04:05.000")
return j.T().String()
}
func (j CustomTime) T() time.Time {
@ -97,3 +104,17 @@ func (j CustomTime) T() time.Time {
func (j CustomTime) Valid() bool {
return j.T().Unix() >= 0
}
func (p *PageParams) Limit() int {
if p.PageSize < 0 {
return 0
}
return p.PageSize
}
func (p *PageParams) Offset() int {
if p.PageSize <= 0 {
return 0
}
return (p.Page - 1) * p.PageSize
}

Some files were not shown because too many files have changed in this diff Show More