mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
274 Commits
1.27.3
...
2.2.3-test
Author | SHA1 | Date | |
---|---|---|---|
ad566993ad | |||
5f1e498454 | |||
2e0f79df3b | |||
4a4e19fcbd | |||
45d4ba89f5 | |||
29b3e619ca | |||
1a85ebc0f7 | |||
4bd58789f4 | |||
09d1124794 | |||
41584bdd82 | |||
1b7baf6fc9 | |||
a76db3e95f | |||
74a5226e73 | |||
d245b1e5d0 | |||
d5eff46651 | |||
30a65b4de9 | |||
9048a8eb7a | |||
1f19c5e93c | |||
4b0a3cf0d6 | |||
d778612242 | |||
ff7d595a86 | |||
9d7688957f | |||
179042f81b | |||
e6441f124c | |||
15c391d1d4 | |||
91c765202c | |||
5276f68918 | |||
e774039831 | |||
40067d252e | |||
1a47243f70 | |||
a1f6c2884b | |||
977420c68d | |||
3ae66a3898 | |||
5c5c462035 | |||
f6cc489425 | |||
5aae18e241 | |||
8a731a252a | |||
8fc0d78f64 | |||
a675417ab9 | |||
408d9086e7 | |||
8f933d8648 | |||
bbc85de34b | |||
ec70d024fa | |||
eae45baf38 | |||
4cea50b5c8 | |||
e4814431e0 | |||
91b4cb2c13 | |||
a3acdc7041 | |||
e7e5254673 | |||
8e558d8dee | |||
b763c4acc6 | |||
d1bd7b96b8 | |||
8c65da9031 | |||
647bf1781d | |||
85515d6cb5 | |||
1258ec0438 | |||
965d8e22b3 | |||
ed6e51b4df | |||
af879f8d57 | |||
f15efcd6f2 | |||
22e91ad362 | |||
932ba111cc | |||
23d00d574b | |||
d4b15e7959 | |||
42808fa38a | |||
52269c780f | |||
302eb33b1b | |||
784adec3c1 | |||
d2cdd35fff | |||
33d65fb33a | |||
6d762f5fd6 | |||
222024dabb | |||
660a09475e | |||
5cc932177f | |||
ac9d96c563 | |||
3758eecc96 | |||
e21788b8b5 | |||
e7f3432113 | |||
7159df30c2 | |||
fce3a3ea20 | |||
bd2a8c5a7f | |||
632a3d4a91 | |||
8a344ce4a2 | |||
cbbb592143 | |||
67f0d19a65 | |||
03b104a390 | |||
4a3fe48cce | |||
1033343702 | |||
31c462c275 | |||
0a7ebc4dc7 | |||
91768cf927 | |||
e967a74e36 | |||
8e6719f0b7 | |||
bcbd6236df | |||
d1cbabf662 | |||
ad4d251154 | |||
2bb3b886c2 | |||
4f183ed637 | |||
b66f9b5cf5 | |||
bf7f93fcd4 | |||
36c96dafca | |||
8f87c4e283 | |||
247aef5ef3 | |||
c0dada7e7a | |||
8b8c5675af | |||
f69dce39d8 | |||
c2d3426bcd | |||
bb0d0569fd | |||
c4c62f31e4 | |||
2bc53e6f11 | |||
fd6c36832e | |||
6f9015d3d8 | |||
cbcdd938eb | |||
bf82935849 | |||
fe3ba79d54 | |||
d80c1a4c4b | |||
a279548c89 | |||
8a3e6f0179 | |||
a72af7d57e | |||
ec236909c9 | |||
92f6d44606 | |||
e14f8c1463 | |||
80252ff701 | |||
374e578a7c | |||
aebfdc535d | |||
c217f8e664 | |||
ba54e7bb96 | |||
1e505b91f3 | |||
26825b07de | |||
6a5f08dc95 | |||
62e3decf0f | |||
0557a5000f | |||
7b7fa8bdf3 | |||
4e7322c985 | |||
af0d2e84e1 | |||
44a2e609fb | |||
ee501ca3c5 | |||
148f581906 | |||
acf16421a6 | |||
0039f67a2f | |||
c8a07cee36 | |||
15c8838fea | |||
f363135261 | |||
d561ce1766 | |||
6712f0a390 | |||
9950da3e7e | |||
c7e12ba3b5 | |||
aaa907a7b2 | |||
0ee52662d3 | |||
e1daf1406e | |||
7dd0967451 | |||
d6aa2c4405 | |||
821ae94c1e | |||
adcd7b35ae | |||
b0bd26f0ec | |||
259f711f2d | |||
1c0477f861 | |||
28a3418ad5 | |||
c5db2c235f | |||
9cbddaeedf | |||
485dfe2888 | |||
78a26dbf3c | |||
b2c72c6420 | |||
6852494d36 | |||
305166ce68 | |||
400f25c23e | |||
3aacd3461d | |||
7e2460e1f0 | |||
57175ae7f8 | |||
5df0f48303 | |||
76a7cf7e80 | |||
7cae3c43d0 | |||
5fc87dd143 | |||
7329f6a34e | |||
3b96bd3723 | |||
2c7977cf63 | |||
782da0b49e | |||
ed9a7ccd5a | |||
9451848ad4 | |||
6c0145b149 | |||
a94092e31c | |||
52744dbcd0 | |||
cc11226eab | |||
8d073aaef2 | |||
d2f078443e | |||
c6e1651d9e | |||
630090e38a | |||
5394349c73 | |||
5cd3bf83a6 | |||
13cf911edf | |||
fe0f41cecb | |||
265080453a | |||
f9fb7c7a8a | |||
90477dbb01 | |||
35926a19e2 | |||
84dc594548 | |||
2f9b8fbcfe | |||
9235c1ca78 | |||
a869897f80 | |||
2f9cafc88c | |||
816d0c8cdc | |||
1ab29b22e1 | |||
cafe4133e4 | |||
5a0a3c40ca | |||
9b5f00ea5d | |||
7a418aa519 | |||
d96a48d5dc | |||
fa4512f79b | |||
398b4c16d6 | |||
d1577fc6be | |||
23f8a5cf7f | |||
81835a3d88 | |||
30de96950b | |||
11291b0d6c | |||
f0ac0f6153 | |||
6aad1633e1 | |||
c07a4d71a0 | |||
dff0b742fc | |||
4f65f94766 | |||
825663acde | |||
f399fd4ea7 | |||
87fadf46f7 | |||
69f5d510dc | |||
0542813ed6 | |||
c962a3891d | |||
2088987a0c | |||
9e3203ac41 | |||
58719182c4 | |||
a8df25be08 | |||
391cc1e5b4 | |||
3bb22e5e84 | |||
93bdb48d95 | |||
533b5d62fc | |||
0af5fab75f | |||
fecc8b3b5f | |||
24b8ff6381 | |||
180e75a5eb | |||
f48b49d26e | |||
47b9cacb26 | |||
23fc1b62cc | |||
74f6a255a8 | |||
7a5dce29bd | |||
0e1596fe70 | |||
48513b660d | |||
69f73fc0ea | |||
0e788b0777 | |||
181aefa2f9 | |||
407925ec53 | |||
5e96e2a601 | |||
4d2a160ccb | |||
c3957ec0c8 | |||
312dfb36d8 | |||
c66605d463 | |||
3c12df52d9 | |||
dd6a040171 | |||
9f1266957b | |||
466f2e1786 | |||
82b8951437 | |||
25464e9519 | |||
650fffa344 | |||
69627fbe11 | |||
561198b203 | |||
7c4a2024b6 | |||
7bcd6890d1 | |||
1e4e530c21 | |||
490cca05eb | |||
3780ae4255 | |||
628ea0b9dd | |||
0d64858721 | |||
c1c78d8d5b | |||
538b9d2463 | |||
f4612fd542 | |||
fb643571d2 | |||
a4d47fb566 |
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal 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
19
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal 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
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: Other (feature request, question, ...)
|
||||||
|
about: Anything else
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
46
.github/workflows/docker.yml
vendored
46
.github/workflows/docker.yml
vendored
@ -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
|
|
||||||
|
48
.github/workflows/linux-build-on-release.yml
vendored
48
.github/workflows/linux-build-on-release.yml
vendored
@ -1,48 +0,0 @@
|
|||||||
name: Build Wakapi on Linux
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
pull_request:
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-release:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: ^1.16
|
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Get dependencies
|
|
||||||
run: |
|
|
||||||
go get
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: GO111MODULE=on go build -v .
|
|
||||||
|
|
||||||
- name: Zip executable and sample config
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
run: |
|
|
||||||
cp config.default.yml config.yml
|
|
||||||
zip -9 release.zip wakapi config.yml
|
|
||||||
|
|
||||||
- name: Upload built executable to Release
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
uses: actions/upload-release-asset@v1.0.2
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
|
||||||
asset_path: release.zip
|
|
||||||
asset_name: wakapi_linux_amd64.zip
|
|
||||||
asset_content_type: application/gzip
|
|
52
.github/workflows/mapi.yml
vendored
Normal file
52
.github/workflows/mapi.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
name: 'Mayhem for API'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.x
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ^1.18
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: go get
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: GO111MODULE=on go build -v .
|
||||||
|
|
||||||
|
- name: start wakapi
|
||||||
|
run: ./wakapi --config config.default.yml &
|
||||||
|
|
||||||
|
- name: create a trivial testing user
|
||||||
|
run: sqlite3 wakapi_db.db "insert into users (id, api_key) values ('mapi', 'test-api-key')"
|
||||||
|
|
||||||
|
- name: Run Mayhem for API
|
||||||
|
uses: ForAllSecure/mapi-action@v1
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
mapi-token: ${{ secrets.MAPI_TOKEN }}
|
||||||
|
api-url: http://localhost:3000/api/
|
||||||
|
api-spec: static/docs/swagger.yaml
|
||||||
|
target: muety/wakapi
|
||||||
|
duration: 1min
|
||||||
|
sarif-report: mapi.sarif
|
||||||
|
run-args: |
|
||||||
|
--header-auth
|
||||||
|
Authorization: Basic dGVzdC1hcGkta2V5
|
||||||
|
|
||||||
|
- name: Upload SARIF file
|
||||||
|
uses: github/codeql-action/upload-sarif@v1
|
||||||
|
with:
|
||||||
|
sarif_file: mapi.sarif
|
@ -1,4 +1,4 @@
|
|||||||
name: Build Wakapi on Windows
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -10,14 +10,19 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
name: Build
|
name: Build & Release
|
||||||
runs-on: windows-latest
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ^1.16
|
go-version: ^1.18
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
@ -28,16 +33,30 @@ jobs:
|
|||||||
go get
|
go get
|
||||||
|
|
||||||
- name: Enable Go 1.11 modules
|
- name: Enable Go 1.11 modules
|
||||||
|
if: runner.os == 'Windows'
|
||||||
run: cmd /c "set GO111MODULE=on"
|
run: cmd /c "set GO111MODULE=on"
|
||||||
|
|
||||||
|
- name: Unit Tests
|
||||||
|
run: go test ./... -run ./...
|
||||||
|
|
||||||
|
- name: API Tests
|
||||||
|
run: |
|
||||||
|
npm -g install newman
|
||||||
|
./testing/run_api_tests.sh
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v .
|
run: go build -v .
|
||||||
|
|
||||||
- name: Compress working folder
|
- name: Compress working folder on Windows
|
||||||
if: github.event_name == 'release'
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
cp .\config.default.yml .\config.yml
|
cp .\config.default.yml .\config.yml
|
||||||
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath release.zip
|
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath release.zip
|
||||||
|
- name: Compress working folder on Unix
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
run: |
|
||||||
|
cp config.default.yml config.yml
|
||||||
|
zip -9 release.zip wakapi config.yml
|
||||||
|
|
||||||
- name: Upload built executable to Release
|
- name: Upload built executable to Release
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
@ -47,5 +66,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
upload_url: ${{ github.event.release.upload_url }}
|
||||||
asset_path: release.zip
|
asset_path: release.zip
|
||||||
asset_name: wakapi_win_amd64.zip
|
asset_name: wakapi_${{ runner.os }}_amd64.zip
|
||||||
asset_content_type: application/gzip
|
asset_content_type: application/gzip
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
6
.gitpod.yml
Normal 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
26
Caddyfile
Normal 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
|
||||||
|
}
|
29
Dockerfile
29
Dockerfile
@ -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.18-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.18-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
|
||||||
@ -45,6 +54,6 @@ ENV WAKAPI_ALLOW_SIGNUP 'true'
|
|||||||
|
|
||||||
COPY --from=build-env /app .
|
COPY --from=build-env /app .
|
||||||
|
|
||||||
VOLUME /data
|
EXPOSE 3000
|
||||||
|
|
||||||
ENTRYPOINT ./entrypoint.sh
|
ENTRYPOINT /app/entrypoint.sh
|
||||||
|
307
README.md
307
README.md
@ -4,15 +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="https://saythanks.io/to/n1try"><img src="https://badges.fw-web.space/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg"></a>
|
<a href="#-treeware"><img src="https://badges.fw-web.space:/treeware/trees/muety/wakapi?color=%234EC820&label=%F0%9F%8C%B3%20trees"></a>
|
||||||
<a href="https://liberapay.com/muety/"><img src="https://badges.fw-web.space/liberapay/receives/muety.svg?logo=liberapay"></a>
|
<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://wakapi.dev/api/badge/n1try/interval:any/project:wakapi?label=wakapi">
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://goreportcard.com/report/github.com/muety/wakapi"><img src="https://goreportcard.com/badge/github.com/muety/wakapi"></a>
|
|
||||||
<img src="https://badges.fw-web.space/github/languages/code-size/muety/wakapi">
|
<img src="https://badges.fw-web.space/github/languages/code-size/muety/wakapi">
|
||||||
<a href="https://sonarcloud.io/dashboard?id=muety_wakapi"><img src="https://sonarcloud.io/api/project_badges/measure?project=muety_wakapi&metric=sqale_index"></a>
|
<a href="https://goreportcard.com/report/github.com/muety/wakapi"><img src="https://goreportcard.com/badge/github.com/muety/wakapi"></a>
|
||||||
<a href="https://sonarcloud.io/dashboard?id=muety_wakapi"><img src="https://sonarcloud.io/api/project_badges/measure?project=muety_wakapi&metric=ncloc"></a>
|
<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>
|
||||||
|
|
||||||
@ -33,34 +29,18 @@
|
|||||||
</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
|
||||||
* ✅ Built by developers for developers
|
* ✅ Built by developers for developers
|
||||||
* ✅ Statistics for projects, languages, editors, hosts and operating systems
|
* ✅ Statistics for projects, languages, editors, hosts and operating systems
|
||||||
* ✅ Badges
|
* ✅ Badges
|
||||||
* ✅ Weekly E-Mail Reports
|
* ✅ Weekly E-Mail reports
|
||||||
* ✅ REST API
|
* ✅ REST API
|
||||||
* ✅ Partially compatible with WakaTime
|
* ✅ Partially compatible with WakaTime
|
||||||
* ✅ WakaTime integration
|
* ✅ WakaTime integration
|
||||||
@ -69,32 +49,38 @@ 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).
|
|
||||||
|
|
||||||
However, we do not guarantee data persistence, so you might potentially lose your data if the service is taken down some day ❕
|
If you want to try out a free, hosted cloud service, all you need to do is create an account and then set up your client-side tooling (see below).
|
||||||
|
|
||||||
|
### 📦 Option 2: Quick-run a release
|
||||||
|
|
||||||
### 📦 Option 2: Quick-run a Release
|
|
||||||
```bash
|
```bash
|
||||||
$ curl -L https://wakapi.dev/get | bash
|
$ curl -L https://wakapi.dev/get | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🐳 Option 3: Use Docker
|
### 🐳 Option 3: Use Docker
|
||||||
|
|
||||||
```bash
|
```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.
|
||||||
@ -102,14 +88,17 @@ $ docker run -d \
|
|||||||
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) for quick and easy deployment.
|
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) for quick and easy deployment.
|
||||||
|
|
||||||
### 🧑💻 Option 4: Compile and run from source
|
### 🧑💻 Option 4: Compile and run from source
|
||||||
#### Prerequisites
|
|
||||||
* Go >= 1.16 (with `$GOPATH` properly set)
|
|
||||||
* gcc (to compile [go-sqlite3](https://github.com/mattn/go-sqlite3))
|
|
||||||
* Fedora / RHEL: `dnf install @development-tools`
|
|
||||||
* Ubuntu / Debian: `apt install build-essential`
|
|
||||||
* Windows: See [here](https://github.com/mattn/go-sqlite3/issues/214#issuecomment-253216476)
|
|
||||||
|
|
||||||
#### Compile & Run
|
#### Prerequisites
|
||||||
|
|
||||||
|
* Go >= 1.18
|
||||||
|
* gcc (to compile [go-sqlite3](https://github.com/mattn/go-sqlite3))
|
||||||
|
* Fedora / RHEL: `dnf install @development-tools`
|
||||||
|
* Ubuntu / Debian: `apt install build-essential`
|
||||||
|
* Windows: See [here](https://github.com/mattn/go-sqlite3/issues/214#issuecomment-253216476)
|
||||||
|
|
||||||
|
#### Compile & run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build the executable
|
# Build the executable
|
||||||
$ go build -o wakapi
|
$ go build -o wakapi
|
||||||
@ -122,19 +111,20 @@ $ vi config.yml
|
|||||||
$ ./wakapi
|
$ ./wakapi
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Check the comments `config.yml` for best practices regarding security configuration and more.
|
**Note:** Check the comments in `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. **Edit your local `~/.wakatime.cfg`** file as follows.
|
||||||
|
|
||||||
```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,66 +132,86 @@ 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.
|
|
||||||
|
|
||||||
| YAML Key | Environment Variable | Default | Description |
|
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.
|
||||||
|---------------------------|---------------------------|--------------|---------------------------------------------------------------------|
|
|
||||||
| `env` | `ENVIRONMENT` | `dev` | Whether to use development- or production settings |
|
| YAML key / Env. variable | Default | Description |
|
||||||
| `app.custom_languages` | - | - | Map from file endings to language names |
|
|------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on |
|
| `env` /<br>`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
|
||||||
| `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
|
| `app.aggregation_time` /<br>`WAKAPI_AGGREGATION_TIME` | `02:15` | Time of day at which to periodically run summary generation for all users |
|
||||||
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
|
| `app.report_time_weekly` /<br>`WAKAPI_REPORT_TIME_WEEKLY` | `fri,18:00` | Week day and time at which to send e-mail reports |
|
||||||
| `server.tls_cert_path` | `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
|
| `app.import_batch_size` /<br>`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services |
|
||||||
| `server.tls_key_path` | `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
|
| `app.inactive_days` /<br>`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) |
|
||||||
| `server.base_path` | `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
|
| `app.heartbeat_max_age /`<br>`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) |
|
||||||
| `security.password_salt` | `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
|
| `app.custom_languages` | - | Map from file endings to language names |
|
||||||
| `security.insecure_cookies` | `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
|
| `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)) |
|
||||||
| `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.port` /<br> `WAKAPI_PORT` | `3000` | Port to listen on |
|
||||||
| `security.allow_signup` | `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
|
| `server.listen_ipv4` /<br> `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
|
||||||
| `security.expose_metrics` | `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
|
| `server.listen_ipv6` /<br> `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
|
||||||
| `db.host` | `WAKAPI_DB_HOST` | - | Database host |
|
| `server.listen_socket` /<br> `WAKAPI_LISTEN_SOCKET` | - | UNIX socket to listen on (leave blank to disable UNIX socket) |
|
||||||
| `db.port` | `WAKAPI_DB_PORT` | - | Database port |
|
| `server.timeout_sec` /<br> `WAKAPI_TIMEOUT_SEC` | `30` | Request timeout in seconds |
|
||||||
| `db.user` | `WAKAPI_DB_USER` | - | Database user |
|
| `server.tls_cert_path` /<br> `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
|
||||||
| `db.password` | `WAKAPI_DB_PASSWORD` | - | Database password |
|
| `server.tls_key_path` /<br> `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
|
||||||
| `db.name` | `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
|
| `server.base_path` /<br> `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
|
||||||
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
|
| `server.public_url` /<br> `WAKAPI_PUBLIC_URL` | `http://localhost:3000` | URL at which your Wakapi instance can be found publicly |
|
||||||
| `db.charset` | `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
|
| `security.password_salt` /<br> `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
|
||||||
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
| `security.insecure_cookies` /<br> `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
|
||||||
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
| `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.automgirate_fail_silently` | `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
|
| `security.allow_signup` /<br> `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
|
||||||
| `mail.enabled` | `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
|
| `security.expose_metrics` /<br> `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
|
||||||
| `mail.sender` | `WAKAPI_MAIL_SENDER` | `noreply@wakapi.dev` | Default sender address for outgoing mails (ignored for MailWhale) |
|
| `db.host` /<br> `WAKAPI_DB_HOST` | - | Database host |
|
||||||
| `mail.provider` | `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
|
| `db.port` /<br> `WAKAPI_DB_PORT` | - | Database port |
|
||||||
| `mail.smtp.*` | `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yaml) for details |
|
| `db.user` /<br> `WAKAPI_DB_USER` | - | Database user |
|
||||||
| `mail.mailwhale.*` | `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yaml) for details |
|
| `db.password` /<br> `WAKAPI_DB_PASSWORD` | - | Database password |
|
||||||
| `sentry.dsn` | `WAKAPI_SENTRY_DSN` | – | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
|
| `db.name` /<br> `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
|
||||||
| `sentry.enable_tracing` | `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
|
| `db.dialect` /<br> `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
|
||||||
| `sentry.sample_rate` | `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
|
| `db.charset` /<br> `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
|
||||||
| `sentry.sample_rate_heartbeats` | `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeats request in Sentry |
|
| `db.max_conn` /<br> `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
||||||
|
| `db.ssl` /<br> `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
||||||
|
| `db.automgirate_fail_silently` /<br> `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
|
||||||
|
| `mail.enabled` /<br> `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
|
||||||
|
| `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `Wakapi <noreply@wakapi.dev>` | Default sender address for outgoing mails (ignored for MailWhale) |
|
||||||
|
| `mail.provider` /<br> `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
|
||||||
|
| `mail.smtp.host` /<br> `WAKAPI_MAIL_SMTP_HOST` | - | SMTP server address for sending mail (if using `smtp` mail provider) |
|
||||||
|
| `mail.smtp.port` /<br> `WAKAPI_MAIL_SMTP_PORT` | - | SMTP server port (usually 465) |
|
||||||
|
| `mail.smtp.username` /<br> `WAKAPI_MAIL_SMTP_USER` | - | SMTP server authentication username |
|
||||||
|
| `mail.smtp.password` /<br> `WAKAPI_MAIL_SMTP_PASS` | - | SMTP server authentication password |
|
||||||
|
| `mail.smtp.tls` /<br> `WAKAPI_MAIL_SMTP_TLS` | `false` | Whether the SMTP server requires TLS encryption (`false` for STARTTLS or no encryption) |
|
||||||
|
| `mail.mailwhale.url` /<br> `WAKAPI_MAIL_MAILWHALE_URL` | - | URL of [MailWhale](https://mailwhale.dev) instance (e.g. `https://mailwhale.dev`) (if using `mailwhale` mail provider) |
|
||||||
|
| `mail.mailwhale.client_id` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_ID` | - | MailWhale API client ID |
|
||||||
|
| `mail.mailwhale.client_secret` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET` | - | MailWhale API client secret |
|
||||||
|
| `sentry.dsn` /<br> `WAKAPI_SENTRY_DSN` | – | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
|
||||||
|
| `sentry.enable_tracing` /<br> `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
|
||||||
|
| `sentry.sample_rate` /<br> `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
|
||||||
|
| `sentry.sample_rate_heartbeats` /<br> `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeat request in Sentry |
|
||||||
|
| `quick_start` /<br> `WAKAPI_QUICK_START` | `false` | Whether to skip initial boot tasks. Use only for development purposes! |
|
||||||
|
|
||||||
### Supported databases
|
### 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.
|
||||||
|
|
||||||
* [SQLite](https://sqlite.org/) (_default, easy setup_)
|
* [SQLite](https://sqlite.org/) (_default, easy setup_)
|
||||||
* [MySQL](https://hub.docker.com/_/mysql) (_recommended, because most extensively tested_)
|
* [MySQL](https://hub.docker.com/_/mysql) (_recommended, because most extensively tested_)
|
||||||
* [MariaDB](https://hub.docker.com/_/mariadb) (_open-source MySQL alternative_)
|
* [MariaDB](https://hub.docker.com/_/mariadb) (_open-source MySQL alternative_)
|
||||||
* [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`)
|
## 🔧 API endpoints
|
||||||
See the [advanced setup instructions](docs/advanced_setup.md).
|
|
||||||
|
|
||||||
## 🔧 API Endpoints
|
|
||||||
See our [Swagger API Documentation](https://wakapi.dev/swagger-ui).
|
See our [Swagger API Documentation](https://wakapi.dev/swagger-ui).
|
||||||
|
|
||||||
### Generating Swagger docs
|
### Generating Swagger docs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ go get -u github.com/swaggo/swag/cmd/swag
|
$ go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
$ swag init -o static/docs
|
$ swag init -o static/docs
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 Integrations
|
## 🤝 Integrations
|
||||||
### Prometheus Export
|
|
||||||
|
### Prometheus export
|
||||||
|
|
||||||
You can export your Wakapi statistics to Prometheus to view them in a Grafana dashboard or so. Here is how.
|
You can export your Wakapi statistics to Prometheus to view them in a Grafana dashboard or so. Here is how.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -216,6 +226,7 @@ $ echo "<YOUR_API_KEY>" | base64
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Scrape config example
|
#### Scrape config example
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
# prometheus.yml
|
# prometheus.yml
|
||||||
# (assuming your Wakapi instance listens at localhost, port 3000)
|
# (assuming your Wakapi instance listens at localhost, port 3000)
|
||||||
@ -229,15 +240,18 @@ scrape_configs:
|
|||||||
- targets: ['localhost:3000']
|
- targets: ['localhost:3000']
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Grafana
|
#### Grafana
|
||||||
|
|
||||||
There is also a [nice Grafana dashboard](https://grafana.com/grafana/dashboards/12790), provided by the author of [wakatime_exporter](https://github.com/MacroPower/wakatime_exporter).
|
There is also a [nice Grafana dashboard](https://grafana.com/grafana/dashboards/12790), provided by the author of [wakatime_exporter](https://github.com/MacroPower/wakatime_exporter).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### WakaTime Integration
|
### WakaTime integration
|
||||||
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
|
|
||||||
|
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
|
||||||
|
|
||||||
|
### GitHub Readme Stats integrations
|
||||||
|
|
||||||
### GitHub Readme Stats Integrations
|
|
||||||
Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats) to generate fancy cards for you. Here is an example.
|
Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats) to generate fancy cards for you. Here is an example.
|
||||||
|
|
||||||

|

|
||||||
@ -245,20 +259,20 @@ Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra
|
|||||||
<details>
|
<details>
|
||||||
<summary>Click to view code</summary>
|
<summary>Click to view code</summary>
|
||||||
|
|
||||||
```md
|
```markdown
|
||||||

|

|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
### Github Readme Metrics integration
|
||||||
|
|
||||||
### Github Readme Metrics Integration
|
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [Metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi.
|
||||||
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi.
|
|
||||||
|
|
||||||
Preview:
|
Preview:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click to view code</summary>
|
<summary>Click to view code</summary>
|
||||||
@ -272,7 +286,7 @@ Preview:
|
|||||||
plugin_wakatime_days: 7 # Display last week stats
|
plugin_wakatime_days: 7 # Display last week stats
|
||||||
plugin_wakatime_sections: time, projects, projects-graphs # Display time and projects sections, along with projects graphs
|
plugin_wakatime_sections: time, projects, projects-graphs # Display time and projects sections, along with projects graphs
|
||||||
plugin_wakatime_limit: 4 # Show 4 entries per graph
|
plugin_wakatime_limit: 4 # Show 4 entries per graph
|
||||||
plugin_wakatime_url: http://wakapi.dev # Wakatime url endpoint
|
plugin_wakatime_url: http://wakapi.dev # Wakatime url endpoint
|
||||||
plugin_wakatime_user: .user.login # User
|
plugin_wakatime_user: .user.login # User
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -280,20 +294,26 @@ Preview:
|
|||||||
</details>
|
</details>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## 👍 Best Practices
|
## 👍 Best practices
|
||||||
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or _nginx_ to enable **TLS encryption** (HTTPS).
|
|
||||||
However, if you want to expose your wakapi instance to the public anyway, you need to set `server.listen_ipv4` to `0.0.0.0` in `config.yml`
|
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or [nginx](https://www.nginx.com/), to enable **TLS encryption** (HTTPS).
|
||||||
|
|
||||||
|
However, if you want to expose your wakapi instance to the public anyway, you need to set `server.listen_ipv4` to `0.0.0.0` in `config.yml`.
|
||||||
|
|
||||||
## 🧪 Tests
|
## 🧪 Tests
|
||||||
### Unit Tests
|
|
||||||
|
### Unit tests
|
||||||
|
|
||||||
Unit tests are supposed to test business logic on a fine-grained level. They are implemented as part of the application, using Go's [testing](https://pkg.go.dev/testing?utm_source=godoc) package alongside [stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify).
|
Unit tests are supposed to test business logic on a fine-grained level. They are implemented as part of the application, using Go's [testing](https://pkg.go.dev/testing?utm_source=godoc) package alongside [stretchr/testify](https://pkg.go.dev/github.com/stretchr/testify).
|
||||||
|
|
||||||
#### How to run
|
#### How to run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
|
$ CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Tests
|
### API tests
|
||||||
|
|
||||||
API tests are implemented as black box tests, which interact with a fully-fledged, standalone Wakapi through HTTP requests. They are supposed to check Wakapi's web stack and endpoints, including response codes, headers and data on a syntactical level, rather than checking the actual content that is returned.
|
API tests are implemented as black box tests, which interact with a fully-fledged, standalone Wakapi through HTTP requests. They are supposed to check Wakapi's web stack and endpoints, including response codes, headers and data on a syntactical level, rather than checking the actual content that is returned.
|
||||||
|
|
||||||
Our API (or end-to-end, in some way) tests are implemented as a [Postman](https://www.postman.com/) collection and can be run either from inside Postman, or using [newman](https://www.npmjs.com/package/newman) as a command-line runner.
|
Our API (or end-to-end, in some way) tests are implemented as a [Postman](https://www.postman.com/) collection and can be run either from inside Postman, or using [newman](https://www.npmjs.com/package/newman) as a command-line runner.
|
||||||
@ -301,72 +321,85 @@ Our API (or end-to-end, in some way) tests are implemented as a [Postman](https:
|
|||||||
To get a predictable environment, tests are run against a fresh and clean Wakapi instance with a SQLite database that is populated with nothing but some seed data (see [data.sql](testing/data.sql)). It is usually recommended for software tests to be [safe](https://www.restapitutorial.com/lessons/idempotency.html), stateless and without side effects. In contrary to that paradigm, our API tests strictly require a fixed execution order (which Postman assures) and their assertions may rely on specific previous tests having succeeded.
|
To get a predictable environment, tests are run against a fresh and clean Wakapi instance with a SQLite database that is populated with nothing but some seed data (see [data.sql](testing/data.sql)). It is usually recommended for software tests to be [safe](https://www.restapitutorial.com/lessons/idempotency.html), stateless and without side effects. In contrary to that paradigm, our API tests strictly require a fixed execution order (which Postman assures) and their assertions may rely on specific previous tests having succeeded.
|
||||||
|
|
||||||
#### Prerequisites (Linux only)
|
#### Prerequisites (Linux only)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
#### How to run (Linux only)
|
#### How to run (Linux only)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./testing/run_api_tests.sh
|
$ ./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.
|
|
||||||
|
|
||||||
#### TailwindCSS
|
To keep things minimal, all JS and CSS assets are included as static files and checked in to Git. [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) and [Iconify](https://iconify.design/docs/icon-bundles/) require an additional build step. To only require this at the time of development, the compiled assets are checked in to Git as well.
|
||||||
```bash
|
|
||||||
$ tailwindcss-cli build static/assets/vendor/tailwind.css -o static/assets/vendor/tailwind.dist.css
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Iconify
|
|
||||||
```bash
|
```bash
|
||||||
$ yarn add -D @iconify/json-tools @iconify/json
|
$ yarn
|
||||||
$ node scripts/bundle_icons.js
|
$ yarn build # or: yarn watch
|
||||||
```
|
```
|
||||||
|
|
||||||
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) largely apply to Wakapi as well. You might find answers there.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>What data is sent to Wakapi?</b></summary>
|
<summary><b>What data are sent to Wakapi?</b></summary>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>File names</li>
|
<li>File names</li>
|
||||||
<li>Project names</li>
|
<li>Project names</li>
|
||||||
<li>Editor names</li>
|
<li>Editor names</li>
|
||||||
<li>You computer's host name</li>
|
<li>Your computer's host name</li>
|
||||||
<li>Timestamps for every action you take in your editor</li>
|
<li>Timestamps for every action you take in your editor</li>
|
||||||
<li>...</li>
|
<li>...</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
See the related [WakaTime FAQ section](https://wakatime.com/faq#data-collected) for details.
|
See the related [WakaTime FAQ section](https://wakatime.com/faq#data-collected) for details.
|
||||||
|
|
||||||
If you host Wakapi yourself, you have control over all your data. However, if you use our webservice and are concerned about privacy, you can also [exclude or obfuscate](https://wakatime.com/faq#exclude-paths) certain file- or project names.
|
If you host Wakapi yourself, you have control over all your data. However, if you use our webservice and are concerned about privacy, you can also [exclude or obfuscate](https://wakatime.com/faq#exclude-paths) certain file- or project names.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>What happens if I'm offline?</b></summary>
|
<summary><b>What happens if I'm offline?</b></summary>
|
||||||
|
|
||||||
All data is cached locally on your machine and sent in batches once you're online again.
|
All data are cached locally on your machine and sent in batches once you're online again.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<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>
|
||||||
@ -379,27 +412,27 @@ Wakapi is a small subset of WakaTime and has a lot less features. Cool WakaTime
|
|||||||
<li><a href="https://wakatime.com/share/embed">Embeddable Charts</a></li>
|
<li><a href="https://wakatime.com/share/embed">Embeddable Charts</a></li>
|
||||||
<li>Personal Goals</li>
|
<li>Personal Goals</li>
|
||||||
<li>Team- / Organization Support</li>
|
<li>Team- / Organization Support</li>
|
||||||
<li>Integrations (with GitLab, etc.)</li>
|
<li>Additional Integrations (with GitLab, etc.)</li>
|
||||||
<li>Richer API</li>
|
<li>Richer API</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
WakaTime is worth the price. However, if you only want basic statistics and keep sovereignty over your data, you might want to go with Wakapi.
|
WakaTime is worth the price. However, if you only need basic statistics and like to keep sovereignty over your data, you might want to go with Wakapi.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<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):
|
||||||
|
|
||||||
```
|
```text
|
||||||
|---o---o--------------o---o---|
|
|---o---o--------------o---o---|
|
||||||
| |10s| 3m |10s| |
|
| |10s| 3m |10s| |
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break or were just no heartbeats being sent, e.g. because the developer was starring at the screen find a solution, but not actually typing code.
|
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break, or were just no heartbeats being sent, e.g. because the developer was staring at the screen trying to find a solution, but not actually typing code?
|
||||||
|
|
||||||
<ul>
|
<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
|
||||||
@ -410,8 +443,22 @@ It is unclear how to handle the three minutes in between. Did the developer do a
|
|||||||
Wakapi adds a "padding" of two minutes before the third heartbeat. This is why total times will slightly vary between Wakapi and WakaTime.
|
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 you’ll 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 📓 License
|
## 📓 License
|
||||||
|
|
||||||
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)
|
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)
|
||||||
|
@ -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
|
||||||
@ -14,11 +16,17 @@ app:
|
|||||||
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
|
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
|
||||||
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
||||||
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
|
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
|
||||||
|
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
|
||||||
custom_languages:
|
custom_languages:
|
||||||
vue: Vue
|
vue: Vue
|
||||||
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 +45,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 +71,6 @@ 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
|
||||||
|
skip_migrations: false # whether to intentionally not run database migrations, only use for dev purposes
|
112
config/config.go
112
config/config.go
@ -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,22 @@ 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"`
|
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
|
||||||
Colors map[string]map[string]string `yaml:"-"`
|
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
|
||||||
|
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 +101,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 +142,25 @@ 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
|
SkipMigrations bool `yaml:"skip_migrations" env:"WAKAPI_SKIP_MIGRATIONS"`
|
||||||
Db dbConfig
|
InstanceId string `yaml:"-"` // only temporary, changes between runs
|
||||||
Server serverConfig
|
App appConfig
|
||||||
Sentry sentryConfig
|
Security securityConfig
|
||||||
Mail mailConfig
|
Db dbConfig
|
||||||
|
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 {
|
||||||
@ -197,6 +208,12 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
|
|||||||
if err := db.AutoMigrate(&models.LanguageMapping{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
if err := db.AutoMigrate(&models.LanguageMapping{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,6 +244,23 @@ func (c *appConfig) GetWeeklyReportTime() string {
|
|||||||
return strings.Split(c.ReportTimeWeekly, ",")[1]
|
return strings.Split(c.ReportTimeWeekly, ",")[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *appConfig) HeartbeatsMaxAge() time.Duration {
|
||||||
|
d, _ := time.ParseDuration(c.HeartbeatMaxAge)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dbConfig) IsSQLite() bool {
|
||||||
|
return c.Dialect == "sqlite3"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dbConfig) IsMySQL() bool {
|
||||||
|
return c.Dialect == "mysql"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dbConfig) IsPostgres() bool {
|
||||||
|
return c.Dialect == "postgres"
|
||||||
|
}
|
||||||
|
|
||||||
func (c *serverConfig) GetPublicUrl() string {
|
func (c *serverConfig) GetPublicUrl() string {
|
||||||
return strings.TrimSuffix(c.PublicUrl, "/")
|
return strings.TrimSuffix(c.PublicUrl, "/")
|
||||||
}
|
}
|
||||||
@ -242,12 +276,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) {
|
||||||
@ -273,6 +307,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +364,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(
|
||||||
@ -347,12 +388,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)
|
||||||
}
|
}
|
||||||
@ -362,6 +407,9 @@ func Load(version string) *Config {
|
|||||||
if _, err := time.Parse("15:04", config.App.AggregationTime); err != nil {
|
if _, err := time.Parse("15:04", config.App.AggregationTime); err != nil {
|
||||||
logbuch.Fatal("invalid interval set for aggregation_time")
|
logbuch.Fatal("invalid interval set for aggregation_time")
|
||||||
}
|
}
|
||||||
|
if _, err := time.ParseDuration(config.App.HeartbeatMaxAge); err != nil {
|
||||||
|
logbuch.Fatal("invalid duration set for heartbeat_max_age")
|
||||||
|
}
|
||||||
|
|
||||||
Set(config)
|
Set(config)
|
||||||
return Get()
|
return Get()
|
||||||
|
@ -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
|
||||||
|
@ -109,8 +109,9 @@ var excludedRoutes = []string{
|
|||||||
|
|
||||||
func initSentry(config sentryConfig, debug bool) {
|
func initSentry(config sentryConfig, debug bool) {
|
||||||
if err := sentry.Init(sentry.ClientOptions{
|
if err := sentry.Init(sentry.ClientOptions{
|
||||||
Dsn: config.Dsn,
|
Dsn: config.Dsn,
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
|
AttachStacktrace: true,
|
||||||
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
|
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
|
||||||
if !config.EnableTracing {
|
if !config.EnableTracing {
|
||||||
return sentry.SampledFalse
|
return sentry.SampledFalse
|
||||||
@ -140,7 +141,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
347
data/colors.json
347
data/colors.json
@ -2,291 +2,396 @@
|
|||||||
"languages": {
|
"languages": {
|
||||||
"1C Enterprise": "#814CCC",
|
"1C Enterprise": "#814CCC",
|
||||||
"ABAP": "#E8274B",
|
"ABAP": "#E8274B",
|
||||||
"ActionScript": "#882B0F",
|
|
||||||
"Ada": "#02f88c",
|
|
||||||
"Agda": "#315665",
|
|
||||||
"AGS Script": "#B9D9FF",
|
"AGS Script": "#B9D9FF",
|
||||||
"Alloy": "#64C800",
|
"AL": "#3AA2B5",
|
||||||
"AMPL": "#E6EFBB",
|
"AMPL": "#E6EFBB",
|
||||||
"AngelScript": "#C7D7DC",
|
|
||||||
"ANTLR": "#9DC3FF",
|
"ANTLR": "#9DC3FF",
|
||||||
"API Blueprint": "#2ACCA8",
|
"API Blueprint": "#2ACCA8",
|
||||||
"APL": "#5A8164",
|
"APL": "#8a0707",
|
||||||
"AppleScript": "#101F1F",
|
"ASP.NET": "#9400ff",
|
||||||
"Arc": "#aa2afe",
|
|
||||||
"ASP": "#6a40fd",
|
|
||||||
"AspectJ": "#a957b0",
|
|
||||||
"Assembly": "#6E4C13",
|
|
||||||
"Asymptote": "#4a0c0c",
|
|
||||||
"ATS": "#1ac620",
|
"ATS": "#1ac620",
|
||||||
|
"ActionScript": "#e3491a",
|
||||||
|
"Ada": "#02f88c",
|
||||||
|
"Agda": "#467C91",
|
||||||
|
"Alloy": "#cc5c24",
|
||||||
|
"AngelScript": "#C7D7DC",
|
||||||
|
"Apex": "#1797c0",
|
||||||
|
"Apollo Guidance Computer": "#0B3D91",
|
||||||
|
"AppleScript": "#101F1F",
|
||||||
|
"Arc": "#ca2afe",
|
||||||
|
"Arduino": "#bd79d1",
|
||||||
|
"AspectJ": "#1957b0",
|
||||||
|
"Assembly": "#6E4C13",
|
||||||
|
"Asymptote": "#ff0000",
|
||||||
|
"Augeas": "#62331f",
|
||||||
"AutoHotkey": "#6594b9",
|
"AutoHotkey": "#6594b9",
|
||||||
"AutoIt": "#1C3552",
|
"AutoIt": "#36699B",
|
||||||
"Ballerina": "#FF5000",
|
"Ballerina": "#FF5000",
|
||||||
"Batchfile": "#C1F12E",
|
"Batchfile": "#C1F12E",
|
||||||
|
"Beef": "#a52f4e",
|
||||||
|
"Bison": "#6A463F",
|
||||||
|
"Blade": "#f7523f",
|
||||||
"BlitzMax": "#cd6400",
|
"BlitzMax": "#cd6400",
|
||||||
"Boo": "#d4bec1",
|
"Boo": "#d4bec1",
|
||||||
|
"Boogie": "#c80fa0",
|
||||||
"Brainfuck": "#2F2530",
|
"Brainfuck": "#2F2530",
|
||||||
|
"Browserslist": "#ffd539",
|
||||||
"C": "#555555",
|
"C": "#555555",
|
||||||
"C#": "#178600",
|
"C Sharp": "#178600",
|
||||||
|
"C#": "#5a25a2",
|
||||||
"C++": "#f34b7d",
|
"C++": "#f34b7d",
|
||||||
|
"CSON": "#244776",
|
||||||
|
"CSS": "#563d7c",
|
||||||
"Ceylon": "#dfa535",
|
"Ceylon": "#dfa535",
|
||||||
"Chapel": "#8dc63f",
|
"Chapel": "#8dc63f",
|
||||||
"Cirru": "#ccccff",
|
"Cirru": "#aaaaff",
|
||||||
"Clarion": "#db901e",
|
"Clarion": "#db901e",
|
||||||
"Clean": "#3F85AF",
|
"Classic ASP": "#6a40fd",
|
||||||
|
"Clean": "#3a81ad",
|
||||||
"Click": "#E4E6F3",
|
"Click": "#E4E6F3",
|
||||||
"Clojure": "#db5855",
|
"Clojure": "#db5855",
|
||||||
|
"Closure Templates": "#0d948f",
|
||||||
"CoffeeScript": "#244776",
|
"CoffeeScript": "#244776",
|
||||||
"ColdFusion": "#ed2cd6",
|
"ColdFusion": "#ed2cd6",
|
||||||
|
"ColdFusion CFC": "#ed2cd6",
|
||||||
"Common Lisp": "#3fb68b",
|
"Common Lisp": "#3fb68b",
|
||||||
"Common Workflow Language": "#B5314C",
|
"Common Workflow Language": "#B5314C",
|
||||||
"Component Pascal": "#B0CE4E",
|
"Component Pascal": "#b0ce4e",
|
||||||
"Crystal": "#000100",
|
"Crystal": "#000100",
|
||||||
"CSS": "#563d7c",
|
|
||||||
"Cuda": "#3A4E3A",
|
"Cuda": "#3A4E3A",
|
||||||
"D": "#ba595e",
|
"D": "#fcd46d",
|
||||||
"Dart": "#00B4AB",
|
"DM": "#075ff1",
|
||||||
|
"Dafny": "#FFEC25",
|
||||||
|
"Dart": "#98BAD6",
|
||||||
"DataWeave": "#003a52",
|
"DataWeave": "#003a52",
|
||||||
"DM": "#447265",
|
"Denizen": "#faf094",
|
||||||
|
"Dhall": "#dfafff",
|
||||||
"Dockerfile": "#384d54",
|
"Dockerfile": "#384d54",
|
||||||
"Docker": "#384d54",
|
|
||||||
"Dogescript": "#cca760",
|
"Dogescript": "#cca760",
|
||||||
"Dylan": "#6c616e",
|
"Dylan": "#3ebc27",
|
||||||
"E": "#ccce35",
|
"E": "#ccce35",
|
||||||
"eC": "#913960",
|
|
||||||
"ECL": "#8a1267",
|
"ECL": "#8a1267",
|
||||||
|
"EJS": "#a91e50",
|
||||||
|
"EQ": "#a78649",
|
||||||
|
"Eagle": "#3994bc",
|
||||||
"Eiffel": "#946d57",
|
"Eiffel": "#946d57",
|
||||||
"Elixir": "#6e4a7e",
|
"Elixir": "#6e4a7e",
|
||||||
"Elm": "#60B5CC",
|
"Elm": "#60B5CC",
|
||||||
"Emacs Lisp": "#c065db",
|
"Emacs Lisp": "#c065db",
|
||||||
"EmberScript": "#FFF4F3",
|
"EmberScript": "#f64e3e",
|
||||||
"EQ": "#a78649",
|
"Erlang": "#0faf8d",
|
||||||
"Erlang": "#B83998",
|
|
||||||
"F#": "#b845fc",
|
"F#": "#b845fc",
|
||||||
"F*": "#572e30",
|
"F*": "#572e30",
|
||||||
|
"FLUX": "#33CCFF",
|
||||||
|
"FORTRAN": "#4d41b1",
|
||||||
"Factor": "#636746",
|
"Factor": "#636746",
|
||||||
"Fancy": "#7b9db4",
|
"Fancy": "#7b9db4",
|
||||||
"Fantom": "#14253c",
|
"Fantom": "#dbded5",
|
||||||
"FLUX": "#88ccff",
|
"Faust": "#c37240",
|
||||||
"Forth": "#341708",
|
"Forth": "#341708",
|
||||||
"Fortran": "#4d41b1",
|
"Fortran": "#4d41b1",
|
||||||
"FreeMarker": "#0050b2",
|
"FreeMarker": "#0050b2",
|
||||||
"Frege": "#00cafe",
|
"Frege": "#00cafe",
|
||||||
"Game Maker Language": "#71b417",
|
"Futhark": "#5f021f",
|
||||||
|
"G-code": "#D08CF2",
|
||||||
|
"GAML": "#FFC766",
|
||||||
"GDScript": "#355570",
|
"GDScript": "#355570",
|
||||||
|
"Game Maker Language": "#8ad353",
|
||||||
"Genie": "#fb855d",
|
"Genie": "#fb855d",
|
||||||
"Gherkin": "#5B2063",
|
"Gherkin": "#5B2063",
|
||||||
"Glyph": "#c1ac7f",
|
"Glyph": "#e4cc98",
|
||||||
"Gnuplot": "#f0a9f0",
|
"Gnuplot": "#f0a9f0",
|
||||||
"Go": "#00ADD8",
|
"Go": "#375eab",
|
||||||
"Golo": "#88562A",
|
"Golo": "#f6a51f",
|
||||||
"Gosu": "#82937f",
|
"Gosu": "#82937f",
|
||||||
"Grammatical Framework": "#79aa7a",
|
"Grammatical Framework": "#ff0000",
|
||||||
|
"GraphQL": "#e10098",
|
||||||
"Groovy": "#e69f56",
|
"Groovy": "#e69f56",
|
||||||
|
"HTML": "#e44b23",
|
||||||
"Hack": "#878787",
|
"Hack": "#878787",
|
||||||
|
"Haml": "#ece2a9",
|
||||||
|
"Handlebars": "#f7931e",
|
||||||
"Harbour": "#0e60e3",
|
"Harbour": "#0e60e3",
|
||||||
"Haskell": "#5e5086",
|
"Haskell": "#29b544",
|
||||||
"Haxe": "#df7900",
|
"Haxe": "#f7941e",
|
||||||
"HiveQL": "#dce200",
|
"HiveQL": "#dce200",
|
||||||
"HTML": "#e34c26",
|
"HolyC": "#ffefaf",
|
||||||
"Hy": "#7790B2",
|
"Hy": "#7891b1",
|
||||||
"IDL": "#a3522f",
|
"IDL": "#e3592c",
|
||||||
|
"IGOR Pro": "#0000cc",
|
||||||
"Idris": "#b30000",
|
"Idris": "#b30000",
|
||||||
|
"ImageJ Macro": "#99AAFF",
|
||||||
"Io": "#a9188d",
|
"Io": "#a9188d",
|
||||||
"Ioke": "#078193",
|
"Ioke": "#078193",
|
||||||
"Isabelle": "#FEFE00",
|
"Isabelle": "#fdcd00",
|
||||||
"J": "#9EEDFF",
|
"J": "#9EEDFF",
|
||||||
|
"JFlex": "#DBCA00",
|
||||||
|
"JSONiq": "#40d47e",
|
||||||
"Java": "#b07219",
|
"Java": "#b07219",
|
||||||
"JavaScript": "#f1e05a",
|
"JavaScript": "#f1e05a",
|
||||||
"Jolie": "#843179",
|
"Jolie": "#843179",
|
||||||
"JSONiq": "#40d47e",
|
|
||||||
"Jsonnet": "#0064bd",
|
"Jsonnet": "#0064bd",
|
||||||
"Julia": "#a270ba",
|
"Julia": "#a270ba",
|
||||||
"Jupyter Notebook": "#DA5B0B",
|
"Jupyter Notebook": "#DA5B0B",
|
||||||
|
"KRL": "#f5c800",
|
||||||
|
"Kaitai Struct": "#773b37",
|
||||||
"Kotlin": "#F18E33",
|
"Kotlin": "#F18E33",
|
||||||
"KRL": "#28430A",
|
"LFE": "#004200",
|
||||||
"Lasso": "#999999",
|
|
||||||
"Lex": "#DBCA00",
|
|
||||||
"LFE": "#4C3023",
|
|
||||||
"LiveScript": "#499886",
|
|
||||||
"LLVM": "#185619",
|
"LLVM": "#185619",
|
||||||
"LOLCODE": "#cc9900",
|
"LOLCODE": "#cc9900",
|
||||||
"LookML": "#652B81",
|
|
||||||
"LSL": "#3d9970",
|
"LSL": "#3d9970",
|
||||||
"Lua": "#000080",
|
"Lark": "#0b130f",
|
||||||
"Makefile": "#427819",
|
"Lasso": "#2584c3",
|
||||||
"Mask": "#f97732",
|
"Latte": "#A8FF97",
|
||||||
|
"Less": "#1d365d",
|
||||||
|
"Lex": "#DBCA00",
|
||||||
|
"Liquid": "#67b8de",
|
||||||
|
"LiveScript": "#499886",
|
||||||
|
"LookML": "#652B81",
|
||||||
|
"Lua": "#fa1fa1",
|
||||||
"MATLAB": "#e16737",
|
"MATLAB": "#e16737",
|
||||||
"Max": "#c4a79c",
|
|
||||||
"MAXScript": "#00a6a6",
|
"MAXScript": "#00a6a6",
|
||||||
"mcfunction": "#E22837",
|
"MLIR": "#5EC8DB",
|
||||||
"Mercury": "#ff2b2b",
|
"MQL4": "#62A8D6",
|
||||||
|
"MQL5": "#4A76B8",
|
||||||
|
"MTML": "#0095d9",
|
||||||
|
"Macaulay2": "#d8ffff",
|
||||||
|
"Makefile": "#427819",
|
||||||
|
"Markdown": "#083fa1",
|
||||||
|
"Marko": "#42bff2",
|
||||||
|
"Mask": "#f97732",
|
||||||
|
"Matlab": "#bb92ac",
|
||||||
|
"Max": "#ce279c",
|
||||||
|
"Mercury": "#abcdef",
|
||||||
"Meson": "#007800",
|
"Meson": "#007800",
|
||||||
"Metal": "#8f14e9",
|
"Metal": "#8f14e9",
|
||||||
"Mirah": "#c7a938",
|
"Mirah": "#c7a938",
|
||||||
"Modula-3": "#223388",
|
"Modula-3": "#223388",
|
||||||
"MQL4": "#62A8D6",
|
"Mustache": "#724b3b",
|
||||||
"MQL5": "#4A76B8",
|
|
||||||
"MTML": "#b7e1f4",
|
|
||||||
"NCL": "#28431f",
|
"NCL": "#28431f",
|
||||||
|
"NWScript": "#111522",
|
||||||
"Nearley": "#990000",
|
"Nearley": "#990000",
|
||||||
"Nemerle": "#3d3c6e",
|
"Nemerle": "#0d3c6e",
|
||||||
"nesC": "#94B0C7",
|
|
||||||
"NetLinx": "#0aa0ff",
|
"NetLinx": "#0aa0ff",
|
||||||
"NetLinx+ERB": "#747faa",
|
"NetLinx+ERB": "#747faa",
|
||||||
"NetLogo": "#ff6375",
|
"NetLogo": "#ff2b2b",
|
||||||
"NewLisp": "#87AED7",
|
"NewLisp": "#eedd66",
|
||||||
"Nextflow": "#3ac486",
|
"Nextflow": "#3ac486",
|
||||||
"Nim": "#37775b",
|
"Nim": "#ffc200",
|
||||||
"Nit": "#009917",
|
"Nimrod": "#37775b",
|
||||||
"Nix": "#7e7eff",
|
"Nit": "#0d8921",
|
||||||
|
"Nix": "#7070ff",
|
||||||
"Nu": "#c9df40",
|
"Nu": "#c9df40",
|
||||||
"Objective-C": "#438eff",
|
"NumPy": "#9C8AF9",
|
||||||
"Objective-C++": "#6866fb",
|
"Nunjucks": "#3d8137",
|
||||||
"Objective-J": "#ff0c5a",
|
|
||||||
"OCaml": "#3be133",
|
"OCaml": "#3be133",
|
||||||
|
"ObjectScript": "#424893",
|
||||||
|
"Objective-C": "#438eff",
|
||||||
|
"Objective-C++": "#4886FC",
|
||||||
|
"Objective-J": "#ff0c5a",
|
||||||
|
"Odin": "#60AFFE",
|
||||||
"Omgrofl": "#cabbff",
|
"Omgrofl": "#cabbff",
|
||||||
"ooc": "#b0b77e",
|
|
||||||
"Opal": "#f7ede0",
|
"Opal": "#f7ede0",
|
||||||
"Oxygene": "#cdd0e3",
|
"OpenQASM": "#AA70FF",
|
||||||
"Oz": "#fab738",
|
"Org": "#77aa99",
|
||||||
|
"Oxygene": "#5a63a3",
|
||||||
|
"Oz": "#fcaf3e",
|
||||||
"P4": "#7055b5",
|
"P4": "#7055b5",
|
||||||
|
"PAWN": "#dbb284",
|
||||||
|
"PHP": "#4F5D95",
|
||||||
|
"PLSQL": "#dad8d8",
|
||||||
"Pan": "#cc0000",
|
"Pan": "#cc0000",
|
||||||
"Papyrus": "#6600cc",
|
"Papyrus": "#6600cc",
|
||||||
"Parrot": "#f3ca0a",
|
"Parrot": "#f3ca0a",
|
||||||
"Pascal": "#E3F171",
|
"Pascal": "#b0ce4e",
|
||||||
"Pawn": "#dbb284",
|
"Pawn": "#dbb284",
|
||||||
"Pep8": "#C76F5B",
|
"Pep8": "#C76F5B",
|
||||||
"Perl": "#0298c3",
|
"Perl": "#0298c3",
|
||||||
"Perl 6": "#0000fb",
|
"Perl6": "#0298c3",
|
||||||
"PHP": "#4F5D95",
|
|
||||||
"PigLatin": "#fcd7de",
|
"PigLatin": "#fcd7de",
|
||||||
"Pike": "#005390",
|
"Pike": "#066ab2",
|
||||||
"PLSQL": "#dad8d8",
|
|
||||||
"PogoScript": "#d80074",
|
"PogoScript": "#d80074",
|
||||||
"PostScript": "#da291c",
|
"PostScript": "#da291c",
|
||||||
"PowerBuilder": "#8f0f8d",
|
"PowerBuilder": "#8f0f8d",
|
||||||
"PowerShell": "#012456",
|
"PowerShell": "#012456",
|
||||||
"Processing": "#0096D8",
|
"Prisma": "#0c344b",
|
||||||
|
"Processing": "#2779ab",
|
||||||
"Prolog": "#74283c",
|
"Prolog": "#74283c",
|
||||||
"Propeller Spin": "#7fa2a7",
|
"Propeller Spin": "#2b446d",
|
||||||
"Puppet": "#302B6D",
|
"Pug": "#a86454",
|
||||||
|
"Puppet": "#cc5555",
|
||||||
|
"Pure Data": "#91de79",
|
||||||
"PureBasic": "#5a6986",
|
"PureBasic": "#5a6986",
|
||||||
"PureScript": "#1D222D",
|
"PureScript": "#bcdc53",
|
||||||
"Python": "#3572A5",
|
"Python": "#3581ba",
|
||||||
"q": "#0040cd",
|
"Q#": "#fed659",
|
||||||
"QML": "#44a51c",
|
"QML": "#44a51c",
|
||||||
|
"Qt Script": "#00b841",
|
||||||
"Quake": "#882233",
|
"Quake": "#882233",
|
||||||
"R": "#198CE7",
|
"R": "#198ce7",
|
||||||
"Racket": "#3c5caa",
|
|
||||||
"Ragel": "#9d5200",
|
|
||||||
"RAML": "#77d9fb",
|
"RAML": "#77d9fb",
|
||||||
|
"RUNOFF": "#665a4e",
|
||||||
|
"Racket": "#ae17ff",
|
||||||
|
"Ragel": "#9d5200",
|
||||||
|
"Ragel in Ruby Host": "#ff9c2e",
|
||||||
|
"Raku": "#0000fb",
|
||||||
"Rascal": "#fffaa0",
|
"Rascal": "#fffaa0",
|
||||||
|
"ReScript": "#ed5051",
|
||||||
|
"Reason": "#ff5847",
|
||||||
"Rebol": "#358a5b",
|
"Rebol": "#358a5b",
|
||||||
"Red": "#f50000",
|
"Record Jar": "#0673ba",
|
||||||
|
"Red": "#ee0000",
|
||||||
"Ren'Py": "#ff7f7f",
|
"Ren'Py": "#ff7f7f",
|
||||||
"Ring": "#2D54CB",
|
"Ring": "#2D54CB",
|
||||||
|
"Riot": "#A71E49",
|
||||||
"Roff": "#ecdebe",
|
"Roff": "#ecdebe",
|
||||||
"Rouge": "#cc0088",
|
"Rouge": "#cc0088",
|
||||||
"Ruby": "#701516",
|
"Ruby": "#701516",
|
||||||
"RUNOFF": "#665a4e",
|
|
||||||
"Rust": "#dea584",
|
"Rust": "#dea584",
|
||||||
|
"SAS": "#1E90FF",
|
||||||
|
"SCSS": "#c6538c",
|
||||||
|
"SQF": "#FFCB1F",
|
||||||
|
"SRecode Template": "#348a34",
|
||||||
|
"SVG": "#ff9900",
|
||||||
"SaltStack": "#646464",
|
"SaltStack": "#646464",
|
||||||
"SAS": "#B34936",
|
"Sass": "#a53b70",
|
||||||
"Scala": "#c22d40",
|
"Scala": "#7dd3b0",
|
||||||
|
"Scaml": "#bd181a",
|
||||||
"Scheme": "#1e4aec",
|
"Scheme": "#1e4aec",
|
||||||
"sed": "#64b970",
|
|
||||||
"Self": "#0579aa",
|
"Self": "#0579aa",
|
||||||
"Shell": "#89e051",
|
"Shell": "#5861ce",
|
||||||
"Shen": "#120F14",
|
"Shen": "#120F14",
|
||||||
"Slash": "#007eff",
|
"Slash": "#007eff",
|
||||||
"Slice": "#003fa2",
|
"Slice": "#003fa2",
|
||||||
|
"Slim": "#ff8877",
|
||||||
|
"SmPL": "#c94949",
|
||||||
"Smalltalk": "#596706",
|
"Smalltalk": "#596706",
|
||||||
"Solidity": "#AA6746",
|
"Solidity": "#AA6746",
|
||||||
"SourcePawn": "#5c7611",
|
"SourcePawn": "#f69e1d",
|
||||||
"SQF": "#3F3F3F",
|
|
||||||
"Squirrel": "#800000",
|
"Squirrel": "#800000",
|
||||||
"SRecode Template": "#348a34",
|
|
||||||
"Stan": "#b2011d",
|
"Stan": "#b2011d",
|
||||||
"Standard ML": "#dc566d",
|
"Standard ML": "#dc566d",
|
||||||
|
"Starlark": "#76d275",
|
||||||
|
"Stylus": "#ff6347",
|
||||||
"SuperCollider": "#46390b",
|
"SuperCollider": "#46390b",
|
||||||
"Svelte": "#ff3e00",
|
"Svelte": "#ff3e00",
|
||||||
"Swift": "#ffac45",
|
"Swift": "#ffac45",
|
||||||
"SystemVerilog": "#DAE1C2",
|
"SystemVerilog": "#343761",
|
||||||
"Tcl": "#e4cc98",
|
|
||||||
"Terra": "#00004c",
|
|
||||||
"TeX": "#3D6117",
|
|
||||||
"TI Program": "#A0AA87",
|
"TI Program": "#A0AA87",
|
||||||
"Turing": "#cf142b",
|
"Tcl": "#e4cc98",
|
||||||
"TypeScript": "#2b7489",
|
"TeX": "#3D6117",
|
||||||
|
"Terra": "#00004c",
|
||||||
|
"Turing": "#45f715",
|
||||||
|
"Twig": "#c1d026",
|
||||||
|
"TypeScript": "#31859c",
|
||||||
|
"Unified Parallel C": "#755223",
|
||||||
|
"Uno": "#9933cc",
|
||||||
"UnrealScript": "#a54c4d",
|
"UnrealScript": "#a54c4d",
|
||||||
"Vala": "#fbe5cd",
|
"V": "#4f87c4",
|
||||||
"VCL": "#148AA8",
|
"VBA": "#867db1",
|
||||||
"Verilog": "#b2b7f8",
|
"VBScript": "#15dcdc",
|
||||||
"VHDL": "#adb2cb",
|
"VCL": "#0298c3",
|
||||||
|
"VHDL": "#543978",
|
||||||
|
"Vala": "#ee7d06",
|
||||||
|
"Verilog": "#848bf3",
|
||||||
"Vim script": "#199f4b",
|
"Vim script": "#199f4b",
|
||||||
|
"VimL": "#199c4b",
|
||||||
"Visual Basic": "#945db7",
|
"Visual Basic": "#945db7",
|
||||||
"Volt": "#1F1F1F",
|
"Visual Basic .NET": "#945db7",
|
||||||
|
"Volt": "#0098db",
|
||||||
"Vue": "#2c3e50",
|
"Vue": "#2c3e50",
|
||||||
"wdl": "#42f1f4",
|
"Web Ontology Language": "#3994bc",
|
||||||
"WebAssembly": "#04133b",
|
"WebAssembly": "#04133b",
|
||||||
"wisp": "#7582D1",
|
"Wollok": "#a23738",
|
||||||
"X10": "#4B6BEF",
|
"X10": "#4B6BEF",
|
||||||
"xBase": "#403a40",
|
|
||||||
"XC": "#99DA07",
|
"XC": "#99DA07",
|
||||||
"XQuery": "#5232e7",
|
"XQuery": "#2700e2",
|
||||||
"XSLT": "#EB8CEB",
|
"XSLT": "#EB8CEB",
|
||||||
"Yacc": "#4B6C4B",
|
"YAML": "#cb171e",
|
||||||
"YARA": "#220000",
|
"YARA": "#220000",
|
||||||
"YASnippet": "#32AB90",
|
"YASnippet": "#32AB90",
|
||||||
|
"Yacc": "#4B6C4B",
|
||||||
"ZAP": "#0d665e",
|
"ZAP": "#0d665e",
|
||||||
|
"ZIL": "#dc75e5",
|
||||||
|
"ZenScript": "#00BCD1",
|
||||||
"Zephir": "#118f9e",
|
"Zephir": "#118f9e",
|
||||||
"Zig": "#ec915c",
|
"Zig": "#ec915c",
|
||||||
"ZIL": "#dc75e5"
|
"cpp": "#f34b7d",
|
||||||
|
"eC": "#913960",
|
||||||
|
"edn": "#db5855",
|
||||||
|
"mIRC Script": "#3d57c3",
|
||||||
|
"mcfunction": "#E22837",
|
||||||
|
"nesC": "#ffce3b",
|
||||||
|
"ooc": "#b0b77e",
|
||||||
|
"q": "#0040cd",
|
||||||
|
"sed": "#64b970",
|
||||||
|
"wdl": "#42f1f4",
|
||||||
|
"wisp": "#7582D1",
|
||||||
|
"xBase": "#3a4040",
|
||||||
|
"Other": "#1f9aef"
|
||||||
},
|
},
|
||||||
"editors": {
|
"editors": {
|
||||||
|
"Adobe XD": "#fd27bc",
|
||||||
"Android Studio": "#99cd00",
|
"Android Studio": "#99cd00",
|
||||||
"AppCode": "#04dbde",
|
"AppCode": "#04dbde",
|
||||||
"Aptana": "#ec8623",
|
"Aptana": "#ec8623",
|
||||||
"Atom": "#49b77e",
|
"Atom": "#49b77e",
|
||||||
"Azure Data Studio": "#0271c6",
|
"Azure Data Studio": "#0271c6",
|
||||||
"Blender": "#fb8007",
|
"Blender": "#fb8007",
|
||||||
|
"BlueJ": "#5d89af",
|
||||||
"Brackets": "#067dc3",
|
"Brackets": "#067dc3",
|
||||||
"Chrome": "#fdd308",
|
"Chrome": "#fdd308",
|
||||||
"CLion": "#14c9a5",
|
"CLion": "#14c9a5",
|
||||||
"Cloud9": "#25a6d9",
|
"Cloud9": "#25a6d9",
|
||||||
"Coda": "#3e8e1c",
|
"Coda": "#3e8e1c",
|
||||||
|
"Code: :Blocks": "#d0ce71",
|
||||||
|
"Code::Blocks": "#d0ce71",
|
||||||
|
"CodeLite": "#1892e5",
|
||||||
"CodeTasty": "#7368a8",
|
"CodeTasty": "#7368a8",
|
||||||
"DataGrip": "#907cf2",
|
"DataGrip": "#907cf2",
|
||||||
"DBeaver": "#897363",
|
"DBeaver": "#897363",
|
||||||
"Eclipse": "#443582",
|
"Eclipse": "#443582",
|
||||||
"Emacs": "#8c76c3",
|
"Emacs": "#8c76c3",
|
||||||
|
"Embarcadero Delphi": "#d9242a",
|
||||||
|
"EmEditor": "#ed3103",
|
||||||
"Eric": "#423f13",
|
"Eric": "#423f13",
|
||||||
"Excel": "#0f753c",
|
"Excel": "#0f753c",
|
||||||
|
"Figma": "#c7b9ff",
|
||||||
|
"Firefox": "#d96527",
|
||||||
"Flash Builder": "#aca3a4",
|
"Flash Builder": "#aca3a4",
|
||||||
|
"Geany": "#fbec75",
|
||||||
"Gedit": "#872114",
|
"Gedit": "#872114",
|
||||||
"GoLand": "#bd4ffc",
|
"GoLand": "#bd4ffc",
|
||||||
"HBuilder X": "#1ba334",
|
"HBuilder X": "#1ba334",
|
||||||
"IntelliJ IDEA": "#237ce2",
|
"IntelliJ IDEA": "#2876e1",
|
||||||
"IntelliJ": "#237ce2",
|
"IntelliJ": "#2876e1",
|
||||||
"Kakoune": "#dd5f4a",
|
"Kakoune": "#dd5f4a",
|
||||||
"Kate": "#3f4040",
|
"Kate": "#3f4040",
|
||||||
|
"KDevelop": "#22a273",
|
||||||
"Komodo": "#fcb414",
|
"Komodo": "#fcb414",
|
||||||
|
"Light Table": "#007ac1",
|
||||||
|
"MacRabbit Espresso": "#e6593f",
|
||||||
"Micro": "#2c3494",
|
"Micro": "#2c3494",
|
||||||
"MonoDevelop": "#6185b3",
|
"MonoDevelop": "#6185b3",
|
||||||
|
"MySQL Workbench": "#245279",
|
||||||
|
"Neovim": "#068304",
|
||||||
"NetBeans": "#f1f6e2",
|
"NetBeans": "#f1f6e2",
|
||||||
"Notepad++": "#9ecf54",
|
"Notepad++": "#9ecf54",
|
||||||
"Nova": "#ff054a",
|
"Nova": "#ff054a",
|
||||||
"Onivim": "#ee848e",
|
"Onivim": "#ee848e",
|
||||||
|
"Photoshop": "#0a0054",
|
||||||
"PhpStorm": "#d93ac1",
|
"PhpStorm": "#d93ac1",
|
||||||
"PowerPoint": "#c6421f",
|
"PowerPoint": "#c6421f",
|
||||||
"Processing": "#6a7152",
|
"Processing": "#6a7152",
|
||||||
"PyCharm": "#d2ee5c",
|
"PyCharm": "#d2ee5c",
|
||||||
"Pymakr": "#323d4f",
|
"Pymakr": "#323d4f",
|
||||||
|
"QtCreator": "#7fc342",
|
||||||
"Rider": "#f7a415",
|
"Rider": "#f7a415",
|
||||||
|
"RStudio": "#2369c7",
|
||||||
"RubyMine": "#ff6336",
|
"RubyMine": "#ff6336",
|
||||||
"Sketch": "#fdad00",
|
"Sketch": "#fdad00",
|
||||||
"SlickEdit": "#57ca57",
|
"SlickEdit": "#57ca57",
|
||||||
|
"Spyder": "#ee181e",
|
||||||
"SQL Server Management Studio": "#ffb901",
|
"SQL Server Management Studio": "#ffb901",
|
||||||
"Sublime Text": "#ff9800",
|
"Sublime Text": "#ff9800",
|
||||||
"Terminal": "#133f1c",
|
"Terminal": "#133f1c",
|
||||||
@ -297,29 +402,17 @@
|
|||||||
"Visual Studio": "#9460cd",
|
"Visual Studio": "#9460cd",
|
||||||
"VS Code": "#027acd",
|
"VS Code": "#027acd",
|
||||||
"VSCode": "#027acd",
|
"VSCode": "#027acd",
|
||||||
|
"WebMatrix": "#aeaeae",
|
||||||
"WebStorm": "#00c6d7",
|
"WebStorm": "#00c6d7",
|
||||||
|
"Wing": "#b3b3b3",
|
||||||
"Word": "#0f4091",
|
"Word": "#0f4091",
|
||||||
"WPS Office": "#fc6143",
|
"WPS Office": "#fc6143",
|
||||||
"Xamarin": "#3598db",
|
"Xamarin": "#3598db",
|
||||||
"Xcode": "#3fa7e4",
|
"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": {
|
"operating_systems": {
|
||||||
"Linux": "#f0b912",
|
"Linux": "#f0b912",
|
||||||
"Windows": "#00b7ee",
|
"Windows": "#00b7ee",
|
||||||
"Mac": "#4d66cb"
|
"Mac": "#4d66cb"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,3 +22,8 @@ services:
|
|||||||
POSTGRES_USER: "wakapi"
|
POSTGRES_USER: "wakapi"
|
||||||
POSTGRES_PASSWORD: "choose-a-password"
|
POSTGRES_PASSWORD: "choose-a-password"
|
||||||
POSTGRES_DB: "wakapi"
|
POSTGRES_DB: "wakapi"
|
||||||
|
volumes:
|
||||||
|
- wakapi-db-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
wakapi-db-data: {}
|
||||||
|
@ -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
|
||||||
|
78
go.mod
78
go.mod
@ -1,39 +1,75 @@
|
|||||||
module github.com/muety/wakapi
|
module github.com/muety/wakapi
|
||||||
|
|
||||||
go 1.16
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69
|
||||||
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/duke-git/lancet/v2 v2.0.4
|
||||||
|
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/getsentry/sentry-go v0.13.0
|
||||||
github.com/getsentry/sentry-go v0.10.0
|
github.com/go-co-op/gocron v1.13.0
|
||||||
github.com/go-co-op/gocron v1.5.0
|
|
||||||
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/jinzhu/configor v1.2.1
|
github.com/jinzhu/configor v1.2.1
|
||||||
github.com/leandro-lugaresi/hub v1.1.1
|
github.com/leandro-lugaresi/hub v1.1.1
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/lpar/gzipped/v2 v2.0.2
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.1
|
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e
|
||||||
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.8.1
|
||||||
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-20220411220226-7b82a4e95df4
|
||||||
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
|
gorm.io/driver/mysql v1.3.3
|
||||||
golang.org/x/text v0.3.6 // indirect
|
gorm.io/driver/postgres v1.3.4
|
||||||
golang.org/x/tools v0.1.0 // indirect
|
gorm.io/driver/sqlite v1.3.1
|
||||||
gorm.io/driver/mysql v1.0.6
|
gorm.io/gorm v1.23.4
|
||||||
gorm.io/driver/postgres v1.0.8
|
)
|
||||||
gorm.io/driver/sqlite v1.1.4
|
|
||||||
gorm.io/gorm v1.21.9
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.6 // indirect
|
||||||
|
github.com/go-openapi/swag v0.21.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.11.0 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
|
github.com/jackc/pgtype v1.10.0 // indirect
|
||||||
|
github.com/jackc/pgx/v4 v4.15.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.2.0 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 // indirect
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
golang.org/x/tools v0.1.10 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
)
|
)
|
||||||
|
563
go.sum
563
go.sum
@ -1,12 +1,8 @@
|
|||||||
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/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/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
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=
|
||||||
@ -15,199 +11,77 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
|
|||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 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/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/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/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/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/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-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/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=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
github.com/duke-git/lancet/v2 v2.0.4 h1:IvMurTpL0cGhQmGPtkCge2eCkuiu3USQtglZJnKXxEo=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/duke-git/lancet/v2 v2.0.4/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
|
||||||
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/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/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/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/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/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
|
||||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
|
||||||
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/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g=
|
|
||||||
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
|
||||||
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/go-co-op/gocron v1.13.0 h1:BjkuNImPy5NuIPEifhWItFG7pYyr27cyjS6BN9w/D4c=
|
||||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
github.com/go-co-op/gocron v1.13.0/go.mod h1:GD5EIEly1YNW+LovFVx5dzbYVcIc8544K99D8UVRpGo=
|
||||||
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.5.0/go.mod h1:7MgKum7jD7YgIRj7Zx7K1iJKAf1MlSIsEieRl18+KyU=
|
|
||||||
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-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
|
||||||
github.com/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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
||||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||||
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||||
github.com/go-openapi/spec v0.20.2 h1:pFPUZsiIbZ20kLUcuCGeuQWG735fPMxW7wHF9BWlnQU=
|
github.com/go-openapi/spec v0.20.2 h1:pFPUZsiIbZ20kLUcuCGeuQWG735fPMxW7wHF9BWlnQU=
|
||||||
github.com/go-openapi/spec v0.20.2/go.mod h1:RW6Xcbs6LOyWLU/mXGdzn2Qc+3aj+ASfI7rvSZh1Vls=
|
github.com/go-openapi/spec v0.20.2/go.mod h1:RW6Xcbs6LOyWLU/mXGdzn2Qc+3aj+ASfI7rvSZh1Vls=
|
||||||
|
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||||
|
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
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-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
|
||||||
|
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
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/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
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.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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0 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/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/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/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
|
||||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
|
||||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
|
||||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
|
||||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
|
||||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
|
||||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
|
||||||
github.com/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/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/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/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
|
||||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
|
||||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
|
||||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
@ -215,245 +89,117 @@ 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.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ=
|
||||||
|
github.com/jackc/pgconn v1.11.0/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/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
github.com/jackc/pgproto3/v2 v2.0.0-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.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
|
||||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||||
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
|
||||||
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.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
|
||||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
|
||||||
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
|
|
||||||
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.1/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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
|
||||||
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/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d/go.mod h1:3FSWkzk9h42opyV0o357Fq6gsLF/A6MI/qOca9kKobY=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
|
||||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
|
||||||
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/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/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/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
|
||||||
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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
|
||||||
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.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
|
||||||
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=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
|
||||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
|
||||||
github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeKl/aw8=
|
github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeKl/aw8=
|
||||||
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
|
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.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/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.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.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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e h1:bR8DQ4ZfItytLJwRlrLOPUHd5z18V6tECwYQFy8W+8g=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.1 h1:L60q1+q7cXE4JeEJJKMnh2brFIe3rZxCihYAB61ypAY=
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.1/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/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.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
|
||||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
|
||||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
|
||||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
|
||||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/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.7.0/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/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/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/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/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/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/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/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/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
|
||||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
|
||||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
|
||||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
|
||||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.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,41 +213,17 @@ 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/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||||
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 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/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
|
||||||
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/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/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/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
|
||||||
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,117 +231,74 @@ 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-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-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-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-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
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/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/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/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-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-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=
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-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/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
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-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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-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-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=
|
||||||
@ -627,76 +306,38 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
|
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
|
||||||
|
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 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/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/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/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/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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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-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.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
|
||||||
gorm.io/driver/mysql v1.0.6/go.mod h1:KdrTanmfLPPyAOeYGyG+UpDys7/7eeWT1zCq+oekYnU=
|
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
|
||||||
gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8=
|
gorm.io/driver/postgres v1.3.4 h1:evZ7plF+Bp+Lr1mO5NdPvd6M/N98XtwHixGB+y7fdEQ=
|
||||||
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
gorm.io/driver/postgres v1.3.4/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw=
|
||||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=
|
||||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=
|
||||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg=
|
||||||
gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E=
|
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
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=
|
|
||||||
|
135
main.go
135
main.go
@ -2,21 +2,25 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"github.com/muety/wakapi/migrations"
|
||||||
"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/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"
|
||||||
"github.com/muety/wakapi/migrations"
|
|
||||||
"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"
|
||||||
@ -49,8 +53,11 @@ var (
|
|||||||
heartbeatRepository repositories.IHeartbeatRepository
|
heartbeatRepository repositories.IHeartbeatRepository
|
||||||
userRepository repositories.IUserRepository
|
userRepository repositories.IUserRepository
|
||||||
languageMappingRepository repositories.ILanguageMappingRepository
|
languageMappingRepository repositories.ILanguageMappingRepository
|
||||||
|
projectLabelRepository repositories.IProjectLabelRepository
|
||||||
summaryRepository repositories.ISummaryRepository
|
summaryRepository repositories.ISummaryRepository
|
||||||
keyValueRepository repositories.IKeyValueRepository
|
keyValueRepository repositories.IKeyValueRepository
|
||||||
|
diagnosticsRepository repositories.IDiagnosticsRepository
|
||||||
|
metricsRepository *repositories.MetricsRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -58,11 +65,14 @@ var (
|
|||||||
heartbeatService services.IHeartbeatService
|
heartbeatService services.IHeartbeatService
|
||||||
userService services.IUserService
|
userService services.IUserService
|
||||||
languageMappingService services.ILanguageMappingService
|
languageMappingService services.ILanguageMappingService
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -111,8 +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;")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.IsDev() {
|
if config.IsDev() {
|
||||||
@ -128,26 +138,34 @@ func main() {
|
|||||||
defer sqlDb.Close()
|
defer sqlDb.Close()
|
||||||
|
|
||||||
// Migrate database schema
|
// Migrate database schema
|
||||||
migrations.Run(db, config)
|
if !config.SkipMigrations {
|
||||||
|
migrations.Run(db, config)
|
||||||
|
}
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
aliasRepository = repositories.NewAliasRepository(db)
|
aliasRepository = repositories.NewAliasRepository(db)
|
||||||
heartbeatRepository = repositories.NewHeartbeatRepository(db)
|
heartbeatRepository = repositories.NewHeartbeatRepository(db)
|
||||||
userRepository = repositories.NewUserRepository(db)
|
userRepository = repositories.NewUserRepository(db)
|
||||||
languageMappingRepository = repositories.NewLanguageMappingRepository(db)
|
languageMappingRepository = repositories.NewLanguageMappingRepository(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)
|
||||||
|
metricsRepository = repositories.NewMetricsRepository(db)
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
aliasService = services.NewAliasService(aliasRepository)
|
|
||||||
userService = services.NewUserService(userRepository)
|
|
||||||
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
|
|
||||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
|
||||||
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService)
|
|
||||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
|
||||||
mailService = mail.NewMailService()
|
mailService = mail.NewMailService()
|
||||||
|
aliasService = services.NewAliasService(aliasRepository)
|
||||||
|
userService = services.NewUserService(mailService, userRepository)
|
||||||
|
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
|
||||||
|
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
|
||||||
|
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
||||||
|
durationService = services.NewDurationService(heartbeatService)
|
||||||
|
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
|
||||||
|
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||||
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
|
||||||
@ -161,31 +179,47 @@ func main() {
|
|||||||
healthApiHandler := api.NewHealthApiHandler(db)
|
healthApiHandler := api.NewHealthApiHandler(db)
|
||||||
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, metricsRepository)
|
||||||
|
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
|
||||||
|
avatarHandler := api.NewAvatarHandler()
|
||||||
|
badgeHandler := api.NewBadgeHandler(userService, summaryService)
|
||||||
|
|
||||||
// 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
|
||||||
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
|
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
|
||||||
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService, mailService)
|
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
|
||||||
homeHandler := routes.NewHomeHandler(keyValueService)
|
homeHandler := routes.NewHomeHandler(keyValueService)
|
||||||
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())
|
||||||
@ -198,29 +232,42 @@ 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)
|
||||||
|
badgeHandler.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),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Listen HTTP
|
// Listen HTTP
|
||||||
@ -228,7 +275,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 != "" {
|
||||||
@ -236,8 +283,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,8 +294,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +332,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)
|
||||||
@ -286,6 +361,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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
37
migrations/20210806_remove_persisted_project_labels.go
Normal file
37
migrations/20210806_remove_persisted_project_labels.go
Normal 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)
|
||||||
|
}
|
52
migrations/20211215_migrate_id_to_bigint.go
Normal file
52
migrations/20211215_migrate_id_to_bigint.go
Normal 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)
|
||||||
|
}
|
48
migrations/20212212_total_summary_heartbeats.go
Normal file
48
migrations/20212212_total_summary_heartbeats.go
Normal 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)
|
||||||
|
}
|
56
migrations/20220317_align_num_heartbeats.go
Normal file
56
migrations/20220317_align_num_heartbeats.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "20220317-align_num_heartbeats"
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logbuch.Info("this may take a while!")
|
||||||
|
|
||||||
|
// find all summaries whose num_heartbeats is zero even though they have items
|
||||||
|
var faultyIds []uint
|
||||||
|
|
||||||
|
if err := db.Model(&models.Summary{}).
|
||||||
|
Distinct("summaries.id").
|
||||||
|
Joins("INNER JOIN summary_items ON summaries.num_heartbeats = 0 AND summaries.id = summary_items.summary_id").
|
||||||
|
Scan(&faultyIds).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update their heartbeats counter
|
||||||
|
result := db.
|
||||||
|
Table("summaries AS s1").
|
||||||
|
Where("s1.id IN ?", faultyIds).
|
||||||
|
Update(
|
||||||
|
"num_heartbeats",
|
||||||
|
db.
|
||||||
|
Model(&models.Heartbeat{}).
|
||||||
|
Select("COUNT(*)").
|
||||||
|
Where("user_id = ?", gorm.Expr("s1.user_id")).
|
||||||
|
Where("time BETWEEN ? AND ?", gorm.Expr("s1.from_time"), gorm.Expr("s1.to_time")),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := result.Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logbuch.Info("corrected heartbeats counter of %d summaries", result.RowsAffected)
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPostMigration(f)
|
||||||
|
}
|
41
migrations/20220318_mysql_timestamp_precision.go
Normal file
41
migrations/20220318_mysql_timestamp_precision.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "20220318-mysql_timestamp_precision"
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Db.IsMySQL() {
|
||||||
|
logbuch.Info("altering heartbeats table, this may take a while (up to hours)")
|
||||||
|
|
||||||
|
db.Exec("SET foreign_key_checks=0;")
|
||||||
|
db.Exec("SET unique_checks=0;")
|
||||||
|
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `time` TIMESTAMP(3) NOT NULL").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec("ALTER TABLE heartbeats MODIFY COLUMN `created_at` TIMESTAMP(3) NOT NULL").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
db.Exec("SET foreign_key_checks=1;")
|
||||||
|
db.Exec("SET unique_checks=1;")
|
||||||
|
|
||||||
|
logbuch.Info("migrated timestamp columns to millisecond precision")
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPostMigration(f)
|
||||||
|
}
|
39
migrations/202203191_drop_diagnostics_user.go
Normal file
39
migrations/202203191_drop_diagnostics_user.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "202203191-drop_diagnostics_user"
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator := db.Migrator()
|
||||||
|
|
||||||
|
if migrator.HasColumn(&models.Diagnostics{}, "user_id") {
|
||||||
|
logbuch.Info("running migration '%s'", name)
|
||||||
|
|
||||||
|
if err := migrator.DropConstraint(&models.Diagnostics{}, "fk_diagnostics_user"); err != nil {
|
||||||
|
logbuch.Warn("failed to drop 'fk_diagnostics_user' constraint (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrator.DropColumn(&models.Diagnostics{}, "user_id"); err != nil {
|
||||||
|
logbuch.Warn("failed to drop user_id column of diagnostics (%v)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPostMigration(f)
|
||||||
|
}
|
40
migrations/20220403_drop_user_project_idx.go
Normal file
40
migrations/20220403_drop_user_project_idx.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// migration to fix https://github.com/muety/wakapi/issues/346
|
||||||
|
// caused by https://github.com/muety/wakapi/blob/2.3.2/migrations/20220319_add_user_project_idx.go in combination with
|
||||||
|
// the wrongly defined index at https://github.com/muety/wakapi/blob/5aae18e2415d9e620f383f98cd8cbdf39cd99f27/models/heartbeat.go#L18
|
||||||
|
// and https://github.com/go-gorm/sqlite/issues/87
|
||||||
|
// -> drop index and let it be auto-created again with properly formatted ddl
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const name = "20220403-drop_user_project_idx"
|
||||||
|
const idxName = "idx_user_project"
|
||||||
|
|
||||||
|
f := migrationFunc{
|
||||||
|
name: name,
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
if !db.Migrator().HasTable(&models.KeyStringValue{}) || hasRun(name, db) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Db.IsSQLite() && db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
|
||||||
|
logbuch.Info("running migration '%s'", name)
|
||||||
|
if err := db.Migrator().DropIndex(&models.Heartbeat{}, idxName); err != nil {
|
||||||
|
logbuch.Warn("failed to drop %s", idxName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRun(name, db)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPreMigration(f)
|
||||||
|
}
|
@ -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
30
migrations/shared.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
16
mocks/duration_service.go
Normal 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)
|
||||||
|
}
|
@ -20,8 +20,8 @@ func (m *HeartbeatServiceMock) InsertBatch(heartbeats []*models.Heartbeat) error
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *HeartbeatServiceMock) Count() (int64, error) {
|
func (m *HeartbeatServiceMock) Count(a bool) (int64, error) {
|
||||||
args := m.Called()
|
args := m.Called(a)
|
||||||
return int64(args.Int(0)), args.Error(1)
|
return int64(args.Int(0)), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +40,11 @@ func (m *HeartbeatServiceMock) GetAllWithin(time time.Time, time2 time.Time, use
|
|||||||
return args.Get(0).([]*models.Heartbeat), args.Error(1)
|
return args.Get(0).([]*models.Heartbeat), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *HeartbeatServiceMock) GetAllWithinByFilters(time time.Time, time2 time.Time, user *models.User, filters *models.Filters) ([]*models.Heartbeat, error) {
|
||||||
|
args := m.Called(time, time2, user, filters)
|
||||||
|
return args.Get(0).([]*models.Heartbeat), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *HeartbeatServiceMock) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
func (m *HeartbeatServiceMock) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.Get(0).([]*models.TimeByUser), args.Error(1)
|
return args.Get(0).([]*models.TimeByUser), args.Error(1)
|
||||||
@ -64,3 +69,8 @@ func (m *HeartbeatServiceMock) DeleteBefore(time time.Time) error {
|
|||||||
args := m.Called(time)
|
args := m.Called(time)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *HeartbeatServiceMock) DeleteByUser(u *models.User) error {
|
||||||
|
args := m.Called(u)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
40
mocks/project_label_service.go
Normal file
40
mocks/project_label_service.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectLabelServiceMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectLabelServiceMock) GetById(u uint) (*models.ProjectLabel, error) {
|
||||||
|
args := p.Called(u)
|
||||||
|
return args.Get(0).(*models.ProjectLabel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectLabelServiceMock) GetByUser(s string) ([]*models.ProjectLabel, error) {
|
||||||
|
args := p.Called(s)
|
||||||
|
return args.Get(0).([]*models.ProjectLabel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectLabelServiceMock) GetByUserGrouped(s string) (map[string][]*models.ProjectLabel, error) {
|
||||||
|
args := p.Called(s)
|
||||||
|
return args.Get(0).(map[string][]*models.ProjectLabel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectLabelServiceMock) GetByUserGroupedInverted(s string) (map[string][]*models.ProjectLabel, error) {
|
||||||
|
args := p.Called(s)
|
||||||
|
return args.Get(0).(map[string][]*models.ProjectLabel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectLabelServiceMock) Create(l *models.ProjectLabel) (*models.ProjectLabel, error) {
|
||||||
|
args := p.Called(l)
|
||||||
|
return args.Get(0).(*models.ProjectLabel), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectLabelServiceMock) Delete(l *models.ProjectLabel) error {
|
||||||
|
args := p.Called(l)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"`
|
||||||
|
@ -3,14 +3,13 @@ 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
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultLabel = "coding time"
|
defaultLabel = "wakapi.dev"
|
||||||
defaultColor = "#2D3748" // not working
|
defaultColor = "2F855A"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BadgeData struct {
|
type BadgeData struct {
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()),
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
11
models/diagnostics.go
Normal file
11
models/diagnostics.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Diagnostics struct {
|
||||||
|
ID uint `gorm:"primary_key"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
CliVersion string `json:"cli_version"`
|
||||||
|
Logs string `json:"logs" gorm:"type:text"`
|
||||||
|
StackTrace string `json:"stacktrace" gorm:"type:text"`
|
||||||
|
}
|
70
models/duration.go
Normal file
70
models/duration.go
Normal 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
46
models/durations.go
Normal 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]
|
||||||
|
}
|
@ -1,45 +1,223 @@
|
|||||||
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 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}
|
|
||||||
}
|
|
||||||
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 {
|
||||||
}
|
case SummaryProject:
|
||||||
return false, 0, ""
|
f.Project = append(f.Project, keys...)
|
||||||
|
case SummaryOS:
|
||||||
|
f.OS = append(f.OS, keys...)
|
||||||
|
case SummaryLanguage:
|
||||||
|
f.Language = append(f.Language, keys...)
|
||||||
|
case SummaryEditor:
|
||||||
|
f.Editor = append(f.Editor, keys...)
|
||||||
|
case SummaryMachine:
|
||||||
|
f.Machine = append(f.Machine, keys...)
|
||||||
|
case SummaryLabel:
|
||||||
|
f.Label = append(f.Label, keys...)
|
||||||
|
case SummaryBranch:
|
||||||
|
f.Branch = append(f.Branch, keys...)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) One() (bool, uint8, OrFilter) {
|
||||||
|
if f.Project != nil && f.Project.Exists() {
|
||||||
|
return true, SummaryProject, f.Project
|
||||||
|
} else if f.OS != nil && f.OS.Exists() {
|
||||||
|
return true, SummaryOS, f.OS
|
||||||
|
} else if f.Language != nil && f.Language.Exists() {
|
||||||
|
return true, SummaryLanguage, f.Language
|
||||||
|
} else if f.Editor != nil && f.Editor.Exists() {
|
||||||
|
return true, SummaryEditor, f.Editor
|
||||||
|
} else if f.Machine != nil && f.Machine.Exists() {
|
||||||
|
return true, SummaryMachine, f.Machine
|
||||||
|
} else if f.Label != nil && f.Label.Exists() {
|
||||||
|
return true, SummaryLabel, f.Label
|
||||||
|
} else if f.Branch != nil && f.Branch.Exists() {
|
||||||
|
return true, SummaryBranch, f.Branch
|
||||||
|
}
|
||||||
|
return false, 0, OrFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) OneOrEmpty() FilterElement {
|
||||||
|
if ok, t, of := f.One(); ok {
|
||||||
|
return FilterElement{entity: t, filter: of}
|
||||||
|
}
|
||||||
|
return FilterElement{entity: SummaryUnknown, filter: []string{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) IsEmpty() bool {
|
||||||
|
nonEmpty, _, _ := f.One()
|
||||||
|
return !nonEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) Count() int {
|
||||||
|
var count int
|
||||||
|
for i := SummaryProject; i <= SummaryBranch; i++ {
|
||||||
|
count += f.CountByEntity(i)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) CountByEntity(entity uint8) int {
|
||||||
|
return len(*f.ResolveEntity(entity))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) EntityCount() int {
|
||||||
|
var count int
|
||||||
|
for i := SummaryProject; i <= SummaryBranch; i++ {
|
||||||
|
if c := f.CountByEntity(i); c > 0 {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) ResolveEntity(entityId uint8) *OrFilter {
|
||||||
|
switch entityId {
|
||||||
|
case SummaryProject:
|
||||||
|
return &f.Project
|
||||||
|
case SummaryLanguage:
|
||||||
|
return &f.Language
|
||||||
|
case SummaryEditor:
|
||||||
|
return &f.Editor
|
||||||
|
case SummaryOS:
|
||||||
|
return &f.OS
|
||||||
|
case SummaryMachine:
|
||||||
|
return &f.Machine
|
||||||
|
case SummaryLabel:
|
||||||
|
return &f.Label
|
||||||
|
case SummaryBranch:
|
||||||
|
return &f.Branch
|
||||||
|
default:
|
||||||
|
return &OrFilter{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) Hash() string {
|
||||||
|
hash, err := hashstructure.Hash(f, hashstructure.FormatV2, nil)
|
||||||
|
if err != nil {
|
||||||
|
logbuch.Error("CRITICAL ERROR: failed to hash struct - %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) Match(h *Heartbeat) bool {
|
||||||
|
return (f.Project == nil || f.Project.MatchAny(h.Project)) &&
|
||||||
|
(f.OS == nil || f.OS.MatchAny(h.OperatingSystem)) &&
|
||||||
|
(f.Language == nil || f.Language.MatchAny(h.Language)) &&
|
||||||
|
(f.Editor == nil || f.Editor.MatchAny(h.Editor)) &&
|
||||||
|
(f.Machine == nil || f.Machine.MatchAny(h.Machine))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAliases adds OR-conditions for every alias of a filter key as additional filter keys
|
||||||
|
func (f *Filters) WithAliases(resolve AliasReverseResolver) *Filters {
|
||||||
|
if f.Project != nil {
|
||||||
|
updated := OrFilter(make([]string, 0, len(f.Project)))
|
||||||
|
for _, e := range f.Project {
|
||||||
|
updated = append(updated, e)
|
||||||
|
updated = append(updated, resolve(SummaryProject, e)...)
|
||||||
|
}
|
||||||
|
f.Project = updated
|
||||||
|
}
|
||||||
|
if f.OS != nil {
|
||||||
|
updated := OrFilter(make([]string, 0, len(f.OS)))
|
||||||
|
for _, e := range f.OS {
|
||||||
|
updated = append(updated, e)
|
||||||
|
updated = append(updated, resolve(SummaryOS, e)...)
|
||||||
|
}
|
||||||
|
f.OS = updated
|
||||||
|
}
|
||||||
|
if f.Language != nil {
|
||||||
|
updated := OrFilter(make([]string, 0, len(f.Language)))
|
||||||
|
for _, e := range f.Language {
|
||||||
|
updated = append(updated, e)
|
||||||
|
updated = append(updated, resolve(SummaryLanguage, e)...)
|
||||||
|
}
|
||||||
|
f.Language = updated
|
||||||
|
}
|
||||||
|
if f.Editor != nil {
|
||||||
|
updated := OrFilter(make([]string, 0, len(f.Editor)))
|
||||||
|
for _, e := range f.Editor {
|
||||||
|
updated = append(updated, e)
|
||||||
|
updated = append(updated, resolve(SummaryEditor, e)...)
|
||||||
|
}
|
||||||
|
f.Editor = updated
|
||||||
|
}
|
||||||
|
if f.Machine != nil {
|
||||||
|
updated := OrFilter(make([]string, 0, len(f.Machine)))
|
||||||
|
for _, e := range f.Machine {
|
||||||
|
updated = append(updated, e)
|
||||||
|
updated = append(updated, resolve(SummaryMachine, e)...)
|
||||||
|
}
|
||||||
|
f.Machine = updated
|
||||||
|
}
|
||||||
|
if f.Branch != nil {
|
||||||
|
updated := OrFilter(make([]string, 0, len(f.Branch)))
|
||||||
|
for _, e := range f.Branch {
|
||||||
|
updated = append(updated, e)
|
||||||
|
updated = append(updated, resolve(SummaryBranch, e)...)
|
||||||
|
}
|
||||||
|
f.Branch = updated
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Filters) WithProjectLabels(resolve ProjectLabelReverseResolver) *Filters {
|
||||||
|
if f.Label == nil || !f.Label.Exists() {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
for _, l := range f.Label {
|
||||||
|
f.WithMultiple(SummaryProject, resolve(l))
|
||||||
|
}
|
||||||
|
return f
|
||||||
}
|
}
|
||||||
|
160
models/filters_test.go
Normal file
160
models/filters_test.go
Normal 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")
|
||||||
|
}
|
@ -9,30 +9,36 @@ 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; index:idx_user_project"` // idx_user_project is for quickly fetching a user's project list (settings page)
|
||||||
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
Entity string `json:"entity" gorm:"not null"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project" gorm:"index:idx_project; index:idx_user_project"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch" gorm:"index:idx_branch"`
|
||||||
Language string `json:"language" gorm:"index:idx_language"`
|
Language string `json:"language" gorm:"index:idx_language"`
|
||||||
IsWrite bool `json:"is_write"`
|
IsWrite bool `json:"is_write"`
|
||||||
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
Editor string `json:"editor" gorm:"index:idx_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" gorm:"index:idx_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" gorm:"index:idx_machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||||
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
UserAgent string `json:"user_agent" hash:"ignore" gorm:"type:varchar(255)"`
|
||||||
|
Time CustomTime `json:"time" gorm:"type:timestamp(3); 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" gorm:"type:varchar(255)"`
|
||||||
OriginId string `json:"-" hash:"ignore"`
|
OriginId string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||||
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp(3)" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Heartbeat) Valid() bool {
|
func (h *Heartbeat) Valid() bool {
|
||||||
return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{})
|
return h.User != nil && h.UserID != "" && h.User.ID == h.UserID && h.Time != CustomTime(time.Time{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Heartbeat) Timely(maxAge time.Duration) bool {
|
||||||
|
now := time.Now()
|
||||||
|
return now.Sub(h.Time.T()) <= maxAge && h.Time.T().Sub(now) < 1*time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
||||||
maxPrec := -1 // precision / mapping complexity -> more concrete ones shall take precedence
|
maxPrec := -1 // precision / mapping complexity -> more concrete ones shall take precedence
|
||||||
for ending, value := range languageMappings {
|
for ending, value := range languageMappings {
|
||||||
@ -55,6 +61,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,8 +99,20 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetEntityColumn(t uint8) string {
|
||||||
|
return []string{
|
||||||
|
"project",
|
||||||
|
"language",
|
||||||
|
"editor",
|
||||||
|
"operating_system",
|
||||||
|
"machine",
|
||||||
|
"label",
|
||||||
|
"branch",
|
||||||
|
}[t]
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import "fmt"
|
|||||||
|
|
||||||
type CounterMetric struct {
|
type CounterMetric struct {
|
||||||
Name string
|
Name string
|
||||||
Value int
|
Value int64
|
||||||
Desc string
|
Desc string
|
||||||
Labels Labels
|
Labels Labels
|
||||||
}
|
}
|
||||||
|
16
models/project_label.go
Normal file
16
models/project_label.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// ProjectLabelReverseResolver returns all projects for a given label
|
||||||
|
type ProjectLabelReverseResolver func(l string) []string
|
||||||
|
|
||||||
|
type ProjectLabel struct {
|
||||||
|
ID uint `json:"id" gorm:"primary_key"`
|
||||||
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
|
UserID string `json:"-" gorm:"not null; index:idx_project_label_user"`
|
||||||
|
ProjectKey string `json:"project"`
|
||||||
|
Label string `json:"label" gorm:"type:varchar(64)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ProjectLabel) IsValid() bool {
|
||||||
|
return l.ProjectKey != "" && l.Label != ""
|
||||||
|
}
|
@ -29,6 +29,11 @@ type Interval struct {
|
|||||||
End time.Time
|
End time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeyedInterval struct {
|
||||||
|
Interval
|
||||||
|
Key *IntervalKey
|
||||||
|
}
|
||||||
|
|
||||||
// CustomTime is a wrapper type around time.Time, mainly used for the purpose of transparently unmarshalling Python timestamps in the format <sec>.<nsec> (e.g. 1619335137.3324468)
|
// CustomTime is a wrapper type around time.Time, mainly used for the purpose of transparently unmarshalling Python timestamps in the format <sec>.<nsec> (e.g. 1619335137.3324468)
|
||||||
type CustomTime time.Time
|
type CustomTime time.Time
|
||||||
|
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NSummaryTypes uint8 = 99
|
NSummaryTypes uint8 = 99
|
||||||
|
SummaryUnknown uint8 = 98
|
||||||
SummaryProject uint8 = 0
|
SummaryProject uint8 = 0
|
||||||
SummaryLanguage uint8 = 1
|
SummaryLanguage uint8 = 1
|
||||||
SummaryEditor uint8 = 2
|
SummaryEditor uint8 = 2
|
||||||
SummaryOS uint8 = 3
|
SummaryOS uint8 = 3
|
||||||
SummaryMachine uint8 = 4
|
SummaryMachine uint8 = 4
|
||||||
|
SummaryLabel uint8 = 5
|
||||||
|
SummaryBranch uint8 = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
const UnknownSummaryKey = "unknown"
|
const UnknownSummaryKey = "unknown"
|
||||||
|
const DefaultProjectLabel = "default"
|
||||||
|
|
||||||
type Summary struct {
|
type Summary struct {
|
||||||
ID uint `json:"-" gorm:"primary_key"`
|
ID uint `json:"-" gorm:"primary_key"`
|
||||||
@ -27,12 +32,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:"-"` // 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"`
|
||||||
@ -45,29 +53,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, SummaryBranch}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +79,8 @@ func (s *Summary) Sorted() *Summary {
|
|||||||
sort.Sort(sort.Reverse(s.OperatingSystems))
|
sort.Sort(sort.Reverse(s.OperatingSystems))
|
||||||
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.Branches))
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,11 +95,43 @@ func (s *Summary) MappedItems() map[uint8]*SummaryItems {
|
|||||||
SummaryEditor: &s.Editors,
|
SummaryEditor: &s.Editors,
|
||||||
SummaryOS: &s.OperatingSystems,
|
SummaryOS: &s.OperatingSystems,
|
||||||
SummaryMachine: &s.Machines,
|
SummaryMachine: &s.Machines,
|
||||||
|
SummaryLabel: &s.Labels,
|
||||||
|
SummaryBranch: &s.Branches,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Summary) ItemsByType(summaryType uint8) *SummaryItems {
|
func (s *Summary) ItemsByType(summaryType uint8) *SummaryItems {
|
||||||
return s.MappedItems()[summaryType]
|
switch summaryType {
|
||||||
|
case SummaryProject:
|
||||||
|
return &s.Projects
|
||||||
|
case SummaryLanguage:
|
||||||
|
return &s.Languages
|
||||||
|
case SummaryEditor:
|
||||||
|
return &s.Editors
|
||||||
|
case SummaryOS:
|
||||||
|
return &s.OperatingSystems
|
||||||
|
case SummaryMachine:
|
||||||
|
return &s.Machines
|
||||||
|
case SummaryLabel:
|
||||||
|
return &s.Labels
|
||||||
|
case SummaryBranch:
|
||||||
|
return &s.Branches
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Summary) KeepOnly(types map[uint8]bool) *Summary {
|
||||||
|
if len(types) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range SummaryTypes() {
|
||||||
|
if keep, ok := types[t]; !keep || !ok {
|
||||||
|
*s.ItemsByType(t) = []*SummaryItem{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Augments the summary in a way that at least one item is present for every type.
|
/* Augments the summary in a way that at least one item is present for every type.
|
||||||
@ -109,7 +145,7 @@ of time than the other ones.
|
|||||||
To avoid having to modify persisted data retrospectively, i.e. inserting a dummy SummaryItem for the new type,
|
To avoid having to modify persisted data retrospectively, i.e. inserting a dummy SummaryItem for the new type,
|
||||||
such is generated dynamically here, considering the "machine" for all old heartbeats "unknown".
|
such is generated dynamically here, considering the "machine" for all old heartbeats "unknown".
|
||||||
*/
|
*/
|
||||||
func (s *Summary) FillUnknown() {
|
func (s *Summary) FillMissing() {
|
||||||
types := s.Types()
|
types := s.Types()
|
||||||
typeItems := s.MappedItems()
|
typeItems := s.MappedItems()
|
||||||
missingTypes := make([]uint8, 0)
|
missingTypes := make([]uint8, 0)
|
||||||
@ -125,15 +161,46 @@ func (s *Summary) FillUnknown() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timeSum := s.TotalTime()
|
|
||||||
|
|
||||||
// construct dummy item for all missing types
|
// construct dummy item for all missing types
|
||||||
|
presentType, err := s.findFirstPresentType()
|
||||||
|
if err != nil {
|
||||||
|
return // all types are either zero or missing entirely, nothing to fill
|
||||||
|
}
|
||||||
for _, t := range missingTypes {
|
for _, t := range missingTypes {
|
||||||
*typeItems[t] = append(*typeItems[t], &SummaryItem{
|
s.FillBy(presentType, t)
|
||||||
Type: t,
|
}
|
||||||
Key: UnknownSummaryKey,
|
}
|
||||||
Total: timeSum,
|
|
||||||
})
|
// inplace!
|
||||||
|
func (s *Summary) FillBy(fromType uint8, toType uint8) {
|
||||||
|
typeItems := s.MappedItems()
|
||||||
|
totalWanted := s.TotalTimeBy(fromType)
|
||||||
|
totalActual := s.TotalTimeBy(toType)
|
||||||
|
|
||||||
|
key := UnknownSummaryKey
|
||||||
|
if toType == SummaryLabel {
|
||||||
|
key = DefaultProjectLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
existingEntryIdx := -1
|
||||||
|
for i, item := range *typeItems[toType] {
|
||||||
|
if item.Key == key {
|
||||||
|
existingEntryIdx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total := (totalWanted - totalActual) / time.Second // workaround
|
||||||
|
if total > 0 {
|
||||||
|
if existingEntryIdx >= 0 {
|
||||||
|
(*typeItems[toType])[existingEntryIdx].Total = total
|
||||||
|
} else {
|
||||||
|
*typeItems[toType] = append(*typeItems[toType], &SummaryItem{
|
||||||
|
Type: toType,
|
||||||
|
Key: key,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,14 +208,12 @@ func (s *Summary) TotalTime() time.Duration {
|
|||||||
var timeSum time.Duration
|
var timeSum time.Duration
|
||||||
|
|
||||||
mappedItems := s.MappedItems()
|
mappedItems := s.MappedItems()
|
||||||
// calculate total duration from any of the present sets of items
|
t, err := s.findFirstPresentType()
|
||||||
for _, t := range s.Types() {
|
if err != nil {
|
||||||
if items := mappedItems[t]; len(*items) > 0 {
|
return 0
|
||||||
for _, item := range *items {
|
}
|
||||||
timeSum += item.Total
|
for _, item := range *mappedItems[t] {
|
||||||
}
|
timeSum += item.Total
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return timeSum * time.Second
|
return timeSum * time.Second
|
||||||
@ -177,16 +242,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 {
|
||||||
@ -231,10 +321,41 @@ func (s *Summary) WithResolvedAliases(resolve AliasResolver) *Summary {
|
|||||||
s.Languages = processAliases(s.Languages)
|
s.Languages = processAliases(s.Languages)
|
||||||
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.Branches = processAliases(s.Branches)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Summary) findFirstPresentType() (uint8, error) {
|
||||||
|
for _, t := range s.Types() {
|
||||||
|
if s.TotalTimeBy(t) != 0 {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSummary_FillUnknown(t *testing.T) {
|
func TestSummary_FillMissing(t *testing.T) {
|
||||||
testDuration := 10 * time.Minute
|
testDuration := 10 * time.Minute
|
||||||
|
|
||||||
sut := &Summary{
|
sut := &Summary{
|
||||||
@ -20,7 +20,7 @@ func TestSummary_FillUnknown(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sut.FillUnknown()
|
sut.FillMissing()
|
||||||
|
|
||||||
itemLists := [][]*SummaryItem{
|
itemLists := [][]*SummaryItem{
|
||||||
sut.Machines,
|
sut.Machines,
|
||||||
@ -31,8 +31,12 @@ func TestSummary_FillUnknown(t *testing.T) {
|
|||||||
for _, l := range itemLists {
|
for _, l := range itemLists {
|
||||||
assert.Len(t, l, 1)
|
assert.Len(t, l, 1)
|
||||||
assert.Equal(t, UnknownSummaryKey, l[0].Key)
|
assert.Equal(t, UnknownSummaryKey, l[0].Key)
|
||||||
assert.Equal(t, testDuration, l[0].Total)
|
assert.Equal(t, testDuration, l[0].TotalFixed())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.Len(t, sut.Labels, 1)
|
||||||
|
assert.Equal(t, DefaultProjectLabel, sut.Labels[0].Key)
|
||||||
|
assert.Equal(t, testDuration, sut.Labels[0].TotalFixed())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSummary_TotalTimeBy(t *testing.T) {
|
func TestSummary_TotalTimeBy(t *testing.T) {
|
||||||
@ -94,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) {
|
||||||
@ -171,6 +168,66 @@ func TestSummary_WithResolvedAliases(t *testing.T) {
|
|||||||
assert.Empty(t, sut.Machines)
|
assert.Empty(t, sut.Machines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSummary_KeepOnly(t *testing.T) {
|
||||||
|
newSummary := func() *Summary {
|
||||||
|
return &Summary{
|
||||||
|
Projects: []*SummaryItem{
|
||||||
|
{
|
||||||
|
Type: SummaryProject,
|
||||||
|
Key: "wakapi",
|
||||||
|
// hack to work around the issue that the total time of a summary item is mistakenly represented in seconds
|
||||||
|
Total: 10 * time.Minute / time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: SummaryProject,
|
||||||
|
Key: "anchr",
|
||||||
|
Total: 10 * time.Minute / time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Languages: []*SummaryItem{
|
||||||
|
{
|
||||||
|
Type: SummaryLanguage,
|
||||||
|
Key: "Go",
|
||||||
|
Total: 10 * time.Minute / time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Editors: []*SummaryItem{
|
||||||
|
{
|
||||||
|
Type: SummaryEditor,
|
||||||
|
Key: "VSCode",
|
||||||
|
Total: 10 * time.Minute / time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sut *Summary
|
||||||
|
|
||||||
|
sut = newSummary().KeepOnly(map[uint8]bool{}) // keep all
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryProject))
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryEditor))
|
||||||
|
assert.Equal(t, 20*time.Minute, sut.TotalTime())
|
||||||
|
|
||||||
|
sut = newSummary().KeepOnly(map[uint8]bool{SummaryProject: true})
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryProject))
|
||||||
|
assert.Zero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||||
|
assert.Zero(t, sut.TotalTimeBy(SummaryEditor))
|
||||||
|
assert.Equal(t, 20*time.Minute, sut.TotalTime())
|
||||||
|
|
||||||
|
sut = newSummary().KeepOnly(map[uint8]bool{SummaryEditor: true, SummaryLanguage: true})
|
||||||
|
assert.Zero(t, sut.TotalTimeBy(SummaryProject))
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryEditor))
|
||||||
|
assert.Equal(t, 10*time.Minute, sut.TotalTime())
|
||||||
|
|
||||||
|
sut = newSummary().KeepOnly(map[uint8]bool{SummaryProject: true})
|
||||||
|
sut.FillMissing()
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryProject))
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryLanguage))
|
||||||
|
assert.NotZero(t, sut.TotalTimeBy(SummaryEditor))
|
||||||
|
}
|
||||||
|
|
||||||
func TestSummaryItems_Sorted(t *testing.T) {
|
func TestSummaryItems_Sorted(t *testing.T) {
|
||||||
testDuration1, testDuration2, testDuration3 := 10*time.Minute, 5*time.Minute, 20*time.Minute
|
testDuration1, testDuration2, testDuration3 := 10*time.Minute, 5*time.Minute, 20*time.Minute
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -11,7 +14,7 @@ func init() {
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id" gorm:"primary_key"`
|
ID string `json:"id" gorm:"primary_key"`
|
||||||
ApiKey string `json:"api_key" gorm:"unique"`
|
ApiKey string `json:"api_key" gorm:"unique; default:NULL"`
|
||||||
Email string `json:"email" gorm:"index:idx_user_email; size:255"`
|
Email string `json:"email" gorm:"index:idx_user_email; size:255"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
@ -23,9 +26,11 @@ type User struct {
|
|||||||
ShareProjects bool `json:"-" gorm:"default:false; type:bool"`
|
ShareProjects bool `json:"-" gorm:"default:false; type:bool"`
|
||||||
ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
|
ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
|
||||||
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
|
ShareMachines 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"`
|
||||||
}
|
}
|
||||||
@ -86,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
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -6,6 +6,9 @@ type SettingsViewModel struct {
|
|||||||
User *models.User
|
User *models.User
|
||||||
LanguageMappings []*models.LanguageMapping
|
LanguageMappings []*models.LanguageMapping
|
||||||
Aliases []*SettingsVMCombinedAlias
|
Aliases []*SettingsVMCombinedAlias
|
||||||
|
Labels []*SettingsVMCombinedLabel
|
||||||
|
Projects []string
|
||||||
|
ApiKey string
|
||||||
Success string
|
Success string
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
@ -16,6 +19,11 @@ type SettingsVMCombinedAlias struct {
|
|||||||
Values []string
|
Values []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SettingsVMCombinedLabel struct {
|
||||||
|
Key string
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel {
|
func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel {
|
||||||
s.Success = m
|
s.Success = m
|
||||||
return s
|
return s
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
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
|
||||||
|
EditorColors map[string]string
|
||||||
|
LanguageColors map[string]string
|
||||||
|
OSColors 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
18
package.json
Normal 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": {}
|
||||||
|
}
|
@ -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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
18
repositories/diagnostics.go
Normal file
18
repositories/diagnostics.go
Normal 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
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
@ -9,11 +9,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HeartbeatRepository struct {
|
type HeartbeatRepository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
config *conf.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeartbeatRepository(db *gorm.DB) *HeartbeatRepository {
|
func NewHeartbeatRepository(db *gorm.DB) *HeartbeatRepository {
|
||||||
return &HeartbeatRepository{db: db}
|
return &HeartbeatRepository{config: conf.Get(), db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use with caution!!
|
// Use with caution!!
|
||||||
@ -77,6 +78,26 @@ func (r *HeartbeatRepository) GetAllWithin(from, to time.Time, user *models.User
|
|||||||
return heartbeats, nil
|
return heartbeats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HeartbeatRepository) GetAllWithinByFilters(from, to time.Time, user *models.User, filterMap map[string][]string) ([]*models.Heartbeat, error) {
|
||||||
|
// https://stackoverflow.com/a/20765152/3112139
|
||||||
|
var heartbeats []*models.Heartbeat
|
||||||
|
|
||||||
|
q := r.db.
|
||||||
|
Where(&models.Heartbeat{UserID: user.ID}).
|
||||||
|
Where("time >= ?", from.Local()).
|
||||||
|
Where("time < ?", to.Local()).
|
||||||
|
Order("time asc")
|
||||||
|
|
||||||
|
for col, vals := range filterMap {
|
||||||
|
q = q.Where(col+" in ?", vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Find(&heartbeats).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return heartbeats, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HeartbeatRepository) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
func (r *HeartbeatRepository) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
||||||
var result []*models.TimeByUser
|
var result []*models.TimeByUser
|
||||||
r.db.Model(&models.User{}).
|
r.db.Model(&models.User{}).
|
||||||
@ -97,12 +118,19 @@ func (r *HeartbeatRepository) GetLastByUsers() ([]*models.TimeByUser, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HeartbeatRepository) Count() (int64, error) {
|
func (r *HeartbeatRepository) Count(approximate bool) (count int64, err error) {
|
||||||
var count int64
|
if r.config.Db.IsMySQL() && approximate {
|
||||||
if err := r.db.
|
err = r.db.Table("information_schema.tables").
|
||||||
Model(&models.Heartbeat{}).
|
Select("table_rows").
|
||||||
Count(&count).Error; err != nil {
|
Where("table_schema = ?", r.config.Db.Name).
|
||||||
return 0, err
|
Where("table_name = 'heartbeats'").
|
||||||
|
Scan(&count).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
err = r.db.
|
||||||
|
Model(&models.Heartbeat{}).
|
||||||
|
Count(&count).Error
|
||||||
}
|
}
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
@ -126,6 +154,10 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
|
|||||||
userIds[i] = u.ID
|
userIds[i] = u.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(userIds) == 0 {
|
||||||
|
return counts, nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
Model(&models.Heartbeat{}).
|
Model(&models.Heartbeat{}).
|
||||||
Select("user_id as user, count(id) as count").
|
Select("user_id as user, count(id) as count").
|
||||||
@ -134,20 +166,15 @@ func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.Coun
|
|||||||
Find(&counts).Error; err != nil {
|
Find(&counts).Error; err != nil {
|
||||||
return counts, err
|
return counts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r HeartbeatRepository) GetEntitySetByUser(entityType uint8, user *models.User) ([]string, error) {
|
func (r HeartbeatRepository) GetEntitySetByUser(entityType uint8, user *models.User) ([]string, error) {
|
||||||
columns := []string{"project", "language", "editor", "operating_system", "machine"}
|
|
||||||
if int(entityType) >= len(columns) {
|
|
||||||
// invalid entity type
|
|
||||||
return nil, errors.New("invalid entity type")
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []string
|
var results []string
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
Model(&models.Heartbeat{}).
|
Model(&models.Heartbeat{}).
|
||||||
Distinct(columns[entityType]).
|
Distinct(models.GetEntityColumn(entityType)).
|
||||||
Where(&models.Heartbeat{UserID: user.ID}).
|
Where(&models.Heartbeat{UserID: user.ID}).
|
||||||
Find(&results).Error; err != nil {
|
Find(&results).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -163,3 +190,12 @@ func (r *HeartbeatRepository) DeleteBefore(t time.Time) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HeartbeatRepository) DeleteByUser(user *models.User) error {
|
||||||
|
if err := r.db.
|
||||||
|
Where("user_id = ?", user.ID).
|
||||||
|
Delete(models.Heartbeat{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -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 {
|
||||||
|
43
repositories/metrics.go
Normal file
43
repositories/metrics.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetricsRepository struct {
|
||||||
|
config *config.Config
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeTplMysql = `
|
||||||
|
SELECT SUM(data_length + index_length)
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = ?
|
||||||
|
GROUP BY table_schema`
|
||||||
|
|
||||||
|
const sizeTplPostgres = `SELECT pg_database_size('%s');`
|
||||||
|
|
||||||
|
const sizeTplSqlite = `
|
||||||
|
SELECT page_count * page_size as size
|
||||||
|
FROM pragma_page_count(), pragma_page_size();`
|
||||||
|
|
||||||
|
func NewMetricsRepository(db *gorm.DB) *MetricsRepository {
|
||||||
|
return &MetricsRepository{config: config.Get(), db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *MetricsRepository) GetDatabaseSize() (size int64, err error) {
|
||||||
|
cfg := srv.config.Db
|
||||||
|
|
||||||
|
query := srv.db.Raw("SELECT 0")
|
||||||
|
if cfg.IsMySQL() {
|
||||||
|
query = srv.db.Raw(sizeTplMysql, cfg.Name)
|
||||||
|
} else if cfg.IsPostgres() {
|
||||||
|
query = srv.db.Raw(sizeTplPostgres, cfg.Name)
|
||||||
|
} else if cfg.IsSQLite() {
|
||||||
|
query = srv.db.Raw(sizeTplSqlite)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = query.Scan(&size).Error
|
||||||
|
return size, err
|
||||||
|
}
|
63
repositories/project_label.go
Normal file
63
repositories/project_label.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectLabelRepository struct {
|
||||||
|
config *config.Config
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectLabelRepository(db *gorm.DB) *ProjectLabelRepository {
|
||||||
|
return &ProjectLabelRepository{config: config.Get(), db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProjectLabelRepository) GetAll() ([]*models.ProjectLabel, error) {
|
||||||
|
var labels []*models.ProjectLabel
|
||||||
|
if err := r.db.Find(&labels).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProjectLabelRepository) GetById(id uint) (*models.ProjectLabel, error) {
|
||||||
|
label := &models.ProjectLabel{}
|
||||||
|
if err := r.db.Where(&models.ProjectLabel{ID: id}).First(label).Error; err != nil {
|
||||||
|
return label, err
|
||||||
|
}
|
||||||
|
return label, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProjectLabelRepository) GetByUser(userId string) ([]*models.ProjectLabel, error) {
|
||||||
|
if userId == "" {
|
||||||
|
return []*models.ProjectLabel{}, nil
|
||||||
|
}
|
||||||
|
var labels []*models.ProjectLabel
|
||||||
|
if err := r.db.
|
||||||
|
Where(&models.ProjectLabel{UserID: userId}).
|
||||||
|
Find(&labels).Error; err != nil {
|
||||||
|
return labels, err
|
||||||
|
}
|
||||||
|
return labels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProjectLabelRepository) Insert(label *models.ProjectLabel) (*models.ProjectLabel, error) {
|
||||||
|
if !label.IsValid() {
|
||||||
|
return nil, errors.New("invalid label")
|
||||||
|
}
|
||||||
|
result := r.db.Create(label)
|
||||||
|
if err := result.Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return label, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProjectLabelRepository) Delete(id uint) error {
|
||||||
|
return r.db.
|
||||||
|
Where("id = ?", id).
|
||||||
|
Delete(models.ProjectLabel{}).Error
|
||||||
|
}
|
@ -20,15 +20,21 @@ type IHeartbeatRepository interface {
|
|||||||
InsertBatch([]*models.Heartbeat) error
|
InsertBatch([]*models.Heartbeat) error
|
||||||
GetAll() ([]*models.Heartbeat, error)
|
GetAll() ([]*models.Heartbeat, error)
|
||||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||||
|
GetAllWithinByFilters(time.Time, time.Time, *models.User, map[string][]string) ([]*models.Heartbeat, error)
|
||||||
GetFirstByUsers() ([]*models.TimeByUser, error)
|
GetFirstByUsers() ([]*models.TimeByUser, error)
|
||||||
GetLastByUsers() ([]*models.TimeByUser, error)
|
GetLastByUsers() ([]*models.TimeByUser, error)
|
||||||
GetLatestByUser(*models.User) (*models.Heartbeat, error)
|
GetLatestByUser(*models.User) (*models.Heartbeat, error)
|
||||||
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||||
Count() (int64, error)
|
Count(bool) (int64, error)
|
||||||
CountByUser(*models.User) (int64, error)
|
CountByUser(*models.User) (int64, error)
|
||||||
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
||||||
GetEntitySetByUser(uint8, *models.User) ([]string, error)
|
GetEntitySetByUser(uint8, *models.User) ([]string, error)
|
||||||
DeleteBefore(time.Time) error
|
DeleteBefore(time.Time) error
|
||||||
|
DeleteByUser(*models.User) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type IDiagnosticsRepository interface {
|
||||||
|
Insert(diagnostics *models.Diagnostics) (*models.Diagnostics, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IKeyValueRepository interface {
|
type IKeyValueRepository interface {
|
||||||
@ -46,6 +52,14 @@ type ILanguageMappingRepository interface {
|
|||||||
Delete(uint) error
|
Delete(uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IProjectLabelRepository interface {
|
||||||
|
GetAll() ([]*models.ProjectLabel, error)
|
||||||
|
GetById(uint) (*models.ProjectLabel, error)
|
||||||
|
GetByUser(string) ([]*models.ProjectLabel, error)
|
||||||
|
Insert(*models.ProjectLabel) (*models.ProjectLabel, error)
|
||||||
|
Delete(uint) error
|
||||||
|
}
|
||||||
|
|
||||||
type ISummaryRepository interface {
|
type ISummaryRepository interface {
|
||||||
Insert(*models.Summary) error
|
Insert(*models.Summary) error
|
||||||
GetAll() ([]*models.Summary, error)
|
GetAll() ([]*models.Summary, error)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,14 +20,15 @@ func (r *SummaryRepository) GetAll() ([]*models.Summary, error) {
|
|||||||
var summaries []*models.Summary
|
var summaries []*models.Summary
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
Order("from_time asc").
|
Order("from_time asc").
|
||||||
Preload("Projects", "type = ?", models.SummaryProject).
|
// branch summaries are currently not persisted, as only relevant in combination with project filter
|
||||||
Preload("Languages", "type = ?", models.SummaryLanguage).
|
|
||||||
Preload("Editors", "type = ?", models.SummaryEditor).
|
|
||||||
Preload("OperatingSystems", "type = ?", models.SummaryOS).
|
|
||||||
Preload("Machines", "type = ?", models.SummaryMachine).
|
|
||||||
Find(&summaries).Error; err != nil {
|
Find(&summaries).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.populateItems(summaries, []clause.Interface{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return summaries, nil
|
return summaries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,19 +41,29 @@ func (r *SummaryRepository) Insert(summary *models.Summary) error {
|
|||||||
|
|
||||||
func (r *SummaryRepository) GetByUserWithin(user *models.User, from, to time.Time) ([]*models.Summary, error) {
|
func (r *SummaryRepository) GetByUserWithin(user *models.User, from, to time.Time) ([]*models.Summary, error) {
|
||||||
var summaries []*models.Summary
|
var summaries []*models.Summary
|
||||||
if err := r.db.
|
|
||||||
Where(&models.Summary{UserID: user.ID}).
|
queryConditions := []clause.Interface{
|
||||||
Where("from_time >= ?", from.Local()).
|
clause.Where{Exprs: r.db.Statement.BuildCondition("user_id = ?", user.ID)},
|
||||||
Where("to_time <= ?", to.Local()).
|
clause.Where{Exprs: r.db.Statement.BuildCondition("from_time >= ?", from.Local())},
|
||||||
Order("from_time asc").
|
clause.Where{Exprs: r.db.Statement.BuildCondition("to_time <= ?", to.Local())},
|
||||||
Preload("Projects", "type = ?", models.SummaryProject).
|
}
|
||||||
Preload("Languages", "type = ?", models.SummaryLanguage).
|
|
||||||
Preload("Editors", "type = ?", models.SummaryEditor).
|
q := r.db.Model(&models.Summary{}).
|
||||||
Preload("OperatingSystems", "type = ?", models.SummaryOS).
|
Order("from_time asc")
|
||||||
Preload("Machines", "type = ?", models.SummaryMachine).
|
|
||||||
Find(&summaries).Error; err != nil {
|
for _, c := range queryConditions {
|
||||||
|
q.Statement.AddClause(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// branch summaries are currently not persisted, as only relevant in combination with project filter
|
||||||
|
if err := q.Find(&summaries).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.populateItems(summaries, queryConditions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return summaries, nil
|
return summaries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,3 +85,36 @@ func (r *SummaryRepository) DeleteByUser(userId string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inplace
|
||||||
|
func (r *SummaryRepository) populateItems(summaries []*models.Summary, conditions []clause.Interface) error {
|
||||||
|
var items []*models.SummaryItem
|
||||||
|
|
||||||
|
summaryMap := slice.GroupWith[*models.Summary, uint](summaries, func(s *models.Summary) uint {
|
||||||
|
return s.ID
|
||||||
|
})
|
||||||
|
|
||||||
|
q := r.db.Model(&models.SummaryItem{}).
|
||||||
|
Select("summary_items.*").
|
||||||
|
Joins("cross join summaries").
|
||||||
|
Where("summary_items.summary_id = summaries.id").
|
||||||
|
Where("num_heartbeats > ?", 0)
|
||||||
|
|
||||||
|
for _, c := range conditions {
|
||||||
|
q.Statement.AddClause(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Find(&items).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if _, ok := summaryMap[item.SummaryID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l := summaryMap[item.SummaryID][0].ItemsByType(item.Type)
|
||||||
|
*l = append(*l, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
@ -95,10 +98,9 @@ func (r *UserRepository) GetByLoggedInAfter(t time.Time) ([]*models.User, error)
|
|||||||
// Returns a list of user ids, whose last heartbeat is not older than t
|
// Returns a list of user ids, whose last heartbeat is not older than t
|
||||||
// NOTE: Only ID field will be populated
|
// NOTE: Only ID field will be populated
|
||||||
func (r *UserRepository) GetByLastActiveAfter(t time.Time) ([]*models.User, error) {
|
func (r *UserRepository) GetByLastActiveAfter(t time.Time) ([]*models.User, error) {
|
||||||
subQuery1 := r.db.Model(&models.User{}).
|
subQuery1 := r.db.Model(&models.Heartbeat{}).
|
||||||
Select("users.id as user, max(time) as time").
|
Select("user_id as user, max(time) as time").
|
||||||
Joins("left join heartbeats on users.id = heartbeats.user_id").
|
Group("user_id")
|
||||||
Group("user")
|
|
||||||
|
|
||||||
var userIds []string
|
var userIds []string
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
@ -123,16 +125,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) {
|
||||||
@ -147,7 +149,9 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
|||||||
"share_oss": user.ShareOSs,
|
"share_oss": user.ShareOSs,
|
||||||
"share_projects": user.ShareProjects,
|
"share_projects": user.ShareProjects,
|
||||||
"share_machines": user.ShareMachines,
|
"share_machines": user.ShareMachines,
|
||||||
|
"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,
|
||||||
@ -159,10 +163,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
51
routes/api/avatar.go
Normal file
51
routes/api/avatar.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"codeberg.org/Codeberg/avatars"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
lru "github.com/hashicorp/golang-lru"
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/utils"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 utils.IsNoCache(r, 1*time.Hour) {
|
||||||
|
h.cache.Remove(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)))
|
||||||
|
}
|
98
routes/api/badge.go
Normal file
98
routes/api/badge.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/duke-git/lancet/v2/maputil"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
v1 "github.com/muety/wakapi/models/compat/shields/v1"
|
||||||
|
routeutils "github.com/muety/wakapi/routes/utils"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
|
"github.com/muety/wakapi/utils"
|
||||||
|
"github.com/narqo/go-badge"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BadgeHandler struct {
|
||||||
|
config *conf.Config
|
||||||
|
cache *cache.Cache
|
||||||
|
userSrvc services.IUserService
|
||||||
|
summarySrvc services.ISummaryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBadgeHandler(userService services.IUserService, summaryService services.ISummaryService) *BadgeHandler {
|
||||||
|
return &BadgeHandler{
|
||||||
|
config: conf.Get(),
|
||||||
|
cache: cache.New(time.Hour, time.Hour),
|
||||||
|
userSrvc: userService,
|
||||||
|
summarySrvc: summaryService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {
|
||||||
|
r := router.PathPrefix("/badge/{user}").Subrouter()
|
||||||
|
r.Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, err := h.userSrvc.GetUserById(mux.Vars(r)["user"])
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interval, filters, err := routeutils.GetBadgeParams(r, user)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
|
||||||
|
noCache := utils.IsNoCache(r, 1*time.Hour)
|
||||||
|
if cacheResult, ok := h.cache.Get(cacheKey); ok && !noCache {
|
||||||
|
respondSvg(w, cacheResult.([]byte))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &models.SummaryParams{
|
||||||
|
From: interval.Start,
|
||||||
|
To: interval.End,
|
||||||
|
User: user,
|
||||||
|
Filters: filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err, status := routeutils.LoadUserSummaryByParams(h.summarySrvc, params)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeData := v1.NewBadgeDataFrom(summary)
|
||||||
|
if customLabel := r.URL.Query().Get("label"); customLabel != "" {
|
||||||
|
badgeData.Label = customLabel
|
||||||
|
}
|
||||||
|
if customColor := r.URL.Query().Get("color"); customColor != "" {
|
||||||
|
badgeData.Color = customColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if badgeData.Color[0:1] != "#" && !slice.Contain(maputil.Keys(badge.ColorScheme), badgeData.Color) {
|
||||||
|
badgeData.Color = "#" + badgeData.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeSvg, err := badge.RenderBytes(badgeData.Label, badgeData.Message, badge.Color(badgeData.Color))
|
||||||
|
h.cache.SetDefault(cacheKey, badgeSvg)
|
||||||
|
respondSvg(w, badgeSvg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondSvg(w http.ResponseWriter, data []byte) {
|
||||||
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
|
w.Header().Set("Cache-Control", "max-age=3600")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(data)
|
||||||
|
}
|
59
routes/api/diagnostics.go
Normal file
59
routes/api/diagnostics.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
|
"github.com/muety/wakapi/utils"
|
||||||
|
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.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"
|
||||||
|
// @Success 201
|
||||||
|
// @Router /plugins/errors [post]
|
||||||
|
func (h *DiagnosticsApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var diagnostics models.Diagnostics
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}{})
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"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"
|
||||||
@ -9,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"
|
|
||||||
|
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
)
|
)
|
||||||
@ -40,16 +40,22 @@ func (h *HeartbeatApiHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
|
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
|
||||||
customMiddleware.NewWakatimeRelayMiddleware().Handler,
|
customMiddleware.NewWakatimeRelayMiddleware().Handler,
|
||||||
)
|
)
|
||||||
r.PathPrefix("/heartbeat").Methods(http.MethodPost).HandlerFunc(h.Post)
|
// see https://github.com/muety/wakapi/issues/203
|
||||||
r.PathPrefix("/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
|
r.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
r.PathPrefix("/compat/wakatime/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
|
r.Path("/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
|
r.Path("/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
|
r.Path("/users/{user}/heartbeats.bulk").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
|
r.Path("/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
|
r.Path("/v1/users/{user}/heartbeats.bulk").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
|
r.Path("/compat/wakatime/v1/users/{user}/heartbeats").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
|
r.Path("/compat/wakatime/v1/users/{user}/heartbeats.bulk").Methods(http.MethodPost).HandlerFunc(h.Post)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Push a new heartbeat
|
// @Summary Push a new heartbeat
|
||||||
// @ID post-heartbeat
|
// @ID post-heartbeat
|
||||||
// @Tags heartbeat
|
// @Tags heartbeat
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param heartbeat body models.Heartbeat true "A heartbeat"
|
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Success 201
|
// @Success 201
|
||||||
// @Router /heartbeat [post]
|
// @Router /heartbeat [post]
|
||||||
@ -60,24 +66,27 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var heartbeats []*models.Heartbeat
|
var heartbeats []*models.Heartbeat
|
||||||
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
|
heartbeats, err = routeutils.ParseHeartbeats(r)
|
||||||
machineName := r.Header.Get("X-Machine-Name")
|
if err != nil {
|
||||||
|
conf.Log().Request(r).Error(err.Error())
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
if err := dec.Decode(&heartbeats); err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
opSys, editor, _ := utils.ParseUserAgent(userAgent)
|
||||||
|
machineName := r.Header.Get("X-Machine-Name")
|
||||||
|
|
||||||
for _, hb := range heartbeats {
|
for _, hb := range heartbeats {
|
||||||
hb.OperatingSystem = opSys
|
hb.OperatingSystem = opSys
|
||||||
hb.Editor = editor
|
hb.Editor = editor
|
||||||
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() || !hb.Timely(h.config.App.HeartbeatsMaxAge()) {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte("invalid heartbeat object"))
|
w.Write([]byte("invalid heartbeat object"))
|
||||||
return
|
return
|
||||||
@ -89,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,17 +107,22 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {}()
|
||||||
|
|
||||||
utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
|
utils.RespondJSON(w, r, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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": [ [ { "data": {...} }, 201 ], ... ] }
|
// response looks like: { "responses": [ [ null, 201 ], ... ] }
|
||||||
|
// this was probably a temporary bug at wakatime, responses actually looks like so: https://pastr.de/p/nyf6kj2e6843fbw4xkj4h4pj
|
||||||
|
// TODO: adapt response format some time
|
||||||
|
// however, wakatime-cli is still able to parse the response (see https://github.com/wakatime/wakatime-cli/blob/c2076c0e1abc1449baf5b7ac7db391b06041c719/pkg/api/heartbeat.go#L127), so no urgent need for action
|
||||||
func constructSuccessResponse(n int) *heartbeatResponseVm {
|
func constructSuccessResponse(n int) *heartbeatResponseVm {
|
||||||
responses := make([][]interface{}, n)
|
responses := make([][]interface{}, n)
|
||||||
|
|
||||||
@ -123,3 +137,81 @@ func constructSuccessResponse(n int) *heartbeatResponseVm {
|
|||||||
Responses: responses,
|
Responses: responses,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only for Swagger
|
||||||
|
|
||||||
|
// @Summary Push a new heartbeat
|
||||||
|
// @ID post-heartbeat-2
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
|
||||||
|
// @Param user path string true "Username (or current)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /v1/users/{user}/heartbeats [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias1() {}
|
||||||
|
|
||||||
|
// @Summary Push a new heartbeat
|
||||||
|
// @ID post-heartbeat-3
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
|
||||||
|
// @Param user path string true "Username (or current)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /compat/wakatime/v1/users/{user}/heartbeats [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias2() {}
|
||||||
|
|
||||||
|
// @Summary Push a new heartbeat
|
||||||
|
// @ID post-heartbeat-4
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body models.Heartbeat true "A single heartbeat"
|
||||||
|
// @Param user path string true "Username (or current)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /users/{user}/heartbeats [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias3() {}
|
||||||
|
|
||||||
|
// @Summary Push new heartbeats
|
||||||
|
// @ID post-heartbeat-5
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /heartbeats [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias4() {}
|
||||||
|
|
||||||
|
// @Summary Push new heartbeats
|
||||||
|
// @ID post-heartbeat-6
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
|
||||||
|
// @Param user path string true "Username (or current)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /v1/users/{user}/heartbeats.bulk [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias5() {}
|
||||||
|
|
||||||
|
// @Summary Push new heartbeats
|
||||||
|
// @ID post-heartbeat-7
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
|
||||||
|
// @Param user path string true "Username (or current)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /compat/wakatime/v1/users/{user}/heartbeats.bulk [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias6() {}
|
||||||
|
|
||||||
|
// @Summary Push new heartbeats
|
||||||
|
// @ID post-heartbeat-8
|
||||||
|
// @Tags heartbeat
|
||||||
|
// @Accept json
|
||||||
|
// @Param heartbeat body []models.Heartbeat true "Multiple heartbeats"
|
||||||
|
// @Param user path string true "Username (or current)"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 201
|
||||||
|
// @Router /users/{user}/heartbeats.bulk [post]
|
||||||
|
func (h *HeartbeatApiHandler) postAlias7() {}
|
||||||
|
@ -9,9 +9,11 @@ import (
|
|||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||||
mm "github.com/muety/wakapi/models/metrics"
|
mm "github.com/muety/wakapi/models/metrics"
|
||||||
|
"github.com/muety/wakapi/repositories"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@ -27,12 +29,18 @@ const (
|
|||||||
DescLanguages = "Total seconds for each language."
|
DescLanguages = "Total seconds for each language."
|
||||||
DescOperatingSystems = "Total seconds for each operating system."
|
DescOperatingSystems = "Total seconds for each operating system."
|
||||||
DescMachines = "Total seconds for each machine."
|
DescMachines = "Total seconds for each machine."
|
||||||
|
DescLabels = "Total seconds for each project label."
|
||||||
|
|
||||||
DescAdminTotalTime = "Total seconds (all users, all time)."
|
DescAdminTotalTime = "Total seconds (all users, all time)."
|
||||||
DescAdminTotalHeartbeats = "Total number of tracked heartbeats (all users, all time)"
|
DescAdminTotalHeartbeats = "Total number of tracked heartbeats (all users, all time)"
|
||||||
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"
|
||||||
|
DescDatabaseSize = "Total database size in bytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetricsHandler struct {
|
type MetricsHandler struct {
|
||||||
@ -41,14 +49,16 @@ type MetricsHandler struct {
|
|||||||
summarySrvc services.ISummaryService
|
summarySrvc services.ISummaryService
|
||||||
heartbeatSrvc services.IHeartbeatService
|
heartbeatSrvc services.IHeartbeatService
|
||||||
keyValueSrvc services.IKeyValueService
|
keyValueSrvc services.IKeyValueService
|
||||||
|
metricsRepo *repositories.MetricsRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService) *MetricsHandler {
|
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService, metricsRepo *repositories.MetricsRepository) *MetricsHandler {
|
||||||
return &MetricsHandler{
|
return &MetricsHandler{
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
summarySrvc: summaryService,
|
summarySrvc: summaryService,
|
||||||
heartbeatSrvc: heartbeatService,
|
heartbeatSrvc: heartbeatService,
|
||||||
keyValueSrvc: keyValueService,
|
keyValueSrvc: keyValueService,
|
||||||
|
metricsRepo: metricsRepo,
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,7 +120,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
|
||||||
@ -118,7 +128,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
|
||||||
@ -135,21 +145,21 @@ 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: int64(v1.NewAllTimeFrom(summaryAllTime).Data.TotalSeconds),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_seconds_total",
|
Name: MetricsPrefix + "_seconds_total",
|
||||||
Desc: DescTotal,
|
Desc: DescTotal,
|
||||||
Value: int(summaryToday.TotalTime().Seconds()),
|
Value: int64(summaryToday.TotalTime().Seconds()),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_heartbeats_total",
|
Name: MetricsPrefix + "_heartbeats_total",
|
||||||
Desc: DescHeartbeats,
|
Desc: DescHeartbeats,
|
||||||
Value: int(heartbeatCount),
|
Value: int64(heartbeatCount),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -157,7 +167,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_project_seconds_total",
|
Name: MetricsPrefix + "_project_seconds_total",
|
||||||
Desc: DescProjects,
|
Desc: DescProjects,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: p.Key}},
|
Labels: []mm.Label{{Key: "name", Value: p.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -166,7 +176,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_language_seconds_total",
|
Name: MetricsPrefix + "_language_seconds_total",
|
||||||
Desc: DescLanguages,
|
Desc: DescLanguages,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: l.Key}},
|
Labels: []mm.Label{{Key: "name", Value: l.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -175,7 +185,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_editor_seconds_total",
|
Name: MetricsPrefix + "_editor_seconds_total",
|
||||||
Desc: DescEditors,
|
Desc: DescEditors,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: e.Key}},
|
Labels: []mm.Label{{Key: "name", Value: e.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -184,7 +194,7 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_operating_system_seconds_total",
|
Name: MetricsPrefix + "_operating_system_seconds_total",
|
||||||
Desc: DescOperatingSystems,
|
Desc: DescOperatingSystems,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: o.Key}},
|
Labels: []mm.Label{{Key: "name", Value: o.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -193,11 +203,58 @@ func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_machine_seconds_total",
|
Name: MetricsPrefix + "_machine_seconds_total",
|
||||||
Desc: DescMachines,
|
Desc: DescMachines,
|
||||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
||||||
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, m := range summaryToday.Labels {
|
||||||
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
|
Name: MetricsPrefix + "_label_seconds_total",
|
||||||
|
Desc: DescLabels,
|
||||||
|
Value: int64(summaryToday.TotalTimeByKey(models.SummaryLabel, m.Key).Seconds()),
|
||||||
|
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime metrics
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
|
Name: MetricsPrefix + "_goroutines_total",
|
||||||
|
Desc: DescGoroutines,
|
||||||
|
Value: int64(runtime.NumGoroutine()),
|
||||||
|
Labels: []mm.Label{},
|
||||||
|
})
|
||||||
|
|
||||||
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
|
Name: MetricsPrefix + "_mem_alloc_total",
|
||||||
|
Desc: DescMemAllocTotal,
|
||||||
|
Value: int64(memStats.Alloc),
|
||||||
|
Labels: []mm.Label{},
|
||||||
|
})
|
||||||
|
|
||||||
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
|
Name: MetricsPrefix + "_mem_sys_total",
|
||||||
|
Desc: DescMemSysTotal,
|
||||||
|
Value: int64(memStats.Sys),
|
||||||
|
Labels: []mm.Label{},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Database metrics
|
||||||
|
dbSize, err := h.metricsRepo.GetDatabaseSize()
|
||||||
|
if err != nil {
|
||||||
|
logbuch.Warn("failed to get database size (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
|
Name: MetricsPrefix + "_db_total_bytes",
|
||||||
|
Desc: DescDatabaseSize,
|
||||||
|
Value: dbSize,
|
||||||
|
Labels: []mm.Label{},
|
||||||
|
})
|
||||||
|
|
||||||
return &metrics, nil
|
return &metrics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,39 +273,39 @@ 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(true)
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_seconds_total",
|
Name: MetricsPrefix + "_admin_seconds_total",
|
||||||
Desc: DescAdminTotalTime,
|
Desc: DescAdminTotalTime,
|
||||||
Value: totalSeconds,
|
Value: int64(totalSeconds),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_heartbeats_total",
|
Name: MetricsPrefix + "_admin_heartbeats_total",
|
||||||
Desc: DescAdminTotalHeartbeats,
|
Desc: DescAdminTotalHeartbeats,
|
||||||
Value: int(totalHeartbeats),
|
Value: totalHeartbeats,
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_users_total",
|
Name: MetricsPrefix + "_admin_users_total",
|
||||||
Desc: DescAdminTotalUsers,
|
Desc: DescAdminTotalUsers,
|
||||||
Value: int(totalUsers),
|
Value: totalUsers,
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_users_active_total",
|
Name: MetricsPrefix + "_admin_users_active_total",
|
||||||
Desc: DescAdminActiveUsers,
|
Desc: DescAdminActiveUsers,
|
||||||
Value: len(activeUsers),
|
Value: int64(len(activeUsers)),
|
||||||
Labels: []mm.Label{},
|
Labels: []mm.Label{},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -264,7 +321,7 @@ func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error)
|
|||||||
metrics = append(metrics, &mm.CounterMetric{
|
metrics = append(metrics, &mm.CounterMetric{
|
||||||
Name: MetricsPrefix + "_admin_user_heartbeats_total",
|
Name: MetricsPrefix + "_admin_user_heartbeats_total",
|
||||||
Desc: DescAdminUserHeartbeats,
|
Desc: DescAdminUserHeartbeats,
|
||||||
Value: int(uc.Count),
|
Value: uc.Count,
|
||||||
Labels: []mm.Label{{Key: "user", Value: uc.User}},
|
Labels: []mm.Label{{Key: "user", Value: uc.User}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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()))
|
||||||
|
@ -2,6 +2,10 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
routeutils "github.com/muety/wakapi/routes/utils"
|
||||||
|
"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/models"
|
"github.com/muety/wakapi/models"
|
||||||
@ -9,14 +13,6 @@ 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 (
|
|
||||||
intervalPattern = `interval:([a-z0-9_]+)`
|
|
||||||
entityFilterPattern = `(project|os|editor|language|machine):([_a-zA-Z0-9-]+)`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BadgeHandler struct {
|
type BadgeHandler struct {
|
||||||
@ -52,72 +48,45 @@ func (h *BadgeHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
// @Success 200 {object} v1.BadgeData
|
// @Success 200 {object} v1.BadgeData
|
||||||
// @Router /compat/shields/v1/{user}/{interval}/{filter} [get]
|
// @Router /compat/shields/v1/{user}/{interval}/{filter} [get]
|
||||||
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
intervalReg := regexp.MustCompile(intervalPattern)
|
user, err := h.userSrvc.GetUserById(mux.Vars(r)["user"])
|
||||||
entityFilterReg := regexp.MustCompile(entityFilterPattern)
|
|
||||||
|
|
||||||
var filterEntity, filterKey string
|
|
||||||
if groups := entityFilterReg.FindStringSubmatch(r.URL.Path); len(groups) > 2 {
|
|
||||||
filterEntity, filterKey = groups[1], groups[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
var interval = models.IntervalPast30Days
|
|
||||||
if groups := intervalReg.FindStringSubmatch(r.URL.Path); len(groups) > 1 {
|
|
||||||
if i, err := utils.ParseInterval(groups[1]); err == nil {
|
|
||||||
interval = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestedUserId := mux.Vars(r)["user"]
|
|
||||||
user, err := h.userSrvc.GetUserById(requestedUserId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, rangeFrom, rangeTo := utils.ResolveIntervalTZ(interval, user.TZ())
|
interval, filters, err := routeutils.GetBadgeParams(r, user)
|
||||||
minStart := utils.StartOfDay(rangeTo.Add(-24 * time.Hour * time.Duration(user.ShareDataMaxDays)))
|
if err != nil {
|
||||||
// negative value means no limit
|
|
||||||
if rangeFrom.Before(minStart) && user.ShareDataMaxDays >= 0 {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
w.Write([]byte("requested time range too broad"))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var filters *models.Filters
|
cacheKey := fmt.Sprintf("%s_%v_%s", user.ID, *interval.Key, filters.Hash())
|
||||||
switch filterEntity {
|
|
||||||
case "project":
|
|
||||||
filters = models.NewFiltersWith(models.SummaryProject, filterKey)
|
|
||||||
case "os":
|
|
||||||
filters = models.NewFiltersWith(models.SummaryOS, filterKey)
|
|
||||||
case "editor":
|
|
||||||
filters = models.NewFiltersWith(models.SummaryEditor, filterKey)
|
|
||||||
case "language":
|
|
||||||
filters = models.NewFiltersWith(models.SummaryLanguage, filterKey)
|
|
||||||
case "machine":
|
|
||||||
filters = models.NewFiltersWith(models.SummaryMachine, filterKey)
|
|
||||||
default:
|
|
||||||
filters = &models.Filters{}
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheKey := fmt.Sprintf("%s_%v_%s_%s", user.ID, *interval, filterEntity, filterKey)
|
|
||||||
if cacheResult, ok := h.cache.Get(cacheKey); ok {
|
if cacheResult, ok := h.cache.Get(cacheKey); ok {
|
||||||
utils.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
|
utils.RespondJSON(w, r, http.StatusOK, cacheResult.(*v1.BadgeData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err, status := h.loadUserSummary(user, interval)
|
params := &models.SummaryParams{
|
||||||
|
From: interval.Start,
|
||||||
|
To: interval.End,
|
||||||
|
User: user,
|
||||||
|
Filters: filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err, status := routeutils.LoadUserSummaryByParams(h.summarySrvc, params)
|
||||||
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
|
||||||
@ -134,7 +103,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
|
||||||
}
|
}
|
||||||
|
43
routes/compat/shields/v1/badge_test.go
Normal file
43
routes/compat/shields/v1/badge_test.go
Normal 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(`(project|os|editor|language|machine|label):([^:?&/]+)`) // see entityFilterPattern in badge_utils.go
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
86
routes/compat/wakatime/v1/heartbeat.go
Normal file
86
routes/compat/wakatime/v1/heartbeat.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/duke-git/lancet/v2/datetime"
|
||||||
|
"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 := datetime.BeginOfDay(date.In(timezone)), datetime.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)
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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]
|
||||||
@ -79,7 +86,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
minStart := utils.StartOfDay(rangeTo.Add(-24 * time.Hour * time.Duration(requestedUser.ShareDataMaxDays)))
|
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(requestedUser.ShareDataMaxDays))
|
||||||
if (authorizedUser == nil || requestedUser.ID != authorizedUser.ID) &&
|
if (authorizedUser == nil || requestedUser.ID != authorizedUser.ID) &&
|
||||||
rangeFrom.Before(minStart) && requestedUser.ShareDataMaxDays >= 0 {
|
rangeFrom.Before(minStart) && requestedUser.ShareDataMaxDays >= 0 {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
107
routes/compat/wakatime/v1/statusbar.go
Normal file
107
routes/compat/wakatime/v1/statusbar.go
Normal 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
|
||||||
|
}
|
@ -2,6 +2,11 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/duke-git/lancet/v2/datetime"
|
||||||
|
"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 +15,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 +53,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 +75,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,20 +121,22 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
|||||||
// i.e. for wakatime, an interval 2021-04-29 - 2021-04-29 is actually 2021-04-29 - 2021-04-30,
|
// i.e. for wakatime, an interval 2021-04-29 - 2021-04-29 is actually 2021-04-29 - 2021-04-30,
|
||||||
// while for wakapi it would be empty
|
// while for wakapi it would be empty
|
||||||
// see https://github.com/muety/wakapi/issues/192
|
// see https://github.com/muety/wakapi/issues/192
|
||||||
end = utils.EndOfDay(end).Add(-1 * time.Second)
|
end = datetime.EndOfDay(end)
|
||||||
|
|
||||||
overallParams := &models.SummaryParams{
|
overallParams := &models.SummaryParams{
|
||||||
From: start,
|
From: start,
|
||||||
To: end,
|
To: end,
|
||||||
User: user,
|
User: user,
|
||||||
Recompute: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, false)
|
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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -91,6 +91,7 @@ func (h *LoginHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
|
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
conf.Log().Request(r).Error("failed to encode secure cookie - %v", err)
|
||||||
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
templates[conf.LoginTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -98,7 +99,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 +108,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +164,7 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, created, err := h.userSrvc.CreateOrGet(&signup, numUsers == 0)
|
_, created, err := h.userSrvc.CreateOrGet(&signup, numUsers == 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
conf.Log().Request(r).Error("failed to create new user - %v", err)
|
||||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user"))
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -237,6 +239,7 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
user.ResetToken = ""
|
user.ResetToken = ""
|
||||||
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
conf.Log().Request(r).Error("failed to set new password - %v", err)
|
||||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@ -245,6 +248,7 @@ func (h *LoginHandler) PostSetPassword(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)
|
||||||
|
conf.Log().Request(r).Error("failed to save new password - %v", err)
|
||||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to save new password"))
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to save new password"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -278,13 +282,14 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
|
|||||||
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
|
if user, err := h.userSrvc.GetUserByEmail(resetRequest.Email); user != nil && err == nil {
|
||||||
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
|
if u, err := h.userSrvc.GenerateResetToken(user); err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
conf.Log().Request(r).Error("failed to generate password reset token - %v", err)
|
||||||
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate password reset token"))
|
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate password reset token"))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
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 +306,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
120
routes/relay/relay.go
Normal 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() {}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user