mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
088bd17803 | |||
2976203ecc | |||
e75bd94531 | |||
4cc8c21f67 | |||
f182b804bb | |||
9586dbf781 | |||
c8ea1a503f | |||
ebbc21f0b1 | |||
6e5bc38e5e | |||
9424c49760 | |||
efd6ba36e3 | |||
b1d7f87095 | |||
ffbcfc7467 | |||
41f6db8f34 | |||
8a21be4306 | |||
31ca4a1e02 | |||
7cab2b0be7 | |||
777997c883 | |||
060a33263a | |||
33d259592c | |||
fbae5f8757 | |||
bc99dc990a | |||
1e9d3f9e80 | |||
2ce720c20f | |||
ef87445e43 | |||
dec5849661 | |||
5609c0ada3 | |||
1632cea949 | |||
23759d526a | |||
82a565738f | |||
1989a69926 | |||
7a07c9d4fc | |||
a27fe04919 | |||
1d7ff4bc2a | |||
b3fa032bde | |||
94377a8dea | |||
dba4da8641 | |||
4a22a19cb0 | |||
13a3d9f03a | |||
beffe71ea6 | |||
0ab7faf7b6 | |||
a2ac049578 | |||
b287c4ca36 | |||
018cc50fb8 | |||
1d4156bdfe | |||
147c79db60 | |||
f204ca888d | |||
e28070b288 | |||
4d217a83c1 | |||
9e0581b311 | |||
ffb529f4cf | |||
c9aac2a273 | |||
dd8658e33e | |||
e399af1f1f | |||
4c1f4ed39b | |||
7e5c00d0ae |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -1,9 +1,7 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@ -34,6 +32,9 @@ jobs:
|
||||
mapi:
|
||||
name: 'Automated pen-tests with Mayhem for API'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
@ -47,7 +48,7 @@ jobs:
|
||||
run: go get
|
||||
|
||||
- name: Build
|
||||
run: GO111MODULE=on go build -v .
|
||||
run: go build -v .
|
||||
|
||||
- name: start wakapi
|
||||
run: ./wakapi --config config.default.yml &
|
||||
@ -82,8 +83,10 @@ jobs:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
|
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@ -8,12 +8,26 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: 'Build, package and release to GitHub'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
GOOS: [darwin, windows, linux]
|
||||
GOARCH: [amd64, arm64]
|
||||
include:
|
||||
- platform: ubuntu-18.04
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
- platform: ubuntu-18.04
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
- platform: windows-latest
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
- platform: macos-latest
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
- platform: macos-latest
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
|
||||
@ -40,11 +54,20 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
working-directory: ./dist
|
||||
shell: bash
|
||||
run: |
|
||||
GOOS=${{ matrix.GOOS }} GOARCH=${{ matrix.GOARCH }} go build -v ../
|
||||
GOOS=${{ matrix.GOOS }} GOARCH=${{ matrix.GOARCH }} CGO_ENABLED=0 \
|
||||
go build -v -ldflags '-w -s' ../
|
||||
|
||||
- name: Compress working folder (Windows PowerShell)
|
||||
working-directory: ./dist
|
||||
if: "${{ matrix.GOOS == 'windows' }}"
|
||||
run: |
|
||||
Compress-Archive -Path .\wakapi.exe, .\config.yml -DestinationPath wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip
|
||||
|
||||
- name: Compress working folder
|
||||
working-directory: ./dist
|
||||
if: "${{ matrix.GOOS != 'windows' }}"
|
||||
run: |
|
||||
zip -9 wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip *
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
FROM golang:1.18-alpine AS build-env
|
||||
WORKDIR /src
|
||||
|
||||
# Required for go-sqlite3
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
|
||||
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
|
||||
|
||||
@ -11,7 +8,7 @@ ADD ./go.mod ./go.sum ./
|
||||
RUN go mod download
|
||||
ADD . .
|
||||
|
||||
RUN go build -ldflags "-s -w" -v -o wakapi main.go
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o wakapi main.go
|
||||
|
||||
WORKDIR /staging
|
||||
RUN mkdir ./data ./app && \
|
||||
|
17
README.md
17
README.md
@ -66,6 +66,11 @@ If you want to try out a free, hosted cloud service, all you need to do is creat
|
||||
$ curl -L https://wakapi.dev/get | bash
|
||||
```
|
||||
|
||||
**Alternatively** using [eget](https://github.com/zyedidia/eget):
|
||||
```bash
|
||||
$ eget muety/wakapi
|
||||
```
|
||||
|
||||
### 🐳 Option 3: Use Docker
|
||||
|
||||
```bash
|
||||
@ -89,16 +94,6 @@ If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https:
|
||||
|
||||
### 🧑💻 Option 4: Compile and run from source
|
||||
|
||||
#### 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
|
||||
# Build and install
|
||||
# Alternatively: go build -o wakapi
|
||||
@ -312,7 +307,7 @@ Unit tests are supposed to test business logic on a fine-grained level. They are
|
||||
#### How to run
|
||||
|
||||
```bash
|
||||
$ CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
|
||||
$ CGO_ENABLED=0 go test -json -coverprofile=coverage/coverage.out ./... -run ./...
|
||||
```
|
||||
|
||||
### API tests
|
||||
|
@ -12,14 +12,18 @@ server:
|
||||
public_url: http://localhost:3000 # required for links (e.g. password reset) in e-mail
|
||||
|
||||
app:
|
||||
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
|
||||
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
|
||||
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
||||
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
|
||||
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
|
||||
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
|
||||
leaderboard_generation_time: '06:00;18:00' # time at which to run daily aggregation batch jobs
|
||||
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
|
||||
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
||||
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
|
||||
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
|
||||
custom_languages:
|
||||
vue: Vue
|
||||
jsx: JSX
|
||||
tsx: TSX
|
||||
cjs: JavaScript
|
||||
ipynb: Python
|
||||
svelte: Svelte
|
||||
|
||||
# url template for user avatar images (to be used with services like gravatar or dicebear)
|
||||
|
@ -65,16 +65,17 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
|
||||
var env string
|
||||
|
||||
type appConfig struct {
|
||||
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
|
||||
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
|
||||
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
|
||||
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
|
||||
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
|
||||
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
|
||||
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
|
||||
AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg" env:"WAKAPI_AVATAR_URL_TEMPLATE"`
|
||||
CustomLanguages map[string]string `yaml:"custom_languages"`
|
||||
Colors map[string]map[string]string `yaml:"-"`
|
||||
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
|
||||
LeaderboardGenerationTime string `yaml:"leaderboard_generation_time" default:"06:00;18:00" env:"WAKAPI_LEADERBOARD_GENERATION_TIME"`
|
||||
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
|
||||
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
|
||||
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
|
||||
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
|
||||
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
|
||||
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
|
||||
AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg" env:"WAKAPI_AVATAR_URL_TEMPLATE"`
|
||||
CustomLanguages map[string]string `yaml:"custom_languages"`
|
||||
Colors map[string]map[string]string `yaml:"-"`
|
||||
}
|
||||
|
||||
type securityConfig struct {
|
||||
@ -97,6 +98,7 @@ type dbConfig struct {
|
||||
Dialect string `yaml:"-"`
|
||||
Charset string `default:"utf8mb4" env:"WAKAPI_DB_CHARSET"`
|
||||
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
|
||||
DSN string `yaml:"DSN" default:"" env:"WAKAPI_DB_DSN"`
|
||||
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
|
||||
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
|
||||
AutoMigrateFailSilently bool `yaml:"automigrate_fail_silently" default:"false" env:"WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY"`
|
||||
@ -216,6 +218,9 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
|
||||
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
if err := db.AutoMigrate(&models.LeaderboardItem{}); err != nil && !c.Db.AutoMigrateFailSilently {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -66,6 +66,10 @@ func mysqlConnectionString(config *dbConfig) string {
|
||||
}
|
||||
|
||||
func postgresConnectionString(config *dbConfig) string {
|
||||
if len(config.DSN) > 0 {
|
||||
return config.DSN
|
||||
}
|
||||
|
||||
sslmode := "disable"
|
||||
if config.Ssl {
|
||||
sslmode = "require"
|
||||
|
@ -9,4 +9,5 @@ const (
|
||||
ResetPasswordTemplate = "reset-password.tpl.html"
|
||||
SettingsTemplate = "settings.tpl.html"
|
||||
SummaryTemplate = "summary.tpl.html"
|
||||
LeaderboardTemplate = "leaderboard.tpl.html"
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
73
go.mod
73
go.mod
@ -3,14 +3,14 @@ module github.com/muety/wakapi
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/duke-git/lancet/v2 v2.0.4
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac
|
||||
codeberg.org/Codeberg/avatars v1.0.0
|
||||
github.com/duke-git/lancet/v2 v2.1.6
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
github.com/emersion/go-smtp v0.15.0
|
||||
github.com/emvi/logbuch v1.2.0
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
github.com/go-co-op/gocron v1.13.0
|
||||
github.com/getsentry/sentry-go v0.14.0
|
||||
github.com/glebarez/sqlite v1.5.0
|
||||
github.com/go-co-op/gocron v1.17.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/schema v1.2.0
|
||||
@ -18,58 +18,65 @@ require (
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/jinzhu/configor v1.2.1
|
||||
github.com/leandro-lugaresi/hub v1.1.1
|
||||
github.com/lpar/gzipped/v2 v2.0.2
|
||||
github.com/lpar/gzipped/v2 v2.1.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.8.1
|
||||
go.uber.org/atomic v1.9.0
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gorm.io/driver/mysql v1.3.3
|
||||
gorm.io/driver/postgres v1.3.4
|
||||
gorm.io/driver/sqlite v1.3.1
|
||||
gorm.io/gorm v1.23.4
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/swaggo/http-swagger v1.3.3
|
||||
github.com/swaggo/swag v1.8.6
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
|
||||
gorm.io/driver/mysql v1.4.1
|
||||
gorm.io/driver/postgres v1.4.4
|
||||
gorm.io/driver/sqlite v1.4.2
|
||||
gorm.io/gorm v1.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.2.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/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/glebarez/go-sqlite v1.19.1 // 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-openapi/spec v0.20.7 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.11.0 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.10.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.15.0 // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||
github.com/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
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
|
||||
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.20.3 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.4.0 // indirect
|
||||
modernc.org/sqlite v1.19.1 // indirect
|
||||
)
|
||||
|
261
go.sum
261
go.sum
@ -1,68 +1,63 @@
|
||||
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69 h1:/XvI42KX57UTpeIOIt7IfM+pmEFTL8FGtiIUGcGDOIU=
|
||||
codeberg.org/Codeberg/avatars v0.0.0-20211228163022-8da63012fe69/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
|
||||
codeberg.org/Codeberg/avatars v1.0.0 h1:MRx5QxuT/oVCcPvC5rXwgwWKD7hc6J0GnZ0Kl67lYEM=
|
||||
codeberg.org/Codeberg/avatars v1.0.0/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/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/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/duke-git/lancet/v2 v2.0.4 h1:IvMurTpL0cGhQmGPtkCge2eCkuiu3USQtglZJnKXxEo=
|
||||
github.com/duke-git/lancet/v2 v2.0.4/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
|
||||
github.com/duke-git/lancet/v2 v2.1.6 h1:zRWZkK3IAoGnzEonbrkmUP2NyHqtH9qIlW0AaSQrzmY=
|
||||
github.com/duke-git/lancet/v2 v2.1.6/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
|
||||
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emvi/logbuch v1.2.0 h1:Bw0jQH1Dbs+oIygZBNx/2Ub1igXRFtKQrIMRrZdVFJM=
|
||||
github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrbQHuc=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
|
||||
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
|
||||
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-co-op/gocron v1.13.0 h1:BjkuNImPy5NuIPEifhWItFG7pYyr27cyjS6BN9w/D4c=
|
||||
github.com/go-co-op/gocron v1.13.0/go.mod h1:GD5EIEly1YNW+LovFVx5dzbYVcIc8544K99D8UVRpGo=
|
||||
github.com/getsentry/sentry-go v0.14.0 h1:rlOBkuFZRKKdUnKO+0U3JclRDQKlRu5vVQtkWSQvC70=
|
||||
github.com/getsentry/sentry-go v0.14.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
|
||||
github.com/glebarez/go-sqlite v1.18.2 h1:ck3PQVaEzzzapP0g7pfhzbB3Jw4rNk+IldLMy/lgdeQ=
|
||||
github.com/glebarez/go-sqlite v1.18.2/go.mod h1:/kOdnnt5T0ztYXqBPdjRVM8JwMpFtyAQp1mtRoNxziM=
|
||||
github.com/glebarez/go-sqlite v1.19.1 h1:o2XhjyR8CQ2m84+bVz10G0cabmG0tY4sIMiCbrcUTrY=
|
||||
github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M=
|
||||
github.com/glebarez/sqlite v1.4.7 h1:tIBxEWLJOPkekuQcwfenNfh13itj9GoVJYxp7GidJAo=
|
||||
github.com/glebarez/sqlite v1.4.7/go.mod h1:UY1smw9rBTSGnJE0He8pVRPvlxCP1C8hlB8Z24K8fG4=
|
||||
github.com/glebarez/sqlite v1.5.0 h1:+8LAEpmywqresSoGlqjjT+I9m4PseIM3NcerIJ/V7mk=
|
||||
github.com/glebarez/sqlite v1.5.0/go.mod h1:0wzXzTvfVJIN2GqRhCdMbnYd+m+aH5/QV7B30rM6NgY=
|
||||
github.com/go-co-op/gocron v1.17.0 h1:IixLXsti+Qo0wMvmn6Kmjp2csk2ykpkcL+EmHmST18w=
|
||||
github.com/go-co-op/gocron v1.17.0/go.mod h1:IpDBSaJOVfFw7hXZuTag3SCSkqazXBBUkbQ1m1aesBs=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/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.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.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
|
||||
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
||||
github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog=
|
||||
github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-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-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@ -70,8 +65,12 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
@ -82,6 +81,7 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
@ -92,8 +92,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.11.0 h1:HiHArx4yFbwl91X3qqIHtUFoiIfLNJXCQRsnzkiwwaQ=
|
||||
github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
||||
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
@ -102,6 +102,7 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
@ -109,26 +110,26 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
|
||||
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38=
|
||||
github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w=
|
||||
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
|
||||
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
|
||||
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@ -138,12 +139,13 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d/go.mod h1:3FSWkzk9h42opyV0o357Fq6gsLF/A6MI/qOca9kKobY=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
|
||||
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
@ -157,8 +159,8 @@ 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.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lpar/gzipped/v2 v2.0.2 h1:y7FjyTH07f8dX0YQ5o0sg2DTbRnmS3oT1pUvxViQ//o=
|
||||
github.com/lpar/gzipped/v2 v2.0.2/go.mod h1:qb7pLOGFgqz5w9xGGiiRFPxuGZ7GqWEuXUKXSbgonkQ=
|
||||
github.com/lpar/gzipped/v2 v2.1.0 h1:87/ug239roEqXLVOnXZg6NjDfFvMwmkGTKnFWJPUA9U=
|
||||
github.com/lpar/gzipped/v2 v2.1.0/go.mod h1:G3UlFoFYzjCx6NV4zDmD1BIWMNBaJuKoUvxrEWJuZ3Y=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
@ -169,14 +171,16 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
|
||||
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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e h1:bR8DQ4ZfItytLJwRlrLOPUHd5z18V6tECwYQFy8W+8g=
|
||||
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
@ -185,45 +189,50 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
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/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
|
||||
github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
|
||||
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
|
||||
github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg=
|
||||
github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
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.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
@ -240,35 +249,34 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
|
||||
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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-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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
|
||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -281,18 +289,20 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
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/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@ -305,39 +315,96 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
|
||||
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/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
|
||||
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
|
||||
gorm.io/driver/postgres v1.3.4 h1:evZ7plF+Bp+Lr1mO5NdPvd6M/N98XtwHixGB+y7fdEQ=
|
||||
gorm.io/driver/postgres v1.3.4/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw=
|
||||
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=
|
||||
gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=
|
||||
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
|
||||
gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||
gorm.io/driver/mysql v1.4.1 h1:4InA6SOaYtt4yYpV1NF9B2kvUKe9TbvUd1iWrvxnjic=
|
||||
gorm.io/driver/mysql v1.4.1/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||
gorm.io/driver/postgres v1.3.10 h1:Fsd+pQpFMGlGxxVMUPJhNo8gG8B1lKtk8QQ4/VZZAJw=
|
||||
gorm.io/driver/postgres v1.3.10/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
|
||||
gorm.io/driver/postgres v1.4.4 h1:zt1fxJ+C+ajparn0SteEnkoPg0BQ6wOWXEQ99bteAmw=
|
||||
gorm.io/driver/postgres v1.4.4/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
|
||||
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
|
||||
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
|
||||
gorm.io/driver/sqlite v1.4.2 h1:F6vYJcmR4Cnh0ErLyoY8JSfabBGyR0epIGuhgHJuNws=
|
||||
gorm.io/driver/sqlite v1.4.2/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.10 h1:4Ne9ZbzID9GUxRkllxN4WjJKpsHx8YbKvekVdgyWh24=
|
||||
gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
|
||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
||||
modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
|
||||
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
|
||||
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||
modernc.org/libc v1.20.0 h1:MEbCfCKpuDC/LRb3HOCM9fZOqnPx8le3kzTJVmUGDbU=
|
||||
modernc.org/libc v1.20.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||
modernc.org/libc v1.20.3 h1:BodaDPuUse7taQchAClMmbE/yZp3T2ZBiwCDFyBLEXw=
|
||||
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
|
||||
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0=
|
||||
modernc.org/sqlite v1.19.1 h1:8xmS5oLnZtAK//vnd4aTVj8VOeTAccEFOtUnIzfSw+4=
|
||||
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0=
|
||||
modernc.org/tcl v1.14.0/go.mod h1:gQ7c1YPMvryCHCcmf8acB6VPabE59QBeuRQLL7cTUlM=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||
|
23
main.go
23
main.go
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"github.com/muety/wakapi/static/docs"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/lpar/gzipped/v2"
|
||||
"github.com/swaggo/http-swagger"
|
||||
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
@ -33,13 +35,17 @@ import (
|
||||
_ "gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
_ "github.com/muety/wakapi/static/docs"
|
||||
)
|
||||
|
||||
// Embed version.txt
|
||||
//
|
||||
//go:embed version.txt
|
||||
var version string
|
||||
|
||||
// Embed static files
|
||||
//
|
||||
//go:embed static
|
||||
var staticFiles embed.FS
|
||||
|
||||
@ -55,6 +61,7 @@ var (
|
||||
languageMappingRepository repositories.ILanguageMappingRepository
|
||||
projectLabelRepository repositories.IProjectLabelRepository
|
||||
summaryRepository repositories.ISummaryRepository
|
||||
leaderboardRepository *repositories.LeaderboardRepository
|
||||
keyValueRepository repositories.IKeyValueRepository
|
||||
diagnosticsRepository repositories.IDiagnosticsRepository
|
||||
metricsRepository *repositories.MetricsRepository
|
||||
@ -68,6 +75,7 @@ var (
|
||||
projectLabelService services.IProjectLabelService
|
||||
durationService services.IDurationService
|
||||
summaryService services.ISummaryService
|
||||
leaderboardService services.ILeaderboardService
|
||||
aggregationService services.IAggregationService
|
||||
mailService services.IMailService
|
||||
keyValueService services.IKeyValueService
|
||||
@ -97,10 +105,12 @@ var (
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
// @BasePath /api
|
||||
func main() {
|
||||
config = conf.Load(version)
|
||||
|
||||
// Configure Swagger docs
|
||||
docs.SwaggerInfo.BasePath = config.Server.BasePath + "/api"
|
||||
|
||||
// Set log level
|
||||
if config.IsDev() {
|
||||
logbuch.SetLevel(logbuch.LevelDebug)
|
||||
@ -153,6 +163,7 @@ func main() {
|
||||
languageMappingRepository = repositories.NewLanguageMappingRepository(db)
|
||||
projectLabelRepository = repositories.NewProjectLabelRepository(db)
|
||||
summaryRepository = repositories.NewSummaryRepository(db)
|
||||
leaderboardRepository = repositories.NewLeaderboardRepository(db)
|
||||
keyValueRepository = repositories.NewKeyValueRepository(db)
|
||||
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
|
||||
metricsRepository = repositories.NewMetricsRepository(db)
|
||||
@ -166,6 +177,7 @@ func main() {
|
||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
||||
durationService = services.NewDurationService(heartbeatService)
|
||||
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
|
||||
leaderboardService = services.NewLeaderboardService(leaderboardRepository, summaryService, userService)
|
||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
|
||||
keyValueService = services.NewKeyValueService(keyValueRepository)
|
||||
reportService = services.NewReportService(summaryService, userService, mailService)
|
||||
@ -174,6 +186,7 @@ func main() {
|
||||
|
||||
// Schedule background tasks
|
||||
go aggregationService.Schedule()
|
||||
go leaderboardService.ScheduleDefault()
|
||||
go miscService.ScheduleCountTotalTime()
|
||||
go reportService.Schedule()
|
||||
|
||||
@ -201,6 +214,7 @@ func main() {
|
||||
// MVC Handlers
|
||||
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
|
||||
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
|
||||
leaderboardHandler := routes.NewLeaderboardHandler(userService, leaderboardService)
|
||||
homeHandler := routes.NewHomeHandler(keyValueService)
|
||||
loginHandler := routes.NewLoginHandler(userService, mailService)
|
||||
imprintHandler := routes.NewImprintHandler(keyValueService)
|
||||
@ -235,6 +249,7 @@ func main() {
|
||||
loginHandler.RegisterRoutes(rootRouter)
|
||||
imprintHandler.RegisterRoutes(rootRouter)
|
||||
summaryHandler.RegisterRoutes(rootRouter)
|
||||
leaderboardHandler.RegisterRoutes(rootRouter)
|
||||
settingsHandler.RegisterRoutes(rootRouter)
|
||||
relayHandler.RegisterRoutes(rootRouter)
|
||||
|
||||
@ -269,10 +284,8 @@ func main() {
|
||||
|
||||
router.PathPrefix("/contribute.json").Handler(staticFileServer)
|
||||
router.PathPrefix("/assets").Handler(assetsFileServer)
|
||||
router.PathPrefix("/swagger-ui").Handler(staticFileServer)
|
||||
router.PathPrefix("/docs").Handler(
|
||||
middlewares.NewFileTypeFilterMiddleware([]string{".go"})(staticFileServer),
|
||||
)
|
||||
router.Path("/swagger-ui").Handler(http.RedirectHandler("swagger-ui/", http.StatusMovedPermanently)) // https://github.com/swaggo/http-swagger/issues/44
|
||||
router.PathPrefix("/swagger-ui").Handler(httpSwagger.WrapHandler)
|
||||
|
||||
// Listen HTTP
|
||||
listen(router)
|
||||
|
24
migrations/20220930_drop_heartbeats_entity_idx.go
Normal file
24
migrations/20220930_drop_heartbeats_entity_idx.go
Normal file
@ -0,0 +1,24 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20220930-drop_heartbeats_entity_idx"
|
||||
const idxName = "idx_entity"
|
||||
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if !db.Migrator().HasTable(&models.Heartbeat{}) || !db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
|
||||
return nil
|
||||
}
|
||||
return db.Migrator().DropIndex(&models.Heartbeat{}, idxName)
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
88
migrations/20221002_fix_summary_id_types.go
Normal file
88
migrations/20221002_fix_summary_id_types.go
Normal file
@ -0,0 +1,88 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fix for https://github.com/muety/wakapi/issues/416
|
||||
|
||||
func init() {
|
||||
const name = "20221002-fix_summary_id_types"
|
||||
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if cfg.Db.Dialect != config.SQLDialectMysql {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !db.Migrator().HasTable(&models.Summary{}) || !db.Migrator().HasTable(&models.SummaryItem{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentType string
|
||||
if err := db.
|
||||
Table("information_schema.columns").
|
||||
Select("data_type").
|
||||
Where("table_name = ?", "summary_items").
|
||||
Where("column_name = ?", "summary_id").
|
||||
Limit(1).
|
||||
Row().Scan(¤tType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(currentType) != "int" {
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_editors") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_editors"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_languages") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_languages"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_machines") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_machines"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_operating_systems") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_operating_systems"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_projects") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_projects"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// https://github.com/muety/wakapi/issues/416#issuecomment-1271674792
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summary_items_summary") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summary_items_summary"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasConstraint(&models.SummaryItem{}, "fk_summaries_labels") {
|
||||
if err := db.Migrator().DropConstraint(&models.SummaryItem{}, "fk_summaries_labels"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Migrator().AlterColumn(&models.Summary{}, "id"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Migrator().AlterColumn(&models.SummaryItem{}, "summary_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
35
migrations/20221016_drop_rank_column.go
Normal file
35
migrations/20221016_drop_rank_column.go
Normal file
@ -0,0 +1,35 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20221016-drop_rank_column"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
if hasRun(name, db) {
|
||||
return nil
|
||||
}
|
||||
|
||||
migrator := db.Migrator()
|
||||
|
||||
if migrator.HasTable(&models.LeaderboardItem{}) && migrator.HasColumn(&models.LeaderboardItem{}, "rank") {
|
||||
logbuch.Info("running migration '%s'", name)
|
||||
|
||||
if err := migrator.DropColumn(&models.LeaderboardItem{}, "rank"); err != nil {
|
||||
logbuch.Warn("failed to drop 'rank' column (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
setHasRun(name, db)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPostMigration(f)
|
||||
}
|
71
migrations/20221028_fix_heartbeats_time_user_idx.go
Normal file
71
migrations/20221028_fix_heartbeats_time_user_idx.go
Normal file
@ -0,0 +1,71 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// due to an error in the model definition, idx_time_user used to only cover 'user_id', but not time column
|
||||
// if that's the case in the current state of the database, drop the index and let it be recreated by auto migration afterwards
|
||||
func init() {
|
||||
const name = "20221028-fix_heartbeats_time_user_idx"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
migrator := db.Migrator()
|
||||
|
||||
if !migrator.HasTable(&models.Heartbeat{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var drop bool
|
||||
if cfg.Db.IsSQLite() {
|
||||
// sqlite migrator doesn't support GetIndexes() currently
|
||||
var ddl string
|
||||
if err := db.
|
||||
Table("sqlite_schema").
|
||||
Select("sql").
|
||||
Where("type = 'index'").
|
||||
Where("tbl_name = 'heartbeats'").
|
||||
Where("name = 'idx_time_user'").
|
||||
Scan(&ddl).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := regexp.MustCompile("(?i)\\((.+)\\)$").FindStringSubmatch(ddl)
|
||||
if len(matches) > 0 && !strings.Contains(matches[0], "`user_id`") || !strings.Contains(matches[0], "`time`") {
|
||||
drop = true
|
||||
}
|
||||
} else {
|
||||
indexes, err := migrator.GetIndexes(&models.Heartbeat{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, idx := range indexes {
|
||||
if idx.Table() == "heartbeats" && idx.Name() == "idx_time_user" && len(idx.Columns()) == 1 {
|
||||
drop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !drop {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := migrator.DropIndex(&models.Heartbeat{}, "idx_time_user"); err != nil {
|
||||
return err
|
||||
}
|
||||
logbuch.Info("index 'idx_time_user' needs to be recreated, this may take a while")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPreMigration(f)
|
||||
}
|
@ -34,6 +34,21 @@ func (m *UserServiceMock) GetAll() ([]*models.User, error) {
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetMany(s []string) ([]*models.User, error) {
|
||||
args := m.Called(s)
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetManyMapped(s []string) (map[string]*models.User, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).(map[string]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetAllByLeaderboard(b bool) ([]*models.User, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) GetAllByReports(b bool) ([]*models.User, error) {
|
||||
args := m.Called(b)
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
|
@ -3,6 +3,7 @@ package v1
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://wakatime.com/developers#all_time_since_today
|
||||
@ -28,11 +29,19 @@ type AllTimeRange struct {
|
||||
|
||||
func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
|
||||
total := summary.TotalTime()
|
||||
tzName, _ := summary.FromTime.T().Zone()
|
||||
return &AllTimeViewModel{
|
||||
Data: &AllTimeData{
|
||||
TotalSeconds: float32(total.Seconds()),
|
||||
Text: utils.FmtWakatimeDuration(total),
|
||||
IsUpToDate: true,
|
||||
Range: &AllTimeRange{
|
||||
End: summary.ToTime.T().Format(time.RFC3339),
|
||||
EndDate: utils.FormatDate(summary.ToTime.T()),
|
||||
Start: summary.FromTime.T().Format(time.RFC3339),
|
||||
StartDate: utils.FormatDate(summary.FromTime.T()),
|
||||
Timezone: tzName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,17 @@ import (
|
||||
// https://pastr.de/v/736450
|
||||
|
||||
type SummariesViewModel struct {
|
||||
Data []*SummariesData `json:"data"`
|
||||
End time.Time `json:"end"`
|
||||
Start time.Time `json:"start"`
|
||||
Data []*SummariesData `json:"data"`
|
||||
End time.Time `json:"end"`
|
||||
Start time.Time `json:"start"`
|
||||
CumulativeTotal *SummariesCumulativeTotal `json:"cummulative_total"` // typo is intended
|
||||
}
|
||||
|
||||
type SummariesCumulativeTotal struct {
|
||||
Decimal string `json:"decimal"`
|
||||
Digital string `json:"digital"`
|
||||
Seconds float64 `json:"seconds"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type SummariesData struct {
|
||||
@ -73,10 +81,23 @@ func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
var totalTime time.Duration
|
||||
for _, s := range summaries {
|
||||
totalTime += s.TotalTime()
|
||||
}
|
||||
|
||||
totalHrs, totalMins, totalSecs := totalTime.Hours(), (totalTime - time.Duration(totalTime.Hours())*time.Hour).Minutes(), totalTime.Seconds()
|
||||
|
||||
return &SummariesViewModel{
|
||||
Data: data,
|
||||
End: maxDate,
|
||||
Start: minDate,
|
||||
CumulativeTotal: &SummariesCumulativeTotal{
|
||||
Decimal: fmt.Sprintf("%.2f", totalHrs),
|
||||
Digital: fmt.Sprintf("%d:%d", int(totalHrs), int(totalMins)),
|
||||
Seconds: totalSecs,
|
||||
Text: utils.FmtWakatimeDuration(totalTime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,8 @@ type Heartbeat struct {
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
||||
UserID string `json:"-" gorm:"not null; index:idx_time_user; index:idx_user_project"` // idx_user_project is for quickly fetching a user's project list (settings page)
|
||||
Entity string `json:"entity" gorm:"not null"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"type" gorm:"size:255"`
|
||||
Category string `json:"category" gorm:"size:255"`
|
||||
Project string `json:"project" gorm:"index:idx_project; index:idx_user_project"`
|
||||
Branch string `json:"branch" gorm:"index:idx_branch"`
|
||||
Language string `json:"language" gorm:"index:idx_language"`
|
||||
@ -23,7 +23,7 @@ type Heartbeat struct {
|
||||
OperatingSystem string `json:"operating_system" gorm:"index:idx_operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||
Machine string `json:"machine" gorm:"index:idx_machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||
UserAgent string `json:"user_agent" hash:"ignore" gorm:"type:varchar(255)"`
|
||||
Time CustomTime `json:"time" gorm:"type:timestamp(3); index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
||||
Time CustomTime `json:"time" gorm:"type:timestamp(3); index:idx_time; index:idx_time_user" swaggertype:"primitive,number"`
|
||||
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
||||
Origin string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||
OriginId string `json:"-" hash:"ignore" gorm:"type:varchar(255)"`
|
||||
|
109
models/leaderboard.go
Normal file
109
models/leaderboard.go
Normal file
@ -0,0 +1,109 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardItem struct {
|
||||
ID uint `json:"-" gorm:"primary_key; size:32"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
UserID string `json:"user_id" gorm:"not null; index:idx_leaderboard_user"`
|
||||
Interval string `json:"interval" gorm:"not null; size:32; index:idx_leaderboard_combined"`
|
||||
By *uint8 `json:"aggregated_by" gorm:"index:idx_leaderboard_combined"` // pointer because nullable
|
||||
Total time.Duration `json:"total" gorm:"not null" swaggertype:"primitive,integer"`
|
||||
Key *string `json:"key" gorm:"size:255"` // pointer because nullable
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
}
|
||||
|
||||
// https://github.com/go-gorm/gorm/issues/5789
|
||||
// https://github.com/go-gorm/gorm/issues/5284#issuecomment-1107775806
|
||||
type LeaderboardItemRanked struct {
|
||||
LeaderboardItem
|
||||
Rank uint
|
||||
}
|
||||
|
||||
func (l1 *LeaderboardItemRanked) Equals(l2 *LeaderboardItemRanked) bool {
|
||||
return l1.ID == l2.ID
|
||||
}
|
||||
|
||||
type Leaderboard []*LeaderboardItemRanked
|
||||
|
||||
func (l *Leaderboard) Add(item *LeaderboardItemRanked) {
|
||||
if _, found := slice.Find[*LeaderboardItemRanked](*l, func(i int, item2 *LeaderboardItemRanked) bool {
|
||||
return item.Equals(item2)
|
||||
}); !found {
|
||||
*l = append(*l, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Leaderboard) AddMany(items []*LeaderboardItemRanked) {
|
||||
for _, item := range items {
|
||||
l.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Leaderboard) UserIDs() []string {
|
||||
return slice.Unique[string](slice.Map[*LeaderboardItemRanked, string](l, func(i int, item *LeaderboardItemRanked) string {
|
||||
return item.UserID
|
||||
}))
|
||||
}
|
||||
|
||||
func (l Leaderboard) HasUser(userId string) bool {
|
||||
return slice.Contain(l.UserIDs(), userId)
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopByKey(by uint8, key string) Leaderboard {
|
||||
return slice.Filter[*LeaderboardItemRanked](l, func(i int, item *LeaderboardItemRanked) bool {
|
||||
return item.By != nil && *item.By == by && item.Key != nil && strings.ToLower(*item.Key) == strings.ToLower(key)
|
||||
})
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopKeys(by uint8) []string {
|
||||
type keyTotal struct {
|
||||
Key string
|
||||
Total time.Duration
|
||||
}
|
||||
|
||||
totalsMapped := make(map[string]*keyTotal, len(l))
|
||||
|
||||
for _, item := range l {
|
||||
if item.Key == nil || item.By == nil || *item.By != by {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(*item.Key)
|
||||
if _, ok := totalsMapped[key]; !ok {
|
||||
totalsMapped[key] = &keyTotal{Key: *item.Key, Total: 0}
|
||||
}
|
||||
totalsMapped[key].Total += item.Total
|
||||
}
|
||||
|
||||
totals := slice.Map[*keyTotal, keyTotal](maputil.Values[string, *keyTotal](totalsMapped), func(i int, item *keyTotal) keyTotal {
|
||||
return *item
|
||||
})
|
||||
if err := slice.SortByField(totals, "Total", "desc"); err != nil {
|
||||
return []string{} // TODO
|
||||
}
|
||||
|
||||
return slice.Map[keyTotal, string](totals, func(i int, item keyTotal) string {
|
||||
return item.Key
|
||||
})
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopKeysByUser(by uint8, userId string) []string {
|
||||
return Leaderboard(slice.Filter[*LeaderboardItemRanked](l, func(i int, item *LeaderboardItemRanked) bool {
|
||||
return item.UserID == userId
|
||||
})).TopKeys(by)
|
||||
}
|
||||
|
||||
func (l Leaderboard) LastUpdate() time.Time {
|
||||
lastUpdate := time.Time{}
|
||||
for _, item := range l {
|
||||
if item.CreatedAt.T().After(lastUpdate) {
|
||||
lastUpdate = item.CreatedAt.T()
|
||||
}
|
||||
}
|
||||
return lastUpdate
|
||||
}
|
@ -34,6 +34,11 @@ type KeyedInterval struct {
|
||||
Key *IntervalKey
|
||||
}
|
||||
|
||||
type PageParams struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// CustomTime is a wrapper type around time.Time, mainly used for the purpose of transparently unmarshalling Python timestamps in the format <sec>.<nsec> (e.g. 1619335137.3324468)
|
||||
type CustomTime time.Time
|
||||
|
||||
@ -99,3 +104,17 @@ func (j CustomTime) T() time.Time {
|
||||
func (j CustomTime) Valid() bool {
|
||||
return j.T().Unix() >= 0
|
||||
}
|
||||
|
||||
func (p *PageParams) Limit() int {
|
||||
if p.PageSize < 0 {
|
||||
return 0
|
||||
}
|
||||
return p.PageSize
|
||||
}
|
||||
|
||||
func (p *PageParams) Offset() int {
|
||||
if p.PageSize <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (p.Page - 1) * p.PageSize
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ const UnknownSummaryKey = "unknown"
|
||||
const DefaultProjectLabel = "default"
|
||||
|
||||
type Summary struct {
|
||||
ID uint `json:"-" gorm:"primary_key"`
|
||||
ID uint `json:"-" gorm:"primary_key; size:32"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
UserID string `json:"user_id" gorm:"not null; index:idx_time_summary_user"`
|
||||
FromTime CustomTime `json:"from" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
@ -42,9 +42,9 @@ type SummaryItems []*SummaryItem
|
||||
type SummaryItem struct {
|
||||
ID uint64 `json:"-" gorm:"primary_key"`
|
||||
Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
SummaryID uint `json:"-"`
|
||||
SummaryID uint `json:"-" gorm:"size:32"`
|
||||
Type uint8 `json:"-" gorm:"index:idx_type"`
|
||||
Key string `json:"key"`
|
||||
Key string `json:"key" gorm:"size:255"`
|
||||
Total time.Duration `json:"total" swaggertype:"primitive,integer"`
|
||||
}
|
||||
|
||||
@ -134,7 +134,9 @@ func (s *Summary) KeepOnly(types map[uint8]bool) *Summary {
|
||||
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.
|
||||
|
||||
If a summary has zero items for a given type, but one or more for any of the other types,
|
||||
the total summary duration can be derived from those and inserted as a dummy-item with key "unknown"
|
||||
for the missing type.
|
||||
|
@ -13,26 +13,27 @@ func init() {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id" gorm:"primary_key"`
|
||||
ApiKey string `json:"api_key" gorm:"unique; default:NULL"`
|
||||
Email string `json:"email" gorm:"index:idx_user_email; size:255"`
|
||||
Location string `json:"location"`
|
||||
Password string `json:"-"`
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
ShareDataMaxDays int `json:"-" gorm:"default:0"`
|
||||
ShareEditors bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLanguages 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"`
|
||||
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLabels bool `json:"-" gorm:"default:false; type:bool"`
|
||||
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
||||
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
||||
WakatimeApiKey string `json:"-"` // for relay middleware and imports
|
||||
WakatimeApiUrl string `json:"-"` // for relay middleware and imports
|
||||
ResetToken string `json:"-"`
|
||||
ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ID string `json:"id" gorm:"primary_key"`
|
||||
ApiKey string `json:"api_key" gorm:"unique; default:NULL"`
|
||||
Email string `json:"email" gorm:"index:idx_user_email; size:255"`
|
||||
Location string `json:"location"`
|
||||
Password string `json:"-"`
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
ShareDataMaxDays int `json:"-" gorm:"default:0"`
|
||||
ShareEditors bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLanguages 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"`
|
||||
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareLabels bool `json:"-" gorm:"default:false; type:bool"`
|
||||
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
||||
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
||||
WakatimeApiKey string `json:"-"` // for relay middleware and imports
|
||||
WakatimeApiUrl string `json:"-"` // for relay middleware and imports
|
||||
ResetToken string `json:"-"`
|
||||
ReportsWeekly bool `json:"-" gorm:"default:false; type:bool"`
|
||||
PublicLeaderboard bool `json:"-" gorm:"default:false; type:bool"`
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
@ -65,9 +66,10 @@ type CredentialsReset struct {
|
||||
}
|
||||
|
||||
type UserDataUpdate struct {
|
||||
Email string `schema:"email"`
|
||||
Location string `schema:"location"`
|
||||
ReportsWeekly bool `schema:"reports_weekly"`
|
||||
Email string `schema:"email"`
|
||||
Location string `schema:"location"`
|
||||
ReportsWeekly bool `schema:"reports_weekly"`
|
||||
PublicLeaderboard bool `schema:"public_leaderboard"`
|
||||
}
|
||||
|
||||
type TimeByUser struct {
|
||||
|
83
models/view/leaderboard.go
Normal file
83
models/view/leaderboard.go
Normal file
@ -0,0 +1,83 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardViewModel struct {
|
||||
User *models.User
|
||||
By string
|
||||
Key string
|
||||
Items []*models.LeaderboardItemRanked
|
||||
TopKeys []string
|
||||
UserLanguages map[string][]string
|
||||
ApiKey string
|
||||
PageParams *models.PageParams
|
||||
Success string
|
||||
Error string
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) WithSuccess(m string) *LeaderboardViewModel {
|
||||
s.Success = m
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) WithError(m string) *LeaderboardViewModel {
|
||||
s.Error = m
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) ColorModifier(item *models.LeaderboardItemRanked, principal *models.User) string {
|
||||
if principal != nil && item.UserID == principal.ID {
|
||||
return "self"
|
||||
}
|
||||
if item.Rank == 1 {
|
||||
return "gold"
|
||||
}
|
||||
if item.Rank == 2 {
|
||||
return "silver"
|
||||
}
|
||||
if item.Rank == 3 {
|
||||
return "bronze"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) LangIcon(lang string) string {
|
||||
// https://icon-sets.iconify.design/mdi/
|
||||
langs := map[string]string{
|
||||
"c++": "language-cpp",
|
||||
"cpp": "language-cpp",
|
||||
"go": "language-go",
|
||||
"haskell": "language-haskell",
|
||||
"html": "language-html5",
|
||||
"java": "language-java",
|
||||
"javascript": "language-javascript",
|
||||
"jsx": "language-javascript",
|
||||
"kotlin": "language-kotlin",
|
||||
"lua": "language-lua",
|
||||
"php": "language-php",
|
||||
"python": "language-python",
|
||||
"r": "language-r",
|
||||
"ruby": "language-ruby",
|
||||
"rust": "language-rust",
|
||||
"swift": "language-swift",
|
||||
"typescript": "language-typescript",
|
||||
"tsx": "language-typescript",
|
||||
"markdown": "language-markdown",
|
||||
"vue": "vuejs",
|
||||
"react": "react",
|
||||
"bash": "bash",
|
||||
"json": "code-json",
|
||||
}
|
||||
if match, ok := langs[strings.ToLower(lang)]; ok {
|
||||
return "mdi:" + match
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) LastUpdate() time.Time {
|
||||
return models.Leaderboard(s.Items).LastUpdate()
|
||||
}
|
@ -9,10 +9,10 @@
|
||||
"compress": "brotli -f static/assets/css/*.dist.css && brotli -f static/assets/js/*.dist.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^1.1.444",
|
||||
"@iconify/json": "^2.1.136",
|
||||
"@iconify/json-tools": "^1.0.10",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"tailwindcss": "2.2.19"
|
||||
"tailwindcss": "^3.1.8"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
108
repositories/leaderboard.go
Normal file
108
repositories/leaderboard.go
Normal file
@ -0,0 +1,108 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type LeaderboardRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLeaderboardRepository(db *gorm.DB) *LeaderboardRepository {
|
||||
return &LeaderboardRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) InsertBatch(items []*models.LeaderboardItem) error {
|
||||
if err := r.db.
|
||||
Clauses(clause.OnConflict{DoNothing: true}).
|
||||
Create(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) CountAllByUser(userId string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.
|
||||
Table("leaderboard_items").
|
||||
Where("user_id = ?", userId).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) CountUsers() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.
|
||||
Table("leaderboard_items").
|
||||
Distinct("user_id").
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItemRanked, error) {
|
||||
// TODO: distinct by (user, key) to filter out potential duplicates ?
|
||||
|
||||
var items []*models.LeaderboardItemRanked
|
||||
subq := r.db.
|
||||
Table("leaderboard_items").
|
||||
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||
Where("\"interval\" in ?", *key)
|
||||
subq = utils.WhereNullable(subq, "\"by\"", by)
|
||||
|
||||
q := r.db.Table("(?) as ranked", subq)
|
||||
q = r.withPaging(q, limit, skip)
|
||||
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) GetAggregatedByUserAndInterval(userId string, key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItemRanked, error) {
|
||||
var items []*models.LeaderboardItemRanked
|
||||
subq := r.db.
|
||||
Table("leaderboard_items").
|
||||
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||
Where("\"interval\" in ?", *key)
|
||||
subq = utils.WhereNullable(subq, "\"by\"", by)
|
||||
|
||||
q := r.db.Table("(?) as ranked", subq).Where("user_id = ?", userId)
|
||||
q = r.withPaging(q, limit, skip)
|
||||
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) DeleteByUser(userId string) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", userId).
|
||||
Delete(models.LeaderboardItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) DeleteByUserAndInterval(userId string, key *models.IntervalKey) error {
|
||||
if err := r.db.
|
||||
Where("user_id = ?", userId).
|
||||
Where("\"interval\" in ?", *key).
|
||||
Delete(models.LeaderboardItem{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LeaderboardRepository) withPaging(q *gorm.DB, limit, skip int) *gorm.DB {
|
||||
if limit > 0 {
|
||||
q = q.Where("\"rank\" <= ?", skip+limit)
|
||||
}
|
||||
if skip > 0 {
|
||||
q = q.Where("\"rank\" > ?", skip)
|
||||
}
|
||||
return q
|
||||
}
|
@ -75,7 +75,9 @@ type IUserRepository interface {
|
||||
GetByEmail(string) (*models.User, error)
|
||||
GetByResetToken(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
GetMany([]string) ([]*models.User, error)
|
||||
GetAllByReports(bool) ([]*models.User, error)
|
||||
GetAllByLeaderboard(bool) ([]*models.User, error)
|
||||
GetByLoggedInAfter(time.Time) ([]*models.User, error)
|
||||
GetByLastActiveAfter(time.Time) ([]*models.User, error)
|
||||
Count() (int64, error)
|
||||
@ -84,3 +86,13 @@ type IUserRepository interface {
|
||||
UpdateField(*models.User, string, interface{}) (*models.User, error)
|
||||
Delete(*models.User) error
|
||||
}
|
||||
|
||||
type ILeaderboardRepository interface {
|
||||
InsertBatch([]*models.LeaderboardItem) error
|
||||
CountAllByUser(string) (int64, error)
|
||||
CountUsers() (int64, error)
|
||||
DeleteByUser(string) error
|
||||
DeleteByUserAndInterval(string, *models.IntervalKey) error
|
||||
GetAllAggregatedByInterval(*models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItemRanked, error)
|
||||
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItemRanked, error)
|
||||
}
|
||||
|
@ -77,6 +77,17 @@ func (r *UserRepository) GetAll() ([]*models.User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetMany(ids []string) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
Table("users").
|
||||
Where("id in ?", ids).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetAllByReports(reportsEnabled bool) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.Where(&models.User{ReportsWeekly: reportsEnabled}).Find(&users).Error; err != nil {
|
||||
@ -85,6 +96,14 @@ func (r *UserRepository) GetAllByReports(reportsEnabled bool) ([]*models.User, e
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetAllByLeaderboard(leaderboardEnabled bool) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.Where(&models.User{PublicLeaderboard: leaderboardEnabled}).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByLoggedInAfter(t time.Time) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
@ -156,6 +175,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
||||
"reset_token": user.ResetToken,
|
||||
"location": user.Location,
|
||||
"reports_weekly": user.ReportsWeekly,
|
||||
"public_leaderboard": user.PublicLeaderboard,
|
||||
}
|
||||
|
||||
result := r.db.Model(user).Updates(updateMap)
|
||||
|
@ -86,7 +86,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(requestedUser.ShareDataMaxDays))
|
||||
minStart := rangeTo.AddDate(0, 0, -requestedUser.ShareDataMaxDays)
|
||||
if (authorizedUser == nil || requestedUser.ID != authorizedUser.ID) &&
|
||||
rangeFrom.Before(minStart) && requestedUser.ShareDataMaxDays >= 0 {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
|
@ -3,6 +3,7 @@ package routes
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/schema"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
@ -66,7 +67,9 @@ func (h *HomeHandler) buildViewModel(r *http.Request) *view.HomeViewModel {
|
||||
}
|
||||
|
||||
if kv, err := h.keyValueSrvc.GetString(conf.KeyNewsbox); err == nil && kv != nil && kv.Value != "" {
|
||||
json.NewDecoder(strings.NewReader(kv.Value)).Decode(&newsbox)
|
||||
if err := json.NewDecoder(strings.NewReader(kv.Value)).Decode(&newsbox); err != nil {
|
||||
logbuch.Error("failed to decode newsbox message - %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &view.HomeViewModel{
|
||||
|
144
routes/leaderboard.go
Normal file
144
routes/leaderboard.go
Normal file
@ -0,0 +1,144 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/models/view"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LeaderboardHandler struct {
|
||||
config *conf.Config
|
||||
userService services.IUserService
|
||||
leaderboardService services.ILeaderboardService
|
||||
}
|
||||
|
||||
var allowedAggregations = map[string]uint8{
|
||||
"language": models.SummaryLanguage,
|
||||
}
|
||||
|
||||
func NewLeaderboardHandler(userService services.IUserService, leaderboardService services.ILeaderboardService) *LeaderboardHandler {
|
||||
return &LeaderboardHandler{
|
||||
config: conf.Get(),
|
||||
userService: userService,
|
||||
leaderboardService: leaderboardService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) RegisterRoutes(router *mux.Router) {
|
||||
r := router.PathPrefix("/leaderboard").Subrouter()
|
||||
r.Use(
|
||||
middlewares.NewAuthenticateMiddleware(h.userService).
|
||||
WithRedirectTarget(defaultErrorRedirectTarget()).
|
||||
WithOptionalFor([]string{"/"}).
|
||||
Handler,
|
||||
)
|
||||
r.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
if err := templates[conf.LeaderboardTemplate].Execute(w, h.buildViewModel(r)); err != nil {
|
||||
logbuch.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardViewModel {
|
||||
user := middlewares.GetPrincipal(r)
|
||||
byParam := strings.ToLower(r.URL.Query().Get("by"))
|
||||
keyParam := strings.ToLower(r.URL.Query().Get("key"))
|
||||
pageParams := utils.ParsePageParamsWithDefault(r, 1, 100)
|
||||
// note: pagination is not fully implemented, yet
|
||||
// count function to get total item / total pages is missing
|
||||
// and according ui (+ optionally search bar) is missing, too
|
||||
|
||||
var err error
|
||||
var leaderboard models.Leaderboard
|
||||
var userLanguages map[string][]string
|
||||
var topKeys []string
|
||||
|
||||
if byParam == "" {
|
||||
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, pageParams, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{Error: criticalError}
|
||||
}
|
||||
|
||||
// regardless of page, always show own rank
|
||||
if user != nil && !leaderboard.HasUser(user.ID) {
|
||||
// but only if leaderboard spans multiple pages
|
||||
if count, err := h.leaderboardService.CountUsers(); err == nil && count > int64(pageParams.PageSize) {
|
||||
if l, err := h.leaderboardService.GetByIntervalAndUser(models.IntervalPast7Days, user.ID, true); err == nil && len(l) > 0 {
|
||||
leaderboard = append(leaderboard, l[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if by, ok := allowedAggregations[byParam]; ok {
|
||||
leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, pageParams, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{Error: criticalError}
|
||||
}
|
||||
|
||||
// regardless of page, always show own rank
|
||||
if user != nil {
|
||||
// but only if leaderboard could, in theory, span multiple pages
|
||||
if count, err := h.leaderboardService.CountUsers(); err == nil && count > int64(pageParams.PageSize) {
|
||||
if l, err := h.leaderboardService.GetAggregatedByIntervalAndUser(models.IntervalPast7Days, user.ID, &by, true); err == nil {
|
||||
leaderboard.AddMany(l)
|
||||
} else {
|
||||
conf.Log().Request(r).Error("error while fetching own aggregated user leaderboard - %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userLeaderboards := slice.GroupWith[*models.LeaderboardItemRanked, string](leaderboard, func(item *models.LeaderboardItemRanked) string {
|
||||
return item.UserID
|
||||
})
|
||||
userLanguages = map[string][]string{}
|
||||
for u, items := range userLeaderboards {
|
||||
userLanguages[u] = models.Leaderboard(items).TopKeysByUser(models.SummaryLanguage, u)
|
||||
}
|
||||
|
||||
topKeys = leaderboard.TopKeys(by)
|
||||
if len(topKeys) > 0 {
|
||||
if keyParam == "" {
|
||||
keyParam = topKeys[0]
|
||||
}
|
||||
keyParam = strings.ToLower(keyParam)
|
||||
leaderboard = leaderboard.TopByKey(by, keyParam)
|
||||
}
|
||||
} else {
|
||||
return &view.LeaderboardViewModel{Error: fmt.Sprintf("unsupported aggregation '%s'", byParam)}
|
||||
}
|
||||
}
|
||||
|
||||
var apiKey string
|
||||
if user != nil {
|
||||
apiKey = user.ApiKey
|
||||
}
|
||||
|
||||
return &view.LeaderboardViewModel{
|
||||
User: user,
|
||||
By: byParam,
|
||||
Key: keyParam,
|
||||
Items: leaderboard,
|
||||
UserLanguages: userLanguages,
|
||||
TopKeys: topKeys,
|
||||
ApiKey: apiKey,
|
||||
PageParams: pageParams,
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
}
|
||||
}
|
@ -35,9 +35,11 @@ func DefaultTemplateFuncs() template.FuncMap {
|
||||
"join": strings.Join,
|
||||
"add": utils.Add,
|
||||
"capitalize": utils.Capitalize,
|
||||
"lower": strings.ToLower,
|
||||
"toRunes": utils.ToRunes,
|
||||
"localTZOffset": utils.LocalTZOffset,
|
||||
"entityTypes": models.SummaryTypes,
|
||||
"strslice": utils.SubSlice[string],
|
||||
"typeName": typeName,
|
||||
"isDev": func() bool {
|
||||
return config.Get().IsDev()
|
||||
@ -54,6 +56,9 @@ func DefaultTemplateFuncs() template.FuncMap {
|
||||
"htmlSafe": func(html string) template.HTML {
|
||||
return template.HTML(html)
|
||||
},
|
||||
"urlSafe": func(s string) template.URL {
|
||||
return template.URL(s)
|
||||
},
|
||||
"avatarUrlTemplate": func() string {
|
||||
return config.Get().App.AvatarURLTemplate
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ package routes
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
datastructure "github.com/duke-git/lancet/v2/datastructure/set"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/schema"
|
||||
@ -145,6 +146,8 @@ func (h *SettingsHandler) dispatchAction(action string) action {
|
||||
return h.actionAddLanguageMapping
|
||||
case "update_sharing":
|
||||
return h.actionUpdateSharing
|
||||
case "update_leaderboard":
|
||||
return h.actionUpdateLeaderboard
|
||||
case "toggle_wakatime":
|
||||
return h.actionSetWakatimeApiKey
|
||||
case "import_wakatime":
|
||||
@ -181,6 +184,7 @@ func (h *SettingsHandler) actionUpdateUser(w http.ResponseWriter, r *http.Reques
|
||||
user.Email = payload.Email
|
||||
user.Location = payload.Location
|
||||
user.ReportsWeekly = payload.ReportsWeekly
|
||||
user.PublicLeaderboard = payload.PublicLeaderboard
|
||||
|
||||
if _, err := h.userSrvc.Update(user); err != nil {
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
@ -250,6 +254,26 @@ func (h *SettingsHandler) actionResetApiKey(w http.ResponseWriter, r *http.Reque
|
||||
return http.StatusOK, msg, ""
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) actionUpdateLeaderboard(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
var err error
|
||||
user := middlewares.GetPrincipal(r)
|
||||
defer h.userSrvc.FlushCache()
|
||||
|
||||
user.PublicLeaderboard, err = strconv.ParseBool(r.PostFormValue("enable_leaderboard"))
|
||||
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, "", "invalid input"
|
||||
}
|
||||
if _, err := h.userSrvc.Update(user); err != nil {
|
||||
return http.StatusInternalServerError, "", "internal sever error"
|
||||
}
|
||||
return http.StatusOK, "settings updated", ""
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) actionUpdateSharing(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
@ -440,7 +464,7 @@ func (h *SettingsHandler) actionSetWakatimeApiKey(w http.ResponseWriter, r *http
|
||||
|
||||
// Healthcheck, if a new API key is set, i.e. the feature is activated
|
||||
if (user.WakatimeApiKey == "" && apiKey != "") && !h.validateWakatimeKey(apiKey, apiUrl) {
|
||||
return http.StatusBadRequest, "", "failed to connect to WakaTime, API key invalid?"
|
||||
return http.StatusBadRequest, "", "failed to connect to WakaTime, API key or endpoint URL invalid?"
|
||||
}
|
||||
|
||||
if _, err := h.userSrvc.SetWakatimeApiCredentials(user, apiKey, apiUrl); err != nil {
|
||||
@ -637,7 +661,7 @@ func (h *SettingsHandler) regenerateSummaries(user *models.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.aggregationSrvc.Run(map[string]bool{user.ID: true}); err != nil {
|
||||
if err := h.aggregationSrvc.Run(datastructure.NewSet(user.ID)); err != nil {
|
||||
logbuch.Error("failed to regenerate summaries: %v", err)
|
||||
return err
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -43,7 +42,7 @@ func GetBadgeParams(r *http.Request, requestedUser *models.User) (*models.KeyedI
|
||||
Key: intervalKey,
|
||||
}
|
||||
|
||||
minStart := rangeTo.Add(-24 * time.Hour * time.Duration(requestedUser.ShareDataMaxDays))
|
||||
minStart := rangeTo.AddDate(0, 0, -requestedUser.ShareDataMaxDays)
|
||||
// negative value means no limit
|
||||
if rangeFrom.Before(minStart) && requestedUser.ShareDataMaxDays >= 0 {
|
||||
return nil, nil, errors.New("requested time range too broad")
|
||||
|
@ -10,6 +10,7 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { Collection } = require('@iconify/json-tools')
|
||||
const { locate } = require("@iconify/json");
|
||||
|
||||
let icons = [
|
||||
'fxemoji:key',
|
||||
@ -53,7 +54,29 @@ let icons = [
|
||||
'ion:rocket',
|
||||
'heroicons-solid:server',
|
||||
'eva:checkmark-circle-2-fill',
|
||||
'fluent:key-24-filled'
|
||||
'fluent:key-24-filled',
|
||||
'mdi:language-c',
|
||||
'mdi:language-cpp',
|
||||
'mdi:language-go',
|
||||
'mdi:language-haskell',
|
||||
'mdi:language-html5',
|
||||
'mdi:language-java',
|
||||
'mdi:language-javascript',
|
||||
'mdi:language-kotlin',
|
||||
'mdi:language-lua',
|
||||
'mdi:language-php',
|
||||
'mdi:language-python',
|
||||
'mdi:language-r',
|
||||
'mdi:language-ruby',
|
||||
'mdi:language-rust',
|
||||
'mdi:language-swift',
|
||||
'mdi:language-typescript',
|
||||
'mdi:language-markdown',
|
||||
'mdi:vuejs',
|
||||
'mdi:react',
|
||||
'mdi:code-json',
|
||||
'mdi:bash',
|
||||
'twemoji:frowning-face',
|
||||
]
|
||||
|
||||
const output = path.normalize(path.join(__dirname, '../static/assets/js/icons.dist.js'))
|
||||
@ -85,7 +108,7 @@ icons.forEach(icon => {
|
||||
let code = ''
|
||||
Object.keys(filtered).forEach(prefix => {
|
||||
let collection = new Collection()
|
||||
if (!collection.loadIconifyCollection(prefix)) {
|
||||
if (!collection.loadFromFile(locate(prefix))) {
|
||||
console.error('Error loading collection', prefix)
|
||||
return
|
||||
}
|
||||
|
137
scripts/get.sh
137
scripts/get.sh
@ -15,54 +15,163 @@
|
||||
set -e -u
|
||||
|
||||
githubLatestTag() {
|
||||
finalUrl=$(curl "https://github.com/$1/releases/latest" -s -L -I -o /dev/null -w '%{url_effective}')
|
||||
printf "%s\n" "${finalUrl##*/}"
|
||||
latestJSON="$( eval "$http 'https://api.github.com/repos/$1/releases/latest'" 2>/dev/null )" || true
|
||||
|
||||
versionNumber=''
|
||||
if ! echo "$latestJSON" | grep 'API rate limit exceeded' >/dev/null 2>&1 ; then
|
||||
if ! versionNumber="$( echo "$latestJSON" | grep -oEm1 '[0-9]+[.][0-9]+[.][0-9]+' - 2>/dev/null )" ; then
|
||||
versionNumber=''
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${versionNumber:-x}" = "x" ] ; then
|
||||
# Try to fallback to previous latest version detection method if curl is available
|
||||
if command -v curl >/dev/null 2>&1 ; then
|
||||
if finalUrl="$( curl "https://github.com/$1/releases/latest" -s -L -I -o /dev/null -w '%{url_effective}' 2>/dev/null )" ; then
|
||||
trimmedVers="${finalUrl##*v}"
|
||||
if [ "${trimmedVers:-x}" != "x" ] ; then
|
||||
echo "$trimmedVers"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
cat 1>&2 << 'EOA'
|
||||
/=====================================\\
|
||||
| FAILED TO HTTP DOWNLOAD FILE |
|
||||
\\=====================================/
|
||||
|
||||
Uh oh! We couldn't download needed internet resources for you. Perhaps you are
|
||||
offline, your DNS servers are not set up properly, your internet plan doesn't
|
||||
include GitHub, or the GitHub servers are down?
|
||||
|
||||
EOA
|
||||
exit 1
|
||||
else
|
||||
echo "$versionNumber"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "${GETMICRO_HTTP:-x}" != "x" ]; then
|
||||
http="$GETMICRO_HTTP"
|
||||
elif command -v curl >/dev/null 2>&1 ; then
|
||||
http="curl -L"
|
||||
elif command -v wget >/dev/null 2>&1 ; then
|
||||
http="wget -O-"
|
||||
else
|
||||
cat 1>&2 << 'EOA'
|
||||
/=====================================\\
|
||||
| COULD NOT FIND HTTP PROGRAM |
|
||||
\\=====================================/
|
||||
|
||||
Uh oh! We couldn't find either curl or wget installed on your system.
|
||||
|
||||
To continue with installation, you have two options:
|
||||
|
||||
A. Install either wget or curl on your system. You may need to run `hash -r`.
|
||||
|
||||
B. Define GETMICRO_HTTP to be a command (with arguments deliminated by spaces)
|
||||
that both follows HTTP redirects AND prints the fetched content to stdout.
|
||||
|
||||
For examples of option B, this script uses the below values for wget and curl:
|
||||
|
||||
$ curl https://wakapi.dev/get | GETMICRO_HTTP="curl -L" sh
|
||||
|
||||
$ wget -O- https://wakapi.dev/get | GETMICRO_HTTP="wget -O-" sh
|
||||
|
||||
EOA
|
||||
exit 1
|
||||
fi
|
||||
|
||||
platform=''
|
||||
machine=$(uname -m) # currently, Wakapi builds are only available for AMD64 anyway
|
||||
machine=$(uname -m)
|
||||
|
||||
if [ "${GETWAKAPI_PLATFORM:-x}" != "x" ]; then
|
||||
platform="$GETWAKAPI_PLATFORM"
|
||||
else
|
||||
case "$(uname -s | tr '[:upper:]' '[:lower:]')" in
|
||||
"linux") platform='linux_amd64' ;;
|
||||
"msys"*|"cygwin"*|"mingw"*|*"_nt"*|"win"*) platform='win_amd64' ;;
|
||||
"linux")
|
||||
case "$machine" in
|
||||
"arm64"* | "aarch64"* ) platform='linux_arm64' ;;
|
||||
*"64") platform='linux_amd64' ;;
|
||||
esac
|
||||
;;
|
||||
"darwin")
|
||||
case "$machine" in
|
||||
"arm64"* | "aarch64"* ) platform='darwin_arm64' ;;
|
||||
*"64") platform='darwin_amd64' ;;
|
||||
esac;;
|
||||
"msys"*|"cygwin"*|"mingw"*|*"_nt"*|"win"*)
|
||||
case "$machine" in
|
||||
"arm64"* | "aarch64"* ) platform='win_arm64' ;;
|
||||
*"64") platform='win_amd64' ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "x$platform" = "x" ]; then
|
||||
cat << 'EOM'
|
||||
if [ "${platform:-x}" = "x" ]; then
|
||||
cat 1>&2 << 'EOM'
|
||||
/=====================================\\
|
||||
| COULD NOT DETECT PLATFORM |
|
||||
\\=====================================/
|
||||
|
||||
Uh oh! We couldn't automatically detect your operating system. You can file a
|
||||
bug here: https://github.com/muety/wakapi
|
||||
|
||||
To continue with installation, please choose from one of the following values:
|
||||
|
||||
- win_amd64
|
||||
- darwin_amd64
|
||||
- linux_amd64
|
||||
|
||||
Export your selection as the GETWAKAPI_PLATFORM environment variable, and then
|
||||
re-run this script.
|
||||
|
||||
For example:
|
||||
|
||||
$ curl https://getmic.ro | GETWAKAPI_PLATFORM=linux_amd64 sh
|
||||
|
||||
EOM
|
||||
exit 1
|
||||
else
|
||||
printf "Detected platform: %s\n" "$platform"
|
||||
echo "Detected platform: $platform"
|
||||
fi
|
||||
|
||||
TAG=$(githubLatestTag muety/wakapi)
|
||||
|
||||
printf "Tag: %s" "$TAG"
|
||||
if command -v grep >/dev/null 2>&1 ; then
|
||||
if ! echo "v$TAG" | grep -E '^v[0-9]+[.][0-9]+[.][0-9]+$' >/dev/null 2>&1 ; then
|
||||
cat 1>&2 << 'EOM'
|
||||
/=====================================\\
|
||||
| INVALID TAG RECIEVED |
|
||||
\\=====================================/
|
||||
|
||||
Uh oh! We recieved an invalid tag and cannot be sure that the tag will not break
|
||||
this script.
|
||||
|
||||
Please open an issue on GitHub at https://github.com/muety/wakapi with
|
||||
the invalid tag included:
|
||||
|
||||
EOM
|
||||
echo "> $TAG" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
extension='zip'
|
||||
|
||||
printf "Latest Version: %s\n" "$TAG"
|
||||
printf "Downloading https://github.com/muety/wakapi/releases/download/%s/wakapi_%s.%s\n" "$TAG" "$platform" "$extension"
|
||||
echo "Latest Version: $TAG"
|
||||
echo "Downloading https://github.com/muety/wakapi/releases/download/$TAG/wakapi_$platform.$extension"
|
||||
|
||||
curl -L "https://github.com/muety/wakapi/releases/download/$TAG/wakapi_$platform.$extension" > "wakapi.$extension"
|
||||
eval "$http 'https://github.com/muety/wakapi/releases/download/$TAG/wakapi_$platform.$extension'" > "wakapi.$extension"
|
||||
|
||||
case "$extension" in
|
||||
"zip") unzip -j "wakapi.$extension" -d "wakapi-$TAG" ;;
|
||||
"tar.gz") tar -xvzf "wakapi.$extension" "wakapi-$TAG/wakapi" ;;
|
||||
esac
|
||||
|
||||
mv "wakapi-$TAG/wakapi" ./wakapi
|
||||
mv "wakapi-$TAG/config.yml" ./config.yml
|
||||
mv wakapi-$TAG/* .
|
||||
|
||||
rm "wakapi.$extension"
|
||||
rm -rf "wakapi-$TAG"
|
||||
|
@ -57,17 +57,19 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time,
|
||||
endDate = maxTo
|
||||
}
|
||||
|
||||
userAgents, err := w.fetchUserAgents(baseUrl)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to fetch user agents while importing wakatime heartbeats for user '%s' - %v", user.ID, err)
|
||||
return
|
||||
}
|
||||
userAgents := map[string]*wakatime.UserAgentEntry{}
|
||||
//userAgents, err := w.fetchUserAgents(baseUrl)
|
||||
// if err != nil {
|
||||
// config.Log().Error("failed to fetch user agents while importing wakatime heartbeats for user '%s' - %v", user.ID, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
machinesNames, err := w.fetchMachineNames(baseUrl)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to fetch machine names while importing wakatime heartbeats for user '%s' - %v", user.ID, err)
|
||||
return
|
||||
}
|
||||
machinesNames := map[string]*wakatime.MachineEntry{}
|
||||
// machinesNames, err := w.fetchMachineNames(baseUrl)
|
||||
// if err != nil {
|
||||
// config.Log().Error("failed to fetch machine names while importing wakatime heartbeats for user '%s' - %v", user.ID, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
days := generateDays(startDate, endDate)
|
||||
|
||||
@ -259,9 +261,17 @@ func mapHeartbeat(
|
||||
) *models.Heartbeat {
|
||||
ua := userAgents[entry.UserAgentId]
|
||||
if ua == nil {
|
||||
ua = &wakatime.UserAgentEntry{
|
||||
Editor: "unknown",
|
||||
Os: "unknown",
|
||||
// try to parse id as an actual user agent string (as returned by wakapi)
|
||||
if opSys, editor, err := utils.ParseUserAgent(entry.UserAgentId); err == nil {
|
||||
ua = &wakatime.UserAgentEntry{
|
||||
Editor: opSys,
|
||||
Os: editor,
|
||||
}
|
||||
} else {
|
||||
ua = &wakatime.UserAgentEntry{
|
||||
Editor: "unknown",
|
||||
Os: "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
264
services/leaderboard.go
Normal file
264
services/leaderboard.go
Normal file
@ -0,0 +1,264 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/leandro-lugaresi/hub"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/repositories"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardService struct {
|
||||
config *config.Config
|
||||
cache *cache.Cache
|
||||
eventBus *hub.Hub
|
||||
repository repositories.ILeaderboardRepository
|
||||
summaryService ISummaryService
|
||||
userService IUserService
|
||||
}
|
||||
|
||||
func NewLeaderboardService(leaderboardRepo repositories.ILeaderboardRepository, summaryService ISummaryService, userService IUserService) *LeaderboardService {
|
||||
srv := &LeaderboardService{
|
||||
config: config.Get(),
|
||||
cache: cache.New(6*time.Hour, 6*time.Hour),
|
||||
eventBus: config.EventBus(),
|
||||
repository: leaderboardRepo,
|
||||
summaryService: summaryService,
|
||||
userService: userService,
|
||||
}
|
||||
|
||||
onUserUpdate := srv.eventBus.Subscribe(0, config.EventUserUpdate)
|
||||
go func(sub *hub.Subscription) {
|
||||
for m := range sub.Receiver {
|
||||
|
||||
// generate leaderboard for updated user, if leaderboard enabled and none present, yet
|
||||
user := m.Fields[config.FieldPayload].(*models.User)
|
||||
|
||||
exists, err := srv.ExistsAnyByUser(user.ID)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to check existing leaderboards upon user update - %v", err)
|
||||
}
|
||||
|
||||
if user.PublicLeaderboard && !exists {
|
||||
logbuch.Info("generating leaderboard for '%s' after settings update", user.ID)
|
||||
srv.Run([]*models.User{user}, models.IntervalPast7Days, []uint8{models.SummaryLanguage})
|
||||
} else if !user.PublicLeaderboard && exists {
|
||||
logbuch.Info("clearing leaderboard for '%s' after settings update", user.ID)
|
||||
if err := srv.repository.DeleteByUser(user.ID); err != nil {
|
||||
config.Log().Error("failed to clear leaderboard for user '%s' - %v", user.ID, err)
|
||||
}
|
||||
srv.cache.Flush()
|
||||
}
|
||||
}
|
||||
}(&onUserUpdate)
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) ScheduleDefault() {
|
||||
runAllUsers := func(interval *models.IntervalKey, by []uint8) {
|
||||
users, err := srv.userService.GetAllByLeaderboard(true)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to get users for leaderboard generation - %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
srv.Run(users, interval, by)
|
||||
}
|
||||
|
||||
s := gocron.NewScheduler(time.Local)
|
||||
s.Every(1).Day().At(srv.config.App.LeaderboardGenerationTime).Do(runAllUsers, models.IntervalPast7Days, []uint8{models.SummaryLanguage})
|
||||
s.StartBlocking()
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) Run(users []*models.User, interval *models.IntervalKey, by []uint8) error {
|
||||
logbuch.Info("generating leaderboard (%s) for %d users (%d aggregations)", (*interval)[0], len(users), len(by))
|
||||
|
||||
for _, user := range users {
|
||||
if err := srv.repository.DeleteByUserAndInterval(user.ID, interval); err != nil {
|
||||
config.Log().Error("failed to delete leaderboard items for user %s (interval %s) - %v", user.ID, (*interval)[0], err)
|
||||
continue
|
||||
}
|
||||
|
||||
item, err := srv.GenerateByUser(user, interval)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to generate general leaderboard for user %s - %v", user.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := srv.repository.InsertBatch([]*models.LeaderboardItem{item}); err != nil {
|
||||
config.Log().Error("failed to persist general leaderboard for user %s - %v", user.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, by := range by {
|
||||
items, err := srv.GenerateAggregatedByUser(user, interval, by)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to generate aggregated (by %s) leaderboard for user %s - %v", models.GetEntityColumn(by), user.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := srv.repository.InsertBatch(items); err != nil {
|
||||
config.Log().Error("failed to persist aggregated (by %s) leaderboard for user %s - %v", models.GetEntityColumn(by), user.ID, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv.cache.Flush()
|
||||
logbuch.Info("finished leaderboard generation")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) ExistsAnyByUser(userId string) (bool, error) {
|
||||
count, err := srv.repository.CountAllByUser(userId)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) CountUsers() (int64, error) {
|
||||
// check cache
|
||||
cacheKey := "count_total"
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||
return cacheResult.(int64), nil
|
||||
}
|
||||
|
||||
count, err := srv.repository.CountUsers()
|
||||
if err != nil {
|
||||
srv.cache.SetDefault(cacheKey, count)
|
||||
}
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) {
|
||||
return srv.GetAggregatedByInterval(interval, nil, pageParams, resolveUsers)
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetByIntervalAndUser(interval *models.IntervalKey, userId string, resolveUser bool) (models.Leaderboard, error) {
|
||||
return srv.GetAggregatedByIntervalAndUser(interval, userId, nil, resolveUser)
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) {
|
||||
// check cache
|
||||
cacheKey := srv.getHash(interval, by, "", pageParams)
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||
return cacheResult.([]*models.LeaderboardItemRanked), nil
|
||||
}
|
||||
|
||||
items, err := srv.repository.GetAllAggregatedByInterval(interval, by, pageParams.Limit(), pageParams.Offset())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resolveUsers {
|
||||
users, err := srv.userService.GetManyMapped(models.Leaderboard(items).UserIDs())
|
||||
if err != nil {
|
||||
config.Log().Error("failed to resolve users for leaderboard item - %v", err)
|
||||
} else {
|
||||
for _, item := range items {
|
||||
if u, ok := users[item.UserID]; ok {
|
||||
item.User = u
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv.cache.SetDefault(cacheKey, items)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetAggregatedByIntervalAndUser(interval *models.IntervalKey, userId string, by *uint8, resolveUser bool) (models.Leaderboard, error) {
|
||||
// check cache
|
||||
cacheKey := srv.getHash(interval, by, userId, nil)
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||
return cacheResult.([]*models.LeaderboardItemRanked), nil
|
||||
}
|
||||
|
||||
items, err := srv.repository.GetAggregatedByUserAndInterval(userId, interval, by, 0, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resolveUser {
|
||||
u, err := srv.userService.GetUserById(userId)
|
||||
if err != nil {
|
||||
config.Log().Error("failed to resolve user for leaderboard item - %v", err)
|
||||
} else {
|
||||
for _, item := range items {
|
||||
item.User = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv.cache.SetDefault(cacheKey, items)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GenerateByUser(user *models.User, interval *models.IntervalKey) (*models.LeaderboardItem, error) {
|
||||
err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summary, err := srv.summaryService.Aliased(from, to, user, srv.summaryService.Retrieve, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.LeaderboardItem{
|
||||
User: user,
|
||||
UserID: user.ID,
|
||||
Interval: (*interval)[0],
|
||||
Total: summary.TotalTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GenerateAggregatedByUser(user *models.User, interval *models.IntervalKey, by uint8) ([]*models.LeaderboardItem, error) {
|
||||
err, from, to := utils.ResolveIntervalTZ(interval, user.TZ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summary, err := srv.summaryService.Aliased(from, to, user, srv.summaryService.Retrieve, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summaryItems := *summary.ItemsByType(by)
|
||||
items := make([]*models.LeaderboardItem, summaryItems.Len())
|
||||
|
||||
for i := 0; i < summaryItems.Len(); i++ {
|
||||
key := summaryItems[i].Key
|
||||
items[i] = &models.LeaderboardItem{
|
||||
User: user,
|
||||
UserID: user.ID,
|
||||
Interval: (*interval)[0],
|
||||
By: &by,
|
||||
Total: summary.TotalTimeByKey(by, key),
|
||||
Key: &key,
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) getHash(interval *models.IntervalKey, by *uint8, user string, pageParams *models.PageParams) string {
|
||||
k := strings.Join(*interval, "__") + "__" + user
|
||||
if by != nil && !reflect.ValueOf(by).IsNil() {
|
||||
k += "__" + models.GetEntityColumn(*by)
|
||||
}
|
||||
if pageParams != nil {
|
||||
k += "__" + strconv.Itoa(pageParams.Page) + "__" + strconv.Itoa(pageParams.PageSize)
|
||||
}
|
||||
return k
|
||||
}
|
@ -97,13 +97,29 @@ type IReportService interface {
|
||||
Run(*models.User, time.Duration) error
|
||||
}
|
||||
|
||||
type ILeaderboardService interface {
|
||||
ScheduleDefault()
|
||||
Run([]*models.User, *models.IntervalKey, []uint8) error
|
||||
ExistsAnyByUser(string) (bool, error)
|
||||
CountUsers() (int64, error)
|
||||
GetByInterval(*models.IntervalKey, *models.PageParams, bool) (models.Leaderboard, error)
|
||||
GetByIntervalAndUser(*models.IntervalKey, string, bool) (models.Leaderboard, error)
|
||||
GetAggregatedByInterval(*models.IntervalKey, *uint8, *models.PageParams, bool) (models.Leaderboard, error)
|
||||
GetAggregatedByIntervalAndUser(*models.IntervalKey, string, *uint8, bool) (models.Leaderboard, error)
|
||||
GenerateByUser(*models.User, *models.IntervalKey) (*models.LeaderboardItem, error)
|
||||
GenerateAggregatedByUser(*models.User, *models.IntervalKey, uint8) ([]*models.LeaderboardItem, error)
|
||||
}
|
||||
|
||||
type IUserService interface {
|
||||
GetUserById(string) (*models.User, error)
|
||||
GetUserByKey(string) (*models.User, error)
|
||||
GetUserByEmail(string) (*models.User, error)
|
||||
GetUserByResetToken(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
GetMany([]string) ([]*models.User, error)
|
||||
GetManyMapped([]string) (map[string]*models.User, error)
|
||||
GetAllByReports(bool) ([]*models.User, error)
|
||||
GetAllByLeaderboard(bool) ([]*models.User, error)
|
||||
GetActive(bool) ([]*models.User, error)
|
||||
Count() (int64, error)
|
||||
CreateOrGet(*models.Signup, bool) (*models.User, bool, error)
|
||||
|
@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/datetime"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/leandro-lugaresi/hub"
|
||||
@ -96,10 +97,28 @@ func (srv *UserService) GetAll() ([]*models.User, error) {
|
||||
return srv.repository.GetAll()
|
||||
}
|
||||
|
||||
func (srv *UserService) GetMany(ids []string) ([]*models.User, error) {
|
||||
return srv.repository.GetMany(ids)
|
||||
}
|
||||
|
||||
func (srv *UserService) GetManyMapped(ids []string) (map[string]*models.User, error) {
|
||||
users, err := srv.repository.GetMany(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertor.ToMap[*models.User, string, *models.User](users, func(u *models.User) (string, *models.User) {
|
||||
return u.ID, u
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (srv *UserService) GetAllByReports(reportsEnabled bool) ([]*models.User, error) {
|
||||
return srv.repository.GetAllByReports(reportsEnabled)
|
||||
}
|
||||
|
||||
func (srv *UserService) GetAllByLeaderboard(leaderboardEnabled bool) ([]*models.User, error) {
|
||||
return srv.repository.GetAllByLeaderboard(leaderboardEnabled)
|
||||
}
|
||||
|
||||
func (srv *UserService) GetActive(exact bool) ([]*models.User, error) {
|
||||
minDate := time.Now().AddDate(0, 0, -1*srv.config.App.InactiveDays)
|
||||
if !exact {
|
||||
|
@ -69,6 +69,10 @@ body {
|
||||
@apply py-2 px-4 font-semibold rounded bg-red-600 hover:bg-red-700 text-white text-sm;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
@apply py-1 px-2;
|
||||
}
|
||||
|
||||
.input-default {
|
||||
@apply appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded w-full py-2 px-4;
|
||||
}
|
||||
@ -109,7 +113,37 @@ body {
|
||||
@apply border-red-700;
|
||||
}
|
||||
|
||||
.leaderboard-default {
|
||||
@apply border-gray-700;
|
||||
}
|
||||
|
||||
.leaderboard-self {
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
padding-left: calc(1rem + 10px);
|
||||
padding-right: calc(1rem + 10px);
|
||||
@apply border-green-700 bg-gray-800;
|
||||
}
|
||||
|
||||
.leaderboard-gold {
|
||||
border-color: #ffd700;
|
||||
}
|
||||
|
||||
.leaderboard-silver {
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
|
||||
.leaderboard-bronze {
|
||||
border-color: #cd7f32;
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.max-available {
|
||||
max-width: -moz-available;
|
||||
max-width: -webkit-fill-available;
|
||||
max-width: fill-available;
|
||||
}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1701,7 +1701,7 @@ const docTemplate = `{
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "",
|
||||
BasePath: "/api",
|
||||
BasePath: "",
|
||||
Schemes: []string{},
|
||||
Title: "Wakapi API",
|
||||
Description: "REST API to interact with [Wakapi](https://wakapi.dev)\n\n## Authentication\nSet header `Authorization` to your API Key encoded as Base64 and prefixed with `Basic`\n**Example:** `Basic ODY2NDhkNzQtMTljNS00NTJiLWJhMDEtZmIzZWM3MGQ0YzJmCg==`",
|
||||
|
@ -14,7 +14,6 @@
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"basePath": "/api",
|
||||
"paths": {
|
||||
"/compat/shields/v1/{user}/{interval}/{filter}": {
|
||||
"get": {
|
||||
|
@ -1,4 +1,3 @@
|
||||
basePath: /api
|
||||
definitions:
|
||||
models.Diagnostics:
|
||||
properties:
|
||||
|
@ -1,62 +0,0 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css" >
|
||||
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@3/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@3/favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "../docs/swagger.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
})
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,12 +1,24 @@
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
purge: {
|
||||
enabled: true,
|
||||
mode: 'all',
|
||||
content: ['./views/*.tpl.html'],
|
||||
safelist: [
|
||||
'newsbox-default',
|
||||
'newsbox-warning',
|
||||
'newsbox-danger',
|
||||
]
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
green: colors.emerald,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
content: [
|
||||
'./views/*.tpl.html',
|
||||
],
|
||||
safelist: [
|
||||
'newsbox-default',
|
||||
'newsbox-warning',
|
||||
'newsbox-danger',
|
||||
'leaderboard-self',
|
||||
'leaderboard-default',
|
||||
'leaderboard-gold',
|
||||
'leaderboard-silver',
|
||||
'leaderboard-bronze',
|
||||
]
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
BEGIN TRANSACTION;
|
||||
INSERT INTO "key_string_values" VALUES ('20210213-add_has_data_field','done');
|
||||
INSERT INTO "key_string_values" VALUES ('20210221-add_created_date_column','done');
|
||||
INSERT INTO "key_string_values" VALUES ('imprint','no content here');
|
||||
INSERT INTO "key_string_values" VALUES ('20210411-add_imprint_content','done');
|
||||
INSERT INTO "key_string_values" VALUES ('20210806-remove_persisted_project_labels','done');
|
||||
INSERT INTO "key_string_values" VALUES ('20211215-migrate_id_to_bigint-add_has_data_field','done');
|
||||
INSERT INTO "key_string_values" VALUES ('latest_total_time','0s');
|
||||
INSERT INTO "key_string_values" VALUES ('latest_total_users','0');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20210213-add_has_data_field','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20210221-add_created_date_column','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('imprint','no content here');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20210411-add_imprint_content','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20210806-remove_persisted_project_labels','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20211215-migrate_id_to_bigint-add_has_data_field','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20212212-total_summary_heartbeats','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20220317-align_num_heartbeats','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('20220318-mysql_timestamp_precision','done');
|
||||
INSERT INTO "key_string_values" ("key","value") VALUES ('202203191-drop_diagnostics_user','done');
|
||||
COMMIT;
|
||||
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
INSERT INTO "users" ("id", "api_key", "email", "location", "password", "created_at", "last_logged_in_at",
|
||||
"share_data_max_days", "share_editors", "share_languages", "share_projects", "share_oss",
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Compiling."
|
||||
go build
|
||||
CGO_ENABLED=0 go build
|
||||
|
||||
if ! command -v newman &> /dev/null
|
||||
then
|
||||
|
@ -7,29 +7,31 @@ BEGIN TRANSACTION;
|
||||
DROP TABLE IF EXISTS "aliases";
|
||||
CREATE TABLE `aliases` (`id` integer,`type` integer NOT NULL,`user_id` text NOT NULL,`key` text NOT NULL,`value` text NOT NULL,PRIMARY KEY (`id`),CONSTRAINT `fk_aliases_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "diagnostics";
|
||||
CREATE TABLE `diagnostics` (`id` integer,`user_id` text NOT NULL,`platform` text,`architecture` text,`plugin` text,`cli_version` text,`logs` text,`stack_trace` text,PRIMARY KEY (`id`),CONSTRAINT `fk_diagnostics_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
CREATE TABLE `diagnostics` (`id` integer,`platform` text,`architecture` text,`plugin` text,`cli_version` text,`logs` text,`stack_trace` text,PRIMARY KEY (`id`));
|
||||
DROP TABLE IF EXISTS "heartbeats";
|
||||
CREATE TABLE `heartbeats` (`id` integer,`user_id` text NOT NULL,`entity` text NOT NULL,`type` text,`category` text,`project` text,`branch` text,`language` text,`is_write` numeric,`editor` text,`operating_system` text,`machine` text,`user_agent` text,`time` timestamp,`hash` varchar(17),`origin` text,`origin_id` text,`created_at` timestamp,PRIMARY KEY (`id`),CONSTRAINT `fk_heartbeats_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
CREATE TABLE `heartbeats` (`id` integer,`user_id` text NOT NULL,`entity` text NOT NULL,`type` text,`category` text,`project` text,`branch` text,`language` text,`is_write` numeric,`editor` text,`operating_system` text,`machine` text,`user_agent` varchar(255),`time` timestamp(3),`hash` varchar(17),`origin` varchar(255),`origin_id` varchar(255),`created_at` timestamp(3),PRIMARY KEY (`id`),CONSTRAINT `fk_heartbeats_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "key_string_values";
|
||||
CREATE TABLE `key_string_values` (`key` text,`value` text,PRIMARY KEY (`key`));
|
||||
DROP TABLE IF EXISTS "language_mappings";
|
||||
CREATE TABLE `language_mappings` (`id` integer,`user_id` text NOT NULL,`extension` varchar(16),`language` varchar(64),PRIMARY KEY (`id`),CONSTRAINT `fk_language_mappings_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "leaderboard_items";
|
||||
CREATE TABLE `leaderboard_items` (`id` integer,`user_id` text NOT NULL,`rank` integer,`interval` text NOT NULL,`by` integer,`total` integer NOT NULL,`key` text,`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`),CONSTRAINT `fk_leaderboard_items_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "project_labels";
|
||||
CREATE TABLE `project_labels` (`id` integer,`user_id` text NOT NULL,`project_key` text,`label` varchar(64),PRIMARY KEY (`id`),CONSTRAINT `fk_project_labels_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "summaries";
|
||||
CREATE TABLE "summaries" (`id` integer,`user_id` text NOT NULL,`from_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`to_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`),CONSTRAINT `fk_summaries_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
CREATE TABLE "summaries" (`id` integer,`user_id` text NOT NULL,`from_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`to_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,`num_heartbeats` integer DEFAULT 0,PRIMARY KEY (`id`),CONSTRAINT `fk_summaries_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "summary_items";
|
||||
CREATE TABLE `summary_items` (`id` integer,`summary_id` integer,`type` integer,`key` text,`total` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_summaries_editors` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_operating_systems` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_machines` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_projects` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_languages` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
CREATE TABLE `summary_items` (`id` integer,`summary_id` integer,`type` integer,`key` text,`total` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_summaries_machines` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_projects` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_languages` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_editors` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `fk_summaries_operating_systems` FOREIGN KEY (`summary_id`) REFERENCES `summaries`(`id`) ON DELETE CASCADE ON UPDATE CASCADE);
|
||||
DROP TABLE IF EXISTS "users";
|
||||
CREATE TABLE `users` (`id` text,`api_key` text UNIQUE,`email` text,`location` text,`password` text,`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,`last_logged_in_at` timestamp DEFAULT CURRENT_TIMESTAMP,`share_data_max_days` integer DEFAULT 0,`share_editors` numeric DEFAULT false,`share_languages` numeric DEFAULT false,`share_projects` numeric DEFAULT false,`share_oss` numeric DEFAULT false,`share_machines` numeric DEFAULT false,`share_labels` numeric DEFAULT false,`is_admin` numeric DEFAULT false,`has_data` numeric DEFAULT false,`wakatime_api_key` text,`reset_token` text,`reports_weekly` numeric DEFAULT false,PRIMARY KEY (`id`));
|
||||
CREATE TABLE "users" (`id` text,`api_key` text UNIQUE DEFAULT NULL,`email` text,`location` text,`password` text,`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,`last_logged_in_at` timestamp DEFAULT CURRENT_TIMESTAMP,`share_data_max_days` integer DEFAULT 0,`share_editors` numeric DEFAULT false,`share_languages` numeric DEFAULT false,`share_projects` numeric DEFAULT false,`share_oss` numeric DEFAULT false,`share_machines` numeric DEFAULT false,`share_labels` numeric DEFAULT false,`is_admin` numeric DEFAULT false,`has_data` numeric DEFAULT false,`wakatime_api_key` text,`wakatime_api_url` text,`reset_token` text,`reports_weekly` numeric DEFAULT false,`public_leaderboard` numeric DEFAULT false,PRIMARY KEY (`id`));
|
||||
DROP INDEX IF EXISTS "idx_alias_type_key";
|
||||
CREATE INDEX `idx_alias_type_key` ON `aliases`(`type`,`key`);
|
||||
DROP INDEX IF EXISTS "idx_alias_user";
|
||||
CREATE INDEX `idx_alias_user` ON `aliases`(`user_id`);
|
||||
DROP INDEX IF EXISTS "idx_diagnostics_user";
|
||||
CREATE INDEX `idx_diagnostics_user` ON `diagnostics`(`user_id`);
|
||||
DROP INDEX IF EXISTS "idx_entity";
|
||||
CREATE INDEX `idx_entity` ON `heartbeats`(`entity`);
|
||||
DROP INDEX IF EXISTS "idx_branch";
|
||||
CREATE INDEX `idx_branch` ON `heartbeats`(`branch`);
|
||||
DROP INDEX IF EXISTS "idx_editor";
|
||||
CREATE INDEX `idx_editor` ON `heartbeats`(`editor`);
|
||||
DROP INDEX IF EXISTS "idx_heartbeats_hash";
|
||||
CREATE UNIQUE INDEX `idx_heartbeats_hash` ON `heartbeats`(`hash`);
|
||||
DROP INDEX IF EXISTS "idx_language";
|
||||
@ -38,6 +40,16 @@ DROP INDEX IF EXISTS "idx_language_mapping_composite";
|
||||
CREATE UNIQUE INDEX `idx_language_mapping_composite` ON `language_mappings`(`user_id`,`extension`);
|
||||
DROP INDEX IF EXISTS "idx_language_mapping_user";
|
||||
CREATE INDEX `idx_language_mapping_user` ON `language_mappings`(`user_id`);
|
||||
DROP INDEX IF EXISTS "idx_leaderboard_combined";
|
||||
CREATE INDEX `idx_leaderboard_combined` ON `leaderboard_items`(`interval`,`by`);
|
||||
DROP INDEX IF EXISTS "idx_leaderboard_user";
|
||||
CREATE INDEX `idx_leaderboard_user` ON `leaderboard_items`(`user_id`);
|
||||
DROP INDEX IF EXISTS "idx_machine";
|
||||
CREATE INDEX `idx_machine` ON `heartbeats`(`machine`);
|
||||
DROP INDEX IF EXISTS "idx_operating_system";
|
||||
CREATE INDEX `idx_operating_system` ON `heartbeats`(`operating_system`);
|
||||
DROP INDEX IF EXISTS "idx_project";
|
||||
CREATE INDEX `idx_project` ON `heartbeats`(`project`);
|
||||
DROP INDEX IF EXISTS "idx_project_label_user";
|
||||
CREATE INDEX `idx_project_label_user` ON `project_labels`(`user_id`);
|
||||
DROP INDEX IF EXISTS "idx_time";
|
||||
@ -50,4 +62,6 @@ DROP INDEX IF EXISTS "idx_type";
|
||||
CREATE INDEX `idx_type` ON `summary_items`(`type`);
|
||||
DROP INDEX IF EXISTS "idx_user_email";
|
||||
CREATE INDEX `idx_user_email` ON `users`(`email`);
|
||||
DROP INDEX IF EXISTS "idx_user_project";
|
||||
CREATE INDEX `idx_user_project` ON `heartbeats`(`user_id`,`project`);
|
||||
COMMIT;
|
||||
|
@ -53,3 +53,10 @@ func ParseUserAgent(ua string) (string, string, error) {
|
||||
}
|
||||
return groups[0][1], groups[0][2], nil
|
||||
}
|
||||
|
||||
func SubSlice[T any](slice []T, from, to uint) []T {
|
||||
if int(to) > len(slice) {
|
||||
to = uint(len(slice))
|
||||
}
|
||||
return slice[from:int(to)]
|
||||
}
|
||||
|
19
utils/db.go
19
utils/db.go
@ -1,8 +1,10 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"gorm.io/gorm"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func IsCleanDB(db *gorm.DB) bool {
|
||||
@ -30,3 +32,20 @@ func HasConstraints(db *gorm.DB) bool {
|
||||
logbuch.Warn("HasForeignKeyConstraints is not yet implemented for dialect '%s'", db.Dialector.Name())
|
||||
return false
|
||||
}
|
||||
|
||||
func WhereNullable(query *gorm.DB, col string, val any) *gorm.DB {
|
||||
if val == nil || reflect.ValueOf(val).IsNil() {
|
||||
return query.Where(fmt.Sprintf("%s is null", col))
|
||||
}
|
||||
return query.Where(fmt.Sprintf("%s = ?", col), val)
|
||||
}
|
||||
|
||||
func WithPaging(query *gorm.DB, limit, skip int) *gorm.DB {
|
||||
if limit >= 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
if skip >= 0 {
|
||||
query = query.Offset(skip)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@ -42,3 +43,27 @@ func IsNoCache(r *http.Request, cacheTtl time.Duration) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ParsePageParams(r *http.Request) *models.PageParams {
|
||||
pageParams := &models.PageParams{}
|
||||
page := r.URL.Query().Get("page")
|
||||
pageSize := r.URL.Query().Get("page_size")
|
||||
if p, err := strconv.Atoi(page); err == nil {
|
||||
pageParams.Page = p
|
||||
}
|
||||
if p, err := strconv.Atoi(pageSize); err == nil && pageParams.Page > 0 {
|
||||
pageParams.PageSize = p
|
||||
}
|
||||
return pageParams
|
||||
}
|
||||
|
||||
func ParsePageParamsWithDefault(r *http.Request, page, size int) *models.PageParams {
|
||||
pageParams := ParsePageParams(r)
|
||||
if pageParams.Page == 0 {
|
||||
pageParams.Page = page
|
||||
}
|
||||
if pageParams.PageSize == 0 {
|
||||
pageParams.PageSize = size
|
||||
}
|
||||
return pageParams
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{{ if .Error }}
|
||||
<div class="flex justify-center w-full">
|
||||
<div class="p-4 font-semibold text-white text-sm bg-red-500 rounded mt-16 shadow flex-grow max-w-lg">
|
||||
<div class="p-4 font-semibold text-white text-sm bg-red-500 rounded mt-16 shadow grow max-w-lg">
|
||||
Error: {{ .Error | capitalize }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else if .Success }}
|
||||
<div class="flex justify-center w-full">
|
||||
<div class="p-4 font-semibold text-white text-sm bg-green-500 rounded mt-16 shadow flex-grow max-w-lg">
|
||||
<div class="p-4 font-semibold text-white text-sm bg-green-500 rounded mt-16 shadow grow max-w-lg">
|
||||
{{ .Success | capitalize }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<footer class="flex justify-between w-full text-center text-gray-500 text-xs mt-20">
|
||||
<footer class="flex justify-between w-full text-gray-500 mt-20 items-center gap-x-4">
|
||||
<div class="text-xs font-mono font-semibold">
|
||||
{{ getVersion }} @ {{ getDbType }}
|
||||
</div>
|
||||
<div class="font-semibold text-sm hidden sm:inline-block">
|
||||
Made with <span class="iconify inline" data-icon="bi:heart-fill"></span> by <a href="https://muetsch.io" class="text-gray-400 hover:text-gray-300">Ferdinand Mütsch</a> as <a
|
||||
href="https://github.com/muety/wakapi" class="text-gray-400 hover:text-gray-300">open-source</a> software
|
||||
<div class="font-semibold text-sm">
|
||||
Made with <span class="iconify inline" data-icon="bi:heart-fill"></span> by
|
||||
<a href="https://muetsch.io" class="text-gray-400 hover:text-gray-300">Ferdinand Mütsch</a> as
|
||||
<a href="https://github.com/muety/wakapi" class="text-gray-400 hover:text-gray-300">open-source</a> software
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<a href="imprint" class="font-semibold hover:text-gray-400">Imprint, Cookies & Data Privacy</a>
|
||||
|
@ -7,8 +7,8 @@
|
||||
|
||||
{{ template "header.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow flex w-full">
|
||||
<div class="flex-grow max-w-4xl flex flex-col">
|
||||
<main class="mt-10 grow flex w-full">
|
||||
<div class="grow max-w-4xl flex flex-col">
|
||||
<h1 class="h1">Imprint & Data Privacy</h1>
|
||||
<p>
|
||||
{{ htmlSafe .HtmlText }}
|
||||
|
@ -9,14 +9,9 @@
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<div class="absolute flex top-0 right-0 mr-4 mt-10 py-2">
|
||||
<div class="mx-1">
|
||||
<a href="login" class="btn-primary">
|
||||
<span class="iconify inline" data-icon="fluent:key-24-filled"></span> Login</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "login-btn.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 px-4 md:px-10 lg:px-24 flex-grow flex justify-center w-full">
|
||||
<main class="mt-10 px-4 md:px-10 lg:px-24 grow flex justify-center w-full">
|
||||
<div class="flex flex-col text-white">
|
||||
{{ if and .Newsbox .Newsbox.Text }}
|
||||
<div class="mb-14 -mt-4 newsbox newsbox-{{ .Newsbox.Type }}">
|
||||
@ -74,6 +69,7 @@
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> 100 % free and open-source</li>
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> Built by developers for developers</li>
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> Fancy statistics and plots</li>
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> Public leaderboards</li>
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> Cool badges for readmes</li>
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> Weekly e-mail reports</li>
|
||||
<li><span class="iconify inline text-green-700" data-icon="eva:checkmark-circle-2-fill"></span> Intuitive REST API</li>
|
||||
|
97
views/leaderboard.tpl.html
Normal file
97
views/leaderboard.tpl.html
Normal file
@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{{ template "head.tpl.html" . }}
|
||||
|
||||
<script>
|
||||
const defaultTab = 'total'
|
||||
</script>
|
||||
|
||||
<body class="relative bg-gray-900 text-gray-700 p-4 pt-10 flex flex-col min-h-screen {{ if .User }} max-w-screen-xl {{ else }} max-w-screen-lg {{end}} mx-auto justify-center">
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
{{ if .User }}
|
||||
{{ template "menu-main.tpl.html" . }}
|
||||
{{ else }}
|
||||
{{ template "header.tpl.html" . }}
|
||||
{{ template "login-btn.tpl.html" . }}
|
||||
{{ end }}
|
||||
|
||||
<main class="mt-10 grow flex justify-center w-full" id="leaderboard-page">
|
||||
<div class="flex flex-col grow mt-10 max-available">
|
||||
<h1 class="h1" style="margin-bottom: 0.5rem">Leaderboard</h1>
|
||||
|
||||
<p class="block text-sm text-gray-300 w-full lg:w-3/4 mb-8">
|
||||
Wakapi's leaderboard shows a ranking of the most active users on this server, given they opted in to get listed on the public leaderboard. Statistics are updated at least every 12 hours and are based on the users' total coding time in the past seven days.
|
||||
To participate, log in, go to <a class="link" href="settings#permissions">Settings 🠒 Permissions</a> and enable leaderboards.
|
||||
</p>
|
||||
|
||||
<ul class="flex space-x-4 mb-4 text-gray-600">
|
||||
<li class="font-semibold text-xl {{ if eq .By "" }} text-gray-300 {{ else }} hover:text-gray-500 {{ end }}">
|
||||
<a href="leaderboard">Total</a>
|
||||
</li>
|
||||
<li class="font-semibold text-xl {{ if eq .By "language" }} text-gray-300 {{ else }} hover:text-gray-500 {{ end }}">
|
||||
<a href="leaderboard?by=language">By Language</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ if ne .By "" }}
|
||||
<div class="flex flex-wrap space-x-2 mb-4">
|
||||
{{ range $i, $key := (strslice .TopKeys 0 10) }}
|
||||
<div class="inline-block mb-4">
|
||||
<a href="leaderboard?by={{ $.By }}&key={{ lower $key }}" class="{{ if eq (lower $.Key) (lower $key) }} btn-primary {{ else }} btn-default {{ end }} btn-small cursor-pointer whitespace-nowrap">
|
||||
{{ if and (eq (lower $.By) "language") ($.LangIcon $key) }}
|
||||
<span class="align-middle leading-none"><span class="iconify inline text-white text-base" data-icon="{{ ($.LangIcon $key) | urlSafe }}"></span> </span>
|
||||
{{ end }}
|
||||
<span>{{ $key }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="flex flex-col space-y-4 mt-4 text-gray-300 w-full lg:w-3/4">
|
||||
{{ if len .Items }}
|
||||
<ol>
|
||||
{{ range $i, $item := .Items }}
|
||||
<li class="px-4 py-2 my-2 rounded-md border-2 leaderboard-{{ ($.ColorModifier $item $.User) }} flex justify-between">
|
||||
<div class="w-1/12 mr-1"><strong># {{ $item.Rank }}</strong></div>
|
||||
<div class="flex w-3/12 mx-1 justify-start items-center space-x-4 align-middle">
|
||||
{{ if avatarUrlTemplate }}
|
||||
<img src="{{ $item.User.AvatarURL avatarUrlTemplate }}" width="24px" class="rounded-full border-green-700" alt="User Profile Avatar"/>
|
||||
{{ else }}
|
||||
<span class="iconify inline cursor-pointer text-gray-500 rounded-full border-green-700" style="width: 24px; height: 24px" data-icon="ic:round-person"></span>
|
||||
{{ end }}
|
||||
<strong class="text-ellipsis truncate">@{{ $item.UserID }}</strong>
|
||||
</div>
|
||||
<div class="w-5/12 mx-1 truncate leading-6 align-middle">
|
||||
{{ range $i, $lang := (index $.UserLanguages $item.UserID) }}
|
||||
{{ if $.LangIcon $lang }}
|
||||
<span class="align-middle leading-none"><span class="iconify inline text-white text-base" data-icon="{{ ($.LangIcon $lang) | urlSafe }}"></span></span>
|
||||
{{ end }}
|
||||
<span class="text-sm leading-6">{{ $lang }}{{ if lt $i (add (len (index $.UserLanguages $item.UserID)) -1) }}, {{ end }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="w-3/12 ml-1 text-right"><span>{{ $item.Total | duration }}</span></div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
<p class="text-sm pt-8">Last Updated: {{ .LastUpdate | datetime }}</p>
|
||||
{{ else }}
|
||||
<p>
|
||||
<span class="iconify inline text-white text-base" data-icon="twemoji:frowning-face"></span>
|
||||
The leaderboard is currently empty ...
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{{ template "footer.tpl.html" . }}
|
||||
|
||||
{{ template "foot.tpl.html" . }}
|
||||
</body>
|
||||
|
||||
</html>
|
10
views/login-btn.tpl.html
Normal file
10
views/login-btn.tpl.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div class="absolute flex top-0 right-0 mr-4 mt-10 py-2">
|
||||
<div class="mx-1">
|
||||
<a href="leaderboard" class="btn-default">
|
||||
<span class="iconify inline" data-icon="fluent:data-bar-horizontal-24-filled"></span> Leaderboard</a>
|
||||
</div>
|
||||
<div class="mx-1">
|
||||
<a href="login" class="btn-primary">
|
||||
<span class="iconify inline" data-icon="fluent:key-24-filled"></span> Login</a>
|
||||
</div>
|
||||
</div>
|
@ -9,8 +9,8 @@
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||
<div class="flex-grow max-w-lg mt-10">
|
||||
<main class="mt-10 grow flex justify-center w-full">
|
||||
<div class="grow max-w-lg mt-10">
|
||||
<div class="mb-8">
|
||||
<h1 class="h1">Welcome!</h1>
|
||||
<span class="h1-subcaption">Log in to continue using Wakapi</span>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script type="module" src="assets/js/components/menu-main.js"></script>
|
||||
|
||||
<div class="flex justify-between space-x-4 items-center relative" id="main-menu" v-scope @vue:mounted="mounted">
|
||||
<div class="mr-8 hidden lg:inline-block flex-shrink-0">
|
||||
<div class="mr-8 hidden lg:inline-block shrink-0">
|
||||
{{ template "logo.tpl.html" }}
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,11 @@
|
||||
<span class="text-gray-300 hidden lg:inline-block">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<a class="menu-item" href="leaderboard">
|
||||
<span class="iconify inline text-2xl text-gray-400" data-icon="fluent:data-bar-horizontal-24-filled"></span>
|
||||
<span class="text-gray-300 hidden lg:inline-block">Leaderboard</span>
|
||||
</a>
|
||||
|
||||
<div class="menu-item hidden sm:flex imp:cursor-not-allowed">
|
||||
<span class="iconify inline text-2xl text-gray-700" data-icon="bi:people-fill"></span>
|
||||
<a class="text-gray-600 leading-none hidden lg:inline-block">Team<br>
|
||||
@ -17,20 +22,13 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-item hidden sm:flex imp:cursor-not-allowed">
|
||||
<span class="iconify inline text-2xl text-gray-700" data-icon="fluent:data-bar-horizontal-24-filled"></span>
|
||||
<a class="text-gray-600 leading-none hidden lg:inline-block">Leaderboard<br>
|
||||
<span class="text-xxs whitespace-nowrap">(coming soon)</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-item relative" @click="state.showDropdownResources = !state.showDropdownResources" data-trigger-for="showDropdownResources">
|
||||
<span class="iconify inline text-2xl text-gray-400" data-icon="ph:books-bold"></span>
|
||||
<a class="text-gray-400 hidden lg:inline-block">Resources</a>
|
||||
<span class="iconify inline text-xl text-gray-400" data-icon="akar-icons:chevron-down"></span>
|
||||
|
||||
<div v-cloak v-show="state.showDropdownResources" class="flex bg-gray-850 shadow-md z-10 p-2 absolute top-0 right-0 rounded popup mt-12 w-full" id="resources-menu-dropdown" style="min-width: 128px">
|
||||
<div class="flex-grow flex flex-col">
|
||||
<div class="grow flex flex-col">
|
||||
<div class="submenu-item">
|
||||
<a class="flex justify-between w-full text-gray-300 items-center px-2 font-semibold" href="https://github.com/muety/wakapi" target="_blank" rel="noreferrer noopener" @click="state.showDropdownResources = !state.showDropdownResources" data-trigger-for="showDropdownResources">
|
||||
<span class="text-sm">GitHub</span>
|
||||
@ -64,9 +62,9 @@
|
||||
<span class="text-gray-400 hidden lg:inline-block">Settings</span>
|
||||
</a>
|
||||
|
||||
<div class="flex-grow"></div>
|
||||
<div class="grow"></div>
|
||||
|
||||
<div class="flex-shrink-0 menu-item relative" @click="state.showDropdownUser = !state.showDropdownUser" data-trigger-for="showDropdownUser">
|
||||
<div class="shrink-0 menu-item relative" @click="state.showDropdownUser = !state.showDropdownUser" data-trigger-for="showDropdownUser">
|
||||
<div class="hidden md:flex flex flex-col text-right">
|
||||
<a class="text-gray-300">{{ .User.ID }}</a>
|
||||
{{ if .User.Email }}
|
||||
@ -80,7 +78,7 @@
|
||||
{{ end }}
|
||||
|
||||
<div v-cloak v-show="state.showDropdownUser" class="flex bg-gray-850 shadow-md z-10 p-2 absolute top-0 right-0 rounded popup mt-16 w-full" id="user-menu-popup" style="min-width: 156px;">
|
||||
<div class="flex-grow flex flex-col">
|
||||
<div class="grow flex flex-col">
|
||||
<div class="submenu-item hover:bg-gray-800 rounded p-1 text-right">
|
||||
<button class="flex justify-between w-full text-gray-300 items-center px-2 font-semibold" @click="state.showApiKey = true" data-trigger-for="showApiKey">
|
||||
<span class="text-sm">Show API Key</span>
|
||||
@ -88,7 +86,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="submenu-item hover:bg-gray-800 rounded p-1 text-right">
|
||||
<form action="logout" method="post" class="flex-grow">
|
||||
<form action="logout" method="post" class="grow">
|
||||
<button type="submit" class="flex justify-between w-full text-gray-300 items-center px-2 font-semibold">
|
||||
<span class="text-sm">Logout</span>
|
||||
<span class="iconify inline" data-icon="ls:logout"></span>
|
||||
@ -100,7 +98,7 @@
|
||||
</div>
|
||||
|
||||
<div v-cloak v-show="state.showApiKey" class="flex bg-gray-850 shadow-md z-10 p-2 absolute top-0 right-0 rounded popup" id="api-key-popup">
|
||||
<div class="flex-grow flex flex-col px-2">
|
||||
<div class="grow flex flex-col px-2">
|
||||
<span class="text-xxs text-gray-500 mx-1">API Key</span>
|
||||
<input type="text" class="bg-transparent text-sm text-white mx-1 font-mono" id="api-key-container" readonly
|
||||
value="{{ .ApiKey }}" style="min-width: 330px">
|
||||
|
@ -9,8 +9,8 @@
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||
<div class="flex-grow max-w-lg mt-10">
|
||||
<main class="mt-10 grow flex justify-center w-full">
|
||||
<div class="grow max-w-lg mt-10">
|
||||
<div class="mb-8">
|
||||
<h1 class="h1">Reset Password</h1>
|
||||
<span class="h1-subcaption">
|
||||
|
@ -9,8 +9,8 @@
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow flex justify-center w-full">
|
||||
<div class="flex-grow max-w-lg mt-10">
|
||||
<main class="mt-10 grow flex justify-center w-full">
|
||||
<div class="grow max-w-lg mt-10">
|
||||
<div class="mb-8">
|
||||
<h1 class="h1">Choose a new password</h1>
|
||||
<span class="h1-subcaption">You have requested to reset your password. Please choose a new one.</span>
|
||||
|
@ -32,8 +32,8 @@
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow flex justify-center w-full" v-scope @vue:mounted="mounted" id="settings-page">
|
||||
<div class="flex flex-col flex-grow mt-10">
|
||||
<main class="mt-10 grow flex justify-center w-full" v-scope @vue:mounted="mounted" id="settings-page">
|
||||
<div class="flex flex-col grow mt-10">
|
||||
<h1 class="font-semibold text-3xl text-white m-0 mb-4">Settings</h1>
|
||||
|
||||
<ul class="flex space-x-4 mb-16 text-gray-600">
|
||||
@ -276,7 +276,7 @@
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex items-center mt-2 w-full text-gray-500 text-sm space-x-4">
|
||||
<select name="key" id="select-project"
|
||||
class="select-default flex-grow">
|
||||
class="select-default grow">
|
||||
{{ range $i, $p := .Projects }}
|
||||
<option value="{{ $p }}">{{ $p }}</option>
|
||||
{{ end }}
|
||||
@ -337,11 +337,11 @@
|
||||
<input type="hidden" name="action" value="add_mapping">
|
||||
<div class="flex items-center w-full text-gray-500 text-sm">
|
||||
<span class="mr-2">When filename ends in</span>
|
||||
<input class="select-default flex-grow"
|
||||
<input class="select-default grow"
|
||||
type="text" id="extension" style="width: 70px"
|
||||
name="extension" placeholder=".py" minlength="1" required>
|
||||
<span class="mx-2">change language to</span>
|
||||
<input class="select-default flex-grow"
|
||||
<input class="select-default grow"
|
||||
type="text" id="language" style="width: 100px"
|
||||
name="language" placeholder="Python" minlength="1" required>
|
||||
<div class="flex justify-end ml-4">
|
||||
@ -380,11 +380,51 @@
|
||||
</div>
|
||||
|
||||
<div v-cloak id="permissions" class="tab flex flex-col space-y-4" v-if="isActive('permissions')">
|
||||
<!-- Public Leaderboard -->
|
||||
<form action="" method="post" class="w-full lg:w-3/4">
|
||||
<div class="flex flex-wrap md:flex-nowrap mb-8 gap-x-4">
|
||||
<div class="w-full md:w-1/2 mb-4 md:mb-0 inline-block">
|
||||
<span class="font-semibold text-gray-300 text-lg">Public Leaderboard</span>
|
||||
<p class="block text-sm text-gray-600">
|
||||
Opt in to get listed in the <a class="link" href="leaderboard">public leaderboard</a>. It shows aggregated statistics from the past 7 days of your coding.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-col w-full md:w-1/2 inline-block space-y-4">
|
||||
<input type="hidden" name="action" value="update_leaderboard">
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_projects">Participate in leaderboard</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="enable_leaderboard" name="enable_leaderboard" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.PublicLeaderboard }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.PublicLeaderboard }} selected {{ end }}>Yes
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<button type="submit" class="btn-primary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="w-full md:w-3/4">
|
||||
<hr class="border-t border-gray-800 my-4">
|
||||
</div>
|
||||
|
||||
<!-- Public Data -->
|
||||
<form action="" method="post" class="w-full lg:w-3/4">
|
||||
<div class="flex flex-wrap md:flex-nowrap mb-8 gap-x-4">
|
||||
<div class="w-full md:w-1/2 mb-4 md:mb-0 inline-block">
|
||||
<span class="font-semibold text-gray-300 text-lg">Aliases</span>
|
||||
<span class="font-semibold text-gray-300 text-lg">Public Data</span>
|
||||
<p class="block text-sm text-gray-600">
|
||||
Some features require public access to your data without authentication. This mainly includes badges ("shields" endpoint) and the integration with GitHub Readme Stats ("stats" endpoint). You can choose which data to share publicly through these endpoints.
|
||||
</p>
|
||||
@ -394,7 +434,7 @@
|
||||
<input type="hidden" name="action" value="update_sharing">
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="max_days">Time Range</label>
|
||||
<span class="block text-sm text-gray-600">(in days; 0 = not public, -1 = unlimited)</span>
|
||||
</div>
|
||||
@ -406,11 +446,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_projects">Share Projects</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="share_projects" name="share_projects" class="select-default flex-grow">
|
||||
<select autocomplete="off" id="share_projects" name="share_projects" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.ShareProjects }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.ShareProjects }} selected {{ end }}>Yes
|
||||
@ -420,11 +460,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_languages">Share Languages</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="share_languages" name="share_languages" class="select-default flex-grow">
|
||||
<select autocomplete="off" id="share_languages" name="share_languages" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.ShareLanguages }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.ShareLanguages }} selected {{ end }}>Yes
|
||||
@ -434,11 +474,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_editors">Share Editors</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="share_editors" name="share_editors" class="select-default flex-grow">
|
||||
<select autocomplete="off" id="share_editors" name="share_editors" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.ShareEditors }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.ShareEditors }} selected {{ end }}>Yes
|
||||
@ -448,11 +488,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_oss">Share OS'</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="share_oss" name="share_oss" class="select-default flex-grow">
|
||||
<select autocomplete="off" id="share_oss" name="share_oss" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.ShareOSs }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.ShareOSs }} selected {{ end }}>Yes
|
||||
@ -462,11 +502,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_machines">Share Machines</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="share_machines" name="share_machines" class="select-default flex-grow">
|
||||
<select autocomplete="off" id="share_machines" name="share_machines" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.ShareMachines }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.ShareMachines }} selected {{ end }}>Yes
|
||||
@ -476,11 +516,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-8">
|
||||
<div class="flex-grow">
|
||||
<div class="grow">
|
||||
<label class="font-semibold text-gray-300" for="share_labels">Share Project Labels</label>
|
||||
</div>
|
||||
<div>
|
||||
<select autocomplete="off" id="share_labels" name="share_labels" class="select-default flex-grow">
|
||||
<select autocomplete="off" id="share_labels" name="share_labels" class="select-default grow">
|
||||
<option value="false" class="cursor-pointer" {{ if not .User.ShareLabels }} selected {{ end }}>No
|
||||
</option>
|
||||
<option value="true" class="cursor-pointer" {{ if .User.ShareLabels }} selected {{ end }}>Yes
|
||||
@ -513,6 +553,7 @@
|
||||
<label class="font-semibold text-gray-300" for="select-timezone">WakaTime</label>
|
||||
<span class="block text-sm text-gray-600">
|
||||
You can connect Wakapi with the official WakaTime (or another Wakapi instance, when optionally specifying a custom API URL) in a way that all heartbeats sent to Wakapi are relayed. This way, you can use both services at the same time. To get started, get <a class="link" href="https://wakatime.com/developers#authentication" rel="noopener noreferrer" target="_blank">your API key</a> and paste it here.<br><br>
|
||||
To forward data to another Wakapi instance, use <span class="text-xs font-mono">https://<your-server>/api/compat/wakatime/v1</span> as a URL.<br><br>
|
||||
Please note: When enabling this feature, the operators of this server will, in theory, have unlimited access to your data stored in WakaTime. If you are concerned about your privacy, please do not enable this integration or wait for OAuth 2 authentication (<a
|
||||
class="link" target="_blank" href="https://github.com/muety/wakapi/issues/94" rel="noopener noreferrer">#94</a>) to be implemented.
|
||||
</span>
|
||||
@ -624,7 +665,7 @@
|
||||
class="with-url-src-no-scheme" alt="Readme Stats Card">
|
||||
</div>
|
||||
<textarea
|
||||
class="with-url-inner-no-scheme flex-shrink w-full font-mono text-xs appearance-none bg-gray-850 text-gray-500 outline-none rounded py-2 px-4 cursor-not-allowed mt-2"
|
||||
class="with-url-inner-no-scheme shrink w-full font-mono text-xs appearance-none bg-gray-850 text-gray-500 outline-none rounded py-2 px-4 cursor-not-allowed mt-2"
|
||||
rows="5" style="resize: none"
|
||||
readonly>https://github-readme-stats.vercel.app/api/wakatime?username={{ .User.ID }}&api_domain=%s&bg_color=1A202C&title_color=2F855A&icon_color=2F855A&text_color=ffffff&custom_title=Wakapi%20Week%20Stats&layout=compact</textarea>
|
||||
{{ end }}
|
||||
|
@ -20,8 +20,8 @@
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow flex justify-center w-full" id="signup-page">
|
||||
<div class="flex-grow max-w-lg mt-10">
|
||||
<main class="mt-10 grow flex justify-center w-full" id="signup-page">
|
||||
<div class="grow max-w-lg mt-10">
|
||||
<div class="mb-8">
|
||||
<h1 class="h1">Sign up to Wakapi</h1>
|
||||
<p class="h1-subcaption">
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
{{ if .User.HasData }}
|
||||
|
||||
<div id="summary-page" class="flex-grow" v-scope>
|
||||
<div id="summary-page" class="grow" v-scope>
|
||||
<div class="flex justify-end mt-12 relative">
|
||||
<div v-scope="TimePicker({
|
||||
fromDate: '{{ .From | simpledate }}',
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
{{ end }}
|
||||
|
||||
<main class="flex flex-col items-center mt-10 flex-grow">
|
||||
<main class="flex flex-col items-center mt-10 grow">
|
||||
|
||||
{{ if .User.HasData }}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template id="time-picker-template">
|
||||
<div class="menu-item flex items-center text-sm font-semibold space-x-2 rounded hover:bg-gray-850 py-2 px-4 cursor-pointer justify-end" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">
|
||||
<span class="iconify inline text-2xl text-gray-400 flex-grow" data-icon="fa-regular:calendar-alt"></span>
|
||||
<span class="iconify inline text-2xl text-gray-400 grow" data-icon="fa-regular:calendar-alt"></span>
|
||||
<a v-cloak id="current-time-selection" class="text-gray-300 -mb-1">${timeSelection}</a>
|
||||
<span class="iconify inline text-2xl text-gray-400" data-icon="akar-icons:chevron-down"></span>
|
||||
</div>
|
||||
|
||||
<div v-cloak v-show="state.showDropdownTimepicker" class="z-10 absolute top-0 right-0 popup mt-12 w-40" id="time-picker-dropdown">
|
||||
<div class="flex-grow flex flex-col flex bg-gray-850 shadow-md rounded w-40 p-1 ">
|
||||
<div class="grow flex flex-col flex bg-gray-850 shadow-md rounded w-40 p-1 ">
|
||||
<a id="time-option-today" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('today')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Today</a>
|
||||
<a id="time-option-yesterday" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('yesterday')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">Yesterday</a>
|
||||
<a id="time-option-week" class="submenu-item hover:bg-gray-800 rounded p-1 text-right w-full text-gray-300 px-2 font-semibold text-sm" :href="intervalLink('week')" @click="state.showDropdownTimepicker = !state.showDropdownTimepicker" data-trigger-for="showDropdownTimepicker">This Week</a>
|
||||
|
Reference in New Issue
Block a user