From c0075db1f3cc251018bfd5465e5e78614a6288fc Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sun, 23 Oct 2022 00:08:02 +0200 Subject: [PATCH] Breaking: Add support for multiple database backends (#32) --- README.md | 55 ++++- go.mod | 36 +++- go.sum | 191 ++++++++++++++--- internal/actors/counter/actor.go | 12 +- internal/actors/counter/database.go | 50 +++-- internal/actors/counter/database_test.go | 30 +++ internal/actors/counter/schema/001.sql | 4 - internal/actors/punish/actor.go | 10 +- internal/actors/punish/database.go | 134 ++++++------ internal/actors/punish/database_test.go | 55 +++++ internal/actors/punish/schema/001.sql | 6 - internal/actors/quotedb/actor.go | 10 +- internal/actors/quotedb/database.go | 194 ++++++++---------- internal/actors/quotedb/database_test.go | 61 ++++++ internal/actors/quotedb/http.go | 10 +- internal/actors/quotedb/schema/001.sql | 7 - internal/actors/variables/actor.go | 12 +- internal/actors/variables/database.go | 60 +++--- internal/actors/variables/database_test.go | 36 ++++ internal/actors/variables/schema/001.sql | 4 - internal/apimodules/overlays/database.go | 68 +++--- internal/apimodules/overlays/database_test.go | 54 +++++ internal/apimodules/overlays/overlays.go | 18 +- internal/apimodules/overlays/schema/001.sql | 9 - internal/service/access/access.go | 104 +++++----- internal/service/timer/schema/001.sql | 4 - internal/service/timer/timer.go | 68 +++--- internal/service/timer/timer_test.go | 37 ++++ internal/v2migrator/core.go | 47 ++--- internal/v2migrator/modOverlays.go | 26 +-- internal/v2migrator/modPunish.go | 39 ---- internal/v2migrator/modQuoteDB.go | 17 +- internal/v2migrator/store.go | 2 - main.go | 20 +- pkg/database/connector.go | 122 ++++++++--- pkg/database/connector_test.go | 108 +++------- pkg/database/coreKV.go | 32 +-- pkg/database/coreKV_test.go | 51 +++++ pkg/database/database.go | 32 +-- pkg/database/logger.go | 21 ++ pkg/database/migration.go | 94 --------- pkg/database/migration_embedfs.go | 35 ---- pkg/database/migration_test.go | 42 ---- pkg/database/schema/001.sql | 6 - pkg/database/schema/core.sql | 6 - pkg/database/testdata/migration1/001.sql | 4 - pkg/database/testdata/migration2/001.sql | 4 - pkg/database/testdata/migration2/002.sql | 1 - pkg/database/testhelper.go | 15 ++ plugins_core.go | 4 +- 50 files changed, 1115 insertions(+), 952 deletions(-) create mode 100644 internal/actors/counter/database_test.go delete mode 100644 internal/actors/counter/schema/001.sql create mode 100644 internal/actors/punish/database_test.go delete mode 100644 internal/actors/punish/schema/001.sql create mode 100644 internal/actors/quotedb/database_test.go delete mode 100644 internal/actors/quotedb/schema/001.sql create mode 100644 internal/actors/variables/database_test.go delete mode 100644 internal/actors/variables/schema/001.sql create mode 100644 internal/apimodules/overlays/database_test.go delete mode 100644 internal/apimodules/overlays/schema/001.sql delete mode 100644 internal/service/timer/schema/001.sql create mode 100644 internal/service/timer/timer_test.go delete mode 100644 internal/v2migrator/modPunish.go create mode 100644 pkg/database/coreKV_test.go create mode 100644 pkg/database/logger.go delete mode 100644 pkg/database/migration.go delete mode 100644 pkg/database/migration_embedfs.go delete mode 100644 pkg/database/migration_test.go delete mode 100644 pkg/database/schema/001.sql delete mode 100644 pkg/database/schema/core.sql delete mode 100644 pkg/database/testdata/migration1/001.sql delete mode 100644 pkg/database/testdata/migration2/001.sql delete mode 100644 pkg/database/testdata/migration2/002.sql create mode 100644 pkg/database/testhelper.go diff --git a/README.md b/README.md index 31b0738..99c70ea 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Usage of twitch-bot: --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") --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) --twitch-client string Client ID to act as --twitch-client-secret string Secret for the Client ID @@ -39,6 +40,58 @@ Supported sub-commands are: help Prints this help message ``` +### Database Connection Strings + +Currently these databases are supported and need their corresponding connection strings: + +#### MySQL + +``` +[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] + +Recommended parameters: + ?charset=utf8mb4&parseTime=True&loc=Local +``` + +- Create your database as follows: + ```sql + CREATE DATABASE twbot_tezrian CHARACTER SET utf8mb4; + ``` +- Start your bot: + ```console + # twitch-bot \ + --storage-conn-type mysql \ + --storage-conn-string 'tezrian:mypass@tcp(mariadb:3306)/twbot_tezrian?charset=utf8mb4&parseTime=True&loc=Local' \ + ... + ``` + +See [driver documentation](https://github.com/go-sql-driver/mysql#dsn-data-source-name) for more details on parameters. + +#### Postgres + +``` +host=localhost port=5432 dbname=mydb connect_timeout=10 +``` + +See [Postgres documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS) for more details in paramters. + +#### SQLite + +``` +storage.db +``` + +Just pass the filename you want to use. + +- Start your bot: + ```console + # twitch-bot \ + --storage-conn-type sqlite \ + --storage-conn-string 'storage.db' \ + ... + ``` + + ## Upgrade from `v2.x` to `v3.x` 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. diff --git a/go.mod b/go.mod index 2388ce7..59db8f3 100644 --- a/go.mod +++ b/go.mod @@ -8,29 +8,35 @@ require ( github.com/Luzifer/korvike/functions v0.6.1 github.com/Luzifer/rconfig/v2 v2.4.0 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/gofrs/uuid v4.2.0+incompatible github.com/gofrs/uuid/v3 v3.1.2 github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 github.com/itchyny/gojq v0.12.9 - github.com/jmoiron/sqlx v1.3.5 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/testify v1.8.0 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/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 ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // 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/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/google/uuid v1.3.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/imdario/mergo v0.3.11 // 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/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/mattn/go-isatty v0.0.16 // 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/reflectwalk v1.0.0 // 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/ryanuber/go-glob 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/src-d/gcfg v1.4.0 // 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/text v0.3.6 // indirect + golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect gopkg.in/validator.v2 v2.0.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - modernc.org/libc v1.16.19 // indirect - modernc.org/mathutil v1.4.1 // indirect - modernc.org/memory v1.1.1 // indirect - modernc.org/sqlite v1.18.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.19.0 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.4.0 // indirect + modernc.org/sqlite v1.19.1 // indirect ) diff --git a/go.sum b/go.sum index 2b6bdf2..56a5bae 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/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/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/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= 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/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-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -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.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 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.18.1/go.mod h1:ydXIGq2M4OzF4YyNhH129SPp7jWoVvgkEgb6pldmS0s= +github.com/glebarez/go-sqlite v1.19.1 h1:o2XhjyR8CQ2m84+bVz10G0cabmG0tY4sIMiCbrcUTrY= +github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M= +github.com/glebarez/sqlite v1.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/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 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-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/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 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-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-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/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/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 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/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 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/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM= 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/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 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.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.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/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.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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.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.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.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 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.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.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 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/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.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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/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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/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/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/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/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/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/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/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 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.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/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-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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-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-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-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/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-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/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/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= @@ -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-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-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-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/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= @@ -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-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-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-20190916202348-b4ddaad3f8a3/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-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-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-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-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= 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/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 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-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-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-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-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-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-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/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-20191011141410-1b5146add898/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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/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.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 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-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.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/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-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= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= +modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19 h1:S8flPn5ZeXx6iw/8yNa986hwTQDrY8RXU7tObZuAozo= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA= +modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0= +modernc.org/libc v1.19.0 h1:bXyVhGQg6KIClTr8FMVIDPl7jtbcs7aS5WP7vLDaxPs= +modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= 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/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.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/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +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/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/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= diff --git a/internal/actors/counter/actor.go b/internal/actors/counter/actor.go index af67231..1d37a4e 100644 --- a/internal/actors/counter/actor.go +++ b/internal/actors/counter/actor.go @@ -24,7 +24,7 @@ var ( //nolint:funlen // This function is a few lines too long but only contains definitions func Register(args plugins.RegistrationArguments) error { 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") } @@ -134,7 +134,7 @@ func Register(args plugins.RegistrationArguments) error { }) args.RegisterTemplateFunction("counterValue", plugins.GenericTemplateFunctionGetter(func(name string, _ ...string) (int64, error) { - return getCounterValue(name) + return GetCounterValue(db, name) })) return nil @@ -160,7 +160,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev } return false, errors.Wrap( - updateCounter(counterName, counterValue, true), + UpdateCounter(db, counterName, counterValue, true), "set counter", ) } @@ -179,7 +179,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev } return false, errors.Wrap( - updateCounter(counterName, counterStep, false), + UpdateCounter(db, counterName, counterStep, false), "update counter", ) } @@ -201,7 +201,7 @@ func routeActorCounterGetValue(w http.ResponseWriter, r *http.Request) { template = "%d" } - cv, err := getCounterValue(mux.Vars(r)["name"]) + cv, err := GetCounterValue(db, mux.Vars(r)["name"]) if err != nil { http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError) return @@ -223,7 +223,7 @@ func routeActorCounterSetValue(w http.ResponseWriter, r *http.Request) { 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) return } diff --git a/internal/actors/counter/database.go b/internal/actors/counter/database.go index 715c50e..704c87d 100644 --- a/internal/actors/counter/database.go +++ b/internal/actors/counter/database.go @@ -1,30 +1,29 @@ package counter import ( - "database/sql" - "embed" - "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "github.com/Luzifer/twitch-bot/pkg/database" ) -//go:embed schema/** -var schema embed.FS +type ( + counter struct { + Name string `gorm:"primaryKey"` + Value int64 + } +) -func getCounterValue(counter string) (int64, error) { - row := db.DB().QueryRow( - `SELECT value - FROM counters - WHERE name = $1`, - counter, - ) +func GetCounterValue(db database.Connector, counterName string) (int64, error) { + var c counter - var cv int64 - err := row.Scan(&cv) + err := db.DB().First(&c, "name = ?", counterName).Error switch { 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 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 { - cv, err := getCounterValue(counter) + cv, err := GetCounterValue(db, counterName) if err != nil { return errors.Wrap(err, "getting previous value") } @@ -42,14 +41,11 @@ func updateCounter(counter string, value int64, absolute bool) error { value += cv } - _, err := db.DB().Exec( - `INSERT INTO counters - (name, value) - VALUES ($1, $2) - ON CONFLICT DO UPDATE - SET value = excluded.value;`, - counter, value, + return errors.Wrap( + db.DB().Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{"value"}), + }).Create(counter{Name: counterName, Value: value}).Error, + "storing counter value", ) - - return errors.Wrap(err, "storing counter value") } diff --git a/internal/actors/counter/database_test.go b/internal/actors/counter/database_test.go new file mode 100644 index 0000000..de392ff --- /dev/null +++ b/internal/actors/counter/database_test.go @@ -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") +} diff --git a/internal/actors/counter/schema/001.sql b/internal/actors/counter/schema/001.sql deleted file mode 100644 index 1eb2f30..0000000 --- a/internal/actors/counter/schema/001.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE counters ( - name STRING NOT NULL PRIMARY KEY, - value INTEGER -); diff --git a/internal/actors/punish/actor.go b/internal/actors/punish/actor.go index ff86895..e79b6cd 100644 --- a/internal/actors/punish/actor.go +++ b/internal/actors/punish/actor.go @@ -30,7 +30,7 @@ var ( func Register(args plugins.RegistrationArguments) error { 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") } @@ -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") } - lvl, err := getPunishment(plugins.DeriveChannel(m, eventData), user, uuid) + lvl, err := getPunishment(db, plugins.DeriveChannel(m, eventData), user, uuid) if err != nil { 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.Executed = time.Now() + lvl.Executed = time.Now().UTC() lvl.LastLevel = nLvl return false, errors.Wrap( - setPunishment(plugins.DeriveChannel(m, eventData), user, uuid, lvl), + setPunishment(db, plugins.DeriveChannel(m, eventData), user, uuid, lvl), "storing punishment level", ) } @@ -236,7 +236,7 @@ func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule } return false, errors.Wrap( - deletePunishment(plugins.DeriveChannel(m, eventData), user, uuid), + deletePunishment(db, plugins.DeriveChannel(m, eventData), user, uuid), "resetting punishment level", ) } diff --git a/internal/actors/punish/database.go b/internal/actors/punish/database.go index 9128a7c..5706688 100644 --- a/internal/actors/punish/database.go +++ b/internal/actors/punish/database.go @@ -1,50 +1,45 @@ package punish import ( - "database/sql" - "embed" "strings" "time" "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "github.com/Luzifer/twitch-bot/pkg/database" ) -//go:embed schema/** -var schema embed.FS +type ( + punishLevel struct { + Key string `gorm:"primaryKey"` -func calculateCurrentPunishments() error { - rows, err := db.DB().Query( - `SELECT key, last_level, executed, cooldown - FROM punish_levels;`, - ) - if err != nil { + LastLevel int + Executed time.Time + Cooldown time.Duration + } +) + +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") } - for rows.Next() { - if err = rows.Err(); err != nil { - return errors.Wrap(err, "advancing rows") - } - + for _, p := range ps { var ( - key string - lastLevel, executed, cooldown int64 - 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 { cooldownTime := lvl.Executed.Add(lvl.Cooldown) - if cooldownTime.After(time.Now()) { + if cooldownTime.After(time.Now().UTC()) { break } @@ -55,61 +50,53 @@ func calculateCurrentPunishments() error { // Level 0 is the first punishment level, so only remove if it drops below 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") } continue } 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(rows.Err(), "finishing rows processing") + return nil } -func deletePunishment(channel, user, uuid string) error { - return deletePunishmentForKey(getDBKey(channel, user, uuid)) +func deletePunishment(db database.Connector, channel, user, uuid string) error { + return deletePunishmentForKey(db, getDBKey(channel, user, uuid)) } -func deletePunishmentForKey(key string) error { - _, err := db.DB().Exec( - `DELETE FROM punish_levels - WHERE key = $1;`, - key, +func deletePunishmentForKey(db database.Connector, key string) error { + return errors.Wrap( + db.DB().Delete(&punishLevel{}, "key = ?", key).Error, + "deleting punishment info", ) - - return errors.Wrap(err, "deleting punishment info") } -func getPunishment(channel, user, uuid string) (*levelConfig, error) { - if err := calculateCurrentPunishments(); err != nil { +func getPunishment(db database.Connector, channel, user, uuid string) (*levelConfig, error) { + if err := calculateCurrentPunishments(db); err != nil { return nil, errors.Wrap(err, "updating punishment states") } - row := db.DB().QueryRow( - `SELECT last_level, executed, cooldown - FROM punish_levels - WHERE key = $1;`, - getDBKey(channel, user, uuid), + var ( + lc = &levelConfig{LastLevel: -1} + p punishLevel ) - lc := &levelConfig{LastLevel: -1} - - var lastLevel, executed, cooldown int64 - err := row.Scan(&lastLevel, &executed, &cooldown) + err := db.DB().First(&p, "key = ?", getDBKey(channel, user, uuid)).Error switch { case err == nil: - lc.LastLevel = int(lastLevel) - lc.Cooldown = time.Duration(cooldown) - lc.Executed = time.Unix(executed, 0) + return &levelConfig{ + LastLevel: p.LastLevel, + Executed: p.Executed, + Cooldown: p.Cooldown, + }, nil - return lc, nil - - case errors.Is(err, sql.ErrNoRows): + case errors.Is(err, gorm.ErrRecordNotFound): return lc, nil default: @@ -117,24 +104,27 @@ func getPunishment(channel, user, uuid string) (*levelConfig, error) { } } -func setPunishment(channel, user, uuid string, lc *levelConfig) error { - return setPunishmentForKey(getDBKey(channel, user, uuid), lc) +func setPunishment(db database.Connector, channel, user, uuid string, lc *levelConfig) error { + return setPunishmentForKey(db, getDBKey(channel, user, uuid), lc) } -func setPunishmentForKey(key string, lc *levelConfig) error { - _, 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), - ) +func setPunishmentForKey(db database.Connector, key string, lc *levelConfig) error { + if lc == nil { + return errors.New("nil levelConfig given") + } - 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 { diff --git a/internal/actors/punish/database_test.go b/internal/actors/punish/database_test.go new file mode 100644 index 0000000..6fa0c21 --- /dev/null +++ b/internal/actors/punish/database_test.go @@ -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") +} diff --git a/internal/actors/punish/schema/001.sql b/internal/actors/punish/schema/001.sql deleted file mode 100644 index 3f9935b..0000000 --- a/internal/actors/punish/schema/001.sql +++ /dev/null @@ -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 -); diff --git a/internal/actors/quotedb/actor.go b/internal/actors/quotedb/actor.go index 2fff440..c2aa1a5 100644 --- a/internal/actors/quotedb/actor.go +++ b/internal/actors/quotedb/actor.go @@ -26,7 +26,7 @@ var ( func Register(args plugins.RegistrationArguments) error { db = args.GetDatabaseConnector() - if err := db.Migrate(actorName, database.NewEmbedFSMigrator(schema, "schema")); err != nil { + if err := db.DB().AutoMigrate("e{}); err != nil { return errors.Wrap(err, "applying schema migration") } @@ -83,7 +83,7 @@ func Register(args plugins.RegistrationArguments) error { args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} { 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( - addQuote(plugins.DeriveChannel(m, eventData), quote), + AddQuote(db, plugins.DeriveChannel(m, eventData), quote), "adding quote", ) case "del": return false, errors.Wrap( - delQuote(plugins.DeriveChannel(m, eventData), index), + DelQuote(db, plugins.DeriveChannel(m, eventData), index), "storing quote database", ) case "get": - idx, quote, err := getQuote(plugins.DeriveChannel(m, eventData), index) + idx, quote, err := GetQuote(db, plugins.DeriveChannel(m, eventData), index) if err != nil { return false, errors.Wrap(err, "getting quote") } diff --git a/internal/actors/quotedb/database.go b/internal/actors/quotedb/database.go index c76d8c8..8a1333d 100644 --- a/internal/actors/quotedb/database.go +++ b/internal/actors/quotedb/database.go @@ -1,120 +1,99 @@ package quotedb import ( - "database/sql" - "embed" "math/rand" "time" "github.com/pkg/errors" + "gorm.io/gorm" + + "github.com/Luzifer/twitch-bot/pkg/database" ) -//go:embed schema/** -var schema embed.FS +type ( + quote struct { + Channel string `gorm:"not null;uniqueIndex:ensure_sort_idx;size:32"` + CreatedAt int64 `gorm:"uniqueIndex:ensure_sort_idx"` + Quote string + } +) -func addQuote(channel, quote string) error { - _, err := db.DB().Exec( - `INSERT INTO quotedb - (channel, created_at, quote) - VALUES ($1, $2, $3);`, - channel, time.Now().UnixNano(), quote, +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", ) - - return errors.Wrap(err, "adding quote to database") } -func delQuote(channel string, quote int) error { - _, createdAt, _, err := getQuoteRaw(channel, quote) +func DelQuote(db database.Connector, channel string, quoteIdx int) error { + _, createdAt, _, err := GetQuoteRaw(db, channel, quoteIdx) if err != nil { return errors.Wrap(err, "fetching specified quote") } - _, err = db.DB().Exec( - `DELETE FROM quotedb - WHERE channel = $1 AND created_at = $2;`, - channel, createdAt, + return errors.Wrap( + db.DB().Delete("e{}, "channel = ? AND created_at = ?", channel, createdAt).Error, + "deleting quote", ) - - return errors.Wrap(err, "deleting quote") } -func getChannelQuotes(channel string) ([]string, error) { - rows, err := db.DB().Query( - `SELECT quote - FROM quotedb - WHERE channel = $1 - ORDER BY created_at ASC`, - channel, - ) - if err != nil { +func GetChannelQuotes(db database.Connector, channel string) ([]string, error) { + var qs []quote + if err := db.DB().Where("channel = ?", channel).Order("created_at").Find(&qs).Error; err != nil { return nil, errors.Wrap(err, "querying quotes") } var quotes []string - for rows.Next() { - if err = rows.Err(); err != nil { - return nil, errors.Wrap(err, "advancing row read") - } - - var quote string - if err = rows.Scan("e); err != nil { - return nil, errors.Wrap(err, "scanning row") - } - - quotes = append(quotes, quote) + for _, q := range qs { + quotes = append(quotes, q.Quote) } - return quotes, errors.Wrap(rows.Err(), "advancing row read") + return quotes, nil } -func getMaxQuoteIdx(channel string) (int, error) { - row := db.DB().QueryRow( - `SELECT COUNT(1) as quoteCount - FROM quotedb - WHERE channel = $1;`, - channel, - ) +func GetMaxQuoteIdx(db database.Connector, channel string) (int, error) { + var count int64 + if err := db.DB(). + Model("e{}). + Where("channel = ?", channel). + Count(&count). + Error; err != nil { + return 0, errors.Wrap(err, "getting quote count") + } - var count int - err := row.Scan(&count) - - return count, errors.Wrap(err, "getting quote count") + return int(count), nil } -func getQuote(channel string, quote int) (int, string, error) { - quoteIdx, _, quoteText, err := getQuoteRaw(channel, quote) +func GetQuote(db database.Connector, channel string, quote int) (int, string, error) { + quoteIdx, _, quoteText, err := GetQuoteRaw(db, channel, quote) return quoteIdx, quoteText, err } -func getQuoteRaw(channel string, quote int) (int, int64, string, error) { - if quote == 0 { - max, err := getMaxQuoteIdx(channel) +func GetQuoteRaw(db database.Connector, channel string, quoteIdx int) (int, int64, string, error) { + if quoteIdx == 0 { + max, err := GetMaxQuoteIdx(db, channel) if err != nil { 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( - `SELECT created_at, quote - FROM quotedb - WHERE channel = $1 - ORDER BY created_at ASC - LIMIT 1 OFFSET $2`, - channel, quote-1, - ) + var q quote + err := db.DB(). + Where("channel = ?", channel). + Limit(1). + Offset(quoteIdx - 1). + First(&q).Error - var ( - createdAt int64 - quoteText string - ) - - err := row.Scan(&createdAt, "eText) switch { 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 default: @@ -122,52 +101,43 @@ func getQuoteRaw(channel string, quote int) (int, int64, string, error) { } } -func setQuotes(channel string, quotes []string) error { - tx, err := db.DB().Begin() - if err != nil { - return errors.Wrap(err, "creating transaction") - } +func SetQuotes(db database.Connector, channel string, quotes []string) error { + return errors.Wrap( + db.DB().Transaction(func(tx *gorm.DB) error { + if err := tx.Where("channel = ?", channel).Delete("e{}).Error; err != nil { + return errors.Wrap(err, "deleting quotes for channel") + } - if _, err = tx.Exec( - `DELETE FROM quotedb - WHERE channel = $1;`, - channel, - ); err != nil { - defer tx.Rollback() - return errors.Wrap(err, "deleting quotes for channel") - } + t := time.Now() + for _, quoteStr := range quotes { + if err := tx.Create(quote{ + Channel: channel, + CreatedAt: t.UnixNano(), + Quote: quoteStr, + }).Error; err != nil { + return errors.Wrap(err, "adding quote") + } - t := time.Now() - for _, quote := range quotes { - if _, err = tx.Exec( - `INSERT INTO quotedb - (channel, created_at, quote) - VALUES ($1, $2, $3);`, - channel, t.UnixNano(), quote, - ); err != nil { - 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 { - _, createdAt, _, err := getQuoteRaw(channel, idx) +func UpdateQuote(db database.Connector, channel string, idx int, quoteStr string) error { + _, createdAt, _, err := GetQuoteRaw(db, channel, idx) if err != nil { return errors.Wrap(err, "fetching specified quote") } - _, err = db.DB().Exec( - `UPDATE quotedb - SET quote = $3 - WHERE channel = $1 - AND created_at = $2;`, - channel, createdAt, quote, + return errors.Wrap( + db.DB(). + Where("channel = ? AND created_at = ?", channel, createdAt). + Update("quote", quoteStr). + Error, + "updating quote", ) - - return errors.Wrap(err, "updating quote") } diff --git a/internal/actors/quotedb/database_test.go b/internal/actors/quotedb/database_test.go new file mode 100644 index 0000000..a13a88a --- /dev/null +++ b/internal/actors/quotedb/database_test.go @@ -0,0 +1,61 @@ +package quotedb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Luzifer/twitch-bot/pkg/database" +) + +func TestQuotesRoundtrip(t *testing.T) { + dbc := database.GetTestDatabase(t) + require.NoError(t, dbc.DB().AutoMigrate("e{})) + + var ( + channel = "#test" + quotes = []string{ + "I'm a quote", + "I might have been said...", + "Testing rocks", + "Lets add some more", + "...or not", + } + ) + + cq, err := GetChannelQuotes(dbc, channel) + assert.NoError(t, err, "querying empty database") + assert.Zero(t, cq, "expecting no quotes") + + for i, q := range quotes { + assert.NoError(t, AddQuote(dbc, channel, q), "adding quote %d", i) + } + + cq, err = GetChannelQuotes(dbc, channel) + assert.NoError(t, err, "querying database") + assert.Equal(t, quotes, cq, "checkin order and presence of quotes") + + assert.NoError(t, DelQuote(dbc, channel, 1), "removing one quote") + assert.NoError(t, DelQuote(dbc, channel, 1), "removing one quote") + + cq, err = GetChannelQuotes(dbc, channel) + assert.NoError(t, err, "querying database") + assert.Len(t, cq, len(quotes)-2, "expecting quotes in db") + + assert.NoError(t, SetQuotes(dbc, channel, quotes), "replacing quotes") + + cq, err = GetChannelQuotes(dbc, channel) + assert.NoError(t, err, "querying database") + assert.Equal(t, quotes, cq, "checkin order and presence of quotes") + + idx, q, err := GetQuote(dbc, channel, 0) + assert.NoError(t, err, "getting random quote") + assert.NotZero(t, idx, "index must not be zero") + assert.NotZero(t, q, "quote must not be zero") + + idx, q, err = GetQuote(dbc, channel, 3) + assert.NoError(t, err, "getting specific quote") + assert.Equal(t, 3, idx, "index must be 3") + assert.Equal(t, quotes[2], q, "quote must not the third") +} diff --git a/internal/actors/quotedb/http.go b/internal/actors/quotedb/http.go index e7140c8..e4d2e7d 100644 --- a/internal/actors/quotedb/http.go +++ b/internal/actors/quotedb/http.go @@ -133,7 +133,7 @@ func handleAddQuotes(w http.ResponseWriter, r *http.Request) { } 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) return } @@ -154,7 +154,7 @@ func handleDeleteQuote(w http.ResponseWriter, r *http.Request) { 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) return } @@ -171,7 +171,7 @@ func handleListQuotes(w http.ResponseWriter, r *http.Request) { channel := "#" + strings.TrimLeft(mux.Vars(r)["channel"], "#") - quotes, err := getChannelQuotes(channel) + quotes, err := GetChannelQuotes(db, channel) if err != nil { http.Error(w, errors.Wrap(err, "getting quotes").Error(), http.StatusInternalServerError) return @@ -192,7 +192,7 @@ func handleReplaceQuotes(w http.ResponseWriter, r *http.Request) { 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) return } @@ -228,7 +228,7 @@ func handleUpdateQuote(w http.ResponseWriter, r *http.Request) { 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) return } diff --git a/internal/actors/quotedb/schema/001.sql b/internal/actors/quotedb/schema/001.sql deleted file mode 100644 index 99232c9..0000000 --- a/internal/actors/quotedb/schema/001.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE quotedb ( - channel STRING NOT NULL, - created_at INTEGER, - quote STRING NOT NULL, - - UNIQUE(channel, created_at) -); diff --git a/internal/actors/variables/actor.go b/internal/actors/variables/actor.go index 063a25a..77bf0cf 100644 --- a/internal/actors/variables/actor.go +++ b/internal/actors/variables/actor.go @@ -22,7 +22,7 @@ var ( func Register(args plugins.RegistrationArguments) error { 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") } @@ -107,7 +107,7 @@ func Register(args plugins.RegistrationArguments) 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 { 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) { return false, errors.Wrap( - removeVariable(varName), + RemoveVariable(db, varName), "removing variable", ) } @@ -142,7 +142,7 @@ func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule } return false, errors.Wrap( - setVariable(varName, value), + SetVariable(db, varName, value), "setting variable", ) } @@ -159,7 +159,7 @@ func (a ActorSetVariable) Validate(attrs *plugins.FieldCollection) (err error) { } 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 { http.Error(w, errors.Wrap(err, "getting value").Error(), http.StatusInternalServerError) return @@ -170,7 +170,7 @@ func routeActorSetVarGetValue(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) return } diff --git a/internal/actors/variables/database.go b/internal/actors/variables/database.go index 3ff2f8f..844e64f 100644 --- a/internal/actors/variables/database.go +++ b/internal/actors/variables/database.go @@ -1,30 +1,28 @@ package variables import ( - "database/sql" - "embed" - "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "github.com/Luzifer/twitch-bot/pkg/database" ) -//go:embed schema/** -var schema embed.FS +type ( + variable struct { + Name string `gorm:"primaryKey"` + Value string + } +) -func getVariable(key string) (string, error) { - row := db.DB().QueryRow( - `SELECT value - FROM variables - WHERE name = $1`, - key, - ) - - var vc string - err := row.Scan(&vc) +func GetVariable(db database.Connector, key string) (string, error) { + var v variable + err := db.DB().First(&v, "name = ?", key).Error switch { 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 default: @@ -32,25 +30,19 @@ func getVariable(key string) (string, error) { } } -func setVariable(key, value string) error { - _, err := db.DB().Exec( - `INSERT INTO variables - (name, value) - VALUES ($1, $2) - ON CONFLICT DO UPDATE - SET value = excluded.value;`, - key, value, +func SetVariable(db database.Connector, key, value string) error { + return errors.Wrap( + db.DB().Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{"value"}), + }).Create(variable{Name: key, Value: value}).Error, + "updating value in database", ) - - return errors.Wrap(err, "updating value in database") } -func removeVariable(key string) error { - _, err := db.DB().Exec( - `DELETE FROM variables - WHERE name = $1;`, - key, +func RemoveVariable(db database.Connector, key string) error { + return errors.Wrap( + db.DB().Delete(&variable{}, "name = ?", key).Error, + "deleting value in database", ) - - return errors.Wrap(err, "deleting value in database") } diff --git a/internal/actors/variables/database_test.go b/internal/actors/variables/database_test.go new file mode 100644 index 0000000..9bebf13 --- /dev/null +++ b/internal/actors/variables/database_test.go @@ -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") +} diff --git a/internal/actors/variables/schema/001.sql b/internal/actors/variables/schema/001.sql deleted file mode 100644 index 02121ee..0000000 --- a/internal/actors/variables/schema/001.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE variables ( - name STRING NOT NULL PRIMARY KEY, - value STRING -); diff --git a/internal/apimodules/overlays/database.go b/internal/apimodules/overlays/database.go index 9e3eaa0..a8a64d1 100644 --- a/internal/apimodules/overlays/database.go +++ b/internal/apimodules/overlays/database.go @@ -2,73 +2,63 @@ package overlays import ( "bytes" - "embed" "encoding/json" "strings" "time" "github.com/pkg/errors" + "github.com/Luzifer/twitch-bot/pkg/database" "github.com/Luzifer/twitch-bot/plugins" ) -//go:embed schema/** -var schema embed.FS +type ( + 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) if err := json.NewEncoder(buf).Encode(evt.Fields); err != nil { return errors.Wrap(err, "encoding fields") } - _, 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()), + return errors.Wrap( + db.DB().Create(overlaysEvent{ + Channel: channel, + CreatedAt: evt.Time.UTC(), + 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) { - rows, err := db.DB().Query( - `SELECT created_at, event_type, fields - FROM overlays_events - WHERE channel = $1 - ORDER BY created_at;`, - channel, - ) - if err != nil { +func GetChannelEvents(db database.Connector, channel string) ([]SocketMessage, error) { + var evts []overlaysEvent + + if err := db.DB().Where("channel = ?", channel).Order("created_at").Find(&evts).Error; err != nil { return nil, errors.Wrap(err, "querying channel events") } - var out []socketMessage - for rows.Next() { - 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") - } - + var out []SocketMessage + for _, e := range evts { 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") } - out = append(out, socketMessage{ + out = append(out, SocketMessage{ IsLive: false, - Time: time.Unix(0, createdAt), - Type: eventType, + Time: e.CreatedAt, + Type: e.EventType, Fields: fields, }) } - return out, errors.Wrap(rows.Err(), "advancing row read") + return out, nil } diff --git a/internal/apimodules/overlays/database_test.go b/internal/apimodules/overlays/database_test.go new file mode 100644 index 0000000..d3cd9f9 --- /dev/null +++ b/internal/apimodules/overlays/database_test.go @@ -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") +} diff --git a/internal/apimodules/overlays/overlays.go b/internal/apimodules/overlays/overlays.go index be7ce81..1536040 100644 --- a/internal/apimodules/overlays/overlays.go +++ b/internal/apimodules/overlays/overlays.go @@ -30,7 +30,7 @@ const ( ) type ( - socketMessage struct { + SocketMessage struct { IsLive bool `json:"is_live"` Time time.Time `json:"time"` Type string `json:"type"` @@ -65,7 +65,7 @@ var ( func Register(args plugins.RegistrationArguments) error { 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") } @@ -129,7 +129,7 @@ func Register(args plugins.RegistrationArguments) error { } return errors.Wrap( - addEvent(plugins.DeriveChannel(nil, eventData), socketMessage{ + AddChannelEvent(db, plugins.DeriveChannel(nil, eventData), SocketMessage{ IsLive: false, Time: time.Now(), Type: event, @@ -156,7 +156,7 @@ func Register(args plugins.RegistrationArguments) error { func handleEventsReplay(w http.ResponseWriter, r *http.Request) { var ( channel = mux.Vars(r)["channel"] - msgs []socketMessage + msgs []SocketMessage since = time.Time{} ) @@ -164,7 +164,7 @@ func handleEventsReplay(w http.ResponseWriter, r *http.Request) { since = s } - events, err := getChannelEvents("#" + strings.TrimLeft(channel, "#")) + events, err := GetChannelEvents(db, "#"+strings.TrimLeft(channel, "#")) if err != nil { http.Error(w, errors.Wrap(err, "getting channel events").Error(), http.StatusInternalServerError) return @@ -210,12 +210,12 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) { connLock = new(sync.Mutex) errC = make(chan error, 1) isAuthorized bool - sendMsgC = make(chan socketMessage, 1) + sendMsgC = make(chan SocketMessage, 1) ) // Register listener unsub := subscribeSocket(func(event string, eventData *plugins.FieldCollection) { - sendMsgC <- socketMessage{ + sendMsgC <- SocketMessage{ IsLive: true, Time: time.Now(), Type: event, @@ -269,7 +269,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) { continue } - var recvMsg socketMessage + var recvMsg SocketMessage if err = json.Unmarshal(p, &recvMsg); err != nil { errC <- errors.Wrap(err, "decoding message") return @@ -290,7 +290,7 @@ func handleSocketSubscription(w http.ResponseWriter, r *http.Request) { authTimeout.Stop() isAuthorized = true - sendMsgC <- socketMessage{ + sendMsgC <- SocketMessage{ IsLive: true, Time: time.Now(), Type: msgTypeRequestAuth, diff --git a/internal/apimodules/overlays/schema/001.sql b/internal/apimodules/overlays/schema/001.sql deleted file mode 100644 index 0e546d7..0000000 --- a/internal/apimodules/overlays/schema/001.sql +++ /dev/null @@ -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); diff --git a/internal/service/access/access.go b/internal/service/access/access.go index e7a2019..e991891 100644 --- a/internal/service/access/access.go +++ b/internal/service/access/access.go @@ -1,10 +1,11 @@ package access import ( - "database/sql" "strings" "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" "github.com/Luzifer/go_helpers/v2/str" "github.com/Luzifer/twitch-bot/pkg/database" @@ -26,11 +27,21 @@ type ( TokenUpdateHook func() } + extendedPermission struct { + Channel string `gorm:"primaryKey"` + AccessToken string + RefreshToken string + Scopes string + } + Service struct{ db database.Connector } ) -func New(db database.Connector) *Service { - return &Service{db} +func New(db database.Connector) (*Service, error) { + return &Service{db}, errors.Wrap( + db.DB().AutoMigrate(&extendedPermission{}), + "migrating database schema", + ) } 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) { - var err error - row := s.db.DB().QueryRow( - `SELECT access_token, refresh_token, scopes - FROM extended_permissions - WHERE channel = $1`, - channel, + var ( + err error + perm extendedPermission ) - var accessToken, refreshToken, scopeStr string - if err = row.Scan(&accessToken, &refreshToken, &scopeStr); err != nil { - return nil, errors.Wrap(err, "getting twitch credentials from database") + if err = s.db.DB().First(&perm, "channel = ?", channel).Error; err != nil { + return nil, errors.Wrap(err, "getting twitch credential 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") } - 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") } - 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 { 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) { - row := s.db.DB().QueryRow( - `SELECT scopes - FROM extended_permissions - WHERE channel = $1`, - channel, + var ( + err error + perm extendedPermission ) - var scopeStr string - if err := row.Scan(&scopeStr); err != nil { - if errors.Is(err, sql.ErrNoRows) { + if err = s.db.DB().First(&perm, "channel = ?", channel).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { 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 { 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) { - row := s.db.DB().QueryRow( - `SELECT scopes - FROM extended_permissions - WHERE channel = $1`, - channel, + var ( + err error + perm extendedPermission ) - var scopeStr string - if err := row.Scan(&scopeStr); err != nil { - if errors.Is(err, sql.ErrNoRows) { + if err = s.db.DB().First(&perm, "channel = ?", channel).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { 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 { 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 { - _, err := s.db.DB().Exec( - `DELETE FROM extended_permissions - WHERE channel = $1`, - channel, + return errors.Wrap( + s.db.DB().Delete(&extendedPermission{}, "channel = ?", channel).Error, + "deleting data from table", ) - - return errors.Wrap(err, "deleting data from table") } 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") } - _, err = s.db.DB().Exec( - `INSERT INTO extended_permissions - (channel, access_token, refresh_token, scopes) - VALUES ($1, $2, $3, $4) - ON CONFLICT DO UPDATE SET - access_token=excluded.access_token, - refresh_token=excluded.refresh_token, - scopes=excluded.scopes;`, - channel, accessToken, refreshToken, strings.Join(scope, " "), + return errors.Wrap( + s.db.DB().Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "channel"}}, + DoUpdates: clause.AssignmentColumns([]string{"access_token", "refresh_token", "scopes"}), + }).Create(extendedPermission{ + Channel: channel, + AccessToken: accessToken, + RefreshToken: refreshToken, + Scopes: strings.Join(scope, " "), + }).Error, + "inserting data into table", ) - - return errors.Wrap(err, "inserting data into table") } diff --git a/internal/service/timer/schema/001.sql b/internal/service/timer/schema/001.sql deleted file mode 100644 index b4a72ac..0000000 --- a/internal/service/timer/schema/001.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE timers ( - id STRING NOT NULL PRIMARY KEY, - expires_at INTEGER -); diff --git a/internal/service/timer/timer.go b/internal/service/timer/timer.go index 122a6d1..37585e7 100644 --- a/internal/service/timer/timer.go +++ b/internal/service/timer/timer.go @@ -2,12 +2,13 @@ package timer import ( "crypto/sha256" - "embed" "fmt" "strings" "time" "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" "github.com/Luzifer/twitch-bot/pkg/database" "github.com/Luzifer/twitch-bot/plugins" @@ -18,21 +19,21 @@ type ( db database.Connector permitTimeout time.Duration } + + timer struct { + ID string `gorm:"primaryKey"` + ExpiresAt time.Time + } ) -var ( - _ plugins.TimerStore = (*Service)(nil) - - //go:embed schema/** - schema embed.FS -) +var _ plugins.TimerStore = (*Service)(nil) func New(db database.Connector) (*Service, error) { s := &Service{ 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) { @@ -42,11 +43,11 @@ func (s *Service) UpdatePermitTimeout(d time.Duration) { // Cooldown timer 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) { - 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 { @@ -58,11 +59,11 @@ func (Service) getCooldownTimerKey(tt plugins.TimerType, limiter, ruleID string) // Permit timer 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) { - return s.hasTimer(s.getPermitTimerKey(channel, username)) + return s.HasTimer(s.getPermitTimerKey(channel, username)) } func (Service) getPermitTimerKey(channel, username string) string { @@ -73,31 +74,30 @@ func (Service) getPermitTimerKey(channel, username string) string { // Generic timer -func (s Service) hasTimer(id string) (bool, error) { - row := s.db.DB().QueryRow( - `SELECT COUNT(1) as active_counters - FROM timers - WHERE id = $1 AND expires_at >= $2`, - id, time.Now().UTC().Unix(), - ) +func (s Service) HasTimer(id string) (bool, error) { + var t timer + err := s.db.DB().First(&t, "id = ? AND expires_at >= ?", id, time.Now().UTC()).Error + switch { + case err == nil: + return true, nil - var nCounters int64 - if err := row.Scan(&nCounters); err != nil { - return false, errors.Wrap(err, "getting active counters from database") + case errors.Is(err, gorm.ErrRecordNotFound): + return false, nil + + default: + return false, errors.Wrap(err, "getting timer information") } - - 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(), +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", ) - - return errors.Wrap(err, "storing counter in database") } diff --git a/internal/service/timer/timer_test.go b/internal/service/timer/timer_test.go new file mode 100644 index 0000000..e0a0b16 --- /dev/null +++ b/internal/service/timer/timer_test.go @@ -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") +} diff --git a/internal/v2migrator/core.go b/internal/v2migrator/core.go index 636754f..d3e43cf 100644 --- a/internal/v2migrator/core.go +++ b/internal/v2migrator/core.go @@ -3,12 +3,18 @@ package v2migrator import ( "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/timer" "github.com/Luzifer/twitch-bot/pkg/database" ) 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 { 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) { - for counter, value := range s.Counters { - if _, err = db.DB().Exec( - `INSERT INTO counters - (name, value) - VALUES ($1, $2) - ON CONFLICT DO UPDATE - SET value = excluded.value;`, - counter, value, - ); err != nil { + for counterName, value := range s.Counters { + if err = counter.UpdateCounter(db, counterName, value, true); err != nil { 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) { - 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 { 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) { + ts, err := timer.New(db) + if err != nil { + return errors.Wrap(err, "creating timer service") + } + for id, expiry := range s.Timers { - if _, err := db.DB().Exec( - `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 { + if err := ts.SetTimer(id, expiry.Time); err != nil { 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) { for key, value := range s.Variables { - if _, err = db.DB().Exec( - `INSERT INTO variables - (name, value) - VALUES ($1, $2) - ON CONFLICT DO UPDATE - SET value = excluded.value;`, - key, value, - ); err != nil { + if err := variables.SetVariable(db, key, value); err != nil { return errors.Wrap(err, "updating value in database") } } diff --git a/internal/v2migrator/modOverlays.go b/internal/v2migrator/modOverlays.go index 6b4db8f..4804f02 100644 --- a/internal/v2migrator/modOverlays.go +++ b/internal/v2migrator/modOverlays.go @@ -1,42 +1,22 @@ package v2migrator import ( - "bytes" - "encoding/json" - "strings" - "time" - "github.com/pkg/errors" + "github.com/Luzifer/twitch-bot/internal/apimodules/overlays" "github.com/Luzifer/twitch-bot/pkg/database" - "github.com/Luzifer/twitch-bot/plugins" ) type ( storageModOverlays struct { - ChannelEvents map[string][]struct { - IsLive bool `json:"is_live"` - Time time.Time `json:"time"` - Type string `json:"type"` - Fields *plugins.FieldCollection `json:"fields"` - } `json:"channel_events"` + ChannelEvents map[string][]overlays.SocketMessage `json:"channel_events"` } ) func (s storageModOverlays) migrate(db database.Connector) (err error) { for channel, evts := range s.ChannelEvents { for _, evt := range evts { - buf := new(bytes.Buffer) - 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 { + if err := overlays.AddChannelEvent(db, channel, evt); err != nil { return errors.Wrap(err, "storing event to database") } } diff --git a/internal/v2migrator/modPunish.go b/internal/v2migrator/modPunish.go deleted file mode 100644 index 8b0041d..0000000 --- a/internal/v2migrator/modPunish.go +++ /dev/null @@ -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 -} diff --git a/internal/v2migrator/modQuoteDB.go b/internal/v2migrator/modQuoteDB.go index 57977a3..8cf89d4 100644 --- a/internal/v2migrator/modQuoteDB.go +++ b/internal/v2migrator/modQuoteDB.go @@ -1,10 +1,9 @@ package v2migrator import ( - "time" - "github.com/pkg/errors" + "github.com/Luzifer/twitch-bot/internal/actors/quotedb" "github.com/Luzifer/twitch-bot/pkg/database" ) @@ -16,18 +15,8 @@ type ( func (s storageModQuoteDB) migrate(db database.Connector) (err error) { for channel, quotes := range s.ChannelQuotes { - t := time.Now() - for _, quote := range quotes { - 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 + if err := quotedb.SetQuotes(db, channel, quotes); err != nil { + return errors.Wrap(err, "setting quotes for channel") } } diff --git a/internal/v2migrator/store.go b/internal/v2migrator/store.go index 7b8e89e..eacd86f 100644 --- a/internal/v2migrator/store.go +++ b/internal/v2migrator/store.go @@ -31,7 +31,6 @@ type ( Variables map[string]string `json:"variables"` ModuleStorage struct { - ModPunish storageModPunish `json:"44ab4646-ce50-4e16-9353-c1f0eb68962b"` ModOverlays storageModOverlays `json:"f9ca2b3a-baf6-45ea-a347-c626168665e8"` ModQuoteDB storageModQuoteDB `json:"917c83ee-ed40-41e4-a558-1c2e59fdf1f5"` } `json:"module_storage"` @@ -105,7 +104,6 @@ func (s storageFile) Migrate(db database.Connector) error { "timers": s.migrateTimers, "variables": s.migrateVariables, // Modules - "mod_punish": s.ModuleStorage.ModPunish.migrate, "mod_overlays": s.ModuleStorage.ModOverlays.migrate, "mod_quotedb": s.ModuleStorage.ModQuoteDB.migrate, } { diff --git a/main.go b/main.go index 48a33c4..8d49d7e 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,8 @@ var ( IRCRateLimit time.Duration `flag:"rate-limit" default:"1500ms" description:"How often to send a message (default: 20/30s=1500ms, if your bot is mod everywhere: 100/30s=300ms, different for known/verified bots)"` 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"` - 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)"` 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"` @@ -207,19 +208,14 @@ func handleSubCommand(args []string) { func main() { var err error - databaseConnectionString := strings.Join([]string{ - cfg.StorageDatabase, - strings.Join([]string{ - "_pragma=locking_mode(EXCLUSIVE)", - "_pragma=synchronous(FULL)", - }, "&"), - }, "?") - - if db, err = database.New("sqlite", databaseConnectionString, cfg.StorageEncryptionPass); err != nil { - log.WithError(err).Fatal("Unable to open storage database") + if db, err = database.New(cfg.StorageConnType, cfg.StorageConnString, cfg.StorageEncryptionPass); err != nil { + log.WithError(err).Fatal("Unable to open storage backend") + } + + if accessService, err = access.New(db); err != nil { + log.WithError(err).Fatal("Unable to apply access migration") } - accessService = access.New(db) if timerService, err = timer.New(db); err != nil { log.WithError(err).Fatal("Unable to apply timer migration") } diff --git a/pkg/database/connector.go b/pkg/database/connector.go index 74da92a..2911e63 100644 --- a/pkg/database/connector.go +++ b/pkg/database/connector.go @@ -1,42 +1,69 @@ package database import ( - "embed" - "regexp" + "database/sql" + "net/url" + "strings" - "github.com/jmoiron/sqlx" "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 ( connector struct { - db *sqlx.DB + db *gorm.DB encryptionSecret string } ) -var ( - // ErrCoreMetaNotFound is the error thrown when reading a non-existent - // core_kv key - ErrCoreMetaNotFound = errors.New("core meta entry not found") - - //go:embed schema/** - schema embed.FS - - migrationFilename = regexp.MustCompile(`^([0-9]+)\.sql$`) -) +// ErrCoreMetaNotFound is the error thrown when reading a non-existent +// core_kv key +var ErrCoreMetaNotFound = errors.New("core meta entry not found") // New creates a new Connector with the given driver and database -func New(driverName, dataSourceName, encryptionSecret string) (Connector, error) { - db, err := sqlx.Connect(driverName, dataSourceName) +func New(driverName, connString, encryptionSecret string) (Connector, error) { + 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 { return nil, errors.Wrap(err, "connecting database") } - db.SetConnMaxIdleTime(0) - db.SetConnMaxLifetime(0) - db.SetMaxIdleConns(1) - db.SetMaxOpenConns(1) + if dbTuner != nil { + if err = dbTuner(db.DB()); err != nil { + return nil, errors.Wrap(err, "tuning database") + } + } conn := &connector{ db: db, @@ -46,22 +73,53 @@ func New(driverName, dataSourceName, encryptionSecret string) (Connector, 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 } func (c connector) applyCoreSchema() error { - coreSQL, err := schema.ReadFile("schema/core.sql") - if err != nil { - return errors.Wrap(err, "reading core.sql content") - } - - if _, err = c.db.Exec(string(coreSQL)); err != nil { - return errors.Wrap(err, "applying core schema") - } - - return errors.Wrap(c.Migrate("core", NewEmbedFSMigrator(schema, "schema")), "applying core migration") + 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 { + return connString, errors.Wrap(err, "parsing connString") + } + + q := u.Query() + + q.Add("_pragma", "locking_mode(EXCLUSIVE)") + q.Add("_pragma", "synchronous(FULL)") + + u.RawQuery = strings.NewReplacer( + "%28", "(", + "%29", ")", + ).Replace(q.Encode()) + + return u.String(), nil +} + +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 } diff --git a/pkg/database/connector_test.go b/pkg/database/connector_test.go index eef6217..7c29844 100644 --- a/pkg/database/connector_test.go +++ b/pkg/database/connector_test.go @@ -1,98 +1,44 @@ package database import ( + "path" "testing" - "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const testEncryptionPass = "password123" func TestNewConnector(t *testing.T) { - dbc, err := New("sqlite", ":memory:", testEncryptionPass) - if err != nil { - t.Fatalf("creating database connector: %s", err) - } - defer dbc.Close() - - row := dbc.DB().QueryRow("SELECT count(1) AS tables FROM sqlite_master WHERE type='table' AND name='core_kv';") - - var count int - if err = row.Scan(&count); err != nil { - t.Fatalf("reading table count result") + cStrings := map[string]string{ + "filesystem": path.Join(t.TempDir(), "storage.db"), + "memory": "file::memory:?cache=shared", } - if count != 1 { - t.Errorf("expected to find one result, got %d in count of core_kv table", count) + 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 + assert.NoError(t, row.Scan(&count).Error, "reading table count result") + + assert.Equal(t, 1, count) + }) } } -func TestCoreMetaRoundtrip(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" - ) - - 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) +func TestPatchSQLiteConnString(t *testing.T) { + for in, out := range map[string]string{ + "storage.db": "storage.db?_pragma=locking_mode(EXCLUSIVE)&_pragma=synchronous(FULL)", + "file::memory:?cache=shared": "file::memory:?_pragma=locking_mode(EXCLUSIVE)&_pragma=synchronous(FULL)&cache=shared", + } { + cs, err := patchSQLiteConnString(in) + require.NoError(t, err, "patching conn string %q", in) + assert.Equal(t, out, cs, "patching conn string %q", in) } } diff --git a/pkg/database/coreKV.go b/pkg/database/coreKV.go index 5c46147..e3b90ab 100644 --- a/pkg/database/coreKV.go +++ b/pkg/database/coreKV.go @@ -2,11 +2,19 @@ package database import ( "bytes" - "database/sql" "encoding/json" "strings" "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 @@ -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) { - var data struct{ Key, Value string } - data.Key = key + var data coreKV - if err = c.db.Get(&data, "SELECT * FROM core_kv WHERE key = $1", data.Key); err != nil { - if errors.Is(err, sql.ErrNoRows) { + if err = c.db.First(&data, "name = ?", key).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { return ErrCoreMetaNotFound } 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( - "INSERT INTO core_kv (key, value) VALUES (:key, :value) ON CONFLICT DO UPDATE SET value=excluded.value;", - map[string]any{ - "key": key, - "value": encValue, - }, + data := coreKV{Name: key, Value: encValue} + return errors.Wrap( + c.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{"value"}), + }).Create(data).Error, + "upserting core meta value", ) - - return errors.Wrap(err, "upserting core meta value") } diff --git a/pkg/database/coreKV_test.go b/pkg/database/coreKV_test.go new file mode 100644 index 0000000..2ca067c --- /dev/null +++ b/pkg/database/coreKV_test.go @@ -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") +} diff --git a/pkg/database/database.go b/pkg/database/database.go index 6459ea7..d561ec3 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -3,12 +3,7 @@ package database import ( - "io/fs" - - "github.com/jmoiron/sqlx" - - // Included support for pure-go sqlite - _ "github.com/glebarez/go-sqlite" + "gorm.io/gorm" ) type ( @@ -16,8 +11,7 @@ type ( // convenience methods Connector interface { Close() error - DB() *sqlx.DB - Migrate(module string, migrations MigrationStorage) error + DB() *gorm.DB ReadCoreMeta(key string, value any) error StoreCoreMeta(key string, value any) error ReadEncryptedCoreMeta(key string, value any) error @@ -25,26 +19,4 @@ type ( DecryptField(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) - } ) diff --git a/pkg/database/logger.go b/pkg/database/logger.go new file mode 100644 index 0000000..f34ea14 --- /dev/null +++ b/pkg/database/logger.go @@ -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...) +} diff --git a/pkg/database/migration.go b/pkg/database/migration.go deleted file mode 100644 index 512ee47..0000000 --- a/pkg/database/migration.go +++ /dev/null @@ -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 -} diff --git a/pkg/database/migration_embedfs.go b/pkg/database/migration_embedfs.go deleted file mode 100644 index eff19ab..0000000 --- a/pkg/database/migration_embedfs.go +++ /dev/null @@ -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) -} diff --git a/pkg/database/migration_test.go b/pkg/database/migration_test.go deleted file mode 100644 index e1584a7..0000000 --- a/pkg/database/migration_test.go +++ /dev/null @@ -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) - } -} diff --git a/pkg/database/schema/001.sql b/pkg/database/schema/001.sql deleted file mode 100644 index 55d5213..0000000 --- a/pkg/database/schema/001.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE extended_permissions ( - channel STRING NOT NULL PRIMARY KEY, - access_token STRING, - refresh_token STRING, - scopes STRING -); diff --git a/pkg/database/schema/core.sql b/pkg/database/schema/core.sql deleted file mode 100644 index 88f1b60..0000000 --- a/pkg/database/schema/core.sql +++ /dev/null @@ -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 -); diff --git a/pkg/database/testdata/migration1/001.sql b/pkg/database/testdata/migration1/001.sql deleted file mode 100644 index ec3f960..0000000 --- a/pkg/database/testdata/migration1/001.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE testdata ( - key STRING NOT NULL PRIMARY KEY, - value STRING -); diff --git a/pkg/database/testdata/migration2/001.sql b/pkg/database/testdata/migration2/001.sql deleted file mode 100644 index ec3f960..0000000 --- a/pkg/database/testdata/migration2/001.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE testdata ( - key STRING NOT NULL PRIMARY KEY, - value STRING -); diff --git a/pkg/database/testdata/migration2/002.sql b/pkg/database/testdata/migration2/002.sql deleted file mode 100644 index c5ba077..0000000 --- a/pkg/database/testdata/migration2/002.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE testdata ADD COLUMN another_value STRING; diff --git a/pkg/database/testhelper.go b/pkg/database/testhelper.go new file mode 100644 index 0000000..c2bc198 --- /dev/null +++ b/pkg/database/testhelper.go @@ -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 +} diff --git a/plugins_core.go b/plugins_core.go index d538a6e..6ae13d5 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -73,9 +73,9 @@ var ( func initCorePlugins() error { args := getRegistrationArguments() - for _, rf := range corePluginRegistrations { + for idx, rf := range corePluginRegistrations { if err := rf(args); err != nil { - return errors.Wrap(err, "registering core plugin") + return errors.Wrapf(err, "registering core plugin %d", idx) } } return nil