Breaking: Add support for multiple database backends (#32)

This commit is contained in:
Knut Ahlers 2022-10-23 00:08:02 +02:00 committed by GitHub
parent b589e4137d
commit c0075db1f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1115 additions and 952 deletions

View file

@ -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&...&paramN=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
View file

@ -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
View file

@ -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=

View file

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

View file

@ -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")
} }

View 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")
}

View file

@ -1,4 +0,0 @@
CREATE TABLE counters (
name STRING NOT NULL PRIMARY KEY,
value INTEGER
);

View file

@ -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",
) )
} }

View file

@ -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 {

View 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")
}

View file

@ -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
);

View file

@ -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(&quote{}); 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")
} }

View file

@ -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(&quote{}, "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(&quote); 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(&quote{}).
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, &quoteText)
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(&quote{}).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")
} }

View 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(&quote{}))
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")
}

View file

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

View file

@ -1,7 +0,0 @@
CREATE TABLE quotedb (
channel STRING NOT NULL,
created_at INTEGER,
quote STRING NOT NULL,
UNIQUE(channel, created_at)
);

View file

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

View file

@ -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")
} }

View 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")
}

View file

@ -1,4 +0,0 @@
CREATE TABLE variables (
name STRING NOT NULL PRIMARY KEY,
value STRING
);

View file

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

View 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")
}

View file

@ -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,

View file

@ -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);

View file

@ -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")
} }

View file

@ -1,4 +0,0 @@
CREATE TABLE timers (
id STRING NOT NULL PRIMARY KEY,
expires_at INTEGER
);

View file

@ -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")
} }

View 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")
}

View file

@ -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")
} }
} }

View file

@ -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")
} }
} }

View file

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

View file

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

View file

@ -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
View file

@ -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")
} }

View file

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

View file

@ -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)
} }
} }

View file

@ -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")
} }

View 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")
}

View file

@ -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
View 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...)
}

View file

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

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -1,6 +0,0 @@
CREATE TABLE extended_permissions (
channel STRING NOT NULL PRIMARY KEY,
access_token STRING,
refresh_token STRING,
scopes STRING
);

View file

@ -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
);

View file

@ -1,4 +0,0 @@
CREATE TABLE testdata (
key STRING NOT NULL PRIMARY KEY,
value STRING
);

View file

@ -1,4 +0,0 @@
CREATE TABLE testdata (
key STRING NOT NULL PRIMARY KEY,
value STRING
);

View file

@ -1 +0,0 @@
ALTER TABLE testdata ADD COLUMN another_value STRING;

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

View file

@ -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