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

Compare commits

..

188 Commits

Author SHA1 Message Date
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
206 changed files with 9709 additions and 110405 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: ''
---

View File

@ -10,33 +10,45 @@ jobs:
docker-publish: docker-publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# https://stackoverflow.com/questions/58177786 - name: Set up QEMU
- name: Get version uses: docker/setup-qemu-action@v1
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 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 - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} 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 uses: docker/build-push-action@v2
with: with:
file: Dockerfile
push: true push: true
tags: | platforms: linux/amd64,linux/arm64,linux/arm/v7
n1try/wakapi:${{ env.GIT_TAG }} tags: ${{ steps.meta.outputs.tags }}
n1try/wakapi:latest cache-from: type=registry,ref=n1try/wakapi:buildcache-alpine
cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=registry,ref=n1try/wakapi:buildcache-alpine,mode=max
cache-to: type=local,dest=/tmp/.buildx-cache

View File

@ -1,4 +1,4 @@
name: Build Wakapi on Linux name: Linux
on: on:
push: push:
@ -10,7 +10,7 @@ on:
jobs: jobs:
build-and-release: build-and-release:
name: Build name: Linux - Build, Test & Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -24,8 +24,15 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Get dependencies - name: Get dependencies
run: go get
- name: Unit Tests
run: go test ./... -run ./...
- name: API Tests
run: | run: |
go get npm -g install newman
./testing/run_api_tests.sh
- name: Build - name: Build
run: GO111MODULE=on go build -v . run: GO111MODULE=on go build -v .

View File

@ -1,4 +1,4 @@
name: Build Wakapi on Windows name: Windows
on: on:
push: push:
@ -10,7 +10,7 @@ on:
jobs: jobs:
build-and-release: build-and-release:
name: Build name: Windows - Build & Release
runs-on: windows-latest runs-on: windows-latest
steps: steps:

2
.gitignore vendored
View File

@ -9,7 +9,5 @@ config*.yml
!config.default.yml !config.default.yml
!testing/config.testing.yml !testing/config.testing.yml
pkged.go pkged.go
package.json
yarn.lock
package-lock.json package-lock.json
node_modules 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

26
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
}

View File

@ -1,16 +1,26 @@
# Build Stage # To build locally: docker buildx build . -t wakapi --load
FROM golang:1.16 AS build-env # Preparation to save some time
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS prep-env
WORKDIR /src WORKDIR /src
ADD ./go.mod . ADD ./go.mod .
RUN go mod download RUN go mod download
ADD . .
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 chmod +x wait-for-it.sh
ADD . . # Build Stage
RUN go build -o wakapi FROM golang:1.17-alpine AS build-env
# Required for go-sqlite3
RUN apk add --no-cache gcc musl-dev
WORKDIR /src
COPY --from=prep-env /src .
RUN go build -v -o wakapi
WORKDIR /app WORKDIR /app
RUN cp /src/wakapi . && \ RUN cp /src/wakapi . && \
@ -25,11 +35,10 @@ RUN cp /src/wakapi . && \
# to override config values using `-e` syntax. # to override config values using `-e` syntax.
# Available options can be found in [README.md#-configuration](README.md#-configuration) # Available options can be found in [README.md#-configuration](README.md#-configuration)
FROM debian FROM alpine:3
WORKDIR /app WORKDIR /app
RUN apt update && \ RUN apk add --no-cache bash ca-certificates tzdata
apt install -y ca-certificates
# See README.md and config.default.yml for all config options # See README.md and config.default.yml for all config options
ENV ENVIRONMENT prod ENV ENVIRONMENT prod
@ -47,4 +56,4 @@ COPY --from=build-env /app .
VOLUME /data VOLUME /data
ENTRYPOINT ./entrypoint.sh ENTRYPOINT /app/entrypoint.sh

176
README.md
View File

@ -4,14 +4,11 @@
<p align="center"> <p align="center">
<img src="https://badges.fw-web.space/github/license/muety/wakapi"> <img src="https://badges.fw-web.space/github/license/muety/wakapi">
<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> <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"> <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">
<img src="https://badges.fw-web.space/github/languages/code-size/muety/wakapi"> <img src="https://badges.fw-web.space/github/languages/code-size/muety/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> <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=sqale_index"></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> <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> </p>
@ -32,27 +29,10 @@
</div> </div>
<p align="center"> <p align="center">
<img src="static/assets/images/screenshot.png" width="500px"> <img src="static/assets/images/screenshot.webp" width="500px">
</p> </p>
## Table of Contents Installation instructions can be found below and in the [Wiki](https://github.com/muety/wakapi/wiki).
* [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)
* [Tests](#-tests)
* [Developer Notes](#-developer-notes)
* [Support](#-support)
* [FAQs](#-faqs)
Further instructions can be found in the [Wiki](https://github.com/muety/wakapi/wiki).
## 📬 **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!
## 🚀 Features ## 🚀 Features
* ✅ 100 % free and open-source * ✅ 100 % free and open-source
@ -68,15 +48,13 @@ I'd love to get some community feedback from active Wakapi users. If you want, p
* ✅ Self-hosted * ✅ Self-hosted
## 🚧 Roadmap ## 🚧 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? ## ⌨️ 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) ### ☁️ 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). 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).
However, we do not guarantee data persistence, so you might potentially lose your data if the service is taken down some day ❕
### 📦 Option 2: Quick-run a Release ### 📦 Option 2: Quick-run a Release
```bash ```bash
@ -88,12 +66,15 @@ $ curl -L https://wakapi.dev/get | bash
# Create a persistent volume # Create a persistent volume
$ docker volume create wakapi-data $ 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 # Run the container
$ docker run -d \ $ docker run -d \
-p 3000:3000 \ -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 \ -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. **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.
@ -124,7 +105,7 @@ $ ./wakapi
**Note:** Check the comments `config.yml` for best practices regarding security configuration and more. **Note:** Check the comments `config.yml` for best practices regarding security configuration and more.
### 💻 Client Setup ### 💻 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) 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. **Editing your local `~/.wakatime.cfg`** file as follows
@ -132,8 +113,8 @@ Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime
```ini ```ini
[settings] [settings]
# Your Wakapi server URL or 'https://wakapi.dev/api' when using the cloud server # Your Wakapi server URL or 'https://wakapi.dev' when using the cloud server
api_url = http://localhost:3000/api api_url = http://localhost:3000/api/heartbeat
# Your Wakapi API key (get it from the web interface after having created an account) # Your Wakapi API key (get it from the web interface after having created an account)
api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b
@ -142,42 +123,46 @@ api_key = 406fe41f-6d69-4183-a4cc-121e0c524c2b
Optionally, you can set up a [client-side proxy](https://github.com/muety/wakapi/wiki/Advanced-Setup:-Client-side-proxy) 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 ## 🔧 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. 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 | Environment Variable | Default | Description | | YAML Key / Env. Variable | Default | Description |
|---------------------------|---------------------------|--------------|---------------------------------------------------------------------| |------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `env` | `ENVIRONMENT` | `dev` | Whether to use development- or production settings | | `env` /<br>`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.custom_languages` | - | - | Map from file endings to language names | | `app.custom_languages` | - | Map from file endings to language names |
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on | | `app.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.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) | | `server.port` /<br> `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) | | `server.listen_ipv4` /<br> `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
| `server.tls_cert_path` | `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) | | `server.listen_ipv6` /<br> `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
| `server.tls_key_path` | `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) | | `server.listen_socket` /<br> `WAKAPI_LISTEN_SOCKET` | - | UNIX socket to listen on (leave blank to disable UNIX socket) |
| `server.base_path` | `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) | | `server.timeout_sec` /<br> `WAKAPI_TIMEOUT_SEC` | `30` | Request timeout in seconds |
| `security.password_salt` | `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing | | `server.tls_cert_path` /<br> `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
| `security.insecure_cookies` | `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP | | `server.tls_key_path` /<br> `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
| `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 | | `server.base_path` /<br> `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
| `security.allow_signup` | `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration | | `security.password_salt` /<br> `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
| `security.expose_metrics` | `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` | | `security.insecure_cookies` /<br> `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
| `db.host` | `WAKAPI_DB_HOST` | - | Database host | | `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 |
| `db.port` | `WAKAPI_DB_PORT` | - | Database port | | `security.allow_signup` /<br> `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
| `db.user` | `WAKAPI_DB_USER` | - | Database user | | `security.expose_metrics` /<br> `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
| `db.password` | `WAKAPI_DB_PASSWORD` | - | Database password | | `db.host` /<br> `WAKAPI_DB_HOST` | - | Database host |
| `db.name` | `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name | | `db.port` /<br> `WAKAPI_DB_PORT` | - | Database port |
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) | | `db.user` /<br> `WAKAPI_DB_USER` | - | Database user |
| `db.charset` | `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) | | `db.password` /<br> `WAKAPI_DB_PASSWORD` | - | Database password |
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections | | `db.name` /<br> `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) | | `db.dialect` /<br> `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
| `db.automgirate_fail_silently` | `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up | | `db.charset` /<br> `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
| `mail.enabled` | `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) | | `db.max_conn` /<br> `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
| `mail.sender` | `WAKAPI_MAIL_SENDER` | `noreply@wakapi.dev` | Default sender address for outgoing mails (ignored for MailWhale) | | `db.ssl` /<br> `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
| `mail.provider` | `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) | | `db.automgirate_fail_silently` /<br> `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
| `mail.smtp.*` | `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yaml) for details | | `mail.enabled` /<br> `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
| `mail.mailwhale.*` | `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yaml) for details | | `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `noreply@wakapi.dev` | Default sender address for outgoing mails (ignored for MailWhale) |
| `sentry.dsn` | `WAKAPI_SENTRY_DSN` | | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) | | `mail.provider` /<br> `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
| `sentry.enable_tracing` | `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing | | `mail.smtp.*` /<br> `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yml) for details |
| `sentry.sample_rate` | `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry | | `mail.mailwhale.*` /<br> `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yml) for details |
| `sentry.sample_rate_heartbeats` | `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeats request in Sentry | | `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 heartbeats request in Sentry |
| `quick_start` /<br> `WAKAPI_QUICK_START` | `false` | Whether to skip initial boot tasks. Use only for development purposes! |
### Supported databases ### Supported databases
Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of different relational databases is supported. Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of different relational databases is supported.
@ -187,9 +172,6 @@ Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of differ
* [Postgres](https://hub.docker.com/_/postgres) (_open-source as well_) * [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_) * [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). See our [Swagger API Documentation](https://wakapi.dev/swagger-ui).
@ -304,10 +286,7 @@ To get a predictable environment, tests are run against a fresh and clean Wakapi
# 1. sqlite (cli) # 1. sqlite (cli)
$ sudo apt install sqlite # Fedora: sudo dnf install sqlite $ sudo apt install sqlite # Fedora: sudo dnf install sqlite
# 2. screen # 2. newman
$ sudo apt install screen # Fedora: sudo dnf install screen
# 3. newman
$ npm install -g newman $ npm install -g newman
``` ```
@ -318,23 +297,32 @@ $ ./testing/run_api_tests.sh
## 🤓 Developer Notes ## 🤓 Developer Notes
### Building web assets ### Building web assets
To keep things minimal, Wakapi does not contain a `package.json`, `node_modules` or any sort of frontend build step. Instead, all JS and CSS assets are included as static files and checked in to Git. This way we can avoid requiring NodeJS to build Wakapi. However, for [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) it makes sense to run it through a "build" step to benefit from purging and significantly reduce it in size. To only require this at the time of development, the compiled asset is checked in to Git as well. Similarly, [Iconify](https://iconify.design/docs/icon-bundles/) bundles are also created at development time and checked in to the repo. 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.
#### TailwindCSS
```bash ```bash
$ tailwindcss-cli build static/assets/vendor/tailwind.css -o static/assets/vendor/tailwind.dist.css $ yarn
``` $ yarn build # or: yarn watch
#### Iconify
```bash
$ yarn add -D @iconify/json-tools @iconify/json
$ node scripts/bundle_icons.js
``` ```
New icons can be added by editing the `icons` array in [scripts/bundle_icons.js](scripts/bundle_icons.js). New icons can be added by editing the `icons` array in [scripts/bundle_icons.js](scripts/bundle_icons.js).
## 🙏 Support #### Precompression
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! 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 ## ❔ 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. 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.
@ -365,7 +353,7 @@ All data is cached locally on your machine and sent in batches once you're onlin
<details> <details>
<summary><b>How did Wakapi come about?</b></summary> <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>
<details> <details>
@ -388,7 +376,7 @@ WakaTime is worth the price. However, if you only want basic statistics and keep
<details> <details>
<summary><b>How are durations calculated?</b></summary> <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): Here is an example (circles are heartbeats):
@ -398,7 +386,7 @@ Here is an example (circles are heartbeats):
``` ```
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 starring at the screen trying to find a solution, but not actually typing code.
<ul> <ul>
<li><b>WakaTime</b> (with 5 min timeout): 3 min 20 sec <li><b>WakaTime</b> (with 5 min timeout): 3 min 20 sec
@ -409,8 +397,18 @@ 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. Wakapi adds a "padding" of two minutes before the third heartbeat. This is why total times will slightly vary between Wakapi and WakaTime.
</details> </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 ## 🙏 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 ## 📓 License
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io) GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)

View File

@ -3,6 +3,8 @@ env: production
server: server:
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4 listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
listen_ipv6: ::1 # leave blank to disable ipv6 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_cert_path: # leave blank to not use https
tls_key_path: # leave blank to not use https tls_key_path: # leave blank to not use https
port: 3000 port: 3000
@ -19,6 +21,11 @@ app:
jsx: JSX jsx: JSX
svelte: Svelte 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: db:
host: # leave blank when using sqlite3 host: # leave blank when using sqlite3
port: # leave blank when using sqlite3 port: # leave blank when using sqlite3
@ -37,6 +44,7 @@ security:
cookie_max_age: 172800 cookie_max_age: 172800
allow_signup: true allow_signup: true
expose_metrics: false expose_metrics: false
enable_proxy: false # only intended for production instance at wakapi.dev
sentry: sentry:
dsn: # leave blank to disable sentry integration dsn: # leave blank to disable sentry integration
@ -62,3 +70,5 @@ mail:
url: url:
client_id: client_id:
client_secret: client_secret:
quick_start: false # whether to skip initial tasks on application startup, like summary generation

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
uuid "github.com/satori/go.uuid"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
@ -33,6 +34,7 @@ const (
SimpleDateTimeFormat = "2006-01-02 15:04:05" SimpleDateTimeFormat = "2006-01-02 15:04:05"
ErrUnauthorized = "401 unauthorized" ErrUnauthorized = "401 unauthorized"
ErrBadRequest = "400 bad request"
ErrInternalServerError = "500 internal server error" ErrInternalServerError = "500 internal server error"
) )
@ -61,18 +63,21 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
var env string var env string
type appConfig struct { type appConfig struct {
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"` AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"` ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"` 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"` ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"` InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
CustomLanguages map[string]string `yaml:"custom_languages"` CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
Colors map[string]map[string]string `yaml:"-"` AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg"`
CustomLanguages map[string]string `yaml:"custom_languages"`
Colors map[string]map[string]string `yaml:"-"`
} }
type securityConfig struct { type securityConfig struct {
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"` AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"` 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)) // this is actually a pepper (https://en.wikipedia.org/wiki/Pepper_(cryptography))
PasswordSalt string `yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT"` PasswordSalt string `yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT"`
InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"` InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"`
@ -95,13 +100,15 @@ type dbConfig struct {
} }
type serverConfig struct { type serverConfig struct {
Port int `default:"3000" env:"WAKAPI_PORT"` Port int `default:"3000" env:"WAKAPI_PORT"`
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"` 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"` ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"` ListenSocket string `yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET"`
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"` TimeoutSec int `yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC"`
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"` BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_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 { type sentryConfig struct {
@ -134,22 +141,24 @@ type SMTPMailConfig struct {
} }
type Config struct { type Config struct {
Env string `default:"dev" env:"ENVIRONMENT"` Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"` Version string `yaml:"-"`
App appConfig QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
Security securityConfig InstanceId string `yaml:"-"` // only temporary, changes between runs
Db dbConfig App appConfig
Server serverConfig Security securityConfig
Sentry sentryConfig Db dbConfig
Mail mailConfig Server serverConfig
Sentry sentryConfig
Mail mailConfig
} }
func (c *Config) CreateCookie(name, value, path string) *http.Cookie { func (c *Config) CreateCookie(name, value string) *http.Cookie {
return c.createCookie(name, value, path, c.Security.CookieMaxAgeSec) return c.createCookie(name, value, c.Server.BasePath, c.Security.CookieMaxAgeSec)
} }
func (c *Config) GetClearCookie(name, path string) *http.Cookie { func (c *Config) GetClearCookie(name string) *http.Cookie {
return c.createCookie(name, "", path, -1) return c.createCookie(name, "", c.Server.BasePath, -1)
} }
func (c *Config) createCookie(name, value, path string, maxAge int) *http.Cookie { func (c *Config) createCookie(name, value, path string, maxAge int) *http.Cookie {
@ -200,6 +209,9 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !c.Db.AutoMigrateFailSilently { if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err return err
} }
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
return nil return nil
} }
} }
@ -230,6 +242,18 @@ func (c *appConfig) GetWeeklyReportTime() string {
return strings.Split(c.ReportTimeWeekly, ",")[1] return strings.Split(c.ReportTimeWeekly, ",")[1]
} }
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 { func (c *serverConfig) GetPublicUrl() string {
return strings.TrimSuffix(c.PublicUrl, "/") return strings.TrimSuffix(c.PublicUrl, "/")
} }
@ -245,12 +269,12 @@ func IsDev(env string) bool {
func readColors() map[string]map[string]string { func readColors() map[string]map[string]string {
// Read language colors // Read language colors
// Source: // Source:
// https://raw.githubusercontent.com/ozh/github-colors/master/colors.json // - https://raw.githubusercontent.com/ozh/github-colors/master/colors.json
// https://wakatime.com/colors/operating_systems // - https://wakatime.com/colors/operating_systems
// - https://wakatime.com/colors/editors // - https://wakatime.com/colors/editors
// Extracted from Wakatime website with XPath (see below) and did a bit of regex magic after. // 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"]/@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"]/div[1]/text()').map(e => e.nodeValue)
raw := data.ColorsFile raw := data.ColorsFile
if IsDev(env) { if IsDev(env) {
@ -276,6 +300,12 @@ func resolveDbDialect(dbType string) string {
if dbType == "cockroach" { if dbType == "cockroach" {
return "postgres" return "postgres"
} }
if dbType == "sqlite" {
return "sqlite3"
}
if dbType == "mariadb" {
return "mysql"
}
return dbType return dbType
} }
@ -327,6 +357,7 @@ func Load(version string) *Config {
env = config.Env env = config.Env
config.Version = strings.TrimSpace(version) config.Version = strings.TrimSpace(version)
config.InstanceId = uuid.NewV4().String()
config.App.Colors = readColors() config.App.Colors = readColors()
config.Db.Dialect = resolveDbDialect(config.Db.Type) config.Db.Dialect = resolveDbDialect(config.Db.Type)
config.Security.SecureCookie = securecookie.New( config.Security.SecureCookie = securecookie.New(
@ -350,12 +381,16 @@ func Load(version string) *Config {
} }
// some validation checks // some validation checks
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" { if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" && config.Server.ListenSocket == "" {
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 must be set") logbuch.Fatal("either of listen_ipv4 or listen_ipv6 or listen_socket must be set")
} }
if config.Db.MaxConn <= 0 { if config.Db.MaxConn <= 0 {
logbuch.Fatal("you must allow at least one database connection") 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 != "" && findString(config.Mail.Provider, emailProviders, "") == "" { if config.Mail.Provider != "" && findString(config.Mail.Provider, emailProviders, "") == "" {
logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider) logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider)
} }

View File

@ -8,9 +8,17 @@ type ApplicationEvent struct {
} }
const ( const (
TopicUser = "user.*" TopicUser = "user.*"
EventUserUpdate = "user.update" TopicHeartbeat = "heartbeat.*"
FieldPayload = "payload" 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 var eventHub *hub.Hub

View File

@ -140,7 +140,7 @@ func initSentry(config sentryConfig, debug bool) {
return event return event
}, },
}); err != nil { }); err != nil {
logbuch.Fatal("failed to initialized sentry %v", err) logbuch.Fatal("failed to initialized sentry - %v", err)
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -243,83 +243,5 @@
"Zephir": "#118f9e", "Zephir": "#118f9e",
"Zig": "#ec915c", "Zig": "#ec915c",
"ZIL": "#dc75e5" "ZIL": "#dc75e5"
},
"editors": {
"Android Studio": "#99cd00",
"AppCode": "#04dbde",
"Aptana": "#ec8623",
"Atom": "#49b77e",
"Azure Data Studio": "#0271c6",
"Blender": "#fb8007",
"Brackets": "#067dc3",
"Chrome": "#fdd308",
"CLion": "#14c9a5",
"Cloud9": "#25a6d9",
"Coda": "#3e8e1c",
"CodeTasty": "#7368a8",
"DataGrip": "#907cf2",
"DBeaver": "#897363",
"Eclipse": "#443582",
"Emacs": "#8c76c3",
"Eric": "#423f13",
"Excel": "#0f753c",
"Flash Builder": "#aca3a4",
"Gedit": "#872114",
"GoLand": "#bd4ffc",
"HBuilder X": "#1ba334",
"IntelliJ IDEA": "#237ce2",
"IntelliJ": "#237ce2",
"Kakoune": "#dd5f4a",
"Kate": "#3f4040",
"Komodo": "#fcb414",
"Micro": "#2c3494",
"MonoDevelop": "#6185b3",
"NetBeans": "#f1f6e2",
"Notepad++": "#9ecf54",
"Nova": "#ff054a",
"Onivim": "#ee848e",
"PhpStorm": "#d93ac1",
"PowerPoint": "#c6421f",
"Processing": "#6a7152",
"PyCharm": "#d2ee5c",
"Pymakr": "#323d4f",
"Rider": "#f7a415",
"RubyMine": "#ff6336",
"Sketch": "#fdad00",
"SlickEdit": "#57ca57",
"SQL Server Management Studio": "#ffb901",
"Sublime Text": "#ff9800",
"Terminal": "#133f1c",
"TeXstudio": "#652d96",
"TextMate": "#822b7a",
"Unity": "#222d36",
"Vim": "#068304",
"Visual Studio": "#9460cd",
"VS Code": "#027acd",
"VSCode": "#027acd",
"WebStorm": "#00c6d7",
"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"
},
"operating_systems": {
"Linux": "#f0b912",
"Windows": "#00b7ee",
"Mac": "#4d66cb"
} }
} }

View File

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

30
go.mod
View File

@ -3,37 +3,39 @@ module github.com/muety/wakapi
go 1.16 go 1.16
require ( require (
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac
github.com/emersion/go-smtp v0.15.0 github.com/emersion/go-smtp v0.15.0
github.com/emvi/logbuch v1.2.0 github.com/emvi/logbuch v1.2.0
github.com/felixge/httpsnoop v1.0.2 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/getsentry/sentry-go v0.10.0 github.com/getsentry/sentry-go v0.11.0
github.com/go-co-op/gocron v1.5.0 github.com/go-co-op/gocron v1.11.0
github.com/go-openapi/spec v0.20.2 // indirect github.com/go-openapi/spec v0.20.2 // indirect
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.2.0 github.com/gorilla/schema v1.2.0
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/jackc/pgproto3/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru v0.5.4
github.com/jackc/pgx/v4 v4.11.0 // indirect github.com/jackc/pgx/v4 v4.14.1 // indirect
github.com/jinzhu/configor v1.2.1 github.com/jinzhu/configor v1.2.1
github.com/jinzhu/now v1.1.4 // indirect
github.com/leandro-lugaresi/hub v1.1.1 github.com/leandro-lugaresi/hub v1.1.1
github.com/lpar/gzipped/v2 v2.0.2
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/hashstructure/v2 v2.0.1 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/swaggo/swag v1.7.0 github.com/swaggo/swag v1.7.0
go.uber.org/atomic v1.7.0 go.uber.org/atomic v1.9.0
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/tools v0.1.0 // indirect golang.org/x/tools v0.1.0 // indirect
gorm.io/driver/mysql v1.0.6 gorm.io/driver/mysql v1.2.1
gorm.io/driver/postgres v1.0.8 gorm.io/driver/postgres v1.2.3
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.2.6
gorm.io/gorm v1.21.9 gorm.io/gorm v1.22.4
) )

391
go.sum
View File

@ -1,12 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69 h1:/XvI42KX57UTpeIOIt7IfM+pmEFTL8FGtiIUGcGDOIU=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
@ -16,49 +16,19 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 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/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
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/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
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 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 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/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/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
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/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 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/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/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 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-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-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-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/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/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@ -69,48 +39,34 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/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/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8= 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/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 h1:Bw0jQH1Dbs+oIygZBNx/2Ub1igXRFtKQrIMRrZdVFJM=
github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrbQHuc= github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrbQHuc=
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/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= github.com/getsentry/sentry-go v0.11.0 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8=
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-co-op/gocron v1.5.0 h1:tIiwAPwKGcazVFJTNmGe0wE73UpZSEHovoahqGGx9+c= github.com/go-co-op/gocron v1.11.0 h1:ujOMubCpGcTxnnR/9vJIPIEpgwuAjbueAYqJRNr+nHg=
github.com/go-co-op/gocron v1.5.0/go.mod h1:7MgKum7jD7YgIRj7Zx7K1iJKAf1MlSIsEieRl18+KyU= github.com/go-co-op/gocron v1.11.0/go.mod h1:qtlsoMpHlSdIZ3E/xuZzrrAbeX3u5JtPvWf2TcdutU0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
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/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -126,82 +82,39 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= 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 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog=
github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 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-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/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.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/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.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.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/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 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.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= 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/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 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 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 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/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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-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.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
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/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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
@ -215,16 +128,17 @@ 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-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-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 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.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8=
github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 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/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-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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
@ -233,67 +147,57 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod
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-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/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.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.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.7 h1:6Pwi1b3QdY65cuv6SyVO0FgPd5J3Bl7wf/nQQjinHMA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgproto3/v2 v2.2.0/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 h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 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-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-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 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.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0=
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs=
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 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-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.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.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU=
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M=
github.com/jackc/pgx/v4 v4.11.0 h1:J86tSWd3Y7nKjwT/43xZBvpi04keQWx8gNC2YkdJhZI=
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 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 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.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= 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/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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 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/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.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/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d h1:BaIpmhcqpBnz4+NZjUjVGxKNA+/E7ovKsjmwqjXcGYc=
github.com/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d/go.mod h1:3FSWkzk9h42opyV0o357Fq6gsLF/A6MI/qOca9kKobY=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
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.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
@ -307,153 +211,84 @@ github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJ
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 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.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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lpar/gzipped/v2 v2.0.2 h1:y7FjyTH07f8dX0YQ5o0sg2DTbRnmS3oT1pUvxViQ//o=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lpar/gzipped/v2 v2.0.2/go.mod h1:qb7pLOGFgqz5w9xGGiiRFPxuGZ7GqWEuXUKXSbgonkQ=
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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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.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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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.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.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.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.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 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/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
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-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/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
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/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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
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.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/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.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/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/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.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
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 h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 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/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/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 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/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 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.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/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 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 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 v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 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/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/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/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
@ -467,13 +302,10 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= 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 v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@ -483,7 +315,6 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
@ -491,17 +322,12 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
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.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.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.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 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.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -509,48 +335,32 @@ 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.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/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= 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-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-20190308221718-c2843e01d9a2/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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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.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.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 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-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-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/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-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-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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -558,68 +368,50 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-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-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-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-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-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-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-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-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/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-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-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/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-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-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-20190823170909-c4a336ef6a2f/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-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -636,43 +428,18 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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 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-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 h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
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/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.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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -684,19 +451,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.6 h1:mA0XRPjIKi4bkE9nv+NKs6qj6QWOchqUSdWOcpd3x1E= gorm.io/driver/mysql v1.2.1 h1:h+3f1l9Ng2C072Y2tIiLgPpWN78r1KXL7bHJ0nTjlhU=
gorm.io/driver/mysql v1.0.6/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU= gorm.io/driver/mysql v1.2.1/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo=
gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM=
gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E= gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk=
gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
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=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 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=

133
main.go
View File

@ -4,11 +4,16 @@ import (
"embed" "embed"
"io/fs" "io/fs"
"log" "log"
"net"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/lpar/gzipped/v2"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/routes/relay"
"github.com/emvi/logbuch" "github.com/emvi/logbuch"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
@ -16,7 +21,7 @@ import (
"github.com/muety/wakapi/repositories" "github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/routes/api" "github.com/muety/wakapi/routes/api"
"github.com/muety/wakapi/services/mail" "github.com/muety/wakapi/services/mail"
"github.com/muety/wakapi/utils" fsutils "github.com/muety/wakapi/utils/fs"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -52,6 +57,7 @@ var (
projectLabelRepository repositories.IProjectLabelRepository projectLabelRepository repositories.IProjectLabelRepository
summaryRepository repositories.ISummaryRepository summaryRepository repositories.ISummaryRepository
keyValueRepository repositories.IKeyValueRepository keyValueRepository repositories.IKeyValueRepository
diagnosticsRepository repositories.IDiagnosticsRepository
) )
var ( var (
@ -60,11 +66,13 @@ var (
userService services.IUserService userService services.IUserService
languageMappingService services.ILanguageMappingService languageMappingService services.ILanguageMappingService
projectLabelService services.IProjectLabelService projectLabelService services.IProjectLabelService
durationService services.IDurationService
summaryService services.ISummaryService summaryService services.ISummaryService
aggregationService services.IAggregationService aggregationService services.IAggregationService
mailService services.IMailService mailService services.IMailService
keyValueService services.IKeyValueService keyValueService services.IKeyValueService
reportService services.IReportService reportService services.IReportService
diagnosticsService services.IDiagnosticsService
miscService services.IMiscService miscService services.IMiscService
) )
@ -113,9 +121,8 @@ func main() {
// Connect to database // Connect to database
var err error var err error
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger}) db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger})
if config.Db.Dialect == "sqlite3" { if config.Db.IsSQLite() {
db.Raw("PRAGMA foreign_keys = ON;") db.Exec("PRAGMA foreign_keys = ON;")
db.DisableForeignKeyConstraintWhenMigrating = true
} }
if config.IsDev() { if config.IsDev() {
@ -141,24 +148,29 @@ func main() {
projectLabelRepository = repositories.NewProjectLabelRepository(db) projectLabelRepository = repositories.NewProjectLabelRepository(db)
summaryRepository = repositories.NewSummaryRepository(db) summaryRepository = repositories.NewSummaryRepository(db)
keyValueRepository = repositories.NewKeyValueRepository(db) keyValueRepository = repositories.NewKeyValueRepository(db)
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
// Services // Services
mailService = mail.NewMailService()
aliasService = services.NewAliasService(aliasRepository) aliasService = services.NewAliasService(aliasRepository)
userService = services.NewUserService(userRepository) userService = services.NewUserService(mailService, userRepository)
languageMappingService = services.NewLanguageMappingService(languageMappingRepository) languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
projectLabelService = services.NewProjectLabelService(projectLabelRepository) projectLabelService = services.NewProjectLabelService(projectLabelRepository)
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService) heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService, projectLabelService) durationService = services.NewDurationService(heartbeatService)
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService) aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
mailService = mail.NewMailService()
keyValueService = services.NewKeyValueService(keyValueRepository) keyValueService = services.NewKeyValueService(keyValueRepository)
reportService = services.NewReportService(summaryService, userService, mailService) reportService = services.NewReportService(summaryService, userService, mailService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
miscService = services.NewMiscService(userService, summaryService, keyValueService) miscService = services.NewMiscService(userService, summaryService, keyValueService)
// Schedule background tasks // Schedule background tasks
go aggregationService.Schedule() if !config.QuickStart {
go miscService.ScheduleCountTotalTime() go aggregationService.Schedule()
go reportService.Schedule() go miscService.ScheduleCountTotalTime()
go reportService.Schedule()
}
routes.Init() routes.Init()
@ -167,13 +179,17 @@ func main() {
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService) heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService) summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService) metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService)
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
avatarHandler := api.NewAvatarHandler()
// Compat Handlers // Compat Handlers
wakatimeV1StatusBarHandler := wtV1Routes.NewStatusBarHandler(userService, summaryService)
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(userService, summaryService) wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(userService, summaryService)
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(userService, summaryService) wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(userService, summaryService)
wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(userService, summaryService) wakatimeV1StatsHandler := wtV1Routes.NewStatsHandler(userService, summaryService)
wakatimeV1UsersHandler := wtV1Routes.NewUsersHandler(userService, heartbeatService) wakatimeV1UsersHandler := wtV1Routes.NewUsersHandler(userService, heartbeatService)
wakatimeV1ProjectsHandler := wtV1Routes.NewProjectsHandler(userService, heartbeatService) wakatimeV1ProjectsHandler := wtV1Routes.NewProjectsHandler(userService, heartbeatService)
wakatimeV1HeartbeatsHandler := wtV1Routes.NewHeartbeatHandler(userService, heartbeatService)
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService) shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
// MVC Handlers // MVC Handlers
@ -183,14 +199,25 @@ func main() {
loginHandler := routes.NewLoginHandler(userService, mailService) loginHandler := routes.NewLoginHandler(userService, mailService)
imprintHandler := routes.NewImprintHandler(keyValueService) imprintHandler := routes.NewImprintHandler(keyValueService)
// Other Handlers
relayHandler := relay.NewRelayHandler()
// Setup Routers // Setup Routers
router := mux.NewRouter() router := mux.NewRouter()
rootRouter := router.PathPrefix("/").Subrouter() rootRouter := router.PathPrefix("/").Subrouter()
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true) 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 // Globally used middlewares
router.Use(middlewares.NewPrincipalMiddleware()) router.Use(middlewares.NewPrincipalMiddleware())
router.Use(middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets"})) router.Use(middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets", "/api/health"}))
router.Use(handlers.RecoveryHandler()) router.Use(handlers.RecoveryHandler())
if config.Sentry.Dsn != "" { if config.Sentry.Dsn != "" {
router.Use(middlewares.NewSentryMiddleware()) router.Use(middlewares.NewSentryMiddleware())
@ -203,37 +230,61 @@ func main() {
imprintHandler.RegisterRoutes(rootRouter) imprintHandler.RegisterRoutes(rootRouter)
summaryHandler.RegisterRoutes(rootRouter) summaryHandler.RegisterRoutes(rootRouter)
settingsHandler.RegisterRoutes(rootRouter) settingsHandler.RegisterRoutes(rootRouter)
relayHandler.RegisterRoutes(rootRouter)
// API route registrations // API route registrations
summaryApiHandler.RegisterRoutes(apiRouter) summaryApiHandler.RegisterRoutes(apiRouter)
healthApiHandler.RegisterRoutes(apiRouter) healthApiHandler.RegisterRoutes(apiRouter)
heartbeatApiHandler.RegisterRoutes(apiRouter) heartbeatApiHandler.RegisterRoutes(apiRouter)
metricsHandler.RegisterRoutes(apiRouter) metricsHandler.RegisterRoutes(apiRouter)
diagnosticsHandler.RegisterRoutes(apiRouter)
avatarHandler.RegisterRoutes(apiRouter)
wakatimeV1StatusBarHandler.RegisterRoutes(apiRouter)
wakatimeV1AllHandler.RegisterRoutes(apiRouter) wakatimeV1AllHandler.RegisterRoutes(apiRouter)
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter) wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
wakatimeV1StatsHandler.RegisterRoutes(apiRouter) wakatimeV1StatsHandler.RegisterRoutes(apiRouter)
wakatimeV1UsersHandler.RegisterRoutes(apiRouter) wakatimeV1UsersHandler.RegisterRoutes(apiRouter)
wakatimeV1ProjectsHandler.RegisterRoutes(apiRouter) wakatimeV1ProjectsHandler.RegisterRoutes(apiRouter)
wakatimeV1HeartbeatsHandler.RegisterRoutes(apiRouter)
shieldV1BadgeHandler.RegisterRoutes(apiRouter) shieldV1BadgeHandler.RegisterRoutes(apiRouter)
// Static Routes // Static Routes
// https://github.com/golang/go/issues/43431 // https://github.com/golang/go/issues/43431
embeddedStatic, _ := fs.Sub(staticFiles, "static") embeddedStatic, _ := fs.Sub(staticFiles, "static")
static := conf.ChooseFS("static", embeddedStatic) static := conf.ChooseFS("static", embeddedStatic)
fileServer := http.FileServer(utils.NeuteredFileSystem{Fs: http.FS(static)})
router.PathPrefix("/contribute.json").Handler(fileServer) assetsFileServer := gzipped.FileServer(fsutils.NewExistsHttpFS(
router.PathPrefix("/assets").Handler(fileServer) fsutils.NewExistsFS(static).WithCache(!config.IsDev()),
router.PathPrefix("/swagger-ui").Handler(fileServer) ))
staticFileServer := http.FileServer(http.FS(
fsutils.NeuteredFileSystem{FS: static},
))
router.PathPrefix("/contribute.json").Handler(staticFileServer)
router.PathPrefix("/assets").Handler(assetsFileServer)
router.PathPrefix("/swagger-ui").Handler(staticFileServer)
router.PathPrefix("/docs").Handler( router.PathPrefix("/docs").Handler(
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(fileServer), middlewares.NewFileTypeFilterMiddleware([]string{".go"})(staticFileServer),
) )
// Miscellaneous
// Pre-warm projects cache
if !config.IsDev() {
allUsers, err := userService.GetAll()
if err == nil {
logbuch.Info("pre-warming user project cache")
for _, u := range allUsers {
go heartbeatService.GetEntitySetByUser(models.SummaryProject, u)
}
}
}
// Listen HTTP // Listen HTTP
listen(router) listen(router)
} }
func listen(handler http.Handler) { func listen(handler http.Handler) {
var s4, s6 *http.Server var s4, s6, sSocket *http.Server
// IPv4 // IPv4
if config.Server.ListenIpV4 != "" { if config.Server.ListenIpV4 != "" {
@ -241,8 +292,8 @@ func listen(handler http.Handler) {
s4 = &http.Server{ s4 = &http.Server{
Handler: handler, Handler: handler,
Addr: bindString4, Addr: bindString4,
ReadTimeout: 10 * time.Second, ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
} }
} }
@ -252,8 +303,24 @@ func listen(handler http.Handler) {
s6 = &http.Server{ s6 = &http.Server{
Handler: handler, Handler: handler,
Addr: bindString6, Addr: bindString6,
ReadTimeout: 10 * time.Second, ReadTimeout: time.Duration(config.Server.TimeoutSec) * time.Second,
WriteTimeout: 10 * 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,
} }
} }
@ -274,6 +341,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 { } else {
if s4 != nil { if s4 != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", s4.Addr) logbuch.Info("--> Listening for HTTP on %s... ✅", s4.Addr)
@ -291,6 +370,18 @@ 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) <-make(chan interface{}, 1)

View File

@ -1,12 +1,23 @@
package middlewares package middlewares
import ( import (
"fmt"
"net/http"
"strings"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "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 { type AuthenticateMiddleware struct {
@ -45,7 +56,10 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
user, err := m.tryGetUserByCookie(r) user, err := m.tryGetUserByCookie(r)
if err != nil { 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 { if err != nil || user == nil {
@ -58,7 +72,7 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(conf.ErrUnauthorized)) w.Write([]byte(conf.ErrUnauthorized))
} else { } 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) http.Redirect(w, r, m.redirectTarget, http.StatusFound)
} }
return return
@ -77,7 +91,7 @@ func (m *AuthenticateMiddleware) isOptional(requestPath string) bool {
return false 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) key, err := utils.ExtractBearerAuth(r)
if err != nil { if err != nil {
return nil, err return nil, err
@ -92,6 +106,20 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKey(r *http.Request) (*models.Us
return user, nil 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) { func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.User, error) {
username, err := utils.ExtractCookieAuth(r, m.config) username, err := utils.ExtractCookieAuth(r, m.config)
if err != nil { if err != nil {

View File

@ -3,14 +3,16 @@ package middlewares
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net/http"
"net/url"
"testing"
"github.com/muety/wakapi/mocks" "github.com/muety/wakapi/mocks"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http"
"testing"
) )
func TestAuthenticateMiddleware_tryGetUserByApiKey_Success(t *testing.T) { func TestAuthenticateMiddleware_tryGetUserByApiKeyHeader_Success(t *testing.T) {
testApiKey := "z5uig69cn9ut93n" testApiKey := "z5uig69cn9ut93n"
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey)) testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
testUser := &models.User{ApiKey: testApiKey} testUser := &models.User{ApiKey: testApiKey}
@ -26,13 +28,13 @@ func TestAuthenticateMiddleware_tryGetUserByApiKey_Success(t *testing.T) {
sut := NewAuthenticateMiddleware(userServiceMock) sut := NewAuthenticateMiddleware(userServiceMock)
result, err := sut.tryGetUserByApiKey(mockRequest) result, err := sut.tryGetUserByApiKeyHeader(mockRequest)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, testUser, result) assert.Equal(t, testUser, result)
} }
func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) { func TestAuthenticateMiddleware_tryGetUserByApiKeyHeader_Invalid(t *testing.T) {
testApiKey := "z5uig69cn9ut93n" testApiKey := "z5uig69cn9ut93n"
testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey)) testToken := base64.StdEncoding.EncodeToString([]byte(testApiKey))
@ -47,10 +49,55 @@ func TestAuthenticateMiddleware_tryGetUserByApiKey_InvalidHeader(t *testing.T) {
sut := NewAuthenticateMiddleware(userServiceMock) sut := NewAuthenticateMiddleware(userServiceMock)
result, err := sut.tryGetUserByApiKey(mockRequest) result, err := sut.tryGetUserByApiKeyHeader(mockRequest)
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, result) 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 // TODO: somehow test cookie auth function

View File

@ -3,19 +3,30 @@ package relay
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors"
"fmt" "fmt"
"github.com/emvi/logbuch" "github.com/emvi/logbuch"
"github.com/leandro-lugaresi/hub"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/patrickmn/go-cache"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time" "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 { type WakatimeRelayMiddleware struct {
httpClient *http.Client httpClient *http.Client
hashCache *cache.Cache
failureCache *cache.Cache
eventBus *hub.Hub
} }
func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware { func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware {
@ -23,6 +34,9 @@ func NewWakatimeRelayMiddleware() *WakatimeRelayMiddleware {
httpClient: &http.Client{ httpClient: &http.Client{
Timeout: 10 * time.Second, 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,7 +49,10 @@ func (m *WakatimeRelayMiddleware) Handler(h http.Handler) http.Handler {
func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer next(w, r) 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 return
} }
@ -44,10 +61,22 @@ func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
return return
} }
err := m.filterByCache(r)
if err != nil {
logbuch.Warn("%v", err)
return
}
body, _ := ioutil.ReadAll(r.Body) body, _ := ioutil.ReadAll(r.Body)
r.Body.Close() r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
// prevent cycles
downstreamInstanceId := ownInstanceId
if originInstanceId != "" {
downstreamInstanceId = originInstanceId
}
headers := http.Header{ headers := http.Header{
"X-Machine-Name": r.Header.Values("X-Machine-Name"), "X-Machine-Name": r.Header.Values("X-Machine-Name"),
"Content-Type": r.Header.Values("Content-Type"), "Content-Type": r.Header.Values("Content-Type"),
@ -56,23 +85,27 @@ func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
"X-Origin": []string{ "X-Origin": []string{
fmt.Sprintf("wakapi v%s", config.Get().Version), fmt.Sprintf("wakapi v%s", config.Get().Version),
}, },
"X-Origin-Instance": []string{downstreamInstanceId},
"Authorization": []string{ "Authorization": []string{
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user.WakatimeApiKey))), fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user.WakatimeApiKey))),
}, },
} }
url := user.WakaTimeURL(config.WakatimeApiUrl) + config.WakatimeApiHeartbeatsBulkUrl
go m.send( go m.send(
http.MethodPost, http.MethodPost,
config.WakatimeApiUrl+config.WakatimeApiHeartbeatsBulkUrl, url,
bytes.NewReader(body), bytes.NewReader(body),
headers, 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) request, err := http.NewRequest(method, url, body)
if err != nil { if err != nil {
logbuch.Warn("error constructing relayed request %v", err) logbuch.Warn("error constructing relayed request - %v", err)
return return
} }
@ -84,11 +117,74 @@ func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, heade
response, err := m.httpClient.Do(request) response, err := m.httpClient.Do(request)
if err != nil { if err != nil {
logbuch.Warn("error executing relayed request %v", err) logbuch.Warn("error executing relayed request - %v", err)
return return
} }
if response.StatusCode < 200 || response.StatusCode >= 300 { 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

@ -6,7 +6,7 @@ import (
var securityHeaders = map[string]string{ var securityHeaders = map[string]string{
"Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Opener-Policy": "same-origin",
"Content-Security-Policy": "default-src 'self' 'unsafe-inline'; img-src 'self' https: data:; form-action 'self'; block-all-mixed-content;", "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-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff", "X-Content-Type-Options": "nosniff",
} }

View File

@ -31,13 +31,7 @@ func init() {
return nil return nil
} }
condition := "key = ?" if hasRun(name, db) {
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)
return nil return nil
} }
@ -64,13 +58,7 @@ func init() {
} }
} }
if err := db.Create(&models.KeyStringValue{ setHasRun(name, db)
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
return nil return nil
}, },
} }

View File

@ -26,13 +26,7 @@ func init() {
return nil return nil
} }
condition := "key = ?" if hasRun(name, db) {
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)
return nil return nil
} }
@ -43,13 +37,7 @@ func init() {
} }
} }
if err := db.Create(&models.KeyStringValue{ setHasRun(name, db)
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
return nil return nil
}, },
} }

View File

@ -1,9 +1,7 @@
package migrations package migrations
import ( import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -12,13 +10,7 @@ func init() {
f := migrationFunc{ f := migrationFunc{
name: name, name: name,
f: func(db *gorm.DB, cfg *config.Config) error { f: func(db *gorm.DB, cfg *config.Config) error {
condition := "key = ?" if hasRun(name, db) {
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)
return nil return nil
} }
@ -26,13 +18,7 @@ func init() {
return err return err
} }
if err := db.Create(&models.KeyStringValue{ setHasRun(name, db)
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
return nil return nil
}, },
} }

View File

@ -1,9 +1,7 @@
package migrations package migrations
import ( import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -12,13 +10,7 @@ func init() {
f := migrationFunc{ f := migrationFunc{
name: name, name: name,
f: func(db *gorm.DB, cfg *config.Config) error { f: func(db *gorm.DB, cfg *config.Config) error {
condition := "key = ?" if hasRun(name, db) {
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)
return nil return nil
} }
@ -26,13 +18,7 @@ func init() {
return err return err
} }
if err := db.Create(&models.KeyStringValue{ setHasRun(name, db)
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
return nil return nil
}, },
} }

View File

@ -1,7 +1,6 @@
package migrations package migrations
import ( import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
"gorm.io/gorm" "gorm.io/gorm"
@ -13,15 +12,14 @@ func init() {
f := migrationFunc{ f := migrationFunc{
name: name, name: name,
f: func(db *gorm.DB, cfg *config.Config) error { f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
condition := "key = ?" condition := "key = ?"
if cfg.Db.Dialect == config.SQLDialectMysql { if cfg.Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?" 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 nil
}
imprintKv := &models.KeyStringValue{Key: "imprint", Value: "no content here"} imprintKv := &models.KeyStringValue{Key: "imprint", Value: "no content here"}
if err := db. if err := db.
@ -32,13 +30,7 @@ func init() {
return err return err
} }
if err := db.Create(&models.KeyStringValue{ setHasRun(name, db)
Key: name,
Value: "done",
}).Error; err != nil {
return err
}
return nil return nil
}, },
} }

View File

@ -11,7 +11,7 @@ func init() {
f := migrationFunc{ f := migrationFunc{
name: name, name: name,
f: func(db *gorm.DB, cfg *config.Config) error { f: func(db *gorm.DB, cfg *config.Config) error {
if err := db.Migrator().DropTable("gorp_migrations"); err != nil { if err := db.Migrator().DropTable("gorp_migrations"); err == nil {
logbuch.Info("dropped table 'gorp_migrations'") logbuch.Info("dropped table 'gorp_migrations'")
} }
return nil return nil

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

@ -46,7 +46,7 @@ func RunPreMigrations(db *gorm.DB, cfg *config.Config) {
for _, m := range preMigrations { for _, m := range preMigrations {
logbuch.Info("potentially running migration '%s'", m.name) logbuch.Info("potentially running migration '%s'", m.name)
if err := m.f(db, cfg); err != nil { 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)
} }
} }
} }
@ -57,7 +57,7 @@ func RunPostMigrations(db *gorm.DB, cfg *config.Config) {
for _, m := range postMigrations { for _, m := range postMigrations {
logbuch.Info("potentially running migration '%s'", m.name) logbuch.Info("potentially running migration '%s'", m.name)
if err := m.f(db, cfg); err != nil { 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

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

@ -24,6 +24,11 @@ func (p *ProjectLabelServiceMock) GetByUserGrouped(s string) (map[string][]*mode
return args.Get(0).(map[string][]*models.ProjectLabel), args.Error(1) 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) { func (p *ProjectLabelServiceMock) Create(l *models.ProjectLabel) (*models.ProjectLabel, error) {
args := p.Called(l) args := p.Called(l)
return args.Get(0).(*models.ProjectLabel), args.Error(1) return args.Get(0).(*models.ProjectLabel), args.Error(1)

View File

@ -39,8 +39,8 @@ func (m *UserServiceMock) GetAllByReports(b bool) ([]*models.User, error) {
return args.Get(0).([]*models.User), args.Error(1) return args.Get(0).([]*models.User), args.Error(1)
} }
func (m *UserServiceMock) GetActive() ([]*models.User, error) { func (m *UserServiceMock) GetActive(b bool) ([]*models.User, error) {
args := m.Called() args := m.Called(b)
return args.Get(0).([]*models.User), args.Error(1) return args.Get(0).([]*models.User), args.Error(1)
} }
@ -74,8 +74,8 @@ func (m *UserServiceMock) ToggleBadges(user *models.User) (*models.User, error)
return args.Get(0).(*models.User), args.Error(1) return args.Get(0).(*models.User), args.Error(1)
} }
func (m *UserServiceMock) SetWakatimeApiKey(user *models.User, s string) (*models.User, error) { func (m *UserServiceMock) SetWakatimeApiCredentials(user *models.User, s1, s2 string) (*models.User, error) {
args := m.Called(user, s) args := m.Called(user, s1, s2)
return args.Get(0).(*models.User), args.Error(1) return args.Get(0).(*models.User), args.Error(1)
} }

View File

@ -1,5 +1,11 @@
package models 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 { type Alias struct {
ID uint `gorm:"primary_key"` ID uint `gorm:"primary_key"`
Type uint8 `gorm:"not null; index:idx_alias_type_key"` Type uint8 `gorm:"not null; index:idx_alias_type_key"`

View File

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

View File

@ -3,7 +3,6 @@ package v1
import ( import (
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"time"
) )
// https://wakatime.com/developers#all_time_since_today // https://wakatime.com/developers#all_time_since_today
@ -27,14 +26,8 @@ type AllTimeRange struct {
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
} }
func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel { func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
var total time.Duration total := summary.TotalTime()
if key := filters.Project; key != "" {
total = summary.TotalTimeByFilters(filters)
} else {
total = summary.TotalTime()
}
return &AllTimeViewModel{ return &AllTimeViewModel{
Data: &AllTimeData{ Data: &AllTimeData{
TotalSeconds: float32(total.Seconds()), TotalSeconds: float32(total.Seconds()),

View File

@ -1,6 +1,9 @@
package v1 package v1
import ( import (
"strconv"
"time"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
) )
@ -12,18 +15,40 @@ type HeartbeatsViewModel struct {
// that is actually required for the import // that is actually required for the import
type HeartbeatEntry struct { type HeartbeatEntry struct {
Id string `json:"id"` Id string `json:"id"`
Branch string `json:"branch"` Branch string `json:"branch"`
Category string `json:"category"` Category string `json:"category"`
Entity string `json:"entity"` Entity string `json:"entity"`
IsWrite bool `json:"is_write"` IsWrite bool `json:"is_write"`
Language string `json:"language"` Language string `json:"language"`
Project string `json:"project"` Project string `json:"project"`
Time models.CustomTime `json:"time"` Time float64 `json:"time"`
Type string `json:"type"` Type string `json:"type"`
UserId string `json:"user_id"` UserId string `json:"user_id"`
MachineNameId string `json:"machine_name_id"` MachineNameId string `json:"machine_name_id"`
UserAgentId string `json:"user_agent_id"` UserAgentId string `json:"user_agent_id"`
CreatedAt models.CustomTime `json:"created_at"` CreatedAt time.Time `json:"created_at"`
ModifiedAt models.CustomTime `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 // https://wakatime.com/api/v1/users/current/machine_names
type MachineViewModel struct { type MachineViewModel struct {
Data []*MachineEntry `json:"data"` Data []*MachineEntry `json:"data"`
TotalPages int `json:"total_pages"`
} }
type MachineEntry struct { type MachineEntry struct {

View File

@ -26,6 +26,7 @@ type StatsData struct {
Machines []*SummariesEntry `json:"machines"` Machines []*SummariesEntry `json:"machines"`
Projects []*SummariesEntry `json:"projects"` Projects []*SummariesEntry `json:"projects"`
OperatingSystems []*SummariesEntry `json:"operating_systems"` OperatingSystems []*SummariesEntry `json:"operating_systems"`
Branches []*SummariesEntry `json:"branches,omitempty"`
} }
func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewModel { func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewModel {
@ -71,11 +72,21 @@ func NewStatsFrom(summary *models.Summary, filters *models.Filters) *StatsViewMo
oss[i] = convertEntry(e, summary.TotalTimeBy(models.SummaryOS)) 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.Editors = editors
data.Languages = languages data.Languages = languages
data.Machines = machines data.Machines = machines
data.Projects = projects data.Projects = projects
data.OperatingSystems = oss data.OperatingSystems = oss
data.Branches = branches
if summary.Branches == nil {
data.Branches = nil
}
return &StatsViewModel{ return &StatsViewModel{
Data: data, Data: data,

View File

@ -26,6 +26,7 @@ type SummariesData struct {
Machines []*SummariesEntry `json:"machines"` Machines []*SummariesEntry `json:"machines"`
OperatingSystems []*SummariesEntry `json:"operating_systems"` OperatingSystems []*SummariesEntry `json:"operating_systems"`
Projects []*SummariesEntry `json:"projects"` Projects []*SummariesEntry `json:"projects"`
Branches []*SummariesEntry `json:"branches,omitempty"`
GrandTotal *SummariesGrandTotal `json:"grand_total"` GrandTotal *SummariesGrandTotal `json:"grand_total"`
Range *SummariesRange `json:"range"` Range *SummariesRange `json:"range"`
} }
@ -57,8 +58,7 @@ type SummariesRange struct {
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
} }
func NewSummariesFrom(summaries []*models.Summary, filters *models.Filters) *SummariesViewModel { func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
// TODO: implement filtering (https://github.com/muety/wakapi/issues/58)
data := make([]*SummariesData, len(summaries)) data := make([]*SummariesData, len(summaries))
minDate, maxDate := time.Now().Add(1*time.Second), time.Time{} minDate, maxDate := time.Now().Add(1*time.Second), time.Time{}
@ -93,6 +93,7 @@ func newDataFrom(s *models.Summary) *SummariesData {
Machines: make([]*SummariesEntry, len(s.Machines)), Machines: make([]*SummariesEntry, len(s.Machines)),
OperatingSystems: make([]*SummariesEntry, len(s.OperatingSystems)), OperatingSystems: make([]*SummariesEntry, len(s.OperatingSystems)),
Projects: make([]*SummariesEntry, len(s.Projects)), Projects: make([]*SummariesEntry, len(s.Projects)),
Branches: make([]*SummariesEntry, len(s.Branches)),
GrandTotal: &SummariesGrandTotal{ GrandTotal: &SummariesGrandTotal{
Digital: fmt.Sprintf("%d:%d", totalHrs, totalMins), Digital: fmt.Sprintf("%d:%d", totalHrs, totalMins),
Hours: totalHrs, Hours: totalHrs,
@ -110,7 +111,7 @@ func newDataFrom(s *models.Summary) *SummariesData {
} }
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(5) wg.Add(6)
go func(data *SummariesData) { go func(data *SummariesData) {
defer wg.Done() defer wg.Done()
@ -147,6 +148,17 @@ func newDataFrom(s *models.Summary) *SummariesData {
} }
}(data) }(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() wg.Wait()
return data return data
} }

View File

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

13
models/diagnostics.go Normal file
View File

@ -0,0 +1,13 @@
package models
type Diagnostics struct {
ID uint `gorm:"primary_key"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
UserID string `json:"-" gorm:"not null; index:idx_diagnostics_user"`
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,50 +1,180 @@
package models package models
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/mitchellh/hashstructure/v2"
)
type Filters struct { type Filters struct {
Project string Project OrFilter
OS string OS OrFilter
Language string Language OrFilter
Editor string Editor OrFilter
Machine string Machine OrFilter
Label string 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 FilterElement struct {
Type uint8 entity uint8
Key string filter OrFilter
} }
func NewFiltersWith(entity uint8, key string) *Filters { func NewFiltersWith(entity uint8, key string) *Filters {
switch entity { return NewFilterWithMultiple(entity, []string{key})
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}
case SummaryLabel:
return &Filters{Label: key}
}
return &Filters{}
} }
func (f *Filters) One() (bool, uint8, string) { func NewFilterWithMultiple(entity uint8, keys []string) *Filters {
if f.Project != "" { filters := &Filters{}
return true, SummaryProject, f.Project return filters.WithMultiple(entity, keys)
} else if f.OS != "" { }
return true, SummaryOS, f.OS
} else if f.Language != "" { func (f *Filters) With(entity uint8, key string) *Filters {
return true, SummaryLanguage, f.Language return f.WithMultiple(entity, []string{key})
} else if f.Editor != "" { }
return true, SummaryEditor, f.Editor
} else if f.Machine != "" { func (f *Filters) WithMultiple(entity uint8, keys []string) *Filters {
return true, SummaryMachine, f.Machine switch entity {
} else if f.Label != "" { case SummaryProject:
return true, SummaryLabel, f.Label f.Project = append(f.Project, keys...)
} case SummaryOS:
return false, 0, "" 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{}
}
func (f *Filters) IsEmpty() bool {
nonEmpty, _, _ := f.One()
return !nonEmpty
}
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

@ -9,7 +9,7 @@ import (
) )
type Heartbeat struct { type Heartbeat struct {
ID uint `gorm:"primary_key" hash:"ignore"` ID uint64 `gorm:"primary_key" hash:"ignore"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" 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"` UserID string `json:"-" gorm:"not null; index:idx_time_user"`
Entity string `json:"entity" gorm:"not null; index:idx_entity"` Entity string `json:"entity" gorm:"not null; index:idx_entity"`
@ -22,6 +22,7 @@ type Heartbeat struct {
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime 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 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 Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
UserAgent string `json:"user_agent" hash:"ignore"`
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"` Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"` Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
Origin string `json:"-" hash:"ignore"` Origin string `json:"-" hash:"ignore"`
@ -55,6 +56,8 @@ func (h *Heartbeat) GetKey(t uint8) (key string) {
key = h.OperatingSystem key = h.OperatingSystem
case SummaryMachine: case SummaryMachine:
key = h.Machine key = h.Machine
case SummaryBranch:
key = h.Branch
} }
if key == "" { if key == "" {
@ -91,7 +94,7 @@ func (h *Heartbeat) String() string {
func (h *Heartbeat) Hashed() *Heartbeat { func (h *Heartbeat) Hashed() *Heartbeat {
hash, err := hashstructure.Hash(h, hashstructure.FormatV2, nil) hash, err := hashstructure.Hash(h, hashstructure.FormatV2, nil)
if err != 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" h.Hash = fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
return h return h

View File

@ -1,5 +1,8 @@
package models package models
// ProjectLabelReverseResolver returns all projects for a given label
type ProjectLabelReverseResolver func(l string) []string
type ProjectLabel struct { type ProjectLabel struct {
ID uint `json:"id" gorm:"primary_key"` ID uint `json:"id" gorm:"primary_key"`
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`

View File

@ -14,6 +14,7 @@ const (
SummaryOS uint8 = 3 SummaryOS uint8 = 3
SummaryMachine uint8 = 4 SummaryMachine uint8 = 4
SummaryLabel uint8 = 5 SummaryLabel uint8 = 5
SummaryBranch uint8 = 6
) )
const UnknownSummaryKey = "unknown" const UnknownSummaryKey = "unknown"
@ -30,13 +31,15 @@ type Summary struct {
Editors SummaryItems `json:"editors" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Editors SummaryItems `json:"editors" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
OperatingSystems SummaryItems `json:"operating_systems" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` OperatingSystems SummaryItems `json:"operating_systems" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Machines SummaryItems `json:"machines" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Machines SummaryItems `json:"machines" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Labels SummaryItems `json:"labels" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Labels SummaryItems `json:"labels" gorm:"-"` // labels are not persisted, but calculated at runtime, i.e. when summary is retrieved
Branches SummaryItems `json:"branches" gorm:"-"` // branches are not persisted, but calculated at runtime in case a project filter is applied
NumHeartbeats int `json:"-" gorm:"default:0"`
} }
type SummaryItems []*SummaryItem type SummaryItems []*SummaryItem
type SummaryItem struct { type SummaryItem struct {
ID uint `json:"-" gorm:"primary_key"` ID uint64 `json:"-" gorm:"primary_key"`
Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
SummaryID uint `json:"-"` SummaryID uint `json:"-"`
Type uint8 `json:"-" gorm:"index:idx_type"` Type uint8 `json:"-" gorm:"index:idx_type"`
@ -49,33 +52,23 @@ type SummaryItemContainer struct {
Items []*SummaryItem Items []*SummaryItem
} }
type SummaryViewModel struct {
*Summary
*SummaryParams
User *User
LanguageColors map[string]string
EditorColors map[string]string
OSColors map[string]string
Error string
Success string
ApiKey string
RawQuery string
}
type SummaryParams struct { type SummaryParams struct {
From time.Time From time.Time
To time.Time To time.Time
User *User User *User
Filters *Filters
Recompute bool Recompute bool
} }
type AliasResolver func(t uint8, k string) string
func SummaryTypes() []uint8 { func SummaryTypes() []uint8 {
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine, SummaryLabel} return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine, SummaryLabel, SummaryBranch}
} }
func NativeSummaryTypes() []uint8 { func NativeSummaryTypes() []uint8 {
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine, SummaryBranch}
}
func PersistedSummaryTypes() []uint8 {
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine} return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine}
} }
@ -86,6 +79,7 @@ func (s *Summary) Sorted() *Summary {
sort.Sort(sort.Reverse(s.Languages)) sort.Sort(sort.Reverse(s.Languages))
sort.Sort(sort.Reverse(s.Editors)) sort.Sort(sort.Reverse(s.Editors))
sort.Sort(sort.Reverse(s.Labels)) sort.Sort(sort.Reverse(s.Labels))
sort.Sort(sort.Reverse(s.Branches))
return s return s
} }
@ -101,6 +95,7 @@ func (s *Summary) MappedItems() map[uint8]*SummaryItems {
SummaryOS: &s.OperatingSystems, SummaryOS: &s.OperatingSystems,
SummaryMachine: &s.Machines, SummaryMachine: &s.Machines,
SummaryLabel: &s.Labels, SummaryLabel: &s.Labels,
SummaryBranch: &s.Branches,
} }
} }
@ -216,16 +211,41 @@ func (s *Summary) TotalTimeByKey(entityType uint8, key string) (timeSum time.Dur
return timeSum return timeSum
} }
func (s *Summary) TotalTimeByFilters(filters *Filters) time.Duration { func (s *Summary) TotalTimeByFilter(filter FilterElement) time.Duration {
do, typeId, key := filters.One() var total time.Duration
if do { for _, f := range filter.filter {
return s.TotalTimeByKey(typeId, key) total += s.TotalTimeByKey(filter.entity, f)
} }
return 0 return total
}
func (s *Summary) MaxBy(entityType uint8) *SummaryItem {
var max *SummaryItem
mappedItems := s.MappedItems()
if items := mappedItems[entityType]; len(*items) > 0 {
for _, item := range *items {
if max == nil || item.Total > max.Total {
max = item
}
}
}
return max
}
func (s *Summary) MaxByToString(entityType uint8) string {
max := s.MaxBy(entityType)
if max == nil {
return "-"
}
return max.Key
} }
func (s *Summary) WithResolvedAliases(resolve AliasResolver) *Summary { func (s *Summary) WithResolvedAliases(resolve AliasResolver) *Summary {
processAliases := func(origin []*SummaryItem) []*SummaryItem { processAliases := func(origin []*SummaryItem) []*SummaryItem {
if origin == nil {
return nil
}
target := make([]*SummaryItem, 0) target := make([]*SummaryItem, 0)
findItem := func(key string) *SummaryItem { findItem := func(key string) *SummaryItem {
@ -271,6 +291,7 @@ func (s *Summary) WithResolvedAliases(resolve AliasResolver) *Summary {
s.OperatingSystems = processAliases(s.OperatingSystems) s.OperatingSystems = processAliases(s.OperatingSystems)
s.Machines = processAliases(s.Machines) s.Machines = processAliases(s.Machines)
s.Labels = processAliases(s.Labels) s.Labels = processAliases(s.Labels)
s.Branches = processAliases(s.Branches)
return s return s
} }
@ -284,6 +305,26 @@ func (s *Summary) findFirstPresentType() (uint8, error) {
return 127, errors.New("no type present") return 127, errors.New("no type present")
} }
func (s *SummaryParams) HasFilters() bool {
return s.Filters != nil && !s.Filters.IsEmpty()
}
func (s *SummaryParams) IsProjectDetails() bool {
if !s.HasFilters() {
return false
}
_, entity, filters := s.Filters.One()
return entity == SummaryProject && len(filters) == 1 // exactly one
}
func (s *SummaryParams) GetProjectFilter() string {
if !s.IsProjectDetails() {
return ""
}
_, _, filters := s.Filters.One()
return filters[0]
}
func (s *SummaryItem) TotalFixed() time.Duration { func (s *SummaryItem) TotalFixed() time.Duration {
// this is a workaround, since currently, the total time of a summary item is mistakenly represented in seconds // 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 // TODO: fix some day, while migrating persisted summary items

View File

@ -98,20 +98,13 @@ func TestSummary_TotalTimeByFilters(t *testing.T) {
}, },
} }
// Specifying filters about multiple entites is not supported at the moment filters1 := NewFiltersWith(SummaryProject, "wakapi").OneOrEmpty()
// as the current, very rudimentary, time calculation logic wouldn't make sense then. filters2 := NewFiltersWith(SummaryLanguage, "Go").OneOrEmpty()
// Evaluating a filter like (project="wakapi", language="go") can only be realized filters3 := FilterElement{}
// before computing the summary in the first place, because afterwards we can't know
// what time coded in "Go" was in the "Wakapi" project
// See https://github.com/muety/wakapi/issues/108
filters1 := &Filters{Project: "wakapi"} assert.Equal(t, testDuration1, sut.TotalTimeByFilter(filters1))
filters2 := &Filters{Language: "Go"} assert.Equal(t, testDuration3, sut.TotalTimeByFilter(filters2))
filters3 := &Filters{} assert.Zero(t, sut.TotalTimeByFilter(filters3))
assert.Equal(t, testDuration1, sut.TotalTimeByFilters(filters1))
assert.Equal(t, testDuration3, sut.TotalTimeByFilters(filters2))
assert.Zero(t, sut.TotalTimeByFilters(filters3))
} }
func TestSummary_WithResolvedAliases(t *testing.T) { func TestSummary_WithResolvedAliases(t *testing.T) {

View File

@ -1,7 +1,10 @@
package models package models
import ( import (
"crypto/md5"
"fmt"
"regexp" "regexp"
"strings"
"time" "time"
) )
@ -26,7 +29,8 @@ type User struct {
ShareLabels bool `json:"-" gorm:"default:false; type:bool"` ShareLabels bool `json:"-" gorm:"default:false; type:bool"`
IsAdmin bool `json:"-" gorm:"default:false; type:bool"` IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
HasData bool `json:"-" gorm:"default:false; type:bool"` HasData bool `json:"-" gorm:"default:false; type:bool"`
WakatimeApiKey string `json:"-"` WakatimeApiKey string `json:"-"` // for relay middleware and imports
WakatimeApiUrl string `json:"-"` // for relay middleware and imports
ResetToken string `json:"-"` ResetToken string `json:"-"`
ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"` ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"`
} }
@ -87,11 +91,33 @@ func (u *User) TZ() *time.Location {
return tz return tz
} }
// TZOffset returns the time difference between the user's current time zone and UTC
// TODO: is this actually working??
func (u *User) TZOffset() time.Duration { func (u *User) TZOffset() time.Duration {
_, offset := time.Now().In(u.TZ()).Zone() _, offset := time.Now().In(u.TZ()).Zone()
return time.Duration(offset * int(time.Second)) return time.Duration(offset * int(time.Second))
} }
func (u *User) AvatarURL(urlTemplate string) string {
urlTemplate = strings.ReplaceAll(urlTemplate, "{username}", u.ID)
urlTemplate = strings.ReplaceAll(urlTemplate, "{email}", u.Email)
if strings.Contains(urlTemplate, "{username_hash}") {
urlTemplate = strings.ReplaceAll(urlTemplate, "{username_hash}", fmt.Sprintf("%x", md5.Sum([]byte(u.ID))))
}
if strings.Contains(urlTemplate, "{email_hash}") {
urlTemplate = strings.ReplaceAll(urlTemplate, "{email_hash}", fmt.Sprintf("%x", md5.Sum([]byte(u.Email))))
}
return urlTemplate
}
// WakaTimeURL returns the user's effective WakaTime URL, i.e. a custom one (which could also point to another Wakapi instance) or fallback if not specified otherwise.
func (u *User) WakaTimeURL(fallback string) string {
if u.WakatimeApiUrl != "" {
return strings.TrimSuffix(u.WakatimeApiUrl, "/")
}
return fallback
}
func (c *CredentialsReset) IsValid() bool { func (c *CredentialsReset) IsValid() bool {
return ValidatePassword(c.PasswordNew) && return ValidatePassword(c.PasswordNew) &&
c.PasswordNew == c.PasswordRepeat c.PasswordNew == c.PasswordRepeat

View File

@ -9,11 +9,12 @@ import (
func TestUser_TZ(t *testing.T) { func TestUser_TZ(t *testing.T) {
sut1, sut2 := &User{Location: ""}, &User{Location: "America/Los_Angeles"} sut1, sut2 := &User{Location: ""}, &User{Location: "America/Los_Angeles"}
pst, _ := time.LoadLocation("America/Los_Angeles") pst, _ := time.LoadLocation("America/Los_Angeles")
_, offset := time.Now().Zone() _, offset1 := time.Now().Zone()
_, offset2 := time.Now().In(pst).Zone()
assert.Equal(t, time.Local, sut1.TZ()) assert.Equal(t, time.Local, sut1.TZ())
assert.Equal(t, pst, sut2.TZ()) assert.Equal(t, pst, sut2.TZ())
assert.InDelta(t, time.Duration(offset*int(time.Second)), sut1.TZOffset(), float64(1*time.Second)) assert.InDelta(t, time.Duration(offset1*int(time.Second)), sut1.TZOffset(), float64(1*time.Second))
assert.InDelta(t, time.Duration(-7*int(time.Hour)), sut2.TZOffset(), float64(1*time.Second)) assert.InDelta(t, time.Duration(offset2*int(time.Second)), sut2.TZOffset(), float64(1*time.Second))
} }

View File

@ -1,9 +1,10 @@
package view package view
type LoginViewModel struct { type LoginViewModel struct {
Success string Success string
Error string Error string
TotalUsers int TotalUsers int
AllowSignup bool
} }
type SetPasswordViewModel struct { type SetPasswordViewModel struct {

View File

@ -8,6 +8,7 @@ type SettingsViewModel struct {
Aliases []*SettingsVMCombinedAlias Aliases []*SettingsVMCombinedAlias
Labels []*SettingsVMCombinedLabel Labels []*SettingsVMCombinedLabel
Projects []string Projects []string
ApiKey string
Success string Success string
Error string Error string
} }

View File

@ -1,8 +1,17 @@
package view package view
import "github.com/muety/wakapi/models"
type SummaryViewModel struct { type SummaryViewModel struct {
Success string *models.Summary
Error string *models.SummaryParams
User *models.User
AvatarURL string
LanguageColors map[string]string
Error string
Success string
ApiKey string
RawQuery string
} }
func (s *SummaryViewModel) WithSuccess(m string) *SummaryViewModel { func (s *SummaryViewModel) WithSuccess(m string) *SummaryViewModel {

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"scripts": {
"build": "npm run build:icons && npm run build:tailwind",
"build:tailwind": "tailwindcss build -i static/assets/css/app.css -o static/assets/css/app.dist.css --minify",
"build:icons": "node scripts/bundle_icons.js",
"build:all:compress": "npm run build && npm run compress",
"watch": "chokidar \"./views/**/*.html\" \"./static/assets/js/**/*.js\" \"./static/assets/css/**/*.css\" -i \"**/vendor/*\" -i \"**/*.dist.*\" -c \"npm run build\"",
"watch:compress": "chokidar \"./views/**/*.html\" \"./static/assets/js/**/*.js\" \"./static/assets/css/**/*.css\" -i \"**/vendor/*\" -i \"**/*.dist.*\" -c \"npm run build:all:compress\"",
"compress": "brotli -f static/assets/css/*.dist.css && brotli -f static/assets/js/*.dist.js"
},
"devDependencies": {
"@iconify/json": "^1.1.444",
"@iconify/json-tools": "^1.0.10",
"chokidar-cli": "^3.0.0",
"tailwindcss": "2.2.19"
},
"dependencies": {}
}

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "3dcc346d-a9a8-4699-8a52-459eb978b382", "_postman_id": "1043ce31-dc5c-4477-a74a-a29a0e1168b0",
"name": "Wakapi", "name": "Wakapi",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
}, },
@ -49,6 +49,50 @@
} }
}, },
"response": [] "response": []
},
{
"name": "Send diagnostics",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Basic {{TOKEN}}",
"type": "text"
},
{
"key": "X-Machine-Name",
"value": "devmachine",
"type": "text"
},
{
"key": "User-Agent",
"value": "wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"platform\": \"unset\",\n \"architecture\": \"unset\",\n \"plugin\": \"\",\n \"cli_version\": \"unset\",\n \"logs\": \"{\\\"caller\\\":\\\"/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:189\\\",\\\"func\\\":\\\"runCmd\\\",\\\"level\\\":\\\"error\\\",\\\"message\\\":\\\"failed to run command: failed to send heartbeat(s) due to api error: failed to send heartbeats via api client: invalid response status from \\\\\\\"https://bin.muetsch.io/n7jnywu/users/current/heartbeats.bulk\\\\\\\". got: 404, want: 201/202. body: \\\\\\\"\\\\\\\"\\\",\\\"now\\\":\\\"2021-08-07T00:33:26+02:00\\\",\\\"version\\\":\\\"unset\\\"}\\n\",\n \"stacktrace\": \"goroutine 1 [running]:\\nruntime/debug.Stack(0x0, 0xc0001f8680, 0x196)\\n\\t/opt/go/src/runtime/debug/stack.go:24 +0x9f\\ngithub.com/wakatime/wakatime-cli/cmd/legacy.runCmd(0xc000103680, 0xc33c60, 0x0)\\n\\t/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:194 +0x26c\\ngithub.com/wakatime/wakatime-cli/cmd/legacy.RunCmdWithOfflineSync(0xc000103680, 0xc33c60)\\n\\t/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:163 +0x35\\ngithub.com/wakatime/wakatime-cli/cmd/legacy.Run(0xc0000be2c0, 0xc000103680)\\n\\t/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:90 +0x62e\\ngithub.com/wakatime/wakatime-cli/cmd.NewRootCMD.func1(0xc0000be2c0, 0xc00028bd40, 0x0, 0x2)\\n\\t/home/ferdinand/dev/wakatime-cli/cmd/root.go:31 +0x34\\ngithub.com/spf13/cobra.(*Command).execute(0xc0000be2c0, 0xc000020190, 0x2, 0x2, 0xc0000be2c0, 0xc000020190)\\n\\t/home/ferdinand/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:854 +0x2c2\\ngithub.com/spf13/cobra.(*Command).ExecuteC(0xc0000be2c0, 0xc000000180, 0xc0006bff78, 0x407d65)\\n\\t/home/ferdinand/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:958 +0x375\\ngithub.com/spf13/cobra.(*Command).Execute(...)\\n\\t/home/ferdinand/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:895\\ngithub.com/wakatime/wakatime-cli/cmd.Execute()\\n\\t/home/ferdinand/dev/wakatime-cli/cmd/root.go:227 +0x2b\\nmain.main()\\n\\t/home/ferdinand/dev/wakatime-cli/main.go:6 +0x25\\n\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{BASE_URL}}/api/plugins/errors",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"plugins",
"errors"
]
}
},
"response": []
} }
] ]
}, },
@ -201,6 +245,41 @@
}, },
"response": [] "response": []
}, },
{
"name": "Get heartbeats",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Basic {{TOKEN}}",
"type": "text"
}
],
"url": {
"raw": "{{BASE_URL}}/api/compat/wakatime/v1/users/current/heartbeats?date=2021-02-10",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"compat",
"wakatime",
"v1",
"users",
"current",
"heartbeats"
],
"query": [
{
"key": "date",
"value": "2021-02-10"
}
]
}
},
"response": []
},
{ {
"name": "Get stats", "name": "Get stats",
"request": { "request": {
@ -298,6 +377,36 @@
} }
}, },
"response": [] "response": []
},
{
"name": "Get statusbar",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Basic {{TOKEN}}",
"type": "text"
}
],
"url": {
"raw": "{{BASE_URL}}/api/compat/wakatime/v1/users/current/statusbar/today",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"compat",
"wakatime",
"v1",
"users",
"current",
"statusbar",
"today"
]
}
},
"response": []
} }
] ]
} }

View File

@ -24,6 +24,9 @@ func (r *AliasRepository) GetAll() ([]*models.Alias, error) {
func (r *AliasRepository) GetByUser(userId string) ([]*models.Alias, error) { func (r *AliasRepository) GetByUser(userId string) ([]*models.Alias, error) {
var aliases []*models.Alias var aliases []*models.Alias
if userId == "" {
return aliases, nil
}
if err := r.db. if err := r.db.
Where(&models.Alias{UserID: userId}). Where(&models.Alias{UserID: userId}).
Find(&aliases).Error; err != nil { Find(&aliases).Error; err != nil {
@ -34,6 +37,9 @@ func (r *AliasRepository) GetByUser(userId string) ([]*models.Alias, error) {
func (r *AliasRepository) GetByUserAndKey(userId, key string) ([]*models.Alias, error) { func (r *AliasRepository) GetByUserAndKey(userId, key string) ([]*models.Alias, error) {
var aliases []*models.Alias var aliases []*models.Alias
if userId == "" {
return aliases, nil
}
if err := r.db. if err := r.db.
Where(&models.Alias{ Where(&models.Alias{
UserID: userId, UserID: userId,
@ -47,6 +53,9 @@ func (r *AliasRepository) GetByUserAndKey(userId, key string) ([]*models.Alias,
func (r *AliasRepository) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) { func (r *AliasRepository) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) {
var aliases []*models.Alias var aliases []*models.Alias
if userId == "" {
return aliases, nil
}
if err := r.db. if err := r.db.
Where(&models.Alias{ Where(&models.Alias{
UserID: userId, UserID: userId,
@ -61,6 +70,9 @@ func (r *AliasRepository) GetByUserAndKeyAndType(userId, key string, summaryType
func (r *AliasRepository) GetByUserAndTypeAndValue(userId string, summaryType uint8, value string) (*models.Alias, error) { func (r *AliasRepository) GetByUserAndTypeAndValue(userId string, summaryType uint8, value string) (*models.Alias, error) {
alias := &models.Alias{} alias := &models.Alias{}
if userId == "" {
return nil, errors.New("invalid input")
}
if err := r.db. if err := r.db.
Where(&models.Alias{ Where(&models.Alias{
UserID: userId, UserID: userId,

View File

@ -0,0 +1,18 @@
package repositories
import (
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
type DiagnosticsRepository struct {
db *gorm.DB
}
func NewDiagnosticsRepository(db *gorm.DB) *DiagnosticsRepository {
return &DiagnosticsRepository{db: db}
}
func (r *DiagnosticsRepository) Insert(diagnostics *models.Diagnostics) (*models.Diagnostics, error) {
return diagnostics, r.db.Create(diagnostics).Error
}

View File

@ -34,6 +34,9 @@ func (r *LanguageMappingRepository) GetById(id uint) (*models.LanguageMapping, e
func (r *LanguageMappingRepository) GetByUser(userId string) ([]*models.LanguageMapping, error) { func (r *LanguageMappingRepository) GetByUser(userId string) ([]*models.LanguageMapping, error) {
var mappings []*models.LanguageMapping var mappings []*models.LanguageMapping
if userId == "" {
return mappings, nil
}
if err := r.db. if err := r.db.
Where(&models.LanguageMapping{UserID: userId}). Where(&models.LanguageMapping{UserID: userId}).
Find(&mappings).Error; err != nil { Find(&mappings).Error; err != nil {

View File

@ -33,6 +33,9 @@ func (r *ProjectLabelRepository) GetById(id uint) (*models.ProjectLabel, error)
} }
func (r *ProjectLabelRepository) GetByUser(userId string) ([]*models.ProjectLabel, error) { func (r *ProjectLabelRepository) GetByUser(userId string) ([]*models.ProjectLabel, error) {
if userId == "" {
return []*models.ProjectLabel{}, nil
}
var labels []*models.ProjectLabel var labels []*models.ProjectLabel
if err := r.db. if err := r.db.
Where(&models.ProjectLabel{UserID: userId}). Where(&models.ProjectLabel{UserID: userId}).

View File

@ -31,6 +31,10 @@ type IHeartbeatRepository interface {
DeleteBefore(time.Time) error DeleteBefore(time.Time) error
} }
type IDiagnosticsRepository interface {
Insert(diagnostics *models.Diagnostics) (*models.Diagnostics, error)
}
type IKeyValueRepository interface { type IKeyValueRepository interface {
GetAll() ([]*models.KeyStringValue, error) GetAll() ([]*models.KeyStringValue, error)
GetString(string) (*models.KeyStringValue, error) GetString(string) (*models.KeyStringValue, error)

View File

@ -23,7 +23,7 @@ func (r *SummaryRepository) GetAll() ([]*models.Summary, error) {
Preload("Editors", "type = ?", models.SummaryEditor). Preload("Editors", "type = ?", models.SummaryEditor).
Preload("OperatingSystems", "type = ?", models.SummaryOS). Preload("OperatingSystems", "type = ?", models.SummaryOS).
Preload("Machines", "type = ?", models.SummaryMachine). Preload("Machines", "type = ?", models.SummaryMachine).
Preload("Labels", "type = ?", models.SummaryLabel). // branch summaries are currently not persisted, as only relevant in combination with project filter
Find(&summaries).Error; err != nil { Find(&summaries).Error; err != nil {
return nil, err return nil, err
} }
@ -49,7 +49,7 @@ func (r *SummaryRepository) GetByUserWithin(user *models.User, from, to time.Tim
Preload("Editors", "type = ?", models.SummaryEditor). Preload("Editors", "type = ?", models.SummaryEditor).
Preload("OperatingSystems", "type = ?", models.SummaryOS). Preload("OperatingSystems", "type = ?", models.SummaryOS).
Preload("Machines", "type = ?", models.SummaryMachine). Preload("Machines", "type = ?", models.SummaryMachine).
Preload("Labels", "type = ?", models.SummaryLabel). // branch summaries are currently not persisted, as only relevant in combination with project filter
Find(&summaries).Error; err != nil { Find(&summaries).Error; err != nil {
return nil, err return nil, err
} }

View File

@ -35,6 +35,9 @@ func (r *UserRepository) GetByIds(userIds []string) ([]*models.User, error) {
} }
func (r *UserRepository) GetByApiKey(key string) (*models.User, error) { func (r *UserRepository) GetByApiKey(key string) (*models.User, error) {
if key == "" {
return nil, errors.New("invalid input")
}
u := &models.User{} u := &models.User{}
if err := r.db.Where(&models.User{ApiKey: key}).First(u).Error; err != nil { if err := r.db.Where(&models.User{ApiKey: key}).First(u).Error; err != nil {
return u, err return u, err
@ -123,16 +126,16 @@ func (r *UserRepository) Count() (int64, error) {
} }
func (r *UserRepository) InsertOrGet(user *models.User) (*models.User, bool, error) { func (r *UserRepository) InsertOrGet(user *models.User) (*models.User, bool, error) {
result := r.db.FirstOrCreate(user, &models.User{ID: user.ID}) if u, err := r.GetById(user.ID); err == nil && u != nil && u.ID != "" {
return u, false, nil
}
result := r.db.Create(user)
if err := result.Error; err != nil { if err := result.Error; err != nil {
return nil, false, err return nil, false, err
} }
if result.RowsAffected == 1 { return user, true, nil
return user, true, nil
}
return user, false, nil
} }
func (r *UserRepository) Update(user *models.User) (*models.User, error) { func (r *UserRepository) Update(user *models.User) (*models.User, error) {
@ -149,6 +152,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
"share_machines": user.ShareMachines, "share_machines": user.ShareMachines,
"share_labels": user.ShareLabels, "share_labels": user.ShareLabels,
"wakatime_api_key": user.WakatimeApiKey, "wakatime_api_key": user.WakatimeApiKey,
"wakatime_api_url": user.WakatimeApiUrl,
"has_data": user.HasData, "has_data": user.HasData,
"reset_token": user.ResetToken, "reset_token": user.ResetToken,
"location": user.Location, "location": user.Location,
@ -160,10 +164,6 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
return nil, err return nil, err
} }
if result.RowsAffected != 1 {
return nil, errors.New("nothing updated")
}
return user, nil return user, nil
} }

45
routes/api/avatar.go Normal file
View File

@ -0,0 +1,45 @@
package api
import (
"codeberg.org/Codeberg/avatars"
"github.com/gorilla/mux"
lru "github.com/hashicorp/golang-lru"
conf "github.com/muety/wakapi/config"
"net/http"
)
type AvatarHandler struct {
config *conf.Config
cache *lru.Cache
}
func NewAvatarHandler() *AvatarHandler {
cache, err := lru.New(1 * 1000 * 64) // assuming an avatar is 1 kb, allocate up to 64 mb of memory for avatars cache
if err != nil {
panic(err)
}
return &AvatarHandler{
config: conf.Get(),
cache: cache,
}
}
func (h *AvatarHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/avatar/{hash}.svg").Subrouter()
r.Path("").Methods(http.MethodGet).HandlerFunc(h.Get)
}
func (h *AvatarHandler) Get(w http.ResponseWriter, r *http.Request) {
hash := mux.Vars(r)["hash"]
if !h.cache.Contains(hash) {
h.cache.Add(hash, avatars.MakeMaleAvatar(hash))
}
data, _ := h.cache.Get(hash)
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "max-age=2592000")
w.WriteHeader(http.StatusOK)
w.Write([]byte(data.(string)))
}

72
routes/api/diagnostics.go Normal file
View File

@ -0,0 +1,72 @@
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"github.com/muety/wakapi/models"
)
type DiagnosticsApiHandler struct {
config *conf.Config
userSrvc services.IUserService
diagnosticsSrvc services.IDiagnosticsService
}
func NewDiagnosticsApiHandler(userService services.IUserService, diagnosticsService services.IDiagnosticsService) *DiagnosticsApiHandler {
return &DiagnosticsApiHandler{
config: conf.Get(),
userSrvc: userService,
diagnosticsSrvc: diagnosticsService,
}
}
func (h *DiagnosticsApiHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/plugins/errors").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
)
r.Path("").Methods(http.MethodPost).HandlerFunc(h.Post)
}
// @Summary Push a new diagnostics object
// @ID post-diagnostics
// @Tags diagnostics
// @Accept json
// @Param diagnostics body models.Diagnostics true "A single diagnostics object sent by WakaTime CLI"
// @Security ApiKeyAuth
// @Success 201
// @Router /plugins/errors [post]
func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
var diagnostics models.Diagnostics
user := middlewares.GetPrincipal(r)
if user == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(conf.ErrUnauthorized))
return
}
if err := json.NewDecoder(r.Body).Decode(&diagnostics); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(conf.ErrBadRequest))
conf.Log().Request(r).Error("failed to parse diagnostics for user %s - %v", err)
return
}
diagnostics.UserID = user.ID
if _, err := h.diagnosticsSrvc.Create(&diagnostics); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(conf.ErrInternalServerError))
conf.Log().Request(r).Error("failed to insert diagnostics for user %s - %v", err)
return
}
utils.RespondJSON(w, r, http.StatusCreated, struct{}{})
}

View File

@ -2,9 +2,10 @@ package api
import ( import (
"fmt" "fmt"
"net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"gorm.io/gorm" "gorm.io/gorm"
"net/http"
) )
type HealthApiHandler struct { type HealthApiHandler struct {

View File

@ -1,8 +1,8 @@
package api package api
import ( import (
"bytes" "net/http"
"encoding/json"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
@ -10,8 +10,6 @@ import (
routeutils "github.com/muety/wakapi/routes/utils" routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"io/ioutil"
"net/http"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
) )
@ -68,17 +66,16 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
} }
var heartbeats []*models.Heartbeat var heartbeats []*models.Heartbeat
heartbeats, err = h.tryParseBulk(r) heartbeats, err = routeutils.ParseHeartbeats(r)
if err != nil { if err != nil {
heartbeats, err = h.tryParseSingle(r) conf.Log().Request(r).Error(err.Error())
if err != nil { w.WriteHeader(http.StatusBadRequest)
w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error()))
w.Write([]byte(err.Error())) return
return
}
} }
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent")) userAgent := r.Header.Get("User-Agent")
opSys, editor, _ := utils.ParseUserAgent(userAgent)
machineName := r.Header.Get("X-Machine-Name") machineName := r.Header.Get("X-Machine-Name")
for _, hb := range heartbeats { for _, hb := range heartbeats {
@ -87,6 +84,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
hb.Machine = machineName hb.Machine = machineName
hb.User = user hb.User = user
hb.UserID = user.ID hb.UserID = user.ID
hb.UserAgent = userAgent
if !hb.Valid() { if !hb.Valid() {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@ -100,7 +98,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
if err := h.heartbeatSrvc.InsertBatch(heartbeats); err != nil { if err := h.heartbeatSrvc.InsertBatch(heartbeats); err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(conf.ErrInternalServerError)) w.Write([]byte(conf.ErrInternalServerError))
conf.Log().Request(r).Error("failed to batch-insert heartbeats %v", err) conf.Log().Request(r).Error("failed to batch-insert heartbeats - %v", err)
return return
} }
@ -109,7 +107,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
if _, err := h.userSrvc.Update(user); err != nil { if _, err := h.userSrvc.Update(user); err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(conf.ErrInternalServerError)) w.Write([]byte(conf.ErrInternalServerError))
conf.Log().Request(r).Error("failed to update user %v", err) conf.Log().Request(r).Error("failed to update user - %v", err)
return return
} }
} }
@ -119,36 +117,6 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats))) utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
} }
func (h *HeartbeatApiHandler) tryParseBulk(r *http.Request) ([]*models.Heartbeat, error) {
var heartbeats []*models.Heartbeat
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
dec := json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
if err := dec.Decode(&heartbeats); err != nil {
return nil, err
}
return heartbeats, nil
}
func (h *HeartbeatApiHandler) tryParseSingle(r *http.Request) ([]*models.Heartbeat, error) {
var heartbeat models.Heartbeat
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
dec := json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
if err := dec.Decode(&heartbeat); err != nil {
return nil, err
}
return []*models.Heartbeat{&heartbeat}, nil
}
// construct weird response format (see https://github.com/wakatime/wakatime/blob/2e636d389bf5da4e998e05d5285a96ce2c181e3d/wakatime/api.py#L288) // construct weird response format (see https://github.com/wakatime/wakatime/blob/2e636d389bf5da4e998e05d5285a96ce2c181e3d/wakatime/api.py#L288)
// to make the cli consider all heartbeats to having been successfully saved // to make the cli consider all heartbeats to having been successfully saved
// response looks like: { "responses": [ [ null, 201 ], ... ] } // response looks like: { "responses": [ [ null, 201 ], ... ] }
@ -177,6 +145,7 @@ func constructSuccessResponse(n int) *heartbeatResponseVm {
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body models.Heartbeat true "A single heartbeat" // @Param heartbeat body models.Heartbeat true "A single heartbeat"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /v1/users/{user}/heartbeats [post] // @Router /v1/users/{user}/heartbeats [post]
@ -187,6 +156,7 @@ func (h *HeartbeatApiHandler) postAlias1() {}
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body models.Heartbeat true "A single heartbeat" // @Param heartbeat body models.Heartbeat true "A single heartbeat"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /compat/wakatime/v1/users/{user}/heartbeats [post] // @Router /compat/wakatime/v1/users/{user}/heartbeats [post]
@ -197,6 +167,7 @@ func (h *HeartbeatApiHandler) postAlias2() {}
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body models.Heartbeat true "A single heartbeat" // @Param heartbeat body models.Heartbeat true "A single heartbeat"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /users/{user}/heartbeats [post] // @Router /users/{user}/heartbeats [post]
@ -217,6 +188,7 @@ func (h *HeartbeatApiHandler) postAlias4() {}
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats" // @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /v1/users/{user}/heartbeats.bulk [post] // @Router /v1/users/{user}/heartbeats.bulk [post]
@ -227,6 +199,7 @@ func (h *HeartbeatApiHandler) postAlias5() {}
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats" // @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /compat/wakatime/v1/users/{user}/heartbeats.bulk [post] // @Router /compat/wakatime/v1/users/{user}/heartbeats.bulk [post]
@ -237,6 +210,7 @@ func (h *HeartbeatApiHandler) postAlias6() {}
// @Tags heartbeat // @Tags heartbeat
// @Accept json // @Accept json
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats" // @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 201 // @Success 201
// @Router /users/{user}/heartbeats.bulk [post] // @Router /users/{user}/heartbeats.bulk [post]

View File

@ -12,6 +12,7 @@ import (
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http" "net/http"
"runtime"
"sort" "sort"
"time" "time"
) )
@ -34,6 +35,10 @@ const (
DescAdminUserHeartbeats = "Total number of tracked heartbeats by user (all time)." DescAdminUserHeartbeats = "Total number of tracked heartbeats by user (all time)."
DescAdminTotalUsers = "Total number of registered users." DescAdminTotalUsers = "Total number of registered users."
DescAdminActiveUsers = "Number of active users." DescAdminActiveUsers = "Number of active users."
DescMemAllocTotal = "Total number of bytes allocated for heap"
DescMemSysTotal = "Total number of bytes obtained from the OS"
DescGoroutines = "Total number of running goroutines"
) )
type MetricsHandler struct { type MetricsHandler struct {
@ -111,7 +116,7 @@ func (h *MetricsHandler) Get(w http.ResponseWriter, r *http.Request) {
func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error) { func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error) {
var metrics mm.Metrics var metrics mm.Metrics
summaryAllTime, err := h.summarySrvc.Aliased(time.Time{}, time.Now(), user, h.summarySrvc.Retrieve, false) summaryAllTime, err := h.summarySrvc.Aliased(time.Time{}, time.Now(), user, h.summarySrvc.Retrieve, nil, false)
if err != nil { if err != nil {
logbuch.Error("failed to retrieve all time summary for user '%s' for metric", user.ID) logbuch.Error("failed to retrieve all time summary for user '%s' for metric", user.ID)
return nil, err return nil, err
@ -119,7 +124,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
from, to := utils.MustResolveIntervalRawTZ("today", user.TZ()) from, to := utils.MustResolveIntervalRawTZ("today", user.TZ())
summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve, false) summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve, nil, false)
if err != nil { if err != nil {
logbuch.Error("failed to retrieve today's summary for user '%s' for metric", user.ID) logbuch.Error("failed to retrieve today's summary for user '%s' for metric", user.ID)
return nil, err return nil, err
@ -136,7 +141,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
metrics = append(metrics, &mm.CounterMetric{ metrics = append(metrics, &mm.CounterMetric{
Name: MetricsPrefix + "_cumulative_seconds_total", Name: MetricsPrefix + "_cumulative_seconds_total",
Desc: DescAllTime, Desc: DescAllTime,
Value: int(v1.NewAllTimeFrom(summaryAllTime, &models.Filters{}).Data.TotalSeconds), Value: int(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
Labels: []mm.Label{}, Labels: []mm.Label{},
}) })
@ -208,6 +213,31 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
}) })
} }
// Runtime metrics
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
metrics = append(metrics, &mm.CounterMetric{
Name: MetricsPrefix + "_goroutines_total",
Desc: DescGoroutines,
Value: runtime.NumGoroutine(),
Labels: []mm.Label{},
})
metrics = append(metrics, &mm.CounterMetric{
Name: MetricsPrefix + "_mem_alloc_total",
Desc: DescMemAllocTotal,
Value: int(memStats.Alloc),
Labels: []mm.Label{},
})
metrics = append(metrics, &mm.CounterMetric{
Name: MetricsPrefix + "_mem_sys_total",
Desc: DescMemSysTotal,
Value: int(memStats.Sys),
Labels: []mm.Label{},
})
return &metrics, nil return &metrics, nil
} }
@ -228,9 +258,9 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
totalUsers, _ := h.userSrvc.Count() totalUsers, _ := h.userSrvc.Count()
totalHeartbeats, _ := h.heartbeatSrvc.Count() totalHeartbeats, _ := h.heartbeatSrvc.Count()
activeUsers, err := h.userSrvc.GetActive() activeUsers, err := h.userSrvc.GetActive(false)
if err != nil { if err != nil {
logbuch.Error("failed to retrieve active users for metric %v", err) logbuch.Error("failed to retrieve active users for metric - %v", err)
return nil, err return nil, err
} }

View File

@ -1,13 +1,14 @@
package api package api
import ( import (
routeutils "github.com/muety/wakapi/routes/utils"
"net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
su "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http"
) )
type SummaryApiHandler struct { type SummaryApiHandler struct {
@ -40,11 +41,17 @@ func (h *SummaryApiHandler) RegisterRoutes(router *mux.Router) {
// @Param from query string false "Start date (e.g. '2021-02-07')" // @Param from query string false "Start date (e.g. '2021-02-07')"
// @Param to query string false "End date (e.g. '2021-02-08')" // @Param to query string false "End date (e.g. '2021-02-08')"
// @Param recompute query bool false "Whether to recompute the summary from raw heartbeat or use cache" // @Param recompute query bool false "Whether to recompute the summary from raw heartbeat or use cache"
// @Param project query string false "Project to filter by"
// @Param language query string false "Language to filter by"
// @Param editor query string false "Editor to filter by"
// @Param operating_system query string false "OS to filter by"
// @Param machine query string false "Machine to filter by"
// @Param label query string false "Project label to filter by"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 200 {object} models.Summary // @Success 200 {object} models.Summary
// @Router /summary [get] // @Router /summary [get]
func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) { func (h *SummaryApiHandler) Get(w http.ResponseWriter, r *http.Request) {
summary, err, status := su.LoadUserSummary(h.summarySrvc, r) summary, err, status := routeutils.LoadUserSummary(h.summarySrvc, r)
if err != nil { if err != nil {
w.WriteHeader(status) w.WriteHeader(status)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))

View File

@ -2,6 +2,10 @@ package v1
import ( import (
"fmt" "fmt"
"net/http"
"regexp"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
@ -9,14 +13,11 @@ import (
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"net/http"
"regexp"
"time"
) )
const ( const (
intervalPattern = `interval:([a-z0-9_]+)` intervalPattern = `interval:([a-z0-9_]+)`
entityFilterPattern = `(project|os|editor|language|machine):([_a-zA-Z0-9-\s]+)` entityFilterPattern = `(project|os|editor|language|machine|label):([_a-zA-Z0-9-\s\.]+)`
) )
type BadgeHandler struct { type BadgeHandler struct {
@ -104,6 +105,7 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
case "label": case "label":
permitEntity = user.ShareLabels permitEntity = user.ShareLabels
filters = models.NewFiltersWith(models.SummaryLabel, filterKey) filters = models.NewFiltersWith(models.SummaryLabel, filterKey)
// branches are intentionally omitted here, as only relevant in combination with a project filter
default: default:
permitEntity = true permitEntity = true
filters = &models.Filters{} filters = &models.Filters{}
@ -121,19 +123,19 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
return return
} }
summary, err, status := h.loadUserSummary(user, interval) summary, err, status := h.loadUserSummary(user, interval, filters)
if err != nil { if err != nil {
w.WriteHeader(status) w.WriteHeader(status)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))
return return
} }
vm := v1.NewBadgeDataFrom(summary, filters) vm := v1.NewBadgeDataFrom(summary)
h.cache.SetDefault(cacheKey, vm) h.cache.SetDefault(cacheKey, vm)
utils.RespondJSON(w, r, http.StatusOK, vm) utils.RespondJSON(w, r, http.StatusOK, vm)
} }
func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.IntervalKey) (*models.Summary, error, int) { func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.IntervalKey, filters *models.Filters) (*models.Summary, error, int) {
err, from, to := utils.ResolveIntervalTZ(interval, user.TZ()) err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
if err != nil { if err != nil {
return nil, err, http.StatusBadRequest return nil, err, http.StatusBadRequest
@ -150,7 +152,14 @@ func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.Inter
retrieveSummary = h.summarySrvc.Summarize retrieveSummary = h.summarySrvc.Summarize
} }
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute) summary, err := h.summarySrvc.Aliased(
summaryParams.From,
summaryParams.To,
summaryParams.User,
retrieveSummary,
filters,
summaryParams.Recompute,
)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -0,0 +1,43 @@
package v1
import (
"github.com/stretchr/testify/assert"
"regexp"
"testing"
)
func TestBadgeHandler_EntityPattern(t *testing.T) {
type test struct {
test string
key string
val string
}
pathPrefix := "/compat/shields/v1/current/today/"
tests := []test{
{test: pathPrefix + "project:wakapi", key: "project", val: "wakapi"},
{test: pathPrefix + "os:Linux", key: "os", val: "Linux"},
{test: pathPrefix + "editor:VSCode", key: "editor", val: "VSCode"},
{test: pathPrefix + "language:Java", key: "language", val: "Java"},
{test: pathPrefix + "machine:devmachine", key: "machine", val: "devmachine"},
{test: pathPrefix + "label:work", key: "label", val: "work"},
{test: pathPrefix + "foo:bar", key: "", val: ""}, // invalid entity
{test: pathPrefix + "project:01234", key: "project", val: "01234"}, // digits only
{test: pathPrefix + "project:anchr-web-ext", key: "project", val: "anchr-web-ext"}, // with dashes
{test: pathPrefix + "project:wakapi v2", key: "project", val: "wakapi v2"}, // with blank space
{test: pathPrefix + "project:project", key: "project", val: "project"},
{test: pathPrefix + "project:Anchr-Android_v2.0", key: "project", val: "Anchr-Android_v2.0"}, // all the way
}
sut := regexp.MustCompile(entityFilterPattern)
for _, tc := range tests {
var key, val string
if groups := sut.FindStringSubmatch(tc.test); len(groups) > 2 {
key, val = groups[1], groups[2]
}
assert.Equal(t, tc.key, key)
assert.Equal(t, tc.val, val)
}
}

View File

@ -10,7 +10,6 @@ import (
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http" "net/http"
"net/url"
"time" "time"
) )
@ -46,25 +45,23 @@ func (h *AllTimeHandler) RegisterRoutes(router *mux.Router) {
// @Success 200 {object} v1.AllTimeViewModel // @Success 200 {object} v1.AllTimeViewModel
// @Router /compat/wakatime/v1/users/{user}/all_time_since_today [get] // @Router /compat/wakatime/v1/users/{user}/all_time_since_today [get]
func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) { func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
values, _ := url.ParseQuery(r.URL.RawQuery)
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current") user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil { if err != nil {
return // response was already sent by util function return // response was already sent by util function
} }
summary, err, status := h.loadUserSummary(user) summary, err, status := h.loadUserSummary(user, utils.ParseSummaryFilters(r))
if err != nil { if err != nil {
w.WriteHeader(status) w.WriteHeader(status)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))
return return
} }
vm := v1.NewAllTimeFrom(summary, models.NewFiltersWith(models.SummaryProject, values.Get("project"))) vm := v1.NewAllTimeFrom(summary)
utils.RespondJSON(w, r, http.StatusOK, vm) utils.RespondJSON(w, r, http.StatusOK, vm)
} }
func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, error, int) { func (h *AllTimeHandler) loadUserSummary(user *models.User, filters *models.Filters) (*models.Summary, error, int) {
summaryParams := &models.SummaryParams{ summaryParams := &models.SummaryParams{
From: time.Time{}, From: time.Time{},
To: time.Now(), To: time.Now(),
@ -77,7 +74,14 @@ func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, er
retrieveSummary = h.summarySrvc.Summarize retrieveSummary = h.summarySrvc.Summarize
} }
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute) summary, err := h.summarySrvc.Aliased(
summaryParams.From,
summaryParams.To,
summaryParams.User,
retrieveSummary,
filters,
summaryParams.Recompute,
)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -0,0 +1,85 @@
package v1
import (
"net/http"
"time"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
wakatime "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
)
type HeartbeatsResult struct {
Data []*wakatime.HeartbeatEntry `json:"data"`
End string `json:"end"`
Start string `json:"start"`
Timezone string `json:"timezone"`
}
type HeartbeatHandler struct {
userSrvc services.IUserService
heartbeatSrvc services.IHeartbeatService
}
func NewHeartbeatHandler(userService services.IUserService, heartbeatService services.IHeartbeatService) *HeartbeatHandler {
return &HeartbeatHandler{
userSrvc: userService,
heartbeatSrvc: heartbeatService,
}
}
func (h *HeartbeatHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
)
r.Path("/compat/wakatime/v1/users/{user}/heartbeats").Methods(http.MethodGet).HandlerFunc(h.Get)
}
// @Summary Get heartbeats of user for specified date
// @ID get-heartbeats
// @Tags heartbeat
// @Param date query string true "Date"
// @Param user path string true "Username (or current)"
// @Security ApiKeyAuth
// @Success 200 {object} HeartbeatsResult
// @Failure 400 {string} string "bad date"
// @Router /compat/wakatime/v1/users/{user}/heartbeats [get]
func (h *HeartbeatHandler) Get(w http.ResponseWriter, r *http.Request) {
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
params := r.URL.Query()
dateParam := params.Get("date")
date, err := time.Parse(conf.SimpleDateFormat, dateParam)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("bad date"))
return
}
timezone := user.TZ()
rangeFrom, rangeTo := utils.StartOfDay(date.In(timezone)), utils.EndOfDay(date.In(timezone))
heartbeats, err := h.heartbeatSrvc.GetAllWithin(rangeFrom, rangeTo, user)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(conf.ErrInternalServerError))
conf.Log().Request(r).Error("failed to retrieve heartbeats - %v", err)
return
}
res := HeartbeatsResult{
Data: wakatime.HeartbeatsToCompat(heartbeats),
Start: rangeFrom.UTC().Format(time.RFC3339),
End: rangeTo.UTC().Format(time.RFC3339),
Timezone: timezone.String(),
}
utils.RespondJSON(w, r, http.StatusOK, res)
}

View File

@ -1,6 +1,9 @@
package v1 package v1
import ( import (
"net/http"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
@ -9,8 +12,6 @@ import (
routeutils "github.com/muety/wakapi/routes/utils" routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http"
"strings"
) )
type ProjectsHandler struct { type ProjectsHandler struct {

View File

@ -1,6 +1,9 @@
package v1 package v1
import ( import (
"net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
@ -8,8 +11,6 @@ import (
v1 "github.com/muety/wakapi/models/compat/wakatime/v1" v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http"
"time"
) )
type StatsHandler struct { type StatsHandler struct {
@ -47,7 +48,13 @@ func (h *StatsHandler) RegisterRoutes(router *mux.Router) {
// @Tags wakatime // @Tags wakatime
// @Produce json // @Produce json
// @Param user path string true "User ID to fetch data for (or 'current')" // @Param user path string true "User ID to fetch data for (or 'current')"
// @Param range query string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any) // @Param range path string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
// @Param project query string false "Project to filter by"
// @Param language query string false "Language to filter by"
// @Param editor query string false "Editor to filter by"
// @Param operating_system query string false "OS to filter by"
// @Param machine query string false "Machine to filter by"
// @Param label query string false "Project label to filter by"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 200 {object} v1.StatsViewModel // @Success 200 {object} v1.StatsViewModel
// @Router /compat/wakatime/v1/users/{user}/stats/{range} [get] // @Router /compat/wakatime/v1/users/{user}/stats/{range} [get]
@ -87,7 +94,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
return return
} }
summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo) summary, err, status := h.loadUserSummary(requestedUser, rangeFrom, rangeTo, utils.ParseSummaryFilters(r))
if err != nil { if err != nil {
w.WriteHeader(status) w.WriteHeader(status)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))
@ -116,7 +123,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
utils.RespondJSON(w, r, http.StatusOK, stats) utils.RespondJSON(w, r, http.StatusOK, stats)
} }
func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time) (*models.Summary, error, int) { func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time, filters *models.Filters) (*models.Summary, error, int) {
overallParams := &models.SummaryParams{ overallParams := &models.SummaryParams{
From: start, From: start,
To: end, To: end,
@ -124,7 +131,7 @@ func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time)
Recompute: false, Recompute: false,
} }
summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve, false) summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve, filters, false)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -0,0 +1,107 @@
package v1
import (
"net/http"
"time"
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
)
type StatusBarViewModel struct {
CachedAt time.Time `json:"cached_at"`
Data v1.SummariesData `json:"data"`
}
type StatusBarHandler struct {
config *conf.Config
userSrvc services.IUserService
summarySrvc services.ISummaryService
}
func NewStatusBarHandler(userService services.IUserService, summaryService services.ISummaryService) *StatusBarHandler {
return &StatusBarHandler{
userSrvc: userService,
summarySrvc: summaryService,
config: conf.Get(),
}
}
func (h *StatusBarHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
)
r.Path("/users/{user}/statusbar/{range}").Methods(http.MethodGet).HandlerFunc(h.Get)
r.Path("/v1/users/{user}/statusbar/{range}").Methods(http.MethodGet).HandlerFunc(h.Get)
r.Path("/compat/wakatime/v1/users/{user}/statusbar/{range}").Methods(http.MethodGet).HandlerFunc(h.Get)
}
// @Summary Retrieve summary for statusbar
// @Description Mimics https://wakatime.com/api/v1/users/current/statusbar/today. Have no official documentation
// @ID statusbar
// @Tags wakatime
// @Produce json
// @Param user path string true "User ID to fetch data for (or 'current')"
// @Security ApiKeyAuth
// @Success 200 {object} StatusBarViewModel
// @Router /users/{user}/statusbar/today [get]
func (h *StatusBarHandler) Get(w http.ResponseWriter, r *http.Request) {
user, err := routeutils.CheckEffectiveUser(w, r, h.userSrvc, "current")
if err != nil {
return // response was already sent by util function
}
var vars = mux.Vars(r)
rangeParam := vars["range"]
if rangeParam == "" {
rangeParam = (*models.IntervalToday)[0]
}
err, rangeFrom, rangeTo := utils.ResolveIntervalRawTZ(rangeParam, user.TZ())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("invalid range"))
return
}
summary, status, err := h.loadUserSummary(user, rangeFrom, rangeTo)
if err != nil {
w.WriteHeader(status)
w.Write([]byte(err.Error()))
return
}
summariesView := v1.NewSummariesFrom([]*models.Summary{summary})
utils.RespondJSON(w, r, http.StatusOK, StatusBarViewModel{
CachedAt: time.Now(),
Data: *summariesView.Data[0],
})
}
func (h *StatusBarHandler) loadUserSummary(user *models.User, start, end time.Time) (*models.Summary, int, error) {
summaryParams := &models.SummaryParams{
From: start,
To: end,
User: user,
Recompute: false,
}
var retrieveSummary services.SummaryRetriever = h.summarySrvc.Retrieve
if summaryParams.Recompute {
retrieveSummary = h.summarySrvc.Summarize
}
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, nil, summaryParams.Recompute)
if err != nil {
return nil, http.StatusInternalServerError, err
}
return summary, http.StatusOK, nil
}

View File

@ -2,6 +2,10 @@ package v1
import ( import (
"errors" "errors"
"net/http"
"strings"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
@ -10,9 +14,6 @@ import (
routeutils "github.com/muety/wakapi/routes/utils" routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http"
"strings"
"time"
) )
type SummariesHandler struct { type SummariesHandler struct {
@ -51,6 +52,12 @@ func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {
// @Param range query string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any) // @Param range query string false "Range interval identifier" Enums(today, yesterday, week, month, year, 7_days, last_7_days, 30_days, last_30_days, 12_months, last_12_months, any)
// @Param start query string false "Start date (e.g. '2021-02-07')" // @Param start query string false "Start date (e.g. '2021-02-07')"
// @Param end query string false "End date (e.g. '2021-02-08')" // @Param end query string false "End date (e.g. '2021-02-08')"
// @Param project query string false "Project to filter by"
// @Param language query string false "Language to filter by"
// @Param editor query string false "Editor to filter by"
// @Param operating_system query string false "OS to filter by"
// @Param machine query string false "Machine to filter by"
// @Param label query string false "Project label to filter by"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 200 {object} v1.SummariesViewModel // @Success 200 {object} v1.SummariesViewModel
// @Router /compat/wakatime/v1/users/{user}/summaries [get] // @Router /compat/wakatime/v1/users/{user}/summaries [get]
@ -67,12 +74,7 @@ func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
return return
} }
filters := &models.Filters{} vm := v1.NewSummariesFrom(summaries)
if projectQuery := r.URL.Query().Get("project"); projectQuery != "" {
filters.Project = projectQuery
}
vm := v1.NewSummariesFrom(summaries, filters)
utils.RespondJSON(w, r, http.StatusOK, vm) utils.RespondJSON(w, r, http.StatusOK, vm)
} }
@ -129,8 +131,11 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
intervals := utils.SplitRangeByDays(overallParams.From, overallParams.To) intervals := utils.SplitRangeByDays(overallParams.From, overallParams.To)
summaries := make([]*models.Summary, len(intervals)) summaries := make([]*models.Summary, len(intervals))
// filtering
filters := utils.ParseSummaryFilters(r)
for i, interval := range intervals { for i, interval := range intervals {
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, end.After(time.Now())) summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, filters, end.After(time.Now()))
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -1,6 +1,8 @@
package v1 package v1
import ( import (
"net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
@ -8,7 +10,6 @@ import (
routeutils "github.com/muety/wakapi/routes/utils" routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"net/http"
) )
type UsersHandler struct { type UsersHandler struct {

View File

@ -98,7 +98,7 @@ func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
user.LastLoggedInAt = models.CustomTime(time.Now()) user.LastLoggedInAt = models.CustomTime(time.Now())
h.userSrvc.Update(user) h.userSrvc.Update(user)
http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded, "/")) http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded))
http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound) http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound)
} }
@ -107,7 +107,7 @@ func (h *LoginHandler) PostLogout(w http.ResponseWriter, r *http.Request) {
loadTemplates() loadTemplates()
} }
http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey, "/")) http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey))
http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound) http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound)
} }
@ -284,7 +284,7 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
go func(user *models.User) { go func(user *models.User) {
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken) link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
if err := h.mailSrvc.SendPasswordReset(user, link); err != nil { if err := h.mailSrvc.SendPasswordReset(user, link); err != nil {
conf.Log().Request(r).Error("failed to send password reset mail to %s %v", user.ID, err) conf.Log().Request(r).Error("failed to send password reset mail to %s - %v", user.ID, err)
} else { } else {
logbuch.Info("sent password reset mail to %s", user.ID) logbuch.Info("sent password reset mail to %s", user.ID)
} }
@ -301,8 +301,9 @@ func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel {
numUsers, _ := h.userSrvc.Count() numUsers, _ := h.userSrvc.Count()
return &view.LoginViewModel{ return &view.LoginViewModel{
Success: r.URL.Query().Get("success"), Success: r.URL.Query().Get("success"),
Error: r.URL.Query().Get("error"), Error: r.URL.Query().Get("error"),
TotalUsers: int(numUsers), TotalUsers: int(numUsers),
AllowSignup: h.config.IsDev() || h.config.Security.AllowSignup,
} }
} }

120
routes/relay/relay.go Normal file
View File

@ -0,0 +1,120 @@
package relay
import (
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
)
const targetUrlHeader = "X-Target-URL"
const pathMatcherPattern = `^/api/(heartbeat|heartbeats|summary|users|v1/users|compat/wakatime)`
type RelayHandler struct {
config *conf.Config
}
func NewRelayHandler() *RelayHandler {
return &RelayHandler{
config: conf.Get(),
}
}
type filteringMiddleware struct {
handler http.Handler
pathMatcher *regexp.Regexp
}
func newFilteringMiddleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &filteringMiddleware{
handler: h,
pathMatcher: regexp.MustCompile(pathMatcherPattern),
}
}
}
func (m *filteringMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
targetUrl, err := url.Parse(r.Header.Get(targetUrlHeader))
if err != nil || !m.pathMatcher.MatchString(targetUrl.Path) {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte{})
return
}
m.handler.ServeHTTP(w, r)
}
func (h *RelayHandler) RegisterRoutes(router *mux.Router) {
if !h.config.Security.EnableProxy {
return
}
r := router.PathPrefix("/relay").Subrouter()
r.Use(newFilteringMiddleware())
r.Path("").HandlerFunc(h.Any)
}
func (h *RelayHandler) Any(w http.ResponseWriter, r *http.Request) {
targetUrl, err := url.Parse(r.Header.Get(targetUrlHeader))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte{})
return
}
p := httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL = targetUrl
r.Host = targetUrl.Host
},
}
p.ServeHTTP(w, r)
}
// @Summary Proxy an GET API request to another Wakapi instance
// @ID relay-get
// @Tags relay
// @Param X-Target-URL header string true "Original URL to perform the request to"
// @Failure 403 {string} string "Returned if request path is not whitelisted"
// @Failure 502 {string} string "Returned if upstream host is down"
// @Router /relay [get]
func (h *RelayHandler) alias1() {}
// @Summary Proxy an POST API request to another Wakapi instance
// @ID relay-post
// @Tags relay
// @Param X-Target-URL header string true "Original URL to perform the request to"
// @Failure 403 {string} string "Returned if request path is not whitelisted"
// @Failure 502 {string} string "Returned if upstream host is down"
// @Router /relay [post]
func (h *RelayHandler) alias2() {}
// @Summary Proxy an PUT API request to another Wakapi instance
// @ID relay-put
// @Tags relay
// @Param X-Target-URL header string true "Original URL to perform the request to"
// @Failure 403 {string} string "Returned if request path is not whitelisted"
// @Failure 502 {string} string "Returned if upstream host is down"
// @Router /relay [put]
func (h *RelayHandler) alias3() {}
// @Summary Proxy an PATCH API request to another Wakapi instance
// @ID relay-patch
// @Tags relay
// @Param X-Target-URL header string true "Original URL to perform the request to"
// @Failure 403 {string} string "Returned if request path is not whitelisted"
// @Failure 502 {string} string "Returned if upstream host is down"
// @Router /relay [patch]
func (h *RelayHandler) alias4() {}
// @Summary Proxy an DELETE API request to another Wakapi instance
// @ID relay-delete
// @Tags relay
// @Param X-Target-URL header string true "Original URL to perform the request to"
// @Failure 403 {string} string "Returned if request path is not whitelisted"
// @Failure 502 {string} string "Returned if upstream host is down"
// @Router /relay [delete]
func (h *RelayHandler) alias5() {}

View File

@ -2,27 +2,24 @@ package routes
import ( import (
"fmt" "fmt"
"github.com/muety/wakapi/views"
"html/template" "html/template"
"io/fs"
"io/ioutil"
"net/http" "net/http"
"path"
"strings" "strings"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
"github.com/muety/wakapi/views"
) )
func Init() {
loadTemplates()
}
type action func(w http.ResponseWriter, r *http.Request) (int, string, string) type action func(w http.ResponseWriter, r *http.Request) (int, string, string)
var templates map[string]*template.Template var templates map[string]*template.Template
func Init() {
loadTemplates()
}
func DefaultTemplateFuncs() template.FuncMap { func DefaultTemplateFuncs() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"json": utils.Json, "json": utils.Json,
@ -38,6 +35,7 @@ func DefaultTemplateFuncs() template.FuncMap {
"add": utils.Add, "add": utils.Add,
"capitalize": utils.Capitalize, "capitalize": utils.Capitalize,
"toRunes": utils.ToRunes, "toRunes": utils.ToRunes,
"localTZOffset": utils.LocalTZOffset,
"entityTypes": models.SummaryTypes, "entityTypes": models.SummaryTypes,
"typeName": typeName, "typeName": typeName,
"isDev": func() bool { "isDev": func() bool {
@ -55,44 +53,12 @@ func DefaultTemplateFuncs() template.FuncMap {
"htmlSafe": func(html string) template.HTML { "htmlSafe": func(html string) template.HTML {
return template.HTML(html) return template.HTML(html)
}, },
} "avatarUrlTemplate": func() string {
} return config.Get().App.AvatarURLTemplate
},
func loadTemplates() { "defaultWakatimeUrl": func() string {
tpls := template.New("").Funcs(DefaultTemplateFuncs()) return config.WakatimeApiUrl
templates = make(map[string]*template.Template) },
// Use local file system when in 'dev' environment, go embed file system otherwise
templateFs := config.ChooseFS("views", views.TemplateFiles)
files, err := fs.ReadDir(templateFs, ".")
if err != nil {
panic(err)
}
for _, file := range files {
tplName := file.Name()
if file.IsDir() || path.Ext(tplName) != ".html" {
continue
}
templateFile, err := templateFs.Open(tplName)
if err != nil {
panic(err)
}
templateData, err := ioutil.ReadAll(templateFile)
if err != nil {
panic(err)
}
templateFile.Close()
tpl, err := tpls.New(tplName).Parse(string(templateData))
if err != nil {
panic(err)
}
templates[tplName] = tpl
} }
} }
@ -115,9 +81,22 @@ func typeName(t uint8) string {
if t == models.SummaryLabel { if t == models.SummaryLabel {
return "label" return "label"
} }
if t == models.SummaryBranch {
return "branch"
}
return "unknown" return "unknown"
} }
func loadTemplates() {
// Use local file system when in 'dev' environment, go embed file system otherwise
templateFs := config.ChooseFS("views", views.TemplateFiles)
if tpls, err := utils.LoadTemplates(templateFs, DefaultTemplateFuncs()); err == nil {
templates = tpls
} else {
panic(err)
}
}
func defaultErrorRedirectTarget() string { func defaultErrorRedirectTarget() string {
return fmt.Sprintf("%s/?error=unauthorized", config.Get().Server.BasePath) return fmt.Sprintf("%s/?error=unauthorized", config.Get().Server.BasePath)
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
"github.com/muety/wakapi/models/view" "github.com/muety/wakapi/models/view"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
"github.com/muety/wakapi/services/imports" "github.com/muety/wakapi/services/imports"
"github.com/muety/wakapi/utils" "github.com/muety/wakapi/utils"
@ -147,7 +148,7 @@ func (h *SettingsHandler) dispatchAction(action string) action {
case "toggle_wakatime": case "toggle_wakatime":
return h.actionSetWakatimeApiKey return h.actionSetWakatimeApiKey
case "import_wakatime": case "import_wakatime":
return h.actionImportWaktime return h.actionImportWakatime
case "regenerate_summaries": case "regenerate_summaries":
return h.actionRegenerateSummaries return h.actionRegenerateSummaries
case "delete_account": case "delete_account":
@ -229,7 +230,7 @@ func (h *SettingsHandler) actionChangePassword(w http.ResponseWriter, r *http.Re
return http.StatusInternalServerError, "", conf.ErrInternalServerError return http.StatusInternalServerError, "", conf.ErrInternalServerError
} }
http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded, "/")) http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded))
return http.StatusOK, "password was updated successfully", "" return http.StatusOK, "password was updated successfully", ""
} }
@ -354,27 +355,23 @@ func (h *SettingsHandler) actionDeleteLabel(w http.ResponseWriter, r *http.Reque
} }
user := middlewares.GetPrincipal(r) user := middlewares.GetPrincipal(r)
labelKey := r.PostFormValue("key") labelKey := r.PostFormValue("key") // label key
labelValue := r.PostFormValue("value") labelValue := r.PostFormValue("value") // project key
labelMap, err := h.projectLabelSrvc.GetByUserGrouped(user.ID) labels, err := h.projectLabelSrvc.GetByUser(user.ID)
if err != nil { if err != nil {
return http.StatusInternalServerError, "", "could not delete label" return http.StatusInternalServerError, "", "could not delete label"
} }
if projectLabels, ok := labelMap[labelKey]; ok { for _, l := range labels {
for _, l := range projectLabels { if l.Label == labelKey && l.ProjectKey == labelValue {
if l.Label == labelValue { if err := h.projectLabelSrvc.Delete(l); err != nil {
if err := h.projectLabelSrvc.Delete(l); err != nil { return http.StatusInternalServerError, "", "could not delete label"
return http.StatusInternalServerError, "", "could not delete label"
}
return http.StatusOK, "label deleted successfully", ""
} }
return http.StatusOK, "label deleted successfully", ""
} }
return http.StatusNotFound, "", "label not found"
} else {
return http.StatusNotFound, "", "project not found"
} }
return http.StatusNotFound, "", "label not found"
} }
func (h *SettingsHandler) actionDeleteLanguageMapping(w http.ResponseWriter, r *http.Request) (int, string, string) { func (h *SettingsHandler) actionDeleteLanguageMapping(w http.ResponseWriter, r *http.Request) (int, string, string) {
@ -434,20 +431,24 @@ func (h *SettingsHandler) actionSetWakatimeApiKey(w http.ResponseWriter, r *http
user := middlewares.GetPrincipal(r) user := middlewares.GetPrincipal(r)
apiKey := r.PostFormValue("api_key") apiKey := r.PostFormValue("api_key")
apiUrl := r.PostFormValue("api_url")
if apiUrl == conf.WakatimeApiUrl || apiKey == "" {
apiUrl = ""
}
// Healthcheck, if a new API key is set, i.e. the feature is activated // Healthcheck, if a new API key is set, i.e. the feature is activated
if (user.WakatimeApiKey == "" && apiKey != "") && !h.validateWakatimeKey(apiKey) { if (user.WakatimeApiKey == "" && apiKey != "") && !h.validateWakatimeKey(apiKey, apiUrl) {
return http.StatusBadRequest, "", "failed to connect to WakaTime, API key invalid?" return http.StatusBadRequest, "", "failed to connect to WakaTime, API key invalid?"
} }
if _, err := h.userSrvc.SetWakatimeApiKey(user, apiKey); err != nil { if _, err := h.userSrvc.SetWakatimeApiCredentials(user, apiKey, apiUrl); err != nil {
return http.StatusInternalServerError, "", conf.ErrInternalServerError return http.StatusInternalServerError, "", conf.ErrInternalServerError
} }
return http.StatusOK, "Wakatime API Key updated successfully", "" return http.StatusOK, "Wakatime API Key updated successfully", ""
} }
func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Request) (int, string, string) { func (h *SettingsHandler) actionImportWakatime(w http.ResponseWriter, r *http.Request) (int, string, string) {
if h.config.IsDev() { if h.config.IsDev() {
loadTemplates() loadTemplates()
} }
@ -491,7 +492,7 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
insert := func(batch []*models.Heartbeat) { insert := func(batch []*models.Heartbeat) {
if err := h.heartbeatSrvc.InsertBatch(batch); err != nil { if err := h.heartbeatSrvc.InsertBatch(batch); err != nil {
logbuch.Warn("failed to insert imported heartbeat, already existing? %v", err) logbuch.Warn("failed to insert imported heartbeat, already existing? - %v", err)
} }
} }
@ -517,13 +518,13 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
if !user.HasData { if !user.HasData {
user.HasData = true user.HasData = true
if _, err := h.userSrvc.Update(user); err != nil { if _, err := h.userSrvc.Update(user); err != nil {
conf.Log().Request(r).Error("failed to set 'has_data' flag for user %s %v", user.ID, err) conf.Log().Request(r).Error("failed to set 'has_data' flag for user %s - %v", user.ID, err)
} }
} }
if user.Email != "" { if user.Email != "" {
if err := h.mailSrvc.SendImportNotification(user, time.Now().Sub(start), int(countAfter-countBefore)); err != nil { if err := h.mailSrvc.SendImportNotification(user, time.Now().Sub(start), int(countAfter-countBefore)); err != nil {
conf.Log().Request(r).Error("failed to send import notification mail to %s %v", user.ID, err) conf.Log().Request(r).Error("failed to send import notification mail to %s - %v", user.ID, err)
} else { } else {
logbuch.Info("sent import notification mail to %s", user.ID) logbuch.Info("sent import notification mail to %s", user.ID)
} }
@ -545,11 +546,11 @@ func (h *SettingsHandler) actionRegenerateSummaries(w http.ResponseWriter, r *ht
go func(user *models.User) { go func(user *models.User) {
if err := h.regenerateSummaries(user); err != nil { if err := h.regenerateSummaries(user); err != nil {
conf.Log().Request(r).Error("failed to regenerate summaries for user '%s' %v", user.ID, err) conf.Log().Request(r).Error("failed to regenerate summaries for user '%s' - %v", user.ID, err)
} }
}(middlewares.GetPrincipal(r)) }(middlewares.GetPrincipal(r))
return http.StatusAccepted, "summaries are being regenerated this may take a up to a couple of minutes, please come back later", "" return http.StatusAccepted, "summaries are being regenerated - this may take a up to a couple of minutes, please come back later", ""
} }
func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Request) (int, string, string) { func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Request) (int, string, string) {
@ -562,18 +563,22 @@ func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Reques
logbuch.Info("deleting user '%s' shortly", user.ID) logbuch.Info("deleting user '%s' shortly", user.ID)
time.Sleep(5 * time.Minute) time.Sleep(5 * time.Minute)
if err := h.userSrvc.Delete(user); err != nil { if err := h.userSrvc.Delete(user); err != nil {
conf.Log().Request(r).Error("failed to delete user '%s' %v", user.ID, err) conf.Log().Request(r).Error("failed to delete user '%s' - %v", user.ID, err)
} else { } else {
logbuch.Info("successfully deleted user '%s'", user.ID) logbuch.Info("successfully deleted user '%s'", user.ID)
} }
}(user) }(user)
http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey, "/")) http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey))
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "Your account will be deleted in a few minutes. Sorry to you go."), http.StatusFound) http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, "Your account will be deleted in a few minutes. Sorry to you go."), http.StatusFound)
return -1, "", "" return -1, "", ""
} }
func (h *SettingsHandler) validateWakatimeKey(apiKey string) bool { func (h *SettingsHandler) validateWakatimeKey(apiKey string, baseUrl string) bool {
if baseUrl == "" {
baseUrl = conf.WakatimeApiUrl
}
headers := http.Header{ headers := http.Header{
"Accept": []string{"application/json"}, "Accept": []string{"application/json"},
"Authorization": []string{ "Authorization": []string{
@ -583,7 +588,7 @@ func (h *SettingsHandler) validateWakatimeKey(apiKey string) bool {
request, err := http.NewRequest( request, err := http.NewRequest(
http.MethodGet, http.MethodGet,
conf.WakatimeApiUrl+conf.WakatimeApiUserUrl, baseUrl+conf.WakatimeApiUserUrl,
nil, nil,
) )
if err != nil { if err != nil {
@ -651,7 +656,7 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode
} }
// labels // labels
labelMap, err := h.projectLabelSrvc.GetByUserGrouped(user.ID) labelMap, err := h.projectLabelSrvc.GetByUserGroupedInverted(user.ID)
if err != nil { if err != nil {
conf.Log().Request(r).Error("error while building settings project label map - %v", err) conf.Log().Request(r).Error("error while building settings project label map - %v", err)
return &view.SettingsViewModel{Error: criticalError} return &view.SettingsViewModel{Error: criticalError}
@ -660,11 +665,11 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode
combinedLabels := make([]*view.SettingsVMCombinedLabel, 0) combinedLabels := make([]*view.SettingsVMCombinedLabel, 0)
for _, l := range labelMap { for _, l := range labelMap {
cl := &view.SettingsVMCombinedLabel{ cl := &view.SettingsVMCombinedLabel{
Key: l[0].ProjectKey, Key: l[0].Label,
Values: make([]string, len(l)), Values: make([]string, len(l)),
} }
for i, l1 := range l { for i, l1 := range l {
cl.Values[i] = l1.Label cl.Values[i] = l1.ProjectKey
} }
combinedLabels = append(combinedLabels, cl) combinedLabels = append(combinedLabels, cl)
} }
@ -673,12 +678,11 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode
}) })
// projects // projects
projects, err := h.heartbeatSrvc.GetEntitySetByUser(models.SummaryProject, user) projects, err := routeutils.GetEffectiveProjectsList(user, h.heartbeatSrvc, h.aliasSrvc)
if err != nil { if err != nil {
conf.Log().Request(r).Error("error while fetching projects - %v", err) conf.Log().Request(r).Error("error while fetching projects - %v", err)
return &view.SettingsViewModel{Error: criticalError} return &view.SettingsViewModel{Error: criticalError}
} }
sort.Strings(projects)
return &view.SettingsViewModel{ return &view.SettingsViewModel{
User: user, User: user,
@ -686,6 +690,7 @@ func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewMode
Aliases: combinedAliases, Aliases: combinedAliases,
Labels: combinedLabels, Labels: combinedLabels,
Projects: projects, Projects: projects,
ApiKey: user.ApiKey,
Success: r.URL.Query().Get("success"), Success: r.URL.Query().Get("success"),
Error: r.URL.Query().Get("error"), Error: r.URL.Query().Get("error"),
} }

View File

@ -4,7 +4,6 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
conf "github.com/muety/wakapi/config" conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/models/view" "github.com/muety/wakapi/models/view"
su "github.com/muety/wakapi/routes/utils" su "github.com/muety/wakapi/routes/utils"
"github.com/muety/wakapi/services" "github.com/muety/wakapi/services"
@ -27,11 +26,13 @@ func NewSummaryHandler(summaryService services.ISummaryService, userService serv
} }
func (h *SummaryHandler) RegisterRoutes(router *mux.Router) { func (h *SummaryHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/summary").Subrouter() r1 := router.PathPrefix("/summary").Subrouter()
r.Use( r1.Use(middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler)
middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler, r1.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
)
r.Methods(http.MethodGet).HandlerFunc(h.GetIndex) r2 := router.PathPrefix("/summary").Subrouter()
r2.Use(middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler)
r2.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
} }
func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) { func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
@ -61,13 +62,11 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
return return
} }
vm := models.SummaryViewModel{ vm := view.SummaryViewModel{
Summary: summary, Summary: summary,
SummaryParams: summaryParams, SummaryParams: summaryParams,
User: user, User: user,
LanguageColors: utils.FilterColors(h.config.App.GetLanguageColors(), summary.Languages), LanguageColors: utils.FilterColors(h.config.App.GetLanguageColors(), summary.Languages),
EditorColors: utils.FilterColors(h.config.App.GetEditorColors(), summary.Editors),
OSColors: utils.FilterColors(h.config.App.GetOSColors(), summary.OperatingSystems),
ApiKey: user.ApiKey, ApiKey: user.ApiKey,
RawQuery: rawQuery, RawQuery: rawQuery,
} }

View File

@ -0,0 +1,53 @@
package utils
import (
"bytes"
"encoding/json"
"github.com/muety/wakapi/models"
"io/ioutil"
"net/http"
)
func ParseHeartbeats(r *http.Request) ([]*models.Heartbeat, error) {
heartbeats, err := tryParseBulk(r)
if err == nil {
return heartbeats, err
}
heartbeats, err = tryParseSingle(r)
if err == nil {
return heartbeats, err
}
return []*models.Heartbeat{}, err
}
func tryParseBulk(r *http.Request) ([]*models.Heartbeat, error) {
var heartbeats []*models.Heartbeat
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
dec := json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
if err := dec.Decode(&heartbeats); err != nil {
return nil, err
}
return heartbeats, nil
}
func tryParseSingle(r *http.Request) ([]*models.Heartbeat, error) {
var heartbeat models.Heartbeat
body, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
dec := json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
if err := dec.Decode(&heartbeat); err != nil {
return nil, err
}
return []*models.Heartbeat{&heartbeat}, nil
}

View File

@ -0,0 +1,53 @@
package utils
import (
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
"sort"
)
// GetEffectiveProjectsList returns the user's projects, including all alias targets and excluding all remapped project names (alias sources)
// Example: "A" mapped to "AB" using an alias
// -> "A" itself should not appear as a project anymore
// -> Instead, the "virtual" project "AB" shall appear
// See https://github.com/muety/wakapi/issues/231
func GetEffectiveProjectsList(user *models.User, heartbeatSrvc services.IHeartbeatService, aliasSrvc services.IAliasService) ([]string, error) {
projectsMap := make(map[string]bool) // proper sets as part of stdlib would be nice...
// extract actual projects from heartbeats
realProjects, err := heartbeatSrvc.GetEntitySetByUser(models.SummaryProject, user)
if err != nil {
return []string{}, err
}
// create a "set" / lookup table
for _, p := range realProjects {
projectsMap[p] = true
}
// fetch aliases
projectAliases, err := aliasSrvc.GetByUserAndType(user.ID, models.SummaryProject)
if err != nil {
return []string{}, err
}
// remove alias values (source of a mapping)
// add alias key (target of a mapping) instead
for _, a := range projectAliases {
if projectsMap[a.Value] {
projectsMap[a.Value] = false
}
projectsMap[a.Key] = true
}
projects := make([]string, 0, len(projectsMap))
for key, val := range projectsMap {
if !val {
continue
}
projects = append(projects, key)
}
sort.Strings(projects)
return projects, nil
}

View File

@ -20,7 +20,7 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ
retrieveSummary = ss.Summarize retrieveSummary = ss.Summarize
} }
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute) summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Filters, summaryParams.Recompute)
if err != nil { if err != nil {
return nil, err, http.StatusInternalServerError return nil, err, http.StatusInternalServerError
} }

View File

@ -32,9 +32,31 @@ let icons = [
'twemoji:gear', 'twemoji:gear',
'eva:corner-right-down-fill', 'eva:corner-right-down-fill',
'bi:heart-fill', 'bi:heart-fill',
'fxemoji:running',
'ic:round-person',
'bx:bxs-bar-chart-alt-2',
'bi:people-fill',
'fluent:data-bar-horizontal-24-filled',
'ic:round-dashboard',
'ci:settings-filled',
'akar-icons:chevron-down',
'ls:logout',
'fluent:key-32-filled',
'majesticons:clipboard-copy',
'fa-regular:calendar-alt',
'ph:books-bold',
'fa-solid:external-link-alt',
'bx:bx-code-curly',
'simple-icons:wakatime',
'bx:bxs-heart',
'heroicons-solid:light-bulb',
'ion:rocket',
'heroicons-solid:server',
'eva:checkmark-circle-2-fill',
'fluent:key-24-filled'
] ]
const output = path.normalize(path.join(__dirname, '../static/assets/icons.js')) const output = path.normalize(path.join(__dirname, '../static/assets/js/icons.dist.js'))
const pretty = false const pretty = false
// Sort icons by collections: filtered[prefix][array of icons] // Sort icons by collections: filtered[prefix][array of icons]

View File

@ -21,6 +21,7 @@ LANGUAGES = {
'PHP': 'php', 'PHP': 'php',
'Blade': 'blade.php' 'Blade': 'blade.php'
} }
BRANCHES = ['master', 'feature-1', 'feature-2']
class Heartbeat: class Heartbeat:
@ -65,6 +66,7 @@ def generate_data(n: int, n_projects: int = 5, n_past_hours: int = 24) -> List[H
p: str = random.choice(projects) p: str = random.choice(projects)
l: str = random.choice(languages) l: str = random.choice(languages)
f: str = randomword(random.randint(2, 8)) f: str = randomword(random.randint(2, 8))
b: str = random.choice(BRANCHES)
delta: timedelta = timedelta( delta: timedelta = timedelta(
hours=random.randint(0, n_past_hours - 1), hours=random.randint(0, n_past_hours - 1),
minutes=random.randint(0, 59), minutes=random.randint(0, 59),
@ -77,6 +79,7 @@ def generate_data(n: int, n_projects: int = 5, n_past_hours: int = 24) -> List[H
entity=f'/home/me/dev/{p}/{f}.{LANGUAGES[l]}', entity=f'/home/me/dev/{p}/{f}.{LANGUAGES[l]}',
project=p, project=p,
language=l, language=l,
branch=b,
time=(now - delta).timestamp() time=(now - delta).timestamp()
)) ))

View File

@ -83,8 +83,8 @@ func (srv *AggregationService) Run(userIds map[string]bool) error {
func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) { func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
for job := range jobs { for job := range jobs {
if summary, err := srv.summaryService.Summarize(job.From, job.To, &models.User{ID: job.UserID}); err != nil { if summary, err := srv.summaryService.Summarize(job.From, job.To, &models.User{ID: job.UserID}, nil); err != nil {
config.Log().Error("failed to generate summary (%v, %v, %s) %v", job.From, job.To, job.UserID, err) config.Log().Error("failed to generate summary (%v, %v, %s) - %v", job.From, job.To, job.UserID, err)
} else { } else {
logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID) logbuch.Info("successfully generated summary (%v, %v, %s)", job.From, job.To, job.UserID)
summaries <- summary summaries <- summary
@ -95,7 +95,7 @@ func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summar
func (srv *AggregationService) persistWorker(summaries <-chan *models.Summary) { func (srv *AggregationService) persistWorker(summaries <-chan *models.Summary) {
for summary := range summaries { for summary := range summaries {
if err := srv.summaryService.Insert(summary); err != nil { if err := srv.summaryService.Insert(summary); err != nil {
config.Log().Error("failed to save summary (%v, %v, %s) %v", summary.UserID, summary.FromTime, summary.ToTime, err) config.Log().Error("failed to save summary (%v, %v, %s) - %v", summary.UserID, summary.FromTime, summary.ToTime, err)
} }
} }
} }

View File

@ -2,6 +2,7 @@ package services
import ( import (
"errors" "errors"
"fmt"
"github.com/emvi/logbuch" "github.com/emvi/logbuch"
"github.com/muety/wakapi/config" "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models" "github.com/muety/wakapi/models"
@ -38,35 +39,50 @@ func (srv *AliasService) InitializeUser(userId string) error {
return err return err
} }
func (srv *AliasService) GetByUser(userId string) ([]*models.Alias, error) { func (srv *AliasService) MayInitializeUser(userId string) {
aliases, err := srv.repository.GetByUser(userId) if err := srv.InitializeUser(userId); err != nil {
if err != nil { logbuch.Error("failed to initialize user alias map for user %s", userId)
return nil, err
} }
return aliases, nil }
func (srv *AliasService) GetByUser(userId string) ([]*models.Alias, error) {
if !srv.IsInitialized(userId) {
srv.MayInitializeUser(userId)
}
if aliases, ok := userAliases.Load(userId); ok {
return aliases.([]*models.Alias), nil
} else {
return nil, errors.New(fmt.Sprintf("no user aliases loaded for user %s", userId))
}
}
func (srv *AliasService) GetByUserAndType(userId string, summaryType uint8) ([]*models.Alias, error) {
check := func(a *models.Alias) bool {
return a.Type == summaryType
}
return srv.getFiltered(userId, check)
} }
func (srv *AliasService) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) { func (srv *AliasService) GetByUserAndKeyAndType(userId, key string, summaryType uint8) ([]*models.Alias, error) {
aliases, err := srv.repository.GetByUserAndKeyAndType(userId, key, summaryType) check := func(a *models.Alias) bool {
if err != nil { return a.Key == key && a.Type == summaryType
return nil, err
} }
return aliases, nil return srv.getFiltered(userId, check)
} }
func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) { func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) {
if !srv.IsInitialized(userId) { if !srv.IsInitialized(userId) {
if err := srv.InitializeUser(userId); err != nil { srv.MayInitializeUser(userId)
return "", err }
if aliases, ok := userAliases.Load(userId); ok {
for _, a := range aliases.([]*models.Alias) {
if a.Type == summaryType && a.Value == value {
return a.Key, nil
}
} }
} }
aliases, _ := userAliases.Load(userId)
for _, a := range aliases.([]*models.Alias) {
if a.Type == summaryType && a.Value == value {
return a.Key, nil
}
}
return value, nil return value, nil
} }
@ -75,7 +91,11 @@ func (srv *AliasService) Create(alias *models.Alias) (*models.Alias, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
go srv.reinitUser(alias.UserID) // manually update cache
srv.updateCache(alias, false)
// reload entire cache (async, though)
go srv.MayInitializeUser(alias.UserID)
return result, nil return result, nil
} }
@ -84,7 +104,14 @@ func (srv *AliasService) Delete(alias *models.Alias) error {
return errors.New("no user id specified") return errors.New("no user id specified")
} }
err := srv.repository.Delete(alias.ID) err := srv.repository.Delete(alias.ID)
go srv.reinitUser(alias.UserID)
// manually update cache
if err == nil {
srv.updateCache(alias, false)
}
// reload entire cache (async, though)
go srv.MayInitializeUser(alias.UserID)
return err return err
} }
@ -101,15 +128,53 @@ func (srv *AliasService) DeleteMulti(aliases []*models.Alias) error {
err := srv.repository.DeleteBatch(ids) err := srv.repository.DeleteBatch(ids)
// manually update cache
if err == nil {
for _, a := range aliases {
srv.updateCache(a, true)
}
}
// reload entire cache (async, though)
for k := range affectedUsers { for k := range affectedUsers {
go srv.reinitUser(k) go srv.MayInitializeUser(k)
} }
return err return err
} }
func (srv *AliasService) reinitUser(userId string) { func (srv *AliasService) updateCache(reason *models.Alias, removal bool) {
if err := srv.InitializeUser(userId); err != nil { if !removal {
logbuch.Error("error initializing user aliases %v", err) if aliases, ok := userAliases.Load(reason.UserID); ok {
updatedAliases := aliases.([]*models.Alias)
updatedAliases = append(updatedAliases, reason)
userAliases.Store(reason.UserID, updatedAliases)
}
} else {
if aliases, ok := userAliases.Load(reason.UserID); ok {
updatedAliases := make([]*models.Alias, 0, len(aliases.([]*models.Alias))) // if we only had generics...
for _, a := range aliases.([]*models.Alias) {
if a.ID != reason.ID {
updatedAliases = append(updatedAliases, a)
}
}
userAliases.Store(reason.UserID, updatedAliases)
}
}
}
func (srv *AliasService) getFiltered(userId string, check func(alias *models.Alias) bool) ([]*models.Alias, error) {
if !srv.IsInitialized(userId) {
srv.MayInitializeUser(userId)
}
if aliases, ok := userAliases.Load(userId); ok {
filteredAliases := make([]*models.Alias, 0, len(aliases.([]*models.Alias)))
for _, a := range aliases.([]*models.Alias) {
if check(a) {
filteredAliases = append(filteredAliases, a)
}
}
return filteredAliases, nil
} else {
return nil, errors.New(fmt.Sprintf("no user aliases loaded for user %s", userId))
} }
} }

View File

@ -52,12 +52,3 @@ func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault() {
assert.Equal(suite.T(), "anchr", result3) assert.Equal(suite.T(), "anchr", result3)
assert.Nil(suite.T(), err3) assert.Nil(suite.T(), err3)
} }
func (suite *AliasServiceTestSuite) TestAliasService_GetAliasOrDefault_ErrorOnNonExistingUser() {
sut := NewAliasService(suite.AliasRepository)
result, err := sut.GetAliasOrDefault("nonexisting", models.SummaryProject, "wakapi-mobile")
assert.Empty(suite.T(), result)
assert.Error(suite.T(), err)
}

23
services/diagnostics.go Normal file
View File

@ -0,0 +1,23 @@
package services
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/repositories"
)
type DiagnosticsService struct {
config *config.Config
repository repositories.IDiagnosticsRepository
}
func NewDiagnosticsService(diagnosticsRepo repositories.IDiagnosticsRepository) *DiagnosticsService {
return &DiagnosticsService{
config: config.Get(),
repository: diagnosticsRepo,
}
}
func (srv *DiagnosticsService) Create(diagnostics *models.Diagnostics) (*models.Diagnostics, error) {
return srv.repository.Insert(diagnostics)
}

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