mirror of https://github.com/muety/wakapi.git
Merge branch '182-leaderboards'
This commit is contained in:
commit
1e9d3f9e80
|
@ -12,11 +12,12 @@ 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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -216,6 +217,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,4 +9,5 @@ const (
|
|||
ResetPasswordTemplate = "reset-password.tpl.html"
|
||||
SettingsTemplate = "settings.tpl.html"
|
||||
SummaryTemplate = "summary.tpl.html"
|
||||
LeaderboardTemplate = "leaderboard.tpl.html"
|
||||
)
|
||||
|
|
72
go.sum
72
go.sum
|
@ -1,17 +1,12 @@
|
|||
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/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||
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=
|
||||
|
@ -21,14 +16,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
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=
|
||||
|
@ -36,8 +27,6 @@ github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVR
|
|||
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=
|
||||
|
@ -46,8 +35,6 @@ github.com/glebarez/go-sqlite v1.18.2 h1:ck3PQVaEzzzapP0g7pfhzbB3Jw4rNk+IldLMy/l
|
|||
github.com/glebarez/go-sqlite v1.18.2/go.mod h1:/kOdnnt5T0ztYXqBPdjRVM8JwMpFtyAQp1mtRoNxziM=
|
||||
github.com/glebarez/sqlite v1.4.7 h1:tIBxEWLJOPkekuQcwfenNfh13itj9GoVJYxp7GidJAo=
|
||||
github.com/glebarez/sqlite v1.4.7/go.mod h1:UY1smw9rBTSGnJE0He8pVRPvlxCP1C8hlB8Z24K8fG4=
|
||||
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/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=
|
||||
|
@ -58,14 +45,10 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe
|
|||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/spec v0.20.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.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=
|
||||
|
@ -90,7 +73,6 @@ 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=
|
||||
|
@ -101,8 +83,6 @@ 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=
|
||||
|
@ -113,7 +93,6 @@ 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=
|
||||
|
@ -121,8 +100,6 @@ 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=
|
||||
|
@ -131,22 +108,17 @@ github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01C
|
|||
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=
|
||||
|
@ -158,12 +130,12 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
|
|||
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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinpollet/nego v0.0.0-20200324111829-b3061ca9dd9d/go.mod h1:3FSWkzk9h42opyV0o357Fq6gsLF/A6MI/qOca9kKobY=
|
||||
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=
|
||||
|
@ -177,8 +149,6 @@ 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=
|
||||
|
@ -193,7 +163,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
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=
|
||||
|
@ -202,9 +171,7 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
|
|||
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/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
|
@ -212,7 +179,6 @@ 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 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
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=
|
||||
|
@ -231,7 +197,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
|
|||
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=
|
||||
|
@ -240,19 +205,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
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/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-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
|
||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
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.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
|
||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
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=
|
||||
|
@ -261,8 +221,6 @@ 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=
|
||||
|
@ -281,21 +239,15 @@ 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/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/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/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.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
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=
|
||||
|
@ -305,14 +257,10 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-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/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=
|
||||
|
@ -330,7 +278,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
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=
|
||||
|
@ -353,8 +300,6 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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.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=
|
||||
|
@ -366,8 +311,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
|
|||
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=
|
||||
|
@ -375,26 +320,16 @@ 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=
|
||||
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.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
|
||||
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
|
||||
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/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/postgres v1.3.10 h1:Fsd+pQpFMGlGxxVMUPJhNo8gG8B1lKtk8QQ4/VZZAJw=
|
||||
gorm.io/driver/postgres v1.3.10/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
|
||||
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=
|
||||
gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=
|
||||
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/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
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 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
|
||||
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=
|
||||
|
@ -419,7 +354,6 @@ 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 h1:EKpC8eyhOcxpstYjohs7vxni7BoQBUVWXsf5rAZzlgk=
|
||||
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
|
||||
modernc.org/libc v1.20.0 h1:MEbCfCKpuDC/LRb3HOCM9fZOqnPx8le3kzTJVmUGDbU=
|
||||
modernc.org/libc v1.20.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||
|
@ -429,12 +363,10 @@ 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 h1:6ZIOLb5ronARPxEPxtZz1WbSRllgA09FCvNNyql5kZg=
|
||||
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/sqlite v1.18.2 h1:S2uFiaNPd/vTAP/4EmyY8Qe2Quzu26A2L1e25xRNTio=
|
||||
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=
|
||||
|
|
7
main.go
7
main.go
|
@ -59,6 +59,7 @@ var (
|
|||
languageMappingRepository repositories.ILanguageMappingRepository
|
||||
projectLabelRepository repositories.IProjectLabelRepository
|
||||
summaryRepository repositories.ISummaryRepository
|
||||
leaderboardRepository *repositories.LeaderboardRepository
|
||||
keyValueRepository repositories.IKeyValueRepository
|
||||
diagnosticsRepository repositories.IDiagnosticsRepository
|
||||
metricsRepository *repositories.MetricsRepository
|
||||
|
@ -72,6 +73,7 @@ var (
|
|||
projectLabelService services.IProjectLabelService
|
||||
durationService services.IDurationService
|
||||
summaryService services.ISummaryService
|
||||
leaderboardService services.ILeaderboardService
|
||||
aggregationService services.IAggregationService
|
||||
mailService services.IMailService
|
||||
keyValueService services.IKeyValueService
|
||||
|
@ -159,6 +161,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)
|
||||
|
@ -172,6 +175,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)
|
||||
|
@ -180,6 +184,7 @@ func main() {
|
|||
|
||||
// Schedule background tasks
|
||||
go aggregationService.Schedule()
|
||||
go leaderboardService.ScheduleDefault()
|
||||
go miscService.ScheduleCountTotalTime()
|
||||
go reportService.Schedule()
|
||||
|
||||
|
@ -207,6 +212,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)
|
||||
|
@ -241,6 +247,7 @@ func main() {
|
|||
loginHandler.RegisterRoutes(rootRouter)
|
||||
imprintHandler.RegisterRoutes(rootRouter)
|
||||
summaryHandler.RegisterRoutes(rootRouter)
|
||||
leaderboardHandler.RegisterRoutes(rootRouter)
|
||||
settingsHandler.RegisterRoutes(rootRouter)
|
||||
relayHandler.RegisterRoutes(rootRouter)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
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"`
|
||||
Rank uint `json:"rank" gorm:"->"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type Leaderboard []*LeaderboardItem
|
||||
|
||||
func (l Leaderboard) UserIDs() []string {
|
||||
return slice.Unique[string](slice.Map[*LeaderboardItem, string](l, func(i int, item *LeaderboardItem) string {
|
||||
return item.UserID
|
||||
}))
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopByKey(by uint8, key string) Leaderboard {
|
||||
return slice.Filter[*LeaderboardItem](l, func(i int, item *LeaderboardItem) 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
|
||||
}
|
||||
if _, ok := totalsMapped[*item.Key]; !ok {
|
||||
totalsMapped[*item.Key] = &keyTotal{Key: *item.Key, Total: 0}
|
||||
}
|
||||
totalsMapped[*item.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[*LeaderboardItem](l, func(i int, item *LeaderboardItem) 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
|
||||
}
|
|
@ -66,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 {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardViewModel struct {
|
||||
User *models.User
|
||||
By string
|
||||
Key string
|
||||
Items []*models.LeaderboardItem
|
||||
TopKeys []string
|
||||
UserLanguages map[string][]string
|
||||
ApiKey string
|
||||
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.LeaderboardItem, 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": "c",
|
||||
"c++": "cpp",
|
||||
"cpp": "cpp",
|
||||
"go": "go",
|
||||
"haskell": "haskell",
|
||||
"html": "html5",
|
||||
"java": "java",
|
||||
"javascript": "javascript",
|
||||
"kotlin": "kotlin",
|
||||
"lua": "lua",
|
||||
"php": "php",
|
||||
"python": "python",
|
||||
"r": "r",
|
||||
"ruby": "ruby",
|
||||
"rust": "rust",
|
||||
"swift": "swift",
|
||||
"typescript": "typescript",
|
||||
}
|
||||
if match, ok := langs[strings.ToLower(lang)]; ok {
|
||||
return "mdi:language-" + match
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) LastUpdate() time.Time {
|
||||
return models.Leaderboard(s.Items).LastUpdate()
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
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) GetAllAggregatedByInterval(key *models.IntervalKey, by *uint8) ([]*models.LeaderboardItem, error) {
|
||||
// TODO: distinct by (user, key) to filter out potential duplicates ?
|
||||
var items []*models.LeaderboardItem
|
||||
q := r.db.
|
||||
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||
Where("\"interval\" in ?", *key)
|
||||
q = utils.WhereNullable(q, "\"by\"", by)
|
||||
|
||||
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) ([]*models.LeaderboardItem, error) {
|
||||
var items []*models.LeaderboardItem
|
||||
q := r.db.
|
||||
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||
Where("user_id = ?", userId).
|
||||
Where("\"interval\" in ?", *key)
|
||||
q = utils.WhereNullable(q, "\"by\"", by)
|
||||
|
||||
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
|
||||
}
|
|
@ -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,12 @@ 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)
|
||||
DeleteByUser(string) error
|
||||
DeleteByUserAndInterval(string, *models.IntervalKey) error
|
||||
GetAllAggregatedByInterval(*models.IntervalKey, *uint8) ([]*models.LeaderboardItem, error)
|
||||
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8) ([]*models.LeaderboardItem, 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)
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
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"
|
||||
"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"))
|
||||
|
||||
var err error
|
||||
var leaderboard models.Leaderboard
|
||||
var userLanguages map[string][]string
|
||||
var topKeys []string
|
||||
|
||||
if byParam == "" {
|
||||
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{Error: criticalError}
|
||||
}
|
||||
} else {
|
||||
if by, ok := allowedAggregations[byParam]; ok {
|
||||
leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{Error: criticalError}
|
||||
}
|
||||
|
||||
userLeaderboards := slice.GroupWith[*models.LeaderboardItem, string](leaderboard, func(item *models.LeaderboardItem) 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 = strings.ToLower(topKeys[0])
|
||||
}
|
||||
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,
|
||||
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
|
||||
},
|
||||
|
|
|
@ -146,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":
|
||||
|
@ -182,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
|
||||
|
@ -251,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()
|
||||
|
|
|
@ -53,7 +53,24 @@ 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',
|
||||
'twemoji:frowning-face',
|
||||
]
|
||||
|
||||
const output = path.normalize(path.join(__dirname, '../static/assets/js/icons.dist.js'))
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
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"
|
||||
"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) GetByInterval(interval *models.IntervalKey, resolveUsers bool) (models.Leaderboard, error) {
|
||||
return srv.GetAggregatedByInterval(interval, nil, resolveUsers)
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8, resolveUsers bool) (models.Leaderboard, error) {
|
||||
// check cache
|
||||
cacheKey := srv.getHash(interval, by)
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||
return cacheResult.([]*models.LeaderboardItem), nil
|
||||
}
|
||||
|
||||
items, err := srv.repository.GetAllAggregatedByInterval(interval, by)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resolveUsers {
|
||||
a := models.Leaderboard(items).UserIDs()
|
||||
println(a)
|
||||
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) 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) string {
|
||||
k := strings.Join(*interval, "__")
|
||||
if by != nil && !reflect.ValueOf(by).IsNil() {
|
||||
k += "__" + models.GetEntityColumn(*by)
|
||||
}
|
||||
return k
|
||||
}
|
|
@ -97,13 +97,26 @@ type IReportService interface {
|
|||
Run(*models.User, time.Duration) error
|
||||
}
|
||||
|
||||
type ILeaderboardService interface {
|
||||
ScheduleDefault()
|
||||
Run([]*models.User, *models.IntervalKey, []uint8) error
|
||||
ExistsAnyByUser(string) (bool, error)
|
||||
GetByInterval(*models.IntervalKey, bool) (models.Leaderboard, error)
|
||||
GetAggregatedByInterval(*models.IntervalKey, *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.
|
@ -7,6 +7,11 @@ module.exports = {
|
|||
'newsbox-default',
|
||||
'newsbox-warning',
|
||||
'newsbox-danger',
|
||||
'leaderboard-self',
|
||||
'leaderboard-default',
|
||||
'leaderboard-gold',
|
||||
'leaderboard-silver',
|
||||
'leaderboard-bronze',
|
||||
]
|
||||
},
|
||||
}
|
|
@ -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)]
|
||||
}
|
||||
|
|
|
@ -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,10 @@ 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)
|
||||
}
|
||||
|
|
|
@ -9,12 +9,7 @@
|
|||
|
||||
{{ 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">
|
||||
<div class="flex flex-col text-white">
|
||||
|
@ -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>
|
||||
|
|
|
@ -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 flex-grow flex justify-center w-full" id="leaderboard-page">
|
||||
<div class="flex flex-col flex-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 servers, 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={{ $key }}" class="{{ if eq $.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>
|
|
@ -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>
|
|
@ -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,13 +22,6 @@
|
|||
</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>
|
||||
|
|
|
@ -380,6 +380,46 @@
|
|||
</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="flex-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 flex-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">
|
||||
|
|
Loading…
Reference in New Issue