mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-08 16:20:02 +00:00
Breaking: Add support for multiple database backends (#32)
This commit is contained in:
parent
b589e4137d
commit
c0075db1f3
50 changed files with 1115 additions and 952 deletions
55
README.md
55
README.md
|
@ -23,7 +23,8 @@ Usage of twitch-bot:
|
||||||
--log-level string Log level (debug, info, warn, error, fatal) (default "info")
|
--log-level string Log level (debug, info, warn, error, fatal) (default "info")
|
||||||
--plugin-dir string Where to find and load plugins (default "/usr/lib/twitch-bot")
|
--plugin-dir string Where to find and load plugins (default "/usr/lib/twitch-bot")
|
||||||
--rate-limit duration How often to send a message (default: 20/30s=1500ms, if your bot is mod everywhere: 100/30s=300ms, different for known/verified bots) (default 1.5s)
|
--rate-limit duration How often to send a message (default: 20/30s=1500ms, if your bot is mod everywhere: 100/30s=300ms, different for known/verified bots) (default 1.5s)
|
||||||
--storage-database string Database file to store data in (default "./storage.db")
|
--storage-conn-string string Connection string for the database (default "./storage.db")
|
||||||
|
--storage-conn-type string One of: mysql, postgres, sqlite (default "sqlite")
|
||||||
--storage-encryption-pass string Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)
|
--storage-encryption-pass string Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)
|
||||||
--twitch-client string Client ID to act as
|
--twitch-client string Client ID to act as
|
||||||
--twitch-client-secret string Secret for the Client ID
|
--twitch-client-secret string Secret for the Client ID
|
||||||
|
@ -39,6 +40,58 @@ Supported sub-commands are:
|
||||||
help Prints this help message
|
help Prints this help message
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Database Connection Strings
|
||||||
|
|
||||||
|
Currently these databases are supported and need their corresponding connection strings:
|
||||||
|
|
||||||
|
#### MySQL
|
||||||
|
|
||||||
|
```
|
||||||
|
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
|
||||||
|
|
||||||
|
Recommended parameters:
|
||||||
|
?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
|
```
|
||||||
|
|
||||||
|
- Create your database as follows:
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE twbot_tezrian CHARACTER SET utf8mb4;
|
||||||
|
```
|
||||||
|
- Start your bot:
|
||||||
|
```console
|
||||||
|
# twitch-bot \
|
||||||
|
--storage-conn-type mysql \
|
||||||
|
--storage-conn-string 'tezrian:mypass@tcp(mariadb:3306)/twbot_tezrian?charset=utf8mb4&parseTime=True&loc=Local' \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
See [driver documentation](https://github.com/go-sql-driver/mysql#dsn-data-source-name) for more details on parameters.
|
||||||
|
|
||||||
|
#### Postgres
|
||||||
|
|
||||||
|
```
|
||||||
|
host=localhost port=5432 dbname=mydb connect_timeout=10
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Postgres documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS) for more details in paramters.
|
||||||
|
|
||||||
|
#### SQLite
|
||||||
|
|
||||||
|
```
|
||||||
|
storage.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Just pass the filename you want to use.
|
||||||
|
|
||||||
|
- Start your bot:
|
||||||
|
```console
|
||||||
|
# twitch-bot \
|
||||||
|
--storage-conn-type sqlite \
|
||||||
|
--storage-conn-string 'storage.db' \
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Upgrade from `v2.x` to `v3.x`
|
## Upgrade from `v2.x` to `v3.x`
|
||||||
|
|
||||||
With the release of `v3.0.0` the bot changed a lot introducing a new storage format. As that storage backend is not compatible with the `v2.x` storage you need to migrate it manually before starting a `v3.x` bot version the first time.
|
With the release of `v3.0.0` the bot changed a lot introducing a new storage format. As that storage backend is not compatible with the `v2.x` storage you need to migrate it manually before starting a `v3.x` bot version the first time.
|
||||||
|
|
36
go.mod
36
go.mod
|
@ -8,29 +8,35 @@ require (
|
||||||
github.com/Luzifer/korvike/functions v0.6.1
|
github.com/Luzifer/korvike/functions v0.6.1
|
||||||
github.com/Luzifer/rconfig/v2 v2.4.0
|
github.com/Luzifer/rconfig/v2 v2.4.0
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2
|
github.com/Masterminds/sprig/v3 v3.2.2
|
||||||
github.com/glebarez/go-sqlite v1.18.1
|
github.com/glebarez/sqlite v1.5.0
|
||||||
github.com/go-irc/irc v2.1.0+incompatible
|
github.com/go-irc/irc v2.1.0+incompatible
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible
|
github.com/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/gofrs/uuid/v3 v3.1.2
|
github.com/gofrs/uuid/v3 v3.1.2
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/itchyny/gojq v0.12.9
|
github.com/itchyny/gojq v0.12.9
|
||||||
github.com/jmoiron/sqlx v1.3.5
|
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb
|
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
gorm.io/driver/mysql v1.4.3
|
||||||
|
gorm.io/driver/postgres v1.4.5
|
||||||
|
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/emirpasic/gods v1.12.0 // indirect
|
github.com/emirpasic/gods v1.12.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.19.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
@ -45,7 +51,17 @@ require (
|
||||||
github.com/huandu/xstrings v1.3.1 // indirect
|
github.com/huandu/xstrings v1.3.1 // indirect
|
||||||
github.com/imdario/mergo v0.3.11 // indirect
|
github.com/imdario/mergo v0.3.11 // indirect
|
||||||
github.com/itchyny/timefmt-go v0.1.4 // indirect
|
github.com/itchyny/timefmt-go v0.1.4 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.13.0 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
|
github.com/jackc/pgtype v1.12.0 // indirect
|
||||||
|
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||||
|
@ -53,6 +69,7 @@ require (
|
||||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/sergi/go-diff v1.0.0 // indirect
|
github.com/sergi/go-diff v1.0.0 // indirect
|
||||||
|
@ -61,16 +78,17 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/src-d/gcfg v1.4.0 // indirect
|
github.com/src-d/gcfg v1.4.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||||
golang.org/x/text v0.3.6 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
|
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
|
||||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
modernc.org/libc v1.16.19 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/mathutil v1.4.1 // indirect
|
modernc.org/libc v1.19.0 // indirect
|
||||||
modernc.org/memory v1.1.1 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/sqlite v1.18.1 // indirect
|
modernc.org/memory v1.4.0 // indirect
|
||||||
|
modernc.org/sqlite v1.19.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
191
go.sum
191
go.sum
|
@ -49,6 +49,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
|
||||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
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/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
|
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
|
||||||
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
|
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
|
||||||
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||||
|
@ -60,6 +62,7 @@ github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH
|
||||||
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
|
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
|
||||||
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -86,8 +89,10 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/glebarez/go-sqlite v1.18.1 h1:w0xtxKWktqYsUsXg//SQK+l1IcpKb3rGOQHmMptvL2U=
|
github.com/glebarez/go-sqlite v1.19.1 h1:o2XhjyR8CQ2m84+bVz10G0cabmG0tY4sIMiCbrcUTrY=
|
||||||
github.com/glebarez/go-sqlite v1.18.1/go.mod h1:ydXIGq2M4OzF4YyNhH129SPp7jWoVvgkEgb6pldmS0s=
|
github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M=
|
||||||
|
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/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
@ -95,11 +100,13 @@ github.com/go-irc/irc v2.1.0+incompatible h1:pg7pMVq5OYQbqTxceByD/EN8VIsba7DtKn4
|
||||||
github.com/go-irc/irc v2.1.0+incompatible/go.mod h1:jJILTRy8s/qOvusiKifAEfhQMVwft1ZwQaVJnnzmyX4=
|
github.com/go-irc/irc v2.1.0+incompatible/go.mod h1:jJILTRy8s/qOvusiKifAEfhQMVwft1ZwQaVJnnzmyX4=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||||
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
|
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
|
||||||
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
@ -108,6 +115,7 @@ github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr6
|
||||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY=
|
github.com/gofrs/uuid/v3 v3.1.2 h1:V3IBv1oU82x6YIr5txe3azVHgmOKYdyKQTowm9moBlY=
|
||||||
|
@ -141,6 +149,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
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/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
@ -212,13 +221,63 @@ github.com/itchyny/gojq v0.12.9 h1:biKpbKwMxVYhCU1d6mR7qMr3f0Hn9F5k5YykCVb3gmM=
|
||||||
github.com/itchyny/gojq v0.12.9/go.mod h1:T4Ip7AETUXeGpD+436m+UEl3m3tokRgajd5pRfsR5oE=
|
github.com/itchyny/gojq v0.12.9/go.mod h1:T4Ip7AETUXeGpD+436m+UEl3m3tokRgajd5pRfsR5oE=
|
||||||
github.com/itchyny/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM=
|
github.com/itchyny/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM=
|
||||||
github.com/itchyny/timefmt-go v0.1.4/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
github.com/itchyny/timefmt-go v0.1.4/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||||
|
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=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||||
|
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||||
|
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
||||||
|
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||||
|
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||||
|
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
|
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||||
|
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||||
|
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
|
||||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
@ -229,6 +288,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
@ -240,22 +300,25 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA=
|
github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA=
|
||||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
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-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.6/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 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||||
|
@ -334,11 +397,17 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/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/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
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/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
@ -360,12 +429,15 @@ github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jW
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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.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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
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.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/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb h1:G0Rrif8QdbAz7Xy53H4Xumy6TuyKHom8pu8z/jdLwwM=
|
github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb h1:G0Rrif8QdbAz7Xy53H4Xumy6TuyKHom8pu8z/jdLwwM=
|
||||||
|
@ -373,24 +445,44 @@ github.com/wzshiming/openapi v0.0.0-20200703171632-c7220b3c9cfb/go.mod h1:398xiA
|
||||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
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.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
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=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
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-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||||
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -411,8 +503,10 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -436,6 +530,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -451,20 +546,24 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
|
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
|
||||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/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 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
||||||
|
@ -475,13 +574,20 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
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-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.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
@ -521,8 +627,10 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||||
|
@ -550,34 +658,49 @@ 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-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
||||||
|
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||||
|
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
|
||||||
|
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
|
||||||
|
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
|
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
|
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755 h1:7AdrbfcvKnzejfqP5g37fdSZOXH/JvaPIzBIHTOqXKk=
|
||||||
|
gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
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=
|
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/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
|
||||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
|
||||||
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/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
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.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||||
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
|
||||||
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
|
||||||
modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
modernc.org/libc v1.19.0 h1:bXyVhGQg6KIClTr8FMVIDPl7jtbcs7aS5WP7vLDaxPs=
|
||||||
modernc.org/libc v1.16.19 h1:S8flPn5ZeXx6iw/8yNa986hwTQDrY8RXU7tObZuAozo=
|
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||||
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
|
||||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
|
||||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||||
|
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
|
||||||
|
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
|
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.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||||
|
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.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||||
|
|
|
@ -24,7 +24,7 @@ var (
|
||||||
//nolint:funlen // This function is a few lines too long but only contains definitions
|
//nolint:funlen // This function is a few lines too long but only contains definitions
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
db = args.GetDatabaseConnector()
|
db = args.GetDatabaseConnector()
|
||||||
if err := db.Migrate("counter", database.NewEmbedFSMigrator(schema, "schema")); err != nil {
|
if err := db.DB().AutoMigrate(&counter{}); err != nil {
|
||||||
return errors.Wrap(err, "applying schema migration")
|
return errors.Wrap(err, "applying schema migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
})
|
})
|
||||||
|
|
||||||
args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) {
|
args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) {
|
||||||
return getCounterValue(name)
|
return GetCounterValue(db, name)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -160,7 +160,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
updateCounter(counterName, counterValue, true),
|
UpdateCounter(db, counterName, counterValue, true),
|
||||||
"set counter",
|
"set counter",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
updateCounter(counterName, counterStep, false),
|
UpdateCounter(db, counterName, counterStep, false),
|
||||||
"update counter",
|
"update counter",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
template = "%d"
|
template = "%d"
|
||||||
}
|
}
|
||||||
|
|
||||||
cv, err := getCounterValue(mux.Vars(r)["name"])
|
cv, err := GetCounterValue(db, mux.Vars(r)["name"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -223,7 +223,7 @@ func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = updateCounter(mux.Vars(r)["name"], value, absolute); err != nil {
|
if err = UpdateCounter(db, mux.Vars(r)["name"], value, absolute); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
package counter
|
package counter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"embed"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema/**
|
type (
|
||||||
var schema embed.FS
|
counter struct {
|
||||||
|
Name string `gorm:"primaryKey"`
|
||||||
func getCounterValue(counter string) (int64, error) {
|
Value int64
|
||||||
row := db.DB().QueryRow(
|
}
|
||||||
`SELECT value
|
|
||||||
FROM counters
|
|
||||||
WHERE name = $1`,
|
|
||||||
counter,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cv int64
|
func GetCounterValue(db database.Connector, counterName string) (int64, error) {
|
||||||
err := row.Scan(&cv)
|
var c counter
|
||||||
|
|
||||||
|
err := db.DB().First(&c, "name = ?", counterName).Error
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return cv, nil
|
return c.Value, nil
|
||||||
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -32,9 +31,9 @@ func getCounterValue(counter string) (int64, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCounter(counter string, value int64, absolute bool) error {
|
func UpdateCounter(db database.Connector, counterName string, value int64, absolute bool) error {
|
||||||
if !absolute {
|
if !absolute {
|
||||||
cv, err := getCounterValue(counter)
|
cv, err := GetCounterValue(db, counterName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "getting previous value")
|
return errors.Wrap(err, "getting previous value")
|
||||||
}
|
}
|
||||||
|
@ -42,14 +41,11 @@ func updateCounter(counter string, value int64, absolute bool) error {
|
||||||
value += cv
|
value += cv
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.DB().Exec(
|
return errors.Wrap(
|
||||||
`INSERT INTO counters
|
db.DB().Clauses(clause.OnConflict{
|
||||||
(name, value)
|
Columns: []clause.Column{{Name: "name"}},
|
||||||
VALUES ($1, $2)
|
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||||
ON CONFLICT DO UPDATE
|
}).Create(counter{Name: counterName, Value: value}).Error,
|
||||||
SET value = excluded.value;`,
|
"storing counter value",
|
||||||
counter, value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "storing counter value")
|
|
||||||
}
|
}
|
||||||
|
|
30
internal/actors/counter/database_test.go
Normal file
30
internal/actors/counter/database_test.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package counter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCounterStoreLoop(t *testing.T) {
|
||||||
|
dbc := database.GetTestDatabase(t)
|
||||||
|
dbc.DB().AutoMigrate(&counter{})
|
||||||
|
|
||||||
|
counterName := "mytestcounter"
|
||||||
|
|
||||||
|
v, err := GetCounterValue(dbc, counterName)
|
||||||
|
assert.NoError(t, err, "reading non-existent counter")
|
||||||
|
assert.Equal(t, int64(0), v, "expecting 0 counter value on non-existent counter")
|
||||||
|
|
||||||
|
err = UpdateCounter(dbc, counterName, 5, true)
|
||||||
|
assert.NoError(t, err, "inserting counter")
|
||||||
|
|
||||||
|
err = UpdateCounter(dbc, counterName, 1, false)
|
||||||
|
assert.NoError(t, err, "updating counter")
|
||||||
|
|
||||||
|
v, err = GetCounterValue(dbc, counterName)
|
||||||
|
assert.NoError(t, err, "reading existent counter")
|
||||||
|
assert.Equal(t, int64(6), v, "expecting counter value on existing counter")
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
CREATE TABLE counters (
|
|
||||||
name STRING NOT NULL PRIMARY KEY,
|
|
||||||
value INTEGER
|
|
||||||
);
|
|
|
@ -30,7 +30,7 @@ var (
|
||||||
|
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
db = args.GetDatabaseConnector()
|
db = args.GetDatabaseConnector()
|
||||||
if err := db.Migrate("punish", database.NewEmbedFSMigrator(schema, "schema")); err != nil {
|
if err := db.DB().AutoMigrate(&punishLevel{}); err != nil {
|
||||||
return errors.Wrap(err, "applying schema migration")
|
return errors.Wrap(err, "applying schema migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
||||||
return false, errors.Wrap(err, "preparing user")
|
return false, errors.Wrap(err, "preparing user")
|
||||||
}
|
}
|
||||||
|
|
||||||
lvl, err := getPunishment(plugins.DeriveChannel(m, eventData), user, uuid)
|
lvl, err := getPunishment(db, plugins.DeriveChannel(m, eventData), user, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "getting stored punishment")
|
return false, errors.Wrap(err, "getting stored punishment")
|
||||||
}
|
}
|
||||||
|
@ -199,11 +199,11 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
|
||||||
}
|
}
|
||||||
|
|
||||||
lvl.Cooldown = cooldown
|
lvl.Cooldown = cooldown
|
||||||
lvl.Executed = time.Now()
|
lvl.Executed = time.Now().UTC()
|
||||||
lvl.LastLevel = nLvl
|
lvl.LastLevel = nLvl
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
setPunishment(plugins.DeriveChannel(m, eventData), user, uuid, lvl),
|
setPunishment(db, plugins.DeriveChannel(m, eventData), user, uuid, lvl),
|
||||||
"storing punishment level",
|
"storing punishment level",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
deletePunishment(plugins.DeriveChannel(m, eventData), user, uuid),
|
deletePunishment(db, plugins.DeriveChannel(m, eventData), user, uuid),
|
||||||
"resetting punishment level",
|
"resetting punishment level",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,45 @@
|
||||||
package punish
|
package punish
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"embed"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema/**
|
type (
|
||||||
var schema embed.FS
|
punishLevel struct {
|
||||||
|
Key string `gorm:"primaryKey"`
|
||||||
|
|
||||||
func calculateCurrentPunishments() error {
|
LastLevel int
|
||||||
rows, err := db.DB().Query(
|
Executed time.Time
|
||||||
`SELECT key, last_level, executed, cooldown
|
Cooldown time.Duration
|
||||||
FROM punish_levels;`,
|
}
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
|
func calculateCurrentPunishments(db database.Connector) (err error) {
|
||||||
|
var ps []punishLevel
|
||||||
|
if err = db.DB().Find(&ps).Error; err != nil {
|
||||||
return errors.Wrap(err, "querying punish_levels")
|
return errors.Wrap(err, "querying punish_levels")
|
||||||
}
|
}
|
||||||
|
|
||||||
for rows.Next() {
|
for _, p := range ps {
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return errors.Wrap(err, "advancing rows")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key string
|
|
||||||
lastLevel, executed, cooldown int64
|
|
||||||
|
|
||||||
actUpdate bool
|
actUpdate bool
|
||||||
|
lvl = &levelConfig{
|
||||||
|
LastLevel: p.LastLevel,
|
||||||
|
Executed: p.Executed,
|
||||||
|
Cooldown: p.Cooldown,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
if err = rows.Scan(&key, &lastLevel, &executed, &cooldown); err != nil {
|
|
||||||
return errors.Wrap(err, "advancing rows")
|
|
||||||
}
|
|
||||||
|
|
||||||
lvl := &levelConfig{
|
|
||||||
LastLevel: int(lastLevel),
|
|
||||||
Cooldown: time.Duration(cooldown),
|
|
||||||
Executed: time.Unix(executed, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
cooldownTime := lvl.Executed.Add(lvl.Cooldown)
|
cooldownTime := lvl.Executed.Add(lvl.Cooldown)
|
||||||
if cooldownTime.After(time.Now()) {
|
if cooldownTime.After(time.Now().UTC()) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,61 +50,53 @@ func calculateCurrentPunishments() error {
|
||||||
|
|
||||||
// Level 0 is the first punishment level, so only remove if it drops below 0
|
// Level 0 is the first punishment level, so only remove if it drops below 0
|
||||||
if lvl.LastLevel < 0 {
|
if lvl.LastLevel < 0 {
|
||||||
if err = deletePunishmentForKey(key); err != nil {
|
if err = deletePunishmentForKey(db, p.Key); err != nil {
|
||||||
return errors.Wrap(err, "cleaning up expired punishment")
|
return errors.Wrap(err, "cleaning up expired punishment")
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if actUpdate {
|
if actUpdate {
|
||||||
if err = setPunishmentForKey(key, lvl); err != nil {
|
if err = setPunishmentForKey(db, p.Key, lvl); err != nil {
|
||||||
return errors.Wrap(err, "updating punishment")
|
return errors.Wrap(err, "updating punishment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(rows.Err(), "finishing rows processing")
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deletePunishment(channel, user, uuid string) error {
|
func deletePunishment(db database.Connector, channel, user, uuid string) error {
|
||||||
return deletePunishmentForKey(getDBKey(channel, user, uuid))
|
return deletePunishmentForKey(db, getDBKey(channel, user, uuid))
|
||||||
}
|
}
|
||||||
|
|
||||||
func deletePunishmentForKey(key string) error {
|
func deletePunishmentForKey(db database.Connector, key string) error {
|
||||||
_, err := db.DB().Exec(
|
return errors.Wrap(
|
||||||
`DELETE FROM punish_levels
|
db.DB().Delete(&punishLevel{}, "key = ?", key).Error,
|
||||||
WHERE key = $1;`,
|
"deleting punishment info",
|
||||||
key,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "deleting punishment info")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPunishment(channel, user, uuid string) (*levelConfig, error) {
|
func getPunishment(db database.Connector, channel, user, uuid string) (*levelConfig, error) {
|
||||||
if err := calculateCurrentPunishments(); err != nil {
|
if err := calculateCurrentPunishments(db); err != nil {
|
||||||
return nil, errors.Wrap(err, "updating punishment states")
|
return nil, errors.Wrap(err, "updating punishment states")
|
||||||
}
|
}
|
||||||
|
|
||||||
row := db.DB().QueryRow(
|
var (
|
||||||
`SELECT last_level, executed, cooldown
|
lc = &levelConfig{LastLevel: -1}
|
||||||
FROM punish_levels
|
p punishLevel
|
||||||
WHERE key = $1;`,
|
|
||||||
getDBKey(channel, user, uuid),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lc := &levelConfig{LastLevel: -1}
|
err := db.DB().First(&p, "key = ?", getDBKey(channel, user, uuid)).Error
|
||||||
|
|
||||||
var lastLevel, executed, cooldown int64
|
|
||||||
err := row.Scan(&lastLevel, &executed, &cooldown)
|
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
lc.LastLevel = int(lastLevel)
|
return &levelConfig{
|
||||||
lc.Cooldown = time.Duration(cooldown)
|
LastLevel: p.LastLevel,
|
||||||
lc.Executed = time.Unix(executed, 0)
|
Executed: p.Executed,
|
||||||
|
Cooldown: p.Cooldown,
|
||||||
|
}, nil
|
||||||
|
|
||||||
return lc, nil
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
|
||||||
return lc, nil
|
return lc, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -117,24 +104,27 @@ func getPunishment(channel, user, uuid string) (*levelConfig, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPunishment(channel, user, uuid string, lc *levelConfig) error {
|
func setPunishment(db database.Connector, channel, user, uuid string, lc *levelConfig) error {
|
||||||
return setPunishmentForKey(getDBKey(channel, user, uuid), lc)
|
return setPunishmentForKey(db, getDBKey(channel, user, uuid), lc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPunishmentForKey(key string, lc *levelConfig) error {
|
func setPunishmentForKey(db database.Connector, key string, lc *levelConfig) error {
|
||||||
_, err := db.DB().Exec(
|
if lc == nil {
|
||||||
`INSERT INTO punish_levels
|
return errors.New("nil levelConfig given")
|
||||||
(key, last_level, executed, cooldown)
|
}
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
ON CONFLICT DO UPDATE
|
|
||||||
SET last_level = excluded.last_level,
|
|
||||||
executed = excluded.executed,
|
|
||||||
cooldown = excluded.cooldown;`,
|
|
||||||
key,
|
|
||||||
lc.LastLevel, lc.Executed.UTC().Unix(), int64(lc.Cooldown),
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors.Wrap(err, "updating punishment info")
|
return errors.Wrap(
|
||||||
|
db.DB().Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "key"}},
|
||||||
|
UpdateAll: true,
|
||||||
|
}).Create(punishLevel{
|
||||||
|
Key: key,
|
||||||
|
LastLevel: lc.LastLevel,
|
||||||
|
Executed: lc.Executed,
|
||||||
|
Cooldown: lc.Cooldown,
|
||||||
|
}).Error,
|
||||||
|
"updating punishment info",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDBKey(channel, user, uuid string) string {
|
func getDBKey(channel, user, uuid string) string {
|
||||||
|
|
55
internal/actors/punish/database_test.go
Normal file
55
internal/actors/punish/database_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package punish
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPunishmentRoundtrip(t *testing.T) {
|
||||||
|
dbc := database.GetTestDatabase(t)
|
||||||
|
require.NoError(t, dbc.DB().AutoMigrate(&punishLevel{}))
|
||||||
|
|
||||||
|
var (
|
||||||
|
channel = "#test"
|
||||||
|
user = "test"
|
||||||
|
uuid = "1befb33d-be89-4724-8ae1-0d465eb58007"
|
||||||
|
)
|
||||||
|
|
||||||
|
pl, err := getPunishment(dbc, channel, user, uuid)
|
||||||
|
assert.NoError(t, err, "query non-existent punishment")
|
||||||
|
assert.Equal(t, -1, pl.LastLevel, "check default level")
|
||||||
|
assert.Zero(t, pl.Executed, "check default time")
|
||||||
|
assert.Zero(t, pl.Cooldown, "check default cooldown")
|
||||||
|
|
||||||
|
err = setPunishment(dbc, channel, user, uuid, &levelConfig{
|
||||||
|
Cooldown: 500 * time.Millisecond,
|
||||||
|
Executed: time.Now().UTC(),
|
||||||
|
LastLevel: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err, "setting punishment")
|
||||||
|
|
||||||
|
pl, err = getPunishment(dbc, channel, user, uuid)
|
||||||
|
assert.NoError(t, err, "query existent punishment")
|
||||||
|
assert.Equal(t, 1, pl.LastLevel, "check level without cooldown")
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond) // Wait for one cooldown to happen
|
||||||
|
|
||||||
|
pl, err = getPunishment(dbc, channel, user, uuid)
|
||||||
|
assert.NoError(t, err, "query existent punishment")
|
||||||
|
assert.Equal(t, 0, pl.LastLevel, "check level after one cooldown")
|
||||||
|
assert.NotZero(t, pl.Executed, "check non-zero-time after one cooldown")
|
||||||
|
assert.Equal(t, 500*time.Millisecond, pl.Cooldown, "check non-zero-cooldown after one cooldown")
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond) // Wait for one cooldown to happen
|
||||||
|
|
||||||
|
pl, err = getPunishment(dbc, channel, user, uuid)
|
||||||
|
assert.NoError(t, err, "query existent punishment")
|
||||||
|
assert.Equal(t, -1, pl.LastLevel, "check level after two cooldown")
|
||||||
|
assert.Zero(t, pl.Executed, "check zero-time after two cooldown")
|
||||||
|
assert.Zero(t, pl.Cooldown, "check zero-cooldown after two cooldown")
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
CREATE TABLE punish_levels (
|
|
||||||
key STRING NOT NULL PRIMARY KEY,
|
|
||||||
last_level INTEGER,
|
|
||||||
executed INTEGER, -- time.Time
|
|
||||||
cooldown INTEGER -- time.Duration
|
|
||||||
);
|
|
|
@ -26,7 +26,7 @@ var (
|
||||||
|
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
db = args.GetDatabaseConnector()
|
db = args.GetDatabaseConnector()
|
||||||
if err := db.Migrate(actorName, database.NewEmbedFSMigrator(schema, "schema")); err != nil {
|
if err := db.DB().AutoMigrate("e{}); err != nil {
|
||||||
return errors.Wrap(err, "applying schema migration")
|
return errors.Wrap(err, "applying schema migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
|
|
||||||
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
|
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
|
||||||
return func() (int, error) {
|
return func() (int, error) {
|
||||||
return getMaxQuoteIdx(plugins.DeriveChannel(m, nil))
|
return GetMaxQuoteIdx(db, plugins.DeriveChannel(m, nil))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -122,18 +122,18 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
addQuote(plugins.DeriveChannel(m, eventData), quote),
|
AddQuote(db, plugins.DeriveChannel(m, eventData), quote),
|
||||||
"adding quote",
|
"adding quote",
|
||||||
)
|
)
|
||||||
|
|
||||||
case "del":
|
case "del":
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
delQuote(plugins.DeriveChannel(m, eventData), index),
|
DelQuote(db, plugins.DeriveChannel(m, eventData), index),
|
||||||
"storing quote database",
|
"storing quote database",
|
||||||
)
|
)
|
||||||
|
|
||||||
case "get":
|
case "get":
|
||||||
idx, quote, err := getQuote(plugins.DeriveChannel(m, eventData), index)
|
idx, quote, err := GetQuote(db, plugins.DeriveChannel(m, eventData), index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "getting quote")
|
return false, errors.Wrap(err, "getting quote")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,120 +1,99 @@
|
||||||
package quotedb
|
package quotedb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"embed"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema/**
|
type (
|
||||||
var schema embed.FS
|
quote struct {
|
||||||
|
Channel string `gorm:"not null;uniqueIndex:ensure_sort_idx;size:32"`
|
||||||
func addQuote(channel, quote string) error {
|
CreatedAt int64 `gorm:"uniqueIndex:ensure_sort_idx"`
|
||||||
_, err := db.DB().Exec(
|
Quote string
|
||||||
`INSERT INTO quotedb
|
}
|
||||||
(channel, created_at, quote)
|
|
||||||
VALUES ($1, $2, $3);`,
|
|
||||||
channel, time.Now().UnixNano(), quote,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "adding quote to database")
|
func AddQuote(db database.Connector, channel, quoteStr string) error {
|
||||||
|
return errors.Wrap(
|
||||||
|
db.DB().Create(quote{
|
||||||
|
Channel: channel,
|
||||||
|
CreatedAt: time.Now().UnixNano(),
|
||||||
|
Quote: quoteStr,
|
||||||
|
}).Error,
|
||||||
|
"adding quote to database",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func delQuote(channel string, quote int) error {
|
func DelQuote(db database.Connector, channel string, quoteIdx int) error {
|
||||||
_, createdAt, _, err := getQuoteRaw(channel, quote)
|
_, createdAt, _, err := GetQuoteRaw(db, channel, quoteIdx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "fetching specified quote")
|
return errors.Wrap(err, "fetching specified quote")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.DB().Exec(
|
return errors.Wrap(
|
||||||
`DELETE FROM quotedb
|
db.DB().Delete("e{}, "channel = ? AND created_at = ?", channel, createdAt).Error,
|
||||||
WHERE channel = $1 AND created_at = $2;`,
|
"deleting quote",
|
||||||
channel, createdAt,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "deleting quote")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChannelQuotes(channel string) ([]string, error) {
|
func GetChannelQuotes(db database.Connector, channel string) ([]string, error) {
|
||||||
rows, err := db.DB().Query(
|
var qs []quote
|
||||||
`SELECT quote
|
if err := db.DB().Where("channel = ?", channel).Order("created_at").Find(&qs).Error; err != nil {
|
||||||
FROM quotedb
|
|
||||||
WHERE channel = $1
|
|
||||||
ORDER BY created_at ASC`,
|
|
||||||
channel,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "querying quotes")
|
return nil, errors.Wrap(err, "querying quotes")
|
||||||
}
|
}
|
||||||
|
|
||||||
var quotes []string
|
var quotes []string
|
||||||
for rows.Next() {
|
for _, q := range qs {
|
||||||
if err = rows.Err(); err != nil {
|
quotes = append(quotes, q.Quote)
|
||||||
return nil, errors.Wrap(err, "advancing row read")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var quote string
|
return quotes, nil
|
||||||
if err = rows.Scan("e); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "scanning row")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quotes = append(quotes, quote)
|
func GetMaxQuoteIdx(db database.Connector, channel string) (int, error) {
|
||||||
|
var count int64
|
||||||
|
if err := db.DB().
|
||||||
|
Model("e{}).
|
||||||
|
Where("channel = ?", channel).
|
||||||
|
Count(&count).
|
||||||
|
Error; err != nil {
|
||||||
|
return 0, errors.Wrap(err, "getting quote count")
|
||||||
}
|
}
|
||||||
|
|
||||||
return quotes, errors.Wrap(rows.Err(), "advancing row read")
|
return int(count), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMaxQuoteIdx(channel string) (int, error) {
|
func GetQuote(db database.Connector, channel string, quote int) (int, string, error) {
|
||||||
row := db.DB().QueryRow(
|
quoteIdx, _, quoteText, err := GetQuoteRaw(db, channel, quote)
|
||||||
`SELECT COUNT(1) as quoteCount
|
|
||||||
FROM quotedb
|
|
||||||
WHERE channel = $1;`,
|
|
||||||
channel,
|
|
||||||
)
|
|
||||||
|
|
||||||
var count int
|
|
||||||
err := row.Scan(&count)
|
|
||||||
|
|
||||||
return count, errors.Wrap(err, "getting quote count")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getQuote(channel string, quote int) (int, string, error) {
|
|
||||||
quoteIdx, _, quoteText, err := getQuoteRaw(channel, quote)
|
|
||||||
return quoteIdx, quoteText, err
|
return quoteIdx, quoteText, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQuoteRaw(channel string, quote int) (int, int64, string, error) {
|
func GetQuoteRaw(db database.Connector, channel string, quoteIdx int) (int, int64, string, error) {
|
||||||
if quote == 0 {
|
if quoteIdx == 0 {
|
||||||
max, err := getMaxQuoteIdx(channel)
|
max, err := GetMaxQuoteIdx(db, channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, "", errors.Wrap(err, "getting max quote idx")
|
return 0, 0, "", errors.Wrap(err, "getting max quote idx")
|
||||||
}
|
}
|
||||||
quote = rand.Intn(max) + 1 // #nosec G404 // no need for cryptographic safety
|
quoteIdx = rand.Intn(max) + 1 // #nosec G404 // no need for cryptographic safety
|
||||||
}
|
}
|
||||||
|
|
||||||
row := db.DB().QueryRow(
|
var q quote
|
||||||
`SELECT created_at, quote
|
err := db.DB().
|
||||||
FROM quotedb
|
Where("channel = ?", channel).
|
||||||
WHERE channel = $1
|
Limit(1).
|
||||||
ORDER BY created_at ASC
|
Offset(quoteIdx - 1).
|
||||||
LIMIT 1 OFFSET $2`,
|
First(&q).Error
|
||||||
channel, quote-1,
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
createdAt int64
|
|
||||||
quoteText string
|
|
||||||
)
|
|
||||||
|
|
||||||
err := row.Scan(&createdAt, "eText)
|
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return quote, createdAt, quoteText, nil
|
return quoteIdx, q.CreatedAt, q.Quote, nil
|
||||||
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
return 0, 0, "", nil
|
return 0, 0, "", nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -122,52 +101,43 @@ func getQuoteRaw(channel string, quote int) (int, int64, string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setQuotes(channel string, quotes []string) error {
|
func SetQuotes(db database.Connector, channel string, quotes []string) error {
|
||||||
tx, err := db.DB().Begin()
|
return errors.Wrap(
|
||||||
if err != nil {
|
db.DB().Transaction(func(tx *gorm.DB) error {
|
||||||
return errors.Wrap(err, "creating transaction")
|
if err := tx.Where("channel = ?", channel).Delete("e{}).Error; err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = tx.Exec(
|
|
||||||
`DELETE FROM quotedb
|
|
||||||
WHERE channel = $1;`,
|
|
||||||
channel,
|
|
||||||
); err != nil {
|
|
||||||
defer tx.Rollback()
|
|
||||||
return errors.Wrap(err, "deleting quotes for channel")
|
return errors.Wrap(err, "deleting quotes for channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
for _, quote := range quotes {
|
for _, quoteStr := range quotes {
|
||||||
if _, err = tx.Exec(
|
if err := tx.Create(quote{
|
||||||
`INSERT INTO quotedb
|
Channel: channel,
|
||||||
(channel, created_at, quote)
|
CreatedAt: t.UnixNano(),
|
||||||
VALUES ($1, $2, $3);`,
|
Quote: quoteStr,
|
||||||
channel, t.UnixNano(), quote,
|
}).Error; err != nil {
|
||||||
); err != nil {
|
return errors.Wrap(err, "adding quote")
|
||||||
defer tx.Rollback()
|
|
||||||
return errors.Wrap(err, "adding quote for channel")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t = t.Add(time.Nanosecond) // Increase by one ns to adhere to unique index
|
t = t.Add(time.Nanosecond) // Increase by one ns to adhere to unique index
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(tx.Commit(), "committing change")
|
return nil
|
||||||
|
}),
|
||||||
|
"replacing quotes",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateQuote(channel string, idx int, quote string) error {
|
func UpdateQuote(db database.Connector, channel string, idx int, quoteStr string) error {
|
||||||
_, createdAt, _, err := getQuoteRaw(channel, idx)
|
_, createdAt, _, err := GetQuoteRaw(db, channel, idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "fetching specified quote")
|
return errors.Wrap(err, "fetching specified quote")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.DB().Exec(
|
return errors.Wrap(
|
||||||
`UPDATE quotedb
|
db.DB().
|
||||||
SET quote = $3
|
Where("channel = ? AND created_at = ?", channel, createdAt).
|
||||||
WHERE channel = $1
|
Update("quote", quoteStr).
|
||||||
AND created_at = $2;`,
|
Error,
|
||||||
channel, createdAt, quote,
|
"updating quote",
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "updating quote")
|
|
||||||
}
|
}
|
||||||
|
|
61
internal/actors/quotedb/database_test.go
Normal file
61
internal/actors/quotedb/database_test.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package quotedb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuotesRoundtrip(t *testing.T) {
|
||||||
|
dbc := database.GetTestDatabase(t)
|
||||||
|
require.NoError(t, dbc.DB().AutoMigrate("e{}))
|
||||||
|
|
||||||
|
var (
|
||||||
|
channel = "#test"
|
||||||
|
quotes = []string{
|
||||||
|
"I'm a quote",
|
||||||
|
"I might have been said...",
|
||||||
|
"Testing rocks",
|
||||||
|
"Lets add some more",
|
||||||
|
"...or not",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cq, err := GetChannelQuotes(dbc, channel)
|
||||||
|
assert.NoError(t, err, "querying empty database")
|
||||||
|
assert.Zero(t, cq, "expecting no quotes")
|
||||||
|
|
||||||
|
for i, q := range quotes {
|
||||||
|
assert.NoError(t, AddQuote(dbc, channel, q), "adding quote %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
cq, err = GetChannelQuotes(dbc, channel)
|
||||||
|
assert.NoError(t, err, "querying database")
|
||||||
|
assert.Equal(t, quotes, cq, "checkin order and presence of quotes")
|
||||||
|
|
||||||
|
assert.NoError(t, DelQuote(dbc, channel, 1), "removing one quote")
|
||||||
|
assert.NoError(t, DelQuote(dbc, channel, 1), "removing one quote")
|
||||||
|
|
||||||
|
cq, err = GetChannelQuotes(dbc, channel)
|
||||||
|
assert.NoError(t, err, "querying database")
|
||||||
|
assert.Len(t, cq, len(quotes)-2, "expecting quotes in db")
|
||||||
|
|
||||||
|
assert.NoError(t, SetQuotes(dbc, channel, quotes), "replacing quotes")
|
||||||
|
|
||||||
|
cq, err = GetChannelQuotes(dbc, channel)
|
||||||
|
assert.NoError(t, err, "querying database")
|
||||||
|
assert.Equal(t, quotes, cq, "checkin order and presence of quotes")
|
||||||
|
|
||||||
|
idx, q, err := GetQuote(dbc, channel, 0)
|
||||||
|
assert.NoError(t, err, "getting random quote")
|
||||||
|
assert.NotZero(t, idx, "index must not be zero")
|
||||||
|
assert.NotZero(t, q, "quote must not be zero")
|
||||||
|
|
||||||
|
idx, q, err = GetQuote(dbc, channel, 3)
|
||||||
|
assert.NoError(t, err, "getting specific quote")
|
||||||
|
assert.Equal(t, 3, idx, "index must be 3")
|
||||||
|
assert.Equal(t, quotes[2], q, "quote must not the third")
|
||||||
|
}
|
|
@ -133,7 +133,7 @@ func handleAddQuotes(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, q := range quotes {
|
for _, q := range quotes {
|
||||||
if err := addQuote(channel, q); err != nil {
|
if err := AddQuote(db, channel, q); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "adding quote").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "adding quote").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ func handleDeleteQuote(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = delQuote(channel, idx); err != nil {
|
if err = DelQuote(db, channel, idx); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "deleting quote").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "deleting quote").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ func handleListQuotes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
channel := "#" + strings.TrimLeft(mux.Vars(r)["channel"], "#")
|
channel := "#" + strings.TrimLeft(mux.Vars(r)["channel"], "#")
|
||||||
|
|
||||||
quotes, err := getChannelQuotes(channel)
|
quotes, err := GetChannelQuotes(db, channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "getting quotes").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "getting quotes").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -192,7 +192,7 @@ func handleReplaceQuotes(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setQuotes(channel, quotes); err != nil {
|
if err := SetQuotes(db, channel, quotes); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "replacing quotes").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "replacing quotes").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -228,7 +228,7 @@ func handleUpdateQuote(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = updateQuote(channel, idx, quotes[0]); err != nil {
|
if err = UpdateQuote(db, channel, idx, quotes[0]); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "updating quote").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "updating quote").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
CREATE TABLE quotedb (
|
|
||||||
channel STRING NOT NULL,
|
|
||||||
created_at INTEGER,
|
|
||||||
quote STRING NOT NULL,
|
|
||||||
|
|
||||||
UNIQUE(channel, created_at)
|
|
||||||
);
|
|
|
@ -22,7 +22,7 @@ var (
|
||||||
|
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
db = args.GetDatabaseConnector()
|
db = args.GetDatabaseConnector()
|
||||||
if err := db.Migrate("setvariable", database.NewEmbedFSMigrator(schema, "schema")); err != nil {
|
if err := db.DB().AutoMigrate(&variable{}); err != nil {
|
||||||
return errors.Wrap(err, "applying schema migration")
|
return errors.Wrap(err, "applying schema migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
})
|
})
|
||||||
|
|
||||||
args.RegisterTemplateFunction("variable", plugins.GenericTemplateFunctionGetter(func(name string, defVal ...string) (string, error) {
|
args.RegisterTemplateFunction("variable", plugins.GenericTemplateFunctionGetter(func(name string, defVal ...string) (string, error) {
|
||||||
value, err := getVariable(name)
|
value, err := GetVariable(db, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "getting variable")
|
return "", errors.Wrap(err, "getting variable")
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
|
||||||
|
|
||||||
if attrs.MustBool("clear", ptrBoolFalse) {
|
if attrs.MustBool("clear", ptrBoolFalse) {
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
removeVariable(varName),
|
RemoveVariable(db, varName),
|
||||||
"removing variable",
|
"removing variable",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -142,7 +142,7 @@ func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.Wrap(
|
return false, errors.Wrap(
|
||||||
setVariable(varName, value),
|
SetVariable(db, varName, value),
|
||||||
"setting variable",
|
"setting variable",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ func (a ActorSetVariable) Validate(attrs *plugins.FieldCollection) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
|
func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
vc, err := getVariable(mux.Vars(r)["name"])
|
vc, err := GetVariable(db, mux.Vars(r)["name"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -170,7 +170,7 @@ func routeActorSetVarGetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeActorSetVarSetValue(w http.ResponseWriter, r *http.Request) {
|
func routeActorSetVarSetValue(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := setVariable(mux.Vars(r)["name"], r.FormValue("value")); err != nil {
|
if err := SetVariable(db, mux.Vars(r)["name"], r.FormValue("value")); err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "updating value").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,28 @@
|
||||||
package variables
|
package variables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"embed"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema/**
|
type (
|
||||||
var schema embed.FS
|
variable struct {
|
||||||
|
Name string `gorm:"primaryKey"`
|
||||||
func getVariable(key string) (string, error) {
|
Value string
|
||||||
row := db.DB().QueryRow(
|
}
|
||||||
`SELECT value
|
|
||||||
FROM variables
|
|
||||||
WHERE name = $1`,
|
|
||||||
key,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var vc string
|
func GetVariable(db database.Connector, key string) (string, error) {
|
||||||
err := row.Scan(&vc)
|
var v variable
|
||||||
|
err := db.DB().First(&v, "name = ?", key).Error
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return vc, nil
|
return v.Value, nil
|
||||||
|
|
||||||
case errors.Is(err, sql.ErrNoRows):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
return "", nil // Compatibility to old behavior
|
return "", nil // Compatibility to old behavior
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -32,25 +30,19 @@ func getVariable(key string) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setVariable(key, value string) error {
|
func SetVariable(db database.Connector, key, value string) error {
|
||||||
_, err := db.DB().Exec(
|
return errors.Wrap(
|
||||||
`INSERT INTO variables
|
db.DB().Clauses(clause.OnConflict{
|
||||||
(name, value)
|
Columns: []clause.Column{{Name: "name"}},
|
||||||
VALUES ($1, $2)
|
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||||
ON CONFLICT DO UPDATE
|
}).Create(variable{Name: key, Value: value}).Error,
|
||||||
SET value = excluded.value;`,
|
"updating value in database",
|
||||||
key, value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "updating value in database")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeVariable(key string) error {
|
func RemoveVariable(db database.Connector, key string) error {
|
||||||
_, err := db.DB().Exec(
|
return errors.Wrap(
|
||||||
`DELETE FROM variables
|
db.DB().Delete(&variable{}, "name = ?", key).Error,
|
||||||
WHERE name = $1;`,
|
"deleting value in database",
|
||||||
key,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "deleting value in database")
|
|
||||||
}
|
}
|
||||||
|
|
36
internal/actors/variables/database_test.go
Normal file
36
internal/actors/variables/database_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package variables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVariableRoundtrip(t *testing.T) {
|
||||||
|
dbc := database.GetTestDatabase(t)
|
||||||
|
require.NoError(t, dbc.DB().AutoMigrate(&variable{}), "applying migration")
|
||||||
|
|
||||||
|
var (
|
||||||
|
name = "myvar"
|
||||||
|
testValue = "ee5e4be5-f292-48aa-a177-cb9fd6f4e171"
|
||||||
|
)
|
||||||
|
|
||||||
|
v, err := GetVariable(dbc, name)
|
||||||
|
assert.NoError(t, err, "getting unset variable")
|
||||||
|
assert.Zero(t, v, "checking zero state on unset variable")
|
||||||
|
|
||||||
|
assert.NoError(t, SetVariable(dbc, name, testValue), "setting variable")
|
||||||
|
|
||||||
|
v, err = GetVariable(dbc, name)
|
||||||
|
assert.NoError(t, err, "getting set variable")
|
||||||
|
assert.NotZero(t, v, "checking non-zero state on set variable")
|
||||||
|
|
||||||
|
assert.NoError(t, RemoveVariable(dbc, name), "removing variable")
|
||||||
|
|
||||||
|
v, err = GetVariable(dbc, name)
|
||||||
|
assert.NoError(t, err, "getting removed variable")
|
||||||
|
assert.Zero(t, v, "checking zero state on removed variable")
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
CREATE TABLE variables (
|
|
||||||
name STRING NOT NULL PRIMARY KEY,
|
|
||||||
value STRING
|
|
||||||
);
|
|
|
@ -2,73 +2,63 @@ package overlays
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/plugins"
|
"github.com/Luzifer/twitch-bot/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema/**
|
type (
|
||||||
var schema embed.FS
|
overlaysEvent struct {
|
||||||
|
Channel string `gorm:"not null;index:overlays_events_sort_idx"`
|
||||||
|
CreatedAt time.Time `gorm:"index:overlays_events_sort_idx"`
|
||||||
|
EventType string
|
||||||
|
Fields string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func addEvent(channel string, evt socketMessage) error {
|
func AddChannelEvent(db database.Connector, channel string, evt SocketMessage) error {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
if err := json.NewEncoder(buf).Encode(evt.Fields); err != nil {
|
if err := json.NewEncoder(buf).Encode(evt.Fields); err != nil {
|
||||||
return errors.Wrap(err, "encoding fields")
|
return errors.Wrap(err, "encoding fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.DB().Exec(
|
return errors.Wrap(
|
||||||
`INSERT INTO overlays_events
|
db.DB().Create(overlaysEvent{
|
||||||
(channel, created_at, event_type, fields)
|
Channel: channel,
|
||||||
VALUES ($1, $2, $3, $4);`,
|
CreatedAt: evt.Time.UTC(),
|
||||||
channel, evt.Time.UnixNano(), evt.Type, strings.TrimSpace(buf.String()),
|
EventType: evt.Type,
|
||||||
|
Fields: strings.TrimSpace(buf.String()),
|
||||||
|
}).Error,
|
||||||
|
"storing event to database",
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "storing event to database")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getChannelEvents(channel string) ([]socketMessage, error) {
|
func GetChannelEvents(db database.Connector, channel string) ([]SocketMessage, error) {
|
||||||
rows, err := db.DB().Query(
|
var evts []overlaysEvent
|
||||||
`SELECT created_at, event_type, fields
|
|
||||||
FROM overlays_events
|
if err := db.DB().Where("channel = ?", channel).Order("created_at").Find(&evts).Error; err != nil {
|
||||||
WHERE channel = $1
|
|
||||||
ORDER BY created_at;`,
|
|
||||||
channel,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "querying channel events")
|
return nil, errors.Wrap(err, "querying channel events")
|
||||||
}
|
}
|
||||||
|
|
||||||
var out []socketMessage
|
var out []SocketMessage
|
||||||
for rows.Next() {
|
for _, e := range evts {
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "advancing row read")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
createdAt int64
|
|
||||||
eventType, rawFields string
|
|
||||||
)
|
|
||||||
if err = rows.Scan(&createdAt, &eventType, &rawFields); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "scanning row")
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := new(plugins.FieldCollection)
|
fields := new(plugins.FieldCollection)
|
||||||
if err = json.NewDecoder(strings.NewReader(rawFields)).Decode(fields); err != nil {
|
if err := json.NewDecoder(strings.NewReader(e.Fields)).Decode(fields); err != nil {
|
||||||
return nil, errors.Wrap(err, "decoding fields")
|
return nil, errors.Wrap(err, "decoding fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
out = append(out, socketMessage{
|
out = append(out, SocketMessage{
|
||||||
IsLive: false,
|
IsLive: false,
|
||||||
Time: time.Unix(0, createdAt),
|
Time: e.CreatedAt,
|
||||||
Type: eventType,
|
Type: e.EventType,
|
||||||
Fields: fields,
|
Fields: fields,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, errors.Wrap(rows.Err(), "advancing row read")
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
54
internal/apimodules/overlays/database_test.go
Normal file
54
internal/apimodules/overlays/database_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package overlays
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
"github.com/Luzifer/twitch-bot/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEventDatabaseRoundtrip(t *testing.T) {
|
||||||
|
dbc := database.GetTestDatabase(t)
|
||||||
|
require.NoError(t, dbc.DB().AutoMigrate(&overlaysEvent{}))
|
||||||
|
|
||||||
|
var (
|
||||||
|
channel = "#test"
|
||||||
|
tEvent1 = time.Now()
|
||||||
|
tEvent2 = tEvent1.Add(time.Second)
|
||||||
|
)
|
||||||
|
|
||||||
|
evts, err := GetChannelEvents(dbc, channel)
|
||||||
|
assert.NoError(t, err, "getting events on empty db")
|
||||||
|
assert.Zero(t, evts, "expect no events on empty db")
|
||||||
|
|
||||||
|
assert.NoError(t, AddChannelEvent(dbc, channel, SocketMessage{
|
||||||
|
IsLive: true,
|
||||||
|
Time: tEvent2,
|
||||||
|
Type: "event 2",
|
||||||
|
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
|
}), "adding second event")
|
||||||
|
|
||||||
|
assert.NoError(t, AddChannelEvent(dbc, channel, SocketMessage{
|
||||||
|
IsLive: true,
|
||||||
|
Time: tEvent1,
|
||||||
|
Type: "event 1",
|
||||||
|
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
|
}), "adding first event")
|
||||||
|
|
||||||
|
assert.NoError(t, AddChannelEvent(dbc, "#otherchannel", SocketMessage{
|
||||||
|
IsLive: true,
|
||||||
|
Time: tEvent1,
|
||||||
|
Type: "event",
|
||||||
|
Fields: plugins.FieldCollectionFromData(map[string]any{"foo": "bar"}),
|
||||||
|
}), "adding other channel event")
|
||||||
|
|
||||||
|
evts, err = GetChannelEvents(dbc, channel)
|
||||||
|
assert.NoError(t, err, "getting events")
|
||||||
|
assert.Len(t, evts, 2, "expect 2 events")
|
||||||
|
|
||||||
|
assert.Less(t, evts[0].Time, evts[1].Time, "expect sorting")
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
socketMessage struct {
|
SocketMessage struct {
|
||||||
IsLive bool `json:"is_live"`
|
IsLive bool `json:"is_live"`
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
@ -65,7 +65,7 @@ var (
|
||||||
|
|
||||||
func Register(args plugins.RegistrationArguments) error {
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
db = args.GetDatabaseConnector()
|
db = args.GetDatabaseConnector()
|
||||||
if err := db.Migrate("overlays", database.NewEmbedFSMigrator(schema, "schema")); err != nil {
|
if err := db.DB().AutoMigrate(&overlaysEvent{}); err != nil {
|
||||||
return errors.Wrap(err, "applying schema migration")
|
return errors.Wrap(err, "applying schema migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
addEvent(plugins.DeriveChannel(nil, eventData), socketMessage{
|
AddChannelEvent(db, plugins.DeriveChannel(nil, eventData), SocketMessage{
|
||||||
IsLive: false,
|
IsLive: false,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: event,
|
Type: event,
|
||||||
|
@ -156,7 +156,7 @@ func Register(args plugins.RegistrationArguments) error {
|
||||||
func handleEventsReplay(w http.ResponseWriter, r *http.Request) {
|
func handleEventsReplay(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
channel = mux.Vars(r)["channel"]
|
channel = mux.Vars(r)["channel"]
|
||||||
msgs []socketMessage
|
msgs []SocketMessage
|
||||||
since = time.Time{}
|
since = time.Time{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ func handleEventsReplay(w http.ResponseWriter, r *http.Request) {
|
||||||
since = s
|
since = s
|
||||||
}
|
}
|
||||||
|
|
||||||
events, err := getChannelEvents("#" + strings.TrimLeft(channel, "#"))
|
events, err := GetChannelEvents(db, "#"+strings.TrimLeft(channel, "#"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, errors.Wrap(err, "getting channel events").Error(), http.StatusInternalServerError)
|
http.Error(w, errors.Wrap(err, "getting channel events").Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -210,12 +210,12 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
||||||
connLock = new(sync.Mutex)
|
connLock = new(sync.Mutex)
|
||||||
errC = make(chan error, 1)
|
errC = make(chan error, 1)
|
||||||
isAuthorized bool
|
isAuthorized bool
|
||||||
sendMsgC = make(chan socketMessage, 1)
|
sendMsgC = make(chan SocketMessage, 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register listener
|
// Register listener
|
||||||
unsub := subscribeSocket(func(event string, eventData *plugins.FieldCollection) {
|
unsub := subscribeSocket(func(event string, eventData *plugins.FieldCollection) {
|
||||||
sendMsgC <- socketMessage{
|
sendMsgC <- SocketMessage{
|
||||||
IsLive: true,
|
IsLive: true,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: event,
|
Type: event,
|
||||||
|
@ -269,7 +269,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var recvMsg socketMessage
|
var recvMsg SocketMessage
|
||||||
if err = json.Unmarshal(p, &recvMsg); err != nil {
|
if err = json.Unmarshal(p, &recvMsg); err != nil {
|
||||||
errC <- errors.Wrap(err, "decoding message")
|
errC <- errors.Wrap(err, "decoding message")
|
||||||
return
|
return
|
||||||
|
@ -290,7 +290,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
authTimeout.Stop()
|
authTimeout.Stop()
|
||||||
isAuthorized = true
|
isAuthorized = true
|
||||||
sendMsgC <- socketMessage{
|
sendMsgC <- SocketMessage{
|
||||||
IsLive: true,
|
IsLive: true,
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Type: msgTypeRequestAuth,
|
Type: msgTypeRequestAuth,
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
CREATE TABLE overlays_events (
|
|
||||||
channel STRING NOT NULL,
|
|
||||||
created_at INTEGER,
|
|
||||||
event_type STRING,
|
|
||||||
fields STRING
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX overlays_events_sort_idx
|
|
||||||
ON overlays_events (channel, created_at DESC);
|
|
|
@ -1,10 +1,11 @@
|
||||||
package access
|
package access
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/v2/str"
|
"github.com/Luzifer/go_helpers/v2/str"
|
||||||
"github.com/Luzifer/twitch-bot/pkg/database"
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
@ -26,11 +27,21 @@ type (
|
||||||
TokenUpdateHook func()
|
TokenUpdateHook func()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extendedPermission struct {
|
||||||
|
Channel string `gorm:"primaryKey"`
|
||||||
|
AccessToken string
|
||||||
|
RefreshToken string
|
||||||
|
Scopes string
|
||||||
|
}
|
||||||
|
|
||||||
Service struct{ db database.Connector }
|
Service struct{ db database.Connector }
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(db database.Connector) *Service {
|
func New(db database.Connector) (*Service, error) {
|
||||||
return &Service{db}
|
return &Service{db}, errors.Wrap(
|
||||||
|
db.DB().AutoMigrate(&extendedPermission{}),
|
||||||
|
"migrating database schema",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
||||||
|
@ -59,30 +70,26 @@ func (s Service) GetBotTwitchClient(cfg ClientConfig) (*twitch.Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*twitch.Client, error) {
|
func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*twitch.Client, error) {
|
||||||
var err error
|
var (
|
||||||
row := s.db.DB().QueryRow(
|
err error
|
||||||
`SELECT access_token, refresh_token, scopes
|
perm extendedPermission
|
||||||
FROM extended_permissions
|
|
||||||
WHERE channel = $1`,
|
|
||||||
channel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var accessToken, refreshToken, scopeStr string
|
if err = s.db.DB().First(&perm, "channel = ?", channel).Error; err != nil {
|
||||||
if err = row.Scan(&accessToken, &refreshToken, &scopeStr); err != nil {
|
return nil, errors.Wrap(err, "getting twitch credential from database")
|
||||||
return nil, errors.Wrap(err, "getting twitch credentials from database")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if accessToken, err = s.db.DecryptField(accessToken); err != nil {
|
if perm.AccessToken, err = s.db.DecryptField(perm.AccessToken); err != nil {
|
||||||
return nil, errors.Wrap(err, "decrypting access token")
|
return nil, errors.Wrap(err, "decrypting access token")
|
||||||
}
|
}
|
||||||
|
|
||||||
if refreshToken, err = s.db.DecryptField(refreshToken); err != nil {
|
if perm.RefreshToken, err = s.db.DecryptField(perm.RefreshToken); err != nil {
|
||||||
return nil, errors.Wrap(err, "decrypting refresh token")
|
return nil, errors.Wrap(err, "decrypting refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
scopes := strings.Split(scopeStr, " ")
|
scopes := strings.Split(perm.Scopes, " ")
|
||||||
|
|
||||||
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, accessToken, refreshToken)
|
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, perm.AccessToken, perm.RefreshToken)
|
||||||
tc.SetTokenUpdateHook(func(at, rt string) error {
|
tc.SetTokenUpdateHook(func(at, rt string) error {
|
||||||
return errors.Wrap(s.SetExtendedTwitchCredentials(channel, at, rt, scopes), "updating extended permissions token")
|
return errors.Wrap(s.SetExtendedTwitchCredentials(channel, at, rt, scopes), "updating extended permissions token")
|
||||||
})
|
})
|
||||||
|
@ -91,22 +98,19 @@ func (s Service) GetTwitchClientForChannel(channel string, cfg ClientConfig) (*t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) HasAnyPermissionForChannel(channel string, scopes ...string) (bool, error) {
|
func (s Service) HasAnyPermissionForChannel(channel string, scopes ...string) (bool, error) {
|
||||||
row := s.db.DB().QueryRow(
|
var (
|
||||||
`SELECT scopes
|
err error
|
||||||
FROM extended_permissions
|
perm extendedPermission
|
||||||
WHERE channel = $1`,
|
|
||||||
channel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var scopeStr string
|
if err = s.db.DB().First(&perm, "channel = ?", channel).Error; err != nil {
|
||||||
if err := row.Scan(&scopeStr); err != nil {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, errors.Wrap(err, "getting scopes from database")
|
return false, errors.Wrap(err, "getting twitch credential from database")
|
||||||
}
|
}
|
||||||
|
|
||||||
storedScopes := strings.Split(scopeStr, " ")
|
storedScopes := strings.Split(perm.Scopes, " ")
|
||||||
|
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
if str.StringInSlice(scope, storedScopes) {
|
if str.StringInSlice(scope, storedScopes) {
|
||||||
|
@ -118,22 +122,19 @@ func (s Service) HasAnyPermissionForChannel(channel string, scopes ...string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) HasPermissionsForChannel(channel string, scopes ...string) (bool, error) {
|
func (s Service) HasPermissionsForChannel(channel string, scopes ...string) (bool, error) {
|
||||||
row := s.db.DB().QueryRow(
|
var (
|
||||||
`SELECT scopes
|
err error
|
||||||
FROM extended_permissions
|
perm extendedPermission
|
||||||
WHERE channel = $1`,
|
|
||||||
channel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var scopeStr string
|
if err = s.db.DB().First(&perm, "channel = ?", channel).Error; err != nil {
|
||||||
if err := row.Scan(&scopeStr); err != nil {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, errors.Wrap(err, "getting scopes from database")
|
return false, errors.Wrap(err, "getting twitch credential from database")
|
||||||
}
|
}
|
||||||
|
|
||||||
storedScopes := strings.Split(scopeStr, " ")
|
storedScopes := strings.Split(perm.Scopes, " ")
|
||||||
|
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
if !str.StringInSlice(scope, storedScopes) {
|
if !str.StringInSlice(scope, storedScopes) {
|
||||||
|
@ -145,13 +146,10 @@ func (s Service) HasPermissionsForChannel(channel string, scopes ...string) (boo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) RemoveExendedTwitchCredentials(channel string) error {
|
func (s Service) RemoveExendedTwitchCredentials(channel string) error {
|
||||||
_, err := s.db.DB().Exec(
|
return errors.Wrap(
|
||||||
`DELETE FROM extended_permissions
|
s.db.DB().Delete(&extendedPermission{}, "channel = ?", channel).Error,
|
||||||
WHERE channel = $1`,
|
"deleting data from table",
|
||||||
channel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "deleting data from table")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) SetBotTwitchCredentials(accessToken, refreshToken string) (err error) {
|
func (s Service) SetBotTwitchCredentials(accessToken, refreshToken string) (err error) {
|
||||||
|
@ -175,16 +173,16 @@ func (s Service) SetExtendedTwitchCredentials(channel, accessToken, refreshToken
|
||||||
return errors.Wrap(err, "encrypting refresh token")
|
return errors.Wrap(err, "encrypting refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.db.DB().Exec(
|
return errors.Wrap(
|
||||||
`INSERT INTO extended_permissions
|
s.db.DB().Clauses(clause.OnConflict{
|
||||||
(channel, access_token, refresh_token, scopes)
|
Columns: []clause.Column{{Name: "channel"}},
|
||||||
VALUES ($1, $2, $3, $4)
|
DoUpdates: clause.AssignmentColumns([]string{"access_token", "refresh_token", "scopes"}),
|
||||||
ON CONFLICT DO UPDATE SET
|
}).Create(extendedPermission{
|
||||||
access_token=excluded.access_token,
|
Channel: channel,
|
||||||
refresh_token=excluded.refresh_token,
|
AccessToken: accessToken,
|
||||||
scopes=excluded.scopes;`,
|
RefreshToken: refreshToken,
|
||||||
channel, accessToken, refreshToken, strings.Join(scope, " "),
|
Scopes: strings.Join(scope, " "),
|
||||||
|
}).Error,
|
||||||
|
"inserting data into table",
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "inserting data into table")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
CREATE TABLE timers (
|
|
||||||
id STRING NOT NULL PRIMARY KEY,
|
|
||||||
expires_at INTEGER
|
|
||||||
);
|
|
|
@ -2,12 +2,13 @@ package timer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
"github.com/Luzifer/twitch-bot/pkg/database"
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/plugins"
|
"github.com/Luzifer/twitch-bot/plugins"
|
||||||
|
@ -18,21 +19,21 @@ type (
|
||||||
db database.Connector
|
db database.Connector
|
||||||
permitTimeout time.Duration
|
permitTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timer struct {
|
||||||
|
ID string `gorm:"primaryKey"`
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var _ plugins.TimerStore = (*Service)(nil)
|
||||||
_ plugins.TimerStore = (*Service)(nil)
|
|
||||||
|
|
||||||
//go:embed schema/**
|
|
||||||
schema embed.FS
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(db database.Connector) (*Service, error) {
|
func New(db database.Connector) (*Service, error) {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, errors.Wrap(s.db.Migrate("timersvc", database.NewEmbedFSMigrator(schema, "schema")), "applying migrations")
|
return s, errors.Wrap(s.db.DB().AutoMigrate(&timer{}), "applying migrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdatePermitTimeout(d time.Duration) {
|
func (s *Service) UpdatePermitTimeout(d time.Duration) {
|
||||||
|
@ -42,11 +43,11 @@ func (s *Service) UpdatePermitTimeout(d time.Duration) {
|
||||||
// Cooldown timer
|
// Cooldown timer
|
||||||
|
|
||||||
func (s Service) AddCooldown(tt plugins.TimerType, limiter, ruleID string, expiry time.Time) error {
|
func (s Service) AddCooldown(tt plugins.TimerType, limiter, ruleID string, expiry time.Time) error {
|
||||||
return s.setTimer(s.getCooldownTimerKey(tt, limiter, ruleID), expiry)
|
return s.SetTimer(s.getCooldownTimerKey(tt, limiter, ruleID), expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) InCooldown(tt plugins.TimerType, limiter, ruleID string) (bool, error) {
|
func (s Service) InCooldown(tt plugins.TimerType, limiter, ruleID string) (bool, error) {
|
||||||
return s.hasTimer(s.getCooldownTimerKey(tt, limiter, ruleID))
|
return s.HasTimer(s.getCooldownTimerKey(tt, limiter, ruleID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string) string {
|
func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string) string {
|
||||||
|
@ -58,11 +59,11 @@ func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string)
|
||||||
// Permit timer
|
// Permit timer
|
||||||
|
|
||||||
func (s Service) AddPermit(channel, username string) error {
|
func (s Service) AddPermit(channel, username string) error {
|
||||||
return s.setTimer(s.getPermitTimerKey(channel, username), time.Now().Add(s.permitTimeout))
|
return s.SetTimer(s.getPermitTimerKey(channel, username), time.Now().Add(s.permitTimeout))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) HasPermit(channel, username string) (bool, error) {
|
func (s Service) HasPermit(channel, username string) (bool, error) {
|
||||||
return s.hasTimer(s.getPermitTimerKey(channel, username))
|
return s.HasTimer(s.getPermitTimerKey(channel, username))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Service) getPermitTimerKey(channel, username string) string {
|
func (Service) getPermitTimerKey(channel, username string) string {
|
||||||
|
@ -73,31 +74,30 @@ func (Service) getPermitTimerKey(channel, username string) string {
|
||||||
|
|
||||||
// Generic timer
|
// Generic timer
|
||||||
|
|
||||||
func (s Service) hasTimer(id string) (bool, error) {
|
func (s Service) HasTimer(id string) (bool, error) {
|
||||||
row := s.db.DB().QueryRow(
|
var t timer
|
||||||
`SELECT COUNT(1) as active_counters
|
err := s.db.DB().First(&t, "id = ? AND expires_at >= ?", id, time.Now().UTC()).Error
|
||||||
FROM timers
|
switch {
|
||||||
WHERE id = $1 AND expires_at >= $2`,
|
case err == nil:
|
||||||
id, time.Now().UTC().Unix(),
|
return true, nil
|
||||||
|
|
||||||
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false, errors.Wrap(err, "getting timer information")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) SetTimer(id string, expiry time.Time) error {
|
||||||
|
return errors.Wrap(
|
||||||
|
s.db.DB().Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"expires_at"}),
|
||||||
|
}).Create(timer{
|
||||||
|
ID: id,
|
||||||
|
ExpiresAt: expiry.UTC(),
|
||||||
|
}).Error,
|
||||||
|
"storing counter in database",
|
||||||
)
|
)
|
||||||
|
|
||||||
var nCounters int64
|
|
||||||
if err := row.Scan(&nCounters); err != nil {
|
|
||||||
return false, errors.Wrap(err, "getting active counters from database")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nCounters > 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Service) setTimer(id string, expiry time.Time) error {
|
|
||||||
_, err := s.db.DB().Exec(
|
|
||||||
`INSERT INTO timers
|
|
||||||
(id, expires_at)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT DO UPDATE
|
|
||||||
SET expires_at = excluded.expires_at;`,
|
|
||||||
id, expiry.UTC().Unix(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors.Wrap(err, "storing counter in database")
|
|
||||||
}
|
}
|
||||||
|
|
37
internal/service/timer/timer_test.go
Normal file
37
internal/service/timer/timer_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package timer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimerRoundtrip(t *testing.T) {
|
||||||
|
dbc := database.GetTestDatabase(t)
|
||||||
|
ts, err := New(dbc)
|
||||||
|
require.NoError(t, err, "creating timer service")
|
||||||
|
|
||||||
|
id := "78c0176a-938e-497b-bed4-83d5bdec6caf"
|
||||||
|
|
||||||
|
has, err := ts.HasTimer(id)
|
||||||
|
require.NoError(t, err, "checking for non-existent timer")
|
||||||
|
assert.False(t, has, "checking existence of non-existing timer")
|
||||||
|
|
||||||
|
err = ts.SetTimer(id, time.Now().Add(500*time.Millisecond))
|
||||||
|
require.NoError(t, err, "setting timer")
|
||||||
|
|
||||||
|
has, err = ts.HasTimer(id)
|
||||||
|
require.NoError(t, err, "checking for existent timer")
|
||||||
|
assert.True(t, has, "checking existence of existing timer")
|
||||||
|
|
||||||
|
err = ts.SetTimer(id, time.Now().Add(-time.Millisecond))
|
||||||
|
require.NoError(t, err, "updating timer")
|
||||||
|
|
||||||
|
has, err = ts.HasTimer(id)
|
||||||
|
require.NoError(t, err, "checking for expired timer")
|
||||||
|
assert.False(t, has, "checking existence of expired timer")
|
||||||
|
}
|
|
@ -3,12 +3,18 @@ package v2migrator
|
||||||
import (
|
import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/internal/actors/counter"
|
||||||
|
"github.com/Luzifer/twitch-bot/internal/actors/variables"
|
||||||
"github.com/Luzifer/twitch-bot/internal/service/access"
|
"github.com/Luzifer/twitch-bot/internal/service/access"
|
||||||
|
"github.com/Luzifer/twitch-bot/internal/service/timer"
|
||||||
"github.com/Luzifer/twitch-bot/pkg/database"
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s storageFile) migrateCoreKV(db database.Connector) (err error) {
|
func (s storageFile) migrateCoreKV(db database.Connector) (err error) {
|
||||||
as := access.New(db)
|
as, err := access.New(db)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating access service")
|
||||||
|
}
|
||||||
|
|
||||||
if err = as.SetBotTwitchCredentials(s.BotAccessToken, s.BotRefreshToken); err != nil {
|
if err = as.SetBotTwitchCredentials(s.BotAccessToken, s.BotRefreshToken); err != nil {
|
||||||
return errors.Wrap(err, "setting bot credentials")
|
return errors.Wrap(err, "setting bot credentials")
|
||||||
|
@ -22,15 +28,8 @@ func (s storageFile) migrateCoreKV(db database.Connector) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s storageFile) migrateCounters(db database.Connector) (err error) {
|
func (s storageFile) migrateCounters(db database.Connector) (err error) {
|
||||||
for counter, value := range s.Counters {
|
for counterName, value := range s.Counters {
|
||||||
if _, err = db.DB().Exec(
|
if err = counter.UpdateCounter(db, counterName, value, true); err != nil {
|
||||||
`INSERT INTO counters
|
|
||||||
(name, value)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT DO UPDATE
|
|
||||||
SET value = excluded.value;`,
|
|
||||||
counter, value,
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(err, "storing counter value")
|
return errors.Wrap(err, "storing counter value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +38,10 @@ func (s storageFile) migrateCounters(db database.Connector) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s storageFile) migratePermissions(db database.Connector) (err error) {
|
func (s storageFile) migratePermissions(db database.Connector) (err error) {
|
||||||
as := access.New(db)
|
as, err := access.New(db)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating access service")
|
||||||
|
}
|
||||||
|
|
||||||
for channel, perms := range s.ExtendedPermissions {
|
for channel, perms := range s.ExtendedPermissions {
|
||||||
if err = as.SetExtendedTwitchCredentials(
|
if err = as.SetExtendedTwitchCredentials(
|
||||||
|
@ -56,15 +58,13 @@ func (s storageFile) migratePermissions(db database.Connector) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s storageFile) migrateTimers(db database.Connector) (err error) {
|
func (s storageFile) migrateTimers(db database.Connector) (err error) {
|
||||||
|
ts, err := timer.New(db)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating timer service")
|
||||||
|
}
|
||||||
|
|
||||||
for id, expiry := range s.Timers {
|
for id, expiry := range s.Timers {
|
||||||
if _, err := db.DB().Exec(
|
if err := ts.SetTimer(id, expiry.Time); err != nil {
|
||||||
`INSERT INTO timers
|
|
||||||
(id, expires_at)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT DO UPDATE
|
|
||||||
SET expires_at = excluded.expires_at;`,
|
|
||||||
id, expiry.Time.Unix(),
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(err, "storing counter in database")
|
return errors.Wrap(err, "storing counter in database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,14 +74,7 @@ func (s storageFile) migrateTimers(db database.Connector) (err error) {
|
||||||
|
|
||||||
func (s storageFile) migrateVariables(db database.Connector) (err error) {
|
func (s storageFile) migrateVariables(db database.Connector) (err error) {
|
||||||
for key, value := range s.Variables {
|
for key, value := range s.Variables {
|
||||||
if _, err = db.DB().Exec(
|
if err := variables.SetVariable(db, key, value); err != nil {
|
||||||
`INSERT INTO variables
|
|
||||||
(name, value)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT DO UPDATE
|
|
||||||
SET value = excluded.value;`,
|
|
||||||
key, value,
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(err, "updating value in database")
|
return errors.Wrap(err, "updating value in database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,22 @@
|
||||||
package v2migrator
|
package v2migrator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/internal/apimodules/overlays"
|
||||||
"github.com/Luzifer/twitch-bot/pkg/database"
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
"github.com/Luzifer/twitch-bot/plugins"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
storageModOverlays struct {
|
storageModOverlays struct {
|
||||||
ChannelEvents map[string][]struct {
|
ChannelEvents map[string][]overlays.SocketMessage `json:"channel_events"`
|
||||||
IsLive bool `json:"is_live"`
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Fields *plugins.FieldCollection `json:"fields"`
|
|
||||||
} `json:"channel_events"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s storageModOverlays) migrate(db database.Connector) (err error) {
|
func (s storageModOverlays) migrate(db database.Connector) (err error) {
|
||||||
for channel, evts := range s.ChannelEvents {
|
for channel, evts := range s.ChannelEvents {
|
||||||
for _, evt := range evts {
|
for _, evt := range evts {
|
||||||
buf := new(bytes.Buffer)
|
if err := overlays.AddChannelEvent(db, channel, evt); err != nil {
|
||||||
if err = json.NewEncoder(buf).Encode(evt.Fields); err != nil {
|
|
||||||
return errors.Wrap(err, "encoding fields")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = db.DB().Exec(
|
|
||||||
`INSERT INTO overlays_events
|
|
||||||
(channel, created_at, event_type, fields)
|
|
||||||
VALUES ($1, $2, $3, $4);`,
|
|
||||||
channel, evt.Time.UnixNano(), evt.Type, strings.TrimSpace(buf.String()),
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(err, "storing event to database")
|
return errors.Wrap(err, "storing event to database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
package v2migrator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/Luzifer/twitch-bot/pkg/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
storageModPunish struct {
|
|
||||||
ActiveLevels map[string]*struct {
|
|
||||||
LastLevel int `json:"last_level"`
|
|
||||||
Executed time.Time `json:"executed"`
|
|
||||||
Cooldown time.Duration `json:"cooldown"`
|
|
||||||
} `json:"active_levels"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s storageModPunish) migrate(db database.Connector) (err error) {
|
|
||||||
for key, lc := range s.ActiveLevels {
|
|
||||||
if _, err = db.DB().Exec(
|
|
||||||
`INSERT INTO punish_levels
|
|
||||||
(key, last_level, executed, cooldown)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
ON CONFLICT DO UPDATE
|
|
||||||
SET last_level = excluded.last_level,
|
|
||||||
executed = excluded.executed,
|
|
||||||
cooldown = excluded.cooldown;`,
|
|
||||||
key,
|
|
||||||
lc.LastLevel, lc.Executed.UTC().Unix(), int64(lc.Cooldown),
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(err, "updating punishment info")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
package v2migrator
|
package v2migrator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/internal/actors/quotedb"
|
||||||
"github.com/Luzifer/twitch-bot/pkg/database"
|
"github.com/Luzifer/twitch-bot/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,18 +15,8 @@ type (
|
||||||
|
|
||||||
func (s storageModQuoteDB) migrate(db database.Connector) (err error) {
|
func (s storageModQuoteDB) migrate(db database.Connector) (err error) {
|
||||||
for channel, quotes := range s.ChannelQuotes {
|
for channel, quotes := range s.ChannelQuotes {
|
||||||
t := time.Now()
|
if err := quotedb.SetQuotes(db, channel, quotes); err != nil {
|
||||||
for _, quote := range quotes {
|
return errors.Wrap(err, "setting quotes for channel")
|
||||||
if _, err = db.DB().Exec(
|
|
||||||
`INSERT INTO quotedb
|
|
||||||
(channel, created_at, quote)
|
|
||||||
VALUES ($1, $2, $3);`,
|
|
||||||
channel, t.UnixNano(), quote,
|
|
||||||
); err != nil {
|
|
||||||
return errors.Wrap(err, "adding quote for channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
t = t.Add(time.Nanosecond) // Increase by one ns to adhere to unique index
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ type (
|
||||||
Variables map[string]string `json:"variables"`
|
Variables map[string]string `json:"variables"`
|
||||||
|
|
||||||
ModuleStorage struct {
|
ModuleStorage struct {
|
||||||
ModPunish storageModPunish `json:"44ab4646-ce50-4e16-9353-c1f0eb68962b"`
|
|
||||||
ModOverlays storageModOverlays `json:"f9ca2b3a-baf6-45ea-a347-c626168665e8"`
|
ModOverlays storageModOverlays `json:"f9ca2b3a-baf6-45ea-a347-c626168665e8"`
|
||||||
ModQuoteDB storageModQuoteDB `json:"917c83ee-ed40-41e4-a558-1c2e59fdf1f5"`
|
ModQuoteDB storageModQuoteDB `json:"917c83ee-ed40-41e4-a558-1c2e59fdf1f5"`
|
||||||
} `json:"module_storage"`
|
} `json:"module_storage"`
|
||||||
|
@ -105,7 +104,6 @@ func (s storageFile) Migrate(db database.Connector) error {
|
||||||
"timers": s.migrateTimers,
|
"timers": s.migrateTimers,
|
||||||
"variables": s.migrateVariables,
|
"variables": s.migrateVariables,
|
||||||
// Modules
|
// Modules
|
||||||
"mod_punish": s.ModuleStorage.ModPunish.migrate,
|
|
||||||
"mod_overlays": s.ModuleStorage.ModOverlays.migrate,
|
"mod_overlays": s.ModuleStorage.ModOverlays.migrate,
|
||||||
"mod_quotedb": s.ModuleStorage.ModQuoteDB.migrate,
|
"mod_quotedb": s.ModuleStorage.ModQuoteDB.migrate,
|
||||||
} {
|
} {
|
||||||
|
|
20
main.go
20
main.go
|
@ -53,7 +53,8 @@ var (
|
||||||
IRCRateLimit time.Duration `flag:"rate-limit" default:"1500ms" description:"How often to send a message (default: 20/30s=1500ms, if your bot is mod everywhere: 100/30s=300ms, different for known/verified bots)"`
|
IRCRateLimit time.Duration `flag:"rate-limit" default:"1500ms" description:"How often to send a message (default: 20/30s=1500ms, if your bot is mod everywhere: 100/30s=300ms, different for known/verified bots)"`
|
||||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||||
PluginDir string `flag:"plugin-dir" default:"/usr/lib/twitch-bot" description:"Where to find and load plugins"`
|
PluginDir string `flag:"plugin-dir" default:"/usr/lib/twitch-bot" description:"Where to find and load plugins"`
|
||||||
StorageDatabase string `flag:"storage-database" default:"./storage.db" description:"Database file to store data in"`
|
StorageConnString string `flag:"storage-conn-string" default:"./storage.db" description:"Connection string for the database"`
|
||||||
|
StorageConnType string `flag:"storage-conn-type" default:"sqlite" description:"One of: mysql, postgres, sqlite"`
|
||||||
StorageEncryptionPass string `flag:"storage-encryption-pass" default:"" description:"Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)"`
|
StorageEncryptionPass string `flag:"storage-encryption-pass" default:"" description:"Passphrase to encrypt secrets inside storage (defaults to twitch-client:twitch-client-secret)"`
|
||||||
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as"`
|
TwitchClient string `flag:"twitch-client" default:"" description:"Client ID to act as"`
|
||||||
TwitchClientSecret string `flag:"twitch-client-secret" default:"" description:"Secret for the Client ID"`
|
TwitchClientSecret string `flag:"twitch-client-secret" default:"" description:"Secret for the Client ID"`
|
||||||
|
@ -207,19 +208,14 @@ func handleSubCommand(args []string) {
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
databaseConnectionString := strings.Join([]string{
|
if db, err = database.New(cfg.StorageConnType, cfg.StorageConnString, cfg.StorageEncryptionPass); err != nil {
|
||||||
cfg.StorageDatabase,
|
log.WithError(err).Fatal("Unable to open storage backend")
|
||||||
strings.Join([]string{
|
}
|
||||||
"_pragma=locking_mode(EXCLUSIVE)",
|
|
||||||
"_pragma=synchronous(FULL)",
|
if accessService, err = access.New(db); err != nil {
|
||||||
}, "&"),
|
log.WithError(err).Fatal("Unable to apply access migration")
|
||||||
}, "?")
|
|
||||||
|
|
||||||
if db, err = database.New("sqlite", databaseConnectionString, cfg.StorageEncryptionPass); err != nil {
|
|
||||||
log.WithError(err).Fatal("Unable to open storage database")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accessService = access.New(db)
|
|
||||||
if timerService, err = timer.New(db); err != nil {
|
if timerService, err = timer.New(db); err != nil {
|
||||||
log.WithError(err).Fatal("Unable to apply timer migration")
|
log.WithError(err).Fatal("Unable to apply timer migration")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,69 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"database/sql"
|
||||||
"regexp"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
connector struct {
|
connector struct {
|
||||||
db *sqlx.DB
|
db *gorm.DB
|
||||||
encryptionSecret string
|
encryptionSecret string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrCoreMetaNotFound is the error thrown when reading a non-existent
|
// ErrCoreMetaNotFound is the error thrown when reading a non-existent
|
||||||
// core_kv key
|
// core_kv key
|
||||||
ErrCoreMetaNotFound = errors.New("core meta entry not found")
|
var ErrCoreMetaNotFound = errors.New("core meta entry not found")
|
||||||
|
|
||||||
//go:embed schema/**
|
|
||||||
schema embed.FS
|
|
||||||
|
|
||||||
migrationFilename = regexp.MustCompile(`^([0-9]+)\.sql$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// New creates a new Connector with the given driver and database
|
// New creates a new Connector with the given driver and database
|
||||||
func New(driverName, dataSourceName, encryptionSecret string) (Connector, error) {
|
func New(driverName, connString, encryptionSecret string) (Connector, error) {
|
||||||
db, err := sqlx.Connect(driverName, dataSourceName)
|
var (
|
||||||
|
dbTuner func(*sql.DB, error) error
|
||||||
|
innerDB gorm.Dialector
|
||||||
|
)
|
||||||
|
|
||||||
|
switch driverName {
|
||||||
|
case "mysql":
|
||||||
|
innerDB = mysql.Open(connString)
|
||||||
|
|
||||||
|
case "postgres":
|
||||||
|
innerDB = postgres.Open(connString)
|
||||||
|
|
||||||
|
case "sqlite":
|
||||||
|
var err error
|
||||||
|
if connString, err = patchSQLiteConnString(connString); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "patching connection string")
|
||||||
|
}
|
||||||
|
innerDB = sqlite.Open(connString)
|
||||||
|
dbTuner = tuneSQLiteDatabase
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("unknown database driver %s", driverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(innerDB, &gorm.Config{
|
||||||
|
Logger: gormLogger(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "connecting database")
|
return nil, errors.Wrap(err, "connecting database")
|
||||||
}
|
}
|
||||||
|
|
||||||
db.SetConnMaxIdleTime(0)
|
if dbTuner != nil {
|
||||||
db.SetConnMaxLifetime(0)
|
if err = dbTuner(db.DB()); err != nil {
|
||||||
db.SetMaxIdleConns(1)
|
return nil, errors.Wrap(err, "tuning database")
|
||||||
db.SetMaxOpenConns(1)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn := &connector{
|
conn := &connector{
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -46,22 +73,53 @@ func New(driverName, dataSourceName, encryptionSecret string) (Connector, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c connector) Close() error {
|
func (c connector) Close() error {
|
||||||
return errors.Wrap(c.db.Close(), "closing database")
|
// return errors.Wrap(c.db.Close(), "closing database")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c connector) DB() *sqlx.DB {
|
func (c connector) DB() *gorm.DB {
|
||||||
return c.db
|
return c.db
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c connector) applyCoreSchema() error {
|
func (c connector) applyCoreSchema() error {
|
||||||
coreSQL, err := schema.ReadFile("schema/core.sql")
|
return errors.Wrap(c.db.AutoMigrate(&coreKV{}), "applying coreKV schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
func gormLogger() logger.Interface {
|
||||||
|
return logger.New(
|
||||||
|
newLogrusLogWriterWithLevel(logrus.TraceLevel),
|
||||||
|
logger.Config{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchSQLiteConnString(connString string) (string, error) {
|
||||||
|
u, err := url.Parse(connString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "reading core.sql content")
|
return connString, errors.Wrap(err, "parsing connString")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = c.db.Exec(string(coreSQL)); err != nil {
|
q := u.Query()
|
||||||
return errors.Wrap(err, "applying core schema")
|
|
||||||
|
q.Add("_pragma", "locking_mode(EXCLUSIVE)")
|
||||||
|
q.Add("_pragma", "synchronous(FULL)")
|
||||||
|
|
||||||
|
u.RawQuery = strings.NewReplacer(
|
||||||
|
"%28", "(",
|
||||||
|
"%29", ")",
|
||||||
|
).Replace(q.Encode())
|
||||||
|
|
||||||
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(c.Migrate("core", NewEmbedFSMigrator(schema, "schema")), "applying core migration")
|
func tuneSQLiteDatabase(db *sql.DB, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting database")
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetConnMaxIdleTime(0)
|
||||||
|
db.SetConnMaxLifetime(0)
|
||||||
|
db.SetMaxIdleConns(1)
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,98 +1,44 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testEncryptionPass = "password123"
|
const testEncryptionPass = "password123"
|
||||||
|
|
||||||
func TestNewConnector(t *testing.T) {
|
func TestNewConnector(t *testing.T) {
|
||||||
dbc, err := New("sqlite", ":memory:", testEncryptionPass)
|
cStrings := map[string]string{
|
||||||
if err != nil {
|
"filesystem": path.Join(t.TempDir(), "storage.db"),
|
||||||
t.Fatalf("creating database connector: %s", err)
|
"memory": "file::memory:?cache=shared",
|
||||||
}
|
}
|
||||||
defer dbc.Close()
|
|
||||||
|
|
||||||
row := dbc.DB().QueryRow("SELECT count(1) AS tables FROM sqlite_master WHERE type='table' AND name='core_kv';")
|
for name := range cStrings {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
dbc, err := New("sqlite", cStrings[name], testEncryptionPass)
|
||||||
|
require.NoError(t, err, "creating database connector")
|
||||||
|
t.Cleanup(func() { dbc.Close() })
|
||||||
|
|
||||||
|
row := dbc.DB().Raw("SELECT count(1) AS tables FROM sqlite_master WHERE type='table' AND name='core_kvs';")
|
||||||
|
|
||||||
var count int
|
var count int
|
||||||
if err = row.Scan(&count); err != nil {
|
assert.NoError(t, row.Scan(&count).Error, "reading table count result")
|
||||||
t.Fatalf("reading table count result")
|
|
||||||
}
|
|
||||||
|
|
||||||
if count != 1 {
|
assert.Equal(t, 1, count)
|
||||||
t.Errorf("expected to find one result, got %d in count of core_kv table", count)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCoreMetaRoundtrip(t *testing.T) {
|
func TestPatchSQLiteConnString(t *testing.T) {
|
||||||
dbc, err := New("sqlite", ":memory:", testEncryptionPass)
|
for in, out := range map[string]string{
|
||||||
if err != nil {
|
"storage.db": "storage.db?_pragma=locking_mode(EXCLUSIVE)&_pragma=synchronous(FULL)",
|
||||||
t.Fatalf("creating database connector: %s", err)
|
"file::memory:?cache=shared": "file::memory:?_pragma=locking_mode(EXCLUSIVE)&_pragma=synchronous(FULL)&cache=shared",
|
||||||
}
|
} {
|
||||||
defer dbc.Close()
|
cs, err := patchSQLiteConnString(in)
|
||||||
|
require.NoError(t, err, "patching conn string %q", in)
|
||||||
var (
|
assert.Equal(t, out, cs, "patching conn string %q", in)
|
||||||
arbitrary struct{ A string }
|
|
||||||
testKey = "arbitrary"
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = dbc.ReadCoreMeta(testKey, &arbitrary); !errors.Is(err, ErrCoreMetaNotFound) {
|
|
||||||
t.Error("expected core_kv not to contain key after init")
|
|
||||||
}
|
|
||||||
|
|
||||||
checkWriteRead := func(testString string) {
|
|
||||||
arbitrary.A = testString
|
|
||||||
if err = dbc.StoreCoreMeta(testKey, arbitrary); err != nil {
|
|
||||||
t.Errorf("storing core_kv: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
arbitrary.A = "" // Clear to test unmarshal
|
|
||||||
if err = dbc.ReadCoreMeta(testKey, &arbitrary); err != nil {
|
|
||||||
t.Errorf("reading core_kv: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if arbitrary.A != testString {
|
|
||||||
t.Errorf("expected meta entry to have %q, got %q", testString, arbitrary.A)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkWriteRead("just a string") // Turn one: Init from not existing
|
|
||||||
checkWriteRead("another random string") // Turn two: Overwrite
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCoreMetaEncryption(t *testing.T) {
|
|
||||||
dbc, err := New("sqlite", ":memory:", testEncryptionPass)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("creating database connector: %s", err)
|
|
||||||
}
|
|
||||||
defer dbc.Close()
|
|
||||||
|
|
||||||
var (
|
|
||||||
arbitrary struct{ A string }
|
|
||||||
testKey = "arbitrary"
|
|
||||||
testString = "foobar"
|
|
||||||
)
|
|
||||||
|
|
||||||
arbitrary.A = testString
|
|
||||||
|
|
||||||
if err = dbc.StoreEncryptedCoreMeta(testKey, arbitrary); err != nil {
|
|
||||||
t.Fatalf("storing encrypted core meta: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = dbc.ReadCoreMeta(testKey, &arbitrary); err == nil {
|
|
||||||
t.Error("reading encrypted meta without decryption succeeded")
|
|
||||||
}
|
|
||||||
|
|
||||||
arbitrary.A = ""
|
|
||||||
|
|
||||||
if err = dbc.ReadEncryptedCoreMeta(testKey, &arbitrary); err != nil {
|
|
||||||
t.Errorf("reading encrypted meta: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if arbitrary.A != testString {
|
|
||||||
t.Errorf("unexpected value: %q != %q", arbitrary.A, testString)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,19 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
coreKV struct {
|
||||||
|
Name string `gorm:"primaryKey"`
|
||||||
|
Value string
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReadCoreMeta reads an entry of the core_kv table specified by
|
// ReadCoreMeta reads an entry of the core_kv table specified by
|
||||||
|
@ -38,11 +46,10 @@ func (c connector) StoreEncryptedCoreMeta(key string, value any) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c connector) readCoreMeta(key string, value any, processor func(string) (string, error)) (err error) {
|
func (c connector) readCoreMeta(key string, value any, processor func(string) (string, error)) (err error) {
|
||||||
var data struct{ Key, Value string }
|
var data coreKV
|
||||||
data.Key = key
|
|
||||||
|
|
||||||
if err = c.db.Get(&data, "SELECT * FROM core_kv WHERE key = $1", data.Key); err != nil {
|
if err = c.db.First(&data, "name = ?", key).Error; err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return ErrCoreMetaNotFound
|
return ErrCoreMetaNotFound
|
||||||
}
|
}
|
||||||
return errors.Wrap(err, "querying core meta table")
|
return errors.Wrap(err, "querying core meta table")
|
||||||
|
@ -78,13 +85,12 @@ func (c connector) storeCoreMeta(key string, value any, processor func(string) (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.db.NamedExec(
|
data := coreKV{Name: key, Value: encValue}
|
||||||
"INSERT INTO core_kv (key, value) VALUES (:key, :value) ON CONFLICT DO UPDATE SET value=excluded.value;",
|
return errors.Wrap(
|
||||||
map[string]any{
|
c.db.Clauses(clause.OnConflict{
|
||||||
"key": key,
|
Columns: []clause.Column{{Name: "name"}},
|
||||||
"value": encValue,
|
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||||
},
|
}).Create(data).Error,
|
||||||
|
"upserting core meta value",
|
||||||
)
|
)
|
||||||
|
|
||||||
return errors.Wrap(err, "upserting core meta value")
|
|
||||||
}
|
}
|
||||||
|
|
51
pkg/database/coreKV_test.go
Normal file
51
pkg/database/coreKV_test.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCoreMetaRoundtrip(t *testing.T) {
|
||||||
|
dbc := GetTestDatabase(t)
|
||||||
|
|
||||||
|
var (
|
||||||
|
arbitrary struct{ A string }
|
||||||
|
testKey = "arbitrary"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, dbc.ReadCoreMeta(testKey, &arbitrary), ErrCoreMetaNotFound, "expected core_kv not to contain key after init")
|
||||||
|
|
||||||
|
checkWriteRead := func(testString string) {
|
||||||
|
arbitrary.A = testString
|
||||||
|
assert.NoError(t, dbc.StoreCoreMeta(testKey, arbitrary), "storing core_kv")
|
||||||
|
|
||||||
|
arbitrary.A = "" // Clear to test unmarshal
|
||||||
|
assert.NoError(t, dbc.ReadCoreMeta(testKey, &arbitrary), "reading core_kv")
|
||||||
|
|
||||||
|
assert.Equal(t, testString, arbitrary.A, "metadata equals")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWriteRead("just a string") // Turn one: Init from not existing
|
||||||
|
checkWriteRead("another random string") // Turn two: Overwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreMetaEncryption(t *testing.T) {
|
||||||
|
dbc := GetTestDatabase(t)
|
||||||
|
|
||||||
|
var (
|
||||||
|
arbitrary struct{ A string }
|
||||||
|
testKey = "arbitrary"
|
||||||
|
testString = "foobar"
|
||||||
|
)
|
||||||
|
|
||||||
|
arbitrary.A = testString
|
||||||
|
assert.NoError(t, dbc.StoreEncryptedCoreMeta(testKey, arbitrary), "storing encrypted core meta")
|
||||||
|
|
||||||
|
assert.Error(t, dbc.ReadCoreMeta(testKey, &arbitrary), "reading encrypted meta without decryption succeeded")
|
||||||
|
|
||||||
|
arbitrary.A = ""
|
||||||
|
|
||||||
|
assert.NoError(t, dbc.ReadEncryptedCoreMeta(testKey, &arbitrary), "reading encrypted meta")
|
||||||
|
assert.Equal(t, testString, arbitrary.A, "unexpected value")
|
||||||
|
}
|
|
@ -3,12 +3,7 @@
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
|
|
||||||
// Included support for pure-go sqlite
|
|
||||||
_ "github.com/glebarez/go-sqlite"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -16,8 +11,7 @@ type (
|
||||||
// convenience methods
|
// convenience methods
|
||||||
Connector interface {
|
Connector interface {
|
||||||
Close() error
|
Close() error
|
||||||
DB() *sqlx.DB
|
DB() *gorm.DB
|
||||||
Migrate(module string, migrations MigrationStorage) error
|
|
||||||
ReadCoreMeta(key string, value any) error
|
ReadCoreMeta(key string, value any) error
|
||||||
StoreCoreMeta(key string, value any) error
|
StoreCoreMeta(key string, value any) error
|
||||||
ReadEncryptedCoreMeta(key string, value any) error
|
ReadEncryptedCoreMeta(key string, value any) error
|
||||||
|
@ -25,26 +19,4 @@ type (
|
||||||
DecryptField(string) (string, error)
|
DecryptField(string) (string, error)
|
||||||
EncryptField(string) (string, error)
|
EncryptField(string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrationStorage represents a file storage containing migration
|
|
||||||
// files to migrate a namespace to its desired state. The files
|
|
||||||
// MUST be named in the schema `[0-9]+\.sql`.
|
|
||||||
//
|
|
||||||
// The storage is scanned recursively and all files are then
|
|
||||||
// string-sorted by their base-name (`/migrations/001.sql => 001.sql`).
|
|
||||||
// The last executed number is stored in numeric format, the next
|
|
||||||
// migration which basename evaluates to higher numeric will be
|
|
||||||
// executed.
|
|
||||||
//
|
|
||||||
// Numbers MUST be consecutive and MUST NOT leave out a number. A
|
|
||||||
// missing number will result in the migration processing not to
|
|
||||||
// catch up any migration afterwards.
|
|
||||||
//
|
|
||||||
// The first migration MUST be number 1
|
|
||||||
//
|
|
||||||
// Previously executed migrations MUST NOT be modified!
|
|
||||||
MigrationStorage interface {
|
|
||||||
ReadDir(name string) ([]fs.DirEntry, error)
|
|
||||||
ReadFile(name string) ([]byte, error)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
21
pkg/database/logger.go
Normal file
21
pkg/database/logger.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
logWriter struct{ io.Writer }
|
||||||
|
)
|
||||||
|
|
||||||
|
func newLogrusLogWriterWithLevel(level logrus.Level) logWriter {
|
||||||
|
writer := logrus.StandardLogger().WriterLevel(level)
|
||||||
|
return logWriter{writer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logWriter) Printf(format string, a ...any) {
|
||||||
|
fmt.Fprintf(l.Writer, format, a...)
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c connector) Migrate(module string, migrations MigrationStorage) error {
|
|
||||||
m, err := collectMigrations(migrations, "/")
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "collecting migrations")
|
|
||||||
}
|
|
||||||
|
|
||||||
migrationKey := strings.Join([]string{"migration_state", module}, "-")
|
|
||||||
|
|
||||||
var lastMigration int
|
|
||||||
if err = c.ReadCoreMeta(migrationKey, &lastMigration); err != nil && !errors.Is(err, ErrCoreMetaNotFound) {
|
|
||||||
return errors.Wrap(err, "getting last migration")
|
|
||||||
}
|
|
||||||
|
|
||||||
nextMigration := lastMigration
|
|
||||||
for {
|
|
||||||
nextMigration++
|
|
||||||
filename := m[nextMigration]
|
|
||||||
if filename == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.applyMigration(migrations, filename); err != nil {
|
|
||||||
return errors.Wrapf(err, "applying migration %d", nextMigration)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = c.StoreCoreMeta(migrationKey, nextMigration); err != nil {
|
|
||||||
return errors.Wrap(err, "updating migration number")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connector) applyMigration(migrations MigrationStorage, filename string) error {
|
|
||||||
rawMigration, err := migrations.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "reading migration file")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.db.Exec(string(rawMigration))
|
|
||||||
return errors.Wrap(err, "executing migration statement(s)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectMigrations(migrations MigrationStorage, dir string) (map[int]string, error) {
|
|
||||||
out := map[int]string{}
|
|
||||||
|
|
||||||
entries, err := migrations.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "reading dir %q", dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range entries {
|
|
||||||
if e.IsDir() {
|
|
||||||
sout, err := collectMigrations(migrations, path.Join(dir, e.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "scanning subdir %q", e.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
for n, p := range sout {
|
|
||||||
if out[n] != "" {
|
|
||||||
return nil, errors.Errorf("migration %d found more than once", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
out[n] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !migrationFilename.MatchString(e.Name()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
matches := migrationFilename.FindStringSubmatch(e.Name())
|
|
||||||
n, err := strconv.Atoi(matches[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "parsing migration number")
|
|
||||||
}
|
|
||||||
|
|
||||||
out[n] = path.Join(dir, e.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// EmbedFSMigrator is a wrapper around embed.FS enabling ReadDir("/")
|
|
||||||
// which normally would cause an error as path "/" is not available
|
|
||||||
// within an embed.FS
|
|
||||||
EmbedFSMigrator struct {
|
|
||||||
BasePath string
|
|
||||||
embed.FS
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewEmbedFSMigrator creates a new EmbedFSMigrator
|
|
||||||
func NewEmbedFSMigrator(fs embed.FS, basePath string) MigrationStorage {
|
|
||||||
return EmbedFSMigrator{BasePath: basePath, FS: fs}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadDir Wraps embed.FS.ReadDir with adjustment of the path prefix
|
|
||||||
func (e EmbedFSMigrator) ReadDir(name string) ([]fs.DirEntry, error) {
|
|
||||||
name = path.Join(e.BasePath, strings.TrimPrefix(name, "/"))
|
|
||||||
return e.FS.ReadDir(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFile Wraps embed.FS.ReadFile with adjustment of the path prefix
|
|
||||||
func (e EmbedFSMigrator) ReadFile(name string) ([]byte, error) {
|
|
||||||
name = path.Join(e.BasePath, strings.TrimPrefix(name, "/"))
|
|
||||||
return e.FS.ReadFile(name)
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed testdata/migration1/**
|
|
||||||
testMigration1 embed.FS
|
|
||||||
//go:embed testdata/migration2/**
|
|
||||||
testMigration2 embed.FS
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMigration(t *testing.T) {
|
|
||||||
dbc, err := New("sqlite", ":memory:", testEncryptionPass)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("creating database connector: %s", err)
|
|
||||||
}
|
|
||||||
defer dbc.Close()
|
|
||||||
|
|
||||||
var (
|
|
||||||
tm1 = NewEmbedFSMigrator(testMigration1, "testdata")
|
|
||||||
tm2 = NewEmbedFSMigrator(testMigration2, "testdata")
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = dbc.Migrate("test", tm1); err != nil {
|
|
||||||
t.Errorf("migration 1 take 1: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = dbc.Migrate("test", tm1); err != nil {
|
|
||||||
t.Errorf("migration 1 take 2: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = dbc.Migrate("test", tm2); err != nil {
|
|
||||||
t.Errorf("migration 2 take 1: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = dbc.Migrate("test", tm2); err != nil {
|
|
||||||
t.Errorf("migration 2 take 2: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
CREATE TABLE extended_permissions (
|
|
||||||
channel STRING NOT NULL PRIMARY KEY,
|
|
||||||
access_token STRING,
|
|
||||||
refresh_token STRING,
|
|
||||||
scopes STRING
|
|
||||||
);
|
|
|
@ -1,6 +0,0 @@
|
||||||
-- Core database structure, to be applied before any migration
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS core_kv (
|
|
||||||
key STRING NOT NULL PRIMARY KEY,
|
|
||||||
value STRING
|
|
||||||
);
|
|
4
pkg/database/testdata/migration1/001.sql
vendored
4
pkg/database/testdata/migration1/001.sql
vendored
|
@ -1,4 +0,0 @@
|
||||||
CREATE TABLE testdata (
|
|
||||||
key STRING NOT NULL PRIMARY KEY,
|
|
||||||
value STRING
|
|
||||||
);
|
|
4
pkg/database/testdata/migration2/001.sql
vendored
4
pkg/database/testdata/migration2/001.sql
vendored
|
@ -1,4 +0,0 @@
|
||||||
CREATE TABLE testdata (
|
|
||||||
key STRING NOT NULL PRIMARY KEY,
|
|
||||||
value STRING
|
|
||||||
);
|
|
1
pkg/database/testdata/migration2/002.sql
vendored
1
pkg/database/testdata/migration2/002.sql
vendored
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE testdata ADD COLUMN another_value STRING;
|
|
15
pkg/database/testhelper.go
Normal file
15
pkg/database/testhelper.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTestDatabase(t *testing.T) Connector {
|
||||||
|
dbc, err := New("sqlite", "file::memory:?cache=shared", "encpass")
|
||||||
|
require.NoError(t, err, "creating database connector")
|
||||||
|
t.Cleanup(func() { dbc.Close() })
|
||||||
|
|
||||||
|
return dbc
|
||||||
|
}
|
|
@ -73,9 +73,9 @@ var (
|
||||||
|
|
||||||
func initCorePlugins() error {
|
func initCorePlugins() error {
|
||||||
args := getRegistrationArguments()
|
args := getRegistrationArguments()
|
||||||
for _, rf := range corePluginRegistrations {
|
for idx, rf := range corePluginRegistrations {
|
||||||
if err := rf(args); err != nil {
|
if err := rf(args); err != nil {
|
||||||
return errors.Wrap(err, "registering core plugin")
|
return errors.Wrapf(err, "registering core plugin %d", idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in a new issue